diff --git a/.appveyor.yml b/.appveyor.yml index 911a9d317..2d1f70b18 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,56 +1,80 @@ # AppVeyor is a CI service to build and run tests under Windows # https://ci.appveyor.com/project/districtdatalabs/yellowbrick +image: + - Previous Visual Studio 2017 + +version: 0.9.{build} +pull_requests: + do_not_increment_build_number: true + +branches: + only: + - master + - develop + environment: matrix: # Tests failing on 32 bit architectures # https://github.com/numpy/numpy/issues/4384 - # - PYTHON: "C:\\Python27" - # PYTHON_VERSION: "2.7.14" - # PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.14" - PYTHON_ARCH: "64" # - PYTHON: "C:\\Python36" # PYTHON_VERSION: "3.6.4" # PYTHON_ARCH: "32" - - PYTHON: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.4" - PYTHON_ARCH: "64" - # - PYTHON: "C:\\Miniconda3" # PYTHON_VERSION: "3.6.4" # MINICONDA_VERSION: "4.4.10" # PYTHON_ARCH: "32" - - PYTHON: "C:\\Miniconda3-x64" - PYTHON_VERSION: "3.6.4" - MINICONDA_VERSION: "4.4.10" + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6" + PYTHON_ARCH: "64" + + - PYTHON: "C:\\Python37-x64" + PYTHON_VERSION: "3.7" PYTHON_ARCH: "64" + - PYTHON: "C:\\Miniconda36-x64" + PYTHON_VERSION: "3.6" + MINICONDA_VERSION: "4.5.4" + PYTHON_ARCH: "64" + + # Failing Tests Due to TypeError: LoadLibrary() + # - PYTHON: "C:\\Miniconda37-x64" + # PYTHON_VERSION: "3.7" + # MINICONDA_VERSION: "4.5.12" + # PYTHON_ARCH: "64" + # Cancel pending jobs after first job failure matrix: fast_finish: true install: - - "%PYTHON%\\python.exe -m pip install wheel" - - "%PYTHON%\\python.exe -m pip install -r requirements.txt" - - "%PYTHON%\\python.exe -m pip install -r tests/requirements.txt" - - "%PYTHON%\\python.exe -m nltk.downloader popular" + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - cmd: "IF '%MINICONDA_VERSION%'=='' ( + python -m pip install -U pip && + python -m pip install -U wheel && + python -m pip install -U -r requirements.txt && + python -m pip install -U -r tests/requirements.txt + ) ELSE ( + conda update -n base conda --yes && + conda config --add channels conda-forge && + conda env create -f tests/requirements.txt -n yellowbrick && + call activate yellowbrick + )" + - "python -m nltk.downloader popular" # No requirement to build any C libraries build: off test_script: - - "%PYTHON%\\python.exe setup.py test" + - "python setup.py test" after_test: - - "%PYTHON%\\python.exe setup.py bdist_wheel" + - "python setup.py bdist_wheel" artifacts: - path: dist\* @@ -60,12 +84,10 @@ cache: - '%APPDATA%\pip\Cache' notifications: - - provider: Email - to: - - bbengfort@districtdatalabs.com - - rbilbro@districtdatalabs.com - - nathan.danielsen@gmail.com - - tojeda@districtdatalabs.com - on_build_success: false - on_build_failure: false + - provider: Slack + auth_token: + secure: 6hd0IQ66qUUiStas9s304izgV7Wh0XyY0gFjuua7H8gtYle41gbFozkfKgzs110oK/iT4XKWrNfWOgz3S5Jcp2SGFXQ9IvF5ZqYmX71ZQRY= + channel: '#yb-ci' + on_build_success: true + on_build_failure: true on_build_status_changed: true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..e582a5b82 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: https://numfocus.org/donate diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..b55897af3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,73 @@ + + +This PR fixes #issue_number _(If you are fixing a bug)_ which reported a bug that caused a problem to occur when users... + +_(or if you are introducing a new feature)_ which requested a feature to allow the user to... + +I have made the following changes: + +1. +2. +3. + +### Sample Code and Plot + +_If you are adding or modifying a visualizer, PLEASE include a sample plot here along with the code you used to generate it._ + +### TODOs and questions + + + +Still to do: + +- [ ] +- [ ] +- [ ] + +Questions for the @DistrictDataLabs/team-oz-maintainers: + +- [ ] +- [ ] + +### CHECKLIST + + + +- [ ] _Is the commit message formatted correctly?_ +- [ ] _Have you noted the new functionality/bugfix in the release notes of the next release?_ + + + +- [ ] _Included a sample plot to visually illustrate your changes?_ +- [ ] _Do all of your functions and methods have docstrings?_ +- [ ] _Have you added/updated unit tests where appropriate?_ +- [ ] _Have you updated the baseline images if necessary?_ +- [ ] _Have you run the unit tests using `pytest`?_ +- [ ] _Is your code style correct (are you using PEP8, pyflakes)?_ +- [ ] _Have you documented your new feature/functionality in the docs?_ + + + +- [ ] _Have you built the docs using `make html`?_ diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md deleted file mode 100644 index 9174d7e06..000000000 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ /dev/null @@ -1,17 +0,0 @@ - -Fixes # - - - - -Changes proposed in this pull request: - -- -- -- - - -@DistrictDataLabs/team-oz-maintainers diff --git a/.gitignore b/.gitignore index a8c557457..aa54c80dc 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ docs/_build/ # IDE/editor droppings *.swp *.swo +.vscode/settings.json # OS droppings .DS_Store @@ -120,6 +121,4 @@ fabric.properties # Data downloaded from Yellowbrick data/ -.vscode/settings.json - -yellowbrick/datasets/fixtures +yellowbrick/datasets/fixtures \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 50cee5af8..be339917a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,53 @@ +dist: xenial language: python -python: - - '2.7' - - '3.6' +matrix: + include: + - name: "Python 3.6 on Xenial Linux" + python: '3.6' + + - name: "Python 3.7 on Xenial Linux" + python: '3.7' + + - name: "Miniconda 3.6 on Xenial Linux" + env: ANACONDA="3.6" + + - name: "Miniconda 3.7 on Xenial Linux" + env: ANACONDA="3.7" before_install: - - sudo apt-get update - - sudo apt-get build-dep python-scipy +- sudo apt-get update; +- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then + sudo apt-get build-dep python-scipy; + MINICONDA_OS="Linux"; + fi install: - - pip install -r requirements.txt - - pip install -r tests/requirements.txt - - pip install coveralls - - python -m nltk.downloader popular +- if [[ -z ${ANACONDA} ]]; then + pip install -r requirements.txt; + pip install -r tests/requirements.txt; + pip install coveralls; + else + wget https://repo.anaconda.com/miniconda/Miniconda3-latest-$MINICONDA_OS-x86_64.sh -O miniconda.sh; + bash miniconda.sh -b -p $HOME/miniconda; + export PATH="$HOME/miniconda/bin:$PATH"; + hash -r; + conda config --set always_yes yes --set changeps1 no; + export BASE_PYTHON=`python -c 'import sys; version=sys.version_info[:3]; print("{0}.{1}.{2}".format(*version))'`; + echo "Base Anaconda Python is $BASE_PYTHON"; + echo "Creating Anaconda Python $ANACONDA conda environment"; + conda update -n base conda --yes; + conda config --add channels conda-forge; + conda env create -f tests/requirements.txt -n yellowbrick python=$ANACONDA; + source activate yellowbrick; + conda install coveralls; + fi -script: make test +script: +- python -m nltk.downloader popular +- make test after_success: coveralls notifications: - email: - recipients: - - bbengfort@districtdatalabs.com - - rbilbro@districtdatalabs.com - - nathan.danielsen@gmail.com - - tojeda@districtdatalabs.com - on_success: change - on_failure: always + slack: + secure: mWKVHmEc22FJSp6Rrnd1j4QYCgZY4NJSrA8kZ5wj2/lf1iHI/CfWGTf7+Qihqe+rt0FOU0+UA9SzvSHRD1bV76q/zINayQ0EyJAfQzvIWIRGGnnMSO/79WoEYF56wwjpc5pLUTh6QV5qqfy+8nNGQ1/uJ0h6FtsUaSa/g61a5ZJEVBIjIpH8PgMxM64dRgJCmAdQuXkBP5Uf3yHlCtYk+Jr+gyXU2oqwMZ1VWgZkEo1Tqo7W9WY8dkOaAkzXDT61OqtcyyTuVSYbmK4i3c84681NBpb7wT6BfiCCAd3tn5AIKCkJVJ0ga0XeF6MdDpnicpku4FaN+fQjwkPiU47o/aFp8RNp27JQ9AhvH7wMuu5O8HDhszjRkfGOlUbuPOTavc22o4j0ShsrLiTQRJRhQQzJoquPuPj5wHqCCN+ice7IVUHj3ZC2jpJKDEYUNnr1fATtOwocimc6PhJM/IoeHgEEHpi37b+AxnhgOFoBlgsq2f4nsRD9JsLHqIpJCHgMjKxc6p3FtcFcXZDlDXQIcCzSRiPhG207dahspA3aPLj4Z+tOLJwh7/PSEfp02kcgPMM/MLYTWcaBv14aYi69kvQoZTfqVY8tIohg3ygda5siOCTTgqGriJYzkmdY5/Dp51kabhl+cEVIxPyY0miqyl3hZjqkqCnnOtg06qqxLLM= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 053be200c..f2da93d0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ For more on the development path, goals, and motivations behind Yellowbrick, che Yellowbrick is an open source project that is supported by a community who will gratefully and humbly accept any contributions you might make to the project. Large or small, any contribution makes a big difference; and if you've never contributed to an open source project before, we hope you will start with Yellowbrick! -Principally, Yellowbrick development is about the addition and creation of *visualizers* --- objects that learn from data and create a visual representation of the data or model. Visualizers integrate with scikit-learn estimators, transformers, and pipelines for specific purposes and as a result, can be simple to build and deploy. The most common contribution is therefore a new visualizer for a specific model or model family. We'll discuss in detail how to build visualizers later. +Principally, Yellowbrick development is about the addition and creation of *visualizers* — objects that learn from data and create a visual representation of the data or model. Visualizers integrate with scikit-learn estimators, transformers, and pipelines for specific purposes and as a result, can be simple to build and deploy. The most common contribution is therefore a new visualizer for a specific model or model family. We'll discuss in detail how to build visualizers later. Beyond creating visualizers, there are many ways to contribute: @@ -32,20 +32,19 @@ The typical workflow for a contributor to the codebase is as follows: 1. **Discover** a bug or a feature by using Yellowbrick. 2. **Discuss** with the core contributes by [adding an issue](https://github.com/DistrictDataLabs/yellowbrick/issues). -3. **Assign** yourself the task by pulling a card from our [Waffle Kanban](https://waffle.io/DistrictDataLabs/yellowbrick). -4. **Fork** the repository into your own GitHub account. -5. Create a **Pull Request** first thing to [connect with us](https://github.com/DistrictDataLabs/yellowbrick/pulls) about your task. -6. **Code** the feature, write the documentation, add your contribution. -7. **Review** the code with core contributors who will guide you to a high quality submission. -8. **Merge** your contribution into the Yellowbrick codebase. +3. **Fork** the repository into your own GitHub account. +4. Create a **Pull Request** first thing to [connect with us](https://github.com/DistrictDataLabs/yellowbrick/pulls) about your task. +5. **Code** the feature, write the documentation, add your contribution. +6. **Review** the code with core contributors who will guide you to a high quality submission. +7. **Merge** your contribution into the Yellowbrick codebase. -**Note**: Create a pull request as soon as possible, even before you've started coding. This will allow the core contributors to give you advice about where to add your code or utilities and discuss other style choices and implementation details as you go. Don't wait! +We believe that *contribution is collaboration* and therefore emphasize *communication* throughout the open source process. We rely heavily on GitHub's social coding tools to allow us to do this. For instance, we use GitHub's [milestone](https://help.github.com/en/articles/about-milestones) feature to focus our development efforts for each Yellowbrick semester, so be sure to check out the issues associated with our [current milestone](https://github.com/districtdatalabs/yellowbrick/milestones)! -We believe that *contribution is collaboration* and therefore emphasize *communication* throughout the open source process. We rely heavily on GitHub's social coding tools to allow us to do this. +Once you have a good sense of how you are going to implement the new feature (or fix the bug!), you can reach out for feedback from the maintainers by creating a [pull request](https://github.com/DistrictDataLabs/yellowbrick/pulls). Please note that if we feel your solution has not been thought out in earnest, or if the PR is not aligned with our [current milestone](https://github.com/districtdatalabs/yellowbrick/milestones) goals, we may reach out to ask that you close the PR so that we can prioritize reviewing the most critical feature requests and bug fixes. Ideally, any pull request should be capable of resolution within 6 weeks of being opened. This timeline helps to keep our pull request queue small and allows Yellowbrick to maintain a robust release schedule to give our users the best experience possible. However, the most important thing is to keep the dialogue going! And if you're unsure whether you can complete your idea within 6 weeks, you should still go ahead and open a PR and we will be happy to help you scope it down as needed. -If we have comments or questions when we evaluate your pull request and receive no response, we will also close the PR after this period of time. Please know that this does not mean we don't value your contribution, just that things go stale. If in the future you want to pick it back up, feel free to address our original feedback and to reference the original PR in a new pull request. +If we have comments or questions when we evaluate your pull request and receive no response, we will also close the PR after this period of time. Please know that this does not mean we don't value your contribution, just that things go stale. If in the future you want to pick it back up, feel free to address our original feedback and to reference the original PR in a new pull request. ### Forking the Repository @@ -104,7 +103,7 @@ Once forked, use the following steps to get your development environment set up $ git checkout develop ``` -At this point you're ready to get started writing code. If you're going to take on a specific task, we'd strongly encourage you to check out the issue on [Waffle](https://waffle.io/DistrictDataLabs/yellowbrick) and create a [pull request](https://github.com/DistrictDataLabs/yellowbrick/pulls) **before you start coding** to better foster communication with other contributors. +At this point you're ready to get started writing code! ### Branching Conventions @@ -144,7 +143,7 @@ $ git branch -d feature-myfeature $ git push origin --delete feature-myfeature ``` -Head back to Waffle and checkout another issue! +Head back to Github and checkout another issue! ## Developing Visualizers @@ -159,12 +158,12 @@ There are two basic types of Visualizers: - **Feature Visualizers** are high dimensional data visualizations that are essentially transformers. - **Score Visualizers** wrap a scikit-learn regressor, classifier, or clusterer and visualize the behavior or performance of the model on test data. -These two basic types of visualizers map well to the two basic objects in scikit-learn: +These two basic types of visualizers map well to the two basic estimator objects in scikit-learn: - **Transformers** take input data and return a new data set. -- **Estimators** are fit to training data and can make predictions. +- **Models** are fit to training data and can make predictions. -The scikit-learn API is object oriented, and estimators and transformers are initialized with parameters by instantiating their class. Hyperparameters can also be set using the `set_attrs()` method and retrieved with the corresponding `get_attrs()` method. All scikit-learn estimators have a `fit(X, y=None)` method that accepts a two dimensional data array, `X`, and optionally a vector `y` of target values. The `fit()` method trains the estimator, making it ready to transform data or make predictions. Transformers have an associated `transform(X)` method that returns a new dataset, `Xprime` and models have a `predict(X)` method that returns a vector of predictions, `yhat`. Models also have a `score(X, y)` method that evaluate the performance of the model. +The scikit-learn API is object oriented, and estimators are initialized with parameters by instantiating their class. Hyperparameters can also be set using the `set_attrs()` method and retrieved with the corresponding `get_attrs()` method. All scikit-learn estimators have a `fit(X, y=None)` method that accepts a two dimensional data array, `X`, and optionally a vector `y` of target values. The `fit()` method trains the estimator, making it ready to transform data or make predictions. Transformers have an associated `transform(X)` method that returns a new dataset, `Xprime` and models have a `predict(X)` method that returns a vector of predictions, `yhat`. Models may also have a `score(X, y)` method that evaluate the performance of the model. Visualizers interact with scikit-learn objects by intersecting with them at the methods defined above. Specifically, visualizers perform actions related to `fit()`, `transform()`, `predict()`, and `score()` then call a `draw()` method which initializes the underlying figure associated with the visualizer. The user calls the visualizer's `poof()` method, which in turn calls a `finalize()` method on the visualizer to draw legends, titles, etc. and then `poof()` renders the figure. The Visualizer API is therefore: @@ -185,14 +184,13 @@ class MyVisualizer(Visualizer): super(MyVisualizer, self).__init__(ax, **kwargs) def fit(self, X, y=None): + super(MyVisualizer, self).fit(X, y) self.draw(X) return self def draw(self, X): - if self.ax is None: - self.ax = self.gca() - self.ax.plot(X) + return self.ax def finalize(self): self.set_title("My Visualizer") @@ -212,28 +210,23 @@ Score visualizers work on the same principle but accept an additional required ` The test package mirrors the `yellowbrick` package in structure and also contains several helper methods and base functionality. To add a test to your visualizer, find the corresponding file to add the test case, or create a new test file in the same place you added your code. -Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. Therefore the primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. To assist with this, we have two primary helpers, `VisualTestCase` and `DatasetMixin`. Create your unit test as follows:: +Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. Therefore the primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. To assist with this, we have a helper, `VisualTestCase`. Create your unit test as follows:: ```python import pytest +from yellowbrick.datasets import load_occupancy + from tests.base import VisualTestCase -from tests.dataset import DatasetMixin -class MyVisualizerTests(VisualTestCase, DatasetMixin): +class MyVisualizerTests(VisualTestCase): def test_my_visualizer(self): """ Test MyVisualizer on a real dataset """ - # Load the data from the fixture - dataset = self.load_data('occupancy') - - # Get the data - X = dataset[[ - "temperature", "relative_humidity", "light", "C02", "humidity" - ]] - y = dataset['occupancy'].astype(int) + # Load the data + X,y = load_occupancy() try: visualizer = MyVisualizer() @@ -255,7 +248,7 @@ You can also run your own test file as follows:: $ pytest tests/test_your_visualizer.py ``` -The Makefile uses the pytest runner and testing suite as well as the coverage library, so make sure you have those dependencies installed! The `DatasetMixin` also requires [requests.py](http://docs.python-requests.org/en/master/) to fetch data from our Amazon S3 account. +The Makefile uses the pytest runner and testing suite as well as the coverage library, so make sure you have those dependencies installed! **Note**: Advanced developers can use our _image comparison tests_ to assert that an image generated matches a baseline image. Read more about this in our [testing documentation](http://www.scikit-yb.org/en/latest/contributing.html#testing) @@ -303,8 +296,4 @@ class MyVisualizer(Visualizer): """ ``` -This is a very good start to producing a high quality visualizer, but unless it is part of the documentation on our website, it will not be visible. For details on including documentation in the `docs` directory see the [Contributing Documentation](http://www.scikit-yb.org/en/latest/contributing.html#documentation) section in the larger contributing guide. - -## Throughput - -[![Throughput Graph](https://graphs.waffle.io/DistrictDataLabs/yellowbrick/throughput.svg)](https://waffle.io/DistrictDataLabs/yellowbrick/metrics/throughput) +This is a very good start to producing a high quality visualizer, but unless it is part of the documentation on our website, it will not be visible. For details on including documentation in the `docs` directory see the [Contributing Documentation](http://www.scikit-yb.org/en/latest/contributing.html#documentation) section in the larger contributing guide. \ No newline at end of file diff --git a/DESCRIPTION.md b/DESCRIPTION.md new file mode 100644 index 000000000..7a862aea7 --- /dev/null +++ b/DESCRIPTION.md @@ -0,0 +1,70 @@ +# Yellowbrick + +[![Visualizers](docs/images/readme/banner.png)](https://www.scikit-yb.org/) + +Yellowbrick is a suite of visual analysis and diagnostic tools designed to facilitate machine learning with scikit-learn. The library implements a new core API object, the `Visualizer` that is an scikit-learn estimator — an object that learns from data. Similar to transformers or models, visualizers learn from data by creating a visual representation of the model selection workflow. + +Visualizer allow users to steer the model selection process, building intuition around feature engineering, algorithm selection and hyperparameter tuning. For instance, they can help diagnose common problems surrounding model complexity and bias, heteroscedasticity, underfit and overtraining, or class balance issues. By applying visualizers to the model selection workflow, Yellowbrick allows you to steer predictive models toward more successful results, faster. + +The full documentation can be found at [scikit-yb.org](https://scikit-yb.org/) and includes a [Quick Start Guide](https://www.scikit-yb.org/en/latest/quickstart.html) for new users. + +## Visualizers + +Visualizers are estimators — objects that learn from data — whose primary objective is to create visualizations that allow insight into the model selection process. In scikit-learn terms, they can be similar to transformers when visualizing the data space or wrap a model estimator similar to how the `ModelCV` (e.g. [`RidgeCV`](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.RidgeCV.html), [`LassoCV`](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LassoCV.html)) methods work. The primary goal of Yellowbrick is to create a sensical API similar to scikit-learn. Some of our most popular visualizers include: + +### Classification Visualization + +- **Classification Report**: a visual classification report that displays a model's precision, recall, and F1 per-class scores as a heatmap +- **Confusion Matrix**: a heatmap view of the confusion matrix of pairs of classes in multi-class classification +- **Discrimination Threshold**: a visualization of the precision, recall, F1-score, and queue rate with respect to the discrimination threshold of a binary classifier +- **Precision-Recall Curve**: plot the precision vs recall scores for different probability thresholds +- **ROCAUC**: graph the receiver operator characteristic (ROC) and area under the curve (AUC) + +### Clustering Visualization + +- **Intercluster Distance Maps**: visualize the relative distance and size of clusters +- **KElbow Visualizer**: visualize cluster according to the specified scoring function, looking for the "elbow" in the curve. +- **Silhouette Visualizer**: select `k` by visualizing the silhouette coefficient scores of each cluster in a single model + +### Feature Visualization + +- **Manifold Visualization**: high-dimensional visualization with manifold learning +- **Parallel Coordinates**: horizontal visualization of instances +- **PCA Projection**: projection of instances based on principal components +- **RadViz Visualizer**: separation of instances around a circular plot +- **Rank Features**: single or pairwise ranking of features to detect relationships + +### Model Selection Visualization + +- **Cross Validation Scores**: display the cross-validated scores as a bar chart with the average score plotted as a horizontal line +- **Feature Importances**: rank features based on their in-model performance +- **Learning Curve**: show if a model might benefit from more data or less complexity +- **Recursive Feature Elimination**: find the best subset of features based on importance +- **Validation Curve**: tune a model with respect to a single hyperparameter + +### Regression Visualization + +- **Alpha Selection**: show how the choice of alpha influences regularization +- **Cook's Distance**: show the influence of instances on linear regression +- **Prediction Error Plots**: find model breakdowns along the domain of the target +- **Residuals Plot**: show the difference in residuals of training and test data + +### Target Visualization + +- **Balanced Binning Reference**: generate a histogram with vertical lines showing the recommended value point to the bin data into evenly distributed bins +- **Class Balance**: show the relationship of the support for each class in both the training and test data by displaying how frequently each class occurs as a bar graph the frequency of the classes' representation in the dataset +- **Feature Correlation**: visualize the correlation between the dependent variables and the target + +### Text Visualization + +- **Dispersion Plot**: visualize how key terms are dispersed throughout a corpus +- **PosTag Visualizer**: plot the counts of different parts-of-speech throughout a tagged corpus +- **Token Frequency Distribution**: visualize the frequency distribution of terms in the corpus +- **t-SNE Corpus Visualization**: uses stochastic neighbor embedding to project documents +- **UMAP Corpus Visualization**: plot similar documents closer together to discover clusters + +... and more! Yellowbrick is adding new visualizers all the time so be sure to check out our [examples gallary](https://www.scikit-yb.org/en/latest/api/index.html) — or even the [develop](https://github.com/districtdatalabs/yellowbrick/tree/develop) branch — and feel free to contribute your ideas for new Visualizers! + +## Affiliations +[![District Data Labs](docs/images/readme/affiliates_ddl.png)](https://www.districtdatalabs.com/) [![NumFOCUS Affiliated Project](docs/images/readme/affiliates_numfocus.png)](https://numfocus.org/) + diff --git a/DESCRIPTION.rst b/DESCRIPTION.rst deleted file mode 100644 index 1b42fad27..000000000 --- a/DESCRIPTION.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. -*- mode: rst -*- - -|Visualizers|_ - -.. |Visualizers| image:: http://www.scikit-yb.org/en/latest/_images/visualizers.png - :width: 800 px -.. _Visualizers: http://www.scikit-yb.org/ - -Yellowbrick -=========== - -Yellowbrick is a suite of visual analysis and diagnostic tools designed to facilitate machine learning with Scikit-Learn. The library implements a new core API object, the "Visualizer" that is an Scikit-Learn estimator: an object that learns from data. Like transformers or models, visualizers learn from data by creating a visual representation of the model selection workflow. - -Visualizers allow users to steer the model selection process, building intuition around feature engineering, algorithm selection, and hyperparameter tuning. For example, visualizers can help diagnose common problems surrounding model complexity and bias, heteroscedasticity, underfit and overtraining, or class balance issues. By applying visualizers to the model selection workflow, Yellowbrick allows you to steer predictive models to more successful results, faster. - -Please see the full documentation at: http://scikit-yb.org/ particularly the `quick start guide `_ - -Visualizers ------------ - -Visualizers are estimators (objects that learn from data) whose primary objective is to create visualizations that allow insight into the model selection process. In Scikit-Learn terms, they can be similar to transformers when visualizing the data space or wrap an model estimator similar to how the “ModelCV” (e.g. RidgeCV_, LassoCV_) methods work. The primary goal of Yellowbrick is to create a sensical API similar to Scikit-Learn. Some of our most popular visualizers include: - -.. _RidgeCV: http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.RidgeCV.html -.. _LassoCV: http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LassoCV.html - -Feature Visualization -~~~~~~~~~~~~~~~~~~~~~ - -- **Rank Features**: single or pairwise ranking of features to detect relationships -- **Parallel Coordinates**: horizontal visualization of instances -- **Radial Visualization**: separation of instances around a circular plot -- **PCA Projection**: projection of instances based on principal components -- **Manifold Visualization**: high dimensional visualization with manifold learning -- **Feature Importances**: rank features based on their in-model performance -- **Recursive Feature Elimination**: find the best subset of features by importance -- **Joint Plots**: direct data visualization with feature selection - -Classification Visualization -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- **Class Balance**: see how the distribution of classes affects the model -- **Class Prediction Error**: shows error and support in classification -- **Classification Report**: visual representation of precision, recall, and F1 -- **ROC/AUC Curves**: receiver operator characteristics and area under the curve -- **Precision-Recall Curves**: precision vs recall for different probability thresholds -- **Confusion Matrices**: visual description of class decision making -- **Discrimination Threshold**: find a threshold that best separates binary classes - -Regression Visualization -~~~~~~~~~~~~~~~~~~~~~~~~ - -- **Prediction Error Plots**: find model breakdowns along the domain of the target -- **Residuals Plot**: show the difference in residuals of training and test data -- **Alpha Selection**: show how the choice of alpha influences regularization - -Clustering Visualization -~~~~~~~~~~~~~~~~~~~~~~~~ - -- **K-Elbow Plot**: select k using the elbow method and various metrics -- **Silhouette Plot**: select k by visualizing silhouette coefficient values -- **Intercluster Distance Maps**: show relative distance and size of clusters - -Model Selection Visualization -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- **Validation Curve**: tune a model with respect to a single hyperparameter -- **Learning Curve**: show if a model might benefit from more data or less complexity - -Text Visualization -~~~~~~~~~~~~~~~~~~ - -- **Term Frequency**: visualize the frequency distribution of terms in the corpus -- **t-SNE Corpus Visualization**: use stochastic neighbor embedding to project documents -- **Dispersion Plot**: visualize how key terms are dispersed throughout a corpus - -Target Visualization -~~~~~~~~~~~~~~~~~~~~ - -- **Feature Correlation**: visualize the correlation between the dependent variables and the target - -... and more! Visualizers are being added all the time; be sure to check the examples_ (or even the develop_ branch) and feel free to contribute your ideas for new Visualizers! - -.. _examples: http://www.scikit-yb.org/en/latest/api/index.html -.. _develop: https://github.com/districtdatalabs/yellowbrick/tree/develop diff --git a/MANIFEST.in b/MANIFEST.in index 1dbdb8797..e3ac99581 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,11 +4,24 @@ include *.txt include *.yml include *.cfg include Makefile -recursive-include docs *.rst -recursive-include docs *.jpg -recursive-include docs *.png -recursive-include docs *.py -recursive-include docs Makefile -recursive-include tests *.py -recursive-include examples *.py -recursive-include examples *.ipynb +include MANIFEST.in + +include examples/*.ipynb +include examples/*.md + +graft docs +prune docs/_build + +graft tests +prune tests/fixtures +prune tests/actual_images + +graft yellowbrick +prune yellowbrick/datasets/fixtures + +global-exclude __pycache__ +global-exclude *.py[co] +global-exclude .ipynb_checkpoints +global-exclude .DS_Store +global-exclude .env +global-exclude .coverage.* \ No newline at end of file diff --git a/README.md b/README.md index 6c43e72ce..2cf88fdbe 100644 --- a/README.md +++ b/README.md @@ -5,83 +5,26 @@ [![Coverage Status](https://coveralls.io/repos/github/DistrictDataLabs/yellowbrick/badge.svg?branch=master)](https://coveralls.io/github/DistrictDataLabs/yellowbrick?branch=master) [![Total Alerts](https://img.shields.io/lgtm/alerts/g/DistrictDataLabs/yellowbrick.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/DistrictDataLabs/yellowbrick/alerts/) [![Language Grade: Python](https://img.shields.io/lgtm/grade/python/g/DistrictDataLabs/yellowbrick.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/DistrictDataLabs/yellowbrick/context:python) - [![PyPI version](https://badge.fury.io/py/yellowbrick.svg)](https://badge.fury.io/py/yellowbrick) [![Documentation Status](https://readthedocs.org/projects/yellowbrick/badge/?version=latest)](http://yellowbrick.readthedocs.io/en/latest/?badge=latest) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1206239.svg)](https://doi.org/10.5281/zenodo.1206239) +[![JOSS](http://joss.theoj.org/papers/10.21105/joss.01075/status.svg)](https://doi.org/10.21105/joss.01075) [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/DistrictDataLabs/yellowbrick/develop?filepath=examples%2Fexamples.ipynb) **Visual analysis and diagnostic tools to facilitate machine learning model selection.** -![Follow the yellow brick road](docs/images/yellowbrickroad.jpg) -Image by [Quatro Cinco](https://flic.kr/p/2Yj9mj), used with permission, Flickr Creative Commons. - -This README is a guide for developers, if you're new to Yellowbrick, get started at our [documentation](http://www.scikit-yb.org/). +[![Banner](docs/images/readme/banner.png)](https://www.scikit-yb.org/en/latest/gallery.html) ## What is Yellowbrick? -Yellowbrick is a suite of visual diagnostic tools called "Visualizers" that extend the scikit-learn API to allow human steering of the model selection process. In a nutshell, Yellowbrick combines scikit-learn with matplotlib in the best tradition of the scikit-learn documentation, but to produce visualizations for _your_ models! - -![Visualizers](docs/images/visualizers.png) - -### Visualizers - -Visualizers are estimators (objects that learn from data) whose primary objective is to create visualizations that allow insight into the model selection process. In scikit-learn terms, they can be similar to transformers when visualizing the data space or wrapping a model estimator similar to how the "ModelCV" (e.g. RidgeCV, LassoCV) methods work. The primary goal of Yellowbrick is to create a sensical API similar to scikit-learn. Some of our most popular visualizers include: - -#### Feature Visualization - -- **Rank Features**: single or pairwise ranking of features to detect relationships -- **Parallel Coordinates**: horizontal visualization of instances -- **Radial Visualization**: separation of instances around a circular plot -- **PCA Projection**: projection of instances based on principal components -- **Manifold Visualization**: high dimensional visualization with manifold learning -- **Feature Importances**: rank features based on their in-model performance -- **Recursive Feature Elimination**: find the best subset of features by importance -- **Joint Plots**: direct data visualization with feature selection - -#### Classification Visualization - -- **Class Balance**: see how the distribution of classes affects the model -- **Class Prediction Error**: shows error and support in classification -- **Classification Report**: visual representation of precision, recall, and F1 -- **ROC/AUC Curves**: receiver operator characteristics and area under the curve -- **Precision-Recall Curves**: precision vs recall for different probability thresholds -- **Confusion Matrices**: visual description of class decision making -- **Discrimination Threshold**: find a threshold that best separates binary classes - -#### Regression Visualization - -- **Prediction Error Plots**: find model breakdowns along the domain of the target -- **Residuals Plot**: show the difference in residuals of training and test data -- **Alpha Selection**: show how the choice of alpha influences regularization - -#### Clustering Visualization - -- **K-Elbow Plot**: select k using the elbow method and various metrics -- **Silhouette Plot**: select k by visualizing silhouette coefficient values -- **Intercluster Distance Maps**: show relative distance and size of clusters - -#### Model Selection Visualization - -- **Validation Curve**: tune a model with respect to a single hyperparameter -- **Learning Curve**: show if a model might benefit from more data or less complexity +Yellowbrick is a suite of visual diagnostic tools called "Visualizers" that extend the scikit-learn API to allow human steering of the model selection process. In a nutshell, Yellowbrick combines scikit-learn with matplotlib in the best tradition of the scikit-learn documentation, but to produce visualizations for _your_ machine learning workflow! -#### Text Visualization - -- **Term Frequency**: visualize the frequency distribution of terms in the corpus -- **t-SNE Corpus Visualization**: use stochastic neighbor embedding to project documents. -- **Dispersion Plot**: visualize how key terms are dispersed throughout a corpus - -#### Target Visualization - -- **Feature Correlation**: visualize the correlation between the dependent variables and the target - -And more! Visualizers are being added all the time, so be sure to check the examples (or even the develop branch) and feel free to contribute your ideas for Visualizers! +For complete documentation on the Yellowbrick API, a gallery of available visualizers, the contributor's guide, tutorials and teaching resources, frequently asked questions, and more, please visit our documentation at [www.scikit-yb.org](https://www.scikit-yb.org/). ## Installing Yellowbrick -Yellowbrick is compatible with Python 2.7 or later but it is preferred to use Python 3.5 or later to take full advantage of all functionality. Yellowbrick also depends on scikit-learn 0.18 or later and matplotlib 1.5 or later. The simplest way to install Yellowbrick is from PyPI with pip, Python's preferred package installer. +Yellowbrick is compatible with Python 3.4 or later and also depends on scikit-learn and matplotlib. The simplest way to install Yellowbrick and its dependencies is from PyPI with pip, Python's preferred package installer. $ pip install yellowbrick @@ -95,8 +38,6 @@ If you're using Anaconda (recommended for Windows users), you can take advantage conda install -c districtdatalabs yellowbrick -Note, however, that there is a [known bug](https://github.com/DistrictDataLabs/yellowbrick/issues/205) installing Yellowbrick on Linux with Anaconda. - ## Using Yellowbrick The Yellowbrick API is specifically designed to play nicely with scikit-learn. Here is an example of a typical workflow sequence with scikit-learn and Yellowbrick: @@ -108,10 +49,12 @@ In this example, we see how Rank2D performs pairwise comparisons of each feature ```python from yellowbrick.features import Rank2D -visualizer = Rank2D(features=features, algorithm='covariance') +visualizer = Rank2D( + features=features, algorithm='covariance' +) visualizer.fit(X, y) # Fit the data to the visualizer visualizer.transform(X) # Transform the data -visualizer.poof() # Draw/show/poof the data +visualizer.poof() # Show the data ``` ### Model Visualization @@ -129,23 +72,19 @@ visualizer.score(X,y) visualizer.poof() ``` -For additional information on getting started with Yellowbrick, check out our [examples notebook](https://github.com/DistrictDataLabs/yellowbrick/blob/develop/examples/examples.ipynb). - -We also have a [quick start guide](https://github.com/DistrictDataLabs/yellowbrick/blob/master/docs/quickstart.rst). +For additional information on getting started with Yellowbrick, view the quickstart guide in the [documentation](https://www.scikit-yb.org/en/latest/) and check out our [examples notebook](https://github.com/DistrictDataLabs/yellowbrick/blob/develop/examples/examples.ipynb). ## Contributing to Yellowbrick Yellowbrick is an open source project that is supported by a community who will gratefully and humbly accept any contributions you might make to the project. Large or small, any contribution makes a big difference; and if you've never contributed to an open source project before, we hope you will start with Yellowbrick! -Principally, Yellowbrick development is about the addition and creation of *visualizers* -- objects that learn from data and create a visual representation of the data or model. Visualizers integrate with scikit-learn estimators, transformers, and pipelines for specific purposes and as a result can be simple to build and deploy. The most common contribution is therefore a new visualizer for a specific model or model family. We'll discuss in detail how to build visualizers later. - -Beyond creating visualizers, there are many ways to contribute: +If you are interested in contributing, check out our [contributor's guide](https://www.scikit-yb.org/en/latest/contributing.html). Beyond creating visualizers, there are many ways to contribute: - Submit a bug report or feature request on [GitHub Issues](https://github.com/DistrictDataLabs/yellowbrick/issues). - Contribute a Jupyter notebook to our examples[ gallery](https://github.com/DistrictDataLabs/yellowbrick/tree/develop/examples). -- Assist us with [user testing](http://www.scikit-yb.org/en/latest/evaluation.html). -- Add to the documentation or help with our website, [scikit-yb.org](http://www.scikit-yb.org). -- Write unit or integration tests for our project. +- Assist us with [user testing](https://www.scikit-yb.org/en/latest/evaluation.html). +- Add to the documentation or help with our website, [scikit-yb.org](https://www.scikit-yb.org). +- [Write unit or integration tests](https://www.scikit-yb.org/en/latest/contributing.html#testing) for our project. - Answer questions on our issues, mailing list, Stack Overflow, and elsewhere. - Translate our documentation into another language. - Write a blog post, tweet, or share our project with others. @@ -153,30 +92,28 @@ Beyond creating visualizers, there are many ways to contribute: As you can see, there are lots of ways to get involved and we would be very happy for you to join us! The only thing we ask is that you abide by the principles of openness, respect, and consideration of others as described in the [Python Software Foundation Code of Conduct](https://www.python.org/psf/codeofconduct/). -For more information, checkout the `CONTRIBUTING.md` file in the root of the repository or the detailed documentation at [Contributing to Yellowbrick](http://www.scikit-yb.org/en/latest/contributing.html) +For more information, checkout the `CONTRIBUTING.md` file in the root of the repository or the detailed documentation at [Contributing to Yellowbrick](https://www.scikit-yb.org/en/latest/contributing.html) -## Development Scripts +## Yellowbrick Datasets -Yellowbrick contains scripts to help with development, including downloading fixture data for tests and managing images for comparison. +Yellowbrick gives easy access to several datasets that are used for the examples in the documentation and testing. These datasets are hosted in our CDN and must be downloaded for use. Typically, when a user calls one of the data loader functions, e.g. `load_bikeshare()` the data is automatically downloaded if it's not already on the user's computer. However, for development and testing, or if you know you will be working without internet access, it might be easier to simply download all the data at once. -### Images +The data downloader script can be run as follows: -The image comparison helper script manages the test directory's `baseline_images` folder by copying files from the `actual_images` folder to setup baselines. To use this script, first run the tests (which will cause image not found errors) then copy the images into baseline as follows: + $ python -m yellowbrick.download -``` -$ python -m tests.images tests/test_visualizer.py -``` +This will download the data to the fixtures directory inside of the Yellowbrick site packages. You can specify the location of the download either as an argument to the downloader script (use `--help` for more details) or by setting the `$YELLOWBRICK_DATA` environment variable. This is the preferred mechanism because this will also influence how data is loaded in Yellowbrick. -Where `tests/test_visualizer.py` is the test file that contains the image comparison tests. All related tests will be discovered, validated, and copied to the baseline directory. To clear out images from both actual and baseline to reset tests, use the `-C` flag: +_Note: Developers who have downloaded data from Yellowbrick versions earlier than v1.0 may experience some problems with the older data format. If this occurs, you can clear out your data cache as follows:_ -``` -$ python -m tests.images -C tests/test_visualizer.py -``` + $ python -m yellowbrick.download --cleanup -Glob syntax can be used to move multiple files. For example to reset all the classifier tests: +_This will remove old datasets and download the new ones. You can also use the `--no-download` flag to simply clear the cache without re-downloading data. Users who are having difficulty with datasets can also use this or they can uninstall and reinstall Yellowbrick using `pip`._ -``` -$ python -m tests.images tests/test_classifier/* -``` +## Citing Yellowbrick + +We would be glad if you used Yellowbrick in your scientific publications! If you do, please cite us using the [citation guidelines](https://www.scikit-yb.org/en/latest/about.html#citing-yellowbrick). + +## Affiliations -Though it is recommended that specific test cases are targeted, rather than updating entire directories. +[![District Data Labs](docs/images/readme/affiliates_ddl.png)](https://districtdatalabs.com/) [![NumFOCUS Affiliated Project](docs/images/readme/affiliates_numfocus.png)](https://numfocus.org) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..7de9bbe02 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,87 @@ +# Yellowbrick Documentation + +*Welcome to the Yellowbrick docs!* + +If you're looking for information about how to use Yellowbrick, for our contributor's guide, for examples and teaching resources, for answers to frequently asked questions, and more, please visit the latest version of our documentation at [www.scikit-yb.org](https://www.scikit-yb.org/). + +## Building the Docs + +To build the documents locally, first install the documentation-specific requirements with `pip` using the `requirements.txt` file in the `docs` directory: + +```bash +$ pip install -r docs/requirements.txt +``` + +You will then be able to build the documentation from inside the `docs` directory by running `make html`; the documentation will be built and rendered in the `_build/html` directory. You can view it by opening `_build/html/index.html` then navigating to your documentation in the browser. + +## reStructuredText + +Yellowbrick uses [Sphinx](http://www.sphinx-doc.org/en/master/index.html) to build our documentation. The advantages of using Sphinx are many; we can more directly link to the documentation and source code of other projects like Matplotlib and scikit-learn using [intersphinx](http://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html). In addition, docstrings used to describe Yellowbrick visualizers can be automatically included when the documentation is built via [autodoc](http://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#sphinx.ext.autodoc). + +To take advantage of these features, our documentation must be written in reStructuredText (or "rst"). reStructuredText is similar to markdown, but not identical, and does take some getting used to. For instance, styling for things like codeblocks, external hyperlinks, internal cross references, notes, and fixed-width text are all unique in rst. + +If you would like to contribute to our documentation and do not have prior experience with rst, we recommend you make use of these resources: + +- [A reStructuredText Primer](http://docutils.sourceforge.net/docs/user/rst/quickstart.html) +- [rst notes and cheatsheet](https://cheat.readthedocs.io/en/latest/rst.html) +- [Using the plot directive](https://matplotlib.org/devel/plot_directive.html) + +## Adding New Visualizers to the Docs + +If you are adding a new visualizer to the docs, there are quite a few examples in the documentation on which you can base your files of similar types. + +The primary format for the API section is as follows: + +``` +.. -*- mode: rst -*- + +My Visualizer +============= + +A brief introduction to my visualizer and how it is useful in the machine learning process. + +.. plot:: + :context: close-figs + :include-source: False + :alt: Example using MyVisualizer + + visualizer = MyVisualizer(LinearRegression()) + + visualizer.fit(X, y) + g = visualizer.poof() + +Discussion about my visualizer and some interpretation of the above plot. + + +API Reference +------------- + +.. automodule:: yellowbrick.regressor.mymodule + :members: MyVisualizer + :undoc-members: + :show-inheritance: +``` + +This is a pretty good structure for a documentation page; a brief introduction followed by a code example with a visualization included using [the plot directive](https://matplotlib.org/devel/plot_directive.html). This will render the `MyVisualizer` image in the document along with links for the complete source code, the png, and the pdf versions of the image. It will also have the "alt-text" (for screen-readers) and will not display the source because of the `:include-source:` option. If `:include-source:` is omitted, the source will also be included. + +The primary section is wrapped up with a discussion about how to interpret the visualizer and use it in practice. Finally the `API Reference` section will use `automodule` to include the documentation from your docstring. + +There are several other places where you can list your visualizer, but to ensure it is included in the documentation it *must be listed in the TOC of the local index*. Find the `index.rst` file in your subdirectory and add your rst file (without the `.rst` extension) to the `..toctree::` directive. This will ensure your documentation is included when it is built. + +## Generating the Gallery + +In v1.0, we have adopted Matplotlib's [plot directive](https://matplotlib.org/devel/plot_directive.html) which means that the majority of the images generated for the documentation are generated automatically. One exception is the gallery; the images for the gallery must still be generated manually. + +If you have contributed a new visualizer as described in the above section, please also add it to the gallery, both to `docs/gallery.py` and to `docs/gallery.rst`. (Make sure you have already installed Yellowbrick in editable mode, from the top level directory: `pip install -e` .) + +If you want to regenerate a single image (e.g. the elbow curve plot), you can do so as follows: + +```bash +$ python docs/gallery.py elbow +``` + +If you want to regenerate them all (note: this takes a long time!) + +```bash +$ python docs/gallery.py all +``` diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 63ee6cc74..88c13bfd2 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -11,3 +11,13 @@ overflow: visible !important; } } + +/* Add padding to the parameters list on RTD theme */ +dl.simple dt { + padding-left: 4px; +} + +dl.simple dt span.classifier { + padding-left: 6px; + font-weight: normal; +} \ No newline at end of file diff --git a/docs/about.rst b/docs/about.rst index d11fc67e7..9ee452210 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -45,33 +45,75 @@ From Wikipedia_: Team ---- -Yellowbrick is developed by data scientists who believe in open source and the project enjoys contributions from Python developers all over the world. The project was started by `@rebeccabilbro`_ and `@bbengfort`_ as an attempt to better explain machine learning concepts to their students; they quickly realized, however, that the potential for visual steering could have a large impact on practical data science and developed it into a high-level Python library. +Yellowbrick is developed by volunteer data scientists who believe in open source and the project enjoys contributions from Python developers all over the world. The project was started by `@rebeccabilbro`_ and `@bbengfort`_ as an attempt to better explain machine learning concepts to their students at Georgetown University where they teach a data science certificate program. They quickly realized, however, that the potential for visual steering could have a large impact on practical data science and developed it into a production-ready Python library. -Yellowbrick is incubated by `District Data Labs`_, an organization that is dedicated to collaboration and open source development. As part of District Data Labs, Yellowbrick was first introduced to the Python Community at `PyCon 2016 `_ in both talks and during the development sprints. The project was then carried on through DDL Research Labs (semester-long sprints where members of the DDL community contribute to various data-related projects). +Yellowbrick was then incubated by District Data Labs (DDL) in partnership with Georgetown University. District Data Labs is an organization that is dedicated to open source development and data science education and provided resources to help Yellowbrick grow. Yellowbrick was first introduced to the Python Community at `PyCon 2016 `_ in both talks and during the development sprints. The project was then carried on through DDL Research Labs -- semester-long sprints where members of the DDL community contribute to various data-related projects. + +Since then, Yellowbrick has enjoyed the participation of a large number of contributors from around the world and growing support in the PyData community. Yellowbrick has been featured in talks at PyData, Scipy, NumFOCUS, and PSF organized events as well as blog posts and Kaggle competitions. We are so thrilled to have such a dedicated community involved in active contributions both large and small. For a full list of current maintainers and core contributors, please see `MAINTAINERS.md `_ in the root of our GitHub repository. Thank you so much to everyone who has `contributed to Yellowbrick `_! +Affiliations +------------ + +Yellowbrick is proud to be affiliated with several organizations that provide institutional support to the project. Such support is sometimes financial, often material, and always in the spirit of free and open source software. We can't thank them enough for their role in making Yellowbrick what it is today. + +`District Data Labs`_: District Data Labs incubated Yellowbrick and sponsors research labs by purchasing food and organizing events. Research labs are semester long sprints that allow Yellowbrick contributors to meet in person, share a meal, and hack on the project. DDL also sponsors travel to PyCon and PyData conferences for Yellowbrick maintainers and helps us buy promotional material such as stickers and t-shirts. + +`NumFOCUS`_: Yellowbrick is a NumFOCUS affiliated project (not a fiscally sponsored project). Our relationship with NumFOCUS has given us a lot of data science cred in the community by being listed on their website. We are also eligible to apply for small development grants and infrastructure support. We often participate in the project developers mailing list and other activities such as Google Summer of Code. + +`Georgetown University`_: Georgetown primarily provides space for Yellowbrick events including the research labs. Additionally, Georgetown Data Science Certificate students are introduced to Yellowbrick at the beginning of their machine learning education and we often perform user testing of new features on them! + +How to Support Yellowbrick +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yellowbrick is developed by volunteers who work on the project in their spare time and not as part of their regular full-time work. If Yellowbrick has become critical to the success of your organization, please consider giving back to Yellowbrick. + + "... open source thrives on human rather than financial resources. There + are many ways to grow human resources, such as distributing the + workload among more contributors or encouraging companies to + make open source part of their employees’ work. An effective + support strategy must include multiple ways to generate time and + resources besides directly financing development. It must start from + the principle that the open source approach is not inherently flawed, + but rather under-resourced." + + -- `Roads and Bridges: The Unseen Labor Behind our Digital Infrastructure `_ + +The main thing that the Yellowbrick maintainers need is *time*. There are many ways to provide that time through non-financial mechanisms such as: + +- Create a written policy in your company handbook that dedicates time for your employees to contribute to open source projects like Yellowbrick. +- Interact with our community giving encouragement and advice, particularly for long term planning and non-code related activities like design and documentation. +- Advocate and evangelize your use of Yellowbrick and other open source software through blog posts and social media. +- Consider long term support strategies rather than ad hoc or one-off actions. +- Teach your students Machine Learning with Yellowbrick. + +More concrete and financial support is also welcome, particularly if it's directed through a specific effort. If you are interested in this kind of support consider: + +- Making a donation to NumFOCUS on behalf of Yellowbrick. +- Engaging District Data Labs for coporate training on visual machine learning with Yellowbrick (which will directly support Yellowbrick maintainers). +- Supporting your employee's continuing professional education in the Georgetown Data Science Certificate. +- Providing long term support for fixed costs such as hosting. + +Yellowbrick's mission is to enhance the machine learning workflow through open source visual steering and diagnostics. If you're interested in a more formal affiliate relationship to support this mission, please get in contact with us directly. + License ------- Yellowbrick is an open source project and its `license `_ is an implementation of the FOSS `Apache 2.0 `_ license by the Apache Software Foundation. `In plain English `_ this means that you can use Yellowbrick for commercial purposes, modify and distribute the source code, and even sublicense it. We want you to use Yellowbrick, profit from it, and contribute back if you do cool things with it. -There are, however, a couple of requirements that we ask from you. First, when you copy or distribute Yellowbrick source code, please include our copyright and license found in the `LICENSE.txt `_ at the root of our software repository. In addition, if we create a file called "NOTICE" in our project you must also include that in your source distribution. The "NOTICE" file will include attribution and thanks to those who have worked so hard on the project! Finally you can't hold District Data Labs or any Yellowbrick contributor liable for your use of our software, nor use any of our names, trademarks, or logos. +There are, however, a couple of requirements that we ask from you. First, when you copy or distribute Yellowbrick source code, please include our copyright and license found in the `LICENSE.txt `_ at the root of our software repository. In addition, if we create a file called "NOTICE" in our project you must also include that in your source distribution. The "NOTICE" file will include attribution and thanks to those who have worked so hard on the project! Note that you may not use our names, trademarks, or logos to promote your work or in any other way than to reference Yellowbrick. Finally, we provide Yellowbrick with no warranty and you can't hold any Yellowbrick contributor or affiliate liable for your use of our software. We think that's a pretty fair deal, and we're big believers in open source. If you make any changes to our software, use it commercially or academically, or have any other interest, we'd love to hear about it. - -.. _SIGMOD: http://cseweb.ucsd.edu/~arunkk/vision/SIGMODRecord15.pdf -.. _Wikipedia: https://en.wikipedia.org/wiki/Yellow_brick_road -.. _`@rebeccabilbro`: https://github.com/rebeccabilbro -.. _`@bbengfort`: https://github.com/bbengfort -.. _`District Data Labs`: http://www.districtdatalabs.com/ - Presentations ------------- Yellowbrick has enjoyed the spotlight in several presentations at recent conferences. We hope that these notebooks, talks, and slides will help you understand Yellowbrick a bit better. +Papers: + - `Yellowbrick: Visualizing the Scikit-Learn Model Selection Process `_ + Conference Presentations (videos): - `Visual Diagnostics for More Informed Machine Learning: Within and Beyond Scikit-Learn (PyCon 2016) `_ - `Yellowbrick: Steering Machine Learning with Visual Transformers (PyData London 2017) `_ @@ -85,9 +127,6 @@ Slides: - `Visualizing Model Selection with Scikit-Yellowbrick `_ - `Visual Pipelines for Text Analysis (Data Intelligence 2017) `_ -.. _QuatroCinco: https://flic.kr/p/2Yj9mj -.. _API: http://scikit-learn.org/stable/modules/classes.html - Citing Yellowbrick ------------------ @@ -95,6 +134,9 @@ Citing Yellowbrick .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.1206239.svg :target: https://doi.org/10.5281/zenodo.1206239 +.. image:: http://joss.theoj.org/papers/10.21105/joss.01075/status.svg + :target: https://doi.org/10.21105/joss.01075 + We hope that Yellowbrick facilitates machine learning of all kinds and we're particularly fond of academic work and research. If you're writing a scientific publication that uses Yellowbrick you can cite *Bengfort et al. (2018)* with the following BibTex: .. code-block:: bibtex @@ -112,18 +154,33 @@ We hope that Yellowbrick facilitates machine learning of all kinds and we're par performance, stability, and predictive value of machine learning models, and assist in diagnosing problems throughout the machine learning workflow.}, - version = {0.6}, - author = {Bengfort, Benjamin and Danielsen, Nathan and - Bilbro, Rebecca and Gray, Larry and {McIntyre}, Kristen and - Richardson, George and Miller, Taylor and Mayfield, Gary and - Schafer, Phillip and Keung, Jason}, - date = {2018-03-17}, + version = {0.9.1}, + author = {Bengfort, Benjamin and Bilbro, Rebecca and Danielsen, Nathan and + Gray, Larry and {McIntyre}, Kristen and Roman, Prema and Poh, Zijie and + others}, + date = {2018-11-14}, + year = {2018}, doi = {10.5281/zenodo.1206264} } You can also find DOI (digital object identifiers) for every version of Yellowbrick on `zenodo.org `_; use the BibTeX on this site to reference specific versions or changes made to the software. -We're also currently working on a scientific paper that describes Yellowbrick in the context of *steering the model selection process*. Stay tuned for a pre-release of this paper on arXiv. +We've also published a paper in the `Journal of Open Source Software (JOSS) `_ that discusses how Yellowbrick is designed to influence the model selection workflow. You may cite this paper if you are discussing Yellowbrick more generally in your research (instead of a specific version) or are interested in discussing visual analytics or visualization for machine learning. Please cite *Bengfort and Bilbro (2019)* with the following BibTex: + +.. code-block:: bibtex + + @article{bengfort_yellowbrick_2019, + title = {Yellowbrick: {{Visualizing}} the {{Scikit}}-{{Learn Model Selection Process}}}, + journaltitle = {The Journal of Open Source Software}, + volume = {4}, + number = {35}, + series = {1075}, + date = {2019-03-24}, + year = {2019}, + author = {Bengfort, Benjamin and Bilbro, Rebecca}, + url = {http://joss.theoj.org/papers/10.21105/joss.01075}, + doi = {10.21105/joss.01075} + } Contacting Us ------------- @@ -139,3 +196,13 @@ The best way to contact the Yellowbrick team is to send us a note on one of the .. _`mailing list`: http://bit.ly/yb-listserv .. _`Stack Overflow`: https://stackoverflow.com/questions/tagged/yellowbrick .. _`Twitter`: https://twitter.com/scikit_yb + +.. _QuatroCinco: https://flic.kr/p/2Yj9mj +.. _API: http://scikit-learn.org/stable/modules/classes.html +.. _SIGMOD: http://cseweb.ucsd.edu/~arunkk/vision/SIGMODRecord15.pdf +.. _Wikipedia: https://en.wikipedia.org/wiki/Yellow_brick_road +.. _`@rebeccabilbro`: https://github.com/rebeccabilbro +.. _`@bbengfort`: https://github.com/bbengfort +.. _`District Data Labs`: http://www.districtdatalabs.com/ +.. _`Georgetown University`: https://scs.georgetown.edu/programs/375/certificate-in-data-science/ +.. _`NumFOCUS`: https://numfocus.org/ \ No newline at end of file diff --git a/docs/api/anscombe.py b/docs/api/anscombe.py index d8f1b8826..5f04cec09 100644 --- a/docs/api/anscombe.py +++ b/docs/api/anscombe.py @@ -1,4 +1,4 @@ -# Creates the anscombe visualization. +# Creates the anscombe visualization. import yellowbrick as yb import matplotlib.pyplot as plt diff --git a/docs/api/classifier/class_prediction_error.py b/docs/api/classifier/class_prediction_error.py deleted file mode 100644 index de6bab33b..000000000 --- a/docs/api/classifier/class_prediction_error.py +++ /dev/null @@ -1,63 +0,0 @@ -# class_prediction_error.py - -""" -Creates the visualizations for the class_prediction_error.rst documentation -""" - -########################################################################## -## Imports -########################################################################## - -import pandas as pd -import matplotlib.pyplot as plt - -from sklearn.datasets import make_classification -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import train_test_split as tts - -from yellowbrick.classifier import ClassPredictionError - - -def make_fruit_dataset(): - X, y = make_classification( - n_samples=1000, n_classes=5, n_informative=3, n_clusters_per_class=1 - ) - - classes = ['apple', 'kiwi', 'pear', 'banana', 'orange'] - return tts(X, y, test_size=0.20, random_state=42), classes - - -def load_credit_dataset(): - data = pd.read_csv("../../../examples/data/credit/credit.csv") - target = "default" - features = list(data.columns) - features.remove(target) - - X = data[features] - y = data[target] - - classes = ["default", "current"] - return tts(X, y, test_size=0.2, random_state=53), classes - - -def make_cb_pred_error(dataset="fruit", path=None, clf=None): - clf = clf or RandomForestClassifier() - - loader = { - 'fruit': make_fruit_dataset, - 'credit': load_credit_dataset, - }[dataset] - - (X_train, X_test, y_train, y_test), classes = loader() - - _, ax = plt.subplots() - viz = ClassPredictionError(clf, ax=ax, classes=classes) - viz.fit(X_train, y_train) - viz.score(X_test, y_test) - - return viz.poof(outpath=path) - - -if __name__ == '__main__': - make_cb_pred_error("fruit", "images/class_prediction_error.png") - make_cb_pred_error("credit", "images/class_prediction_error_credit.png") diff --git a/docs/api/classifier/class_prediction_error.rst b/docs/api/classifier/class_prediction_error.rst index b7e2ddf85..9e9696616 100644 --- a/docs/api/classifier/class_prediction_error.rst +++ b/docs/api/classifier/class_prediction_error.rst @@ -2,16 +2,25 @@ Class Prediction Error ====================== + +The Yellowbrick ``ClassPredictionError`` plot is a twist on other and sometimes more familiar classification model diagnostic tools like the :doc:`confusion_matrix` and :doc:`classification_report`. Like the :doc:`classification_report`, this plot shows the support (number of training samples) for each class in the fitted classification model as a stacked bar chart. Each bar is segmented to show the proportion of predictions (including false negatives and false positives, like a :doc:`confusion_matrix`) for each class. You can use a ``ClassPredictionError`` to visualize which classes your classifier is having a particularly difficult time with, and more importantly, what incorrect answers it is giving on a per-class basis. This can often enable you to better understand strengths and weaknesses of different models and particular challenges unique to your dataset. + The class prediction error chart provides a way to quickly understand how good your classifier is at predicting the right classes. -.. code:: python +.. plot:: + :context: close-figs + :alt: Class Prediction Error plot on Fruit from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split + from sklearn.ensemble import RandomForestClassifier + from yellowbrick.classifier import ClassPredictionError + # Create classification dataset X, y = make_classification( - n_samples=1000, n_classes=5, n_informative=3, n_clusters_per_class=1 + n_samples=1000, n_classes=5, n_informative=3, n_clusters_per_class=1, + random_state=36, ) classes = ["apple", "kiwi", "pear", "banana", "orange"] @@ -19,16 +28,44 @@ The class prediction error chart provides a way to quickly understand how good y # Perform 80/20 training/test split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42) + # Instantiate the classification model and visualizer + visualizer = ClassPredictionError( + RandomForestClassifier(random_state=42, n_estimators=10), classes=classes + ) -.. code:: python + # Fit the training data to the visualizer + visualizer.fit(X_train, y_train) - from sklearn.ensemble import RandomForestClassifier + # Evaluate the model on the test data + visualizer.score(X_test, y_test) + # Draw visualization + visualizer.poof() + +In the above example, while the ``RandomForestClassifier`` appears to be fairly good at correctly predicting apples based on the features of the fruit, it often incorrectly labels pears as kiwis and mistakes kiwis for bananas. + +By contrast, in the following example, the ``RandomForestClassifier`` does a great job at correctly predicting accounts in default, but it is a bit of a coin toss in predicting account holders who stayed current on bills. + +.. plot:: + :context: close-figs + :alt: Class Prediction Error on account standing + + from sklearn.model_selection import train_test_split + from sklearn.ensemble import RandomForestClassifier from yellowbrick.classifier import ClassPredictionError + from yellowbrick.datasets import load_credit + + X, y = load_credit() + + classes = ['account in default', 'current with bills'] + # Perform 80/20 training/test split + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, + random_state=42) + # Instantiate the classification model and visualizer visualizer = ClassPredictionError( - RandomForestClassifier(), classes=classes + RandomForestClassifier(n_estimators=10), classes=classes ) # Fit the training data to the visualizer @@ -38,10 +75,10 @@ The class prediction error chart provides a way to quickly understand how good y visualizer.score(X_test, y_test) # Draw visualization - g = visualizer.poof() - -.. image:: images/class_prediction_error.png + visualizer.poof() + + API Reference ------------- diff --git a/docs/api/classifier/classification_report.py b/docs/api/classifier/classification_report.py deleted file mode 100644 index 2cabec28a..000000000 --- a/docs/api/classifier/classification_report.py +++ /dev/null @@ -1,58 +0,0 @@ -# classification_report -# Generates images for the classification report documentation. -# -# Author: Benjamin Bengfort -# Created: Sun Mar 18 16:35:30 2018 -0400 -# -# ID: classification_report.py [] benjamin@bengfort.com $ - -""" -Generates images for the classification report documentation. -""" - -########################################################################## -## Imports -########################################################################## - -import pandas as pd -import matplotlib.pyplot as plt - -from sklearn.naive_bayes import GaussianNB -from sklearn.model_selection import train_test_split as tts - -from yellowbrick.classifier import ClassificationReport - - -########################################################################## -## Quick Methods -########################################################################## - -def make_dataset(): - data = pd.read_csv("../../../examples/data/occupancy/occupancy.csv") - - X = data[["temperature", "relative humidity", "light", "C02", "humidity"]] - y = data.occupancy - - return tts(X, y, test_size=0.2) - - -def make_gb_report(path="images/classification_report.png"): - X_train, X_test, y_train, y_test = make_dataset() - - _, ax = plt.subplots() - - bayes = GaussianNB() - viz = ClassificationReport(bayes, ax=ax, classes=['unoccupied', 'occupied']) - - viz.fit(X_train, y_train) - viz.score(X_test, y_test) - - viz.poof(outpath=path) - - -########################################################################## -## Main Method -########################################################################## - -if __name__ == '__main__': - make_gb_report() diff --git a/docs/api/classifier/classification_report.rst b/docs/api/classifier/classification_report.rst index 8ac9a8a65..3c357b63a 100644 --- a/docs/api/classifier/classification_report.rst +++ b/docs/api/classifier/classification_report.rst @@ -5,42 +5,35 @@ Classification Report The classification report visualizer displays the precision, recall, F1, and support scores for the model. In order to support easier interpretation and problem detection, the report integrates numerical scores with a color-coded heatmap. All heatmaps are in the range ``(0.0, 1.0)`` to facilitate easy comparison of classification models across different classification reports. -.. code:: python - - from sklearn.model_selection import train_test_split - - # Load the classification data set - data = load_data("occupancy") - - # Specify the features of interest and the classes of the target - features = [ - "temperature", "relative humidity", "light", "C02", "humidity" - ] - classes = ["unoccupied", "occupied"] - - # Extract the instances and target - X = data[features] - y = data.occupancy - - # Create the train and test data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) - -.. code:: python +.. plot:: + :context: close-figs + :alt: Classification Report + from sklearn.model_selection import TimeSeriesSplit from sklearn.naive_bayes import GaussianNB + from yellowbrick.classifier import ClassificationReport + from yellowbrick.datasets import load_occupancy - # Instantiate the classification model and visualizer - bayes = GaussianNB() - visualizer = ClassificationReport(bayes, classes=classes, support=True) + # Load the classification dataset + X, y = load_occupancy() - visualizer.fit(X_train, y_train) # Fit the visualizer and the model - visualizer.score(X_test, y_test) # Evaluate the model on the test data - g = visualizer.poof() # Draw/show/poof the data + # Specify the target classes + classes = ["unoccupied", "occupied"] + # Create the training and test data + tscv = TimeSeriesSplit() + for train_index, test_index in tscv.split(X): + X_train, X_test = X.iloc[train_index], X.iloc[test_index] + y_train, y_test = y.iloc[train_index], y.iloc[test_index] + # Instantiate the classification model and visualizer + model = GaussianNB() + visualizer = ClassificationReport(model, classes=classes, support=True) -.. image:: images/classification_report.png + visualizer.fit(X_train, y_train) # Fit the visualizer and the model + visualizer.score(X_test, y_test) # Evaluate the model on the test data + visualizer.poof() # Draw/show/poof the data The classification report shows a representation of the main classification metrics on a per-class basis. This gives a deeper intuition of the classifier behavior over global accuracy which can mask functional weaknesses in one class of a multiclass problem. Visual classification reports are used to compare classification models to select models that are "redder", e.g. have stronger classification metrics or that are more balanced. @@ -60,6 +53,7 @@ The metrics are defined in terms of true and false positives, and true and false **support** Support is the number of actual occurrences of the class in the specified dataset. Imbalanced support in the training data may indicate structural weaknesses in the reported scores of the classifier and could indicate the need for stratified sampling or rebalancing. Support doesn't change between models but instead diagnoses the evaluation process. +.. note:: This example uses ``TimeSeriesSplit`` to split the data into the training and test sets. For more information on this cross-validation method, please refer to the scikit-learn `documentation `_. API Reference ------------- diff --git a/docs/api/classifier/confusion_matrix.py b/docs/api/classifier/confusion_matrix.py deleted file mode 100644 index c5a0952c4..000000000 --- a/docs/api/classifier/confusion_matrix.py +++ /dev/null @@ -1,36 +0,0 @@ -from sklearn.datasets import load_digits, load_iris -from sklearn.linear_model import LogisticRegression -from sklearn.model_selection import train_test_split as tts - -from yellowbrick.classifier import ConfusionMatrix - - -if __name__ == '__main__': - digits = load_digits() - digit_X = digits.data - digit_y = digits.target - d_X_train, d_X_test, d_y_train, d_y_test = tts( - digit_X, digit_y, test_size=0.2 - ) - model = LogisticRegression() - digit_cm = ConfusionMatrix(model, classes=[0,1,2,3,4,5,6,7,8,9]) - digit_cm.fit(d_X_train, d_y_train) - digit_cm.score(d_X_test, d_y_test) - d = digit_cm.poof(outpath="images/confusion_matrix_digits.png") - - - iris = load_iris() - iris_X = iris.data - iris_y = iris.target - iris_classes = iris.target_names - i_X_train, i_X_test, i_y_train, i_y_test = tts( - iris_X, iris_y, test_size=0.2 - ) - model = LogisticRegression() - iris_cm = ConfusionMatrix( - model, classes=iris_classes, - label_encoder={0: 'setosa', 1: 'versicolor', 2: 'virginica'} - ) - iris_cm.fit(i_X_train, i_y_train) - iris_cm.score(i_X_test, i_y_test) - i = iris_cm.poof(outpath="images/confusion_matrix_iris.png") diff --git a/docs/api/classifier/confusion_matrix.rst b/docs/api/classifier/confusion_matrix.rst index 878b910a8..c286543ef 100644 --- a/docs/api/classifier/confusion_matrix.rst +++ b/docs/api/classifier/confusion_matrix.rst @@ -3,29 +3,28 @@ Confusion Matrix ================ -The ``ConfusionMatrix`` visualizer is a ScoreVisualizer that takes a -fitted scikit-learn classifier and a set of test X and y values and +The ``ConfusionMatrix`` visualizer is a ``ScoreVisualizer`` that takes a +fitted scikit-learn classifier and a set of test ``X`` and ``y`` values and returns a report showing how each of the test values predicted classes compare to their actual classes. Data scientists use confusion matrices to understand which classes are most easily confused. These provide -similar information as what is available in a ClassificationReport, but +similar information as what is available in a ``ClassificationReport``, but rather than top-level scores, they provide deeper insight into the classification of individual data points. -Below are a few examples of using the ConfusionMatrix visualizer; more +Below are a few examples of using the ``ConfusionMatrix`` visualizer; more information can be found by looking at the scikit-learn documentation on `confusion matrices `_. -.. code:: python +.. plot:: + :context: close-figs + :alt: ConfusionMatrix plot of sklearn Digits dataset from sklearn.datasets import load_digits - from sklearn.model_selection import train_test_split + from sklearn.model_selection import train_test_split as tts from sklearn.linear_model import LogisticRegression - from yellowbrick.classifier import ConfusionMatrix -.. code:: python - # We'll use the handwritten digits data set from scikit-learn. # Each feature of this dataset is an 8x8 pixel image of a handwritten number. # Digits.data converts these 64 pixels into a single array of features @@ -33,9 +32,9 @@ scikit-learn documentation on `confusion matrices `_ (or anything with an `inverse_transform` method that performs the mapping), or a `dict` with the encoding-to-string mapping as in the example below: +Class names can be added to a ``ConfusionMatrix`` plot using the ``label_encoder`` argument. The ``label_encoder`` can be a `sklearn.preprocessing.LabelEncoder `_ (or anything with an ``inverse_transform`` method that performs the mapping), or a ``dict`` with the encoding-to-string mapping as in the example below: -.. code:: python +.. plot:: + :context: close-figs + :alt: ConfusionMatrix plot with class names + from sklearn.datasets import load_iris + from sklearn.model_selection import train_test_split as tts + from sklearn.linear_model import LogisticRegression + from yellowbrick.classifier import ConfusionMatrix + iris = load_iris() X = iris.data y = iris.target @@ -68,7 +71,7 @@ Class names can be added to a `ConfusionMatrix` plot using the `label_encoder` a X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2) - model = LogisticRegression() + model = LogisticRegression(multi_class="auto", solver="liblinear") iris_cm = ConfusionMatrix( model, classes=classes, @@ -81,9 +84,6 @@ Class names can be added to a `ConfusionMatrix` plot using the `label_encoder` a iris_cm.poof() -.. image:: images/confusion_matrix_iris.png - - API Reference ------------- diff --git a/docs/api/classifier/images/binary_precision_recall.png b/docs/api/classifier/images/binary_precision_recall.png deleted file mode 100644 index e3602a65e..000000000 Binary files a/docs/api/classifier/images/binary_precision_recall.png and /dev/null differ diff --git a/docs/api/classifier/images/churn_discrimination_threshold.png b/docs/api/classifier/images/churn_discrimination_threshold.png deleted file mode 100644 index c66bbe3ef..000000000 Binary files a/docs/api/classifier/images/churn_discrimination_threshold.png and /dev/null differ diff --git a/docs/api/classifier/images/class_prediction_error.png b/docs/api/classifier/images/class_prediction_error.png deleted file mode 100644 index ed7855f0d..000000000 Binary files a/docs/api/classifier/images/class_prediction_error.png and /dev/null differ diff --git a/docs/api/classifier/images/class_prediction_error_credit.png b/docs/api/classifier/images/class_prediction_error_credit.png deleted file mode 100644 index 0edb9b9b1..000000000 Binary files a/docs/api/classifier/images/class_prediction_error_credit.png and /dev/null differ diff --git a/docs/api/classifier/images/classification_report.png b/docs/api/classifier/images/classification_report.png deleted file mode 100644 index 81e8a3b2b..000000000 Binary files a/docs/api/classifier/images/classification_report.png and /dev/null differ diff --git a/docs/api/classifier/images/confusion_matrix_digits.png b/docs/api/classifier/images/confusion_matrix_digits.png deleted file mode 100644 index 120c56d8d..000000000 Binary files a/docs/api/classifier/images/confusion_matrix_digits.png and /dev/null differ diff --git a/docs/api/classifier/images/confusion_matrix_iris.png b/docs/api/classifier/images/confusion_matrix_iris.png deleted file mode 100644 index f536db142..000000000 Binary files a/docs/api/classifier/images/confusion_matrix_iris.png and /dev/null differ diff --git a/docs/api/classifier/images/multiclass_precision_recall.png b/docs/api/classifier/images/multiclass_precision_recall.png deleted file mode 100644 index 923c9b915..000000000 Binary files a/docs/api/classifier/images/multiclass_precision_recall.png and /dev/null differ diff --git a/docs/api/classifier/images/multiclass_precision_recall_full.png b/docs/api/classifier/images/multiclass_precision_recall_full.png deleted file mode 100644 index ec5e7ca51..000000000 Binary files a/docs/api/classifier/images/multiclass_precision_recall_full.png and /dev/null differ diff --git a/docs/api/classifier/images/rocauc.png b/docs/api/classifier/images/rocauc.png deleted file mode 100644 index 145a993c2..000000000 Binary files a/docs/api/classifier/images/rocauc.png and /dev/null differ diff --git a/docs/api/classifier/images/rocauc_binary.png b/docs/api/classifier/images/rocauc_binary.png deleted file mode 100644 index 2e9e7ce6e..000000000 Binary files a/docs/api/classifier/images/rocauc_binary.png and /dev/null differ diff --git a/docs/api/classifier/images/rocauc_multiclass.png b/docs/api/classifier/images/rocauc_multiclass.png deleted file mode 100644 index 0da15e398..000000000 Binary files a/docs/api/classifier/images/rocauc_multiclass.png and /dev/null differ diff --git a/docs/api/classifier/images/spam_discrimination_threshold.png b/docs/api/classifier/images/spam_discrimination_threshold.png deleted file mode 100644 index 5267df270..000000000 Binary files a/docs/api/classifier/images/spam_discrimination_threshold.png and /dev/null differ diff --git a/docs/api/classifier/prcurve.py b/docs/api/classifier/prcurve.py deleted file mode 100644 index f4ac5224a..000000000 --- a/docs/api/classifier/prcurve.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python - -import os -import pandas as pd -import matplotlib.pyplot as plt - -from sklearn.naive_bayes import MultinomialNB -from sklearn.linear_model import RidgeClassifier -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import train_test_split -from sklearn.preprocessing import LabelEncoder - -from yellowbrick.classifier import PrecisionRecallCurve - - -# Location of downloaded datasets from Yellowbrick -FIXTURES = os.path.join( - os.path.dirname(__file__), "..", "..", "..", "yellowbrick", "datasets", "fixtures" -) - - -def load_binary(split=True): - data = pd.read_csv(os.path.join(FIXTURES, "spam", "spam.csv")) - - target = "is_spam" - features = [col for col in data.columns if col != target] - - X = data[features] - y = data[target] - - if split: - return train_test_split(X, y, test_size=0.2, shuffle=True) - return X, y - - -def load_multiclass(split=True): - data = pd.read_csv(os.path.join(FIXTURES, "game", "game.csv")) - - # Encode the categorical variables - data.replace({'x':0, 'o':1, 'b':2}, inplace=True) - - # Extract the numpy arrays from the data frame - X = data.iloc[:, data.columns != 'outcome'] - y = LabelEncoder().fit_transform(data['outcome']) - - if split: - return train_test_split(X, y, test_size=0.2, shuffle=True) - return X, y - - -def draw_binary(outpath=None): - _, ax = plt.subplots(figsize=(9,6)) - - X_train, X_test, y_train, y_test = load_binary(split=True) - - oz = PrecisionRecallCurve(RidgeClassifier(), ax=ax) - oz.fit(X_train, y_train) - oz.score(X_test, y_test) - oz.poof(outpath=outpath) - - -def draw_multiclass(outpath=None, simple=True): - _, ax = plt.subplots(figsize=(9,6)) - - X_train, X_test, y_train, y_test = load_multiclass() - - if simple: - oz = PrecisionRecallCurve(RandomForestClassifier(), ax=ax) - else: - oz = PrecisionRecallCurve(MultinomialNB(), ax=ax, per_class=True, iso_f1_curves=True, fill_area=False, micro=False) - - oz.fit(X_train, y_train) - oz.score(X_test, y_test) - oz.poof(outpath=outpath) - - - -if __name__ == '__main__': - draw_binary(outpath="images/binary_precision_recall.png") - draw_multiclass(simple=True, outpath="images/multiclass_precision_recall.png") - draw_multiclass(simple=False, outpath="images/multiclass_precision_recall_full.png") diff --git a/docs/api/classifier/prcurve.rst b/docs/api/classifier/prcurve.rst index d1762b80e..c0954029e 100644 --- a/docs/api/classifier/prcurve.rst +++ b/docs/api/classifier/prcurve.rst @@ -14,16 +14,17 @@ for the majority of classes it selects. Binary Classification --------------------- -.. code:: python +.. plot:: + :context: close-figs + :alt: PrecisionRecallCurve with Binary Classification from sklearn.linear_model import RidgeClassifier from sklearn.model_selection import train_test_split as tts from yellowbrick.classifier import PrecisionRecallCurve + from yellowbrick.datasets import load_spam # Load the dataset and split into train/test splits - data = load_spam() - X = data[[col for col in data.columns if col != "is_spam"]] - y = data["is_spam"] + X, y = load_spam() X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, shuffle=True) @@ -34,8 +35,6 @@ Binary Classification viz.poof() -.. image:: images/binary_precision_recall.png - The base case for precision-recall curves is the binary classification case, and this case is also the most visually interpretable. In the figure above we can see the precision plotted on the y-axis against the recall on the x-axis. The larger the filled in area, the stronger the classifier is. The red line annotates the *average precision*, a summary of the entire plot computed as the weighted average of precision achieved at each threshold such that the weight is the difference in recall from the previous threshold. Multi-Label Classification @@ -43,45 +42,59 @@ Multi-Label Classification To support multi-label classification, the estimator is wrapped in a `OneVsRestClassifier `_ to produce binary comparisons for each class (e.g. the positive case is the class and the negative case is any other class). The Precision-Recall curve is then computed as the micro-average of the precision and recall for all classes: -.. code:: python +.. plot:: + :context: close-figs + :alt: PrecisionRecallCurves with Multi-label Classification from sklearn.ensemble import RandomForestClassifier - from sklearn.preprocessing import LabelEncoder + from sklearn.preprocessing import LabelEncoder, OrdinalEncoder + from sklearn.model_selection import train_test_split as tts + from yellowbrick.classifier import PrecisionRecallCurve + from yellowbrick.datasets import load_game # Load dataset and encode categorical variables - data = load_game() - data.replace({'x':0, 'o':1, 'b':2}, inplace=True) - - # Create train/test splits - X = data.iloc[:, data.columns != 'outcome'] - y = LabelEncoder().fit_transform(data['outcome']) + X, y = load_game() + X = OrdinalEncoder().fit_transform(X) + y = LabelEncoder().fit_transform(y) X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, shuffle=True) # Create the visualizer, fit, score, and poof it - viz = PrecisionRecallCurve(RandomForestClassifier()) + viz = PrecisionRecallCurve(RandomForestClassifier(n_estimators=10)) viz.fit(X_train, y_train) viz.score(X_test, y_test) viz.poof() -.. image:: images/multiclass_precision_recall.png A more complex Precision-Recall curve can be computed, however, displaying the each curve individually, along with F1-score ISO curves (e.g. that show the relationship between precision and recall for various F1 scores). -.. code:: python +.. plot:: + :context: close-figs + :alt: PrecisionRecallCurves displaying each curve individually from sklearn.naive_bayes import MultinomialNB + from sklearn.preprocessing import LabelEncoder, OrdinalEncoder + from sklearn.model_selection import train_test_split as tts + from yellowbrick.classifier import PrecisionRecallCurve + from yellowbrick.datasets import load_game - oz = PrecisionRecallCurve( + # Load dataset and encode categorical variables + X, y = load_game() + X = OrdinalEncoder().fit_transform(X) + encoder = LabelEncoder() + y = encoder.fit_transform(y) + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, shuffle=True) + + # Create the visualizer, fit, score, and poof it + viz = PrecisionRecallCurve( MultinomialNB(), per_class=True, iso_f1_curves=True, - fill_area=False, micro=False + fill_area=False, micro=False, classes=encoder.classes_ ) viz.fit(X_train, y_train) viz.score(X_test, y_test) viz.poof() -.. image:: images/multiclass_precision_recall_full.png - .. seealso:: `Scikit-Learn: Model Selection with Precision Recall Curves `_ diff --git a/docs/api/classifier/rocauc.py b/docs/api/classifier/rocauc.py deleted file mode 100644 index 98708dd32..000000000 --- a/docs/api/classifier/rocauc.py +++ /dev/null @@ -1,77 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -from sklearn.svm import LinearSVC -from sklearn.linear_model import LogisticRegression, RidgeClassifier -from sklearn.model_selection import train_test_split - -from yellowbrick.classifier import ROCAUC - - -def load_occupancy(): - # Load the binary classification data set - room = pd.read_csv("../../../examples/data/occupancy/occupancy.csv") - - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - classes = ['unoccupied', 'occupied'] - - # Extract the numpy arrays from the data frame - X = room[features].values - y = room.occupancy.values - - return X, y, classes - - -def load_game(): - # Load multi-class classification dataset - game = pd.read_csv('../../../examples/data/game/game.csv') - - classes = ["win", "loss", "draw"] - game.replace({'loss':-1, 'draw':0, 'win':1, 'x':2, 'o':3, 'b':4}, inplace=True) - - # Extract the numpy arrays from the data frame - X = game.iloc[:, game.columns != 'outcome'] - y = game['outcome'] - - return X, y, classes - - -def rocauc(X, y, model, outpath, **kwargs): - # Create a new figure and axes - _, ax = plt.subplots() - - # Instantiate the classification model and visualizer - visualizer = ROCAUC(model, ax=ax, **kwargs) - - # Create the train and test data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) - - visualizer.fit(X_train, y_train) - visualizer.score(X_test, y_test) - - # Save to disk - visualizer.poof(outpath=outpath) - -if __name__ == '__main__': - - # Occupancy data visualization - X, y, classes = load_occupancy() - - # Draw the binary rocauc - rocauc( - X, y, LogisticRegression(), "images/rocauc_binary.png", classes=classes - ) - - # Draw a single binary decision curve - rocauc( - X, y, LinearSVC(), "images/rocauc_binary.png", - micro=False, macro=False, per_class=False - ) - - # Game data visualization - X, y, classes = load_game() - - # Draw the multiclass roc_auc - rocauc( - X, y, RidgeClassifier(), "images/rocauc_multiclass.png", classes=classes - ) diff --git a/docs/api/classifier/rocauc.rst b/docs/api/classifier/rocauc.rst index 5291dc0d0..35fc89023 100644 --- a/docs/api/classifier/rocauc.rst +++ b/docs/api/classifier/rocauc.rst @@ -9,87 +9,79 @@ The Receiver Operating Characteristic (ROC) is a measure of a classifier's predi This leads to another metric, area under the curve (AUC), which is a computation of the relationship between false positives and true positives. The higher the AUC, the better the model generally is. However, it is also important to inspect the "steepness" of the curve, as this describes the maximization of the true positive rate while minimizing the false positive rate. -.. code:: python +.. plot:: + :context: close-figs + :alt: ROCAUC Binary Classification + from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split - # Load the classification data set - data = load_data("occupancy") - - # Specify the features of interest and the classes of the target - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - classes = ["unoccupied", "occupied"] - - # Extract the instances and target - X = data[features] - y = data.occupancy - - # Create the train and test data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) - -.. code:: python - from yellowbrick.classifier import ROCAUC - from sklearn.linear_model import LogisticRegression + from yellowbrick.datasets import load_spam - # Instantiate the visualizer with the classification model - visualizer = ROCAUC(LogisticRegression(), classes=classes) + # Load the classification dataset + X, y = load_spam() - visualizer.fit(X_train, y_train) # Fit the training data to the visualizer - visualizer.score(X_test, y_test) # Evaluate the model on the test data - g = visualizer.poof() # Draw/show/poof the data + # Create the training and test data + X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) + # Instantiate the visualizer with the classification model + model = LogisticRegression(multi_class="auto", solver="liblinear") + visualizer = ROCAUC(model, classes=["not_spam", "is_spam"]) -.. image:: images/rocauc_binary.png + visualizer.fit(X_train, y_train) # Fit the training data to the visualizer + visualizer.score(X_test, y_test) # Evaluate the model on the test data + visualizer.poof() # Draw/show/poof the data .. warning:: - Binary classification using a Scikit-learn-style estimator with only a - ``decision_function``, triggers an ``IndexError`` because the predictions - will be a 1D array, meaning there is only sufficient information to plot a - single curve. More on this bug can be found in this `notebook `_. The bug was addressed in a `July 2018 PR `_ - and will be fixed in v0.9, where the solution will be to set the ``micro``, - ``macro``, and ``per-class`` parameters of ``ROCAUC`` to ``False``. + Versions of Yellowbrick =< v0.8 had a `bug `_ + that triggered an ``IndexError`` when attempting binary classification using + a Scikit-learn-style estimator with only a ``decision_function``. This has been + fixed as of v0.9, where the ``micro``, ``macro``, and ``per-class`` parameters of + ``ROCAUC`` are set to ``False`` for such classifiers. Multi-class ROCAUC Curves -######################### +------------------------- Yellowbrick's ``ROCAUC`` Visualizer does allow for plotting multiclass classification curves. ROC curves are typically used in binary classification, and in fact the Scikit-Learn ``roc_curve`` metric is only able to perform metrics for binary classifiers. Yellowbrick addresses this by binarizing the output (per-class) or to use one-vs-rest (micro score) or one-vs-all (macro score) strategies of classification. -.. code:: +.. plot:: + :context: close-figs + :alt: ROCAUC multiclass classification curves - # Load multi-class classification dataset - game = load_game() + from sklearn.linear_model import RidgeClassifier + from sklearn.model_selection import train_test_split + from sklearn.preprocessing import OrdinalEncoder, LabelEncoder + + from yellowbrick.classifier import ROCAUC + from yellowbrick.datasets import load_game - classes = ["win", "loss", "draw"] + # Load multi-class classification dataset + X, y = load_game() # Encode the non-numeric columns - game.replace({'loss':-1, 'draw':0, 'win':1, 'x':2, 'o':3, 'b':4}, inplace=True) - - # Extract the instances and target - X = game.iloc[:, game.columns != 'outcome'] - y = game['outcome'] + X = OrdinalEncoder().fit_transform(X) + y = LabelEncoder().fit_transform(y) # Create the train and test data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) -.. code:: + # Instaniate the classification model and visualizer + model = RidgeClassifier() + visualizer = ROCAUC(model, classes=["win", "loss", "draw"]) - from sklearn.linear_model import RidgeClassifier - - visualizer = ROCAUC(RidgeClassifier(), classes=classes) + visualizer.fit(X_train, y_train) # Fit the training data to the visualizer + visualizer.score(X_test, y_test) # Evaluate the model on the test data + visualizer.poof() # Draw/show/poof the data - visualizer.fit(X_train, y_train) # Fit the training data to the visualizer - visualizer.score(X_test, y_test) # Evaluate the model on the test data - g = visualizer.poof() # Draw/show/poof the data +.. warning:: + The target ``y`` must be numeric for this figure to work, or update to the latest version of sklearn. By default with multi-class ROCAUC visualizations, a curve for each class is plotted, in addition to the micro- and macro-average curves for each class. This enables the user to inspect the tradeoff between sensitivity and specificity on a per-class basis. Note that for multi-class ``ROCAUC``, at least one of the ``micro``, ``macro``, or ``per_class`` parameters must be set to ``True`` (by default, all are set to ``True``). -.. image:: images/rocauc_multiclass.png - - API Reference ------------- diff --git a/docs/api/classifier/threshold.py b/docs/api/classifier/threshold.py deleted file mode 100644 index 056360be9..000000000 --- a/docs/api/classifier/threshold.py +++ /dev/null @@ -1,81 +0,0 @@ -import os -import pandas as pd -import matplotlib.pyplot as plt - -from yellowbrick.classifier import DiscriminationThreshold -from sklearn.linear_model import LogisticRegression -from sklearn.ensemble import RandomForestClassifier -from sklearn.preprocessing import LabelEncoder - -from functools import partial - -BASE = os.path.join("..", "..", "..", os.path.dirname("__file__")) -EXAMPLES = os.path.join(BASE, "examples", "data") - -# TODO: Make these examples part of the code base -CHURN_DATASET = os.path.join(EXAMPLES, "churn", "churn.txt") -SPAM_DATASET = os.path.join(EXAMPLES, "spam", "spam.csv") - - -def load_spam(): - df = pd.read_csv(SPAM_DATASET) - - target = 'is_spam' - features = [col for col in df.columns if col != target] - - X = df[features] - y = df[target] - - return X, y - - -def load_churn(): - df = pd.read_csv(CHURN_DATASET) - df.columns = [ - c.lower().replace(' ', '_').replace('?', '').replace("'", "") - for c in df.columns - ] - - state_encoder = LabelEncoder() - df.state = state_encoder.fit_transform(df.state) - - del df['phone'] - - for col in ['intl_plan', 'vmail_plan', 'churn']: - df[col] = df[col].map({'no': 0, 'False.': 0, 'yes': 1, 'True.': 1}) - - X = df[[c for c in df.columns if c != 'churn']] - y = df['churn'] - - return X, y - - -def plot_discrimination_threshold(clf, data='spam', outpath=None): - if data == 'spam': - X, y = load_spam() - elif data == 'churn': - X, y = load_churn() - else: - raise ValueError("no dataset loader '{}'".format(data)) - - _, ax = plt.subplots() - - visualizer = DiscriminationThreshold(clf, ax=ax) - visualizer.fit(X, y) - visualizer.poof(outpath=outpath) - - -plot_churn = partial( - plot_discrimination_threshold, data='churn', - outpath="images/churn_discrimination_threshold.png" -) - -plot_spam = partial( - plot_discrimination_threshold, data='spam', - outpath="images/spam_discrimination_threshold.png" -) - - -if __name__ == '__main__': - plot_churn(RandomForestClassifier()) - plot_spam(LogisticRegression()) diff --git a/docs/api/classifier/threshold.rst b/docs/api/classifier/threshold.rst index f287f1e5c..443d728a2 100644 --- a/docs/api/classifier/threshold.rst +++ b/docs/api/classifier/threshold.rst @@ -7,30 +7,24 @@ Discrimination Threshold A visualization of precision, recall, f1 score, and queue rate with respect to the discrimination threshold of a binary classifier. The *discrimination threshold* is the probability or score at which the positive class is chosen over the negative class. Generally, this is set to 50% but the threshold can be adjusted to increase or decrease the sensitivity to false positives or to other application factors. -.. code:: python - - # Load a binary classification dataset - data = load_data("spam") - target = "is_spam" - features = [col for col in data.columns if col != target] - - # Extract the instances and target from the dataset - X = data[features] - y = data[target] - -.. code:: python +.. plot:: + :context: close-figs + :alt: Discrimination Threshold of a binary classifier from sklearn.linear_model import LogisticRegression + from yellowbrick.classifier import DiscriminationThreshold + from yellowbrick.datasets import load_spam - # Instantiate the classification model and visualizer - logistic = LogisticRegression() - visualizer = DiscriminationThreshold(logistic) + # Load a binary classification dataset + X, y = load_spam() - visualizer.fit(X, y) # Fit the training data to the visualizer - visualizer.poof() # Draw/show/poof the data + # Instantiate the classification model and visualizer + model = LogisticRegression(multi_class="auto", solver="liblinear") + visualizer = DiscriminationThreshold(model) -.. image:: images/spam_discrimination_threshold.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data One common use of binary classification algorithms is to use the score or probability they produce to determine cases that require special treatment. For example, a fraud prevention application might use a classification algorithm to determine if a transaction is likely fraudulent and needs to be investigated in detail. In the figure above, we present an example where a binary classifier determines if an email is "spam" (the positive case) or "not spam" (the negative case). Emails that are detected as spam are moved to a hidden folder and eventually deleted. diff --git a/docs/api/cluster/elbow.py b/docs/api/cluster/elbow.py deleted file mode 100644 index fd362308d..000000000 --- a/docs/api/cluster/elbow.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python - -""" -Generate images for the elbow plot documentation. -""" - -# Import necessary modules -import matplotlib.pyplot as plt - -from sklearn.cluster import KMeans -from sklearn.datasets import make_blobs -from yellowbrick.cluster import KElbowVisualizer - - -def draw_elbow(path="images/elbow.png"): - # Generate synthetic dataset with 8 blobs - X, y = make_blobs( - centers=8, n_features=12, n_samples=1000, - shuffle=True, random_state=42 - ) - - # Create a new figure to draw the clustering visualizer on - _, ax = plt.subplots() - - # Instantiate the clustering model and visualizer - model = KMeans() - visualizer = KElbowVisualizer(model, ax=ax, k=(4,12)) - - visualizer.fit(X) # Fit the data to the visualizer - visualizer.poof(outpath=path) # Draw/show/poof the data - - -def draw_calinski_harabaz(path="images/calinski_harabaz.png"): - # Generate synthetic dataset with 8 blobs - X, y = make_blobs( - centers=8, n_features=12, n_samples=1000, - shuffle=True, random_state=42 - ) - - # Create a new figure to draw the clustering visualizer on - _, ax = plt.subplots() - - # Instantiate the clustering model and visualizer - model = KMeans() - visualizer = KElbowVisualizer( - model, ax=ax, k=(4,12), - metric='calinski_harabaz', timings=False - ) - visualizer.fit(X) # Fit the data to the visualizer - visualizer.poof(outpath=path) # Draw/show/poof the data - - -if __name__ == '__main__': - draw_elbow() - draw_calinski_harabaz() diff --git a/docs/api/cluster/elbow.rst b/docs/api/cluster/elbow.rst index 50e3d8277..aed5d0c6d 100644 --- a/docs/api/cluster/elbow.rst +++ b/docs/api/cluster/elbow.rst @@ -3,53 +3,80 @@ Elbow Method ============ -The ``KElbowVisualizer`` implements the "elbow" method to help data scientists select the optimal number of clusters by fitting the model with a range of values for :math:`K`. If the line chart resembles an arm, then the "elbow" (the point of inflection on the curve) is a good indication that the underlying model fits best at that point. +The ``KElbowVisualizer`` implements the "elbow" method to help data scientists select the optimal number of clusters by fitting the model with a range of values for :math:`K`. If the line chart resembles an arm, then the "elbow" (the point of inflection on the curve) is a good indication that the underlying model fits best at that point. In the visualizer "elbow" will be annotated with a dashed line. -To demonstrate, in the following example the ``KElbowVisualizer`` fits the ``KMeans`` model for a range of :math:`K` values from 4 to 11 on a sample two-dimensional dataset with 8 random clusters of points. When the model is fit with 8 clusters, we can see an "elbow" in the graph, which in this case we know to be the optimal number. +To demonstrate, in the following example the ``KElbowVisualizer`` fits the ``KMeans`` model for a range of :math:`K` values from 4 to 11 on a sample two-dimensional dataset with 8 random clusters of points. When the model is fit with 8 clusters, we can see a line annotating the "elbow" in the graph, which in this case we know to be the optimal number. -.. code:: python +.. plot:: + :context: close-figs + :alt: KElbowVisualizer on synthetic dataset with 8 random clusters + from sklearn.cluster import KMeans from sklearn.datasets import make_blobs - # Create synthetic dataset with 8 random clusters - X, y = make_blobs(centers=8, n_features=12, shuffle=True, random_state=42) - -.. code:: python - - from sklearn.cluster import KMeans from yellowbrick.cluster import KElbowVisualizer + # Generate synthetic dataset with 8 random clusters + X, y = make_blobs(n_samples=1000, n_features=12, centers=8, random_state=42) + # Instantiate the clustering model and visualizer model = KMeans() visualizer = KElbowVisualizer(model, k=(4,12)) - visualizer.fit(X) # Fit the data to the visualizer - visualizer.poof() # Draw/show/poof the data - -.. image:: images/elbow.png + visualizer.fit(X) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data By default, the scoring parameter ``metric`` is set to ``distortion``, which computes the sum of squared distances from each point to its assigned center. -However, two other metrics can also be used with the ``KElbowVisualizer`` -- ``silhouette`` and ``calinski_harabaz``. The ``silhouette`` score calculates the mean Silhouette Coefficient of all samples, while the ``calinski_harabaz`` score computes the ratio of dispersion between and within clusters. +However, two other metrics can also be used with the ``KElbowVisualizer`` -- ``silhouette`` and ``calinski_harabasz``. The ``silhouette`` score calculates the mean Silhouette Coefficient of all samples, while the ``calinski_harabasz`` score computes the ratio of dispersion between and within clusters. -The ``KElbowVisualizer`` also displays the amount of time to train the clustering model per :math:`K` as a dashed green line, but is can be hidden by setting ``timings=False``. In the following example, we'll use the ``calinski_harabaz`` score and hide the time to fit the model. +The ``KElbowVisualizer`` also displays the amount of time to train the clustering model per :math:`K` as a dashed green line, but is can be hidden by setting ``timings=False``. In the following example, we'll use the ``calinski_harabasz`` score and hide the time to fit the model. -.. code:: python +.. plot:: + :context: close-figs + :alt: KElbowVisualizer on synthetic dataset with 8 random clusters from sklearn.cluster import KMeans + from sklearn.datasets import make_blobs + from yellowbrick.cluster import KElbowVisualizer - + + # Generate synthetic dataset with 8 random clusters + X, y = make_blobs(n_samples=1000, n_features=12, centers=8, random_state=42) + # Instantiate the clustering model and visualizer model = KMeans() visualizer = KElbowVisualizer( - model, k=(4,12), metric='calinski_harabaz', timings=False + model, k=(4,12), metric='calinski_harabasz', timings=False ) - visualizer.fit(X) # Fit the data to the visualizer - visualizer.poof() # Draw/show/poof the data + visualizer.fit(X) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data + +By default, the parameter ``locate_elbow`` is set to ``True``, which automatically find the "elbow" which likely corresponds to the optimal value of k using the "knee point detection algorithm". However, users can turn off the feature by setting ``locate_elbow=False``. You can read about the implementation of this algorithm at "`Knee point detection in Python `_" by Kevin Arvai. + +In the following example, we'll use the ``calinski_harabasz`` score and turn off ``locate_elbow`` feature. +.. plot:: + :context: close-figs + :alt: KElbowVisualizer on synthetic dataset with 8 random clusters + + from sklearn.cluster import KMeans + from sklearn.datasets import make_blobs + + from yellowbrick.cluster import KElbowVisualizer + + # Generate synthetic dataset with 8 random clusters + X, y = make_blobs(n_samples=1000, n_features=12, centers=8, random_state=42) + + # Instantiate the clustering model and visualizer + model = KMeans() + visualizer = KElbowVisualizer( + model, k=(4,12), metric='calinski_harabasz', timings=False, locate_elbow=False + ) -.. image:: images/calinski_harabaz.png + visualizer.fit(X) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data It is important to remember that the "elbow" method does not work well if the data is not very clustered. In this case, you might see a smooth curve and the optimal value of :math:`K` will be unclear. diff --git a/docs/api/cluster/icdm.py b/docs/api/cluster/icdm.py deleted file mode 100644 index 0438a90c2..000000000 --- a/docs/api/cluster/icdm.py +++ /dev/null @@ -1,27 +0,0 @@ -# Clustering Evaluation Imports -from functools import partial - -from sklearn.cluster import KMeans -from sklearn.datasets import make_blobs as sk_make_blobs - -from yellowbrick.cluster import InterclusterDistance - -# Helpers for easy dataset creation -N_SAMPLES = 1000 -N_FEATURES = 12 -SHUFFLE = True - -# Make blobs partial -make_blobs = partial(sk_make_blobs, n_samples=N_SAMPLES, n_features=N_FEATURES, shuffle=SHUFFLE) - - -if __name__ == '__main__': - # Make 8 blobs dataset - X, y = make_blobs(centers=12) - - # Instantiate the clustering model and visualizer - # Instantiate the clustering model and visualizer - visualizer = InterclusterDistance(KMeans(9)) - - visualizer.fit(X) # Fit the training data to the visualizer - visualizer.poof(outpath="images/icdm.png") # Draw/show/poof the data diff --git a/docs/api/cluster/icdm.rst b/docs/api/cluster/icdm.rst index 42af8e868..a7c1a70dd 100644 --- a/docs/api/cluster/icdm.rst +++ b/docs/api/cluster/icdm.rst @@ -5,26 +5,24 @@ Intercluster Distance Maps Intercluster distance maps display an embedding of the cluster centers in 2 dimensions with the distance to other centers preserved. E.g. the closer to centers are in the visualization, the closer they are in the original feature space. The clusters are sized according to a scoring metric. By default, they are sized by membership, e.g. the number of instances that belong to each center. This gives a sense of the relative importance of clusters. Note however, that because two clusters overlap in the 2D space, it does not imply that they overlap in the original feature space. -.. code:: python +.. plot:: + :context: close-figs + :alt: Intercluster Distance Visualizer on dataset with 12 random clusters + from sklearn.cluster import KMeans from sklearn.datasets import make_blobs - # Make 12 blobs dataset - X, y = make_blobs(centers=12, n_samples=1000, n_features=16, shuffle=True) - -.. code:: python - - from sklearn.cluster import KMeans from yellowbrick.cluster import InterclusterDistance - # Instantiate the clustering model and visualizer - visualizer = InterclusterDistance(KMeans(9)) - - visualizer.fit(X) # Fit the training data to the visualizer - visualizer.poof() # Draw/show/poof the data + # Generate synthetic dataset with 12 random clusters + X, y = make_blobs(n_samples=1000, n_features=12, centers=12, random_state=42) + # Instantiate the clustering model and visualizer + model = KMeans(6) + visualizer = InterclusterDistance(model) -.. image:: images/icdm.png + visualizer.fit(X) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data API Reference ------------- diff --git a/docs/api/cluster/images/calinski_harabaz.png b/docs/api/cluster/images/calinski_harabaz.png deleted file mode 100644 index 6cddfeeea..000000000 Binary files a/docs/api/cluster/images/calinski_harabaz.png and /dev/null differ diff --git a/docs/api/cluster/images/elbow.png b/docs/api/cluster/images/elbow.png deleted file mode 100644 index decf30c5f..000000000 Binary files a/docs/api/cluster/images/elbow.png and /dev/null differ diff --git a/docs/api/cluster/images/icdm.png b/docs/api/cluster/images/icdm.png deleted file mode 100644 index 51c5b5a0d..000000000 Binary files a/docs/api/cluster/images/icdm.png and /dev/null differ diff --git a/docs/api/cluster/images/silhouette.png b/docs/api/cluster/images/silhouette.png deleted file mode 100644 index b2d5d538b..000000000 Binary files a/docs/api/cluster/images/silhouette.png and /dev/null differ diff --git a/docs/api/cluster/silhouette.py b/docs/api/cluster/silhouette.py deleted file mode 100644 index c62f31751..000000000 --- a/docs/api/cluster/silhouette.py +++ /dev/null @@ -1,27 +0,0 @@ -# Clustering Evaluation Imports -from functools import partial - -from sklearn.cluster import MiniBatchKMeans -from sklearn.datasets import make_blobs as sk_make_blobs - -from yellowbrick.cluster import SilhouetteVisualizer - -# Helpers for easy dataset creation -N_SAMPLES = 1000 -N_FEATURES = 12 -SHUFFLE = True - -# Make blobs partial -make_blobs = partial(sk_make_blobs, n_samples=N_SAMPLES, n_features=N_FEATURES, shuffle=SHUFFLE) - - -if __name__ == '__main__': - # Make 8 blobs dataset - X, y = make_blobs(centers=8) - - # Instantiate the clustering model and visualizer - model = MiniBatchKMeans(6) - visualizer = SilhouetteVisualizer(model) - - visualizer.fit(X) # Fit the training data to the visualizer - visualizer.poof(outpath="images/silhouette.png") # Draw/show/poof the data diff --git a/docs/api/cluster/silhouette.rst b/docs/api/cluster/silhouette.rst index feee9af63..bfe237329 100644 --- a/docs/api/cluster/silhouette.rst +++ b/docs/api/cluster/silhouette.rst @@ -7,29 +7,28 @@ The Silhouette Coefficient is used when the ground-truth about the dataset is un The Silhouette Visualizer displays the silhouette coefficient for each sample on a per-cluster basis, visualizing which clusters are dense and which are not. This is particularly useful for determining cluster imbalance, or for selecting a value for :math:`K` by comparing multiple visualizers. -.. code:: python +.. plot:: + :context: close-figs + :alt: SilhouetteVisualizer on the nfl dataset with 4 clusters - from sklearn.datasets import make_blobs - - # Make 8 blobs dataset - X, y = make_blobs(centers=8) - -.. code:: python - - from sklearn.cluster import MiniBatchKMeans + from sklearn.cluster import KMeans from yellowbrick.cluster import SilhouetteVisualizer + from yellowbrick.datasets import load_nfl - # Instantiate the clustering model and visualizer - model = MiniBatchKMeans(6) - visualizer = SilhouetteVisualizer(model) - - visualizer.fit(X) # Fit the training data to the visualizer - visualizer.poof() # Draw/show/poof the data + # Load a clustering dataset + X, y = load_nfl() + # Specify the features to use for clustering + features = ['Rec', 'Yds', 'TD', 'Fmb', 'Ctch_Rate'] + X = X.query('Tgt >= 20')[features] -.. image:: images/silhouette.png + # Instantiate the clustering model and visualizer + model = KMeans(5, random_state=42) + visualizer = SilhouetteVisualizer(model, colors='yellowbrick') + visualizer.fit(X) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data API Reference diff --git a/docs/api/contrib/boundaries.py b/docs/api/contrib/boundaries.py deleted file mode 100644 index 4cd5ac823..000000000 --- a/docs/api/contrib/boundaries.py +++ /dev/null @@ -1,32 +0,0 @@ -import numpy as np - -from sklearn.model_selection import train_test_split -from sklearn.preprocessing import StandardScaler -from sklearn.datasets import make_moons, make_classification -from sklearn.neighbors import KNeighborsClassifier -from sklearn.svm import SVC - -from yellowbrick.contrib.classifier import DecisionViz - -X, y = make_classification(n_features=2, n_redundant=0, n_informative=2, - random_state=1, n_clusters_per_class=1) - -rng = np.random.RandomState(2) -X += 2 * rng.uniform(size=X.shape) -linearly_separable = (X, y) - -data_set = make_moons(noise=0.3, random_state=0) - -X, y = data_set -X = StandardScaler().fit_transform(X) -X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=42) - -viz = DecisionViz(KNeighborsClassifier(3), title="Nearest Neighbors", features=['Feature One', 'Feature Two'], classes=['A', 'B']) -viz.fit(X_train, y_train) -viz.draw(X_test, y_test) -viz.poof(outpath="images/knn_decisionviz.png") - -viz = DecisionViz(SVC(kernel="linear", C=0.025), title="Linear SVM", features=['Feature One', 'Feature Two'], classes=['A', 'B']) -viz.fit(X_train, y_train) -viz.draw(X_test, y_test) -viz.poof(outpath="images/svc_decisionviz.png") diff --git a/docs/api/contrib/boundaries.rst b/docs/api/contrib/boundaries.rst index 2dbc68dc3..21d677033 100644 --- a/docs/api/contrib/boundaries.rst +++ b/docs/api/contrib/boundaries.rst @@ -5,37 +5,56 @@ DecisionBoundaries Vizualizer The DecisionBoundariesVisualizer is a bivariate data visualization algorithm that plots the decision boundaries of each class. -.. code:: python +.. plot:: + :context: close-figs + :alt: DecisionBoundariesVisualizer Nearest Neighbors - from sklearn.model_selection import train_test_split + from sklearn.model_selection import train_test_split as tts from sklearn.preprocessing import StandardScaler - from sklearn.datasets import make_moons, make_classification - - # Create dummy data - X, y = make_classification(n_features=2, n_redundant=0, n_informative=2, - random_state=1, n_clusters_per_class=1) - - rng = np.random.RandomState(2) - X += 2 * rng.uniform(size=X.shape) - linearly_separable = (X, y) + from sklearn.datasets import make_moons + from sklearn.neighbors import KNeighborsClassifier + from yellowbrick.contrib.classifier import DecisionViz data_set = make_moons(noise=0.3, random_state=0) X, y = data_set X = StandardScaler().fit_transform(X) - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.4, random_state=42) + X_train, X_test, y_train, y_test = tts(X, y, test_size=.4, random_state=42) + + viz = DecisionViz( + KNeighborsClassifier(3), title="Nearest Neighbors", + features=['Feature One', 'Feature Two'], classes=['A', 'B'] + ) + viz.fit(X_train, y_train) + viz.draw(X_test, y_test) + viz.poof() + -.. code:: python +.. plot:: + :context: close-figs + :alt: DecisionBoundariesVisualizer Linear SVM + from sklearn.svm import SVC + from sklearn.model_selection import train_test_split as tts + from sklearn.preprocessing import StandardScaler + from sklearn.datasets import make_moons from sklearn.neighbors import KNeighborsClassifier from yellowbrick.contrib.classifier import DecisionViz - viz = DecisionViz(KNeighborsClassifier(3), title="Nearest Neighbors", features=['Feature One', 'Feature Two'], classes=['A', 'B']) + data_set = make_moons(noise=0.3, random_state=0) + + X, y = data_set + X = StandardScaler().fit_transform(X) + X_train, X_test, y_train, y_test = tts(X, y, test_size=.4, random_state=42) + + + viz = DecisionViz( + SVC(kernel="linear", C=0.025), title="Linear SVM", + features=['Feature One', 'Feature Two'], classes=['A', 'B'] + ) viz.fit(X_train, y_train) viz.draw(X_test, y_test) - viz.poof(outpath="images/knn_decisionviz.png") - -.. image:: images/knn_decisionviz.png + viz.poof() API Reference diff --git a/docs/api/contrib/images/knn_decisionviz.png b/docs/api/contrib/images/knn_decisionviz.png deleted file mode 100644 index 94ab3d0e0..000000000 Binary files a/docs/api/contrib/images/knn_decisionviz.png and /dev/null differ diff --git a/docs/api/contrib/images/scatter.png b/docs/api/contrib/images/scatter.png deleted file mode 100644 index b71e17e9b..000000000 Binary files a/docs/api/contrib/images/scatter.png and /dev/null differ diff --git a/docs/api/contrib/images/svc_decisionviz.png b/docs/api/contrib/images/svc_decisionviz.png deleted file mode 100644 index b01cb867f..000000000 Binary files a/docs/api/contrib/images/svc_decisionviz.png and /dev/null differ diff --git a/docs/api/contrib/missing/bar.py b/docs/api/contrib/missing/bar.py deleted file mode 100644 index 51695b844..000000000 --- a/docs/api/contrib/missing/bar.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np -from sklearn.datasets import make_classification - -# Create dummy data -X, y = make_classification( - n_samples=400, n_features=10, n_informative=2, n_redundant=3, - n_classes=2, n_clusters_per_class=2, random_state=854 - ) - -# assign some NaN values -X[X > 1.5] = np.nan -features = ["Feature {}".format(str(n)) for n in range(10)] - -from yellowbrick.contrib.missing import MissingValuesBar - -viz = MissingValuesBar(features=features) -viz.fit(X) -viz.poof(outpath="images/missingbar.png") - - -viz = MissingValuesBar(features=features) -viz.fit(X, y=y) -viz.poof(outpath="images/missingbar_with_targets.png") diff --git a/docs/api/contrib/missing/bar.rst b/docs/api/contrib/missing/bar.rst index 1a77ea633..dd876f33e 100644 --- a/docs/api/contrib/missing/bar.rst +++ b/docs/api/contrib/missing/bar.rst @@ -1,54 +1,67 @@ .. -*- mode: rst -*- MissingValues Bar -============================= +================= -The MissingValues Bar visualizer creates a bar graph that counts the number of missing values per feature column. +The MissingValues Bar visualizer creates a bar graph that counts the number of missing values per feature column. If the target ``y`` is supplied to fit, a stacked bar chart is produced. -If the target y is supplied to fit, then produces a stacked bar chart. -**Setup** +Without Targets Supplied +------------------------ -.. code:: python +.. plot:: + :context: close-figs + :alt: MissingValues Bar visualization on a dataset with no targets supplied import numpy as np + from sklearn.datasets import make_classification + from yellowbrick.contrib.missing import MissingValuesBar + # Make a classification dataset X, y = make_classification( - n_samples=400, n_features=10, n_informative=2, n_redundant=3, - n_classes=2, n_clusters_per_class=2, random_state=854 - ) - # assign some NaN values + n_samples=400, n_features=10, n_informative=2, n_redundant=3, + n_classes=2, n_clusters_per_class=2, random_state=854 + ) + + # Assign NaN values X[X > 1.5] = np.nan features = ["Feature {}".format(str(n)) for n in range(10)] -------------------------------------------- -Without Targets Supplied -------------------------------------------- - -.. code:: python + # Instantiate the visualizer + visualizer = MissingValuesBar(features=features) - from yellowbrick.contrib.missing import MissingValuesBar + visualizer.fit(X) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data - viz = MissingValuesBar(features=features) - viz.fit(X) - viz.poof() -.. image:: images/missingbar.png +With Targets (``y``) Supplied +----------------------------- -------------------------------------------- -With Targets (y) Supplied -------------------------------------------- +.. plot:: + :context: close-figs + :alt: MissingValuesBar visualization on a dataset with targets supplied -.. code:: python + import numpy as np + from sklearn.datasets import make_classification from yellowbrick.contrib.missing import MissingValuesBar - viz = MissingValuesBar(features=features) - viz.fit(X, y=y) # supply the targets via y - viz.poof() + # Make a classification dataset + X, y = make_classification( + n_samples=400, n_features=10, n_informative=2, n_redundant=3, + n_classes=2, n_clusters_per_class=2, random_state=854 + ) + + # Assign NaN values + X[X > 1.5] = np.nan + features = ["Feature {}".format(str(n)) for n in range(10)] + + # Instantiate the visualizer + visualizer = MissingValuesBar(features=features) -.. image:: images/missingbar_with_targets.png + visualizer.fit(X, y=y) # Supply the targets via y + visualizer.poof() # Draw/show/poof the data API Reference diff --git a/docs/api/contrib/missing/dispersion.py b/docs/api/contrib/missing/dispersion.py deleted file mode 100644 index e09cdb6dd..000000000 --- a/docs/api/contrib/missing/dispersion.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np -from sklearn.datasets import make_classification - -# Create dummy data -X, y = make_classification( - n_samples=400, n_features=10, n_informative=2, n_redundant=3, - n_classes=2, n_clusters_per_class=2, random_state=854 - ) - -# assign some NaN values -X[X > 1.5] = np.nan -features = ["Feature {}".format(str(n)) for n in range(10)] - -from yellowbrick.contrib.missing import MissingValuesDispersion - -viz = MissingValuesDispersion(features=features) -viz.fit(X) -viz.poof(outpath="images/missingdispersion.png") - - -viz = MissingValuesDispersion(features=features) -viz.fit(X, y=y) -viz.poof(outpath="images/missingdispersion_with_targets.png") diff --git a/docs/api/contrib/missing/dispersion.rst b/docs/api/contrib/missing/dispersion.rst index 3c36e6139..791c2006e 100644 --- a/docs/api/contrib/missing/dispersion.rst +++ b/docs/api/contrib/missing/dispersion.rst @@ -1,54 +1,64 @@ .. -*- mode: rst -*- MissingValues Dispersion -============================= +======================== The MissingValues Dispersion visualizer creates a chart that maps the position of missing values by the order of the index. -**Setup** -.. code:: python +Without Targets Supplied +------------------------ + +.. plot:: + :context: close-figs + :alt: MissingValues Dispersion visualization on a dataset with no targets supplied import numpy as np + from sklearn.datasets import make_classification + from yellowbrick.contrib.missing import MissingValuesDispersion X, y = make_classification( - n_samples=400, n_features=10, n_informative=2, n_redundant=3, - n_classes=2, n_clusters_per_class=2, random_state=854 - ) + n_samples=400, n_features=10, n_informative=2, n_redundant=3, + n_classes=2, n_clusters_per_class=2, random_state=854 + ) + # assign some NaN values X[X > 1.5] = np.nan features = ["Feature {}".format(str(n)) for n in range(10)] -------------------------------------------- -Without Targets Supplied -------------------------------------------- - -.. code:: python - - from yellowbrick.contrib.missing import MissingValuesDispersion + visualizer = MissingValuesDispersion(features=features) - viz = MissingValuesDispersion(features=features) - viz.fit(X) - viz.poof() + visualizer.fit(X) + visualizer.poof() -.. image:: images/missingdispersion.png -------------------------------------------- With Targets (y) Supplied -------------------------------------------- +------------------------- + +.. plot:: + :context: close-figs + :alt: MissingValues Dispersion visualization on a dataset with no targets supplied -.. code:: python + import numpy as np + from sklearn.datasets import make_classification from yellowbrick.contrib.missing import MissingValuesDispersion - viz = MissingValuesDispersion(features=features) - viz.fit(X, y=y) # supply the targets via y - viz.poof() + X, y = make_classification( + n_samples=400, n_features=10, n_informative=2, n_redundant=3, + n_classes=2, n_clusters_per_class=2, random_state=854 + ) -.. image:: images/missingdispersion_with_targets.png + # assign some NaN values + X[X > 1.5] = np.nan + features = ["Feature {}".format(str(n)) for n in range(10)] + # Instantiate the visualizer + visualizer = MissingValuesDispersion(features=features) + visualizer.fit(X, y=y) # supply the targets via y + visualizer.poof() API Reference diff --git a/docs/api/contrib/missing/images/missingbar.png b/docs/api/contrib/missing/images/missingbar.png deleted file mode 100644 index b4cb4b7ff..000000000 Binary files a/docs/api/contrib/missing/images/missingbar.png and /dev/null differ diff --git a/docs/api/contrib/missing/images/missingbar_with_targets.png b/docs/api/contrib/missing/images/missingbar_with_targets.png deleted file mode 100644 index 9b8af9eb5..000000000 Binary files a/docs/api/contrib/missing/images/missingbar_with_targets.png and /dev/null differ diff --git a/docs/api/contrib/missing/images/missingdispersion.png b/docs/api/contrib/missing/images/missingdispersion.png deleted file mode 100644 index 662729b5b..000000000 Binary files a/docs/api/contrib/missing/images/missingdispersion.png and /dev/null differ diff --git a/docs/api/contrib/missing/images/missingdispersion_with_targets.png b/docs/api/contrib/missing/images/missingdispersion_with_targets.png deleted file mode 100644 index 0729760d7..000000000 Binary files a/docs/api/contrib/missing/images/missingdispersion_with_targets.png and /dev/null differ diff --git a/docs/api/contrib/scatter.py b/docs/api/contrib/scatter.py deleted file mode 100644 index 63cc234e6..000000000 --- a/docs/api/contrib/scatter.py +++ /dev/null @@ -1,34 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -from yellowbrick.contrib.scatter import ScatterVisualizer - - -def scatter(data, target, outpath, **kwargs): - # Create a new figure and axes - _, ax = plt.subplots() - - # Create the visualizer - visualizer = ScatterVisualizer(ax=ax, **kwargs) - visualizer.fit(data, target) - visualizer.transform(data) - - # Save to disk - visualizer.poof(outpath=outpath) - print(outpath) - - -if __name__ == '__main__': - # Load the classification data set - data = pd.read_csv("../../../examples/data/occupancy/occupancy.csv") - - # Specify the features of interest and the classes of the target - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - classes = ['unoccupied', 'occupied'] - - # Extract the numpy arrays from the data frame - X = data[features] - y = data.occupancy - - # Draw the scatter visualizer - scatter(X, y, "images/scatter.png", x='light', y='C02', classes=classes) diff --git a/docs/api/contrib/scatter.rst b/docs/api/contrib/scatter.rst index 58e4f08fc..4ca7a4c27 100644 --- a/docs/api/contrib/scatter.rst +++ b/docs/api/contrib/scatter.rst @@ -7,31 +7,25 @@ Sometimes for feature analysis you simply need a scatter plot to determine the d A scatter visualizer simply plots two features against each other and colors the points according to the target. This can be useful in assessing the relationship of pairs of features to an individual target. -.. code:: python +.. plot:: + :context: close-figs + :alt: ScatterVisualizer on occupancy dataset - # Load the classification data set - data = load_data("occupancy") + from yellowbrick.contrib import ScatterVisualizer + from yellowbrick.datasets import load_occupancy - # Specify the features of interest and the classes of the target - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - classes = ["unoccupied", "occupied"] - - # Extract the numpy arrays from the data frame - X = data[features] - y = data.occupancy - -.. code:: python + # Load the classification dataset + X, y = load_occupancy() - from yellowbrick.contrib.scatter import ScatterVisualizer - - visualizer = ScatterVisualizer(x="light", y="C02", classes=classes) - - visualizer.fit(X, y) - visualizer.transform(X) - visualizer.poof() + # Specify the target classes + classes = ["unoccupied", "occupied"] + # Instantiate the visualizer + visualizer = ScatterVisualizer(x="light", y="CO2", classes=classes) -.. image:: images/scatter.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.transform(X) # Transform the data + visualizer.poof() # Draw/show/poof the data API Reference diff --git a/docs/api/datasets.rst b/docs/api/datasets.rst deleted file mode 100644 index b8e4a0618..000000000 --- a/docs/api/datasets.rst +++ /dev/null @@ -1,81 +0,0 @@ -.. -*- mode: rst -*- - -Example Datasets -================ - -Yellowbrick hosts several datasets wrangled from the `UCI Machine -Learning Repository `__ to present the -examples used throughout this documentation. If you haven't downloaded the data, you can do so by -running: - -:: - - $ python -m yellowbrick.download - -This should create a folder named ``data`` in your current working directory that contains all of the datasets. You can load a specified dataset with ``pandas.read_csv`` as follows: - -.. code:: python - - import pandas as pd - - data = pd.read_csv('data/concrete/concrete.csv') - -The following code snippet can be found at the top of the ``examples/examples.ipynb`` notebook in Yellowbrick. Please reference this code when trying to load a specific data set: - -.. code:: python - - import os - - from yellowbrick.download import download_all - - ## The path to the test data sets - FIXTURES = os.path.join(os.getcwd(), "data") - - ## Dataset loading mechanisms - datasets = { - "bikeshare": os.path.join(FIXTURES, "bikeshare", "bikeshare.csv"), - "concrete": os.path.join(FIXTURES, "concrete", "concrete.csv"), - "credit": os.path.join(FIXTURES, "credit", "credit.csv"), - "energy": os.path.join(FIXTURES, "energy", "energy.csv"), - "game": os.path.join(FIXTURES, "game", "game.csv"), - "mushroom": os.path.join(FIXTURES, "mushroom", "mushroom.csv"), - "occupancy": os.path.join(FIXTURES, "occupancy", "occupancy.csv"), - "spam": os.path.join(FIXTURES, "spam", "spam.csv"), - } - - - def load_data(name, download=True): - """ - Loads and wrangles the passed in dataset by name. - If download is specified, this method will download any missing files. - """ - - # Get the path from the datasets - path = datasets[name] - - # Check if the data exists, otherwise download or raise - if not os.path.exists(path): - if download: - download_all() - else: - raise ValueError(( - "'{}' dataset has not been downloaded, " - "use the download.py module to fetch datasets" - ).format(name)) - - - # Return the data frame - return pd.read_csv(path) - - -Unless otherwise specified, most of the examples currently use one or more of the listed datasets. Each dataset has a ``README.md`` with detailed information about the data source, attributes, and target. Here is a complete listing of all datasets in Yellowbrick and their associated analytical tasks: - -- **bikeshare**: suitable for regression -- **concrete**: suitable for regression -- **credit**: suitable for classification/clustering -- **energy**: suitable for regression -- **game**: suitable for classification -- **hobbies**: suitable for text analysis -- **mushroom**: suitable for classification/clustering -- **occupancy**: suitable for classification -- **spam**: suitable for binary classification diff --git a/docs/api/datasets/bikeshare.rst b/docs/api/datasets/bikeshare.rst new file mode 100644 index 000000000..1686ce52a --- /dev/null +++ b/docs/api/datasets/bikeshare.rst @@ -0,0 +1,33 @@ +.. -*- mode: rst -*- + +Bikeshare +========= + +This dataset contains the hourly and daily count of rental bikes between years 2011 and 2012 in Capital bikeshare system with the corresponding weather and seasonal information. + +================= =============== +Samples total 17379 +Dimensionality 12 +Features real, positive +Targets ints, 1-977 +Task(s) regression +================= =============== + +Description +----------- + +Bike sharing systems are new generation of traditional bike rentals where whole process from membership, rental and return back has become automatic. Through these systems, user is able to easily rent a bike from a particular position and return back at another position. Currently, there are about over 500 bike-sharing programs around the world which is composed of over 500 thousands bicycles. Today, there exists great interest in these systems due to their important role in traffic, environmental and health issues. + +Apart from interesting real world applications of bike sharing systems, the characteristics of data being generated by these systems make them attractive for the research. Opposed to other transport services such as bus or subway, the duration of travel, departure and arrival position is explicitly recorded in these systems. This feature turns bike sharing system into a virtual sensor network that can be used for sensing mobility in the city. Hence, it is expected that most of important events in the city could be detected via monitoring these data. + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ on May 4, 2017. + +Fanaee-T, Hadi, and Gama, Joao, 'Event labeling combining ensemble detectors and background knowledge', Progress in Artificial Intelligence (2013): pp. 1-15, Springer Berlin Heidelberg + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_bikeshare diff --git a/docs/api/datasets/concrete.rst b/docs/api/datasets/concrete.rst new file mode 100644 index 000000000..4ea4771e8 --- /dev/null +++ b/docs/api/datasets/concrete.rst @@ -0,0 +1,31 @@ +.. -*- mode: rst -*- + +Concrete +======== + +Concrete is the most important material in civil engineering. The concrete compressive strength is a highly nonlinear function of age and ingredients. + +================= =============== +Samples total 1030 +Dimensionality 9 +Features real +Targets float, 2.3-82.6 +Task(s) regression +================= =============== + +Description +----------- + +Given are the variable name, variable type, the measurement unit and a brief description. The concrete compressive strength is the regression problem. The order of this listing corresponds to the order of numerals along the rows of the database. + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ on October 13, 2016. + +Yeh, I-C. "Modeling of strength of high-performance concrete using artificial neural networks." Cement and Concrete research 28.12 (1998): 1797-1808. + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_concrete diff --git a/docs/api/datasets/credit.rst b/docs/api/datasets/credit.rst new file mode 100644 index 000000000..8b609a7c8 --- /dev/null +++ b/docs/api/datasets/credit.rst @@ -0,0 +1,31 @@ +.. -*- mode: rst -*- + +Credit +====== + +This research aimed at the case of customers' default payments in Taiwan and compares the predictive accuracy of probability of default among six data mining methods. + +================= =============== +Samples total 30000 +Dimensionality 24 +Features real, int +Targets int, 0 or 1 +Task(s) classification +================= =============== + +Description +----------- + +This research aimed at the case of customers' default payments in Taiwan and compares the predictive accuracy of probability of default among six data mining methods. From the perspective of risk management, the result of predictive accuracy of the estimated probability of default will be more valuable than the binary result of classification - credible or not credible clients. Because the real probability of default is unknown, this study presented the novel "Sorting Smoothing Method" to estimate the real probability of default. With the real probability of default as the response variable (Y), and the predictive probability of default as the independent variable (X), the simple linear regression result (Y = A + BX) shows that the forecasting model produced by artificial neural network has the highest coefficient of determination; its regression intercept (A) is close to zero, and regression coefficient (B) to one. Therefore, among the six data mining techniques, artificial neural network is the only one that can accurately estimate the real probability of default. + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ on October 13, 2016. + +Yeh, I. C., & Lien, C. H. (2009). The comparisons of data mining techniques for the predictive accuracy of probability of default of credit card clients. Expert Systems with Applications, 36(2), 2473-2480. + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_credit diff --git a/docs/api/datasets/energy.rst b/docs/api/datasets/energy.rst new file mode 100644 index 000000000..06fb40475 --- /dev/null +++ b/docs/api/datasets/energy.rst @@ -0,0 +1,70 @@ +.. -*- mode: rst -*- + +Energy +====== + +The dataset was created by Angeliki Xifara (angxifara '@' gmail.com, Civil/Structural Engineer) and was processed by Athanasios Tsanas (tsanasthanasis '@' gmail.com, Oxford Centre for Industrial and Applied Mathematics, University of Oxford, UK). + +================= ========================== +Samples total 768 +Dimensionality 8 +Features real, int +Targets float, 6.01-43.1 +Task(s) regression, classification +================= ========================== + +Description +----------- + +We perform energy analysis using 12 different building shapes simulated in Ecotect. The buildings differ with respect to the glazing area, the glazing area distribution, and the orientation, amongst other parameters. We simulate various settings as functions of the afore-mentioned characteristics to obtain 768 building shapes. The dataset comprises 768 samples and 8 features, aiming to predict two real valued responses. It can also be used as a multi-class classification problem if the response is rounded to the nearest integer. + +Example +------- + +The energy dataset contains a multi-target supervised dataset for both the heating and the cooling load of buildings. By default only the heating load is returned for most examples. To perform a multi-target regression, simply access the dataframe and select both the heating and cooling load columns as follows: + +.. code:: python + + from yellowbrick.datasets import load_energy + from sklearn.ensemble import RandomForestRegressor + from sklearn.model_selection import train_test_split as tts + + features = [ + "relative compactness", + "surface area", + "wall area", + "roof area", + "overall height", + "orientation", + "glazing area", + "glazing area distribution", + ] + target = ["heating load", "cooling load"] + + df = load_energy(return_dataset=True).to_dataframe() + X, y = df[features], df[target] + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2) + + model = RandomForestRegressor().fit(X_train, y_train) + model.score(X_test, y_test) + + +Note that not all regressors support multi-target regression, one simple strategy in this case is to use a :class:`sklearn.multioutput.MultiOutputRegressor`, which fits an estimator for each target. + + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ March 23, 2015. + +A. Tsanas, A. Xifara: 'Accurate quantitative estimation of energy performance of residential buildings using statistical machine learning tools', Energy and Buildings, Vol. 49, pp. 560-567, 2012 + +For further details on the data analysis methodology: + +A. Tsanas, 'Accurate telemonitoring of Parkinson's disease symptom severity using nonlinear speech signal processing and statistical machine learning', D.Phil. thesis, University of Oxford, 2012 + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_energy diff --git a/docs/api/datasets/game.rst b/docs/api/datasets/game.rst new file mode 100644 index 000000000..6c4388625 --- /dev/null +++ b/docs/api/datasets/game.rst @@ -0,0 +1,45 @@ +.. -*- mode: rst -*- + +Game +==== + +The dataset was created and donated to the UCI ML Repository by John Tromp (tromp '@' cwi.nl). + +================= ============================== +Samples total 67557 +Dimensionality 42 +Features categorical +Targets str: {"win", "loss", "draw"} +Task(s) classification +================= ============================== + +Description +----------- + +This database contains all legal 8-ply positions in the game of connect-4 in which neither player has won yet, and in which the next move is not forced. + +The symbol x represents the first player; o the second. The dataset contains the state of the game by representing each position in a 6x7 grid board. The outcome class is the game theoretical value for the first player. + +Example +------- + +Note that to use the game dataset the categorical data in the features array must be encoded numerically. There are a number of numeric encoding mechanisms such as the :class:`sklearn.preprocessing.OrdinalEncoder` or the :class:`sklearn.preprocessing.OneHotEncoder` that may be used as follows: + +.. code:: python + + from sklearn.preprocessing import OneHotEncoder + from yellowbrick.datasets import load_game + + X, y = load_game() + X = OneHotEncoder().fit_transform(X) + + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ on May 4, 2017. + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_game diff --git a/docs/api/datasets/hobbies.rst b/docs/api/datasets/hobbies.rst new file mode 100644 index 000000000..4c1db2de1 --- /dev/null +++ b/docs/api/datasets/hobbies.rst @@ -0,0 +1,92 @@ +.. -*- mode: rst -*- + +Hobbies +======= + +The Baleen hobbies corpus contains 448 files in 5 categories. + +================= ========================================================== +Samples total 448 +Dimensionality 23738 +Features strings (tokens) +Targets str: {"books", "cinema", "cooking", "gaming", "sports"} +Task(s) classification, clustering +================= ========================================================== + +Description +----------- + +The hobbies corpus is a text corpus wrangled from the `Baleen RSS Corpus `_ in order to enable students and readers to practice different techniques in Natural Language Processing. For more information see `Applied Text Analysis with Python: Enabling Language-Aware Data Products with Machine Learning `_ and the associated `code repository `_. It is structured as: + +Documents and File Size +~~~~~~~~~~~~~~~~~~~~~~~ + +- books: 72 docs (4.1MiB) +- cinema: 100 docs (9.2MiB) +- cooking: 30 docs (3.0MiB) +- gaming: 128 docs (8.8MiB) +- sports: 118 docs (15.9MiB) + +Document Structure +~~~~~~~~~~~~~~~~~~ + +Overall: + +- 7,420 paragraphs (16.562 mean paragraphs per file) +- 14,251 sentences (1.921 mean sentences per paragraph). + +By Category: + +- books: 844 paragraphs and 2,030 sentences +- cinema: 1,475 paragraphs and 3,047 sentences +- cooking: 1,190 paragraphs and 2,425 sentences +- gaming: 1,802 paragraphs and 3,373 sentences +- sports: 2,109 paragraphs and 3,376 sentences + +Words and Vocabulary +~~~~~~~~~~~~~~~~~~~~ + +Word count of 288,520 with a vocabulary of 23,738 (12.154 lexical diversity). + +- books: 41,851 words with a vocabulary size of 7,838 +- cinema: 69,153 words with a vocabulary size of 10,274 +- cooking: 37,854 words with a vocabulary size of 5,038 +- gaming: 70,778 words with a vocabulary size of 9,120 +- sports: 68,884 words with a vocabulary size of 8,028 + +Example +------- + +The hobbies corpus loader returns a ``Corpus`` object with the raw text associated with the data set. This must be vectorized into a numeric form for use with scikit-learn. For example, you could use the :class:`sklearn.feature_extraction.text.TfidfVectorizer` as follows: + +.. code:: python + + from yellowbrick.datasets import load_hobbies + + from sklearn.naive_bayes import MultinomialNB + from sklearn.preprocessing import LabelEncoder + from sklearn.feature_extraction.text import TfidfVectorizer + from sklearn.model_selection import train_test_split as tts + + corpus = load_hobbies() + X = TfidfVectorizer().fit_transform(corpus.data) + y = LabelEncoder().fit_transform(corpus.target) + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2) + + model = MultinomialNB().fit(X_train, y_train) + model.score(X_test, y_test) + +For more detail on text analytics and machine learning with scikit-learn, please refer to `"Working with Text Data" `_ in the scikit-learn documentation. + +Citation +-------- + +Exported from S3 on: Jan 21, 2017 at 06:42. + +Bengfort, Benjamin, Rebecca Bilbro, and Tony Ojeda. Applied Text Analysis with Python: Enabling Language-aware Data Products with Machine Learning. " O'Reilly Media, Inc.", 2018. + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_hobbies diff --git a/docs/api/datasets/index.rst b/docs/api/datasets/index.rst new file mode 100644 index 000000000..55c3b67f5 --- /dev/null +++ b/docs/api/datasets/index.rst @@ -0,0 +1,199 @@ +.. -*- mode: rst -*- + +Example Datasets +================ + +Yellowbrick hosts several datasets wrangled from the `UCI Machine Learning Repository `__ to present the examples used throughout this documentation. These datasets are hosted in our CDN and must be downloaded for use. Typically, when a user calls one of the data loader functions, e.g. ``load_bikeshare()`` the data is automatically downloaded if it's not already on the user's computer. However, for development and testing, or if you know you will be working without internet access, it might be easier to simply download all the data at once. + +The data downloader script can be run as follows: + +:: + + $ python -m yellowbrick.download + +This will download all of the data to the ``fixtures`` directory inside of the Yellowbrick site packages. You can specify the location of the download either as an argument to the downloader script (use ``--help`` for more details) or by setting the ``$YELLOWBRICK_DATA`` environment variable. This is the preferred mechanism because this will also influence how data is loaded in Yellowbrick. + +.. NOTE:: Developers who have downloaded data from Yellowbrick versions earlier than v1.0 may experience some problems with the older data format. If this occurs, you can clear out your data cache by running ``python -m yellowbrick.download --cleanup``. This will remove old datasets and download the new ones. You can also use the ``--no-download`` flag to simply clear the cache without re-downloading data. Users who are having difficulty with datasets can also use this or they can uninstall and reinstall Yellowbrick using ``pip``. + +Once you have downloaded the example datasets, you can load and use them as follows: + +.. code:: python + + from yellowbrick.datasets import load_bikeshare + + X, y = load_bikeshare() # returns features and targets for the bikeshare dataset + + +Each dataset has a ``README.md`` with detailed information about the data source, attributes, and target as well as other metadata. To get access to the metadata or to more precisely control your data access you can return the dataset directly from the loader as follows: + +.. code:: python + + dataset = load_bikeshare(return_dataset=True) + print(dataset.README) + + df = dataset.to_dataframe() + df.head() + + +Datasets +-------- + +Unless otherwise specified, most of the documentation examples currently use one or more of the listed datasets. Here is a complete listing of all datasets in Yellowbrick and the analytical tasks with which they are most commonly associated: + +.. Below is a custom ToC, please add new datasets both to the list with a link and + the file containing the dataset information to the toctree directive below. + +- :doc:`bikeshare`: suitable for regression +- :doc:`concrete`: suitable for regression +- :doc:`credit`: suitable for classification/clustering +- :doc:`energy`: suitable for regression +- :doc:`game`: suitable for multi-class classification +- :doc:`hobbies`: suitable for text analysis/classification +- :doc:`mushroom`: suitable for classification/clustering +- :doc:`occupancy`: suitable for classification +- :doc:`spam`: suitable for binary classification +- :doc:`walking`: suitable for time series analysis/clustering +- :doc:`nfl`: suitable for clustering + +.. toctree:: + :hidden: + + bikeshare + concrete + credit + energy + game + hobbies + mushroom + occupancy + spam + walking + nfl + + +Yellowbrick has included these datasets in our package for demonstration purposes only. The datasets have been repackaged with the permission of the authors or in accordance with the terms of use of the source material. If you use a Yellowbrick wrangled dataset, please be sure to cite the original author. + +API Reference +------------- + +By default, the dataset loaders return a features table, ``X``, and a target vector ``y`` when called. If the user has Pandas installed, the data types will be a ``pd.DataFrame`` and ``pd.Series`` respectively, otherwise the data will be returned as numpy arrays. This functionality ensures that the primary use of the datasets, to follow along with the documentation examples, is as simple as possible. However, advanced users may note that there does exist an underlying object with advanced functionality that can be accessed as follows: + +.. code:: python + + dataset = load_occupancy(return_dataset=True) + + +There are two basic types of dataset, the ``Dataset`` which is used for :ref:`tabular data ` loaded from a CSV and the ``Corpus``, used to load :ref:`text corpora ` from disk. Both types of dataset give access to a readme file, a citation in BibTex format, json metadata that describe the fields and target, and different data types associated with the underlying datasset. Both objects are also responsible for locating the dataset on disk and downloading it safely if it doesn't exist yet. For more on how Yellowbrick downloads and stores data, please see :ref:`local-storage`. + +.. _tabular-data: + +Tabular Data +~~~~~~~~~~~~ + +Most example datasets are returned as tabular data structures loaded either from a .csv file (using Pandas) or from dtype encoded .npz file to ensure correct numpy arrays are returned. The ``Dataset`` object loads the data from these stored files, preferring to use Pandas if it is installed. It then uses metadata to slice the DataFrame into a feature matrix and target array. Using the dataset directly provides extra functionality, and can be retrieved as follows: + +.. code:: python + + from yellowbrick.datasets import load_concrete + dataset = load_concrete(return_dataset=True) + +For example if you wish to get the raw data frame you can do so as follows: + +.. code:: python + + df = dataset.to_dataframe() + df.head() + +There may be additional columns in the DataFrame that were part of the original dataset but were excluded from the featureset. For example, the :doc:`energy dataset ` contains two targets, the heating and the cooling load, but only the heating load is returned by default. The api documentation that follows describes in details the metadata properties and other functionality associated with the ``Dataset``: + +.. autoclass:: yellowbrick.datasets.base.Dataset + :show-inheritance: + :members: + :inherited-members: + +.. _text-corpora: + +Text Corpora +~~~~~~~~~~~~ + +Yellowbrick supports many text-specific machine learning visualizations in the :doc:`yellowbrick.text <../text/index>` module. To facilitate these examples and show an end-to-end visual diagnostics workflow that includes text preprocessing, Yellowbrick supports a ``Corpus`` dataset loader that provides access to raw text data from individual documents. Most notably used with the :doc:`hobbies corpus `, a collection of blog posts from different topics that can be used for text classification tasks. + +A text corpus is composed of individual documents that are stored on disk in a directory structure that also identifies document relationships. The file name of each document is a unique file ID (e.g. the MD5 hash of its contents). For example, the hobbies corpus is structured as follows: + +:: + + data/hobbies + ├── README.md + └── books + | ├── 56d62a53c1808113ffb87f1f.txt + | └── 5745a9c7c180810be6efd70b.txt + └── cinema + | ├── 56d629b5c1808113ffb87d8f.txt + | └── 57408e5fc180810be6e574c8.txt + └── cooking + | ├── 56d62b25c1808113ffb8813b.txt + | └── 573f0728c180810be6e2575c.txt + └── gaming + | ├── 56d62654c1808113ffb87938.txt + | └── 574585d7c180810be6ef7ffc.txt + └── sports + ├── 56d62adec1808113ffb88054.txt + └── 56d70f17c180810560aec345.txt + +Unlike the ``Dataset``, corpus dataset loaders do not return ``X`` and ``y`` specially prepared for machine learning. Instead, these loaders return a ``Corpus`` object, which can be used to get a more detailed view of the dataset. For example, to list the unique categories in the corpus, you would access the ``labels`` property as follows: + +.. code:: python + + from yellowbrick.datasets import load_hobbies + + corpus = load_hobbies() + corpus.labels + +Addtionally, you can access the list of the absolute paths of each file, which allows you to read individual documents or to use scikit-learn utilties that read the documents streaming one at a time rather than loading them into memory all at once. + +.. code:: python + + with open(corpus.files[8], 'r') as f: + print(f.read()) + +To get the raw text data and target labels, use the ``data`` and ``target`` properties. + +.. code:: python + + X, y = corpus.data, corpus.target + +For more details on the other metadata properties associated with the ``Corpus``, please refer to the API reference below. For more detail on text analytics and machine learning with scikit-learn, please refer to `"Working with Text Data" `_ in the scikit-learn documentation. + +.. autoclass:: yellowbrick.datasets.base.Corpus + :show-inheritance: + :members: + :inherited-members: + +.. _local-storage: + +Local Storage +~~~~~~~~~~~~~ + +Yellowbrick datasets are stored in a compressed format in the cloud to ensure that the install process is as streamlined and lightweight as possible. When you request a dataset via the loader module, Yellowbrick checks to see if it has been downloaded already, and if not, it downloads it to your local disk. + +By default the dataset is stored, uncompressed, in the ``site-packages`` folder of your Python installation alongside the Yellowbrick code. This means that if you install Yellowbrick in multiple virtual environments, the datasets will be downloaded multiple times in each environment. + +To cleanup downloaded datasets, you may use the download module as a command line tool. Note, however, that this will only cleanup the datasets in the yellowbrick package that is on the ``$PYTHON_PATH`` of the environment you're currently in. + +.. code:: + + $ python -m yellowbrick.download --cleanup --no-download + +Alternatively, because the data is stored in the same directory as the code, you can simply ``pip uninstall yellowbrick`` to cleanup the data. + +A better option may be to use a single dataset directory across all virtual environments. To specify this directory, you must set the ``$YELLOWBRICK_DATA`` environment variable, usually by adding it to your bash profile so it is exported every time you open a terminal window. This will ensure that you have only downloaded the data once. + +.. code:: + + $ export YELLOWBRICK_DATA="~/.yellowbrick" + $ python -m yellowbrick.download -f + $ ls $YELLOWBRICK_DATA + +To identify the location that the Yellowbrick datasets are stored for your installation of Python/Yellowbrick, you can use the ``get_data_home`` function: + +.. autofunction:: yellowbrick.datasets.path.get_data_home diff --git a/docs/api/datasets/mushroom.rst b/docs/api/datasets/mushroom.rst new file mode 100644 index 000000000..5617bc30d --- /dev/null +++ b/docs/api/datasets/mushroom.rst @@ -0,0 +1,37 @@ +.. -*- mode: rst -*- + +Mushroom +======== + +From Audobon Society Field Guide; mushrooms described in terms of physical characteristics; classification: poisonous or edible. + +================= ============================== +Samples total 8124 +Dimensionality 4 (reduced from 22) +Features categorical +Targets str: {"edible", "poisonous"} +Task(s) classification +================= ============================== + +Description +----------- + +This data set includes descriptions of hypothetical samples corresponding to 23 species of gilled mushrooms in the Agaricus and Lepiota Family (pp. 500-525). Each species is identified as definitely edible, definitely poisonous, or of unknown edibility and not recommended. This latter class was combined with the poisonous one. The Guide clearly states that there is no simple rule for determining the edibility of a mushroom; no rule like "leaflets three, let it be" for Poisonous Oak and Ivy. + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ on February 28, 2017. + +Schlimmer, Jeffrey Curtis. "Concept acquisition through representational adjustment." (1987). + +Langley, Pat. "Trading off simplicity and coverage in incremental concept learning." Machine Learning Proceedings 1988 (2014): 73. + +Duch, Włodzisław, Rafał Adamczak, and Krzysztof Grabczewski. "Extraction of logical rules from training data using backpropagation networks." The 1st Online Workshop on Soft Computing. 1996. + +Duch, Wlodzislaw, Rafal Adamczak, and Krzysztof Grabczewski. "Extraction of crisp logical rules using constrained backpropagation networks." (1997). + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_mushroom diff --git a/docs/api/datasets/nfl.rst b/docs/api/datasets/nfl.rst new file mode 100644 index 000000000..c2e032dc2 --- /dev/null +++ b/docs/api/datasets/nfl.rst @@ -0,0 +1,32 @@ +.. -*- mode: rst -*- + +NFL +=== + +This dataset is comprised of statistics on all eligible receivers from the 2018 NFL regular season. + +================= ================== +Samples total 494 +Dimensionality 20 +Features str, int +Targets N/A +Task(s) clustering +================= ================== + +Description +----------- + +The dataset consists of an aggregate of all relevant statistics for eligible receivers that played in at least 1 game and had at least 1 target throughout the season. This is not limited to players specifically designated as wide-receivers, but may include other positions such as running-backs and tight-ends. + +Citation +-------- + +Redistributed with the permission of Sports Reference LLC on June 11, 2019 via email. + +Sports Reference LLC, "2018 NFL Receiving," Pro-Football-Reference.com - Pro Football Statistics and History. +[Online]. Available `here `_. [Accessed: 18-Jun-2019] + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_nfl diff --git a/docs/api/datasets/occupancy.rst b/docs/api/datasets/occupancy.rst new file mode 100644 index 000000000..23977a4f8 --- /dev/null +++ b/docs/api/datasets/occupancy.rst @@ -0,0 +1,29 @@ +.. -*- mode: rst -*- + +Occupancy +========= + +Experimental data used for binary classification (room occupancy) from Temperature, Humidity, Light and CO2. Ground-truth occupancy was obtained from time stamped pictures that were taken every minute. + +================= ========================================== +Samples total 20560 +Dimensionality 6 +Features real, positive +Targets int: {1 for occupied, 0 for not occupied} +Task(s) classification +Samples per class imbalanced +================= ========================================== + +Description +----------- + +Three data sets are submitted, for training and testing. Ground-truth occupancy was obtained from time stamped pictures that were taken every minute. For the journal publication, the processing R scripts can be found on `GitHub `_. + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ on October 13, 2016. + +Candanedo, Luis M., and Véronique Feldheim. "Accurate occupancy detection of an office room from light, temperature, humidity and CO 2 measurements using statistical learning models." Energy and Buildings 112 (2016): 28-39. + +.. autofunction:: yellowbrick.datasets.loaders.load_occupancy diff --git a/docs/api/datasets/spam.rst b/docs/api/datasets/spam.rst new file mode 100644 index 000000000..0661d0614 --- /dev/null +++ b/docs/api/datasets/spam.rst @@ -0,0 +1,38 @@ +.. -*- mode: rst -*- + +Spam +==== + +Classifying Email as Spam or Non-Spam. + +================= ===================================== +Samples total 4601 +Dimensionality 57 +Features real, integer +Targets int: {1 for spam, 0 for not spam} +Task(s) classification +================= ===================================== + + +Description +----------- + +The "spam" concept is diverse: advertisements for products/web sites, make +money fast schemes, chain letters, pornography... + +Our collection of spam e-mails came from our postmaster and individuals who had filed spam. Our collection of non-spam e-mails came from filed work and personal e-mails, and hence the word 'george' and the area code '650' are indicators of non-spam. These are useful when constructing a personalized spam filter. One would either have to blind such non-spam indicators or get a very wide collection of non-spam to generate a general purpose spam filter. + +Determine whether a given email is spam or not. + +~7% misclassification error. False positives (marking good mail as spam) are very undesirable.If we insist on zero false positives in the training/testing set, 20-25% of the spam passed through the filter. + +Citation +-------- +Downloaded from the `UCI Machine Learning Repository `_ on March 23, 2018. + +Cranor, Lorrie Faith, and Brian A. LaMacchia. "Spam!." Communications of the ACM 41.8 (1998): 74-83. + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_spam diff --git a/docs/api/datasets/walking.rst b/docs/api/datasets/walking.rst new file mode 100644 index 000000000..335ec7768 --- /dev/null +++ b/docs/api/datasets/walking.rst @@ -0,0 +1,31 @@ +.. -*- mode: rst -*- + +Walking +======= + +The dataset collects data from an Android smartphone positioned in the chest pocket. Accelerometer Data are collected from 22 participants walking in the wild over a predefined path. The dataset is intended for Activity Recognition research purposes. It provides challenges for identification and authentication of people using motion patterns. Sampling frequency of the accelerometer: DELAY_FASTEST with network connections disabled. + +================= =========================== +Samples total 149331 +Dimensionality 4 +Features real +Targets int, 1-22 +Task(s) classification, clustering +================= =========================== + +Description +----------- + +In this article, a novel technique for user's authentication and verification using gait as a biometric unobtrusive pattern is proposed. The method is based on a two stages pipeline. First, a general activity recognition classifier is personalized for an specific user using a small sample of her/his walking pattern. As a result, the system is much more selective with respect to the new walking pattern. A second stage verifies whether the user is an authorized one or not. This stage is defined as a one-class classification problem. In order to solve this problem, a four-layer architecture is built around the geometric concept of convex hull. This architecture allows to improve robustness to outliers, modeling non-convex shapes, and to take into account temporal coherence information. Two different scenarios are proposed as validation with two different wearable systems. First, a custom high-performance wearable system is built and used in a free environment. A second dataset is acquired from an Android-based commercial device in a 'wild' scenario with rough terrains, adversarial conditions, crowded places and obstacles. Results on both systems and datasets are very promising, reducing the verification error rates by an order of magnitude with respect to the state-of-the-art technologies. + +Citation +-------- + +Downloaded from the `UCI Machine Learning Repository `_ on August 23, 2018. + +Casale, Pierluigi, Oriol Pujol, and Petia Radeva. "Personalization and user verification in wearable systems using biometric walking patterns." Personal and Ubiquitous Computing 16.5 (2012): 563-580. + +Loader +------ + +.. autofunction:: yellowbrick.datasets.loaders.load_walking diff --git a/docs/api/features/images/concrete_isomap_manifold.png b/docs/api/features/images/concrete_isomap_manifold.png index 186ac4e41..124264e9d 100644 Binary files a/docs/api/features/images/concrete_isomap_manifold.png and b/docs/api/features/images/concrete_isomap_manifold.png differ diff --git a/docs/api/features/images/concrete_tsne_manifold.png b/docs/api/features/images/concrete_tsne_manifold.png index f0e3d3bd6..44529db55 100644 Binary files a/docs/api/features/images/concrete_tsne_manifold.png and b/docs/api/features/images/concrete_tsne_manifold.png differ diff --git a/docs/api/features/images/fast_vs_slow_parallel_coordinates.png b/docs/api/features/images/fast_vs_slow_parallel_coordinates.png deleted file mode 100644 index d7d021b7b..000000000 Binary files a/docs/api/features/images/fast_vs_slow_parallel_coordinates.png and /dev/null differ diff --git a/docs/api/features/images/feature_importances.png b/docs/api/features/images/feature_importances.png deleted file mode 100644 index e179794db..000000000 Binary files a/docs/api/features/images/feature_importances.png and /dev/null differ diff --git a/docs/api/features/images/feature_importances_coef.png b/docs/api/features/images/feature_importances_coef.png deleted file mode 100644 index de8af8640..000000000 Binary files a/docs/api/features/images/feature_importances_coef.png and /dev/null differ diff --git a/docs/api/features/images/jointplot.png b/docs/api/features/images/jointplot.png deleted file mode 100644 index 89e116936..000000000 Binary files a/docs/api/features/images/jointplot.png and /dev/null differ diff --git a/docs/api/features/images/jointplot_hex.png b/docs/api/features/images/jointplot_hex.png deleted file mode 100644 index ee5964296..000000000 Binary files a/docs/api/features/images/jointplot_hex.png and /dev/null differ diff --git a/docs/api/features/images/normalized_sampled_parallel_coordinates.png b/docs/api/features/images/normalized_sampled_parallel_coordinates.png deleted file mode 100644 index fbefb8dec..000000000 Binary files a/docs/api/features/images/normalized_sampled_parallel_coordinates.png and /dev/null differ diff --git a/docs/api/features/images/occupancy_select_k_best_isomap_manifold.png b/docs/api/features/images/occupancy_select_k_best_isomap_manifold.png index 62fe1a378..b15feed80 100644 Binary files a/docs/api/features/images/occupancy_select_k_best_isomap_manifold.png and b/docs/api/features/images/occupancy_select_k_best_isomap_manifold.png differ diff --git a/docs/api/features/images/occupancy_tsne_manifold.png b/docs/api/features/images/occupancy_tsne_manifold.png index 4fe31fc8d..ed2f566eb 100644 Binary files a/docs/api/features/images/occupancy_tsne_manifold.png and b/docs/api/features/images/occupancy_tsne_manifold.png differ diff --git a/docs/api/features/images/parallel_coordinates.png b/docs/api/features/images/parallel_coordinates.png deleted file mode 100644 index 459030b96..000000000 Binary files a/docs/api/features/images/parallel_coordinates.png and /dev/null differ diff --git a/docs/api/features/images/pca_biplot_2d.png b/docs/api/features/images/pca_biplot_2d.png deleted file mode 100644 index 594072c2a..000000000 Binary files a/docs/api/features/images/pca_biplot_2d.png and /dev/null differ diff --git a/docs/api/features/images/pca_biplot_3d.png b/docs/api/features/images/pca_biplot_3d.png deleted file mode 100644 index a5c1abc61..000000000 Binary files a/docs/api/features/images/pca_biplot_3d.png and /dev/null differ diff --git a/docs/api/features/images/pca_projection_2d.png b/docs/api/features/images/pca_projection_2d.png deleted file mode 100644 index 71da82205..000000000 Binary files a/docs/api/features/images/pca_projection_2d.png and /dev/null differ diff --git a/docs/api/features/images/pca_projection_3d.png b/docs/api/features/images/pca_projection_3d.png deleted file mode 100644 index 47f22fa8b..000000000 Binary files a/docs/api/features/images/pca_projection_3d.png and /dev/null differ diff --git a/docs/api/features/images/radviz.png b/docs/api/features/images/radviz.png deleted file mode 100644 index 679a6ffce..000000000 Binary files a/docs/api/features/images/radviz.png and /dev/null differ diff --git a/docs/api/features/images/rank1d_shapiro.png b/docs/api/features/images/rank1d_shapiro.png deleted file mode 100644 index 5d7bcb29d..000000000 Binary files a/docs/api/features/images/rank1d_shapiro.png and /dev/null differ diff --git a/docs/api/features/images/rank2d_covariance.png b/docs/api/features/images/rank2d_covariance.png deleted file mode 100644 index d7a3bf57c..000000000 Binary files a/docs/api/features/images/rank2d_covariance.png and /dev/null differ diff --git a/docs/api/features/images/rank2d_pearson.png b/docs/api/features/images/rank2d_pearson.png deleted file mode 100644 index 54916b3e7..000000000 Binary files a/docs/api/features/images/rank2d_pearson.png and /dev/null differ diff --git a/docs/api/features/images/rfecv_credit.png b/docs/api/features/images/rfecv_credit.png deleted file mode 100644 index e324f8d64..000000000 Binary files a/docs/api/features/images/rfecv_credit.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_hessian_manifold.png b/docs/api/features/images/s_curve_hessian_manifold.png deleted file mode 100644 index bc55f5454..000000000 Binary files a/docs/api/features/images/s_curve_hessian_manifold.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_isomap_manifold.png b/docs/api/features/images/s_curve_isomap_manifold.png deleted file mode 100644 index b15f6a9ab..000000000 Binary files a/docs/api/features/images/s_curve_isomap_manifold.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_lle_manifold.png b/docs/api/features/images/s_curve_lle_manifold.png deleted file mode 100644 index f9d271e3f..000000000 Binary files a/docs/api/features/images/s_curve_lle_manifold.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_ltsa_manifold.png b/docs/api/features/images/s_curve_ltsa_manifold.png deleted file mode 100644 index 45aac6f49..000000000 Binary files a/docs/api/features/images/s_curve_ltsa_manifold.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_mds_manifold.png b/docs/api/features/images/s_curve_mds_manifold.png deleted file mode 100644 index f72621153..000000000 Binary files a/docs/api/features/images/s_curve_mds_manifold.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_modified_manifold.png b/docs/api/features/images/s_curve_modified_manifold.png deleted file mode 100644 index 33b1e6f64..000000000 Binary files a/docs/api/features/images/s_curve_modified_manifold.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_spectral_manifold.png b/docs/api/features/images/s_curve_spectral_manifold.png deleted file mode 100644 index 973b45bff..000000000 Binary files a/docs/api/features/images/s_curve_spectral_manifold.png and /dev/null differ diff --git a/docs/api/features/images/s_curve_tsne_manifold.png b/docs/api/features/images/s_curve_tsne_manifold.png deleted file mode 100644 index 9dc07573d..000000000 Binary files a/docs/api/features/images/s_curve_tsne_manifold.png and /dev/null differ diff --git a/docs/api/features/importances.py b/docs/api/features/importances.py deleted file mode 100644 index 34abe033a..000000000 --- a/docs/api/features/importances.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -import pandas as pd -import matplotlib.pyplot as plt - -from yellowbrick.features.importances import FeatureImportances -from sklearn.ensemble import GradientBoostingClassifier -from sklearn.linear_model import Lasso - - -DATA_DIR = os.path.relpath(os.path.join( - os.path.dirname(__file__), "..", "..", "..", "examples", "data" -)) - - -def feature_importances_(outpath): - occupancy = pd.read_csv(os.path.join(DATA_DIR, "occupancy", "occupancy.csv")) - - feats = [ - "temperature", "relative humidity", "light", "C02", "humidity" - ] - - X = occupancy[feats] - y = occupancy['occupancy'].astype(int) - - fig = plt.figure() - ax = fig.add_subplot() - - viz = FeatureImportances(GradientBoostingClassifier(), ax=ax) - viz.fit(X, y) - viz.poof(outpath=outpath) - - -def coef_(outpath): - concrete = pd.read_csv(os.path.join(DATA_DIR, "concrete", "concrete.csv")) - - feats = ['cement','slag','ash','water','splast','coarse','fine','age'] - X = concrete[feats] - y = concrete['strength'] - - fig = plt.figure() - ax = fig.add_subplot() - - feats = list(map(lambda s: s.title(), feats)) - viz = FeatureImportances(Lasso(), ax=ax, labels=feats, relative=False) - viz.fit(X, y) - viz.poof(outpath=outpath) - - -if __name__ == '__main__': - feature_importances_("images/feature_importances.png") - coef_("images/feature_importances_coef.png") diff --git a/docs/api/features/index.rst b/docs/api/features/index.rst index b9b769a75..63879a5fd 100644 --- a/docs/api/features/index.rst +++ b/docs/api/features/index.rst @@ -20,8 +20,6 @@ At the moment we have the following feature analysis visualizers implemented: detect classes or clusters - :doc:`pca`: project higher dimensions into a visual space using PCA - :doc:`manifold`: visualize high dimensional data using manifold learning -- :doc:`importances`: rank features by relative importance in a model -- :doc:`rfecv`: select a subset of features by importance - :doc:`jointplot`: (aka Jointplots) plot 2D correlation between features and target Feature analysis visualizers implement the ``Transformer`` API from @@ -41,8 +39,6 @@ is called which displays the image. from yellowbrick.features.jointplot import JointPlotVisualizer from yellowbrick.features.pca import PCADecomposition from yellowbrick.features.manifold import Manifold - from yellowbrick.features.importances import FeatureImportances - from yellowbrick.features.rfecv import RFECV .. toctree:: @@ -53,6 +49,4 @@ is called which displays the image. pcoords pca manifold - importances - rfecv jointplot diff --git a/docs/api/features/jointplot.py b/docs/api/features/jointplot.py deleted file mode 100644 index d86dff126..000000000 --- a/docs/api/features/jointplot.py +++ /dev/null @@ -1,34 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -from yellowbrick.features import JointPlotVisualizer - - -def jointplot(X, y, outpath, **kwargs): - # Create the visualizer - visualizer = JointPlotVisualizer(**kwargs) - visualizer.fit(X, y) - visualizer.transform(X) - - # Save to disk - visualizer.poof(outpath=outpath) - plt.savefig(outpath) - - -if __name__ == '__main__': - - # Load the regression data set - data = pd.read_csv("../../../examples/data/concrete/concrete.csv") - - feature = 'cement' - target = 'strength' - - # Get the X and y data from the DataFrame - Xs = data[feature] - ys = data[target] - - # Draw the joint plot visualizer - jointplot(Xs, ys, "images/jointplot.png", feature=feature, target=target) - - # Draw the joint plot visualizer with hexadecimal scatter plot - jointplot(Xs, ys, "images/jointplot_hex.png", feature=feature, target=target, joint_plot='hex') diff --git a/docs/api/features/jointplot.rst b/docs/api/features/jointplot.rst index 90847fda3..cda14ccc9 100644 --- a/docs/api/features/jointplot.rst +++ b/docs/api/features/jointplot.rst @@ -8,49 +8,69 @@ Sometimes for feature analysis you simply need a scatter plot to determine the d Joint Plot Visualization ------------------------ -A joint plot visualizer plots a feature against the target and shows the distribution of each via a histogram on each axis. +The ``JointPlotVisualizer`` plots a feature against the target and shows the distribution of each via a histogram on each axis. -.. code:: python - # Load the data - df = load_data("concrete") - feature = "cement" - target = "strength" +.. plot:: + :context: close-figs + :alt: JointPlot - # Get the X and y data from the DataFrame - X = df[feature] - y = df[target] + from yellowbrick.datasets import load_concrete + from yellowbrick.features import JointPlotVisualizer + + # Load the dataset + X, y = load_concrete() + + # Instantiate the visualizer + visualizer = JointPlotVisualizer(columns="cement") + + visualizer.fit_transform(X, y) # Fit and transform the data + visualizer.poof() # Draw/show/poof the data -.. code:: python +The ``JointPlotVisualizer`` can also be used to compare two features. + +.. plot:: + :context: close-figs + :alt: JointPlot comparing two features + + from yellowbrick.datasets import load_concrete from yellowbrick.features import JointPlotVisualizer - visualizer = JointPlotVisualizer(feature=feature, target=target) + # Load the dataset + X, y = load_concrete() - visualizer.fit(X, y) - visualizer.poof() + # Instantiate the visualizer + visualizer = JointPlotVisualizer(columns=["cement", "ash"]) + visualizer.fit_transform(X, y) # Fit and transform the data + visualizer.poof() # Draw/show/poof the data -.. image:: images/jointplot.png -The joint plot visualizer can also be plotted with hexbins in the case of many, many points. +In addition, the ``JointPlotVisualizer`` can be plotted with hexbins in the case +of many, many points. -.. code:: python +.. plot:: + :context: close-figs + :alt: JointPlot + + from yellowbrick.datasets import load_concrete + from yellowbrick.features import JointPlotVisualizer - visualizer = JointPlotVisualizer( - feature=feature, target=target, joint_plot='hex' - ) + # Load the dataset + X, y = load_concrete() - visualizer.fit(X, y) - visualizer.poof() + # Instantiate the visualizer + visualizer = JointPlotVisualizer(columns="cement", kind="hexbin") -.. image:: images/jointplot_hex.png + visualizer.fit_transform(X, y) # Fit and transform the data + visualizer.poof() # Draw/show/poof the data API Reference ------------- .. automodule:: yellowbrick.features.jointplot - :members: JointPlotVisualizer + :members: JointPlot :undoc-members: :show-inheritance: diff --git a/docs/api/features/manifold.py b/docs/api/features/manifold.py index faff912fc..eb02263b6 100644 --- a/docs/api/features/manifold.py +++ b/docs/api/features/manifold.py @@ -2,9 +2,12 @@ # manifold.py # Produce images for manifold documentation. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Sat May 12 11:26:18 2018 -0400 # +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: manifold.py [] benjamin@bengfort.com $ """ @@ -16,84 +19,60 @@ ########################################################################## import os - -import pandas as pd import matplotlib.pyplot as plt from sklearn import datasets from sklearn.pipeline import Pipeline +from sklearn.feature_selection import f_classif from sklearn.feature_selection import SelectKBest -from sklearn.feature_selection import f_classif#, mutual_info_classif + +from yellowbrick.datasets import load_occupancy, load_concrete from yellowbrick.features.manifold import Manifold, MANIFOLD_ALGORITHMS + SKIP = ( - 'ltsa', # produces no result - 'hessian', # errors because of matrix - 'mds', # uses way too much memory + "ltsa", # produces no result + "hessian", # errors because of matrix + "mds", # uses way too much memory ) -FIXTURES = os.path.normpath(os.path.join( - os.path.dirname(__file__), - "..", "..", "..", "examples", "data" -)) - - -def load_occupancy_data(): - # Load the classification data set - data = pd.read_csv(os.path.join(FIXTURES, 'occupancy', 'occupancy.csv')) - - # Specify the features of interest and the classes of the target - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - - X = data[features] - y = pd.Series(['occupied' if y == 1 else 'unoccupied' for y in data.occupancy]) - - return X, y - - -def load_concrete_data(): - # Load a regression data set - data = pd.read_csv(os.path.join(FIXTURES, 'concrete', 'concrete.csv')) - - # Specify the features of interest - feature_names = ['cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age'] - target_name = 'strength' - - # Get the X and y data from the DataFrame - X = data[feature_names] - y = data[target_name] - - return X, y +FIXTURES = os.path.normpath( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "examples", "data") +) -def dataset_example(dataset="occupancy", manifold="all", path="images/"): +def dataset_example(dataset="occupancy", manifold="all", path="images/", **kwargs): if manifold == "all": if path is not None and not os.path.isdir(path): "please specify a directory to save examples to" for algorithm in MANIFOLD_ALGORITHMS: - if algorithm in SKIP: continue + if algorithm in SKIP: + continue print("generating {} {} manifold".format(dataset, algorithm)) fpath = os.path.join(path, "{}_{}_manifold.png".format(dataset, algorithm)) try: dataset_example(dataset, algorithm, fpath) except Exception as e: - print("could not visualize {} manifold on {} data: {}".format(algorithm, dataset, e)) + print( + "could not visualize {} manifold on {} data: {}".format( + algorithm, dataset, e + ) + ) continue - # Break here! return # Create single example - _, ax = plt.subplots(figsize=(9,6)) - oz = Manifold(ax=ax, manifold=manifold) + _, ax = plt.subplots(figsize=(9, 6)) + oz = Manifold(ax=ax, manifold=manifold, **kwargs) if dataset == "occupancy": - X, y = load_occupancy_data() + X, y = load_occupancy() elif dataset == "concrete": - X, y = load_concrete_data() + X, y = load_concrete() else: raise Exception("unknown dataset '{}'".format(dataset)) @@ -101,17 +80,23 @@ def dataset_example(dataset="occupancy", manifold="all", path="images/"): oz.poof(outpath=path) -def select_features_example(algorithm='isomap', path="images/occupancy_select_k_best_isomap_manifold.png"): - _, ax = plt.subplots(figsize=(9,6)) +def select_features_example( + algorithm="isomap", + path="images/occupancy_select_k_best_isomap_manifold.png", + **kwargs +): + _, ax = plt.subplots(figsize=(9, 6)) - model = Pipeline([ - ("selectk", SelectKBest(k=3, score_func=f_classif)), - ("viz", Manifold(ax=ax, manifold=algorithm)), - ]) + model = Pipeline( + [ + ("selectk", SelectKBest(k=3, score_func=f_classif)), + ("viz", Manifold(ax=ax, manifold=algorithm, **kwargs)), + ] + ) - X, y = load_occupancy_data() + X, y = load_occupancy() model.fit(X, y) - model.named_steps['viz'].poof(outpath=path) + model.named_steps["viz"].poof(outpath=path) class SCurveExample(object): @@ -131,7 +116,7 @@ def _make_path(self, path, name): if not os.path.exists(path): os.mkdirs(path) - if os.path.isdir(path) : + if os.path.isdir(path): return os.path.join(path, name) return path @@ -146,13 +131,10 @@ def plot_manifold_embedding(self, algorithm="lle", path="images"): """ Draw the manifold embedding for the specified algorithm """ - _, ax = plt.subplots(figsize=(9,6)) + _, ax = plt.subplots(figsize=(9, 6)) path = self._make_path(path, "s_curve_{}_manifold.png".format(algorithm)) - oz = Manifold( - ax=ax, manifold=algorithm, - target='continuous', colors='nipy_spectral' - ) + oz = Manifold(ax=ax, manifold=algorithm, colors="nipy_spectral") oz.fit(self.X, self.y) oz.poof(outpath=path) @@ -165,11 +147,12 @@ def plot_all_manifolds(self, path="images"): self.plot_manifold_embedding(algorithm) -if __name__ == '__main__': +if __name__ == "__main__": # curve = SCurveExample() # curve.plot_all_manifolds() - - dataset_example('occupancy', 'tsne', path="images/occupancy_tsne_manifold.png") - # dataset_example('concrete', 'all') - - # select_features_example() + dataset_example("concrete", "tsne", path="images/concrete_tsne_manifold.png") + dataset_example("occupancy", "tsne", path="images/occupancy_tsne_manifold.png") + dataset_example( + "concrete", "isomap", path="images/concrete_isomap_manifold.png", n_neighbors=10 + ) + select_features_example(algorithm="isomap", n_neighbors=10) diff --git a/docs/api/features/manifold.rst b/docs/api/features/manifold.rst index c05147517..e45ba0f81 100644 --- a/docs/api/features/manifold.rst +++ b/docs/api/features/manifold.rst @@ -59,27 +59,22 @@ discrete labels - the classes or categories in the supervised problem, or the clusters they belong to in the unsupervised version. The manifold visualizes this by assigning a color to each label and showing the labels in a legend. -.. code:: python - - # Load the classification data set - data = load_data('occupancy') +.. note to contributors: the below code takes a long time to run so has not been + modified with a plot directive. See manifold.py to regenerate images. - # Specify the features of interest - features = [ - "temperature", "relative humidity", "light", "C02", "humidity" - ] +.. code:: python - # Extract the instances and target - X = data[features] - y = data.occupancy + from yellowbrick.features import Manifold + from yellowbrick.datasets import load_occupancy -.. code:: python + # Load the classification dataset + X, y = load_occupancy() - from yellowbrick.features.manifold import Manifold + # Instantiate the visualizer + visualizer = Manifold(manifold="tsne") - visualizer = Manifold(manifold='tsne', target='discrete') - visualizer.fit_transform(X,y) - visualizer.poof() + visualizer.fit(X, y) # Fit the data + visualizer.poof() # Draw/show/poof the data .. image:: images/occupancy_tsne_manifold.png @@ -91,35 +86,32 @@ another is to sample your instances (e.g. using ``train_test_split`` to preserve class stratification) or to filter features to decrease sparsity in the dataset. -One common mechanism is to use `SelectKBest` to select the features that have +One common mechanism is to use ``SelectKBest`` to select the features that have a statistical correlation with the target dataset. For example, we can use the ``f_classif`` score to find the 3 best features in our occupancy dataset. +.. note to contributors: the below code takes a long time to run so has not been + modified with a plot directive. See manifold.py to regenerate images. + .. code:: python from sklearn.pipeline import Pipeline - from sklearn.feature_selection import SelectKBest - from sklearn.feature_selection import f_classif + from sklearn.feature_selection import f_classif, SelectKBest - model = Pipeline([ - ("selectk", SelectKBest(k=3, score_func=f_classif)), - ("viz", Manifold(manifold='isomap', target='discrete')), - ]) + from yellowbrick.features import Manifold + from yellowbrick.datasets import load_occupancy # Load the classification dataset - data = load_data("occupancy") - - # Specify the features of interest - features = [ - "temperature", "relative humidity", "light", "CO2", "humidity" - ] + X, y = load_occupancy() - # Extract the instances and target - X = data[features] - y = data.occupancy + # Create a pipeline + model = Pipeline([ + ("selectk", SelectKBest(k=3, score_func=f_classif)), + ("viz", Manifold(manifold="isomap", n_neighbors=10)), + ]) - model.fit(X, y) - model.named_steps['viz'].poof() + model.fit(X, y) # Fit the data to the model + model.named_steps['viz'].poof() # Draw/show/poof the data .. image:: images/occupancy_select_k_best_isomap_manifold.png @@ -127,27 +119,26 @@ Continuous Target ----------------- For a regression target or to specify color as a heat-map of continuous -values, specify ``target='continuous'``. Note that by default the param -``target='auto'`` is set, which determines if the target is discrete or +values, specify ``target_type="continuous"``. Note that by default the param +``target_type="auto"`` is set, which determines if the target is discrete or continuous by counting the number of unique values in ``y``. +.. note to contributors: the below code takes a long time to run so has not been + modified with a plot directive. See manifold.py to regenerate images. + .. code:: python - # Specify the features of interest - feature_names = [ - 'cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age' - ] - target_name = 'strength' + from yellowbrick.features import Manifold + from yellowbrick.datasets import load_concrete - # Get the X and y data from the DataFrame - X = data[feature_names] - y = data[target_name] + # Load the regression dataset + X, y = load_concrete() -.. code:: python + # Instantiate the visualizer + visualizer = Manifold(manifold="isomap", n_neighbors=10) - visualizer = Manifold(manifold='isomap', target='continuous') - visualizer.fit_transform(X,y) - visualizer.poof() + visualizer.fit(X, y) # Fit the data + visualizer.poof() # Draw/show/poof the data .. image:: images/concrete_isomap_manifold.png diff --git a/docs/api/features/pca.py b/docs/api/features/pca.py deleted file mode 100644 index 4daed6d5f..000000000 --- a/docs/api/features/pca.py +++ /dev/null @@ -1,57 +0,0 @@ -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -from yellowbrick.features.pca import PCADecomposition - - -def pca(X, y, outpath, **kwargs): - # Create a new figure and axes - _, ax = plt.subplots() - - viz = PCADecomposition(ax=ax, **kwargs) - viz.fit_transform(X, y) - viz.poof(outpath=outpath) - - -def load_credit(): - # Load the credit data set - data = pd.read_csv("../../../examples/data/credit/credit.csv") - - # Specify the features of interest - target = "default" - features = [col for col in data.columns if col != target] - - # Extract the numpy arrays from the data frame - X = data[features] - y = data[target] - return X, y - - -def load_concrete(): - # Load the credit data set - data = pd.read_csv("../../../examples/data/concrete/concrete.csv") - - # Specify the features of interest - feature_names = ['cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age'] - target_name = 'strength' - - # Get the X and y data from the DataFrame - X = data[feature_names] - y = data[target_name] - - return X, y - - -if __name__ == '__main__': - - # Draw PCA with credit data set - X, y = load_credit() - colors = np.array(['r' if yi else 'b' for yi in y]) - pca(X, y, "images/pca_projection_2d.png", scale=True, color=colors) - pca(X, y, "images/pca_projection_3d.png", scale=True, color=colors, proj_dim=3) - - # Draw biplots with concrete data set - X, y = load_concrete() - pca(X, y, "images/pca_biplot_2d.png", scale=True, proj_features=True) - pca(X, y, "images/pca_biplot_3d.png", scale=True, proj_features=True, proj_dim=3) diff --git a/docs/api/features/pca.rst b/docs/api/features/pca.rst index d10f66522..d3f799dcc 100644 --- a/docs/api/features/pca.rst +++ b/docs/api/features/pca.rst @@ -5,87 +5,79 @@ PCA Projection The PCA Decomposition visualizer utilizes principal component analysis to decompose high dimensional data into two or three dimensions so that each instance can be plotted in a scatter plot. The use of PCA means that the projected dataset can be analyzed along axes of principal variation and can be interpreted to determine if spherical distance metrics can be utilized. -.. code:: python +.. plot:: + :context: close-figs + :alt: PCA Projection, 2D - # Load the classification data set - data = load_data('credit') + from yellowbrick.datasets import load_credit + from yellowbrick.features.pca import PCADecomposition # Specify the features of interest and the target - target = "default" - features = [col for col in data.columns if col != target] - - # Extract the instance data and the target - X = data[features] - y = data[target] + X, y = load_credit() # Create a list of colors to assign to points in the plot colors = np.array(['r' if yi else 'b' for yi in y]) -.. code:: python - - from yellowbrick.features.pca import PCADecomposition - visualizer = PCADecomposition(scale=True, color=colors) visualizer.fit_transform(X, y) visualizer.poof() -.. image:: images/pca_projection_2d.png - The PCA projection can also be plotted in three dimensions to attempt to visualize more principal components and get a better sense of the distribution in high dimensions. -.. code:: python +.. plot:: + :context: close-figs + :alt: PCA Projection, 3D + + from yellowbrick.datasets import load_credit + from yellowbrick.features.pca import PCADecomposition + + X, y = load_credit() + + colors = np.array(['r' if yi else 'b' for yi in y]) visualizer = PCADecomposition(scale=True, color=colors, proj_dim=3) visualizer.fit_transform(X, y) visualizer.poof() -.. image:: images/pca_projection_3d.png - Biplot ------ -The PCA projection can be enhanced to a biplot whose points are the projected instances and whose vectors represent the structure of the data in high dimensional space. By using the ``proj_features=True`` flag, vectors for each feature in the dataset are drawn on the scatter plot in the direction of the maximum variance for that feature. These structures can be used to analyze the importance of a feature to the decomposition or to find features of related variance for further analysis. - -.. code:: python +The PCA projection can be enhanced to a biplot whose points are the projected instances and whose vectors represent the structure of the data in high dimensional space. By using ``proj_features=True``, vectors for each feature in the dataset are drawn on the scatter plot in the direction of the maximum variance for that feature. These structures can be used to analyze the importance of a feature to the decomposition or to find features of related variance for further analysis. - # Load the classification data set - data = load_data('concrete') +.. plot:: + :context: close-figs + :alt: PCA biplot projection, 2D - # Specify the features of interest and the target - target = "strength" - features = [ - 'cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age' - ] - - # Extract the instance data and the target - X = data[features] - y = data[target] + from yellowbrick.datasets import load_concrete + from yellowbrick.features.pca import PCADecomposition -.. code:: python + # Load the concrete dataset + X, y = load_concrete() visualizer = PCADecomposition(scale=True, proj_features=True) visualizer.fit_transform(X, y) visualizer.poof() -.. image:: images/pca_biplot_2d.png +.. plot:: + :context: close-figs + :alt: PCA biplot projection, 3D -.. code:: python + from yellowbrick.datasets import load_concrete + from yellowbrick.features.pca import PCADecomposition + + X, y = load_concrete() visualizer = PCADecomposition(scale=True, proj_features=True, proj_dim=3) visualizer.fit_transform(X, y) visualizer.poof() - -.. image:: images/pca_biplot_3d.png - - API Reference ------------- .. automodule:: yellowbrick.features.pca - :members: PCADecomposition + :members: PCA :undoc-members: :show-inheritance: diff --git a/docs/api/features/pcoords.py b/docs/api/features/pcoords.py deleted file mode 100644 index e65454dee..000000000 --- a/docs/api/features/pcoords.py +++ /dev/null @@ -1,129 +0,0 @@ -import time -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -from yellowbrick.features import ParallelCoordinates -from sklearn.datasets import load_iris - - -def load_occupancy_data(): - # Load the classification data set - data = pd.read_csv("../../../examples/data/occupancy/occupancy.csv") - - # Specify the features of interest and the classes of the target - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - classes = ['unoccupied', 'occupied'] - - # Extract the instances and target - X = data[features] - y = data.occupancy - - return X, y, features, classes - - -def pcoords(X, y, outpath, **kwargs): - # Create a new figure and axes - _, ax = plt.subplots() - - # Create the visualizer - visualizer = ParallelCoordinates(ax=ax, **kwargs) - visualizer.fit_transform(X, y) - - # Save to disk - visualizer.poof(outpath=outpath) - - -def plot_fast_vs_slow(): - data = load_iris() - - _, axes = plt.subplots(nrows=2, figsize=(9,9)) - - for idx, fast in enumerate((False, True)): - title = "Fast Parallel Coordinates" if fast else "Standard Parallel Coordinates" - oz = ParallelCoordinates(ax=axes[idx], fast=fast, title=title) - oz.fit_transform(data.data, data.target) - oz.finalize() - - plt.tight_layout() - plt.savefig("images/fast_vs_slow_parallel_coordinates.png") - - -def plot_speedup(trials=5, factors=np.arange(1, 11)): - - def pcoords_time(X, y, fast=True): - _, ax = plt.subplots() - oz = ParallelCoordinates(fast=fast, ax=ax) - - start = time.time() - oz.fit_transform(X, y) - delta = time.time() - start - - plt.cla() # clear current axis - plt.clf() # clear current figure - plt.close("all") # close all existing plots - - return delta - - def pcoords_speedup(X, y): - fast_time = pcoords_time(X, y, fast=True) - slow_time = pcoords_time(X, y, fast=False) - - return slow_time / fast_time - - data = load_iris() - - speedups = [] - variance = [] - - for factor in factors: - X = np.repeat(data.data, factor, axis=0) - y = np.repeat(data.target, factor, axis=0) - - local_speedups = [] - for trial in range(trials): - local_speedups.append(pcoords_speedup(X, y)) - - local_speedups = np.array(local_speedups) - speedups.append(local_speedups.mean()) - variance.append(local_speedups.std()) - - speedups = np.array(speedups) - variance = np.array(variance) - - series = pd.Series(speedups, index=factors) - _, ax = plt.subplots(figsize=(9,6)) - series.plot(ax=ax, marker='o', label="speedup factor", color='b') - - # Plot one standard deviation above and below the mean - ax.fill_between( - factors, speedups - variance, speedups + variance, alpha=0.25, - color='b', - ) - - ax.set_ylabel("speedup factor") - ax.set_xlabel("dataset size (number of repeats in Iris dataset)") - ax.set_title("Speed Improvement of Fast Parallel Coordinates") - plt.savefig("images/fast_parallel_coordinates_speedup.png") - - -if __name__ == '__main__': - # plot_fast_vs_slow() - # plot_speedup() - - # Occupancy data visualizations - X, y, features, classes = load_occupancy_data() - - # Draw the full, original parallel coordinates - pcoords( - X, y, "images/parallel_coordinates.png", - classes=classes, features=features, - sample=0.05, shuffle=True, random_state=19, - ) - - # Draw the noramlized, sampled parallel coordinates - pcoords( - X, y, "images/normalized_sampled_parallel_coordinates.png", - classes=classes, features=features, - normalize='standard', sample=0.05, shuffle=True, random_state=19, - ) diff --git a/docs/api/features/pcoords.rst b/docs/api/features/pcoords.rst index 8cc96bc86..723942115 100644 --- a/docs/api/features/pcoords.rst +++ b/docs/api/features/pcoords.rst @@ -5,32 +5,29 @@ Parallel Coordinates Parallel coordinates is multi-dimensional feature visualization technique where the vertical axis is duplicated horizontally for each feature. Instances are displayed as a single line segment drawn from each vertical axes to the location representing their value for that feature. This allows many dimensions to be visualized at once; in fact given infinite horizontal space (e.g. a scrolling window), technically an infinite number of dimensions can be displayed! -Data scientists use this method to detect clusters of instances that have similar classes, and to note features that have high variance or different distributions. We can see this in action after first loading our occupancy classification dataset: +Data scientists use this method to detect clusters of instances that have similar classes, and to note features that have high variance or different distributions. We can see this in action after first loading our occupancy classification dataset. -.. code:: python +.. note:: These visualizations can be produced with either the ``ParallelCoordinates`` visualizer or by using the ``parallel_coordinates`` quick method. + +.. plot:: + :context: close-figs + :alt: Parallel Coordinates + + from yellowbrick.features import ParallelCoordinates + from yellowbrick.datasets import load_occupancy # Load the classification data set - data = load_data("occupancy") + X, y = load_occupancy() # Specify the features of interest and the classes of the target features = [ - "temperature", "relative humidity", "light", "C02", "humidity" + "temperature", "relative humidity", "light", "CO2", "humidity" ] classes = ["unoccupied", "occupied"] - # Extract the instances and target - X = data[features] - y = data.occupancy - -The visualization can be drawn with either the ``ParallelCoordinates`` visualizer or using the ``parallel_coordinates`` quick method: - -.. code:: python - - from yellowbrick.features import ParallelCoordinates - # Instantiate the visualizer visualizer = ParallelCoordinates( - classes=classes, features=features, sample=0.5, shuffle=True + classes=classes, features=features, sample=0.05, shuffle=True ) # Fit and transform the data to the visualizer @@ -40,17 +37,27 @@ The visualization can be drawn with either the ``ParallelCoordinates`` visualize visualizer.poof() -.. image:: images/parallel_coordinates.png - By inspecting the visualization closely, we can see that the combination of transparency and overlap gives us the sense of groups of similar instances, sometimes referred to as "braids". If there are distinct braids of different classes, it suggests that there is enough separability that a classification algorithm might be able to discern between each class. Unfortunately, as we inspect this class, we can see that the domain of each feature may make the visualization hard to interpret. In the above visualization, the domain of the ``light`` feature is from in ``[0, 1600]``, far larger than the range of temperature in ``[50, 96]``. To solve this problem, each feature should be scaled or normalized so they are approximately in the same domain. -Normalization techniques can be directly applied to the visualizer without pre-transforming the data (though you could also do this) by using the ``normalize`` parameter. Several transformers are available; try using ``minmax``, ``minabs``, ``standard``, ``l1``, or ``l2`` normalization to change perspectives in the parallel coordinates as follows: +Normalization techniques can be directly applied to the visualizer without pre-transforming the data (though you could also do this) by using the ``normalize`` parameter. Several transformers are available; try using ``minmax``, ``maxabs``, ``standard``, ``l1``, or ``l2`` normalization to change perspectives in the parallel coordinates as follows: -.. code:: python +.. plot:: + :context: close-figs + :alt: Parallel Coordinates with Normalization from yellowbrick.features import ParallelCoordinates + from yellowbrick.datasets import load_occupancy + + # Load the classification data set + X, y = load_occupancy() + + # Specify the features of interest and the classes of the target + features = [ + "temperature", "relative humidity", "light", "CO2", "humidity" + ] + classes = ["unoccupied", "occupied"] # Instantiate the visualizer visualizer = ParallelCoordinates( @@ -62,7 +69,6 @@ Normalization techniques can be directly applied to the visualizer without pre-t visualizer.fit_transform(X, y) visualizer.poof() -.. image:: images/normalized_sampled_parallel_coordinates.png Now we can see that each feature is in the range ``[-3, 3]`` where the mean of the feature is set to zero and each feature has a unit variance applied between ``[-1, 1]`` (because we're using the ``StandardScaler`` via the ``standard`` normalize parameter). This version of parallel coordinates gives us a much better sense of the distribution of the features and if any features are highly variable with respect to any one class. @@ -78,7 +84,26 @@ Parallel coordinates can take a long time to draw since each instance is represe The "fast" drawing mode vastly improves the performance of the parallel coordinates drawing algorithm by drawing each line segment by class rather than each instance individually. However, this improved performance comes at a cost, as the visualization produced is subtly different; compare the visualizations in fast and standard drawing modes below: -.. image:: images/fast_vs_slow_parallel_coordinates.png +.. plot:: + :include-source: False + :context: close-figs + :alt: Parallel Coordinates in fast drawing mode + + import matplotlib.pyplot as plt + from sklearn.datasets import load_iris + from yellowbrick.features import ParallelCoordinates + + data = load_iris() + + _, axes = plt.subplots(nrows=2, figsize=(9,9)) + + for idx, fast in enumerate((False, True)): + title = "Fast Parallel Coordinates" if fast else "Standard Parallel Coordinates" + oz = ParallelCoordinates(ax=axes[idx], fast=fast, title=title) + oz.fit_transform(data.data, data.target) + oz.finalize() + + plt.tight_layout() As you can see the "fast" drawing algorithm does not have the same build up of color density where instances of the same class intersect. Because there is only one line per class, there is only a darkening effect between classes. This can lead to a different interpretation of the plot, though it still may be effective for analytical purposes, particularly when you're plotting a lot of data. Needless to say, the performance benefits are dramatic: diff --git a/docs/api/features/pcoords_benchmark.py b/docs/api/features/pcoords_benchmark.py new file mode 100644 index 000000000..d47d7a3a5 --- /dev/null +++ b/docs/api/features/pcoords_benchmark.py @@ -0,0 +1,66 @@ +import time +import matplotlib.pyplot as plt +from sklearn.datasets import load_iris +from yellowbrick.features import ParallelCoordinates +import pandas as pd +import numpy as np + + +def plot_speedup(trials=5, factors=np.arange(1, 11)): + def pcoords_time(X, y, fast=True): + _, ax = plt.subplots() + oz = ParallelCoordinates(fast=fast, ax=ax) + + start = time.time() + oz.fit_transform(X, y) + delta = time.time() - start + + plt.cla() # clear current axis + plt.clf() # clear current figure + plt.close("all") # close all existing plots + + return delta + + def pcoords_speedup(X, y): + fast_time = pcoords_time(X, y, fast=True) + slow_time = pcoords_time(X, y, fast=False) + + return slow_time / fast_time + + data = load_iris() + + speedups = [] + variance = [] + + for factor in factors: + X = np.repeat(data.data, factor, axis=0) + y = np.repeat(data.target, factor, axis=0) + + local_speedups = [] + for trial in range(trials): + local_speedups.append(pcoords_speedup(X, y)) + + local_speedups = np.array(local_speedups) + speedups.append(local_speedups.mean()) + variance.append(local_speedups.std()) + + speedups = np.array(speedups) + variance = np.array(variance) + + series = pd.Series(speedups, index=factors) + _, ax = plt.subplots(figsize=(9, 6)) + series.plot(ax=ax, marker="o", label="speedup factor", color="b") + + # Plot one standard deviation above and below the mean + ax.fill_between( + factors, speedups - variance, speedups + variance, alpha=0.25, color="b" + ) + + ax.set_ylabel("speedup factor") + ax.set_xlabel("dataset size (number of repeats in Iris dataset)") + ax.set_title("Speed Improvement of Fast Parallel Coordinates") + plt.savefig("images/fast_parallel_coordinates_speedup_benchmark.png") + + +if __name__ == "__main__": + plot_speedup() diff --git a/docs/api/features/radviz.py b/docs/api/features/radviz.py deleted file mode 100644 index 45de47299..000000000 --- a/docs/api/features/radviz.py +++ /dev/null @@ -1,21 +0,0 @@ -import pandas as pd -from yellowbrick.features import RadViz - - -# Load the classification data set -data = pd.read_csv("../../../examples/data/occupancy/occupancy.csv") - -# Specify the features of interest and the classes of the target -features = ["temperature", "relative humidity", "light", "C02", "humidity"] -classes = ['unoccupied', 'occupied'] - -# Extract the instances and target -X = data[features] -y = data.occupancy - -# Instantiate the visualizer -visualizer = RadViz(classes=classes, features=features) - -visualizer.fit(X, y) -visualizer.transform(X) -visualizer.poof(outpath="images/radviz.png") diff --git a/docs/api/features/radviz.rst b/docs/api/features/radviz.rst index 425b097da..dc0529f98 100644 --- a/docs/api/features/radviz.rst +++ b/docs/api/features/radviz.rst @@ -3,7 +3,7 @@ RadViz Visualizer ================= -RadViz is a multivariate data visualization algorithm that plots each +``RadViz`` is a multivariate data visualization algorithm that plots each feature dimension uniformly around the circumference of a circle then plots points on the interior of the circle such that the point normalizes its values on the axes from the center to each arc. This @@ -16,43 +16,33 @@ just too much noise? If your data contains rows with missing values (``numpy.nan``), those missing values will not be plotted. In other words, you may not get the entire -picture of your data. RadViz will raise a DataWarning to inform you of the +picture of your data. ``RadViz`` will raise a ``DataWarning`` to inform you of the percent missing. If you do receive this warning, you may want to look at imputation strategies. A good starting place is the `scikit-learn Imputer. `_ -.. code:: python +.. plot:: + :context: close-figs + :alt: RadViz on the Occupancy Dataset - # Load the classification data set - data = load_data("occupancy") - - # Specify the features of interest and the classes of the target - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - classes = ["unoccupied", "occupied"] - - # Extract the instances and target - X = data[features] - y = data.occupancy - -.. code:: python - - # Import the visualizer + from yellowbrick.datasets import load_occupancy from yellowbrick.features import RadViz - # Instantiate the visualizer - visualizer = RadViz(classes=classes, features=features) - - visualizer.fit(X, y) # Fit the data to the visualizer - visualizer.transform(X) # Transform the data - visualizer.poof() # Draw/show/poof the data - + # Load the classification dataset + X, y = load_occupancy() + # Specify the target classes + classes = ["unoccupied", "occupied"] -.. image:: images/radviz.png + # Instantiate the visualizer + visualizer = RadViz(classes=classes) + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.transform(X) # Transform the data + visualizer.poof() # Draw/show/poof the data -For regression, the RadViz visualizer should use a color sequence to +For regression, the ``RadViz`` visualizer should use a color sequence to display the target information, as opposed to discrete colors. API Reference diff --git a/docs/api/features/rankd.py b/docs/api/features/rankd.py deleted file mode 100644 index 75eca93dd..000000000 --- a/docs/api/features/rankd.py +++ /dev/null @@ -1,56 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -from yellowbrick.features.rankd import Rank1D, Rank2D - -def rank1d(X, y, outpath, **kwargs): - # Create a new figure and axes - _, ax = plt.subplots() - - # Create the visualizer - visualizer = Rank1D(ax=ax, **kwargs) - visualizer.fit(X, y) - visualizer.transform(X) - - # Save to disk - visualizer.poof(outpath=outpath) - - -def rank2d(X, y, outpath, **kwargs): - # Create a new figure and axes - _, ax = plt.subplots() - - # Create the visualizer - visualizer = Rank2D(ax=ax, **kwargs) - visualizer.fit(X, y) - visualizer.transform(X) - - # Save to disk - plt.tight_layout() - visualizer.poof(outpath=outpath) - - -if __name__ == '__main__': - # Load the regression data set - data = pd.read_csv("../../../examples/data/credit/credit.csv") - - # Specify the features of interest - features = [ - 'limit', 'sex', 'edu', 'married', 'age', 'apr_delay', 'may_delay', - 'jun_delay', 'jul_delay', 'aug_delay', 'sep_delay', 'apr_bill', 'may_bill', - 'jun_bill', 'jul_bill', 'aug_bill', 'sep_bill', 'apr_pay', 'may_pay', 'jun_pay', - 'jul_pay', 'aug_pay', 'sep_pay', - ] - - # Extract the instances and target - X = data[features] - y = data.default - - # Instantiate the visualizer with the Shapiro-Wilk ranking algorithm - rank1d(X, y, "images/rank1d_shapiro.png", features=features, algorithm='shapiro') - - # Instantiate the visualizer with the Covariance ranking algorithm - rank2d(X, y, "images/rank2d_covariance.png", features=features, algorithm='covariance') - - # Instantiate the visualizer with the Pearson ranking algorithm - rank2d(X, y, "images/rank2d_pearson.png", features=features, algorithm='pearson') diff --git a/docs/api/features/rankd.rst b/docs/api/features/rankd.rst index f51b785d2..f0d437828 100644 --- a/docs/api/features/rankd.rst +++ b/docs/api/features/rankd.rst @@ -3,81 +3,76 @@ Rank Features ============= -Rank1D and Rank2D evaluate single features or pairs of features using a variety of metrics that score the features on the scale [-1, 1] or [0, 1] allowing them to be ranked. A similar concept to SPLOMs, the scores are visualized on a lower-left triangle heatmap so that patterns between pairs of features can be easily discerned for downstream analysis. +``Rank1D`` and ``Rank2D`` evaluate single features or pairs of features using a variety of metrics that score the features on the scale [-1, 1] or [0, 1] allowing them to be ranked. A similar concept to SPLOMs, the scores are visualized on a lower-left triangle heatmap so that patterns between pairs of features can be easily discerned for downstream analysis. In this example, we'll use the credit default data set from the UCI Machine Learning repository to rank features. The code below creates our instance matrix and target vector. -.. code:: python - - # Load the dataset - data = load_data('credit') - - # Specify the features of interest - features = [ - 'limit', 'sex', 'edu', 'married', 'age', 'apr_delay', 'may_delay', - 'jun_delay', 'jul_delay', 'aug_delay', 'sep_delay', 'apr_bill', 'may_bill', - 'jun_bill', 'jul_bill', 'aug_bill', 'sep_bill', 'apr_pay', 'may_pay', 'jun_pay', - 'jul_pay', 'aug_pay', 'sep_pay', - ] - - # Extract the instances and target - X = data[features] - y = data.default - Rank 1D ------- -A one dimensional ranking of features utilizes a ranking algorithm that takes into account only a single feature at a time (e.g. histogram analysis). By default we utilize the Shapiro-Wilk algorithm to assess the normality of the distribution of instances with respect to the feature. A barplot is then drawn showing the relative ranks of each feature. +A one-dimensional ranking of features utilizes a ranking algorithm that takes into account only a single feature at a time (e.g. histogram analysis). By default we utilize the Shapiro-Wilk algorithm to assess the normality of the distribution of instances with respect to the feature. A barplot is then drawn showing the relative ranks of each feature. -.. code:: python +.. plot:: + :context: close-figs + :alt: Rank1D on the credit dataset with the Shapiro ranking algorithm + from yellowbrick.datasets import load_credit from yellowbrick.features import Rank1D - # Instantiate the 1D visualizer with the Sharpiro ranking algorithm - visualizer = Rank1D(features=features, algorithm='shapiro') - - visualizer.fit(X, y) # Fit the data to the visualizer - visualizer.transform(X) # Transform the data - visualizer.poof() # Draw/show/poof the data + # Load the credit dataset + X, y = load_credit() + # Instantiate the 1D visualizer with the Sharpiro ranking algorithm + visualizer = Rank1D(algorithm='shapiro') -.. image:: images/rank1d_shapiro.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.transform(X) # Transform the data + visualizer.poof() # Draw/show/poof the data Rank 2D ------- -A two dimensional ranking of features utilizes a ranking algorithm that takes into account pairs of features at a time (e.g. joint plot analysis). The pairs of features are then ranked by score and visualized using the lower left triangle of a feature co-occurence matrix. +A two-dimensional ranking of features utilizes a ranking algorithm that takes into account pairs of features at a time (e.g. joint plot analysis). The pairs of features are then ranked by score and visualized using the lower left triangle of a feature co-occurence matrix. -The default ranking algorithm is covariance, which attempts to compute the mean value of the product of deviations of variates from their respective means. Covariance loosely attempts to detect a colinear relationship between features. +By default, the ``Rank2D`` visualizer utilizes the Pearson correlation score to detect colinear relationships. -.. code:: python +.. plot:: + :context: close-figs + :alt: Rank2D on the credit dataset using Pearson ranking algorithm + from yellowbrick.datasets import load_credit from yellowbrick.features import Rank2D - # Instantiate the visualizer with the Covariance ranking algorithm - visualizer = Rank2D(features=features, algorithm='covariance') - - visualizer.fit(X, y) # Fit the data to the visualizer - visualizer.transform(X) # Transform the data - visualizer.poof() # Draw/show/poof the data + # Load the credit dataset + X, y = load_credit() + + # Instantiate the visualizer with the Pearson ranking algorithm + visualizer = Rank2D(algorithm='pearson') + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.transform(X) # Transform the data + visualizer.poof() # Draw/show/poof the data -.. image:: images/rank2d_covariance.png -Alternatively, we can utilize a linear correlation algorithm such as a Pearson score to similarly detect colinear relationships. Compare the output from Pearson below to the covariance ranking above. +Alternatively, we can utilize the covariance ranking algorithm, which attempts to compute the mean value of the product of deviations of variates from their respective means. Covariance loosely attempts to detect a colinear relationship between features. Compare the output from Pearson above to the covariance ranking below. -.. code:: python +.. plot:: + :context: close-figs + :alt: Rank2D on the credit dataset with the covariance algorithm - # Instantiate the visualizer with the Pearson ranking algorithm - visualizer = Rank2D(features=features, algorithm='pearson') + from yellowbrick.datasets import load_credit + from yellowbrick.features import Rank2D - visualizer.fit(X, y) # Fit the data to the visualizer - visualizer.transform(X) # Transform the data - visualizer.poof() # Draw/show/poof the data + # Load the credit dataset + X, y = load_credit() + # Instantiate the visualizer with the covariance ranking algorithm + visualizer = Rank2D(algorithm='covariance') -.. image:: images/rank2d_pearson.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.transform(X) # Transform the data + visualizer.poof() # Draw/show/poof the data API Reference diff --git a/docs/api/figures.rst b/docs/api/figures.rst new file mode 100644 index 000000000..f80f4c6b7 --- /dev/null +++ b/docs/api/figures.rst @@ -0,0 +1,81 @@ +.. -*- mode: rst -*- + +Figures and Axes +================ + +This document is an open letter to the PyData community, particularly those that are involved in matplotlib development. We'd like to get some advice on the API choice we've made and thoughts about our use of the matplotlib Axes objects. + +One of the most complex parts of designing a visualization library around matplotlib is working with figures and axes. As defined in `The Lifecycle of a Plot `_, these central objects of matplotlib plots are as follows: + +- A Figure is the final image that may contain 1 or more Axes. +- An Axes represents an individual plot + +Based on these definitions and and the advice to "try to use the object-oriented interface over the pyplot interface", the Yellowbrick interface is designed to wrap a matplotlib ``axes.Axes``. We propose the following general use case for most visualizers: + +.. code:: python + + import matplotlib.pyplot as plt + from yellowbrick import Visualizer, quick_visualizer + + fig, ax = plt.subplots() + + # Object oriented approach + viz = Visualizer(ax=ax) + viz.fit(X, y) + viz.poof() + + # Quick method approach + viz = quick_visualizer(X, y, ax=ax) + viz.poof() + +This design allows users to more directly control the size, style, and interaction with the plot (though YB does provide some helpers for these as well). For example, if a user wanted to generate a report with multiple visualizers for a classification problem, it may looks something like: + +.. code:: python + + import matplotlib.pyplot as plt + + from yellowbrick.features import FeatureImportances + from yellowbrick.classifier import ConfusionMatrix, ClassificationReport, ROCAUC + from sklearn.linear_model import LogisticRegression + + fig, axes = plot.subplots(2, 2) + + model = LogisticRegression() + visualgrid = [ + FeatureImportances(ax=axes[0][0]), + ConfusionMatrix(model, ax=axes[0][1]), + ClassificationReport(model, ax=axes[1][0]), + ROCAUC(model, ax=axes[1][1]), + ] + + for viz in visualgrid: + viz.fit(X_train, y_train) + viz.score(X_test, y_test) + viz.finalize() + + plt.show() + +This is a common use case and we're working on the idea of "visual pipelines" to support this type of development because, for machine learning, users generally want a suite of visualizers or a report, not just a single visualization. The API requirement to support this has therefore been that visualizers use the ``ax`` object passed to them and not ``plt``. If the user does not pass a specific ``ax`` then the global current axes is used via ``plt.gca``. Generally, visualizers should behave as though they are a plot that as part of a larger figure. + +Visualizers are getting more complex, however, and some are becoming multi-axes plots in their own right. For example: + +- The ResidualsPlot has a scatter plot axes and a histogram axes +- The JointPlot has a scatter plot and two histogram axes +- Data driven scatter plot axes often have colorbar axes +- The PCA plot has scatter plot, color bar, and heatmap axes +- The confusion matrix probability histogram is a grid of axes for each class pair +- The ICDM has an inset axes that acts as a dynamic legend + +Although it would have been easier to simply embed the figure into the visualizer and use a ``GridSpec`` or other layout tool, the focus on ensuring visualizers are individual plots that wrap an Axes has made us bend over backward to adjust the plot inside of the axes area that was originally supplied, primarily by using ``make_axes_locateable``, which is part of the AxesGrid toolkit. + +Generally, it appears that the `AxesGrid Toolkit `_ is the right tool for Yellowbrick - many of the examples shown are similar to the things that Yellowbrick is trying to do. However, this package is not fully documented with examples and some helper utilities that would be useful, for example the ``ImageGrid``, still require a ``figure.Figure`` object. + +At this point we are left with some important questions about Yellowbrick's development roadmap: + +1. Like Seaborn, should YB have two classes of visualizer, one that wraps an axes and one that wraps a figure? +2. Should we go all in on the AxesGrid toolkit and continue to restrict our use of the figure, will this method be supported in the long run? + + +Other notes and discussion: + +- `Create equal aspect (square) plot with multiple axes when data limits are different? `_ diff --git a/docs/api/index.rst b/docs/api/index.rst index 6ce5d79f6..708b40e4f 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -8,7 +8,7 @@ Welcome to the API documentation for Yellowbrick! This section contains a comple .. toctree:: :maxdepth: 2 - datasets + datasets/index anscombe features/index target/index @@ -19,8 +19,9 @@ Welcome to the API documentation for Yellowbrick! This section contains a comple text/index contrib/index palettes + figures -.. note:: Many examples utilize data from the UCI Machine Learning repository. In order to run the accompanying code, make sure to follow the instructions in :doc:`datasets` to download and load the required data. +.. note:: Many examples utilize data from the UCI Machine Learning repository. In order to run the accompanying code, make sure to follow the instructions in :doc:`datasets/index` to download and load the required data. A guide to finding the visualizer you're looking for: generally speaking, visualizers can be data visualizers which visualize instances relative to the model space; score visualizers which visualize model performance; model selection visualizers which compare multiple model forms against each other; and application specific-visualizers. This can be a bit confusing, so we've grouped visualizers according to the type of analysis they are well suited for. diff --git a/docs/api/model_selection/cross_validation.py b/docs/api/model_selection/cross_validation.py deleted file mode 100644 index a2f60534b..000000000 --- a/docs/api/model_selection/cross_validation.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -# cross_validation.py - -""" -Generates a CVScores image -""" - -########################################################################## -## Imports -########################################################################## - -import pandas as pd -import matplotlib.pyplot as plt - -from sklearn.linear_model import Ridge -from sklearn.naive_bayes import MultinomialNB -from sklearn.model_selection import KFold, StratifiedKFold - -from yellowbrick.model_selection import CVScores - - -########################################################################## -## Helper Methods -########################################################################## - -def load_occupancy(): - # Load the classification data set - room = pd.read_csv("../../../examples/data/occupancy/occupancy.csv") - - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - - # Extract the numpy arrays from the data frame - X = room[features].values - y = room.occupancy.values - - return X, y - - -def load_energy(): - # Load regression dataset - energy = pd.read_csv('../../../examples/data/energy/energy.csv') - - targets = ["heating load", "cooling load"] - features = [col for col in energy.columns if col not in targets] - - X = energy[features] - y = energy[targets[1]] - - return X, y - -def classification_cvscores(outpath="images/cv_scores_classifier.png", **kwargs): - X, y = load_occupancy() - - # Create a new figure and axes - _, ax = plt.subplots() - - cv = StratifiedKFold(12) - - oz = CVScores( - MultinomialNB(), ax=ax, cv=cv, scoring='f1_weighted' - ) - - oz.fit(X, y) - - # Save to disk - oz.poof(outpath=outpath) - - -def regression_cvscores(outpath="images/cv_scores_regressor.png", **kwargs): - X, y = load_energy() - - # Create a new figure and axes - _, ax = plt.subplots() - - cv = KFold(12) - - oz = CVScores( - Ridge(), ax=ax, cv=cv, scoring='r2' - ) - - oz.fit(X, y) - - # Save to disk - oz.poof(outpath=outpath) - - -if __name__ == '__main__': - classification_cvscores() - regression_cvscores() diff --git a/docs/api/model_selection/cross_validation.rst b/docs/api/model_selection/cross_validation.rst index b1479b5bf..5ab30977d 100644 --- a/docs/api/model_selection/cross_validation.rst +++ b/docs/api/model_selection/cross_validation.rst @@ -6,7 +6,7 @@ Cross Validation Scores Generally we determine whether a given model is optimal by looking at it's F1, precision, recall, and accuracy (for classification), or it's coefficient of determination (R2) and error (for regression). However, real world data is often distributed somewhat unevenly, meaning that the fitted model is likely to perform better on some sections of the data than on others. Yellowbrick's ``CVScores`` visualizer enables us to visually explore these variations in performance using different cross validation strategies. Cross Validation -################ +---------------- Cross-validation starts by shuffling the data (to prevent any unintentional ordering errors) and splitting it into `k` folds. Then `k` models are fit on :math:`\frac{k-1} {k}` of the data (called the training split) and evaluated on :math:`\frac {1} {k}` of the data (called the test split). The results from each evaluation are averaged together for a final score, then the final model is fit on the entire dataset for operationalization. @@ -17,90 +17,63 @@ In Yellowbrick, the ``CVScores`` visualizer displays cross-validated scores as a Classification -------------- -In the following example we show how to visualize cross-validated scores for a classification model. After loading a ``DataFrame``, we create a ``StratifiedKFold`` cross-validation strategy to ensure all of our classes in each split are represented with the same proportion. We then fit the ``CVScores`` visualizer using the ``f1_weighted`` scoring metric as opposed to the default metric, accuracy, to get a better sense of the relationship of precision and recall in our classifier across all of our folds. +In the following example, we show how to visualize cross-validated scores for a classification model. After loading our occupancy data as a ``DataFrame``, we created a ``StratifiedKFold`` cross-validation strategy to ensure all of our classes in each split are represented with the same proportion. We then fit the ``CVScores`` visualizer using the ``f1_weighted`` scoring metric as opposed to the default metric, accuracy, to get a better sense of the relationship of precision and recall in our classifier across all of our folds. -.. code:: python +.. plot:: + :context: close-figs + :alt: Cross validation on the occupancy data set using StratifiedKFold - import pandas as pd - import matplotlib.pyplot as plt - - from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import StratifiedKFold + from sklearn.naive_bayes import MultinomialNB + from yellowbrick.datasets import load_occupancy from yellowbrick.model_selection import CVScores - - # Load the classification data set - data = load_data("occupancy") - - # Specify the features of interest - features = ["temperature", "relative humidity", "light", "C02", "humidity"] - - # Extract the instances and target - X = data[features] - y = data.occupancy - - # Create a new figure and axes - _, ax = plt.subplots() + # Load the classification dataset + X, y = load_occupancy() # Create a cross-validation strategy - cv = StratifiedKFold(12) + cv = StratifiedKFold(n_splits=12, random_state=42) - # Create the cv score visualizer - oz = CVScores( - MultinomialNB(), ax=ax, cv=cv, scoring='f1_weighted' - ) - - oz.fit(X, y) - oz.poof() + # Instantiate the classification model and visualizer + model = MultinomialNB() + visualizer = CVScores(model, cv=cv, scoring='f1_weighted') + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data Our resulting visualization shows that while our average cross-validation score is quite high, there are some splits for which our fitted ``MultinomialNB`` classifier performs significantly less well. -.. image:: images/cv_scores_classifier.png - - Regression ---------- -In this next example we show how to visualize cross-validated scores for a regression model. After loading our energy data into a ``DataFrame``, we instantiate a simple ``KFold`` cross-validation strategy. We then fit the ``CVScores`` visualizer using the ``r2`` scoring metric, to get a sense of the coefficient of determination for our regressor across all of our folds. +In this next example we show how to visualize cross-validated scores for a regression model. After loading our energy data as a ``DataFrame``, we instantiated a simple ``KFold`` cross-validation strategy. We then fit the ``CVScores`` visualizer using the ``r2`` scoring metric, to get a sense of the coefficient of determination for our regressor across all of our folds. -.. code:: python +.. plot:: + :context: close-figs + :alt: Cross validation on the energy data set using KFold from sklearn.linear_model import Ridge from sklearn.model_selection import KFold + from yellowbrick.datasets import load_energy + from yellowbrick.model_selection import CVScores - # Load the regression data set - data = load_data("energy") - - # Specify the features of interest and the target - targets = ["heating load", "cooling load"] - features = [col for col in data.columns if col not in targets] - - # Extract the instances and target - X = data[features] - y = data[targets[1]] - - # Create a new figure and axes - _, ax = plt.subplots() - - cv = KFold(12) + # Load the regression dataset + X, y = load_energy() - oz = CVScores( - Ridge(), ax=ax, cv=cv, scoring='r2' - ) + # Instantiate the regression model and visualizer + cv = KFold(n_splits=12, random_state=42) - oz.fit(X, y) - oz.poof() + model = Ridge() + visualizer = CVScores(model, cv=cv, scoring='r2') + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data As with our classification ``CVScores`` visualization, our regression visualization suggests that our ``Ridge`` regressor performs very well (e.g. produces a high coefficient of determination) across nearly every fold, resulting in another fairly high overall R2 score. -.. image:: images/cv_scores_regressor.png - - API Reference ------------- diff --git a/docs/api/model_selection/images/cv_scores_classifier.png b/docs/api/model_selection/images/cv_scores_classifier.png deleted file mode 100644 index 7a557a14a..000000000 Binary files a/docs/api/model_selection/images/cv_scores_classifier.png and /dev/null differ diff --git a/docs/api/model_selection/images/cv_scores_regressor.png b/docs/api/model_selection/images/cv_scores_regressor.png deleted file mode 100644 index 9fd292f18..000000000 Binary files a/docs/api/model_selection/images/cv_scores_regressor.png and /dev/null differ diff --git a/docs/api/model_selection/images/learning_curve_classifier.png b/docs/api/model_selection/images/learning_curve_classifier.png deleted file mode 100644 index 43740aa5a..000000000 Binary files a/docs/api/model_selection/images/learning_curve_classifier.png and /dev/null differ diff --git a/docs/api/model_selection/images/learning_curve_clusterer.png b/docs/api/model_selection/images/learning_curve_clusterer.png deleted file mode 100644 index 6cb8c9c97..000000000 Binary files a/docs/api/model_selection/images/learning_curve_clusterer.png and /dev/null differ diff --git a/docs/api/model_selection/images/learning_curve_regressor.png b/docs/api/model_selection/images/learning_curve_regressor.png deleted file mode 100644 index d76462474..000000000 Binary files a/docs/api/model_selection/images/learning_curve_regressor.png and /dev/null differ diff --git a/docs/api/model_selection/images/rfecv_credit.png b/docs/api/model_selection/images/rfecv_credit.png new file mode 100644 index 000000000..57c77c012 Binary files /dev/null and b/docs/api/model_selection/images/rfecv_credit.png differ diff --git a/docs/api/features/images/rfecv_sklearn_example.png b/docs/api/model_selection/images/rfecv_sklearn_example.png similarity index 100% rename from docs/api/features/images/rfecv_sklearn_example.png rename to docs/api/model_selection/images/rfecv_sklearn_example.png diff --git a/docs/api/model_selection/images/validation_curve_classifier_alt.png b/docs/api/model_selection/images/validation_curve_classifier_knn.png similarity index 100% rename from docs/api/model_selection/images/validation_curve_classifier_alt.png rename to docs/api/model_selection/images/validation_curve_classifier_knn.png diff --git a/docs/api/model_selection/images/validation_curve_classifier.png b/docs/api/model_selection/images/validation_curve_classifier_svc.png similarity index 100% rename from docs/api/model_selection/images/validation_curve_classifier.png rename to docs/api/model_selection/images/validation_curve_classifier_svc.png diff --git a/docs/api/model_selection/images/validation_curve_regressor.png b/docs/api/model_selection/images/validation_curve_regressor.png deleted file mode 100644 index d796e662e..000000000 Binary files a/docs/api/model_selection/images/validation_curve_regressor.png and /dev/null differ diff --git a/docs/api/model_selection/images/validation_curve_sklearn_example.png b/docs/api/model_selection/images/validation_curve_sklearn_example.png deleted file mode 100644 index cbbe3905b..000000000 Binary files a/docs/api/model_selection/images/validation_curve_sklearn_example.png and /dev/null differ diff --git a/docs/api/features/importances.rst b/docs/api/model_selection/importances.rst similarity index 65% rename from docs/api/features/importances.rst rename to docs/api/model_selection/importances.rst index 4fa7a8767..a9e6666f5 100644 --- a/docs/api/features/importances.rst +++ b/docs/api/model_selection/importances.rst @@ -17,44 +17,30 @@ Random Forest, Gradient Boosting, and Ada Boost provide a ``feature_importances_`` attribute when fitted. The Yellowbrick ``FeatureImportances`` visualizer utilizes this attribute to rank and plot relative importances. Let's start with an example; first load a -classification dataset as follows: +classification dataset. -.. code:: python - - # Load the classification data set - data = load_data("occupancy") - - # Specify the features of interest - features = [ - "temperature", "relative humidity", "light", "C02", "humidity" - ] - - # Extract the instances and target - X = data[features] - y = data.occupancy - -Once the dataset has been loaded, we can create a new figure (this is +Then we can create a new figure (this is optional, if an ``Axes`` isn't specified, Yellowbrick will use the current figure or create one). We can then fit a ``FeatureImportances`` visualizer -with a ``GradientBoostingClassifier`` to visualize the ranked features: - -.. code:: python +with a ``GradientBoostingClassifier`` to visualize the ranked features. - import matplotlib.pyplot as plt +.. plot:: + :context: close-figs + :alt: Feature importances of Random Forest classifier - from sklearn.ensemble import GradientBoostingClassifier + from sklearn.ensemble import RandomForestClassifier - from yellowbrick.features.importances import FeatureImportances + from yellowbrick.datasets import load_occupancy + from yellowbrick.model_selection import FeatureImportances - # Create a new matplotlib figure - fig = plt.figure() - ax = fig.add_subplot() + # Load the classification data set + X, y = load_occupancy() - viz = FeatureImportances(GradientBoostingClassifier(), ax=ax) + model = RandomForestClassifier(n_estimators=10) + viz = FeatureImportances(model) viz.fit(X, y) viz.poof() -.. image:: images/feature_importances.png The above figure shows the features ranked according to the explained variance each feature contributes to the model. In this case the features are plotted @@ -64,22 +50,7 @@ most important feature. The visualizer also contains ``features_`` and For models that do not support a ``feature_importances_`` attribute, the ``FeatureImportances`` visualizer will also draw a bar plot for the ``coef_`` -attribute that many linear models provide. First we start by loading a -regression dataset: - -.. code:: python - - # Load a regression data set - data = load_data("concrete") - - # Specify the features of interest - features = [ - "cement","slag","ash","water","splast","coarse","fine","age" - ] - - # Extract the instances and target - X = data[features] - y = data.strength +attribute that many linear models provide. When using a model with a ``coef_`` attribute, it is better to set ``relative=False`` to draw the true magnitude of the coefficient (which may @@ -87,30 +58,51 @@ be negative). We can also specify our own set of labels if the dataset does not have column names or to print better titles. In the example below we title case our features for better readability: -.. code:: python +.. plot:: + :context: close-figs + :alt: Coefficient importances for LASSO regression - import matplotlib.pyplot as plt - from sklearn.linear_model import Lasso - - from yellowbrick.features.importances import FeatureImportances + from yellowbrick.datasets import load_concrete + from yellowbrick.model_selection import FeatureImportances - # Create a new figure - fig = plt.figure() - ax = fig.add_subplot() + # Load the regression dataset + dataset = load_concrete(return_dataset=True) + X, y = dataset.to_data() # Title case the feature for better display and create the visualizer - labels = list(map(lambda s: s.title(), features)) - viz = FeatureImportances(Lasso(), ax=ax, labels=labels, relative=False) + labels = list(map(lambda s: s.title(), dataset.meta['features'])) + viz = FeatureImportances(Lasso(), labels=labels, relative=False) # Fit and show the feature importances viz.fit(X, y) viz.poof() +.. NOTE:: The interpretation of the importance of coeficients depends on the model; see the discussion below for more details. -.. image:: images/feature_importances_coef.png +Stacked Feature Importances +--------------------------- + +Some estimators return a multi-dimensonal array for either ``feature_importances_`` or ``coef_`` attributes. For example the ``LogisticRegression`` classifier returns a ``coef_`` array in the shape of ``(n_classes, n_features)`` in the multiclass case. These coefficients map the importance of the feature to the prediction of the probability of a specific class. Although the interpretation of multi-dimensional feature importances depends on the specific estimator and model family, the data is treated the same in the ``FeatureImportances`` visualizer -- namely the importances are averaged. + +Taking the mean of the importances may be undesirable for several reasons. For example, a feature may be more informative for some classes than others. Multi-output estimators also do not benefit from having averages taken across what are essentially multiple internal models. In this case, use the ``stack=True`` parameter to draw a stacked bar chart of importances as follows: + +.. plot:: + :context: close-figs + :alt: Stacked per-class importances with Logistic Regression + + from yellowbrick.model_selection import FeatureImportances + from sklearn.linear_model import LogisticRegression + from sklearn.datasets import load_iris + + data = load_iris() + X, y = data.data, data.target + + model = LogisticRegression(multi_class="auto", solver="liblinear") + viz = FeatureImportances(model, stack=True, relative=False) + viz.fit(X, y) + viz.poof() -.. NOTE:: The interpretation of the importance of coeficients depends on the model; see the discussion below for more details. Discussion ---------- @@ -138,7 +130,7 @@ This method may also be used for instances; but generally there are very many in API Reference ------------- -.. automodule:: yellowbrick.features.importances +.. automodule:: yellowbrick.model_selection.importances :members: FeatureImportances :undoc-members: :show-inheritance: diff --git a/docs/api/model_selection/index.rst b/docs/api/model_selection/index.rst index f0744341d..126490862 100644 --- a/docs/api/model_selection/index.rst +++ b/docs/api/model_selection/index.rst @@ -12,6 +12,8 @@ The currently implemented model selection visualizers are as follows: - :doc:`validation_curve`: visualizes how the adjustment of a hyperparameter influences training and test scores to tune the bias/variance trade-off. - :doc:`learning_curve`: shows how the size of training data influences the model to diagnose if a model suffers more from variance error vs. bias error. - :doc:`cross_validation`: displays cross-validated scores as a bar chart with average as a horizontal line. +- :doc:`importances`: rank features by relative importance in a model +- :doc:`rfecv`: select a subset of features by importance Model selection makes heavy use of cross validation to measure the performance of an estimator. Cross validation splits a dataset into a training data set and a test data set; the model is fit on the training data and evaluated on the test data. This helps avoid a common pitfall, overfitting, where the model simply memorizes the training data and does not generalize well to new or unknown input. @@ -23,3 +25,5 @@ There are many ways to define how to split a dataset for cross validation. For m validation_curve learning_curve cross_validation + importances + rfecv diff --git a/docs/api/model_selection/learning_curve.py b/docs/api/model_selection/learning_curve.py deleted file mode 100644 index af4673820..000000000 --- a/docs/api/model_selection/learning_curve.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# learning_curve.py - -""" -Generates the learning curve visualizations for the documentation -""" - -########################################################################## -## Imports -########################################################################## - -import os -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -from sklearn.svm import SVC -from sklearn.cluster import KMeans -from sklearn.datasets import make_blobs -from sklearn.datasets import load_digits -from sklearn.linear_model import RidgeCV -from sklearn.naive_bayes import GaussianNB -from sklearn.naive_bayes import MultinomialNB -from sklearn.model_selection import ShuffleSplit -from sklearn.model_selection import StratifiedKFold - -from yellowbrick.model_selection import LearningCurve - - -FIXTURES = os.path.join("..", "..", "..", "examples", "data") - - -########################################################################## -## Helper Methods -########################################################################## - -def learning_curve_sklearn_example(path="images/learning_curve_sklearn_example.png"): - digits = load_digits() - X, y = digits.data, digits.target - - _, ax = plt.subplots(nrows=1, ncols=2, sharey=True, figsize=(9,4)) - - cv = ShuffleSplit(n_splits=100, test_size=0.2, random_state=0) - oz = LearningCurve(GaussianNB(), ax=ax[0], cv=cv, n_jobs=4) - oz.fit(X, y) - oz.finalize() - - cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0) - oz = LearningCurve(SVC(gamma=0.001), ax=ax[1], cv=cv, n_jobs=4) - oz.fit(X, y) - oz.poof(outpath=path) - - -def learning_curve_classifier(path="images/learning_curve_classifier.png"): - - data = pd.read_csv(os.path.join(FIXTURES, "game", "game.csv")) - - target = "outcome" - features = [col for col in data.columns if col != target] - - X = pd.get_dummies(data[features]) - y = data[target] - - _, ax = plt.subplots() - cv = StratifiedKFold(12) - sizes = np.linspace(0.3, 1.0, 10) - - oz = LearningCurve( - MultinomialNB(), ax=ax, cv=cv, n_jobs=4, - train_sizes=sizes, scoring='f1_weighted' - ) - - oz.fit(X, y) - oz.poof(outpath=path) - - -def learning_curve_regressor(path="images/learning_curve_regressor.png"): - - data = pd.read_csv(os.path.join(FIXTURES, "energy", "energy.csv")) - - targets = ["heating load", "cooling load"] - features = [col for col in data.columns if col not in targets] - - X = data[features] - y = data[targets[0]] - - _, ax = plt.subplots() - sizes = np.linspace(0.3, 1.0, 10) - - oz = LearningCurve(RidgeCV(), ax=ax, train_sizes=sizes, scoring='r2') - oz.fit(X, y) - oz.poof(outpath=path) - - -def learning_curve_clusterer(path="images/learning_curve_clusterer.png"): - - X, y = make_blobs(n_samples=1000, centers=5) - - _, ax = plt.subplots() - sizes = np.linspace(0.3, 1.0, 10) - - oz = LearningCurve( - KMeans(), ax=ax, train_sizes=sizes, scoring="adjusted_rand_score" - ) - oz.fit(X, y) - oz.poof(outpath=path) - -########################################################################## -## Main Method -########################################################################## - -if __name__ == '__main__': - learning_curve_sklearn_example() - learning_curve_classifier() - learning_curve_regressor() - learning_curve_clusterer() diff --git a/docs/api/model_selection/learning_curve.rst b/docs/api/model_selection/learning_curve.rst index cd6b860ea..e903e4296 100644 --- a/docs/api/model_selection/learning_curve.rst +++ b/docs/api/model_selection/learning_curve.rst @@ -3,7 +3,7 @@ Learning Curve ============== -A learning curve shows the relationship of the training score vs the cross validated test score for an estimator with a varying number of training samples. This visualization is typically used two show two things: +A learning curve shows the relationship of the training score versus the cross validated test score for an estimator with a varying number of training samples. This visualization is typically used two show two things: 1. How much the estimator benefits from more data (e.g. do we have "enough data" or will the estimator get better if used in an online fashion). 2. If the estimator is more sensitive to error due to variance vs. error due to bias. @@ -12,7 +12,7 @@ Consider the following learning curves (generated with Yellowbrick, but from `Pl .. image:: images/learning_curve_sklearn_example.png -If the training and cross validation scores converge together as more data is added (shown in the left figure), then the model will probably not benefit from more data. If the training score is much greater than the validation score (as shown in the right figure) then the model probably requires more training examples in order to generalize more effectively. +If the training and cross-validation scores converge together as more data is added (shown in the left figure), then the model will probably not benefit from more data. If the training score is much greater than the validation score then the model probably requires more training examples in order to generalize more effectively. The curves are plotted with the mean scores, however variability during cross-validation is shown with the shaded areas that represent a standard deviation above and below the mean for all cross-validations. If the model suffers from error due to bias, then there will likely be more variability around the training score curve. If the model suffers from error due to variance, then there will be more variability around the cross validated score. @@ -21,41 +21,40 @@ The curves are plotted with the mean scores, however variability during cross-va Classification -------------- -In the following example we show how to visualize the learning curve of a classification model. After loading a ``DataFrame`` and performing categorical encoding, we create a ``StratifiedKFold`` cross-validation strategy to ensure all of our classes in each split are represented with the same proportion. We then fit the visualizer using the ``f1_weighted`` scoring metric as opposed to the default metric, accuracy, to get a better sense of the relationship of precision and recall in our classifier. +In the following example, we show how to visualize the learning curve of a classification model. After loading a ``DataFrame`` and performing categorical encoding, we create a ``StratifiedKFold`` cross-validation strategy to ensure all of our classes in each split are represented with the same proportion. We then fit the visualizer using the ``f1_weighted`` scoring metric as opposed to the default metric, accuracy, to get a better sense of the relationship of precision and recall in our classifier. -.. code:: python +.. plot:: + :context: close-figs + :alt: Learning Curve on the Game dataset using StratifiedKFold import numpy as np - from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import StratifiedKFold - from yellowbrick.model_selection import LearningCurve + from sklearn.naive_bayes import MultinomialNB + from sklearn.preprocessing import OneHotEncoder, LabelEncoder - # Load a classification data set - data = load_data('game') + from yellowbrick.datasets import load_game + from yellowbrick.model_selection import LearningCurve - # Specify the features of interest and the target - target = "outcome" - features = [col for col in data.columns if col != target] + # Load a classification dataset + X, y = load_game() - # Encode the categorical data with one-hot encoding - X = pd.get_dummies(data[features]) - y = data[target] + # Encode the categorical data + X = OneHotEncoder().fit_transform(X) + y = LabelEncoder().fit_transform(y) # Create the learning curve visualizer - cv = StratifiedKFold(12) + cv = StratifiedKFold(n_splits=12) sizes = np.linspace(0.3, 1.0, 10) - viz = LearningCurve( - MultinomialNB(), cv=cv, train_sizes=sizes, - scoring='f1_weighted', n_jobs=4 + # Instantiate the classification model and visualizer + model = MultinomialNB() + visualizer = LearningCurve( + model, cv=cv, scoring='f1_weighted', train_sizes=sizes, n_jobs=4 ) - # Fit and poof the visualizer - viz.fit(X, y) - viz.poof() - -.. image:: images/learning_curve_classifier.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data This learning curve shows high test variability and a low score up to around 30,000 instances, however after this level the model begins to converge on an F1 score of around 0.6. We can see that the training and test scores have not yet converged, so potentially this model would benefit from more training data. Finally, this model suffers primarily from error due to variance (the CV scores for the test data are more variable than for training data) so it is possible that the model is overfitting. @@ -64,27 +63,24 @@ Regression Building a learning curve for a regression is straight forward and very similar. In the below example, after loading our data and selecting our target, we explore the learning curve score according to the coefficient of determination or R2 score. -.. code:: python +.. plot:: + :context: close-figs + :alt: Learning Curve on the Energy dataset using RidgeCV from sklearn.linear_model import RidgeCV - # Load a regression dataset - data = load_data('energy') - - # Specify features of interest and the target - targets = ["heating load", "cooling load"] - features = [col for col in data.columns if col not in targets] + from yellowbrick.datasets import load_energy + from yellowbrick.model_selection import LearningCurve - # Extract the instances and target - X = data[features] - y = data[targets[0]] + # Load a regression dataset + X, y = load_energy() - # Create the learning curve visualizer, fit and poof - viz = LearningCurve(RidgeCV(), train_sizes=sizes, scoring='r2') - viz.fit(X, y) - viz.poof() + # Instantiate the regression model and visualizer + model = RidgeCV() + visualizer = LearningCurve(model, scoring='r2') -.. image:: images/learning_curve_regressor.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data This learning curve shows a very high variability and much lower score until about 350 instances. It is clear that this model could benefit from more data because it is converging at a very high score. Potentially, with more data and a larger alpha for regularization, this model would become far less variable in the test data. @@ -93,23 +89,24 @@ Clustering Learning curves also work for clustering models and can use metrics that specify the shape or organization of clusters such as silhouette scores or density scores. If the membership is known in advance, then rand scores can be used to compare clustering performance as shown below: -.. code:: python +.. plot:: + :context: close-figs + :alt: Learning Curve on clustering models from sklearn.cluster import KMeans from sklearn.datasets import make_blobs - # Create a dataset of blobs - X, y = make_blobs(n_samples=1000, centers=5) - - viz = LearningCurve( - KMeans(), train_sizes=sizes, scoring="adjusted_rand_score" - ) + from yellowbrick.model_selection import LearningCurve - viz.fit(X, y) - viz.poof() + # Generate synthetic dataset with 5 random clusters + X, y = make_blobs(n_samples=1000, centers=5, random_state=42) + # Instantiate the clustering model and visualizer + model = KMeans() + visualizer = LearningCurve(model, scoring="adjusted_rand_score", random_state=42) -.. image:: images/learning_curve_clusterer.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data Unfortunately, with random data these curves are highly variable, but serve to point out some clustering-specific items. First, note the y-axis is very narrow, roughly speaking these curves are converged and actually the clustering algorithm is performing very well. Second, for clustering, convergence for data points is not necessarily a bad thing; in fact we want to ensure as more data is added, the training and cross-validation scores do not diverge. diff --git a/docs/api/features/rfecv.py b/docs/api/model_selection/rfecv.py similarity index 54% rename from docs/api/features/rfecv.py rename to docs/api/model_selection/rfecv.py index 2cd17b13c..05e7a5d06 100644 --- a/docs/api/features/rfecv.py +++ b/docs/api/model_selection/rfecv.py @@ -1,50 +1,57 @@ #!/usr/bin/env python3 +# rfecv.py # Generates RFECV visualizations for the documentation +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: rfecv.py [] $ import os -import pandas as pd import matplotlib.pyplot as plt from sklearn.svm import SVC -from yellowbrick.features import RFECV from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold -CWD = os.path.dirname(__file__) -DATA = os.path.join(CWD, "..", "..", "..", "examples", "data") +from yellowbrick.model_selection import RFECV +from yellowbrick.datasets import load_credit + + +CWD = os.path.dirname(__file__) IMAGES = os.path.join(CWD, "images") def rfecv_sklearn_example(image="rfecv_sklearn_example.png"): X, y = make_classification( - n_samples=1000, n_features=25, n_informative=3, n_redundant=2, - n_repeated=0, n_classes=8, n_clusters_per_class=1, random_state=0 + n_samples=1000, + n_features=25, + n_informative=3, + n_redundant=2, + n_repeated=0, + n_classes=8, + n_clusters_per_class=1, + random_state=0, ) _, ax = plt.subplots() - oz = RFECV(SVC(kernel='linear', C=1), ax=ax) + oz = RFECV(SVC(kernel="linear", C=1), ax=ax) oz.fit(X, y) oz.poof(outpath=os.path.join(IMAGES, image)) def rfecv_credit_example(image="rfecv_credit.png"): - data = pd.read_csv(os.path.join(DATA, "credit", "credit.csv")) - - target = "default" - features = [col for col in data.columns if col != target] - - X = data[features] - y = data[target] + X, y = load_credit() _, ax = plt.subplots() cv = StratifiedKFold(5) - oz = RFECV(RandomForestClassifier(), ax=ax, cv=cv, scoring='f1_weighted') + oz = RFECV(RandomForestClassifier(), ax=ax, cv=cv, scoring="f1_weighted") oz.fit(X, y) oz.poof(outpath=os.path.join(IMAGES, image)) -if __name__ == '__main__': +if __name__ == "__main__": rfecv_sklearn_example() rfecv_credit_example() diff --git a/docs/api/features/rfecv.rst b/docs/api/model_selection/rfecv.rst similarity index 77% rename from docs/api/features/rfecv.rst rename to docs/api/model_selection/rfecv.rst index 39b61b12a..9d4228319 100644 --- a/docs/api/features/rfecv.rst +++ b/docs/api/model_selection/rfecv.rst @@ -9,12 +9,15 @@ RFE requires a specified number of features to keep, however it is often not kno To show how this works in practice, we'll start with a contrived example using a dataset that has only 3 informative features out of 25. +.. note to contributors: the below code takes a long time to run so has not been + modified with a plot directive. See rfecv.py to regenerate images. + .. code:: python from sklearn.svm import SVC from sklearn.datasets import make_classification - from yellowbrick.features import RFECV + from yellowbrick.model_selection import RFECV # Create a dataset with only 3 informative features X, y = make_classification( @@ -22,10 +25,11 @@ To show how this works in practice, we'll start with a contrived example using a n_repeated=0, n_classes=8, n_clusters_per_class=1, random_state=0 ) - # Create RFECV visualizer with linear SVM classifier - viz = RFECV(SVC(kernel='linear', C=1)) - viz.fit(X, y) - viz.poof() + # Instantiate RFECV visualizer with a linear SVM classifier + visualizer = RFECV(SVC(kernel='linear', C=1)) + + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data .. image:: images/rfecv_sklearn_example.png @@ -33,24 +37,25 @@ This figure shows an ideal RFECV curve, the curve jumps to an excellent accuracy Exploring a real dataset, we can see the impact of RFECV on a credit default binary classifier. +.. note to contributors: the below code takes a long time to run so has not been + modified with a plot directive. See rfecv.py to regenerate images. + .. code:: python from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold - df = load_data('credit') - - target = 'default' - features = [col for col in data.columns if col != target] + from yellowbrick.model_selection import RFECV + from yellowbrick.datasets import load_credit - X = data[features] - y = data[target] + # Load classification dataset + X, y = load_credit() cv = StratifiedKFold(5) - oz = RFECV(RandomForestClassifier(), cv=cv, scoring='f1_weighted') + visualizer = RFECV(RandomForestClassifier(), cv=cv, scoring='f1_weighted') - oz.fit(X, y) - oz.poof() + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data .. image:: images/rfecv_credit.png @@ -61,7 +66,7 @@ In this example we can see that 19 features were selected, though there doesn't API Reference ------------- -.. automodule:: yellowbrick.features.rfecv +.. automodule:: yellowbrick.model_selection.rfecv :members: RFECV :undoc-members: :show-inheritance: diff --git a/docs/api/model_selection/validation_curve.py b/docs/api/model_selection/validation_curve.py index e4f26ad22..be6b99b44 100644 --- a/docs/api/model_selection/validation_curve.py +++ b/docs/api/model_selection/validation_curve.py @@ -9,27 +9,27 @@ ## Imports ########################################################################## -import os import numpy as np -import pandas as pd import matplotlib.pyplot as plt from sklearn.svm import SVC from sklearn.datasets import load_digits -from sklearn.tree import DecisionTreeRegressor +from sklearn.preprocessing import OneHotEncoder from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import StratifiedKFold +from yellowbrick.datasets import load_game from yellowbrick.model_selection import ValidationCurve -FIXTURES = os.path.join("..", "..", "..", "examples", "data") - ########################################################################## ## Helper Methods ########################################################################## -def validation_curve_sklearn_example(path="images/validation_curve_sklearn_example.png"): + +def validation_curve_sklearn_example( + path="images/validation_curve_sklearn_example.png" +): digits = load_digits() X, y = digits.data, digits.target @@ -37,71 +37,61 @@ def validation_curve_sklearn_example(path="images/validation_curve_sklearn_examp param_range = np.logspace(-6, -1, 5) oz = ValidationCurve( - SVC(), ax=ax, param_name="gamma", param_range=param_range, - logx=True, cv=10, scoring="accuracy", n_jobs=4 + SVC(), + ax=ax, + param_name="gamma", + param_range=param_range, + logx=True, + cv=10, + scoring="accuracy", + n_jobs=4, ) oz.fit(X, y) oz.poof(outpath=path) -def validation_curve_classifier(path="images/validation_curve_classifier.png"): - data = pd.read_csv(os.path.join(FIXTURES, "game", "game.csv")) - - target = "outcome" - features = [col for col in data.columns if col != target] - - X = pd.get_dummies(data[features]) - y = data[target] +def validation_curve_classifier_svc(path="images/validation_curve_classifier_svc.png"): + X, y = load_game() + X = OneHotEncoder().fit_transform(X) _, ax = plt.subplots() cv = StratifiedKFold(12) param_range = np.logspace(-6, -1, 12) + print("warning: generating the SVC validation curve can take a very long time!") + oz = ValidationCurve( - SVC(), ax=ax, param_name="gamma", param_range=param_range, - logx=True, cv=cv, scoring="f1_weighted", n_jobs=8, + SVC(), + ax=ax, + param_name="gamma", + param_range=param_range, + logx=True, + cv=cv, + scoring="f1_weighted", + n_jobs=8, ) oz.fit(X, y) oz.poof(outpath=path) -def validation_curve_classifier_alt(path="images/validation_curve_classifier_alt.png"): - data = pd.read_csv(os.path.join(FIXTURES, "game", "game.csv")) - - target = "outcome" - features = [col for col in data.columns if col != target] - - X = pd.get_dummies(data[features]) - y = data[target] +def validation_curve_classifier_knn(path="images/validation_curve_classifier_knn.png"): + X, y = load_game() + X = OneHotEncoder().fit_transform(X) _, ax = plt.subplots() cv = StratifiedKFold(4) param_range = np.arange(3, 20, 2) - oz = ValidationCurve( - KNeighborsClassifier(), ax=ax, param_name="n_neighbors", - param_range=param_range, cv=cv, scoring="f1_weighted", n_jobs=8, - ) - oz.fit(X, y) - oz.poof(outpath=path) - - -def validation_curve_regressor(path="images/validation_curve_regressor.png"): - - data = pd.read_csv(os.path.join(FIXTURES, "energy", "energy.csv")) - - targets = ["heating load", "cooling load"] - features = [col for col in data.columns if col not in targets] - - X = data[features] - y = data[targets[1]] - - _, ax = plt.subplots() - param_range = np.arange(1, 11) + print("warning: generating the KNN validation curve can take a very long time!") oz = ValidationCurve( - DecisionTreeRegressor(), ax=ax, param_name="max_depth", - param_range=param_range, cv=10, scoring="r2", n_jobs=8, + KNeighborsClassifier(), + ax=ax, + param_name="n_neighbors", + param_range=param_range, + cv=cv, + scoring="f1_weighted", + n_jobs=8, ) oz.fit(X, y) oz.poof(outpath=path) @@ -111,8 +101,7 @@ def validation_curve_regressor(path="images/validation_curve_regressor.png"): ## Main Method ########################################################################## -if __name__ == '__main__': - validation_curve_sklearn_example() - # validation_curve_classifier() - validation_curve_classifier_alt() - validation_curve_regressor() +if __name__ == "__main__": + # validation_curve_sklearn_example() + validation_curve_classifier_svc() + validation_curve_classifier_knn() diff --git a/docs/api/model_selection/validation_curve.rst b/docs/api/model_selection/validation_curve.rst index bd9c339fb..2c152c8a3 100644 --- a/docs/api/model_selection/validation_curve.rst +++ b/docs/api/model_selection/validation_curve.rst @@ -9,23 +9,19 @@ In order to maximize the score, the hyperparameters of the model must be selecte In our first example, we'll explore using the ``ValidationCurve`` visualizer with a regression dataset and in the second, a classification dataset. Note that any estimator that implements ``fit()`` and ``predict()`` and has an appropriate scoring mechanism can be used with this visualizer. -.. code:: python +.. plot:: + :context: close-figs + :alt: Validation Curve for Max Depth of Decision Tree regressor import numpy as np - from sklearn.tree import DecisionTreeRegressor + from yellowbrick.datasets import load_energy from yellowbrick.model_selection import ValidationCurve - # Load a regression dataset - data = load_data('energy') - - # Specify features of interest and the target - targets = ["heating load", "cooling load"] - features = [col for col in data.columns if col not in targets] + from sklearn.tree import DecisionTreeRegressor - # Extract the instances and target - X = data[features] - y = data[targets[0]] + # Load a regression dataset + X, y = load_energy() viz = ValidationCurve( DecisionTreeRegressor(), param_name="max_depth", @@ -36,29 +32,26 @@ In our first example, we'll explore using the ``ValidationCurve`` visualizer wit viz.fit(X, y) viz.poof() -.. image:: images/validation_curve_regressor.png - After loading and wrangling the data, we initialize the ``ValidationCurve`` with a ``DecisionTreeRegressor``. Decision trees become more overfit the deeper they are because at each level of the tree the partitions are dealing with a smaller subset of data. One way to deal with this overfitting process is to limit the depth of the tree. The validation curve explores the relationship of the ``"max_depth"`` parameter to the R2 score with 10 shuffle split cross-validation. The ``param_range`` argument specifies the values of ``max_depth``, here from 1 to 10 inclusive. We can see in the resulting visualization that a depth limit of less than 5 levels severely underfits the model on this data set because the training score and testing score climb together in this parameter range, and because of the high variability of cross validation on the test scores. After a depth of 7, the training and test scores diverge, this is because deeper trees are beginning to overfit the training data, providing no generalizability to the model. However, because the cross validation score does not necessarily decrease, the model is not suffering from high error due to variance. In the next visualizer, we will see an example that more dramatically visualizes the bias/variance tradeoff. +.. note to contributors: the below code takes a long time to run so has not been + modified with a plot directive. See validation_curve.py to regenerate images. + .. code:: python from sklearn.svm import SVC + from sklearn.preprocessing import OneHotEncoder from sklearn.model_selection import StratifiedKFold # Load a classification data set - data = load_data('game') - - # Specify the features of interest and the target - target = "outcome" - features = [col for col in data.columns if col != target] + X, y = load_game() # Encode the categorical data with one-hot encoding - X = pd.get_dummies(data[features]) - y = data[target] + X = OneHotEncoder().fit_transform(X) # Create the validation curve visualizer cv = StratifiedKFold(12) @@ -73,16 +66,19 @@ In the next visualizer, we will see an example that more dramatically visualizes viz.poof() -.. image:: images/validation_curve_classifier.png +.. image:: images/validation_curve_classifier_svc.png After loading data and one-hot encoding it using the Pandas ``get_dummies`` function, we create a stratified k-folds cross-validation strategy. The hyperparameter of interest is the gamma of a support vector classifier, the coefficient of the RBF kernel. Gamma controls how much influence a single example has, the larger gamma is, the tighter the support vector is around single points (overfitting the model). In this visualization we see a definite inflection point around ``gamma=0.1``. At this point the training score climbs rapidly as the SVC memorizes the data, while the cross-validation score begins to decrease as the model cannot generalize to unseen data. -.. warning:: Note that running this example may take a long time. Even with parallelism using n_jobs=8, this can take several hours. +.. warning:: Note that running this and the next example may take a long time. Even with parallelism using n_jobs=8, it can take several hours to go through all the combinations. Reducing the parameter range and minimizing the amount of cross-validation can speed up the validation curve visualization. Validation curves can be performance intensive since they are training ``n_params * n_splits`` models and scoring them. It is critically important to ensure that the specified hyperparameter range is correct, as we will see in the next example. +.. note to contributors: the below code takes a long time to run so has not been + modified with a plot directive. See validation_curve.py to regenerate images. + .. code:: python from sklearn.neighbors import KNeighborsClassifier @@ -99,8 +95,7 @@ Validation curves can be performance intensive since they are training ``n_param oz.fit(X, y) oz.poof() - -.. image:: images/validation_curve_classifier_alt.png +.. image:: images/validation_curve_classifier_knn.png The k nearest neighbors (kNN) model is commonly used when similarity is important to the interpretation of the model. Choosing k is difficult, the higher k is the more data is included in a classification, creating more complex decision topologies, whereas the lower k is, the simpler the model is and the less it may generalize. Using a validation curve seems like an excellent strategy for choosing k, and often it is. However in the example above, all we can see is a decreasing variability in the cross-validated scores. diff --git a/docs/api/regressor/alphas.py b/docs/api/regressor/alphas.py deleted file mode 100644 index 40780c166..000000000 --- a/docs/api/regressor/alphas.py +++ /dev/null @@ -1,25 +0,0 @@ -import numpy as np -import pandas as pd - -from sklearn.linear_model import LassoCV - -from yellowbrick.regressor import AlphaSelection - - -if __name__ == '__main__': - # Load the regression data set - df = pd.read_csv("../../../examples/data/concrete/concrete.csv") - - feature_names = ['cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age'] - target_name = 'strength' - - # Get the X and y data from the DataFrame - X = df[feature_names] - y = df[target_name] - - # Instantiate the linear model and visualizer - alphas = np.logspace(-10, 1, 400) - visualizer = AlphaSelection(LassoCV(alphas=alphas)) - - visualizer.fit(X, y) - g = visualizer.poof(outpath="images/alpha_selection.png") diff --git a/docs/api/regressor/alphas.rst b/docs/api/regressor/alphas.rst index a94d889c2..dcbfa7624 100644 --- a/docs/api/regressor/alphas.rst +++ b/docs/api/regressor/alphas.rst @@ -7,27 +7,18 @@ Regularization is designed to penalize model complexity, therefore the higher th The AlphaSelection Visualizer demonstrates how different values of alpha influence model selection during the regularization of linear models. Generally speaking, alpha increases the affect of regularization, e.g. if alpha is zero there is no regularization and the higher the alpha, the more the regularization parameter influences the final model. -.. code:: python - - # Load the regression data set - df = load_data('concrete') - - # Specify the features of interest and the target - features = [ - 'cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age' - ] - target = 'strength' - - # Extract the instances and target - X = df[features] - y = df[target] - -.. code:: python +.. plot:: + :context: close-figs + :alt: Alpha selection on the concrete data set import numpy as np from sklearn.linear_model import LassoCV from yellowbrick.regressor import AlphaSelection + from yellowbrick.datasets import load_concrete + + # Load the regression dataset + X, y = load_concrete() # Create a list of alphas to cross-validate against alphas = np.logspace(-10, 1, 400) @@ -35,12 +26,9 @@ The AlphaSelection Visualizer demonstrates how different values of alpha influen # Instantiate the linear model and visualizer model = LassoCV(alphas=alphas) visualizer = AlphaSelection(model) - visualizer.fit(X, y) - g = visualizer.poof() - + visualizer.poof() -.. image:: images/alpha_selection.png API Reference diff --git a/docs/api/regressor/images/alpha_selection.png b/docs/api/regressor/images/alpha_selection.png deleted file mode 100644 index 4a515cbc2..000000000 Binary files a/docs/api/regressor/images/alpha_selection.png and /dev/null differ diff --git a/docs/api/regressor/images/prediction_error.png b/docs/api/regressor/images/prediction_error.png deleted file mode 100644 index a649e12b8..000000000 Binary files a/docs/api/regressor/images/prediction_error.png and /dev/null differ diff --git a/docs/api/regressor/images/residuals.png b/docs/api/regressor/images/residuals.png deleted file mode 100644 index eb6f17fb1..000000000 Binary files a/docs/api/regressor/images/residuals.png and /dev/null differ diff --git a/docs/api/regressor/images/residuals_no_hist.png b/docs/api/regressor/images/residuals_no_hist.png deleted file mode 100644 index 48cf08ba0..000000000 Binary files a/docs/api/regressor/images/residuals_no_hist.png and /dev/null differ diff --git a/docs/api/regressor/index.rst b/docs/api/regressor/index.rst index 388de01b7..35898d16e 100644 --- a/docs/api/regressor/index.rst +++ b/docs/api/regressor/index.rst @@ -36,3 +36,4 @@ the final step in a ``Pipeline`` or ``VisualPipeline``. residuals peplot alphas + influence \ No newline at end of file diff --git a/docs/api/regressor/influence.rst b/docs/api/regressor/influence.rst new file mode 100644 index 000000000..6aeafb5ea --- /dev/null +++ b/docs/api/regressor/influence.rst @@ -0,0 +1,36 @@ +.. -*- mode: rst -*- + +Cook's Distance +=============== + +Cook's Distance is a measure of an observation or instances' influence on a linear +regression. Instances with a large influence may be outliers and datasets that have a +large number of highly influential points might not be good predictors to fit linear +models. The ``CooksDistance`` visualizer shows a stem plot of all instances by index +and their associated distance score, along with a heuristic threshold to quickly show +what percent of the dataset may be impacting OLS regression models. + +.. plot:: + :context: close-figs + :alt: Cook's distance using concrete dataset + + from yellowbrick.regressor import CooksDistance + from yellowbrick.datasets import load_concrete + + # Load the regression dataset + X, y = load_concrete() + + # Instantiate and fit the visualizer + visualizer = CooksDistance() + visualizer.fit(X, y) + visualizer.poof() + + +API Reference +------------- + +.. automodule:: yellowbrick.regressor.influence + :members: CooksDistance + :undoc-members: + :show-inheritance: + diff --git a/docs/api/regressor/peplot.py b/docs/api/regressor/peplot.py deleted file mode 100644 index f341f5a88..000000000 --- a/docs/api/regressor/peplot.py +++ /dev/null @@ -1,29 +0,0 @@ -import pandas as pd - -from sklearn.linear_model import Lasso -from sklearn.model_selection import train_test_split - -from yellowbrick.regressor import PredictionError - - -if __name__ == '__main__': - # Load the regression data set - df = pd.read_csv("../../../examples/data/concrete/concrete.csv") - - feature_names = ['cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age'] - target_name = 'strength' - - # Get the X and y data from the DataFrame - X = df[feature_names] - y = df[target_name] - - # Create the train and test data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) - - # Instantiate the linear model and visualizer - lasso = Lasso() - visualizer = PredictionError(lasso) - - visualizer.fit(X_train, y_train) # Fit the training data to the visualizer - visualizer.score(X_test, y_test) # Evaluate the model on the test data - g = visualizer.poof(outpath="images/prediction_error.png") # Draw/show/poof the data diff --git a/docs/api/regressor/peplot.rst b/docs/api/regressor/peplot.rst index 0988c93e7..a42a74cb6 100644 --- a/docs/api/regressor/peplot.rst +++ b/docs/api/regressor/peplot.rst @@ -5,42 +5,28 @@ Prediction Error Plot A prediction error plot shows the actual targets from the dataset against the predicted values generated by our model. This allows us to see how much variance is in the model. Data scientists can diagnose regression models using this plot by comparing against the 45 degree line, where the prediction exactly matches the model. -.. code:: python +.. plot:: + :context: close-figs + :alt: Prediction Error plot on the Concrete dataset using a linear model from sklearn.model_selection import train_test_split + from sklearn.linear_model import Lasso + from yellowbrick.datasets import load_concrete + from yellowbrick.regressor import PredictionError - # Load the regression data set - data = load_data('concrete') - - # Specify the features of interest and the target - features = ['cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age'] - target = 'strength' - - # Extract the instances and target - X = data[features] - y = data[target] + # Load a regression dataset + X, y = load_concrete() # Create the train and test data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) - -.. code:: python - - from sklearn.linear_model import Lasso - - from yellowbrick.regressor import PredictionError + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Instantiate the linear model and visualizer - lasso = Lasso() - visualizer = PredictionError(lasso) + model = Lasso() + visualizer = PredictionError(model) visualizer.fit(X_train, y_train) # Fit the training data to the visualizer visualizer.score(X_test, y_test) # Evaluate the model on the test data - g = visualizer.poof() # Draw/show/poof the data - - - -.. image:: images/prediction_error.png - + visualizer.poof() # Draw/show/poof the data API Reference ------------- @@ -49,3 +35,4 @@ API Reference :members: PredictionError :undoc-members: :show-inheritance: + :noindex: \ No newline at end of file diff --git a/docs/api/regressor/residuals.py b/docs/api/regressor/residuals.py deleted file mode 100644 index 2b6236b39..000000000 --- a/docs/api/regressor/residuals.py +++ /dev/null @@ -1,44 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -from sklearn.linear_model import Ridge -from sklearn.model_selection import train_test_split - -from yellowbrick.regressor import ResidualsPlot - - -def plot_residuals(X, y, model, outpath="images/residuals.png", **kwargs): - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) - - _, ax = plt.subplots() - - visualizer = ResidualsPlot(model, ax=ax, **kwargs) - visualizer.fit(X_train, y_train) - visualizer.score(X_test, y_test) - visualizer.poof(outpath=outpath) - - -def load_concrete(): - # Load the regression data set - df = pd.read_csv("../../../examples/data/concrete/concrete.csv") - - feature_names = [ - 'cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age' - ] - target_name = 'strength' - - # Get the X and y data from the DataFrame - X = df[feature_names] - y = df[target_name] - - return X, y - - - -if __name__ == '__main__': - # Draw the default residuals graph - X, y = load_concrete() - plot_residuals(X, y, Ridge()) - - # Draw the residuals graph with no histogram - plot_residuals(X, y, Ridge(), "images/residuals_no_hist.png", hist=False) diff --git a/docs/api/regressor/residuals.rst b/docs/api/regressor/residuals.rst index 5689696d1..5adb3f045 100644 --- a/docs/api/regressor/residuals.rst +++ b/docs/api/regressor/residuals.rst @@ -3,58 +3,46 @@ Residuals Plot ============== -Residuals, in the context of regression models, are the difference between the observed value of the target variable (y) and the predicted value (ŷ), e.g. the error of the prediction. The residuals plot shows the difference between residuals on the vertical axis and the dependent variable on the horizontal axis, allowing you to detect regions within the target that may be susceptible to more or less error. +Residuals, in the context of regression models, are the difference between the observed value of the target variable (y) and the predicted value (ŷ), i.e. the error of the prediction. The residuals plot shows the difference between residuals on the vertical axis and the dependent variable on the horizontal axis, allowing you to detect regions within the target that may be susceptible to more or less error. -.. code:: python +.. plot:: + :context: close-figs + :alt: Residuals Plot on the Concrete dataset using a linear model from sklearn.model_selection import train_test_split + from sklearn.linear_model import Ridge + from yellowbrick.datasets import load_concrete + from yellowbrick.regressor import ResidualsPlot - # Load the data - df = load_data('concrete') - - # Identify the feature and target columns - feature_names = [ - 'cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age' - ] - target_name = 'strength' - - # Separate the instance data from the target data - X = df[feature_names] - y = df[target_name] + # Load a regression dataset + X, y = load_concrete() # Create the train and test data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) - -.. code:: python - - from sklearn.linear_model import Ridge - from yellowbrick.regressor import ResidualsPlot + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Instantiate the linear model and visualizer - ridge = Ridge() - visualizer = ResidualsPlot(ridge) + model = Ridge() + visualizer = ResidualsPlot(model) - visualizer.fit(X_train, y_train) # Fit the training data to the model + visualizer.fit(X_train, y_train) # Fit the training data to the visualizer visualizer.score(X_test, y_test) # Evaluate the model on the test data visualizer.poof() # Draw/show/poof the data -.. image:: images/residuals.png - A common use of the residuals plot is to analyze the variance of the error of the regressor. If the points are randomly dispersed around the horizontal axis, a linear regression model is usually appropriate for the data; otherwise, a non-linear model is more appropriate. In the case above, we see a fairly random, uniform distribution of the residuals against the target in two dimensions. This seems to indicate that our linear model is performing well. We can also see from the histogram that our error is normally distributed around zero, which also generally indicates a well fitted model. Note that if the histogram is not desired, it can be turned off with the ``hist=False`` flag: -.. code:: python +.. plot:: + :context: close-figs + :alt: Residuals Plot on the Concrete dataset without a histogram - visualizer = ResidualsPlot(ridge, hist=False) + visualizer = ResidualsPlot(model, hist=False) visualizer.fit(X_train, y_train) visualizer.score(X_test, y_test) visualizer.poof() -.. image:: images/residuals_no_hist.png - -.. warning:: The histogram on the residuals plot requires matplotlib 2.0.2 or greater. If you are using an earlier version of matplotlib, simply set the ``hist=False`` flag so that the histogram is not drawn. +.. warning:: The histogram on the residuals plot requires matplotlib 2.0.2 or greater. If you are using an earlier version of matplotlib, simply set the ``hist=False`` flag so that the histogram is not drawn. API Reference ------------- diff --git a/docs/api/target/binning.py b/docs/api/target/binning.py deleted file mode 100644 index 1d799033b..000000000 --- a/docs/api/target/binning.py +++ /dev/null @@ -1,36 +0,0 @@ -# yellowbrick.target.binning -# Generates images for the balance binning reference documentation. -# -# Author: Kristen McIntyre -# Created: Tue Sept 11 12:09:40 2018 -0400 -# -# ID: binning.py [] kautumn06@gmail.com $ - -""" -Generates images for the balanced binning reference documentation. -""" - -########################################################################## -## Imports -########################################################################## - -from yellowbrick.target import BalancedBinningReference -from sklearn.datasets import load_diabetes - - -def balanced_binning_reference(path="images/balanced_binning_reference.png"): - # Load a regression data set - data = load_diabetes() - - # Extract the target variable - y = data['target'] - - # Instantiate and fit the visualizer - visualizer = BalancedBinningReference() - visualizer.fit(y) - return visualizer.poof(outpath=path) - - - -if __name__ == '__main__': - balanced_binning_reference() diff --git a/docs/api/target/binning.rst b/docs/api/target/binning.rst index a716cfffe..d87218458 100644 --- a/docs/api/target/binning.rst +++ b/docs/api/target/binning.rst @@ -10,24 +10,22 @@ Sometimes when the your target variable is continuously-valued, there simply are To help the user select the optimal number of bins, the ``BalancedBinningReference`` visualizer takes the target variable ``y`` as input and generates a histogram with vertical lines indicating the recommended value points to ensure that the data is evenly distributed into each bin. -.. code:: python +.. plot:: + :context: close-figs + :alt: BalancedBinningReference on concrete dataset + from yellowbrick.datasets import load_concrete from yellowbrick.target import BalancedBinningReference - # Load the a regression data set - data = load_data("concrete") - - # Extract the target of interest - y = data["strength"] + # Load the concrete dataset + X, y = load_concrete() # Instantiate the visualizer visualizer = BalancedBinningReference() - visualizer.fit(y) # Fit the data to the visualizer - visualizer.poof() # Draw/show/poof the data - + visualizer.fit(y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data -.. image:: images/balanced_binning_reference.png .. seealso:: diff --git a/docs/api/target/class_balance.py b/docs/api/target/class_balance.py deleted file mode 100644 index 6a7fae6fc..000000000 --- a/docs/api/target/class_balance.py +++ /dev/null @@ -1,54 +0,0 @@ -# class_balance -# Generates images for the class balance documentation. -# -# Author: Benjamin Bengfort -# Created: Thu Jul 19 12:09:40 2018 -0400 -# -# ID: class_balance.py [] benjamin@bengfort.com $ - -""" -Generates images for the class balance documentation. -""" - -########################################################################## -## Imports -########################################################################## - -from yellowbrick.target import ClassBalance -from yellowbrick.datasets import load_occupancy, load_game - -from sklearn.model_selection import train_test_split - -def compare_class_balance(path="images/class_balance_compare.png"): - data = load_occupancy() - - features = ["temperature", "relative_humidity", "light", "C02", "humidity"] - classes = ['unoccupied', 'occupied'] - - # Extract the numpy arrays from the data frame - X = data[features] - y = data["occupancy"] - - # Create the train and test data - _, _, y_train, y_test = train_test_split(X, y, test_size=0.2) - - # Instantiate the classification model and visualizer - visualizer = ClassBalance(labels=classes) - - visualizer.fit(y_train, y_test) - return visualizer.poof(outpath=path) - - -def balance_class_balance(path="images/class_balance.png"): - data = load_game() - y = data["outcome"] - - oz = ClassBalance(labels=["draw", "loss", "win"]) - oz.fit(y) - return oz.poof(outpath=path) - - - -if __name__ == '__main__': - compare_class_balance() - balance_class_balance() diff --git a/docs/api/target/class_balance.rst b/docs/api/target/class_balance.rst index 7dd9bf3f0..1661f93d6 100644 --- a/docs/api/target/class_balance.rst +++ b/docs/api/target/class_balance.rst @@ -7,61 +7,59 @@ One of the biggest challenges for classification models is an imbalance of class There are several techniques for dealing with class imbalance such as stratified sampling, down sampling the majority class, weighting, etc. But before these actions can be taken, it is important to understand what the class balance is in the training data. The ``ClassBalance`` visualizer supports this by creating a bar chart of the *support* for each class, that is the frequency of the classes' representation in the dataset. -.. code:: python +.. plot:: + :context: close-figs + :alt: ClassBalance Visualizer on the game dataset from yellowbrick.datasets import load_game from yellowbrick.target import ClassBalance - # Load the classification data set - data = load_game() - - # Specify the target - y = data["outcome"] + # Load the classification dataset + X, y = load_game() + # Instantiate the visualizer visualizer = ClassBalance(labels=["draw", "loss", "win"]) - visualizer.fit(y) - visualizer.poof() -.. image:: images/class_balance.png + visualizer.fit(y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data The resulting figure allows us to diagnose the severity of the balance issue. In this figure we can see that the ``"win"`` class dominates the other two classes. One potential solution might be to create a binary classifier: ``"win"`` vs ``"not win"`` and combining the ``"loss"`` and ``"draw"`` classes into one class. .. warning:: - The ``ClassBalance`` visualizer interface has changed in version 0.9, a classification model is no longer required to instantiate the visualizer, it can operate on data only. Additionally the signature of the fit method has changed from ``fit(X, y=None)`` to ``fit(y_train, y_test=None)``, passing in ``X`` is no longer required. + The ``ClassBalance`` visualizer interface has changed in version 0.9, a classification model is no longer required to instantiate the visualizer, it can operate on data only. Additionally, the signature of the fit method has changed from ``fit(X, y=None)`` to ``fit(y_train, y_test=None)``, passing in ``X`` is no longer required. If a class imbalance must be maintained during evaluation (e.g. the event being classified is actually as rare as the frequency implies) then *stratified sampling* should be used to create train and test splits. This ensures that the test data has roughly the same proportion of classes as the training data. While scikit-learn does this by default in ``train_test_split`` and other ``cv`` methods, it can be useful to compare the support of each class in both splits. The ``ClassBalance`` visualizer has a "compare" mode, where the train and test data can be passed to ``fit()``, creating a side-by-side bar chart instead of a single bar chart as follows: -.. code:: python - - from sklearn.model_selection import train_test_split - from yellowbrick.model_selection import ClassBalance +.. plot:: + :context: close-figs + :alt: ClassBalance Visualizer on the occupancy dataset - # Load the classification data set - data = load_data('occupancy') + from sklearn.model_selection import TimeSeriesSplit - # Specify the features of interest and the target - features = ["temperature", "relative_humidity", "light", "C02", "humidity"] - classes = ["unoccupied", "occupied"] + from yellowbrick.datasets import load_occupancy + from yellowbrick.target import ClassBalance - # Extract the instances and target - X = data[features] - y = data["occupancy"] + # Load the classification dataset + X, y = load_occupancy() - # Create the train and test data - _, _, y_train, y_test = train_test_split(X, y, test_size=0.2) + # Create the training and test data + tscv = TimeSeriesSplit() + for train_index, test_index in tscv.split(X): + X_train, X_test = X.iloc[train_index], X.iloc[test_index] + y_train, y_test = y.iloc[train_index], y.iloc[test_index] - # Instantiate the classification model and visualizer - visualizer = ClassBalance(labels=classes) + # Instantiate the visualizer + visualizer = ClassBalance(labels=["unoccupied", "occupied"]) - visualizer.fit(y_train, y_test) - return visualizer.poof() + visualizer.fit(y_train, y_test) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data -.. image:: images/class_balance_compare.png This visualization allows us to do a quick check to ensure that the proportion of each class is roughly similar in both splits. This visualization should be a first stop particularly when evaluation metrics are highly variable across different splits. +.. note:: This example uses ``TimeSeriesSplit`` to split the data into the training and test sets. For more information on this cross-validation method, please refer to the scikit-learn `documentation `_. API Reference ------------- diff --git a/docs/api/target/feature_correlation.py b/docs/api/target/feature_correlation.py deleted file mode 100644 index e451b4371..000000000 --- a/docs/api/target/feature_correlation.py +++ /dev/null @@ -1,71 +0,0 @@ -# feature_correlation -# Generates images for the feature correlation documentation. -# -# Author: Zijie (ZJ) Poh -# Created: Tue Jul 31 20:21:32 2018 -0700 -# -# -""" -Generates images for the feature correlation documentation. -""" - -########################################################################## -## Imports -########################################################################## - -import numpy as np -import pandas as pd -from sklearn import datasets - -from yellowbrick.target import FeatureCorrelation - - -########################################################################## -## Plotting Functions -########################################################################## - -def feature_correlation_pearson( - path="images/feature_correlation_pearson.png"): - data = datasets.load_diabetes() - X, y = data['data'], data['target'] - feature_names = np.array(data['feature_names']) - - visualizer = FeatureCorrelation(labels=feature_names) - visualizer.fit(X, y) - visualizer.poof(outpath=path, clear_figure=True) - - -def feature_correlation_mutual_info_classification( - path="images/feature_correlation_mutual_info_classification.png"): - data = datasets.load_wine() - X, y = data['data'], data['target'] - feature_names = np.array(data['feature_names']) - X_pd = pd.DataFrame(X, columns=feature_names) - - feature_to_plot = ['alcohol', 'ash', 'hue', 'proline', 'total_phenols'] - - visualizer = FeatureCorrelation(method='mutual_info-classification', - feature_names=feature_to_plot) - visualizer.fit(X_pd, y, random_state=0) - visualizer.poof(outpath=path, clear_figure=True) - - -def feature_correlation_mutual_info_regression( - path="images/feature_correlation_mutual_info_regression.png"): - data = datasets.load_diabetes() - X, y = data['data'], data['target'] - feature_names = np.array(data['feature_names']) - - discrete_features = [False for _ in range(len(feature_names))] - discrete_features[1] = True - - visualizer = FeatureCorrelation(method='mutual_info-regression', - labels=feature_names, sort=True) - visualizer.fit(X, y, discrete_features=discrete_features, random_state=0) - visualizer.poof(outpath=path, clear_figure=True) - - -if __name__ == '__main__': - feature_correlation_pearson() - feature_correlation_mutual_info_classification() - feature_correlation_mutual_info_regression() diff --git a/docs/api/target/feature_correlation.rst b/docs/api/target/feature_correlation.rst index c1c4166e5..b44836970 100644 --- a/docs/api/target/feature_correlation.rst +++ b/docs/api/target/feature_correlation.rst @@ -11,21 +11,25 @@ Pearson Correlation The default calculation is Pearson correlation, which is perform with ``scipy.stats.pearsonr``. -.. code:: python +.. plot:: + :context: close-figs + :alt: FeatureCorrelation on the diabetes dataset using Pearson correlation from sklearn import datasets from yellowbrick.target import FeatureCorrelation - # Load the regression data set + # Load the regression dataset data = datasets.load_diabetes() X, y = data['data'], data['target'] - feature_names = np.array(data['feature_names']) - visualizer = FeatureCorrelation(labels=feature_names) - visualizer.fit(X, y) - visualizer.poof() + # Create a list of the feature names + features = np.array(data['feature_names']) + + # Instantiate the visualizer + visualizer = FeatureCorrelation(labels=features) -.. image:: images/feature_correlation_pearson.png + visualizer.fit(X, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data Mutual Information - Regression ------------------------------- @@ -34,25 +38,30 @@ Mutual information between features and the dependent variable is calculated wit It is very important to specify discrete features when calculating mutual information because the calculation for continuous and discrete variables are different. See `scikit-learn documentation `_ for more details. -.. code:: python +.. plot:: + :context: close-figs + :alt: FeatureCorrelation on the diabetes dataset using mutual_info-regression from sklearn import datasets from yellowbrick.target import FeatureCorrelation - # Load the regression data set + # Load the regression dataset data = datasets.load_diabetes() X, y = data['data'], data['target'] - feature_names = np.array(data['feature_names']) - discrete_features = [False for _ in range(len(feature_names))] - discrete_features[1] = True + # Create a list of the feature names + features = np.array(data['feature_names']) + + # Create a list of the discrete features + discrete = [False for _ in range(len(features))] + discrete[1] = True - visualizer = FeatureCorrelation(method='mutual_info-regression', - labels=feature_names) - visualizer.fit(X, y, discrete_features=discrete_features, random_state=0) + # Instantiate the visualizer + visualizer = FeatureCorrelation(method='mutual_info-regression', labels=features) + + visualizer.fit(X, y, discrete_features=discrete, random_state=0) visualizer.poof() -.. image:: images/feature_correlation_mutual_info_regression.png Mutual Information - Classification ----------------------------------- @@ -60,25 +69,31 @@ Mutual Information - Classification By fitting with a pandas DataFrame, the feature labels are automatically obtained from the column names. This visualizer also allows sorting of the bar plot according to the calculated mutual information (or Pearson correlation coefficients) and selecting features to plot by specifying the names of the features or the feature index. -.. code:: python +.. plot:: + :context: close-figs + :alt: FeatureCorrelation on the wine dataset using mutual_info-classification + + import pandas as pd from sklearn import datasets from yellowbrick.target import FeatureCorrelation - - # Load the regression data set - data = datasets.load_diabetes() + + # Load the regression dataset + data = datasets.load_wine() X, y = data['data'], data['target'] - feature_names = np.array(data['feature_names']) - X_pd = pd.DataFrame(X, columns=feature_names) + X_pd = pd.DataFrame(X, columns=data['feature_names']) - feature_to_plot = ['alcohol', 'ash', 'hue', 'proline', 'total_phenols'] + # Create a list of the features to plot + features = ['alcohol', 'ash', 'hue', 'proline', 'total_phenols'] - visualizer = FeatureCorrelation(method='mutual_info-classification', - feature_names=feature_to_plot, sort=True) - visualizer.fit(X_pd, y, random_state=0) - visualizer.poof() + # Instaniate the visualizer + visualizer = FeatureCorrelation( + method='mutual_info-classification', feature_names=features, sort=True + ) + + visualizer.fit(X_pd, y) # Fit the data to the visualizer + visualizer.poof() # Draw/show/poof the data -.. image:: images/feature_correlation_mutual_info_classification.png API Reference ------------- diff --git a/docs/api/target/images/balanced_binning_reference.png b/docs/api/target/images/balanced_binning_reference.png deleted file mode 100644 index f287419a2..000000000 Binary files a/docs/api/target/images/balanced_binning_reference.png and /dev/null differ diff --git a/docs/api/target/images/class_balance.png b/docs/api/target/images/class_balance.png deleted file mode 100644 index 1393e7497..000000000 Binary files a/docs/api/target/images/class_balance.png and /dev/null differ diff --git a/docs/api/target/images/class_balance_compare.png b/docs/api/target/images/class_balance_compare.png deleted file mode 100644 index 59fdd2210..000000000 Binary files a/docs/api/target/images/class_balance_compare.png and /dev/null differ diff --git a/docs/api/target/images/feature_correlation_mutual_info_classification.png b/docs/api/target/images/feature_correlation_mutual_info_classification.png deleted file mode 100644 index 5bbb2cf0a..000000000 Binary files a/docs/api/target/images/feature_correlation_mutual_info_classification.png and /dev/null differ diff --git a/docs/api/target/images/feature_correlation_mutual_info_regression.png b/docs/api/target/images/feature_correlation_mutual_info_regression.png deleted file mode 100644 index 3d00a22b9..000000000 Binary files a/docs/api/target/images/feature_correlation_mutual_info_regression.png and /dev/null differ diff --git a/docs/api/target/images/feature_correlation_pearson.png b/docs/api/target/images/feature_correlation_pearson.png deleted file mode 100644 index 40dc20e89..000000000 Binary files a/docs/api/target/images/feature_correlation_pearson.png and /dev/null differ diff --git a/docs/api/text/corpus.py b/docs/api/text/corpus.py deleted file mode 100644 index 2c8942f99..000000000 --- a/docs/api/text/corpus.py +++ /dev/null @@ -1,44 +0,0 @@ -# Corpus loader for generating example images. - -import os -from sklearn.datasets.base import Bunch - -def load_corpus(path): - """ - Loads and wrangles the passed in text corpus by path. - """ - - # Check if the data exists, otherwise download or raise - if not os.path.exists(path): - raise ValueError(( - "'{}' dataset has not been downloaded, " - "use the yellowbrick.download module to fetch datasets" - ).format(path)) - - # Read the directories in the directory as the categories. - categories = [ - cat for cat in os.listdir(path) - if os.path.isdir(os.path.join(path, cat)) - ] - - files = [] # holds the file names relative to the root - data = [] # holds the text read from the file - target = [] # holds the string of the category - - # Load the data from the files in the corpus - for cat in categories: - for name in os.listdir(os.path.join(path, cat)): - files.append(os.path.join(path, cat, name)) - target.append(cat) - - with open(os.path.join(path, cat, name), 'r') as f: - data.append(f.read()) - - - # Return the data bunch for use similar to the newsgroups example - return Bunch( - categories=categories, - files=files, - data=data, - target=target, - ) diff --git a/docs/api/text/corpus.rst b/docs/api/text/corpus.rst deleted file mode 100644 index 2c0c91f87..000000000 --- a/docs/api/text/corpus.rst +++ /dev/null @@ -1,95 +0,0 @@ -.. -*- mode: rst -*- - -Loading a Text Corpus -===================== - -As in the previous sections, Yellowbrick has provided a sample dataset to run the following cells. In particular, we are going to use a text corpus wrangled from the `Baleen RSS Corpus `_ to present the following examples. If you haven't already downloaded the data, you can do so by running: - -:: - - $ python -m yellowbrick.download - -Note that this will create a directory called ``data`` in your current working directory that contains subdirectories with the provided datasets. - -.. note:: If you've already followed the instructions from :doc:`downloading example datasets <../datasets>`, you don't have to repeat these steps here. Simply check to ensure there is a directory called ``hobbies`` in your data directory. - -The following code snippet creates a utility that will load the corpus from disk into a scikit-learn Bunch object. This method creates a corpus that is exactly the same as the one found in the `"working with text data" `_ example on the scikit-learn website, hopefully making the examples easier to use. - -.. code:: python - - import os - from sklearn.datasets.base import Bunch - - def load_corpus(path): - """ - Loads and wrangles the passed in text corpus by path. - """ - - # Check if the data exists, otherwise download or raise - if not os.path.exists(path): - raise ValueError(( - "'{}' dataset has not been downloaded, " - "use the yellowbrick.download module to fetch datasets" - ).format(path)) - - # Read the directories in the directory as the categories. - categories = [ - cat for cat in os.listdir(path) - if os.path.isdir(os.path.join(path, cat)) - ] - - files = [] # holds the file names relative to the root - data = [] # holds the text read from the file - target = [] # holds the string of the category - - # Load the data from the files in the corpus - for cat in categories: - for name in os.listdir(os.path.join(path, cat)): - files.append(os.path.join(path, cat, name)) - target.append(cat) - - with open(os.path.join(path, cat, name), 'r') as f: - data.append(f.read()) - - - # Return the data bunch for use similar to the newsgroups example - return Bunch( - categories=categories, - files=files, - data=data, - target=target, - ) - -This is a fairly long bit of code, so let's walk through it step by step. The data in the corpus directory is stored as follows: - -:: - - data/hobbies - ├── README.md - └── books - | ├── 56d62a53c1808113ffb87f1f.txt - | └── 5745a9c7c180810be6efd70b.txt - └── cinema - | ├── 56d629b5c1808113ffb87d8f.txt - | └── 57408e5fc180810be6e574c8.txt - └── cooking - | ├── 56d62b25c1808113ffb8813b.txt - | └── 573f0728c180810be6e2575c.txt - └── gaming - | ├── 56d62654c1808113ffb87938.txt - | └── 574585d7c180810be6ef7ffc.txt - └── sports - ├── 56d62adec1808113ffb88054.txt - └── 56d70f17c180810560aec345.txt - -Each of the documents in the corpus is stored in a text file labeled with its hash signature in a directory that specifies its label or category. Therefore the first step after checking to make sure the specified path exists is to list all the directories in the `hobbies` directory---this gives us each of our categories, which we will store later in the bunch. - -The second step is to create placeholders for holding filenames, text data, and labels. We can then loop through the list of categories, list the files in each category directory, add those files to the files list, add the category name to the target list, then open and read the file to add it to data. - -To load the corpus into memory, we can simply use the following snippet: - -.. code:: python - - corpus = load_corpus("data/hobbies") - -We'll use this snippet in all of the text examples in this section! diff --git a/docs/api/text/dispersion.py b/docs/api/text/dispersion.py deleted file mode 100644 index 66dfab43a..000000000 --- a/docs/api/text/dispersion.py +++ /dev/null @@ -1,48 +0,0 @@ -# ID: dispersion.py [] lwgray@gmail.com $ - -""" -Generate figures for Dispersion Plot documentation. -""" - -########################################################################## -## Imports -########################################################################## - -import matplotlib.pyplot as plt - -from corpus import load_corpus -from yellowbrick.text.dispersion import DispersionPlot - -########################################################################## -## Generate -########################################################################## - -def dispersion(target_words, text, outpath, **kwargs): - # Create a new figure and axes - _, ax = plt.subplots() - - # Visualize the Dispersion of target words - visualizer = DispersionPlot(target_words, ax=ax, **kwargs) - visualizer.fit(text) - visualizer.poof(outpath=outpath) - - -########################################################################## -## Main Method -########################################################################## - -if __name__ == '__main__': - - # Load the corpus - corpus = load_corpus("../../../examples/data/hobbies") - - # Convert corpus into a list of all words from beginning to end - # text = [word for doc in corpus.data for word in doc.split()] - text = [doc.split() for doc in corpus.data] - # Select target words to visualize - target_words = ['Game', 'player', 'score', 'oil', 'Man'] - - # Display dispersion of target words throughout corpus - dispersion(target_words, text, "images/dispersion_docs.png") - - diff --git a/docs/api/text/dispersion.rst b/docs/api/text/dispersion.rst index 800440492..4c6ae7167 100644 --- a/docs/api/text/dispersion.rst +++ b/docs/api/text/dispersion.rst @@ -5,16 +5,15 @@ Dispersion Plot A word's importance can be weighed by its dispersion in a corpus. Lexical dispersion is a measure of a word's homogeneity across the parts of a corpus. This plot notes the occurrences of a word and how many words from the beginning of the corpus it appears. -.. code:: python +.. plot:: + :context: close-figs + :alt: Dispersion Plot from yellowbrick.text import DispersionPlot - -After importing the visualizer, we can :doc:`load the corpus ` - -.. code:: python + from yellowbrick.datasets import load_hobbies # Load the text data - corpus = load_corpus("hobbies") + corpus = load_hobbies() # Create a list of words from the corpus text text = [doc.split() for doc in corpus.data] @@ -28,8 +27,6 @@ After importing the visualizer, we can :doc:`load the corpus ` visualizer.poof() -.. image:: images/dispersion_docs.png - API Reference ------------- diff --git a/docs/api/text/freqdist.py b/docs/api/text/freqdist.py deleted file mode 100644 index 3ba42c61d..000000000 --- a/docs/api/text/freqdist.py +++ /dev/null @@ -1,45 +0,0 @@ -import matplotlib.pyplot as plt - -from corpus import load_corpus -from collections import defaultdict -from yellowbrick.text.freqdist import FreqDistVisualizer -from sklearn.feature_extraction.text import CountVectorizer - - -def freqdist(docs, outpath, corpus_kwargs={}, **kwargs): - # Create a new figure and axes - fig = plt.figure() - ax = fig.add_subplot(111) - - # Vectorize the corpus - vectorizer = CountVectorizer(**corpus_kwargs) - docs = vectorizer.fit_transform(docs) - features = vectorizer.get_feature_names() - - # Visualize the frequency distribution - visualizer = FreqDistVisualizer(ax=ax, features=features, **kwargs) - visualizer.fit(docs) - visualizer.poof(outpath=outpath) - - -if __name__ == '__main__': - - # Load the corpus - corpus = load_corpus("../../../examples/data/hobbies") - - # Whole corpus visualization - freqdist(corpus.data, "images/freqdist_corpus.png", orient='v') - - # Stopwords removed - freqdist(corpus.data, "images/freqdist_stopwords.png", {'stop_words': 'english'}, orient='v') - - # Specific categories - hobbies = defaultdict(list) - for text, label in zip(corpus.data, corpus.target): - hobbies[label].append(text) - - # Cooking Category - freqdist(hobbies["cooking"], "images/freqdist_cooking.png", {'stop_words': 'english'}, orient='v') - - # Gaming Category - freqdist(hobbies["gaming"], "images/freqdist_gaming.png", {'stop_words': 'english'}, orient='v') diff --git a/docs/api/text/freqdist.rst b/docs/api/text/freqdist.rst index 279e28973..39bccc586 100644 --- a/docs/api/text/freqdist.rst +++ b/docs/api/text/freqdist.rst @@ -5,74 +5,121 @@ Token Frequency Distribution A method for visualizing the frequency of tokens within and across corpora is frequency distribution. A frequency distribution tells us the frequency of each vocabulary item in the text. In general, it could count any kind of observable event. It is a distribution because it tells us how the total number of word tokens in the text are distributed across the vocabulary items. -.. code:: python +.. NOTE:: The ``FreqDistVisualizer`` does not perform any normalization or vectorization, and it expects text that has already been count vectorized. - from yellowbrick.text import FreqDistVisualizer - from sklearn.feature_extraction.text import CountVectorizer +We first instantiate a ``FreqDistVisualizer`` object, and then call ``fit()`` on that object with the count vectorized documents and the features (i.e. the words from the corpus), which computes the frequency distribution. The visualizer then plots a bar chart of the top 50 most frequent terms in the corpus, with the terms listed along the x-axis and frequency counts depicted at y-axis values. As with other Yellowbrick visualizers, when the user invokes ``poof()``, the finalized visualization is shown. Note that in this plot and in the subsequent one, we can orient our plot vertically by passing in ``orient='v'`` on instantiation (the plot will orient horizontally by default): -Note that the ``FreqDistVisualizer`` does not perform any normalization or vectorization, and it expects text that has already be count vectorized. +.. plot:: + :context: close-figs + :alt: Frequency Distribution Plot -We first instantiate a ``FreqDistVisualizer`` object, and then call ``fit()`` on that object with the count vectorized documents and the features (i.e. the words from the corpus), which computes the frequency distribution. The visualizer then plots a bar chart of the top 50 most frequent terms in the corpus, with the terms listed along the x-axis and frequency counts depicted at y-axis values. As with other Yellowbrick visualizers, when the user invokes ``poof()``, the finalized visualization is shown. + from sklearn.feature_extraction.text import CountVectorizer -.. code:: python + from yellowbrick.text import FreqDistVisualizer + from yellowbrick.datasets import load_hobbies + + # Load the text data + corpus = load_hobbies() vectorizer = CountVectorizer() docs = vectorizer.fit_transform(corpus.data) features = vectorizer.get_feature_names() - visualizer = FreqDistVisualizer(features=features) + visualizer = FreqDistVisualizer(features=features, orient='v') visualizer.fit(docs) visualizer.poof() -.. image:: images/freqdist_corpus.png It is interesting to compare the results of the ``FreqDistVisualizer`` before and after stopwords have been removed from the corpus: -.. code:: python + +.. plot:: + :context: close-figs + :include-source: False + :alt: Frequency Distribution Plot without Stopwords + + from sklearn.feature_extraction.text import CountVectorizer + + from yellowbrick.text import FreqDistVisualizer + from yellowbrick.datasets import load_hobbies + + # Load the text data + corpus = load_hobbies() vectorizer = CountVectorizer(stop_words='english') docs = vectorizer.fit_transform(corpus.data) features = vectorizer.get_feature_names() - visualizer = FreqDistVisualizer(features=features) + visualizer = FreqDistVisualizer(features=features, orient='v') visualizer.fit(docs) visualizer.poof() -.. image:: images/freqdist_stopwords.png +It is also interesting to explore the differences in tokens across a corpus. The hobbies corpus that comes with Yellowbrick has already been categorized (try ``corpus.target``), so let's visually compare the differences in the frequency distributions for two of the categories: *"cooking"* and *"gaming"*. -It is also interesting to explore the differences in tokens across a corpus. The hobbies corpus that comes with Yellowbrick has already been categorized (try ``corpus['categories']``), so let's visually compare the differences in the frequency distributions for two of the categories: *"cooking"* and *"gaming"*. +Here is the plot for the cooking corpus (oriented horizontally this time): -.. code:: python +.. plot:: + :context: close-figs + :include-source: False + :alt: Frequency Distribution Plot for Cooking Corpus from collections import defaultdict + from sklearn.feature_extraction.text import CountVectorizer + + from yellowbrick.text import FreqDistVisualizer + from yellowbrick.datasets import load_hobbies + + # Load the text data + corpus = load_hobbies() + + # Create a dict to map target labels to documents of that category hobbies = defaultdict(list) - for text, label in zip(corpus.data, corpus.label): + for text, label in zip(corpus.data, corpus.target): hobbies[label].append(text) -.. code:: python - vectorizer = CountVectorizer(stop_words='english') docs = vectorizer.fit_transform(text for text in hobbies['cooking']) features = vectorizer.get_feature_names() - visualizer = FreqDistVisualizer(features=features) + visualizer = FreqDistVisualizer( + features=features, size=(1080, 720) + ) visualizer.fit(docs) visualizer.poof() -.. image:: images/freqdist_cooking.png +And for the gaming corpus (again oriented horizontally): + +.. plot:: + :context: close-figs + :include-source: False + :alt: Frequency Distribution Plot for Gaming Corpus -.. code:: python + from collections import defaultdict + + from sklearn.feature_extraction.text import CountVectorizer + + from yellowbrick.text import FreqDistVisualizer + from yellowbrick.datasets import load_hobbies + + # Load the text data + corpus = load_hobbies() + + # Create a dict to map target labels to documents of that category + hobbies = defaultdict(list) + for text, label in zip(corpus.data, corpus.target): + hobbies[label].append(text) vectorizer = CountVectorizer(stop_words='english') docs = vectorizer.fit_transform(text for text in hobbies['gaming']) features = vectorizer.get_feature_names() - visualizer = FreqDistVisualizer(features=features) + visualizer = FreqDistVisualizer( + features=features, size=(1080, 720) + ) visualizer.fit(docs) visualizer.poof() -.. image:: images/freqdist_gaming.png API Reference ------------- diff --git a/docs/api/text/images/dispersion_docs.png b/docs/api/text/images/dispersion_docs.png deleted file mode 100644 index b2f623209..000000000 Binary files a/docs/api/text/images/dispersion_docs.png and /dev/null differ diff --git a/docs/api/text/images/freqdist_cooking.png b/docs/api/text/images/freqdist_cooking.png deleted file mode 100644 index d734b5264..000000000 Binary files a/docs/api/text/images/freqdist_cooking.png and /dev/null differ diff --git a/docs/api/text/images/freqdist_corpus.png b/docs/api/text/images/freqdist_corpus.png deleted file mode 100644 index 2c8f75a72..000000000 Binary files a/docs/api/text/images/freqdist_corpus.png and /dev/null differ diff --git a/docs/api/text/images/freqdist_gaming.png b/docs/api/text/images/freqdist_gaming.png deleted file mode 100644 index c06bff85f..000000000 Binary files a/docs/api/text/images/freqdist_gaming.png and /dev/null differ diff --git a/docs/api/text/images/freqdist_stopwords.png b/docs/api/text/images/freqdist_stopwords.png deleted file mode 100644 index 91699c746..000000000 Binary files a/docs/api/text/images/freqdist_stopwords.png and /dev/null differ diff --git a/docs/api/text/images/tsne_all_docs.png b/docs/api/text/images/tsne_all_docs.png deleted file mode 100644 index c057210e6..000000000 Binary files a/docs/api/text/images/tsne_all_docs.png and /dev/null differ diff --git a/docs/api/text/images/tsne_kmeans.png b/docs/api/text/images/tsne_kmeans.png deleted file mode 100644 index a571f80d0..000000000 Binary files a/docs/api/text/images/tsne_kmeans.png and /dev/null differ diff --git a/docs/api/text/images/tsne_no_labels.png b/docs/api/text/images/tsne_no_labels.png deleted file mode 100644 index ccfc0d38a..000000000 Binary files a/docs/api/text/images/tsne_no_labels.png and /dev/null differ diff --git a/docs/api/text/images/umap.png b/docs/api/text/images/umap.png new file mode 100644 index 000000000..10eaaf7b1 Binary files /dev/null and b/docs/api/text/images/umap.png differ diff --git a/docs/api/text/images/umap_cosine.png b/docs/api/text/images/umap_cosine.png new file mode 100644 index 000000000..50b115f66 Binary files /dev/null and b/docs/api/text/images/umap_cosine.png differ diff --git a/docs/api/text/images/umap_kmeans.png b/docs/api/text/images/umap_kmeans.png new file mode 100644 index 000000000..f360fe763 Binary files /dev/null and b/docs/api/text/images/umap_kmeans.png differ diff --git a/docs/api/text/images/umap_no_labels.png b/docs/api/text/images/umap_no_labels.png new file mode 100644 index 000000000..4d50694b5 Binary files /dev/null and b/docs/api/text/images/umap_no_labels.png differ diff --git a/docs/api/text/index.rst b/docs/api/text/index.rst index 33a1d2281..5166c6aed 100644 --- a/docs/api/text/index.rst +++ b/docs/api/text/index.rst @@ -5,19 +5,23 @@ Text Modeling Visualizers Yellowbrick provides the ``yellowbrick.text`` module for text-specific visualizers. The ``TextVisualizer`` class specifically deals with datasets that are corpora and not simple numeric arrays or DataFrames, providing utilities for analyzing word dispersion and distribution, showing document similarity, or simply wrapping some of the other standard visualizers with text-specific display properties. -We currently have three text-specific visualizations implemented: +We currently have four text-specific visualizations implemented: - :doc:`freqdist`: plot the frequency of tokens in a corpus - :doc:`tsne`: plot similar documents closer together to discover clusters +- :doc:`umap_vis`: plot similar documents closer together to discover clusters - :doc:`dispersion`: plot the dispersion of target words throughout a corpus +- :doc:`postag`: plot the counts of different parts-of-speech throughout a tagged corpus -Note that the examples in this section require a corpus of text data, see :doc:`loading a text corpus ` for more information. +Note that the examples in this section require a corpus of text data, see :doc:`the hobbies corpus <../datasets/hobbies>` for a sample dataset. .. code:: python from yellowbrick.text import FreqDistVisualizer from yellowbrick.text import TSNEVisualizer + from yellowbrick.text import UMAPVisualizer from yellowbrick.text import DispersionPlot + from yellowbrick.text import PosTagVisualizer from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.feature_extraction.text import CountVectorizer @@ -25,7 +29,8 @@ Note that the examples in this section require a corpus of text data, see :doc:` .. toctree:: :maxdepth: 2 - corpus freqdist tsne + umap_vis dispersion + postag \ No newline at end of file diff --git a/docs/api/text/postag.rst b/docs/api/text/postag.rst new file mode 100644 index 000000000..ace5548dd --- /dev/null +++ b/docs/api/text/postag.rst @@ -0,0 +1,233 @@ +.. -*- mode: rst -*- + +PosTag Visualization +==================== + +Parts of speech (e.g. verbs, nouns, prepositions, adjectives) indicate how a word is functioning within the context of a sentence. In English as in many other languages, a single word can function in multiple ways. Part-of-speech tagging lets us encode information not only about a word’s definition, but also its use in context (for example the words “ship” and “shop” can be either a verb or a noun, depending on the context). + +The ``PosTagVisualizer`` is intended to support grammar-based feature extraction techniques for machine learning workflows that require natural language processing. The visualizer reads in a corpus that has already been sentence- and word-segmented, and tagged, creating a bar chart to visualize the relative proportions of different parts-of-speech in a corpus. + +.. note:: + The ``PosTagVisualizer`` currently works with both Penn-Treebank (e.g. via NLTK) and Universal Dependencies (e.g. via SpaCy)-tagged corpora, but expects corpora that have already been tagged, and which take the form of a list of (document) lists of (sentence) lists of ``(token, tag)`` tuples, as in the example below. + +Penn Treebank Tags +------------------ + +.. plot:: + :context: close-figs + :alt: PosTag plot with Penn Treebank tags + + from yellowbrick.text import PosTagVisualizer + + + tagged_stanzas = [ + [ + [ + ('Whose', 'JJ'),('woods', 'NNS'),('these', 'DT'), + ('are', 'VBP'),('I', 'PRP'),('think', 'VBP'),('I', 'PRP'), + ('know', 'VBP'),('.', '.') + ], + [ + ('His', 'PRP$'),('house', 'NN'),('is', 'VBZ'),('in', 'IN'), + ('the', 'DT'),('village', 'NN'),('though', 'IN'),(';', ':'), + ('He', 'PRP'),('will', 'MD'),('not', 'RB'),('see', 'VB'), + ('me', 'PRP'),('stopping', 'VBG'), ('here', 'RB'),('To', 'TO'), + ('watch', 'VB'),('his', 'PRP$'),('woods', 'NNS'),('fill', 'VB'), + ('up', 'RP'),('with', 'IN'),('snow', 'NNS'),('.', '.') + ] + ], + [ + [ + ('My', 'PRP$'),('little', 'JJ'),('horse', 'NN'),('must', 'MD'), + ('think', 'VB'),('it', 'PRP'),('queer', 'JJR'),('To', 'TO'), + ('stop', 'VB'),('without', 'IN'),('a', 'DT'),('farmhouse', 'NN'), + ('near', 'IN'),('Between', 'NNP'),('the', 'DT'),('woods', 'NNS'), + ('and', 'CC'),('frozen', 'JJ'),('lake', 'VB'),('The', 'DT'), + ('darkest', 'JJS'),('evening', 'NN'),('of', 'IN'),('the', 'DT'), + ('year', 'NN'),('.', '.') + ] + ], + [ + [ + ('He', 'PRP'),('gives', 'VBZ'),('his', 'PRP$'),('harness', 'NN'), + ('bells', 'VBZ'),('a', 'DT'),('shake', 'NN'),('To', 'TO'), + ('ask', 'VB'),('if', 'IN'),('there', 'EX'),('is', 'VBZ'), + ('some', 'DT'),('mistake', 'NN'),('.', '.') + ], + [ + ('The', 'DT'),('only', 'JJ'),('other', 'JJ'),('sound', 'NN'), + ('’', 'NNP'),('s', 'VBZ'),('the', 'DT'),('sweep', 'NN'), + ('Of', 'IN'),('easy', 'JJ'),('wind', 'NN'),('and', 'CC'), + ('downy', 'JJ'),('flake', 'NN'),('.', '.') + ] + ], + [ + [ + ('The', 'DT'),('woods', 'NNS'),('are', 'VBP'),('lovely', 'RB'), + (',', ','),('dark', 'JJ'),('and', 'CC'),('deep', 'JJ'),(',', ','), + ('But', 'CC'),('I', 'PRP'),('have', 'VBP'),('promises', 'NNS'), + ('to', 'TO'),('keep', 'VB'),(',', ','),('And', 'CC'),('miles', 'NNS'), + ('to', 'TO'),('go', 'VB'),('before', 'IN'),('I', 'PRP'), + ('sleep', 'VBP'),(',', ','),('And', 'CC'),('miles', 'NNS'), + ('to', 'TO'),('go', 'VB'),('before', 'IN'),('I', 'PRP'), + ('sleep', 'VBP'),('.', '.') + ] + ] + ] + + # Create the visualizer, fit, score, and poof it + viz = PosTagVisualizer() + viz.fit(tagged_stanzas) + viz.poof() + +Universal Dependencies Tags +--------------------------- + +Libraries like SpaCy use tags from the Universal Dependencies (UD) framework. The ``PosTagVisualizer`` can also be used with text tagged using this framework by specifying the ``tagset`` keyword as "universal" on instantiation. + +.. code:: python + + tagged_speech = [ + [ + [ + ('In', 'ADP'),('all', 'DET'),('honesty', 'NOUN'),(',', 'PUNCT'), + ('I', 'PRON'),('said', 'VERB'),('yes', 'INTJ'),('to', 'ADP'), + ('the', 'DET'),('fear', 'NOUN'),('of', 'ADP'),('being', 'VERB'), + ('on', 'ADP'),('this', 'DET'),('stage', 'NOUN'),('tonight', 'NOUN'), + ('because', 'ADP'),('I', 'PRON'),('wanted', 'VERB'),('to', 'PART'), + ('be', 'VERB'),('here', 'ADV'),(',', 'PUNCT'),('to', 'PART'), + ('look', 'VERB'),('out', 'PART'),('into', 'ADP'),('this', 'DET'), + ('audience', 'NOUN'),(',', 'PUNCT'),('and', 'CCONJ'), + ('witness', 'VERB'),('this', 'DET'),('moment', 'NOUN'),('of', 'ADP'), + ('change', 'NOUN') + ], + [ + ('and', 'CCONJ'),('I', 'PRON'),("'m", 'VERB'),('not', 'ADV'), + ('fooling', 'VERB'),('myself', 'PRON'),('.', 'PUNCT') + ], + [ + ('I', 'PRON'),("'m", 'VERB'),('not', 'ADV'),('fooling', 'VERB'), + ('myself', 'PRON'),('.', 'PUNCT') + ], + [ + ('Next', 'ADJ'),('year', 'NOUN'),('could', 'VERB'),('be', 'VERB'), + ('different', 'ADJ'),('.', 'PUNCT') + ], + [ + ('It', 'PRON'),('probably', 'ADV'),('will', 'VERB'),('be', 'VERB'), + (',', 'PUNCT'),('but', 'CCONJ'),('right', 'ADV'),('now', 'ADV'), + ('this', 'DET'),('moment', 'NOUN'),('is', 'VERB'),('real', 'ADJ'), + ('.', 'PUNCT') + ], + [ + ('Trust', 'VERB'),('me', 'PRON'),(',', 'PUNCT'),('it', 'PRON'), + ('is', 'VERB'),('real', 'ADJ'),('because', 'ADP'),('I', 'PRON'), + ('see', 'VERB'),('you', 'PRON') + ], + [ + ('and', 'CCONJ'), ('I', 'PRON'), ('see', 'VERB'), ('you', 'PRON') + ], + [ + ('—', 'PUNCT') + ], + [ + ('all', 'ADJ'),('these', 'DET'),('faces', 'NOUN'),('of', 'ADP'), + ('change', 'NOUN') + ], + [ + ('—', 'PUNCT'),('and', 'CCONJ'),('now', 'ADV'),('so', 'ADV'), + ('will', 'VERB'),('everyone', 'NOUN'),('else', 'ADV'), ('.', 'PUNCT') + ] + ] + ] + + # Create the visualizer, fit, score, and poof it + viz = PosTagVisualizer(tagset="universal") + viz.fit(tagged_speech) + viz.poof() + + ++-------------------+------------------------------------------+----------------------+--------------------------+ +| Penn-Treebank Tag | Description | Universal Tag | Description | ++===================+==========================================+======================+==========================+ +| CC | Coordinating conjunction | ADJ | adjective | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| CD | Cardinal number | ADP | adposition | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| DT | Determiner | ADV | adverb | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| EX | Existential *there* | AUX | auxiliary | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| FW | Foreign word | CONJ | conjunction | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| IN | Preposition or subordinating conjunction | CCONJ | coordinating conjunction | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| JJ | Adjective | DET | determiner | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| JJR | Adjective, comparative | INTJ | interjection | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| JJS | Adjective, superlative | NOUN | noun | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| LS | List item marker | NUM | numeral | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| MD | Modal | PART | particle | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| NN | Noun, singular or mass | PRON | pronoun | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| NNS | Noun, plural | PROPN | proper noun | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| NNP | Proper noun, singular | PUNCT | punctuation | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| NNPS | Proper noun, plural | SCONJ | subordinating conjunction| ++-------------------+------------------------------------------+----------------------+--------------------------+ +| PDT | Predeterminer | SYM | symbol | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| POS | Possessive ending | VERB | verb | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| PRP | Personal pronoun | X | other | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| PRP$ | Possessive pronoun | SPACE | space | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| RB | Adverb | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| RBR | Adverb, comparative | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| RBS | Adverb, superlative | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| RP | Particle | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| SYM | Symbol | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| TO | *to* | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| UH | Interjection | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| VB | Verb, base form | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| VBD | Verb, past tense | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| VBG | Verb, gerund or present participle | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| VBN | Verb, past participle | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| VBP | Verb, non-3rd person singular present | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| VBZ | Verb, 3rd person singular present | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| WDT | Wh-determiner | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| WP | Wh-pronoun | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| WP$ | Possessive wn-pronoun | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ +| WRB | Wh-adverb | | | ++-------------------+------------------------------------------+----------------------+--------------------------+ + + +API Reference +------------- + +.. automodule:: yellowbrick.text.postag + :members: PosTagVisualizer + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api/text/tsne.rst b/docs/api/text/tsne.rst index 09aa71654..837bb8495 100644 --- a/docs/api/text/tsne.rst +++ b/docs/api/text/tsne.rst @@ -7,59 +7,86 @@ One very popular method for visualizing document similarity is to use t-distribu Unfortunately, ``TSNE`` is very expensive, so typically a simpler decomposition method such as SVD or PCA is applied ahead of time. The ``TSNEVisualizer`` creates an inner transformer pipeline that applies such a decomposition first (SVD with 50 components by default), then performs the t-SNE embedding. The visualizer then plots the scatter plot, coloring by cluster or by class, or neither if a structural analysis is required. -.. code:: python - - from yellowbrick.text import TSNEVisualizer - from sklearn.feature_extraction.text import TfidfVectorizer +After importing the required tools, we can use the :doc:`hobbies corpus <../datasets/hobbies>` and vectorize the text using TF-IDF. Once the corpus is vectorized we can visualize it, showing the distribution of classes. -After importing the required tools, we can :doc:`load the corpus ` and vectorize the text using TF-IDF. -.. code:: python +.. plot:: + :context: close-figs + :alt: TSNE Plot - # Load the data and create document vectors - corpus = load_corpus('hobbies') - tfidf = TfidfVectorizer() + from sklearn.feature_extraction.text import TfidfVectorizer - docs = tfidf.fit_transform(corpus.data) - labels = corpus.target + from yellowbrick.text import TSNEVisualizer + from yellowbrick.datasets import load_hobbies -Now that the corpus is vectorized we can visualize it, showing the distribution of classes. + # Load the data and create document vectors + corpus = load_hobbies() + tfidf = TfidfVectorizer() -.. code:: python + X = tfidf.fit_transform(corpus.data) + y = corpus.target # Create the visualizer and draw the vectors tsne = TSNEVisualizer() - tsne.fit(docs, labels) + tsne.fit(X, y) tsne.poof() -.. image:: images/tsne_all_docs.png +Note that you can pass the class labels or document categories directly to the ``TSNEVisualizer`` as follows: + +.. code:: python + + labels = corpus.labels + tsne = TSNEVisualizer(labels=labels) + tsne.fit(X, y) + tsne.poof() If we omit the target during fit, we can visualize the whole dataset to see if any meaningful patterns are observed. -.. code:: python +.. plot:: + :context: close-figs + :include-source: False + :alt: TSNE Plot without Class Coloring - # Don't color points with their classes + from sklearn.feature_extraction.text import TfidfVectorizer + + from yellowbrick.text import TSNEVisualizer + from yellowbrick.datasets import load_hobbies + + # Load the data and create document vectors + corpus = load_hobbies() + tfidf = TfidfVectorizer() + + X = tfidf.fit_transform(corpus.data) tsne = TSNEVisualizer(labels=["documents"]) - tsne.fit(docs) + tsne.fit(X) tsne.poof() -.. image:: images/tsne_no_labels.png - This means we don't have to use class labels at all. Instead we can use cluster membership from K-Means to label each document. This will allow us to look for clusters of related text by their contents: -.. code:: python +.. plot:: + :context: close-figs + :include-source: False + :alt: TSNE Plot without Clustering - # Apply clustering instead of class names. from sklearn.cluster import KMeans + from sklearn.feature_extraction.text import TfidfVectorizer + + from yellowbrick.text import TSNEVisualizer + from yellowbrick.datasets import load_hobbies + + # Load the data and create document vectors + corpus = load_hobbies() + tfidf = TfidfVectorizer() + + X = tfidf.fit_transform(corpus.data) clusters = KMeans(n_clusters=5) - clusters.fit(docs) + clusters.fit(X) tsne = TSNEVisualizer() - tsne.fit(docs, ["c{}".format(c) for c in clusters.labels_]) + tsne.fit(X, ["c{}".format(c) for c in clusters.labels_]) tsne.poof() -.. image:: images/tsne_kmeans.png API Reference ------------- diff --git a/docs/api/text/tsne.py b/docs/api/text/umap_vis.py similarity index 53% rename from docs/api/text/tsne.py rename to docs/api/text/umap_vis.py index c85d6b206..114e42ea4 100644 --- a/docs/api/text/tsne.py +++ b/docs/api/text/umap_vis.py @@ -1,59 +1,58 @@ -# ID: tsne.py [] benjamin@bengfort.com $ +#!/usr/bin/env python3 +# ID: umap_vis.py [73a44e5] jchealy@gmail.com $ """ -Generate figures for TSNE documentation. +Manually generate figures for the UMAP documentation. """ - ########################################################################## ## Imports ########################################################################## import matplotlib.pyplot as plt -from corpus import load_corpus -from yellowbrick.text import TSNEVisualizer - from sklearn.cluster import KMeans from sklearn.feature_extraction.text import TfidfVectorizer +from yellowbrick.text import UMAPVisualizer +from yellowbrick.datasets import load_hobbies ########################################################################## ## Generate ########################################################################## -def tsne(docs, target, outpath, **kwargs): + +def umap(docs, target, outpath, **kwargs): # Create a new figure and axes fig = plt.figure() ax = fig.add_subplot(111) # Visualize the frequency distribution - visualizer = TSNEVisualizer(ax=ax, **kwargs) + visualizer = UMAPVisualizer(ax=ax, **kwargs) visualizer.fit(docs, target) visualizer.poof(outpath=outpath) -########################################################################## -## Main Method -########################################################################## - -if __name__ == '__main__': +if __name__ == "__main__": # Load and vectorize the corpus - corpus = load_corpus("../../../examples/data/hobbies") + corpus = load_hobbies() tfidf = TfidfVectorizer() - docs = tfidf.fit_transform(corpus.data) - target = corpus.target + docs = tfidf.fit_transform(corpus.data) + labels = corpus.target + + # Whole corpus visualization + umap(docs, labels, "images/umap.png") # Whole corpus visualization - tsne(docs, target, "images/tsne_all_docs.png") + umap(docs, labels, "images/umap_cosine.png", metric="cosine") # No labels - tsne(docs, None, "images/tsne_no_labels.png", labels=["documents"]) + umap(docs, None, "images/umap_no_labels.png", labels=["documents"], metric="cosine") - # Apply clustering instead of class names. + # Apply clustering instead of class names clusters = KMeans(n_clusters=5) clusters.fit(docs) centers = ["c{}".format(c) for c in clusters.labels_] - tsne(docs, centers, "images/tsne_kmeans.png") + umap(docs, centers, "images/umap_kmeans.png") diff --git a/docs/api/text/umap_vis.rst b/docs/api/text/umap_vis.rst new file mode 100644 index 000000000..8867d550c --- /dev/null +++ b/docs/api/text/umap_vis.rst @@ -0,0 +1,134 @@ +.. -*- mode: rst -*- + +UMAP Corpus Visualization +========================== + +`Uniform Manifold Approximation and Projection (UMAP) `__ is a nonlinear +dimensionality reduction method that is well suited to embedding in two +or three dimensions for visualization as a scatter plot. UMAP is a +relatively new technique but is very effective for visualizing clusters or +groups of data points and their relative proximities. It does a good job +of learning the local structure within your data but also attempts to +preserve the relationships between your groups as can be seen in its +`exploration of +MNIST `__. +It is fast, scalable, and can be applied directly to sparse matrices, +eliminating the need to run ``TruncatedSVD`` as a pre-processing step. +Additionally, it supports a wide variety of distance measures allowing +for easy exploration of your data. For a more detailed explanation of the algorithm +the paper can be found `here `__. + +In this example, we represent documents via a `term frequency inverse +document +frequency `__ (TF-IDF) +vector and then use UMAP to find a low dimensional representation of these +documents. Next, the Yellowbrick visualizer plots the scatter plot, +coloring by cluster or by class, or neither if a structural analysis is +required. + +After importing the required tools, we can use the :doc:`the hobbies corpus <../datasets/hobbies>` and vectorize the text using TF-IDF. Once the corpus is vectorized we can visualize it, showing the distribution of classes. + +.. note to contributors: the below code requires an additional dependency on umap-learn + that is not part of the core requirements, so has not been modified with a plot + directive. See umap_vis.py to regenerate images. + +.. code:: python + + from sklearn.feature_extraction.text import TfidfVectorizer + + from yellowbrick.datasets import load_hobbies + from yellowbrick.text import UMAPVisualizer + + # Load the text data + corpus = load_hobbies() + + tfidf = TfidfVectorizer() + docs = tfidf.fit_transform(corpus.data) + labels = corpus.target + + # Instantiate the text visualizer + umap = UMAPVisualizer() + umap.fit(docs, labels) + umap.poof() + + +.. image:: images/umap.png + +Alternatively, if we believed that cosine distance was a more +appropriate metric on our feature space we could specify that via a +``metric`` parameter passed through to the underlying UMAP function by +the ``UMAPVisualizer``. + +.. code:: python + + umap = UMAPVisualizer(metric='cosine') + umap.fit(docs, labels) + umap.poof() + + +.. image:: images/umap_cosine.png + +If we omit the target during fit, we can visualize the whole dataset to +see if any meaningful patterns are observed. + +.. code:: python + + # Don't color points with their classes + umap = UMAPVisualizer(labels=["documents"], metric='cosine') + umap.fit(docs) + umap.poof() + + +.. image:: images/umap_no_labels.png + +This means we don’t have to use class labels at all. Instead, we can use +cluster membership from K-Means to label each document. This will allow +us to look for clusters of related text by their contents: + +.. code:: python + + from sklearn.cluster import KMeans + from sklearn.feature_extraction.text import TfidfVectorizer + + from yellowbrick.datasets import load_hobbies + from yellowbrick.text import UMAPVisualizer + + # Load the text data + corpus = load_hobbies() + + tfidf = TfidfVectorizer() + docs = tfidf.fit_transform(corpus.data) + + # Instantiate the clustering model + clusters = KMeans(n_clusters=5) + clusters.fit(docs) + + umap = UMAPVisualizer() + umap.fit(docs, ["c{}".format(c) for c in clusters.labels_]) + umap.poof() + +.. image:: images/umap_kmeans.png + +On one hand, these clusters aren’t particularly well concentrated by the +two-dimensional embedding of UMAP; while on the other hand, the true labels +for this data are. That is a good indication that your data does indeed +live on a manifold in your TF-IDF space and that structure is being +ignored by the K-Means algorithm. Clustering can be quite tricky in high +dimensional spaces and it is often a good idea to reduce your dimension +before running clustering algorithms on your data. + +UMAP, it should be noted, is a manifold learning technique and as such +does not seek to preserve the distances between your data points in high +space but instead to learn the distances along an underlying manifold on +which your data points lie. As such, one shouldn’t be too surprised when +it disagrees with a non-manifold based clustering technique. A detailed +explanation of this phenomenon can be found in this `UMAP +documentation `__. + +API Reference +------------- + +.. automodule:: yellowbrick.text.umap_vis + :members: UMAPVisualizer + :undoc-members: + :show-inheritance: diff --git a/docs/changelog.rst b/docs/changelog.rst index ebb822017..0b4786591 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,87 @@ Changelog ========= +Version 1.0 +----------- + +* Tag: v1.0_ +* Deployed: Not yet deployed +* Contributors: Benjamin Bengfort, Rebecca Bilbro, Nathan Danielsen, Kristen McIntyre, Larry Gray, Prema Roman, Carl Dawson, Daniel Navarrete, Francois Dion, Halee Mason, Jeff Hale, Jiayi Zhang, Jimmy Shah, John Healy, Justin Ormont, Kevin Arvai, Michael Garod, Mike Curry, Nabanita Dash, Naresh Bachwani, Nicholas A. Brown, Piyush Gautam, Pradeep Singh, Rohit Ganapathy, Ry Whittington, Sangarshanan, Sourav Singh, Thomas J Fan, Zijie (ZJ) Poh, Zonghan, Xie + +.. warning:: **Python 2 Deprecation**: Please note that this release deprecates Yellowbrick's support for Python 2.7. After careful consideration and following the lead of our primary dependencies (NumPy, scikit-learn, and Matplolib), we have chosen to move forward with the community and support Python 3.4 and later. + +Major Changes: + - New ``JointPlot`` visualizer that is specifically designed for machine learning. The new visualizer can compare a feature to a target, features to features, and even feature to feature to target using color. The visualizer gives correlation information at a glance and is designed to work on ML datasets. + - New ``PosTagVisualizer`` is specifically designed for diagnostics around natural language processing and grammar-based feature extraction for machine learning. This new visualizer shows counts of different parts-of-speech throughout a tagged corpus. + - New datasets module that provide greater support for interacting with Yellowbrick example datasets including support for Pandas, npz, and text corpora. + - Management repository for Yellowbrick example data, ``yellowbrick-datasets``. + - Add support for matplotlib 3.0.1 or greater. + - ``UMAPVisualizer`` as an alternative manifold to TSNE for corpus visualization that is fast enough to not require preprocessing PCA or SVD decomposition and preserves higher order similarities and distances. + - Added ``..plot::`` directives to the documentation to automatically build the images along with the docs and keep them as up to date as possible. The directives also include the source code making it much simpler to recreate examples. + - Added ``target_color_type`` functionality to determine continuous or discrete color representations based on the type of the target variable. + - Added alpha param for both test and train residual points in ``ResidualsPlot``. + - Added ``frameon`` param to ``Manifold``. + - Added frequency sort feature to ``PosTagVisualizer``. + - Added elbow detection using the "kneedle" method to the ``KElbowVisualizer``. + - Added governance document outlining new Yellowbrick structure. + - Added ``CooksDistance`` regression visualizer. + - Updated ``DataVisualizer`` to handle target type identification. + - Extended ``DataVisualizer`` and updated its subclasses. + - Added ``ProjectionVisualizer`` base class. + - Restructured ``yellowbrick.target``, ``yellowbrick.features``, and ``yellowbrick.model_selection`` API. + - Restructured regressor and classifier API. + +Minor Changes: + - Updated ``Rank2D`` to include Kendall-Tau metric. + - Added user specification of ISO F1 values to ``PrecisionRecallCurve`` and updated the quick method to accept train and test splits. + - Added code review checklist and conventions to the documentation and expanded the contributing docs to include other tricks and tips. + - Added polish to missing value visualizers code, tests, and documentation. + - Improved ``RankD`` tests for better coverage. + - Added quick method test for ``DispersionPlot`` visualizer. + - BugFix: fixed resolve colors bug in TSNE and UMAP text visualizers and added regression tests to prevent future errors. + - BugFix: Added support for Yellowbrick palettes to return ``colormap``. + - BugFix: fixed ``PrecisionRecallCurve`` visual display problem with multi-class labels. + - BugFix: fixed the ``RFECV`` step display bug. + - BugFix: fixed error in distortion score calculation. + - Extended ``FeatureImportances`` documentation and tests for stacked importances and added a warning when stack should be true. + - Improved the documentation readability and structure. + - Refreshed the ``README.md`` and added testing and documentation READMEs. + - Updated the gallery to generate thumbnail-quality images. + - Updated the example notebooks and created a quickstart notebook. + - Fixed broken links in the documentation. + - Enhanced the ``SilhouetteVisualizer`` with ``legend`` and ``color`` parameter, while also move labels to the y-axis. + - Extended ``FeatureImportances`` docs/tests for stacked importances. + - Documented the ``yellowbrick.download`` script. + - Added JOSS citation for "Yellowbrick: Visualizing the Scikit-Learn Model Selection Process". + - Added new pull request (PR) template. + - Added ``alpha`` param to PCA Decomposition Visualizer. + - Updated documentation with affiliations. + - Added a ``windows_tol`` for the visual unittest suite. + - Added stacked barchart to ``PosTagVisualizer``. + - Let users set colors for ``FreqDistVisualizer`` and other ``ax_bar`` visualizers. + - Updated ``Manifold`` to extend ``ProjectionVisualizer``. + - Check if an estimator is already fitted before calling ``fit`` method. + - Ensure ``poof`` returns ``ax``. + +Compatibility Notes: + - This version provides support for matplotlib 3.0.1 or greater and drops support for matplotlib versions less than 2.0. + - This version drops support for Python 2 + +.. _v1.0: https://github.com/DistrictDataLabs/yellowbrick/releases/tag/v1.0 + + +Hotfix 0.9.1 +------------ + +This hotfix adds matplotlib3 support by requiring any version of matplotlib except for 3.0.0 which had a backend bug that affected Yellowbrick. + +* Tag: v0.9.1_ +* Deployed: Tuesday, February 5, 2019 +* Contributors: Benjamin Bengfort, Rebecca Bilbro, Ian Ozsvald, Francois Dion + +.. _v0.9.1: https://github.com/DistrictDataLabs/yellowbrick/releases/tag/v0.9.1 + + Version 0.9 ----------- * Tag: v0.9_ diff --git a/docs/conf.py b/docs/conf.py index d1e700567..88b151555 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,81 +1,100 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# conf +# Yellowbrick documentation build config file, created by sphinx-quickstart # -# yellowbrick documentation build configuration file, created by -# sphinx-quickstart on Tue Jul 5 19:45:43 2016. +# Created: Tue Jul 05 19:45:43 2016 -0400 +# Copyright (C) 2016-2019 The scikit-yb developers +# For license information, see LICENSE.txt # -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# ID: conf.py [6d697b7] lalorenz6@gmail.com $ + +""" +Yellowbrick documentation build config file, created by sphinx-quickstart. + +This file is executed with the current directory set to its containing dir +by ``execfile()``, e.g. the working directory will be yellowbrick/docs. +Ensure that all specified paths relative to the docs directory are made +absolute by using ``os.path.abspath``. + +Note that not all possible configuration values are present in this +autogenerated file. + +All configuration values have a default; values that are commented out +serve to show the default. + +See: https://www.sphinx-doc.org/en/master/usage/configuration.html +for more details on configuring the documentation build. +""" + +########################################################################## +## Imports +########################################################################## + +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # Set the backend of matplotlib to prevent build errors. import matplotlib -matplotlib.use('agg') +matplotlib.use("agg") + +# Import yellowbrick information. import yellowbrick as yb -# -- General configuration ------------------------------------------------ +########################################################################## +## General configuration +########################################################################## # If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' +# needs_sphinx = '1.8' + +# General information about the project. +project = "Yellowbrick" +copyright = "2016-2019, The scikit-yb developers." +author = "The scikit-yb developers" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. + +# The short X.Y version. +version = yb.get_version(short=True) +# The full version, including alpha/beta/rc tags. +release = "v" + yb.get_version(short=False) # Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. +# extensions coming with Sphinx (named 'sphinx.ext.*') or custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.todo', - 'numpydoc', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.todo", + "numpydoc", + "matplotlib.sphinxext.plot_directive", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: -# # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -# # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'yellowbrick' -copyright = '2016, District Data Labs' -author = 'District Data Labs' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = yb.__version__ -# The full version, including alpha/beta/rc tags. -release = yb.__version__ +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -86,39 +105,32 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -# # today = '' -# + # Else, today_fmt is used as the format for a strftime call. -# # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -# The reST default role (used for this markup: `text`) to use for all -# documents. -# +# The reST default role (used for this markup: `text`) for all docs. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -# # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -# # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -# # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -126,16 +138,63 @@ # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False +########################################################################## +## Extension Configuration +########################################################################## + # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +# Auto-plot settings either as extension or (file format, dpi) +plot_formats = [ + "png", + "pdf", + # ('hires.png', 350), +] + +# By default, include the source code generating plots in documentation +plot_include_source = True + +# Whether to show a link to the source in HTML. +plot_html_show_source_link = True + +# Code that should be executed before each plot. +plot_pre_code = ( + "import numpy as np\n" + "import matplotlib.pyplot as plt\n" + "from yellowbrick.datasets import *\n" +) + +# Whether to show links to the files in HTML. +plot_html_show_formats = True -# -- Options for HTML output ---------------------------------------------- +# A dictionary containing any non-standard rcParams that should be applied before each plot. +plot_rcparams = {"figure.figsize": (9, 6), "figure.dpi": 128} + +# Autodoc requires numpy to skip class members otherwise we get an exception: +# toctree contains reference to nonexisting document +# See: https://github.com/phn/pytpm/issues/3#issuecomment-12133978 +numpydoc_show_class_members = False + +# Locations of objects.inv files for intersphinx extension that auto-links +# to external api docs. +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "matplotlib": ("http://matplotlib.org/", None), + "scipy": ("http://docs.scipy.org/doc/scipy/reference", None), + "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "cycler": ("http://matplotlib.org/cycler/", None), + "sklearn": ("http://scikit-learn.org/stable/", None), +} + +########################################################################## +## Options for HTML output +########################################################################## # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -164,16 +223,18 @@ # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # -# html_favicon = None +html_favicon = "images/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] + def setup(app): app.add_stylesheet("theme_overrides.css") + # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. @@ -252,34 +313,36 @@ def setup(app): # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'yellowbrickdoc' +htmlhelp_basename = "yellowbrickdoc" -# -- Options for LaTeX output --------------------------------------------- +########################################################################## +## Options for LaTeX output +########################################################################## latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', } -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). +# Grouping the document tree into LaTeX files. List of tuples. latex_documents = [ - (master_doc, 'yellowbrick.tex', 'yellowbrick Documentation', - 'District Data Labs', 'manual'), + ( + master_doc, # source start file + "yellowbrick.tex", # target name + "{} Documentation".format(project), # title + author, # author + "manual", # documentclass [howto,manual, or own class] + ) ] # The name of an image file (relative to this directory) to place at the top of @@ -308,15 +371,13 @@ def setup(app): # # latex_domain_indices = True - -# -- Options for manual page output --------------------------------------- +########################################################################## +## Options for manual page output +########################################################################## # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'yellowbrick', 'yellowbrick Documentation', - [author], 1) -] +man_pages = [(master_doc, project, "{} Documentation".format(project), [author], 1)] # If true, show URL addresses after external links. # @@ -329,9 +390,15 @@ def setup(app): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'yellowbrick', 'yellowbrick Documentation', - author, 'yellowbrick', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "yellowbrick", + "{} Documentation".format(project), + author, + "yellowbrick", + "machine learning visualization", + "scientific visualization", + ) ] # Documents to append as an appendix to all manuals. @@ -349,16 +416,3 @@ def setup(app): # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False - -# Autodoc requires numpy to skip class members otherwise we get an exception: -# toctree contains reference to nonexisting document -# See: https://github.com/phn/pytpm/issues/3#issuecomment-12133978 -numpydoc_show_class_members = False - -# Locations of objects.inv files for intersphinx extension that auto links to external api docs. -intersphinx_mapping = {'python': ('https://docs.python.org/3', None), - 'matplotlib': ('http://matplotlib.org/', None), - 'scipy': ('http://scipy.github.io/devdocs/', None), - 'numpy': ('https://docs.scipy.org/doc/numpy-dev/', None), - 'cycler': ('http://matplotlib.org/cycler/', None), - 'sklearn': ('http://scikit-learn.org/stable/', None)} diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index 9765ec1ed..000000000 --- a/docs/contributing.rst +++ /dev/null @@ -1,426 +0,0 @@ -.. -*- mode: rst -*- - -Contributing -============ - -Yellowbrick is an open source project that is supported by a community who will gratefully and humbly accept any contributions you might make to the project. Large or small, any contribution makes a big difference; and if you've never contributed to an open source project before, we hope you will start with Yellowbrick! - -Principally, Yellowbrick development is about the addition and creation of *visualizers* --- objects that learn from data and create a visual representation of the data or model. Visualizers integrate with scikit-learn estimators, transformers, and pipelines for specific purposes and as a result, can be simple to build and deploy. The most common contribution is a new visualizer for a specific model or model family. We'll discuss in detail how to build visualizers later. - -Beyond creating visualizers, there are many ways to contribute: - -- Submit a bug report or feature request on `GitHub issues`_. -- Contribute an Jupyter notebook to our `examples gallery`_. -- Assist us with :doc:`user testing `. -- Add to the documentation or help with our website, `scikit-yb.org`_ -- Write unit or integration tests for our project. -- Answer questions on our `GitHub issues`_, `mailing list`_, `Stack Overflow`_, and `Twitter`_. -- Translate our documentation into another language. -- Write a blog post, tweet, or share our project with others. -- Teach someone how to use Yellowbrick. - -As you can see, there are lots of ways to get involved and we would be very happy for you to join us! The only thing we ask is that you abide by the principles of openness, respect, and consideration of others as described in our :doc:`code_of_conduct`. - -.. note:: If you're unsure where to start, perhaps the best place is to drop the maintainers a note via our mailing list: http://bit.ly/yb-listserv. - -.. _`examples gallery`: https://github.com/DistrictDataLabs/yellowbrick/tree/develop/examples -.. _`scikit-yb.org`: http://www.scikit-yb.org -.. _`GitHub issues`: https://github.com/DistrictDataLabs/yellowbrick/issues -.. _`mailing list`: http://bit.ly/yb-listserv -.. _`Stack Overflow`: https://stackoverflow.com/questions/tagged/yellowbrick -.. _`Twitter`: https://twitter.com/scikit_yb - -Getting Started on GitHub -------------------------- - -Yellowbrick is hosted on GitHub at https://github.com/DistrictDataLabs/yellowbrick. - -The typical workflow for a contributor to the codebase is as follows: - -1. **Discover** a bug or a feature by using Yellowbrick. -2. **Discuss** with the core contributors by `adding an issue `_. -3. **Assign** yourself the task by pulling a card from our `Waffle Kanban `_ -4. **Fork** the repository into your own GitHub account. -5. Create a **Pull Request** first thing to `connect with us `_ about your task. -6. **Code** the feature, write the tests and documentation, add your contribution. -7. **Review** the code with core contributors who will guide you to a high quality submission. -8. **Merge** your contribution into the Yellowbrick codebase. - -.. note:: Please create a pull request as soon as possible, even before you've started coding. This will allow the core contributors to give you advice about where to add your code or utilities and discuss other style choices and implementation details as you go. Don't wait! - -We believe that *contribution is collaboration* and therefore emphasize *communication* throughout the open source process. We rely heavily on GitHub's social coding tools to allow us to do this. - -Forking the Repository -~~~~~~~~~~~~~~~~~~~~~~ - -The first step is to fork the repository into your own account. This will create a copy of the codebase that you can edit and write to. Do so by clicking the **"fork"** button in the upper right corner of the Yellowbrick GitHub page. - -Once forked, use the following steps to get your development environment set up on your computer: - -1. Clone the repository. - - After clicking the fork button, you should be redirected to the GitHub page of the repository in your user account. You can then clone a copy of the code to your local machine.:: - - $ git clone https://github.com/[YOURUSERNAME]/yellowbrick - $ cd yellowbrick - -2. Create a virtual environment. - - Yellowbrick developers typically use `virtualenv `_ (and `virtualenvwrapper `_), `pyenv `_ or `conda envs `_ in order to manage their Python version and dependencies. Using the virtual environment tool of your choice, create one for Yellowbrick. Here's how with virtualenv:: - - $ virtualenv venv - -3. Install dependencies. - - Yellowbrick's dependencies are in the ``requirements.txt`` document at the root of the repository. Open this file and uncomment the dependencies that are for development only. Then install the dependencies with ``pip``:: - - $ pip install -r requirements.txt - - Note that there may be other dependencies required for development and testing; you can simply install them with ``pip``. For example to install - the additional dependencies for building the documentation or to run the - test suite, use the ``requirements.txt`` files in those directories:: - - $ pip install -r tests/requirements.txt - $ pip install -r docs/requirements.txt - -4. Switch to the develop branch. - - The Yellowbrick repository has a ``develop`` branch that is the primary working branch for contributions. It is probably already the branch you're on, but you can make sure and switch to it as follows:: - - $ git fetch - $ git checkout develop - -At this point you're ready to get started writing code. If you're going to take on a specific task, we'd strongly encourage you to check out the issue on `Waffle `_ and create a `pull request `_ *before you start coding* to better foster communication with other contributors. More on this in the next section. - -Pull Requests -~~~~~~~~~~~~~ - -A `pull request (PR) `_ is a GitHub tool for initiating an exchange of code and creating a communication channel for Yellowbrick maintainers to discuss your contribution. In essenence, you are requesting that the maintainers merge code from your forked repository into the develop branch of the primary Yellowbrick repository. Once completed, your code will be part of Yellowbrick! - -When starting a Yellowbrick contribution, *open the pull request as soon as possible*. We use your PR issue page to discuss your intentions and to give guidance and direction. Every time you push a commit into your forked repository, the commit is automatically included with your pull request, therefore we can review as you code. The earlier you open a PR, the more easily we can incorporate your updates, we'd hate for you to do a ton of work only to discover someone else already did it or that you went in the wrong direction and need to refactor. - -.. note:: For a great example of a pull request for a new feature visualizer, check out `this one `_ by `Carlo Morales `_. - -When you open a pull request, ensure it is from your forked repository to the develop branch of `github.com/districtdatalabs/yellowbrick `_; we will not merge a PR into the master branch. Title your Pull Request so that it is easy to understand what you're working on at a glance. Also be sure to include a reference to the issue that you're working on so that correct references are set up. - -After you open a PR, you should get a message from one of the maintainers. Use that time to discuss your idea and where best to implement your work. Feel free to go back and forth as you are developing with questions in the comment thread of the PR. Once you are ready, please ensure that you explicitly ping the maintainer to do a code review. Before code review, your PR should contain the following: - -1. Your code contribution -2. Tests for your contribution -3. Documentation for your contribution -4. A PR comment describing the changes you made and how to use them -5. A PR comment that includes an image/example of your visualizer - -At this point your code will be formally reviewed by one of the contributors. We use GitHub's code review tool, starting a new code review and adding comments to specific lines of code as well as general global comments. Please respond to the comments promptly, and don't be afraid to ask for help implementing any requested changes! You may have to go back and forth a couple of times to complete the code review. - -When the following is true: - -1. Code is reviewed by at least one maintainer -2. Continuous Integration tests have passed -3. Code coverage and quality have not decreased -4. Code is up to date with the yellowbrick develop branch - -Then we will "Squash and Merge" your contribution, combining all of your commits into a single commit and merging it into the develop branch of Yellowbrick. Congratulations! Once your contribution has been merged into master, you will be officially listed as a contributor. - -Developing Visualizers ----------------------- - -In this section, we'll discuss the basics of developing visualizers. This of course is a big topic, but hopefully these simple tips and tricks will help make sense. First thing though, check out this presentation that we put together on yellowbrick development, it discusses the expected user workflow, our integration with scikit-learn, our plans and roadmap, etc: - -.. raw:: html - - - -One thing that is necessary is a good understanding of scikit-learn and Matplotlib. Because our API is intended to integrate with scikit-learn, a good start is to review `"APIs of scikit-learn objects" `_ and `"rolling your own estimator" `_. In terms of matplotlib, use Yellowbrick's guide :doc:`matplotlib`. Additional resources include `Nicolas P. Rougier's Matplotlib tutorial `_ and `Chris Moffitt's Effectively Using Matplotlib `_. - -Visualizer API -~~~~~~~~~~~~~~ - -There are two basic types of Visualizers: - -- **Feature Visualizers** are high dimensional data visualizations that are essentially transformers. -- **Score Visualizers** wrap a scikit-learn regressor, classifier, or clusterer and visualize the behavior or performance of the model on test data. - -These two basic types of visualizers map well to the two basic objects in scikit-learn: - -- **Transformers** take input data and return a new data set. -- **Estimators** are fit to training data and can make predictions. - -The scikit-learn API is object oriented, and estimators and transformers are initialized with parameters by instantiating their class. Hyperparameters can also be set using the ``set_attrs()`` method and retrieved with the corresponding ``get_attrs()`` method. All scikit-learn estimators have a ``fit(X, y=None)`` method that accepts a two dimensional data array, ``X``, and optionally a vector ``y`` of target values. The ``fit()`` method trains the estimator, making it ready to transform data or make predictions. Transformers have an associated ``transform(X)`` method that returns a new dataset, ``Xprime`` and models have a ``predict(X)`` method that returns a vector of predictions, ``yhat``. Models also have a ``score(X, y)`` method that evaluate the performance of the model. - -Visualizers interact with scikit-learn objects by intersecting with them at the methods defined above. Specifically, visualizers perform actions related to ``fit()``, ``transform()``, ``predict()``, and ``score()`` then call a ``draw()`` method which initializes the underlying figure associated with the visualizer. The user calls the visualizer's ``poof()`` method, which in turn calls a ``finalize()`` method on the visualizer to draw legends, titles, etc. and then ``poof()`` renders the figure. The Visualizer API is therefore: - -- ``draw()``: add visual elements to the underlying axes object -- ``finalize()``: prepare the figure for rendering, adding final touches such as legends, titles, axis labels, etc. -- ``poof()``: render the figure for the user (or saves it to disk). - -Creating a visualizer means defining a class that extends ``Visualizer`` or one of its subclasses, then implementing several of the methods described above. A barebones implementation is as follows:: - - import matplotlib.pyplot as plt - - from yellowbrick.base import Visualizer - - class MyVisualizer(Visualizer): - - def __init__(self, ax=None, **kwargs): - super(MyVisualizer, self).__init__(ax, **kwargs) - - def fit(self, X, y=None): - self.draw(X) - return self - - def draw(self, X): - if self.ax is None: - self.ax = self.gca() - - self.ax.plt(X) - - def finalize(self): - self.set_title("My Visualizer") - -This simple visualizer simply draws a line graph for some input dataset X, intersecting with the scikit-learn API at the ``fit()`` method. A user would use this visualizer in the typical style:: - - visualizer = MyVisualizer() - visualizer.fit(X) - visualizer.poof() - -Score visualizers work on the same principle but accept an additional required ``model`` argument. Score visualizers wrap the model (which can be either instantiated or uninstantiated) and then pass through all attributes and methods through to the underlying model, drawing where necessary. - -Testing -~~~~~~~ - -The test package mirrors the yellowbrick package in structure and also contains several helper methods and base functionality. To add a test to your visualizer, find the corresponding file to add the test case, or create a new test file in the same place you added your code. - -Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. Therefore the primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. To assist with this, we have two primary helpers, ``VisualTestCase`` and ``DatasetMixin``. Create your unittest as follows:: - - import pytest - from tests.base import VisualTestCase - from tests.dataset import DatasetMixin - - class MyVisualizerTests(VisualTestCase, DatasetMixin): - - def test_my_visualizer(self): - """ - Test MyVisualizer on a real dataset - """ - # Load the data from the fixture - dataset = self.load_data('occupancy') - - # Get the data - X = dataset[[ - "temperature", "relative_humidity", "light", "C02", "humidity" - ]] - y = dataset['occupancy'].astype(int) - - try: - visualizer = MyVisualizer() - visualizer.fit(X) - visualizer.poof() - except Exception as e: - pytest.fail("my visualizer didn't work") - -Tests can be run as follows:: - - $ make test - -The Makefile uses the pytest runner and testing suite as well as the coverage library, so make sure you have those dependencies installed! The ``DatasetMixin`` also requires `requests.py `_ to fetch data from our Amazon S3 account. - -Image Comparison Tests -~~~~~~~~~~~~~~~~~~~~~~ - -Writing an image based comparison test is only a little more difficult than the simple testcase presented above. We have adapted matplotlib's image comparison test utility into an easy to use assert method : ``self.assert_images_similar(visualizer)`` - -The main consideration is that you must specify the “baseline”, or expected, image in the ``tests/baseline_images/`` folder structure. - -For example, create your unittest located in ``tests/test_regressor/test_myvisualizer.py`` as follows:: - - from tests.base import VisualTestCase - ... - def test_my_visualizer_output(self): - ... - visualizer = MyVisualizer() - visualizer.fit(X) - visualizer.poof() - self.assert_images_similar(visualizer) - -The first time this test is run, there will be no baseline image to compare against, so the test will fail. Copy the output images (in this case ``tests/actual_images/test_regressor/test_myvisualizer/test_my_visualizer_output.png``) to the correct subdirectory of baseline_images tree in the source directory (in this case ``tests/baseline_images/test_regressor/test_myvisualizer/test_my_visualizer_output.png``). Put this new file under source code revision control (with git add). When rerunning the tests, they should now pass. - -We also have a helper script, ``tests/images.py`` to clean up and manage baseline images automatically. It is run using the ``python -m`` command to execute a module as main, and it takes as an argument the path to your *test file*. To copy the figures as above:: - - $ python -m tests.images tests/test_regressor/test_myvisualizer.py - -This will move all related test images from ``actual_images`` to ``baseline_images`` on your behalf (note you'll have had to run the tests at least once to generate the images). You can also clean up images from both actual and baseline as follows:: - - $ python -m tests.images -C tests/test_regressor/test_myvisualizer.py - -This is useful particularly if you're stuck trying to get an image comparison to work. For more information on the images helper script, use ``python -m tests.images --help``. - -Documentation -~~~~~~~~~~~~~ - -The initial documentation for your visualizer will be a well structured docstring. Yellowbrick uses Sphinx to build documentation, therefore docstrings should be written in reStructuredText in numpydoc format (similar to scikit-learn). The primary location of your docstring should be right under the class definition, here is an example:: - - class MyVisualizer(Visualizer): - """ - This initial section should describe the visualizer and what - it's about, including how to use it. Take as many paragraphs - as needed to get as much detail as possible. - - In the next section describe the parameters to __init__. - - Parameters - ---------- - - model : a scikit-learn regressor - Should be an instance of a regressor, and specifically one whose name - ends with "CV" otherwise a will raise a YellowbrickTypeError exception - on instantiation. To use non-CV regressors see: - ``ManualAlphaSelection``. - - ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes - will be used (or generated if required). - - kwargs : dict - Keyword arguments that are passed to the base class and may influence - the visualization as defined in other Visualizers. - - Examples - -------- - - >>> model = MyVisualizer() - >>> model.fit(X) - >>> model.poof() - - Notes - ----- - - In the notes section specify any gotchas or other info. - """ - -When your visualizer is added to the API section of the documentation, this docstring will be rendered in HTML to show the various options and functionality of your visualizer! - -To add the visualizer to the documentation it needs to be added to the ``docs/api`` folder in the correct subdirectory. For example if your visualizer is a model score visualizer related to regression it would go in the ``docs/api/regressor`` subdirectory. If you have a question where your documentation should be located, please ask the maintainers via your pull request, we'd be happy to help! - -There are two primary files that need to be created: - -1. **mymodule.rst**: the reStructuredText document -2. **mymodule.py**: a python file that generates images for the rst document - -There are quite a few examples in the documentation on which you can base your files of similar types. The primary format for the API section is as follows:: - - .. -*- mode: rst -*- - - My Visualizer - ============= - - Intro to my visualizer - - .. code:: python - - # Example to run MyVisualizer - visualizer = MyVisualizer(LinearRegression()) - - visualizer.fit(X, y) - g = visualizer.poof() - - - .. image:: images/my_visualizer.png - - Discussion about my visualizer - - - API Reference - ------------- - - .. automodule:: yellowbrick.regressor.mymodule - :members: MyVisualizer - :undoc-members: - :show-inheritance: - -This is a pretty good structure for a documentation page; a brief introduction followed by a code example with a visualization included (using the ``mymodule.py`` to generate the images into the local directory's ``images`` subdirectory). The primary section is wrapped up with a discussion about how to interpret the visualizer and use it in practice. Finally the ``API Reference`` section will use ``automodule`` to include the documentation from your docstring. - -At this point there are several places where you can list your visualizer, but to ensure it is included in the documentation it *must be listed in the TOC of the local index*. Find the ``index.rst`` file in your subdirectory and add your rst file (without the ``.rst`` extension) to the ``..toctree::`` directive. This will ensure the documentation is included when it is built. - -Speaking of, you can build your documentation by changing into the ``docs`` directory and running ``make html``, the documentation will be built and rendered in the ``_build/html`` directory. You can view it by opening ``_build/html/index.html`` then navigating to your documentation in the browser. - -There are several other places that you can list your visualizer including: - - - ``docs/index.rst`` for a high level overview of our visualizers - - ``DESCRIPTION.rst`` for inclusion on PyPI - - ``README.md`` for inclusion on GitHub - -Please ask for the maintainer's advice about how to include your visualizer in these pages. - -Advanced Development --------------------- - -In this section we discuss more advanced contributing guidelines including setting up branches for development as well as the release cycle. This section is intended for maintainers and core contributors of the Yellowbrick project. If you would like to be a maintainer please contact one of the current maintainers of the project. - -Branching Convention -~~~~~~~~~~~~~~~~~~~~ - -The Yellowbrick repository is set up in a typical production/release/development cycle as described in "`A Successful Git Branching Model `_." The primary working branch is the ``develop`` branch. This should be the branch that you are working on and from, since this has all the latest code. The ``master`` branch contains the latest stable version and release_, which is pushed to PyPI_. No one but core contributors will generally push to master. - -.. note:: All pull requests should be into the ``yellowbrick/develop`` branch from your forked repository. - -You can work directly in your fork and create a pull request from your fork's develop branch into ours. We also recommend setting up an ``upstream`` remote so that you can easily pull the latest development changes from the main Yellowbrick repository (see `configuring a remote for a fork `_). You can do that as follows:: - - $ git remote add upstream https://github.com/DistrictDataLabs/yellowbrick.git - $ git remote -v - origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch) - origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push) - upstream https://github.com/DistrictDataLabs/yellowbrick.git (fetch) - upstream https://github.com/DistrictDataLabs/yellowbrick.git (push) - -When you're ready, request a code review for your pull request. Then, when reviewed and approved, you can merge your fork into our main branch. Make sure to use the "Squash and Merge" option in order to create a Git history that is understandable. - -.. note:: When merging a pull request, use the "squash and merge" option. - -Core contributors have write access to the repository. In order to reduce the number of merges (and merge conflicts) we recommend that you utilize a feature branch off of develop to do intermediate work in:: - - $ git checkout -b feature-myfeature develop - -Once you are done working (and everything is tested) merge your feature into develop.:: - - $ git checkout develop - $ git merge --no-ff feature-myfeature - $ git branch -d feature-myfeature - $ git push origin develop - -Head back to Waffle and checkout another issue! - -Releases -~~~~~~~~ - -When ready to create a new release we branch off of develop as follows:: - - $ git checkout -b release-x.x - -This creates a release branch for version x.x. At this point do the version bump by modifying ``version.py`` and the test version in ``tests/__init__.py``. Make sure all tests pass for the release and that the documentation is up to date. There may be style changes or deployment options that have to be done at this phase in the release branch. At this phase you'll also modify the ``changelog`` with the features and changes in the release. - -Once the release is ready for prime-time, merge into master:: - - $ git checkout master - $ git merge --no-ff --no-edit release-x.x - -Tag the release in GitHub:: - - $ git tag -a vx.x - $ git push origin vx.x - -You'll have to go to the release_ page to edit the release with similar information as added to the changelog. Once done, push the release to PyPI:: - - $ make build - $ make deploy - -Check that the PyPI page is updated with the correct version and that ``pip install -U yellowbrick`` updates the version and works correctly. Also check the documentation on PyHosted, ReadTheDocs, and on our website to make sure that it was correctly updated. Finally merge the release into develop and clean up:: - - $ git checkout develop - $ git merge --no-ff --no-edit release-x.x - $ git branch -d release-x.x - -Hotfixes and minor releases also follow a similar pattern; the goal is to effectively get new code to users as soon as possible! - -.. _release: https://github.com/DistrictDataLabs/yellowbrick/releases -.. _PyPI: https://pypi.python.org/pypi/yellowbrick diff --git a/docs/contributing/advanced_development_topics.rst b/docs/contributing/advanced_development_topics.rst new file mode 100644 index 000000000..6f32446c2 --- /dev/null +++ b/docs/contributing/advanced_development_topics.rst @@ -0,0 +1,233 @@ +.. -*- mode: rst -*- + +Advanced Development Topics +=========================== + +In this section we discuss more advanced contributing guidelines such as code conventions,the release life cycle or branch management. This section is intended for maintainers and core contributors of the Yellowbrick project. If you would like to be a maintainer please contact one of the current maintainers of the project. + +Reviewing Pull Requests +----------------------- + +We use several strategies when reviewing pull requests from contributors to Yellowbrick. If the pull request affects only a single file or a small portion of the code base, it is sometimes sufficient to review the code using `GitHub's lightweight code review feature `_. However, if the changes impact a number of files or modify the documentation, our convention is to add the contributor's fork as a remote, pull, and check out their feature branch locally. From inside your fork of Yellowbrick, this can be done as follows:: + + $ git remote add contribsusername https://github.com/contribsusername/yellowbrick.git + $ git fetch contribsusername + $ git checkout -b contribsfeaturebranch contribsusername/contribsfeaturebranch + +This will allow you to inspect their changes, run the tests, and build the docs locally. If the contributor has elected to allow reviewers to modify their feature branch, you will also be able to push changes directly to their branch:: + + $ git add filethatyouchanged.py + $ git commit -m "Adjusted tolerance levels to appease AppVeyor" + $ git push contribsusername contribsfeaturebranch + +These changes will automatically go into the pull request, which can be useful for making small modifications (e.g. visual test tolerance levels) to get the PR over the finish line. + + +Visualizer Review Checklist +--------------------------- + +As the visualizer API has matured over time, we've realized that there are a number of routine items that must be in place to consider a visualizer truly complete and ready for prime time. This list is also extremely helpful for reviewing code submissions to ensure that visualizers are consistently implemented, tested, and documented. Though we do not expect these items to be checked off on every PR, the below list includes some guidance about what to look for when reviewing or writing a new Visualizer. + +.. note:: The ``contrib`` module is a great place for work-in-progress Visualizers! + +Code Conventions +~~~~~~~~~~~~~~~~ + +- Ensure the visualizer API is met. + + The basic principle of the visualizer API is that scikit-learn methods such as ``fit()``, ``transform()``, ``score()``, etc. perform interactions with scikit-learn or other computations and call the ``draw()`` method. Calls to matplotlib should happen only in ``draw()`` or ``finalize()``. + +- Create a quick method for the visualizer. + + In addition to creating the visualizer class, ensure there is an associated quick method that returns the visualizer and creates the visualization in one line of code! + +- Subclass the correct visualizer. + + Ensure that the visualizer is correctly subclassed in the class hierarchy. If you're not sure what to subclass, please ping a maintainer, they'd be glad to help! + +- Ensure numpy array comparisons are not ambiguous. + + Often there is code such as ``if y:`` where ``y`` is an array. However this is ambiguous when used with numpy arrays and other data containers. Change this code to ``y is not None`` or ``len(y) > 0`` or use ``np.all`` or ``np.any`` to test if the contents of the array are truthy/falsy. + +- Add ``random_state`` argument to visualizer. + + If the visualizer uses/wraps a utility that also has ``random_state``, then the visualizer itself needs to also have this argument which defaults to ``None`` and is passed to all internal stochastic behaviors. This ensures that image comparison testing will work and that users can get repeated behavior from visualizers. + +- Use ``np.unique`` instead of `set`. + + If you need the unique values from a list or array, we prefer to use numpy methods wherever possible. We performed some limited benchmarking and believe that ``np.unique`` is a bit faster and more efficient. + +- Use sklearn underscore suffix for learned parameters. + + Any parameters that are learned during ``fit()`` should only be added to the visualizer when ``fit()`` is called (this is also how we determine if a visualizer is fitted or not) and should be identified with an underscore suffix. For example, in classification visualizers, the classes can be either passed in by the user or determined when they are passed in via fit, therefore it should be ``self.classes_``. This is also true for other learned parameters, e.g. ``self.score_``, even though this is not created during ``fit()``. + +- Correctly set the title in finalize. + + Use the ``self.set_title()`` method to set a default title; this allows the user to specify a custom title in the initialization arguments. + +Testing Conventions +~~~~~~~~~~~~~~~~~~~ + +- Ensure there is an image comparison test. + + Ensure there is at least one image comparison test per visualizer. This is the primary regression testing of Yellowbrick and these tests catch a lot when changes occur in our dependencies or environment. + +- Use pytest assertions rather than ``unittest.TestCase`` methods. + + We prefer ``assert 2+2 == 4`` rather than ``self.assertEquals(2+2, 4)``. As a result, test classes should not extend ``unittest.Testcase`` but should extend the ``VisualTestCase`` in the tests package. Note that if you're writing tests that do not generate matplotlib figures you can simply extend ``object``. + +- Use test fixtures and sklearn dataset generators. + + Data is the key to testing with Yellowbrick; often the test package will have fixtures in ``conftest.py`` that can be directly used (e.g. binary vs. multiclass in the ``test_classifier`` package). If one isn't available feel free to use randomly generated datasets from the ``sklearn.datasets`` module e.g. ``make_classification``, ``make_regression``, or ``make_blobs``. For integration testing, please feel free to use one of the Yellowbrick datasets. + +- Fix all ``random_state`` arguments. + + Be on the lookout for any method (particularly sklearn methods) that have a ``random_state`` argument and be sure to fix them so that tests always pass! + +- Test a variety of inputs. + + Machine learning can be done on a variety of inputs for ``X`` and ``y``, ensure there is a test with numpy arrays, pandas DataFrame and Series objects, and with Python lists. + +- Test that ``fit()`` returns self. + + When doing end-to-end testing, we like to ``assert oz.fit() is oz`` to ensure the API is maintained. + +- Test that ``score()`` between zero and one. + + With visualizers that have a ``score()`` method, we like to ``assert 0.0 <= oz.score() >=1.0`` to ensure the API is maintained. + +Documentation Conventions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Visualizer DocString is correct. + + The visualizer docstring should be present under the class and contain a narrative about the visualizer and its arguments with the numpydoc style. + +- API Documentation. + + All visualizers should have their own API page under ``docs/api/[yb-module]``. This documentation should include an ``automodule`` statement. Generally speaking there is also an image generation script of the same name in this folder so that the documentation images can be generated on demand. + +- Listing the visualizer. + + The visualizer should be listed in a number of places including: ``docs/api/[yb-module]/index.rst``, ``docs/api/index.rst``, ``docs/index.rst``, ``README.md``, and ``DESCRIPTION.rst``. + +- Include a gallery image. + + Please also add the visualizer image to the gallery! + +- Update added to the changelog. + + To reduce the time it takes to put together the changelog, we'd like to update it when we add new features and visualizers rather than right before the release. + +Merging Pull Requests +--------------------- + +Our convention is that the person who performs the code review should merge the pull request (since reviewing is hard work and deserves due credit!). Only core contributors have write access to the repository and can merge pull requests. Some preferences for commit messages when merging in pull requests: + +- Make sure to use the "Squash and Merge" option in order to create a Git history that is understandable. +- Keep the title of the commit short and descriptive; be sure it includes the PR #. +- Craft a commit message body that is 1-3 sentences, depending on the complexity of the commit; it should explicitly reference any issues being closed or opened using `GitHub's commit message keywords `_. + +.. note:: When merging a pull request, use the "squash and merge" option. + + +Releases +-------- + +To ensure we get new code to our users as soon and as bug free as possible we periodically create major, minor, and hotfix version releases that are merged from the ``develop`` branch into ``master`` and pushed to PyPI and Anaconda Cloud. Our release cycle ensures that stable code can be found in the master branch and pip installed and that we can test our development code thoroughly before a release. + +.. note:: The following steps must be taken by a maintainer with access to the primary (upstream) Yellowbrick repository. Any reference to ``origin`` refers to github.com/DistrictDataLabs/yellowbrick. + +The first step is to create a release branch from develop - this allows us to do "release-work" (e.g. a version bump, changelog stuff, etc.) in a branch that is neither ``develop`` nor ``master`` and to test the release before deployment:: + + $ git checkout develop + $ git pull origin develop + $ git checkout -b release-x.x + +This creates a release branch for version ``x.x`` where ``x`` is a digit. Release versions are described as a number ``x.y.z`` where ``x`` is the major version, ``y`` is the minor version and ``z`` is a patch version. Generally speaking most releases are minor version releases where ``x.y`` becomes ``x.y+1```. Patch versions are infrequent but may also be needed where very little has changed or something quick has to be pushed to fix a critical bug, e.g.g ``x.y`` becomes ``x.y.1``. Major version releases where ``x.y`` become ``x+1.0`` are rare. + +At this point do the version bump by modifying ``version.py`` and the test version in ``tests/__init__.py``. Make sure all tests pass for the release and that the documentation is up to date. To build the docs see the :ref:`documentation notes `. There may be style changes or deployment options that have to be done at this phase in the release branch. At this phase you'll also modify the ``changelog`` with the features and changes in the release that have not already been marked. + +.. note:: Before merging the release to master make sure that the release checklist has been completed! + +Once the release is ready for prime-time, merge into master:: + + $ git checkout master + $ git merge --no-ff --no-edit release-x.x + $ git push origin master + +Tag the release in GitHub:: + + $ git tag -a vx.x + $ git push origin vx.x + +Now go to the release_ page to convert the tag into a release and add a Markdown version of the changelog notes for those that are accessing the release directly from GitHub. + +Deploying to PyPI +~~~~~~~~~~~~~~~~~ + +Deploying the release to PyPI is fairly straight forward. Ensure that you have valid PyPI login credentials in ``~/.pypirc`` and use the Makefile to deploy as follows:: + + $ make build + $ make deploy + +The build process should create ``build`` and ``dist`` directories containing the wheel and source packages as well as a ``.egg-info`` file for deployment. The deploy command registers the version in PyPI and uploads it with Twine. + +Deploying to Anaconda Cloud +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These instructions follow the tutorial `"Building conda packages with conda skeleton" `_. To deploy release to Anaconda Cloud you first need to have Miniconda or Anaconda installed along with ``conda-build`` and ``anaconda-client`` (which can be installed using ``conda``). Make sure that you run the ``anaconda login`` command using the credentials that allow access to the Yellowbrick channel. If you have an old skeleton directory, make sure to save it with a different name (e.g. yellowbrick.old) before running the skeleton command:: + + $ conda skeleton pypi yellowbrick + +This should install the latest version of yellowbrick from PyPI - make sure the version matches the expected version of the release! There are some edits that must be made to the ``yellowbrick/meta.yaml`` that is generated as follows:: + + about: + home: http://scikit-yb.org/ + license_file: LICENSE.txt + doc_url: https://www.scikit-yb.org/en/latest/ + dev_url: https://github.com/DistrictDataLabs/yellowbrick + +In addition, you must remove the entire ``test:`` section of the yaml file and add the following to the ``requirements:`` under both ``host:`` and ``run:``. See `example meta.yaml `_ for a detailed version. Note that the description field in the metadata is pulled from the ``DESCRIPTION.rst`` in the root of the Yellowbrick project. However, Anaconda Cloud requires a Markdown description - the easiest thing to do is to copy it from the existing description. + +With the ``meta.yaml`` file setup you can now run the build command for the various Python distributes that Yellowbrick supports:: + + $ conda build --python 3.6 yellowbrick + $ conda build --python 3.7 yellowbrick + +After this command completes you should have build files in ``$MINICONDA_HOME/conda-bld/[OS]/yellowbrick-x.x-py3.x_0.tar.bz2``. You can now run conda convert for each of the Python versions using this directory as follows:: + + $ conda convert --platform all [path to build] -o $MINICONDA_HOME/conda-bld + +At this point you should have builds for all the versions of Python and all platforms Yellowbrick supports. Unfortunately at this point you have to upload them all to Anaconda Cloud:: + + $ anaconda upload $MINICONDA_HOME/conda-bld/[OS]/yellowbrick-x.x-py3.x_0.tar.bz2 + +Once uploaded, the Anaconda Cloud page should reflect the latest version, you may have to edit the description to make sure it's in Markdown format. + +Finalizing the Release +~~~~~~~~~~~~~~~~~~~~~~ + +The last steps in the release process are to check to make sure the release completed successfully. Make sure that the `PyPI page`_ and the `Anaconda Cloud Page`_ are correctly updated to the latest version. Also ensure that ReadTheDocs has correctly built the "latest" documentation on `scikit-yb.org `_. + +Make sure that you can update the package on your local machine, either in a virtual environment that does not include yellowbrick or in a Python install that is not used for development (e.g. not in the yellowbrick project directory):: + + $ pip install -U yellowbrick + $ python -c "import yellowbrick; print(yellowbrick.__version__)" + +After verifying that the version has been correctly updated you can clean up the project directory:: + + $ make clean + +After this, it's time to merge the release into develop so that we can get started on the next version! :: + + $ git checkout develop + $ git merge --no-ff --no-edit release-x.x + $ git branch -d release-x.x + $ git push origin develop + +Make sure to celebrate the release with the other maintainers and to tweet to everyone to let them know it's time to update Yellowbrick! + +.. _release: https://github.com/DistrictDataLabs/yellowbrick/releases +.. _PyPI Page: https://pypi.org/project/yellowbrick/ +.. _Anaconda Cloud Page: https://anaconda.org/DistrictDataLabs/yellowbrick diff --git a/docs/contributing/developing_visualizers.rst b/docs/contributing/developing_visualizers.rst new file mode 100644 index 000000000..2b1af4539 --- /dev/null +++ b/docs/contributing/developing_visualizers.rst @@ -0,0 +1,449 @@ +.. -*- mode: rst -*- + +Developing Visualizers +====================== + +In this section, we'll discuss the basics of developing visualizers. This of course is a big topic, but hopefully these simple tips and tricks will help make sense. First thing though, check out this presentation that we put together on yellowbrick development, it discusses the expected user workflow, our integration with scikit-learn, our plans and roadmap, etc: + +.. raw:: html + + + +One thing that is necessary is a good understanding of scikit-learn and Matplotlib. Because our API is intended to integrate with scikit-learn, a good start is to review `"APIs of scikit-learn objects" `_ and `"rolling your own estimator" `_. In terms of matplotlib, use Yellowbrick's guide :doc:`../matplotlib`. Additional resources include `Nicolas P. Rougier's Matplotlib tutorial `_ and `Chris Moffitt's Effectively Using Matplotlib `_. + +Visualizer API +-------------- + +There are two basic types of Visualizers: + +- **Feature Visualizers** are high dimensional data visualizations that are essentially transformers. +- **Score Visualizers** wrap a scikit-learn regressor, classifier, or clusterer and visualize the behavior or performance of the model on test data. + +These two basic types of visualizers map well to the two basic estimator objects in scikit-learn: + +- **Transformers** take input data and return a new data set. +- **Models** are fit to training data and can make predictions. + +The scikit-learn API is object oriented, and estimators are initialized with parameters by instantiating their class. Hyperparameters can also be set using the ``set_attrs()`` method and retrieved with the corresponding ``get_attrs()`` method. All scikit-learn estimators have a ``fit(X, y=None)`` method that accepts a two dimensional data array, ``X``, and optionally a vector ``y`` of target values. The ``fit()`` method trains the estimator, making it ready to transform data or make predictions. Transformers have an associated ``transform(X)`` method that returns a new dataset, ``Xprime`` and models have a ``predict(X)`` method that returns a vector of predictions, ``yhat``. Models may also have a ``score(X, y)`` method that evaluate the performance of the model. + +Visualizers interact with scikit-learn objects by intersecting with them at the methods defined above. Specifically, visualizers perform actions related to ``fit()``, ``transform()``, ``predict()``, and ``score()`` then call a ``draw()`` method which initializes the underlying figure associated with the visualizer. The user calls the visualizer's ``poof()`` method, which in turn calls a ``finalize()`` method on the visualizer to draw legends, titles, etc. and then ``poof()`` renders the figure. The Visualizer API is therefore: + +- ``draw()``: add visual elements to the underlying axes object +- ``finalize()``: prepare the figure for rendering, adding final touches such as legends, titles, axis labels, etc. +- ``poof()``: render the figure for the user (or saves it to disk). + +Creating a visualizer means defining a class that extends ``Visualizer`` or one of its subclasses, then implementing several of the methods described above. A barebones implementation is as follows: + +.. code:: python + + import matplotlib.pyplot as plt + + from yellowbrick.base import Visualizer + + class MyVisualizer(Visualizer): + + def __init__(self, ax=None, **kwargs): + super(MyVisualizer, self).__init__(ax, **kwargs) + + def fit(self, X, y=None): + self.draw(X) + return self + + def draw(self, X): + self.ax.plt(X) + return self.ax + + def finalize(self): + self.set_title("My Visualizer") + +This simple visualizer simply draws a line graph for some input dataset X, intersecting with the scikit-learn API at the ``fit()`` method. A user would use this visualizer in the typical style: + +.. code:: python + + visualizer = MyVisualizer() + visualizer.fit(X) + visualizer.poof() + +Score visualizers work on the same principle but accept an additional required ``estimator`` argument. Score visualizers wrap the model (which can be either fitted or unfitted) and then pass through all attributes and methods through to the underlying model, drawing where necessary. + +.. code:: python + + from yellowbrick.base import ScoreVisualizer + + class MyScoreVisualizer(ScoreVisualizer): + + def __init__(self, estimator, ax=None, **kwargs): + super(MyScoreVisualizer, self).__init__(estimator, ax=ax, **kwargs) + + def fit(self, X_train, y_train=None): + # Fit the underlying model + super(MyScoreVisualizer, self).fit(X_train, y_train) + self.draw(X_train, y_train) + return self + + def score(self, X_test, y_test): + # Score the underlying model + super(MyScoreVisualizer, self).fit(X_train, y_train) + self.draw(X_test, y_test) + return self.score_ + + def draw(self, X, y): + self.ax.scatter(X, c=y) + return self.ax + + def finalize(self): + self.set_title("My Score Visualizer") + +Note that the calls to ``super`` in the above code ensure that the base functionality (e.g. fitting a model and computing the score) are required to ensure the visualizer is consistent with other visualizers. + +Datasets +-------- + +Yellowbrick gives easy access to several datasets that are used for the examples in the documentation and testing. These datasets are hosted in our CDN and must be downloaded for use. Typically, when a user calls one of the data loader functions, e.g. ``load_bikeshare()`` the data is automatically downloaded if it's not already on the user's computer. However, for development and testing, or if you know you will be working without internet access, it might be easier to simply download all the data at once. + +The data downloader script can be run as follows:: + + $ python -m yellowbrick.download + +This will download the data to the fixtures directory inside of the Yellowbrick site packages. You can specify the location of the download either as an argument to the downloader script (use ``--help`` for more details) or by setting the ``$YELLOWBRICK_DATA`` environment variable. This is the preferred mechanism because this will also influence how data is loaded in Yellowbrick. + +Note that developers who have downloaded data from Yellowbrick versions earlier than v1.0 may experience some problems with the older data format. If this occurs, you can clear out your data cache as follows:: + + $ python -m yellowbrick.download --cleanup + +This will remove old datasets and download the new ones. You can also use the ``--no-download`` flag to simply clear the cache without re-downloading data. Users who are having difficulty with datasets can also use this or they can uninstall and reinstall Yellowbrick using ``pip``. + +Testing +------- + +The test package mirrors the yellowbrick package in structure and also contains several helper methods and base functionality. To add a test to your visualizer, find the corresponding file to add the test case, or create a new test file in the same place you added your code. + +Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. Therefore the primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. To assist with this, we have a helper, ``VisualTestCase``. Create your tests as follows: + +.. code:: python + + import pytest + + from tests.base import VisualTestCase + from yellowbrick.datasets import load_occupancy + + class MyVisualizerTests(VisualTestCase): + + def test_my_visualizer(self): + """ + Test MyVisualizer on a real dataset + """ + # Load the occupancy dataset + X, y = load_occupancy() + + try: + visualizer = MyVisualizer() + assert visualizer.fit(X, y) is visualizer, "fit should return self" + visualizer.poof() + except Exception as e: + pytest.fail("my visualizer didn't work: {}".format(e)) + +This simple test case is an excellent start to a larger test package and we recommend starting with this test as you develop your visualizer. Once you've completed the development and prototyping you can start to include :ref:`test fixtures ` and test various normal use cases and edge cases with unit tests, then build :ref:`image similarity tests ` to more thoroughly define the integration tests. + + +Running the Test Suite +~~~~~~~~~~~~~~~~~~~~~~ + +To run the test suite, first install the testing dependencies that are located in the `tests` folder as follows:: + + $ pip install -r tests/requirements.txt + +The required dependencies for the test suite include testing utilities and libraries such as pandas and nltk that are not included in the core dependencies. + +Tests can be run as follows from the project root:: + + $ make test + +The Makefile uses the pytest runner and testing suite as well as the coverage library. + +.. _assert_images_similar: + +Image Comparison Tests +~~~~~~~~~~~~~~~~~~~~~~ + +Writing an image based comparison test is only a little more difficult than the simple testcase presented above. We have adapted matplotlib's image comparison test utility into an easy to use assert method : ``self.assert_images_similar(visualizer)`` + +The main consideration is that you must specify the “baseline”, or expected, image in the ``tests/baseline_images/`` folder structure. + +For example, create your test function located in ``tests/test_regressor/test_myvisualizer.py`` as follows: + +.. code:: python + + from tests.base import VisualTestCase + + class MyVisualizerTests(VisualTestCase): + + def test_my_visualizer_output(self): + visualizer = MyVisualizer() + visualizer.fit(X) + visualizer.poof() + self.assert_images_similar(visualizer) + +The first time this test is run, there will be no baseline image to compare against, so the test will fail. Copy the output images (in this case ``tests/actual_images/test_regressor/test_myvisualizer/test_my_visualizer_output.png``) to the correct subdirectory of baseline_images tree in the source directory (in this case ``tests/baseline_images/test_regressor/test_myvisualizer/test_my_visualizer_output.png``). Put this new file under source code revision control (with git add). When rerunning the tests, they should now pass. + +We also have a helper script, ``tests/images.py`` to clean up and manage baseline images automatically. It is run using the ``python -m`` command to execute a module as main, and it takes as an argument the path to your *test file*. To copy the figures as above:: + + $ python -m tests.images tests/test_regressor/test_myvisualizer.py + +This will move all related test images from ``actual_images`` to ``baseline_images`` on your behalf (note you'll have had to run the tests at least once to generate the images). You can also clean up images from both actual and baseline as follows:: + + $ python -m tests.images -C tests/test_regressor/test_myvisualizer.py + +This is useful particularly if you're stuck trying to get an image comparison to work. For more information on the images helper script, use ``python -m tests.images --help``. + +.. _fixtures: + +Test Fixtures +~~~~~~~~~~~~~ + +Often, you will need a controlled dataset to test your visualizer as specifically as possible. To do this, we recommend that you make use of `pytest fixtures `_ and `scikit-learn's generated datasets `_. Together these tools ensure that you have complete control over your test fixtures and can test different user scenarios as precisely as possible. For example, consider the case where we want to test both a binary and a multiclass dataset for a classification score visualizer. + +.. code:: python + + from tests.fixtures import Dataset, Split + + from sklearn.datasets import make_classification + from sklearn.model_selection import train_test_split as tts + + @pytest.fixture(scope="class") + def binary(request): + """ + Creates a random binary classification dataset fixture + """ + X, y = make_classification( + n_samples=500, + n_features=20, + n_informative=8, + n_redundant=2, + n_classes=2, + n_clusters_per_class=3, + random_state=2001, + ) + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=42) + + dataset = Dataset(Split(X_train, X_test), Split(y_train, y_test)) + request.cls.binary = dataset + +In this example, we make use of :func:`sklearn.datasets.make_classification` to randomly generate exactly the dataset that we'd like, in this case a dataset with 2 classes and enough variability so as to be interesting. Because we're using this with a score visualizer, it is helpful to divide this into train and test splits. The ``Dataset`` and ``Split`` objects in ``tests.fixtures`` are namedtuples that allow you to easily access ``X`` and ``y`` properties on the dataset and ``train`` and ``test`` properties on the split. Creating a dataset this way means we can access ``dataset.X.train`` and ``dataset.y.test`` easily in our test functions. + +Similarly, we can create a custom multiclass function as well: + +.. code:: python + + @pytest.fixture(scope="class") + def multiclass(request): + """ + Creates a random multiclass classification dataset fixture + """ + X, y = make_classification( + n_samples=500, + n_features=20, + n_informative=8, + n_redundant=2, + n_classes=6, + n_clusters_per_class=3, + random_state=87, + ) + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=93) + + dataset = Dataset(Split(X_train, X_test), Split(y_train, y_test)) + request.cls.multiclass = dataset + +.. note:: Fixtures that are added to ``conftest.py`` are available to tests in the same directory or a subdirectory as ``conftest.py``. This is special pytest magic since fixtures are identified by strings. Note that the two above example fixtures are in ``tests/test_classifier/conftest.py`` so you can use these exactly in the ``tests/test_classifier`` directory without having to create new fixtures. + +To use these fixtures with a ``VisualTestCase`` you must decorate the test class with the fixture. Once done, the fixture will be *generated once per class* and stored in the ``request.cls.`` variable. Here's how to use the above fixtures: + +.. code:: python + + @pytest.mark.usefixtures("binary", "multiclass") + class TestMyScoreVisualizer(VisualTestCase): + + def test_binary(self): + oz = MyScoreVisualizer() + assert oz.fit(self.binary.X.train, self.binary.y.train) is oz + assert 0.0 <= oz.score(self.binary.X.test, self.binary.y.test) <= 1.0 + oz.finalize() + + self.assert_images_similar(oz) + +In the above test examples, we showed the use of the yellowbrick dataset loaders, e.g. ``load_occupancy()``. You should feel free to use those datasets and the scikit-learn datasets for tests, particularly for integration tests (described next). The use of the generated datasets and fixtures allows a lot of control over what is being tested and ensures that the tests run as quickly as possible, therefore please use fixtures for the majority of test cases. + +Integration Tests +~~~~~~~~~~~~~~~~~ + +The majority of test cases will use generated test fixtures as described above. But as a visualizer is concluded, it is important to create two "integration tests" that use real-world data in the form of Pandas and numpy arrays from the yellowbrick datasets loaders. These tests often take the following form: + +.. code:: python + + try: + import pandas as pd + except ImportError: + pd = None + + class MyVisualizerTests(VisualTestCase): + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_pandas_integration(self): + """ + Test with Pandas DataFrame and Series input + """ + X, y = load_occupancy(return_datset=True).to_pandas() + oz = MyScoreVisualizer().fit(X, y) + oz.finalize() + self.assert_images_similar(oz) + + def test_numpy_integration(self): + """ + Test with numpy arrays + """ + X, y = load_occupancy(return_datset=True).to_numpy() + oz = MyScoreVisualizer().fit(X, y) + oz.finalize() + self.assert_images_similar(oz) + +These tests often offer the most complications with your visual test cases, so be sure to reserve them for the last tests you create! + +.. _documentation: + +Documentation +------------- + +Yellowbrick uses `Sphinx `_ to build our documentation. The advantages of using Sphinx are many; we can more directly link to the documentation and source code of other projects like Matplotlib and scikit-learn using `intersphinx `_. In addition, docstrings used to describe Yellowbrick visualizers can be automatically included when the documentation is built via `autodoc `_. + +To take advantage of these features, our documentation must be written in reStructuredText (or "rst"). reStructuredText is similar to markdown, but not identical, and does take some getting used to. For instance, styling for things like codeblocks, external hyperlinks, internal cross references, notes, and fixed-width text are all unique in rst. + +If you would like to contribute to our documentation and do not have prior experience with rst, we recommend you make use of these resources: + +- `A reStructuredText Primer `_ +- `rst notes and cheatsheet `_ +- `Using the plot directive `_ + +Docstrings +~~~~~~~~~~ + +The initial documentation for your visualizer will be a well structured docstring. Yellowbrick uses Sphinx to build documentation, therefore docstrings should be written in reStructuredText in numpydoc format (similar to scikit-learn). The primary location of your docstring should be right under the class definition, here is an example: + +.. code:: python + + class MyVisualizer(Visualizer): + """Short description of MyVisualizer + + This initial section should describe the visualizer and what + it's about, including how to use it. Take as many paragraphs + as needed to get as much detail as possible. + + In the next section describe the parameters to __init__. + + Parameters + ---------- + model : a scikit-learn regressor + Should be an instance of a regressor, and specifically one whose name + ends with "CV" otherwise a will raise a YellowbrickTypeError exception + on instantiation. To use non-CV regressors see: + ``ManualAlphaSelection``. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes + will be used (or generated if required). + + kwargs : dict + Keyword arguments that are passed to the base class and may influence + the visualization as defined in other Visualizers. + + Attributes + ---------- + score_ : float + The coefficient of determination that is learned during the visual + diagnostic, saved for reference after the image has been created. + + Examples + -------- + >>> model = MyVisualizer() + >>> model.fit(X) + >>> model.poof() + + Notes + ----- + In the notes section specify any gotchas or other info. + """ + +When your visualizer is added to the API section of the documentation, this docstring will be rendered in HTML to show the various options and functionality of your visualizer! + +API Documentation Page +~~~~~~~~~~~~~~~~~~~~~~ + +To add the visualizer to the documentation it needs to be added to the ``docs/api`` folder in the correct subdirectory. For example if your visualizer is a model score visualizer related to regression it would go in the ``docs/api/regressor`` subdirectory. Add your file named after your module, e.g. ``docs/api/regressor/mymodule.rst``. If you have a question where your documentation should be located, please ask the maintainers via your pull request, we'd be happy to help! + +There are quite a few examples in the documentation on which you can base your files of similar types. The primary format for the API section is as follows: + +.. code:: rst + + .. -*- mode: rst -*- + + My Visualizer + ============= + + A brief introduction to my visualizer and how it is useful in the machine learning process. + + .. plot:: + :context: close-figs + :include-source: False + :alt: Example using MyVisualizer + + visualizer = MyVisualizer(LinearRegression()) + + visualizer.fit(X, y) + g = visualizer.poof() + + Discussion about my visualizer and some interpretation of the above plot. + + + API Reference + ------------- + + .. automodule:: yellowbrick.regressor.mymodule + :members: MyVisualizer + :undoc-members: + :show-inheritance: + +This is a pretty good structure for a documentation page; a brief introduction followed by a code example with a visualization included using `the plot directive `_. This will render the ``MyVisualizer`` image in the document along with links for the complete source code, the png, and the pdf versions of the image. It will also have the "alt-text" (for screen-readers) and will not display the source because of the ``:include-source:`` option. If ``:include-source:`` is omitted, the source will be included by default. + +The primary section is wrapped up with a discussion about how to interpret the visualizer and use it in practice. Finally the ``API Reference`` section will use ``automodule`` to include the documentation from your docstring. + +At this point there are several places where you can list your visualizer, but to ensure it is included in the documentation it *must be listed in the TOC of the local index*. Find the ``index.rst`` file in your subdirectory and add your rst file (without the ``.rst`` extension) to the ``..toctree::`` directive. This will ensure the documentation is included when it is built. + +Building the Docs +~~~~~~~~~~~~~~~~~ + +Speaking of, you can build your documentation by changing into the ``docs`` directory and running ``make html``, the documentation will be built and rendered in the ``_build/html`` directory. You can view it by opening ``_build/html/index.html`` then navigating to your documentation in the browser. + +There are several other places that you can list your visualizer including: + + - ``docs/index.rst`` for a high level overview of our visualizers + - ``DESCRIPTION.rst`` for inclusion on PyPI + - ``README.md`` for inclusion on GitHub + +Please ask for the maintainer's advice about how to include your visualizer in these pages. + + +Generating the Gallery +~~~~~~~~~~~~~~~~~~~~~~ + +In v1.0, we have adopted Matplotlib's `plot directive `_ which means that the majority of the images generated for the documentation are generated automatically. One exception is the gallery; the images for the gallery must still be generated manually. + +If you have contributed a new visualizer as described in the above section, please also add it to the gallery, both to docs/gallery.py and to docs/gallery.rst. (Make sure you have already installed Yellowbrick in editable mode, from the top level directory: pip install -e .) + +If you want to regenerate a single image (e.g. the elbow curve plot), you can do so as follows: :: + + $ python docs/gallery.py elbow + +If you want to regenerate them all (note: this takes a long time!) :: + + $ python docs/gallery.py all diff --git a/docs/contributing/getting_started.rst b/docs/contributing/getting_started.rst new file mode 100644 index 000000000..27a59369d --- /dev/null +++ b/docs/contributing/getting_started.rst @@ -0,0 +1,147 @@ +.. -*- mode: rst -*- + +Getting Started on GitHub +========================= + +Yellowbrick is hosted on GitHub at https://github.com/DistrictDataLabs/yellowbrick. + +The typical workflow for a contributor to the codebase is as follows: + +1. **Discover** a bug or a feature by using Yellowbrick. +2. **Discuss** with the core contributors by `adding an issue `_. +3. **Fork** the repository into your own GitHub account. +4. Create a **Pull Request** first thing to `connect with us `_ about your task. +5. **Code** the feature, write the tests and documentation, add your contribution. +6. **Review** the code with core contributors who will guide you to a high quality submission. +7. **Merge** your contribution into the Yellowbrick codebase. + +We believe that *contribution is collaboration* and therefore emphasize *communication* throughout the open source process. We rely heavily on GitHub's social coding tools to allow us to do this. For instance, we use GitHub's `milestone `_ feature to focus our development efforts for each Yellowbrick semester, so be sure to check out the issues associated with our `current milestone `_! + +Once you have a good sense of how you are going to implement the new feature (or fix the bug!), you can reach out for feedback from the maintainers by creating a `pull request `_. Ideally, any pull request should be capable of resolution within 6 weeks of being opened. This timeline helps to keep our pull request queue small and allows Yellowbrick to maintain a robust release schedule to give our users the best experience possible. However, the most important thing is to keep the dialogue going! And if you're unsure whether you can complete your idea within 6 weeks, you should still go ahead and open a PR and we will be happy to help you scope it down as needed. + +If we have comments or questions when we evaluate your pull request and receive no response, we will also close the PR after this period of time. Please know that this does not mean we don't value your contribution, just that things go stale. If in the future you want to pick it back up, feel free to address our original feedback and to reference the original PR in a new pull request. + +.. note:: Please note that if we feel your solution has not been thought out in earnest, or if the PR is not aligned with our `current milestone `_ goals, we may reach out to ask that you close the PR so that we can prioritize reviewing the most critical feature requests and bug fixes. + +Forking the Repository +---------------------- + +The first step is to fork the repository into your own account. This will create a copy of the codebase that you can edit and write to. Do so by clicking the **"fork"** button in the upper right corner of the Yellowbrick GitHub page. + +Once forked, use the following steps to get your development environment set up on your computer: + +1. Clone the repository. + + After clicking the fork button, you should be redirected to the GitHub page of the repository in your user account. You can then clone a copy of the code to your local machine.:: + + $ git clone https://github.com/[YOURUSERNAME]/yellowbrick + $ cd yellowbrick + +2. Create a virtual environment. + + Yellowbrick developers typically use `virtualenv `_ (and `virtualenvwrapper `_), `pyenv `_ or `conda envs `_ in order to manage their Python version and dependencies. Using the virtual environment tool of your choice, create one for Yellowbrick. Here's how with virtualenv:: + + $ virtualenv venv + + To develop with a conda environment, the conda-forge channel is needed to install some testing dependencies. The following command adds the channel with the highest priority:: + + $ conda config --add channels conda-forge + +3. Install dependencies. + + Yellowbrick's dependencies are in the ``requirements.txt`` document at the root of the repository. Open this file and uncomment any dependencies marked as for development only. Then install the package in editable mode:: + + $ pip install -e . + + This will add Yellowbrick to your PYTHONPATH so that you don't need to reinstall it each time you make a change during development. + + Note that there may be other dependencies required for development and testing; you can simply install them with ``pip``. For example to install + the additional dependencies for building the documentation or to run the + test suite, use the ``requirements.txt`` files in those directories:: + + $ pip install -r tests/requirements.txt + $ pip install -r docs/requirements.txt + +4. Switch to the develop branch. + + The Yellowbrick repository has a ``develop`` branch that is the primary working branch for contributions. It is probably already the branch you're on, but you can make sure and switch to it as follows:: + + $ git fetch + $ git checkout develop + +At this point you're ready to get started writing code. + +Branching Convention +-------------------- + +The Yellowbrick repository is set up in a typical production/release/development cycle as described in "`A Successful Git Branching Model `_." The primary working branch is the ``develop`` branch. This should be the branch that you are working on and from, since this has all the latest code. The ``master`` branch contains the latest stable version and release_, which is pushed to PyPI_. No one but core contributors will generally push to master. + +You should work directly in your fork. In order to reduce the number of merges (and merge conflicts) we kindly request that you utilize a feature branch off of ``develop`` to work in:: + + $ git checkout -b feature-myfeature develop + +We also recommend setting up an ``upstream`` remote so that you can easily pull the latest development changes from the main Yellowbrick repository (see `configuring a remote for a fork `_). You can do that as follows:: + + $ git remote add upstream https://github.com/DistrictDataLabs/yellowbrick.git + $ git remote -v + origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch) + origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push) + upstream https://github.com/DistrictDataLabs/yellowbrick.git (fetch) + upstream https://github.com/DistrictDataLabs/yellowbrick.git (push) + +When you're ready, request a code review for your pull request. + +Pull Requests +------------- + +A `pull request (PR) `_ is a GitHub tool for initiating an exchange of code and creating a communication channel for Yellowbrick maintainers to discuss your contribution. In essenence, you are requesting that the maintainers merge code from your forked repository into the develop branch of the primary Yellowbrick repository. Once completed, your code will be part of Yellowbrick! + +When starting a Yellowbrick contribution, *open the pull request as soon as possible*. We use your PR issue page to discuss your intentions and to give guidance and direction. Every time you push a commit into your forked repository, the commit is automatically included with your pull request, therefore we can review as you code. The earlier you open a PR, the more easily we can incorporate your updates, we'd hate for you to do a ton of work only to discover someone else already did it or that you went in the wrong direction and need to refactor. + +.. note:: For a great example of a pull request for a new feature visualizer, check out `this one `_ by `Carlo Morales `_. + +Opening a Pull Request +~~~~~~~~~~~~~~~~~~~~~~ + +When you open a pull request, ensure it is from your forked repository to the develop branch of `github.com/districtdatalabs/yellowbrick `_; we will not merge a PR into the master branch. Title your Pull Request so that it is easy to understand what you're working on at a glance. Also be sure to include a reference to the issue that you're working on so that correct references are set up. + +.. note:: All pull requests should be into the ``yellowbrick/develop`` branch from your forked repository. + +After you open a PR, you should get a message from one of the maintainers. Use that time to discuss your idea and where best to implement your work. Feel free to go back and forth as you are developing with questions in the comment thread of the PR. Once you are ready, please ensure that you explicitly ping the maintainer to do a code review. Before code review, your PR should contain the following: + +1. Your code contribution +2. Tests for your contribution +3. Documentation for your contribution +4. A PR comment describing the changes you made and how to use them +5. A PR comment that includes an image/example of your visualizer + +At this point your code will be formally reviewed by one of the contributors. We use GitHub's code review tool, starting a new code review and adding comments to specific lines of code as well as general global comments. Please respond to the comments promptly, and don't be afraid to ask for help implementing any requested changes! You may have to go back and forth a couple of times to complete the code review. + +When the following is true: + +1. Code is reviewed by at least one maintainer +2. Continuous Integration tests have passed +3. Code coverage and quality have not decreased +4. Code is up to date with the yellowbrick develop branch + +Then we will "Squash and Merge" your contribution, combining all of your commits into a single commit and merging it into the ``develop`` branch of Yellowbrick. Congratulations! Once your contribution has been merged into master, you will be officially listed as a contributor. + +After Your Pull Request is Merged +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After your pull request is merged, you should update your local fork, either by pulling from ``upstream`` ``develop``:: + + $ git checkout develop + $ git pull upstream develop + $ git push origin develop + +or by manually merging your feature into your fork's ``develop`` branch.:: + + $ git checkout develop + $ git merge --no-ff feature-myfeature + $ git push origin develop + +Then you can safely delete the old feature branch, both locally and on GitHub. Now head back to `the backlog `_ and checkout another issue! + +.. _release: https://github.com/DistrictDataLabs/yellowbrick/releases +.. _PyPI: https://pypi.python.org/pypi/yellowbrick \ No newline at end of file diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst new file mode 100644 index 000000000..d132dfb23 --- /dev/null +++ b/docs/contributing/index.rst @@ -0,0 +1,39 @@ +.. -*- mode: rst -*- + +Contributing +============ + +Yellowbrick is an open source project that is supported by a community who will gratefully and humbly accept any contributions you might make to the project. Large or small, any contribution makes a big difference; and if you've never contributed to an open source project before, we hope you will start with Yellowbrick! + +Principally, Yellowbrick development is about the addition and creation of *visualizers* --- objects that learn from data and create a visual representation of the data or model. Visualizers integrate with scikit-learn estimators, transformers, and pipelines for specific purposes and as a result, can be simple to build and deploy. The most common contribution is a new visualizer for a specific model or model family. We'll discuss in detail how to build visualizers later. + +Beyond creating visualizers, there are many ways to contribute: + +- Submit a bug report or feature request on `GitHub issues`_. +- Contribute an Jupyter notebook to our `examples gallery`_. +- Assist us with :doc:`user testing <../evaluation>`. +- Add to the documentation or help with our website, `scikit-yb.org`_ +- Write unit or integration tests for our project. +- Answer questions on our `GitHub issues`_, `mailing list`_, `Stack Overflow`_, and `Twitter`_. +- Translate our documentation into another language. +- Write a blog post, tweet, or share our project with others. +- Teach someone how to use Yellowbrick. + +As you can see, there are lots of ways to get involved and we would be very happy for you to join us! The only thing we ask is that you abide by the principles of openness, respect, and consideration of others as described in our :doc:`../code_of_conduct`. + +.. note:: If you're unsure where to start, perhaps the best place is to drop the maintainers a note via our mailing list: http://bit.ly/yb-listserv. + +.. _`examples gallery`: https://github.com/DistrictDataLabs/yellowbrick/tree/develop/examples +.. _`scikit-yb.org`: http://www.scikit-yb.org +.. _`GitHub issues`: https://github.com/DistrictDataLabs/yellowbrick/issues +.. _`mailing list`: http://bit.ly/yb-listserv +.. _`Stack Overflow`: https://stackoverflow.com/questions/tagged/yellowbrick +.. _`Twitter`: https://twitter.com/scikit_yb + +.. toctree:: + :caption: Contributing Guide + :maxdepth: 2 + + getting_started + developing_visualizers + advanced_development_topics diff --git a/docs/faq.rst b/docs/faq.rst index 036ad10c3..f8b231278 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -27,7 +27,7 @@ You can change the ``size`` of a plot by passing in the desired dimensions in pi Note: we are considering adding support for passing in ``size`` in inches in a future Yellowbrick release. For a convenient inch-to-pixel converter, check out `www.unitconversion.org `_. How can I change the title of a Yellowbrick plot? ---------------------------------------------------- +------------------------------------------------- You can change the ``title`` of a plot by passing in the desired title as a string on instantiation: @@ -51,23 +51,17 @@ You can change the ``title`` of a plot by passing in the desired title as a stri How can I change the color of a Yellowbrick plot? ------------------------------------------------- -To customize coloring in your plot, use the ``colors`` or ``cmap`` (or ``colormap``) arguments. Note that different visualizers may require slightly different arguments depending on how they construct the plots. - -For instance, the :doc:`api/features/manifold` accepts a ``colors`` argument, for which ``discrete`` targets should be the name of one of the :doc:`api/palettes` or a list of `matplotlib colors `_ represented as strings: -For instance, the :doc:`api/features/manifold` accepts a ``colors`` argument, for which ``discrete`` targets should be the name of a palette from the Yellowbrick :doc:`api/palettes` or a list of `matplotlib colors `_ represented as strings: - -.. code:: python +Yellowbrick uses colors to make visualzers as interpretable as possible for intuitive machine learning diagnostics. Generally, color is specified by the target variable, ``y`` that you might pass to an estimator's fit method. Therefore Yellowbrick considers color based on the datatype of the target: - from yellowbrick.features.manifold import Manifold +- **Discrete**: when the target is represented by discrete classes, Yellowbrick uses categorical colors that are easy to discriminate from each other. +- **Continuous**: when the target is represented by continous values, Yellowbrick uses a sequential colormap to show the range of data. - visualizer = Manifold( - manifold="tsne", target="discrete", colors=["teal", "orchid"] - ) +*Most* visualizers therefore accept the ``colors`` and ``colormap`` arguments when they are initialized. Generally speaking, if the target is discrete, specify `colors` as a list of valid matplotlib colors; otherwise if your target is continuous, specify a matplotlib colormap or colormap name. Most Yellowbrick visualizers are smart enough to figure out the colors for each of your data points based on what you pass in; for example if you pass in a colormap for a discrete target, the visualizer will create a list of discrete colors from the sequential colors. - ... +.. note:: Although most visualizers support these arguments, please be sure to check the visualizer as it may have specific color requirements. E.g. the :doc:`ResidualsPlot ` accepts the ``train_color``, ``test_color``, and ``line_color`` to modify its visualization. To see a visualizer's arguments you can use ``help(Visualizer)`` or ``visualizer.get_params()``. -... whereas for ``continuous`` targets, ``colors`` should be a colormap: +For example, the :doc:`Manifold ` can visualize both discrete and sequential targets. In the discrete case, pass a list of `valid color values `_ as follows: .. code:: python @@ -75,55 +69,31 @@ For instance, the :doc:`api/features/manifold` accepts a ``colors`` argument, fo from yellowbrick.features.manifold import Manifold visualizer = Manifold( - manifold="isomap", target="continuous", colors="YlOrRd" + manifold="tsne", target="discrete", colors=["teal", "orchid"] ) ... -Other visualizers accept a ``cmap`` argument: - -.. code:: python - - from sklearn.linear_model import LogisticRegression - from yellowbrick.classifier import ConfusionMatrix - - visualizer = ConfusionMatrix( - LogisticRegression(), cmap="YlGnBu" - ) - - ... +... whereas for ``continuous`` targets, it is better to specify a `matplotlib colormap `_: -Or a ``colormap`` argument: .. code:: python - from yellowbrick.features import ParallelCoordinates + from yellowbrick.features.manifold import Manifold - # Instantiate the visualizer - visualizer = ParallelCoordinates( - classes=classes, features=features, colormap="PRGn" + visualizer = Manifold( + manifold="isomap", target="continuous", colormap="YlOrRd" ) ... -The :doc:`api/regressor/residuals` accepts color argument for the training and test points, ``train_color`` and ``test_color``, respectively: - -.. code:: python - - from yellowbrick.regressor import ResidualsPlot - from sklearn.linear_model import ElasticNet - visualizer = ResidualsPlot( - model=ElasticNet() - train_color=train_color, # color of points model was trained on - test_color=train_color, # color of points model was tested on - line_color=line_color # color of zero-error line - ) +Finally please note that you can manipulate the default colors that Yellowbrick uses by modifying the `matplotlib styles `_, particularly the default color cycle. Yellowbrick also has some tools for style management, please see :doc:`api/palettes` for more information. How can I save a Yellowbrick plot? ------------------------------------ +---------------------------------- Save your Yellowbrick plot by passing an ``outpath`` into ``poof()``: @@ -143,7 +113,7 @@ Most backends support png, pdf, ps, eps and svg to save your work! How can I make overlapping points show up better? ----------------------------------------------------- +------------------------------------------------- You can use the ``alpha`` param to change the opacity of plotted points (where ``alpha=1`` is complete opacity, and ``alpha=0`` is complete transparency): @@ -157,6 +127,6 @@ You can use the ``alpha`` param to change the opacity of plotted points (where ` How can I access the sample datasets used in the examples? ---------------------------------------------------------------- +---------------------------------------------------------- -Visit the :doc:`api/datasets` page. +Visit the :doc:`api/datasets/index` page. diff --git a/docs/gallery.py b/docs/gallery.py new file mode 100644 index 000000000..ff7b185a8 --- /dev/null +++ b/docs/gallery.py @@ -0,0 +1,649 @@ +#!/usr/bin/env python3 +# Generates images for the gallery + +import os +import argparse +import numpy as np +import os.path as path +import matplotlib.pyplot as plt + +from yellowbrick.datasets import load_occupancy, load_credit, load_concrete +from yellowbrick.datasets import load_spam, load_game, load_energy, load_hobbies + +from yellowbrick.model_selection import RFECV, FeatureImportances + +from yellowbrick.features import PCA, Manifold, JointPlot +from yellowbrick.features import RadViz, Rank1D, Rank2D, ParallelCoordinates + +from yellowbrick.contrib.scatter import ScatterVisualizer + +from yellowbrick.regressor import ResidualsPlot, PredictionError, AlphaSelection + +from yellowbrick.classifier import DiscriminationThreshold +from yellowbrick.classifier import ClassificationReport, ConfusionMatrix +from yellowbrick.classifier import ROCAUC, PRCurve, ClassPredictionError + +from yellowbrick.cluster import KElbowVisualizer, SilhouetteVisualizer +from yellowbrick.cluster import InterclusterDistance + +from yellowbrick.model_selection import ValidationCurve, LearningCurve, CVScores +from yellowbrick.contrib.classifier import DecisionViz + +from yellowbrick.text import ( + FreqDistVisualizer, + TSNEVisualizer, + DispersionPlot, + PosTagVisualizer, +) + +from yellowbrick.target import ( + BalancedBinningReference, + ClassBalance, + FeatureCorrelation, +) + +from sklearn.tree import DecisionTreeRegressor +from sklearn.preprocessing import StandardScaler +from sklearn.linear_model import RidgeClassifier +from sklearn.neighbors import KNeighborsClassifier +from sklearn.cluster import KMeans, MiniBatchKMeans +from sklearn.linear_model import LogisticRegression +from sklearn.ensemble import RandomForestClassifier +from sklearn.naive_bayes import GaussianNB, MultinomialNB +from sklearn.model_selection import train_test_split as tts +from sklearn.preprocessing import OrdinalEncoder, LabelEncoder +from sklearn.linear_model import Ridge, Lasso, LassoCV, RidgeCV +from sklearn.datasets import load_iris, load_digits, load_diabetes +from sklearn.datasets import make_classification, make_blobs, make_moons +from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer + + +GALLERY = path.join(path.dirname(__file__), "images", "gallery") + + +########################################################################## +## Helper Methods +########################################################################## + + +def newfig(): + """ + Helper function to create an axes object of the gallery dimensions. + """ + # NOTE: this figsize generates a better thumbnail + _, ax = plt.subplots(figsize=(8, 4)) + return ax + + +def savefig(viz, name, gallery=GALLERY): + """ + Saves the figure to the gallery directory + """ + if not path.exists(gallery): + os.makedirs(gallery) + + # Must save as png + if len(name.split(".")) > 1: + raise ValueError("name should not specify extension") + + outpath = path.join(gallery, name + ".png") + viz.poof(outpath=outpath) + print("created {}".format(outpath)) + + +########################################################################## +## Feature Analysis +########################################################################## + + +def radviz(): + X, y = load_occupancy() + oz = RadViz(ax=newfig()) + oz.fit_transform(X, y) + savefig(oz, "radviz") + + +def rank1d(): + X, y = load_credit() + oz = Rank1D(algorithm="shapiro", ax=newfig()) + oz.fit_transform(X, y) + savefig(oz, "rank1d_shapiro") + + +def rank2d(): + X, y = load_credit() + oz = Rank2D(algorithm="covariance", ax=newfig()) + oz.fit_transform(X, y) + savefig(oz, "rank2d_covariance") + + +def pcoords(): + X, y = load_occupancy() + oz = ParallelCoordinates(sample=0.05, shuffle=True, ax=newfig()) + oz.fit_transform(X, y) + savefig(oz, "parallel_coordinates") + + +def pca(): + X, y = load_credit() + colors = np.array(["r" if yi else "b" for yi in y]) + oz = PCA(scale=True, color=colors, proj_dim=3) + oz.fit_transform(X, y) + savefig(oz, "pca_projection_3d") + + +def manifold(dataset, manifold): + if dataset == "concrete": + X, y = load_concrete() + elif dataset == "occupancy": + X, y = load_occupancy() + else: + raise ValueError("unknown dataset") + + oz = Manifold(manifold=manifold, ax=newfig()) + oz.fit_transform(X, y) + savefig(oz, "{}_{}_manifold".format(dataset, manifold)) + + +def scatter(): + X, y = load_occupancy() + oz = ScatterVisualizer(x="light", y="CO2", ax=newfig()) + oz.fit_transform(X, y) + savefig(oz, "scatter") + + +def jointplot(): + X, y = load_concrete() + oz = JointPlot(columns=["cement", "splast"], ax=newfig()) + oz.fit_transform(X, y) + savefig(oz, "jointplot") + + +########################################################################## +## Regression +########################################################################## + + +def residuals(): + X, y = load_concrete() + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2) + oz = ResidualsPlot(Ridge(), ax=newfig()) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + savefig(oz, "residuals") + + +def peplot(): + X, y = load_concrete() + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2) + oz = PredictionError(Lasso(), ax=newfig()) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + savefig(oz, "prediction_error") + + +def alphas(): + X, y = load_concrete() + alphas = np.logspace(-10, 1, 400) + oz = AlphaSelection(LassoCV(alphas=alphas), ax=newfig()) + oz.fit(X, y) + savefig(oz, "alpha_selection") + + +########################################################################## +## Classification +########################################################################## + + +def classreport(): + X, y = load_occupancy() + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2) + oz = ClassificationReport(GaussianNB(), support=True, ax=newfig()) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + savefig(oz, "classification_report") + + +def confusion(dataset): + if dataset == "iris": + data = load_iris() + elif dataset == "digits": + data = load_digits() + else: + raise ValueError("uknown dataset") + + X_train, X_test, y_train, y_test = tts(data.data, data.target, test_size=0.2) + oz = ConfusionMatrix(LogisticRegression(), ax=newfig()) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + savefig(oz, "confusion_matrix_{}".format(dataset)) + + +def rocauc(dataset): + if dataset == "binary": + X, y = load_occupancy() + model = GaussianNB() + elif dataset == "multiclass": + X, y = load_game() + X = OrdinalEncoder().fit_transform(X) + model = RidgeClassifier() + else: + raise ValueError("uknown dataset") + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2) + oz = ROCAUC(model, ax=newfig()) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + savefig(oz, "rocauc_{}".format(dataset)) + + +def prcurve(dataset): + if dataset == "binary": + X, y = load_spam() + model = RidgeClassifier() + kws = {} + elif dataset == "multiclass": + X, y = load_game() + X = OrdinalEncoder().fit_transform(X) + y = LabelEncoder().fit_transform(y) + model = MultinomialNB() + kws = { + "per_class": True, + "iso_f1_curves": True, + "fill_area": False, + "micro": False, + } + else: + raise ValueError("uknown dataset") + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, shuffle=True) + oz = PRCurve(model, ax=newfig(), **kws) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + savefig(oz, "precision_recall_{}".format(dataset)) + + +def classprede(): + X, y = make_classification( + n_samples=1000, n_classes=5, n_informative=3, n_clusters_per_class=1 + ) + + classes = ["apple", "kiwi", "pear", "banana", "orange"] + + # Perform 80/20 training/test split + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.20) + oz = ClassPredictionError(RandomForestClassifier(), classes=classes, ax=newfig()) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + savefig(oz, "class_prediction_error") + + +def discrimination(): + X, y = load_spam() + oz = DiscriminationThreshold(LogisticRegression(solver="lbfgs"), ax=newfig()) + oz.fit(X, y) + savefig(oz, "discrimination_threshold") + + +########################################################################## +## Clustering +########################################################################## + + +def elbow(): + X, _ = make_blobs(centers=8, n_features=12, shuffle=True) + oz = KElbowVisualizer(KMeans(), k=(4, 12), ax=newfig()) + oz.fit(X) + savefig(oz, "elbow") + + +def silhouette(): + X, _ = make_blobs(centers=8) + oz = SilhouetteVisualizer(MiniBatchKMeans(6), ax=newfig()) + oz.fit(X) + savefig(oz, "silhouette") + + +def icdm(): + X, _ = make_blobs(centers=12, n_samples=1000, n_features=16, shuffle=True) + oz = InterclusterDistance(KMeans(9), ax=newfig()) + oz.fit(X) + savefig(oz, "icdm") + + +########################################################################## +## Model Selection +########################################################################## + + +def validation(): + X, y = load_energy() + oz = ValidationCurve( + DecisionTreeRegressor(), + param_name="max_depth", + param_range=np.arange(1, 11), + cv=10, + scoring="r2", + ax=newfig(), + ) + oz.fit(X, y) + savefig(oz, "validation_curve") + + +def learning(): + X, y = load_energy() + sizes = np.linspace(0.3, 1.0, 10) + oz = LearningCurve(RidgeCV(), train_sizes=sizes, scoring="r2", ax=newfig()) + oz.fit(X, y) + savefig(oz, "learning_curve") + + +def cvscores(): + X, y = load_energy() + oz = CVScores(Ridge(), scoring="r2", cv=10, ax=newfig()) + oz.fit(X, y) + savefig(oz, "cv_scores") + + +def decision(): + X, y = make_moons(noise=0.3) + X = StandardScaler().fit_transform(X) + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.20) + + oz = DecisionViz(KNeighborsClassifier(3), ax=newfig()) + oz.fit(X_train, y_train) + oz.draw(X_test, y_test) + savefig(oz, "decision_boundaries") + + +def importances(): + X, y = load_occupancy() + oz = FeatureImportances(RandomForestClassifier(), ax=newfig()) + oz.fit(X, y) + savefig(oz, "feature_importances") + + +def rfecv(): + X, y = load_credit() + model = RandomForestClassifier(n_estimators=10) + oz = RFECV(model, cv=3, scoring="f1_weighted", ax=newfig()) + oz.fit(X, y) + savefig(oz, "rfecv_sklearn_example") + + +########################################################################## +## Text Model Diagnostics +########################################################################## + + +def freqdist(): + corpus = load_hobbies() + vecs = CountVectorizer() + docs = vecs.fit_transform(corpus.data) + + oz = FreqDistVisualizer(features=vecs.get_feature_names(), ax=newfig()) + oz.fit(docs) + savefig(oz, "freqdist") + + +def tsne(): + corpus = load_hobbies() + docs = TfidfVectorizer().fit_transform(corpus.data) + + oz = TSNEVisualizer(ax=newfig()) + oz.fit(docs, corpus.target) + savefig(oz, "corpus_tsne") + + +def dispersion(): + corpus = load_hobbies() + target_words = ["Game", "player", "score", "oil", "Man"] + + oz = DispersionPlot(target_words, ax=newfig()) + oz.fit([doc.split() for doc in corpus.data]) + savefig(oz, "dispersion") + + +def postag(): + tagged_stanzas = [ + [ + [ + ("Whose", "JJ"), + ("woods", "NNS"), + ("these", "DT"), + ("are", "VBP"), + ("I", "PRP"), + ("think", "VBP"), + ("I", "PRP"), + ("know", "VBP"), + (".", "."), + ], + [ + ("His", "PRP$"), + ("house", "NN"), + ("is", "VBZ"), + ("in", "IN"), + ("the", "DT"), + ("village", "NN"), + ("though", "IN"), + (";", ":"), + ("He", "PRP"), + ("will", "MD"), + ("not", "RB"), + ("see", "VB"), + ("me", "PRP"), + ("stopping", "VBG"), + ("here", "RB"), + ("To", "TO"), + ("watch", "VB"), + ("his", "PRP$"), + ("woods", "NNS"), + ("fill", "VB"), + ("up", "RP"), + ("with", "IN"), + ("snow", "NNS"), + (".", "."), + ], + ], + [ + [ + ("My", "PRP$"), + ("little", "JJ"), + ("horse", "NN"), + ("must", "MD"), + ("think", "VB"), + ("it", "PRP"), + ("queer", "JJR"), + ("To", "TO"), + ("stop", "VB"), + ("without", "IN"), + ("a", "DT"), + ("farmhouse", "NN"), + ("near", "IN"), + ("Between", "NNP"), + ("the", "DT"), + ("woods", "NNS"), + ("and", "CC"), + ("frozen", "JJ"), + ("lake", "VB"), + ("The", "DT"), + ("darkest", "JJS"), + ("evening", "NN"), + ("of", "IN"), + ("the", "DT"), + ("year", "NN"), + (".", "."), + ] + ], + [ + [ + ("He", "PRP"), + ("gives", "VBZ"), + ("his", "PRP$"), + ("harness", "NN"), + ("bells", "VBZ"), + ("a", "DT"), + ("shake", "NN"), + ("To", "TO"), + ("ask", "VB"), + ("if", "IN"), + ("there", "EX"), + ("is", "VBZ"), + ("some", "DT"), + ("mistake", "NN"), + (".", "."), + ], + [ + ("The", "DT"), + ("only", "JJ"), + ("other", "JJ"), + ("sound", "NN"), + ("’", "NNP"), + ("s", "VBZ"), + ("the", "DT"), + ("sweep", "NN"), + ("Of", "IN"), + ("easy", "JJ"), + ("wind", "NN"), + ("and", "CC"), + ("downy", "JJ"), + ("flake", "NN"), + (".", "."), + ], + ], + [ + [ + ("The", "DT"), + ("woods", "NNS"), + ("are", "VBP"), + ("lovely", "RB"), + (",", ","), + ("dark", "JJ"), + ("and", "CC"), + ("deep", "JJ"), + (",", ","), + ("But", "CC"), + ("I", "PRP"), + ("have", "VBP"), + ("promises", "NNS"), + ("to", "TO"), + ("keep", "VB"), + (",", ","), + ("And", "CC"), + ("miles", "NNS"), + ("to", "TO"), + ("go", "VB"), + ("before", "IN"), + ("I", "PRP"), + ("sleep", "VBP"), + (",", ","), + ("And", "CC"), + ("miles", "NNS"), + ("to", "TO"), + ("go", "VB"), + ("before", "IN"), + ("I", "PRP"), + ("sleep", "VBP"), + (".", "."), + ] + ], + ] + oz = PosTagVisualizer(ax=newfig()) + oz.fit(tagged_stanzas) + savefig(oz, "postag") + + +########################################################################## +## Target Visualizations +########################################################################## + + +def binning(): + _, y = load_concrete() + oz = BalancedBinningReference(ax=newfig()) + oz.fit(y) + savefig(oz, "balanced_binning_reference") + + +def balance(): + X, y = load_occupancy() + _, _, y_train, y_test = tts(X, y, test_size=0.2) + + oz = ClassBalance(ax=newfig(), labels=["unoccupied", "occupied"]) + oz.fit(y_train, y_test) + savefig(oz, "class_balance") + + +def featcorr(): + data = load_diabetes() + + oz = FeatureCorrelation(ax=newfig()) + oz.fit(data.data, data.target) + savefig(oz, "feature_correlation") + + +########################################################################## +## Main Method +########################################################################## + +if __name__ == "__main__": + plots = { + "all": None, + "radviz": radviz, + "rank1d": rank1d, + "rank2d": rank2d, + "pcoords": pcoords, + "pca": pca, + "concrete_tsne": lambda: manifold("concrete", "tsne"), + "occupancy_tsne": lambda: manifold("occupancy", "tsne"), + "concrete_isomap": lambda: manifold("concrete", "isomap"), + "importances": importances, + "rfecv": rfecv, + "scatter": scatter, + "jointplot": jointplot, + "residuals": residuals, + "peplot": peplot, + "alphas": alphas, + "classreport": classreport, + "confusion_digits": lambda: confusion("digits"), + "confusion_iris": lambda: confusion("iris"), + "rocauc_binary": lambda: rocauc("binary"), + "rocauc_multi": lambda: rocauc("multiclass"), + "prcurve_binary": lambda: prcurve("binary"), + "prcurve_multi": lambda: prcurve("multiclass"), + "classprede": classprede, + "discrimination": discrimination, + "elbow": elbow, + "silhouette": silhouette, + "icdm": icdm, + "validation": validation, + "learning": learning, + "cvscores": cvscores, + "freqdist": freqdist, + "tsne": tsne, + "dispersion": dispersion, + "postag": postag, + "decision": decision, + "binning": binning, + "balance": balance, + "featcorr": featcorr, + } + + parser = argparse.ArgumentParser(description="gallery image generator") + parser.add_argument( + "plots", + nargs="+", + choices=plots.keys(), + metavar="plot", + help="names of images to generate", + ) + args = parser.parse_args() + + queue = frozenset(args.plots) + if "all" in queue: + queue = frozenset(plots.keys()) + + for item in queue: + method = plots[item] + if method is not None: + method() diff --git a/docs/gallery.rst b/docs/gallery.rst index f78f56a4d..ade6cb809 100644 --- a/docs/gallery.rst +++ b/docs/gallery.rst @@ -6,73 +6,61 @@ Gallery Feature Analysis ---------------- -.. image:: api/features/images/radviz.png +.. image:: images/gallery/radviz.png :width: 200px :height: 100px - :alt: Features Analysis adViz Visualizer + :alt: Features Analysis RadViz Visualizer :target: api/features/radviz.html#radviz-visualizer -.. image:: api/features/images/rank1d_shapiro.png +.. image:: images/gallery/rank1d_shapiro.png :width: 200px :height: 100px - :alt: Features Analysis rank1d shapiro\ + :alt: Features Analysis Shapiro Rank1D :target: api/features/rankd.html#rank-1d -.. image:: api/features/images/rank2d_covariance.png +.. image:: images/gallery/rank2d_covariance.png :width: 200px :height: 100px - :alt: Features Analysis rank2d covariance + :alt: Features Analysis Covariance Rank2D :target: api/features/rankd.html#rank-2d -.. image:: api/features/images/parallel_coordinates.png +.. image:: images/gallery/parallel_coordinates.png :width: 200px :height: 100px :alt: Parallel Coordinates for 5 features :target: api/features/pcoords.html#parallel-coordinates -.. image:: api/features/images/pca_projection_3d.png +.. image:: images/gallery/pca_projection_3d.png :width: 200px :height: 100px - :alt: Principal Component Plot + :alt: Principal Component Plot 3D :target: api/features/pca.html#pca-projection -.. image:: api/features/images/concrete_tsne_manifold.png +.. image:: images/gallery/concrete_tsne_manifold.png :width: 200px :height: 100px :alt: t-SNE Manifold Visualization :target: api/features/manifold.html#manifold-visualization -.. image:: api/features/images/occupancy_tsne_manifold.png +.. image:: images/gallery/occupancy_tsne_manifold.png :width: 200px :height: 100px :alt: t-SNE Manifold Visualization Discrete Target :target: api/features/manifold.html#discrete-target -.. image:: api/features/images/concrete_isomap_manifold.png +.. image:: images/gallery/concrete_isomap_manifold.png :width: 200px :height: 100px :alt: Isomap Manifold Visualization :target: api/features/manifold.html#continuous-target -.. image:: api/features/images/feature_importances.png - :width: 200px - :height: 100px - :alt: Feature Importance using Gradient Boosting Classifier - :target: api/features/importances.html#feature-importances - -.. image:: api/features/images/rfecv_sklearn_example.png - :width: 200px - :height: 100px - :alt: Feature Importance using Gradient Boosting Classifier - :target: api/features/rfecv.html#recursive-feature-elimination - -.. image:: api/contrib/images/scatter.png +.. image:: images/gallery/scatter.png :width: 200px :height: 100px :alt: Scatter Visualization :target: api/contrib/scatter.html#scatter-visualization -.. image:: api/features/images/jointplot.png +.. image:: images/gallery/jointplot.png :width: 200px :height: 100px :alt: Joint Point Visualization @@ -82,98 +70,98 @@ Feature Analysis Regression Visualizers ---------------------- -.. image:: api/regressor/images/residuals.png +.. image:: images/gallery/residuals.png :width: 200px :height: 100px :alt: Residuals for Ridge Model :target: api/regressor/residuals.html#residuals-plot -.. image:: api/regressor/images/prediction_error.png +.. image:: images/gallery/prediction_error.png :width: 200px :height: 100px :alt: Prediction Error for Lasso :target: api/regressor/peplot.html#residuals-plot -.. image:: api/regressor/images/alpha_selection.png +.. image:: images/gallery/alpha_selection.png :width: 200px :height: 100px - :alt: Lasso Alpha Error + :alt: Alpha Selection for Lasso :target: api/regressor/alphas.html#alpha-selection Classification Visualizers -------------------------- -.. image:: api/classifier/images/classification_report.png +.. image:: images/gallery/classification_report.png :width: 200px :height: 100px :alt: GaussianNB Classification Report :target: api/classifier/classification_report.html#classification-report -.. image:: api/classifier/images/confusion_matrix_digits.png +.. image:: images/gallery/confusion_matrix_digits.png :width: 200px :height: 100px :alt: Logistic Regression Confusion Matrix with Numeric Labels :target: api/classifier/confusion_matrix.html#confusion-matrix -.. image:: api/classifier/images/confusion_matrix_iris.png +.. image:: images/gallery/confusion_matrix_iris.png :width: 200px :height: 100px :alt: Logistic Regression Confusion Matrix with Class Name Labels :target: api/classifier/confusion_matrix.html#plotting-with-class-names -.. image:: api/classifier/images/rocauc_binary.png +.. image:: images/gallery/rocauc_binary.png :width: 200px :height: 100px - :alt: Binary ROC Curves for Logistic Regression + :alt: Binary ROC Curves :target: api/classifier/rocauc.html#rocauc -.. image:: api/classifier/images/rocauc_multiclass.png +.. image:: images/gallery/rocauc_multiclass.png :width: 200px :height: 100px :alt: Multiclass ROC Curves :target: api/classifier/rocauc.html#multi-class-rocauc-curves -.. image:: api/classifier/images/binary_precision_recall.png +.. image:: images/gallery/precision_recall_binary.png :width: 200px :height: 100px :alt: Precision-Recall Curves :target: api/classifier/prcurve.html -.. image:: api/classifier/images/multiclass_precision_recall_full.png +.. image:: images/gallery/precision_recall_multiclass.png :width: 200px :height: 100px - :alt: Multi-Label Precision-Recall Curves + :alt: Multiclass Precision-Recall Curves :target: api/classifier/prcurve.html#multi-label-classification -.. image:: api/classifier/images/class_prediction_error.png +.. image:: images/gallery/class_prediction_error.png :width: 200px :height: 100px - :alt: Class Prediction Error for Random Forest Classifier + :alt: Class Prediction Error of Classifier :target: api/classifier/class_prediction_error.html#class-prediction-error -.. image:: api/classifier/images/spam_discrimination_threshold.png +.. image:: images/gallery/discrimination_threshold.png :width: 200px :height: 100px - :alt: Threshold Plot for Logistic Regression + :alt: Discrimination Threshold Plot :target: api/classifier/threshold.html#discrimination-threshold Clustering Visualizers ---------------------- -.. image:: api/cluster/images/elbow.png +.. image:: images/gallery/elbow.png :width: 200px :height: 100px :alt: Distortion Score Elbow for Mini Batch Means Clustering :target: api/cluster/elbow.html#elbow-method -.. image:: api/cluster/images/silhouette.png +.. image:: images/gallery/silhouette.png :width: 200px :height: 100px :alt: Silhoutte Plot of Mini Batch Kmeans Clustering :target: api/cluster/silhouette.html#silhouette-visualizer -.. image:: api/cluster/images/icdm.png +.. image:: images/gallery/icdm.png :width: 200px :height: 100px :alt: Intercluster Distance Maps @@ -182,95 +170,87 @@ Clustering Visualizers Model Selection Visualizers --------------------------- -.. image:: api/model_selection/images/validation_curve_regressor.png +.. image:: images/gallery/validation_curve.png :width: 200px :height: 100px - :alt: Validation Curve for Decision Tree Regresor + :alt: Validation Curve Hyperparameter Tuning :target: api/model_selection/validation_curve.html#validation-curve -.. image:: api/model_selection/images/learning_curve_classifier.png +.. image:: images/gallery/learning_curve.png :width: 200px :height: 100px - :alt: Learning Curve for MultinomialNB - :target: api/model_selection/learning_curve.html#classification + :alt: Learning Curves for Data Sufficiency + :target: api/model_selection/learning_curve.html -.. image:: api/model_selection/images/learning_curve_clusterer.png +.. image:: images/gallery/cv_scores.png :width: 200px :height: 100px - :alt: Learning Curve for KMeans - :target: api/model_selection/learning_curve.html#clustering + :alt: Cross Validation Scores + :target: api/model_selection/cross_validation.html + +.. image:: images/gallery/decision_boundaries.png + :width: 200px + :height: 100px + :alt: Nearest Neighbor Decision Boundary Prototype + :target: api/contrib/boundaries.html#decisionboundaries-vizualizer -.. image:: api/model_selection/images/cv_scores_classifier.png +.. image:: images/gallery/feature_importances.png :width: 200px :height: 100px - :alt: CV Scores for MultinomialNB Classification - :target: api/model_selection/cross_validation.html#classification + :alt: Feature Importance using Ensemble Classifier + :target: api/model_selection/importances.html#feature-importances -.. image:: api/model_selection/images/cv_scores_regressor.png +.. image:: images/gallery/rfecv_sklearn_example.png :width: 200px :height: 100px - :alt: CV Scores for Ridge Regression - :target: api/model_selection/cross_validation.html#regression + :alt: Recursive Feature Elimination with Ensemble Classifier + :target: api/model_selection/rfecv.html#recursive-feature-elimination Text Modeling Visualizers --------------------------- -.. image:: api/text/images/freqdist_corpus.png +.. image:: images/gallery/freqdist.png :width: 200px :height: 100px - :alt: Validation Curve for Decision Tree Regresor + :alt: Token Frequency Distribution :target: api/text/freqdist.html#token-frequency-distribution -.. image:: api/text/images/tsne_all_docs.png +.. image:: images/gallery/corpus_tsne.png :width: 200px :height: 100px :alt: TSNE Projection of Documents :target: api/text/tsne.html#t-sne-corpus-visualization -.. image:: api/text/images/dispersion_docs.png +.. image:: images/gallery/dispersion.png :width: 200px :height: 100px - :alt: Dispersion Plot + :alt: Dispersion of Words in a Corpus :target: api/text/dispersion.html#dispersion-plot -Decision Boundaries Visualizer ------------------------------- - -.. image:: api/contrib/images/knn_decisionviz.png +.. image:: images/gallery/postag.png :width: 200px :height: 100px - :alt: Nearest Neighbor Boundary Visualizer - :target: api/contrib/boundaries.html#decisionboundaries-vizualizer + :alt: Parts-of-Speech in a Tagged Corpus + :target: api/text/postag.html#postag-visualization + Target Visualizers ------------------ -.. image:: api/target/images/balanced_binning_reference.png +.. image:: images/gallery/balanced_binning_reference.png :width: 200px :height: 100px :alt: Balanced Binning Reference :target: api/target/binning.html#balanced-binning-reference -.. image:: api/target/images/class_balance_compare.png +.. image:: images/gallery/class_balance.png :width: 200px :height: 100px :alt: Class Balance :target: api/target/class_balance.html#class-balance -.. image:: api/target/images/feature_correlation_pearson.png +.. image:: images/gallery/feature_correlation.png :width: 200px :height: 100px :alt: Feature Correlation Pearson Correlation Coefficients :target: api/target/feature_correlation.html#pearson-correlation - -.. image:: api/target/images/feature_correlation_mutual_info_regression.png - :width: 200px - :height: 100px - :alt: Feature Correlation Mutual Information - Regression - :target: api/target/feature_correlation.html#mutual-information-regression - -.. image:: api/target/images/feature_correlation_mutual_info_classification.png - :width: 200px - :height: 100px - :alt: Feature Correlation Mutual Information - Classification - :target: api/target/feature_correlation.html#mutual-information-classification diff --git a/docs/governance/index.rst b/docs/governance/index.rst new file mode 100644 index 000000000..d27f18f26 --- /dev/null +++ b/docs/governance/index.rst @@ -0,0 +1,218 @@ +.. -*- mode: rst -*- + +Governance +========== + +**Version 1.1 on May 15, 2019** + +Purpose +------- +Yellowbrick has grown from a project with a handful of people hacking on it in their spare time into one with thousands of users, dozens of contributors, and several organizational affiliations. The burden of the effort to maintain the project has so far been borne by a few individuals dedicated to its success. However, as the project continues to grow, we believe it is important to establish a system that clearly defines how the project will be administered, organized, and maintained in the coming years. We hope that this clarity will lighten the administrative load of the maintainers and core-developers, streamline decision making, and allow new people to participate meaningfully. Most importantly, we hope that the product of these effects is to ensure the members of the Yellowbrick project are able to maintain a healthy and productive relationship with the project, allowing them to avoid burnout and fatigue. + +The purpose of this document is to establish a system of governance that will define the interactions of Yellowbrick members and facilitate decision-making processes. This governance document serves as guidance to Yellowbrick members, and all Yellowbrick members must agree to adhere to its guidance in order to participate in the project. Although not legally binding, this document may also serve as bylaws for the organization of Yellowbrick contributors. We believe this document should be treated as a living document that is amended regularly in order to support the future objectives of the project. + +Organization +------------ +The Yellowbrick project, also referred to as scikit-yellowbrick or scikit-yb, is an open source Python library for machine learning visual analysis and diagnostics. The library is licensed by the Apache 2.0 open source license, the source code is version controlled on GitHub, and the package is distributed via PyPI. The project is maintained by volunteer contributors who refer to themselves corporately as “the scikit-yb developers”. + +Founders and Leadership +~~~~~~~~~~~~~~~~~~~~~~~ +Yellowbrick was founded by Benjamin Bengfort and Rebecca Bilbro, who were solely responsible for the initial prototypes and development of the library. In the tradition of Python, they are sometimes referred to as the “benevolent dictators for life”, or BDFLs, of the project. For the purpose of this document and to emphasize the role of maintainers and core-contributors, they are simply referred to as “the founders”. From a governance perspective, the founders have a special role in that they provide vision, thought leadership, and high-level direction for the project’s members and contributors. + +The Yellowbrick project is bigger than two people, however, therefore the primary mechanism of governance will be a collaboration of contributors and advisors in the tradition of the Apache Software Foundation. This collaboration ensures that the most active project contributors and users (the primary stakeholders of the project) have a voice in decision-making responsibilities. Note that Yellowbrick defines who active project contributors are (e.g. advisors, maintainers, core-contributors, and contributors) in a very specific and time-limited fashion. Depending on the type of required action, a subset of the active project contributors will deliberate and make a decision through majority voting consensus, subject to a final say of the founders. To ensure this happens correctly, contributors must be individuals who represent themselves and not companies. + +Contributors and Roles +~~~~~~~~~~~~~~~~~~~~~~ +There are many ways to participate as a contributor to the Yellowbrick community, but first and foremost, all Yellowbrick contributors are users. Only by using Yellowbrick do contributors gain the requisite understanding and context to meaningfully contribute. Whether you use Yellowbrick as a student to learn machine learning (or as a teacher to teach it) or you are a professional data scientist or analyst who uses it for visual analytics or diagnostics, you cannot be a contributor without some use-case driven motivation. This definition also specifically excludes other motivations, such as financial motivations or attention-seeking. The reason for this is simple, Yellowbrick is a technical library that is intended for a technical audience. + +You become a contributor when you give something back to the Yellowbrick community. A contribution can be anything large or small—related to code or something unique to your talents. In the end, the rest of the community of contributors will decide what constitutes a meaningful contribution, but some of the most common contributions include: + +- Successfully merging a pull request after a code review. +- Contributing to the scikit-yb documentation. +- Translating the scikit-yb documentation into other languages. +- Submitting detailed, reproducible bug reports. +- Submitting detailed, prototyped visualizer or feature requests. +- Writing blog posts or giving talks that demonstrate Yellowbrick’s use. +- Answering questions on Stack Overflow or on our GitHub issues. +- Organizing a Yellowbrick users meetup or other events. + +Once you have contributed to Yellowbrick, you *will always be a contributor*. We believe this is a badge of honor and we hold all Yellowbrick contributors in special esteem and respect. + +If you are routinely or frequently contributing to Yellowbrick, you have the opportunity to assume one of the roles of an active project contributor. These roles allow you to fundamentally guide the course of the Yellowbrick project but are also time-limited to help us avoid burnout and fatigue. We would like to plan our efforts as fixed-length sprints rather than as an open-ended run. + +Note that the roles described below are not mutually exclusive. A Yellowbrick contributor can simultaneously be a core-contributor, a maintainer, and an advisor if they would like to take on all of those responsibilities at the same time. None of these roles “outranks” another, they are simply a delineation of different responsibilities. In fact, they are designed to be fluid so that members can pick and choose how they participate at any given time. A detailed description of the roles follows. + +Core Contributor +^^^^^^^^^^^^^^^^ +A core-contributor commits to actively participate in a 4-month *semester* of Yellowbrick development, which culminates in the next release (we will discuss semesters in greater detail later in this document). At the beginning of the semester, the core-contributor will outline their participation and goals for the release, usually by accepting the assignment of 1-5 issues that should be completed before the semester is over. Core-contributors work with the maintainers over the course of the semester to ensure the library is at the highest quality. The responsibilities of core-contributors are as follows: + +- Set specific and reasonable goals for contributions over the semester. +- Work with maintainers to complete issues and goals for the next release. +- Respond to maintainer requests and be generally available for discussion. +- Participating actively on the #yellowbrick Slack channel. +- Join any co-working or pair-programming sessions required to achieve goals. + +Core-contributors can reasonably set as high or as low a challenge for themselves as they feel comfortable. We expect core-contributors to work on Yellowbrick for a total of about 5 hours per week. If core-contributors do not meet their goals by the end of the semester, they will likely not be invited to participate at that level in the next semester, though they may still volunteer subject to the approval of the maintainers for that semester. + +Maintainer +^^^^^^^^^^ +Maintainers commit to actively manage Yellowbrick during a 4-month semester of Yellowbrick development and are primarily responsible for building and deploying the next release. Maintainers may also act as core-contributors and use the dedicated semester group to ensure the release objectives set by the advisors are met. This primarily manifests itself in the maintenance of GitHub issues and code reviews of Pull Requests. The responsibilities of maintainers are as follows: + +- Being active and responsive on the #yellowbrick and #oz-maintainers Slack channels. +- Being active and responsive on the team-oz/maintainers GitHub group. +- Respond to user messages on GitHub and the ListServ (Stack Overflow, etc). +- Code review pull requests and safely merge them into the project. +- Maintain the project’s high code quality and well-defined API. +- Release the next version of Yellowbrick. +- Find and share things of interest to the Yellowbrick community. + +The maintainer role is an exhausting and time-consuming role. We expect maintainers to work on Yellowbrick 10-20 hours per week. Maintainers should have first been core-contributors so they understand what the role entails (and to allow a current maintainer to mentor them to assume the role). Moreover, we recommend that maintainers periodically/regularly take semesters off to ensure they don’t get burnt out. + +Coordinator +^^^^^^^^^^^ +If a semester has a large enough group of maintainers and core-contributors, we may optionally appoint a contributor as a coordinator. Coordinators specialize in the project management aspects of a version release and also direct traffic to the actively participating group. The coordinator may or may not be a maintainer. If the coordinator is nominated from the maintainers, the coordination role is an auxiliary responsibility. Otherwise, the coordinator may be a dedicated contributor that focuses only on communication and progress. The coordinator’s primary responsibilities are as follows: + +- Ensure that the features, bugs, and issues assigned to the version release are - progressing. +- Be the first to respond welcomingly to new contributors. +- Assign incoming pull requests, issues, and other responses to maintainers. +- Communicate progress to the advisors and to the Yellowbrick community. +- Encourage and motivate the active contributors group. +- Coach core-contributors and maintainers. +- Organize meetups, video calls, and other social and engagement activities. + +The coordinator’s role is mostly about communication and the amount of dedicated effort can vary depending on external factors. Based on our experience it could be as little as a couple of hours per week to as much work as being a maintainer. The coordinator’s responsibilities are organizational and are ideal for someone who wants to be a part of the community but has less of a software background. In fact, we see this role as being similar to a tech project management role, which is often entry level and serves as experience to propel coordinators into more active development. Alternatively, the coordinator can be a more experienced maintainer who needs a break from review but is willing to focus on coaching. + +Mentor-Contributor +^^^^^^^^^^^^^^^^^^ + +A mentor-contributor assumes all the responsibilities of a core contributor but commits 25-50% of their time toward mentoring a new contributor who has little-to-no experience. The purpose of the mentor-contributor role is to assist in recruiting and developing a pipeline of new contributors for Yellowbrick. + +A mentor-contributor would guide a new contributor in understanding the community, roles, ethos, expectations, systems, and processes in participating and contributing to an open source project such as Yellowbrick. A new contributor is someone who is interested in open source software projects but has little-to-no experience working on an open source project or participating in the community. + +The mentor-contributor would work in tandem with the new contributor during the semester to complete assigned issues and prepare the new contributor to be a core contributor in a future semester. A mentor-contributor would mostly likely work on fewer issues during the semester in order to allocate time for mentoring. Mentor-contributors would be matched with new contributors prior to the start of each semester or recruit their own contributor (eg. colleague or friend). The responsibilities of mentor-contributors are as follows: + +- When possible, recruit new contributors to Yellowbrick. +- Schedule weekly mentoring sessions and assign discrete yet increasingly complex tasks to the new contributor. +- Work with the new contributor to complete assigned issues during the semester. +- Set specific and reasonable goals for contributions over the semester. +- Work with maintainers to complete issues and goals for the next release. +- Respond to maintainer requests and be generally available for discussion. +- Participating actively on the #yellowbrick Slack channel. +- Join any co-working or pair-programming sessions required to achieve goals. +- Make a determination at the end of the semester on the readiness of the new contributor to be a core-contributor. + +The mentor-contributor role may also appeal to students who use Yellowbrick in a machine learning course and seek the experience of contributing to an open source project. + +Advisor +^^^^^^^ +Advisors are the primary decision-making body for Yellowbrick. They serve over the course of 3 semesters (1 calendar year) and are broadly responsible for defining the roadmap for Yellowbrick development for the next three releases. Advisors meet regularly, at least at the beginning of every semester, to deliberate and discuss next steps for Yellowbrick and to give guidance for core-contributors and maintainers for the semester. The detailed responsibilities of the advisors are as follows: + +- Contribute dues based on the number of advisors in a year to meet fixed running costs. +- Make decisions that affect the entire group through votes (ensure a quorum). +- Meet at least three times a year at the beginning of each semester (or more if required). +- Elect officers to conduct the business of Yellowbrick. +- Ensure Yellowbrick’s financial responsibilities are met. +- Create a roadmap for Yellowbrick development. +- Approve the release of the next Yellowbrick version. +- Approve core-contributor and maintainer volunteers. +- Ensure Yellowbrick is growing by getting more users. +- Ensure Yellowbrick is a good citizen of the PyData and Python communities. +- Recruit more contributors to participate in the Yellowbrick community. + +The advisor role is primarily intended to allow members to guide the course of Yellowbrick and actively participate in decisions without making them feel that they need to participate as maintainers (seriously, take a break - don’t burn out!). As such, the role of advisors is limited to preparing for and participating in the semester meetings or any other meetings that are called. Therefore, we assume that the time commitment of an advisor is roughly 30-40 hours per year (less than 1 hour per week). + +The board of advisors is open to every contributor, and in fact, is open to all users because joining the board of advisors requires a financial contribution. Yellowbrick has limited fixed running costs every year, for example, $10 a month to Read The Docs and $14.99 for our domain name. When you volunteer to be an advisor for a year, you commit to paying an equal portion of those fixed running costs, based on the number of advisor volunteers. This also ensures that we have a fixed quorum to ensure votes run smoothly. + +Affiliations +~~~~~~~~~~~~ +Yellowbrick may be affiliated with one or more organizations that promote Yellowbrick and provide financial assistance or other support. Yellowbrick affiliations are determined by the advisors who should work to ensure that affiliations are in both the organization’s and Yellowbrick’s best interests. Yellowbrick is currently affiliated with: + +- District Data Labs +- NumFOCUS +- Georgetown University + +The advisors should update this section to reflect all current and past affiliations. + +Operations +---------- +The primary activity of Yellowbrick contributors is to develop the Yellowbrick library into the best tool for visual model diagnostics and machine learning analytics. A secondary activity is to support Yellowbrick users and to provide a high level of customer service. Tertiary activities include being good citizens of the Python and PyData communities and to the open source community at large. These activities are directed to the primary mission of Yellowbrick: to greatly enhance the machine learning workflow with visual steering and diagnostics. + +Although Yellowbrick is not a commercial enterprise, it does not preclude its members from pursuing commercial activities using Yellowbrick subject to our license agreement. + +In this section, we break down the maintenance and development operations of the project. These operations are a separate activity from administration and decision making, which we will discuss in the following section. + +Semesters and Service Terms +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In order to ensure that maintainers are able to contribute meaningfully and effectively without getting burned out, we divided the maintenance and development life cycle into three semesters a year as follows: + +- Spring semester: January - April +- Summer semester: May - August +- Fall semester: September - December + +Every maintainer and core-contributor serves in these terms and is only expected to participate at the commitment level described by the roles section for the term they have joined. At the end of the term, there is no expectation that the maintainer must continue to the next term. If they wish to do so, they must volunteer to participate in the next term. We hope that this allows maintainers to be more strategic in their participation, e.g. participating every other semester, or taking off a semester where they know they will be busy with other obligations. + +An advisor’s service term is 1 year, and they must accept the advisory role by the end of January in that calendar year by paying the dues computed by the number of participating advisors. Advisors can remain advisors so long as they wish to participate by paying the dues, though if advising meetings are unable to achieve quorum; absent advisors may be asked to step down. + +The goal of the advising service term is to allow maintainers who wish to take a semester off to still have a meaningful say in the development of the project. We hope that this will also facilitate maintainers feeling that they can take a break without being cut out of the loop, and allowing them to rejoin the project as maintainers or core-contributors in a meaningful way when they are ready again. + +Release Guidelines +~~~~~~~~~~~~~~~~~~ +The focus of the semester is to release the next version of Yellowbrick. The advisors set a roadmap based on the current issues and features they wish to address. Core-contributors volunteer to take on specific issues and maintainers are responsible for organizing themselves and the core-contributors to complete the work in the version, releasing it at the end of the semester. + +Maintainers may also include in the semester release any other contributions or pull requests made by members of the community at their discretion. Maintainers should address all pull-requests and opened issues (as well as emails on the listserv and questions on Stack Overflow, Twitter, etc.) - facilitating their inclusion in the release, or their closure if they become stale or are out of band. + +During the middle of a semester, maintainers may be required to issue a hotfix release for time-critical or security-related fixes. Hotfix releases should be small, incremental improvements on the previous release. We hope that the expected release schedule of April, August, and December also assists the user community in giving feedback and guidance about the direction of Yellowbrick. + +Advisors +-------- +Yellowbrick advisors are contributors who take on the responsibility of setting the vision and direction for the development of the project. They may, for example, make decisions about which features are focused on during a semester or to apply for a small development grant and use the funds to improve Yellowbrick. They may also ask to affiliate with other projects and programs that are tangential to but crucial to the success of Yellowbrick - e.g. organizing workshops or talks, creating t-shirts or stickers, etc. + +Advisors influence Yellowbrick primarily through advisor meetings. This section describes advisor interactions, meetings, and decision-making structure. + +Officers +~~~~~~~~ +During the first advisor meeting in January, three officers should be elected to serve in special capacities over the course of the year. The officer positions are as follows: + +**Chair**: the chair’s responsibility is to organize and lead the advisor meetings, to ensure good conduct and order, and to adjudicate any disputes. The chair may call additional advisor meetings and introduce proposals for the advisors to make decisions upon. The chair calls votes to be held and may make a tie-breaking vote if a tie exists. At the beginning of every meeting, the chair should report the status of Yellowbrick and the state of the current release. + +**Secretary**: the secretary’s responsibility is to create an agenda and record the minutes of advisor meetings, including all decisions undertaken and their results. The secretary may take the place of the chair if the chair is not available during a meeting. + +**Treasurer**: the treasurer assumes responsibility for tracking cash flow—both dues payments from the advisors as well as any outgoing payments for expenses. The treasurer may also be responsible to handle monies received from outside sources, such as development grants. The treasurer should report on the financial status of the project at the beginning of every advisor meeting. + +Officers may either self-nominate or be nominated by other advisors. Nominations should be submitted before the first advisor meeting in January, and the first order of business for that meeting should be to vote the officers into their positions. Officer votes follow the normal rules of voting as described below; in the case of a tie, the founders cast the tie-breaking decision. + +Meetings +~~~~~~~~ +Advisors must schedule at least three meetings per year in the first month of each semester. At the advisor’s discretion, they may schedule additional meetings as necessary. No vote may take place without a meeting and verbal deliberation (e.g. no voting by email or by poll). Meetings must be held with a virtual component. E.g. even if the meeting is in-person with available advisors, there must be some teleconference or video capability to allow remote advisors to participate meaningfully in the meetings. + +Scheduling +^^^^^^^^^^ +To schedule a meeting, the chair must prepare a poll to find the availability of all advisors with at least 6 options over the next 10 days. The chair must ensure that every reasonable step is taken to include as many of the advisors as possible. No meeting may be scheduled without a quorum attending. + +Meetings must be scheduled in January, May, and September; as close to the start of the semester as possible. It is advisable to send the scheduling poll for those meetings in December, April, and August. Advisors may hold any number of additional meetings. + +Voting +^^^^^^ +Voting rules are simple—a proposal for a vote must be made by one of the advisors, submitted to the chair who determines if a vote may be held. The proposal may be seconded to require the chair to hold a vote on the issue. Votes may only be held if a quorum of all advisors (half of the advisors plus one) is present or accounted for in some way (e.g. through a delegation). Prior to the vote, a period of discussion of no less than 5 minutes must be allowed so that members may speak for or against the proposal. + +Votes may take place in two ways, the mechanism of which must be decided by the chair as part of the vote proposal. The vote can either be performed by secret ballot as needed or, more generally, by counting the individual votes of advisors. Advisors can either submit a “yea”, “nay”, or “abstain” vote. A proposal is passed if a majority (half of the advisors plus one) submit a “yea” vote. A proposal is rejected if a majority (half of the advisors plus one) submit a “nay” vote. If neither a majority “yea” or “nay” votes are obtained, the vote can be conducted again at the next advisors meeting. + +The proposal is not ratified until it is agreed upon by the founders, who have the final say in all decision making. This can be interpreted as either a veto (the founders reject a passed proposal) or as a request for clarification (the founders require another vote for a rejected proposal). There is no way to overturn a veto by the founders, though they recognize that by “taking their ball and going home”, they are not fostering a sense of community and expect that these disagreements will be extraordinarily rare. + +Agenda and Minutes +^^^^^^^^^^^^^^^^^^ +The secretary is responsible for preparing a meeting agenda and sending it to all advisors at least 24 hours before the advisors meeting. Minutes that describe in detail the discussion, any proposals, and the outcome of all votes should be taken as well. Minutes should be made public to the rest of the Yellowbrick community. + +Amendments +---------- +Amendments or changes to this governance document can be made by a vote of the advisors. Proposed changes must be submitted to the advisors for review at least one week prior to the vote taking place. Prior to the vote, the advisors should allow for a period of discussion where the changes or amendments can be reviewed by the group. Amendments are ratified by the normal voting procedure described above. + +Amendments should update the version and date at the top of this document. + +Board of Advisors Minutes +------------------------- + +.. toctree:: + :maxdepth: 1 + + minutes/2019-05-15.rst \ No newline at end of file diff --git a/docs/governance/minutes/2019-05-15.rst b/docs/governance/minutes/2019-05-15.rst new file mode 100644 index 000000000..6da4070d3 --- /dev/null +++ b/docs/governance/minutes/2019-05-15.rst @@ -0,0 +1,287 @@ +.. -*- mode: rst -*- + +May 15, 2019 +============ + +Yellowbrick Advisory Board Meeting Held on May 15, 2019 from 2030-2230 EST via Video Conference Call. Minutes taken by Benjamin Bengfort. + +Attendees: Edwin Schmierer, Kristen McIntyre, Larry Gray, Nathan Danielsen, Adam Morris, Prema Roman, Rebecca Bilbro, Tony Ojeda, Benjamin Bengfort. + +Agenda +------ + +A broad overview of the topics for discussion in the order they were presented: + +1. Welcome and introduction (Benjamin Bengfort) +2. Officer nominations +3. Advisor dues and Yellowbrick budget +4. PyCon Sprints debrief (Larry Gray) +5. Summer 2019 contributors and roles +6. Google Summer of Code (Adam Morris) +7. Yellowbrick v1.0 milestone planning +8. Project roadmap through 2020 +9. Proposed amendment to governance: mentor role (Edwin Schmierer) +10. Other business + +Votes and Resolutions +--------------------- + +Officer Elections +~~~~~~~~~~~~~~~~~ + +During the first advisory board meeting of the year, officers are nominated and elected to manage Yellowbrick for the year. The following nominations were proposed: + +Nominations for Chair: + +- Rebecca Bilbro by Benjamin Bengfort + +Nominations for Secretary: + +- Benjamin Bengfort by Rebecca Bilbro + +Nominations for Treasurer: + +- Edwin Schmierer by Rebecca Bilbro + +Motion: a vote to elect the nominated officers: Rebecca Bilbro as Chair, Benjamin Bengfort as Secretary, and Edwin Schmierer as Treasurer. Moved by Nathan Danielsen, seconded by Adam Morris. + +*The motion was adopted unanimously*. + +Operating Budget +~~~~~~~~~~~~~~~~ + +The following operating budget is proposed for 2019. + +Last year’s budget consisted of only two line items, which we believe will also primarily contribute to this year’s budget. The total budget is **$266.49** we have 9 advisors this year, therefore the dues are $29.61 per advisor. + +Budget breakdown: + +============================================= ================ ========= + Description Frequency Total +============================================= ================ ========= +Name.com domain registration (scikit-yb.org) annually $12.99 +Read the Docs Gold Membership $10 monthly $120.00 +Stickers (StickerMule.com) annually $133.50 +Datasets hosting on S3 monthly (DDL) $10-15 +Cheatsheets annually (DDL) $70 +============================================= ================ ========= + +Based on this budget, it is proposed that the dues are rounded to **$30.00** per advisor. Further, dues should be submitted to the treasurer via Venmo or PayPal no later than May 30, 2019. The treasurer will reimburse the payees of our expenses directly. + +A special thank you to District Data Labs for covering travel costs to PyCon, the printing of cheatsheets, and hosting our datasets on S3. + +Motion: confirm the 2019 budget and advisor dues. Moved by Rebecca Bilbro, seconded by Nathan Danielsen. + +*The motion was adopted unanimously*. + +Governance Amendment +~~~~~~~~~~~~~~~~~~~~ + +A proposed amendment to the governance document adding a "mentor-contributor" role as follows: + +.. code-block:: text + + Mentor-Contributor + ^^^^^^^^^^^^^^^^^^ + + A mentor-contributor assumes all the responsibilities of a core contributor but + commits 25-50% of their time toward mentoring a new contributor who has + little-to-no experience. The purpose of the mentor-contributor role is to assist in + recruiting and developing a pipeline of new contributors for Yellowbrick. + + A mentor-contributor would guide a new contributor in understanding the community, + roles, ethos, expectations, systems, and processes in participating and + contributing to an open source project such as Yellowbrick. A new contributor is + someone who is interested in open source software projects but has little-to-no + experience working on an open source project or participating in the community. + + The mentor-contributor would work in tandem with the new contributor during the + semester to complete assigned issues and prepare the new contributor to be a core + contributor in a future semester. A mentor-contributor would mostly likely work on + fewer issues during the semester in order to allocate time for mentoring. + Mentor-contributors would be matched with new contributors prior to the start of + each semester or recruit their own contributor (eg. colleague or friend). The + responsibilities of mentor-contributors are as follows: + + - When possible, recruit new contributors to Yellowbrick. + - Schedule weekly mentoring sessions and assign discrete yet increasingly complex tasks to the new contributor. + - Work with the new contributor to complete assigned issues during the semester. + - Set specific and reasonable goals for contributions over the semester. + - Work with maintainers to complete issues and goals for the next release. + - Respond to maintainer requests and be generally available for discussion. + - Participating actively on the #yellowbrick Slack channel. + - Join any co-working or pair-programming sessions required to achieve goals. + - Make a determination at the end of the semester on the readiness of the new contributor to be a core-contributor. + + The mentor-contributor role may also appeal to students who use Yellowbrick in a + machine learning course and seek the experience of contributing to an open source + project. + +Motion: accept the amendment adding a mentor contributor role to the governance document. Moved by Edwin Schmierer, seconded by Rebecca Bilbro. + +*The motion was adopted unanimously without modifications*. + +Semester and Roadmap +-------------------- + +The summer semester will be dedicated to completing **Yellowbrick Version 1.0**. The issues associated with this release can be found in the `v1.0 Milestone on GitHub `_. This release has a number of deep issues: including figuring out the axes/figure API and compatibility. It is a year behind and it'll be good to move forward on it! + +We have a critical need for maintainers, we only have 2-3 people regularly reviewing PRs and far more PRs than we can handle. So far there have been no volunteers for summer maintainer status. We will address this in two ways: + +1. Maintainer mentorship +2. Redirect non v1.0 PRs to the fall + +Rebecca will take the lead in mentoring new maintainers so that they feel more confident in conducting code reviews. This will include peer-reviews and a deep discussion of what we're looking for during code reviews. These code reviews will only be conducted on PRs by the summer contributors. + +Otherwise PRs will be handled as follows this semester: + +- Triage new PRs/Issues. If it is critical to v1.0 a bugfix or a docfix, assign a maintainer. +- Otherwise we will request the contributor join the fall semester. +- If a PR is older than 30 days it is "gone stale" and will be closed. +- Recommend contributors push to ``yellowbrick.contrib`` or write blog post or notebook. + +We likely will need at least three maintainers per semester moving forward. + +Summer 2019 Contributors +~~~~~~~~~~~~~~~~~~~~~~~~ + +=================== ================= +Name Role +=================== ================= +Lawrence Gray Coordinator +Prashi Doval Core Contributor +Benjamin Bengfort Core Contributor +Saurabh Daalia Core Contributor +Bashar Jaan Khan Core Contributor +Piyush Gautam Core Contributor +Naresh Bachwani GSoC Student +Xinyu You Core Contributor +Sandeep Banerjee Core Contributor +Carl Dawson Core Contributor +Mike Curry Core Contributor +Nathan Danielsen Maintainer +Rebecca Bilbro Mentor +Prema Roman Maintainer +Kristen McIntyre Maintainer +=================== ================= + +Project Roadmap +~~~~~~~~~~~~~~~ + +The v1.0 release has a number of significant changes that may not be backward compatible with previous versions (though for the most part it will be). Because of this, and because many of the issues are *contagious* (e.g. affect many files in Yellowbrick), we are reluctant to plan too much into the future for Yellowbrick. Instead we have created a `v1.1 milestone in GitHub `_ to start tracking issues there. + +Broadly some roadmap items are: + +- *Make quick methods primetime*. Our primary API is the visualizer, which allows for the most configuration and customization of visualizations. The quick methods, however, are a simple workflow that is in demand. The quick methods will do all the work of the most basic visualizer functions in one line of code and return the visualizer for further customization. +- *Add a neural package for ANN specific modeling*. We already have a text package for natural language processing, as deep learning is becoming more important, Yellowbrick should help with the interpretability of these models as well. + +Other roadmap ideas and planning discussed included: + +- A yb-altair prototype potentially leading to an Altair backend side-by-side with matplotlib. +- Devops/engineering focused content for Yellowbrick (e.g. model management and maintenance). +- Fundraising to pay for more ambitious YB development. +- Having Yellowbrick attendees at more conferences. +- Determining who is really using Yellowbrick to better understand the community we're supporting. + +Minutes +------- + +Comments on the Inaugural Advisors Meeting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Remarks delivered by Benjamin Bengfort* + +Welcome to the inaugural meeting of the Yellowbrick Board of Advisors. Due to the number of agenda items and limited time, this meeting will have a more formal tone than normal if simply to provide structure so that we can get to everything. Before we start, however, I wanted to say a few words to mark this occasion and to remark on the path that led us here. + +When Rebecca and I started Yellowbrick 3 years ago this month, I don’t think that we intended to create a top tier Python library, nor did we envision how fast the project would grow. At the time, we wanted to make a statement about the role of visual analytics - the iterative interaction between human and computer - in the machine learning workflow. Though Rebecca and I communicated to different audiences, we were lucky enough to say this at a time when our message resonated with both students and professionals. Because of this, Yellowbrick has enjoyed a lot of success across a variety of different metrics: downloads, stars, blog posts. But the most important metric to me personally is the number of contributors we have had. + +Yellowbrick has become more than a software package, it has also become a community. And it is in no small part due to the efforts of the people here tonight. I think it is not inappropriate and perhaps can serve as an introduction for me to individually recognize your contributions to the rest of the group. + +Starting at the beginning, *Tony Ojeda* has uniquely supported Yellowbrick in a way no one else could. He incubated the project personally through his company, District Data Labs, even though there was no direct market value to him. He put his money where his heart was and to me, that will always make him the model of the ideal entrepreneur. + +*Larry Gray* joined Yellowbrick by way of the research labs. In a way, he was exactly the type of person we hoped would join the labs - a professional data scientist who wanted to contribute to an open source data project. Since then he has become so much more, working hard to organize and coordinate the project and investing time and emotional energy which we have come to rely on. + +*Nathan Danielsen* represents a counterpoint to the data science contributor and instead brings a level of software engineering professionalism that is sorely needed in every open source project. Although we often mention his work on image similarity tests - what that effort really did was to open the door to many new contributors, allowing us to review and code with confidence know the tests had our backs. + +*Prema Roman* has brought a thoughtful, measured approach to the way she tackles issues and is personally responsible for the prototyping and development of several visualizers including JointPlot and CrossValidation. We have relied on her to deliver new, high-impact features to the library. + +*Kristen McIntyre* is the voice of our users, an essential role we could not do without. Without her, we would be envious of scikit-learn’s documentation. Instead, we are blessed to have clear, consistent, and extensive descriptions of the visualizers which unlock their use to new and veteran users of our software. + +*Adam Morris* has become the voice of our project, particularly on Twitter. I’m joyously surprised every time I read one of his tweets. It is not the voice I would have used and that’s what makes it so sweet. He has taken to heart our code of conduct’s admonishments to over the top positivity and encouragement, and I’m always delighted by it. + +*Edwin Schmierer* many of you may not have met yet - but without him, this project would never have happened. He is responsible for creating the Data Science program at Georgetown and he’s been a good friend and advisor ever since. During our lunches and breakfasts together, we have often discussed the context of Yellowbrick in the bigger picture. I’m very excited to have him be able to give us advice more directly. + +Every time I use Yellowbrick I can see or sense your individual impact and signature on the project. Thank you all so much for joining Rebecca and I in building Yellowbrick into what it is today. It is not an understatement that Yellowbrick has been successful beyond what we expected or hoped. + +With success comes responsibility. I know that we have all felt that responsibility keenly, particularly recently as the volume of direct contacts with the community has been increasing. We have reached a stage where that responsibility cannot be borne lightly by a few individuals, because the decisions we make reach and affect a much larger group of people then we may intend. To address this, we as a group, have ratified a new governance document whose primary purpose is to help us organize and act strategically as a cohesive group. I believe that the structures we have put into place will allow us to move Yellowbrick even further toward being a professional grade software library and will help us minimize risks to the project - particularly the risk of maintainer burnout. + +We are now making another statement at another pivotal moment because we are not alone in formalizing or changing our mode of governance. Perhaps the most public example of this is Python itself, but the message of maintainer-exhaustion and burnout has been discussed for the past couple of years in a variety of settings - OSCON, PyCON, video and podcast interviews, surveys and reports. It has caused growing concern and an essential question: “how do we support open source projects that are critical to research and infrastructure when they are not supported by traditional commercial and economic mechanisms”? + +Many of you have heard me say that “I believe in open source the way I believe in democracy”. But like democracy, open source is evolving as new contributors, new technology, and new cultures start to participate more. Yellowbrick is representative of what open source can achieve and what it does both for and to the people who maintain it. As we move forward from here, the choices we make may have an impact beyond our project and perhaps even beyond Python and I hope that you, like me, find that both exciting and terrifying. + +Today we are here to discuss how we will conduct the project toward a version 1.0 release, a significant milestone, while also mentoring a Google Summer of Code Student and managing the many open pull requests and issues currently outstanding. The details matter, but they must also be considered within a context. Therefore I would like to personally commission you all toward two goals that set that context: + +1. **To be a shining example of what an open source project should be.** +2. **To think strategically of how we can support a community of both users and maintainers.** + +I’m very much looking forward to the future, and am so excited to be doing it together with you all. + +PyCon 2019 Debrief +~~~~~~~~~~~~~~~~~~ + +Yellowbrick had an open source booth at PyCon 2019 for the second time, manned by Larry, Nathan, Prema, and Kristen. There were lots of visitors to the booth who were genuinely excited about using Yellowbrick. Next year it would be great to have a large banner and vertical display of visualizers to help people learn more. Guido von Rossum even had lunch at the booth, which sparked a lot of attention! Special thank you to Tony for his financial support including shirts, stickers, transportation, and supplies. + +We also hosted two days of development sprints with 13 sprinters and 9 PRs. The majority of sprinters were there for both days. The sprints included a wide variety of people including a high schooler and even a second year sprinter. Guido von Rossum also stopped by during the sprints. Prema and Kristen announced the sprints on stage and there is a great picture of them that we tweeted. Special thank you to Daniel for creating the cheatsheets, which cut down a lot of explaining, and to Nathan for the appetizers. Beets are delicious! + +Hot takes from PyCon: + +- Cheatsheets were a huge success and very important +- It would be good to have a "Contributing to Yellowbrick" cheatsheet for next year +- Have to capitalize on attention before the conference starts +- More visual stand out: e.g. metal grommets and banner with visualizers +- NumFOCUS has an in-house designer who may be able to help + +Important request: we know that there will be a large number of PRs after PyCon or during Hacktoberfest, we need to *plan* for this and ensure we know who/how is triaging them and handling them ahead of the event. The maintainers who did not attend PyCon ended up slammed with work even though they couldn't enjoy participating in the event itself. + +Google Summer of Code +~~~~~~~~~~~~~~~~~~~~~ + +We applied to GSoC under the NumFOCUS umbrella and have been given a student, Naresh, to work with us over the summer. Adam has already had initial communication with him and he's eager to get started. So far we've already interacted with him on Github via some of his early PRs and he's going to be a great resource. + +In terms of the GSoC actions, the coding period starts May 27 and ends August 26 when deliverables are due. Every 4 weeks we do a 360 review where he evaluates us and we evaluate him (pass/fail). NumFOCUS requires a blog post and will work with Naresh directly on the posts. + +We need to determine what he should work on. His proposal was about extending PCA, but Visual Pipelines are also extremely important. Proposal, break his work into three phases. First, get the first introductory PR across the finish line, then scope the ideas in his proposal and involve him in the visualizer audit. After working on blog posts and using YB on his own dataset, then get started on actual deliverables. + +Action Items: + +- Take note that Naresh is in India and is 9.5 hours ahead (time zone). +- Schedule introduction to team and maintainers. +- Coding period begins May 27. +- How do we want to manage communications (Slack) +- 360 evaluations due every 4 weeks +- Determine final deliverables due on Aug 19-26 + +Other Topics +~~~~~~~~~~~~ + +Yellowbrick as a startup. Perhaps we can think big and pay for a full-time developer? There are many potential grant sources. The problem is that more money means more responsibility and we can't keep things together as it is now. + +It would be good to send updates to NumFOCUS and District Data Labs to keep them apprised of what we're doing. For example: + +- JOSS paper releases +- Sprints and conference attendance +- Version releases +- Talks or presentations + +Hopefully this will allow them to also spread the word about what we're doing. + +Action Items +------------ + +- Send dues to Edwin Schmierer via Venmo/PayPal (all) +- Prepare more budget options for December board meeting (Treasurer) +- Create PR with Governance Amendment (Secretary) +- Modify documentation to change language about "open a PR as early as possible" (Larry) +- Add Naresh to Slack (Adam) +- Let Adam know what talks are coming up so he can tweet them (all) +- Send updates big and small to NumFOCUS (all) diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico new file mode 100644 index 000000000..7b9871483 Binary files /dev/null and b/docs/images/favicon.ico differ diff --git a/docs/images/gallery/alpha_selection.png b/docs/images/gallery/alpha_selection.png new file mode 100644 index 000000000..3081dbcaf Binary files /dev/null and b/docs/images/gallery/alpha_selection.png differ diff --git a/docs/images/gallery/balanced_binning_reference.png b/docs/images/gallery/balanced_binning_reference.png new file mode 100644 index 000000000..76115b0ed Binary files /dev/null and b/docs/images/gallery/balanced_binning_reference.png differ diff --git a/docs/images/gallery/class_balance.png b/docs/images/gallery/class_balance.png new file mode 100644 index 000000000..95dccd4c7 Binary files /dev/null and b/docs/images/gallery/class_balance.png differ diff --git a/docs/images/gallery/class_prediction_error.png b/docs/images/gallery/class_prediction_error.png new file mode 100644 index 000000000..da0af1721 Binary files /dev/null and b/docs/images/gallery/class_prediction_error.png differ diff --git a/docs/images/gallery/classification_report.png b/docs/images/gallery/classification_report.png new file mode 100644 index 000000000..d879ec371 Binary files /dev/null and b/docs/images/gallery/classification_report.png differ diff --git a/docs/images/gallery/concrete_isomap_manifold.png b/docs/images/gallery/concrete_isomap_manifold.png new file mode 100644 index 000000000..7a500632d Binary files /dev/null and b/docs/images/gallery/concrete_isomap_manifold.png differ diff --git a/docs/images/gallery/concrete_tsne_manifold.png b/docs/images/gallery/concrete_tsne_manifold.png new file mode 100644 index 000000000..803970533 Binary files /dev/null and b/docs/images/gallery/concrete_tsne_manifold.png differ diff --git a/docs/images/gallery/confusion_matrix_digits.png b/docs/images/gallery/confusion_matrix_digits.png new file mode 100644 index 000000000..15d3cb1b7 Binary files /dev/null and b/docs/images/gallery/confusion_matrix_digits.png differ diff --git a/docs/images/gallery/confusion_matrix_iris.png b/docs/images/gallery/confusion_matrix_iris.png new file mode 100644 index 000000000..d94adb000 Binary files /dev/null and b/docs/images/gallery/confusion_matrix_iris.png differ diff --git a/docs/images/gallery/corpus_tsne.png b/docs/images/gallery/corpus_tsne.png new file mode 100644 index 000000000..6d51af342 Binary files /dev/null and b/docs/images/gallery/corpus_tsne.png differ diff --git a/docs/images/gallery/cv_scores.png b/docs/images/gallery/cv_scores.png new file mode 100644 index 000000000..52dbce3c6 Binary files /dev/null and b/docs/images/gallery/cv_scores.png differ diff --git a/docs/images/gallery/decision_boundaries.png b/docs/images/gallery/decision_boundaries.png new file mode 100644 index 000000000..993d6bba0 Binary files /dev/null and b/docs/images/gallery/decision_boundaries.png differ diff --git a/docs/images/gallery/discrimination_threshold.png b/docs/images/gallery/discrimination_threshold.png new file mode 100644 index 000000000..2378d752b Binary files /dev/null and b/docs/images/gallery/discrimination_threshold.png differ diff --git a/docs/images/gallery/dispersion.png b/docs/images/gallery/dispersion.png new file mode 100644 index 000000000..a14821ac4 Binary files /dev/null and b/docs/images/gallery/dispersion.png differ diff --git a/docs/images/gallery/elbow.png b/docs/images/gallery/elbow.png new file mode 100644 index 000000000..2c6708533 Binary files /dev/null and b/docs/images/gallery/elbow.png differ diff --git a/docs/images/gallery/feature_correlation.png b/docs/images/gallery/feature_correlation.png new file mode 100644 index 000000000..d6671c580 Binary files /dev/null and b/docs/images/gallery/feature_correlation.png differ diff --git a/docs/images/gallery/feature_importances.png b/docs/images/gallery/feature_importances.png new file mode 100644 index 000000000..80f958483 Binary files /dev/null and b/docs/images/gallery/feature_importances.png differ diff --git a/docs/images/gallery/freqdist.png b/docs/images/gallery/freqdist.png new file mode 100644 index 000000000..22f9f5eb5 Binary files /dev/null and b/docs/images/gallery/freqdist.png differ diff --git a/docs/images/gallery/icdm.png b/docs/images/gallery/icdm.png new file mode 100644 index 000000000..b1746433e Binary files /dev/null and b/docs/images/gallery/icdm.png differ diff --git a/docs/images/gallery/jointplot.png b/docs/images/gallery/jointplot.png new file mode 100644 index 000000000..572669956 Binary files /dev/null and b/docs/images/gallery/jointplot.png differ diff --git a/docs/images/gallery/learning_curve.png b/docs/images/gallery/learning_curve.png new file mode 100644 index 000000000..1d9ee09d5 Binary files /dev/null and b/docs/images/gallery/learning_curve.png differ diff --git a/docs/images/gallery/occupancy_tsne_manifold.png b/docs/images/gallery/occupancy_tsne_manifold.png new file mode 100644 index 000000000..a0e0a9c1e Binary files /dev/null and b/docs/images/gallery/occupancy_tsne_manifold.png differ diff --git a/docs/images/gallery/parallel_coordinates.png b/docs/images/gallery/parallel_coordinates.png new file mode 100644 index 000000000..16f1f0641 Binary files /dev/null and b/docs/images/gallery/parallel_coordinates.png differ diff --git a/docs/images/gallery/pca_projection_3d.png b/docs/images/gallery/pca_projection_3d.png new file mode 100644 index 000000000..fdadf4db9 Binary files /dev/null and b/docs/images/gallery/pca_projection_3d.png differ diff --git a/docs/images/gallery/postag.png b/docs/images/gallery/postag.png new file mode 100644 index 000000000..6f375fb13 Binary files /dev/null and b/docs/images/gallery/postag.png differ diff --git a/docs/images/gallery/precision_recall_binary.png b/docs/images/gallery/precision_recall_binary.png new file mode 100644 index 000000000..0cc48363c Binary files /dev/null and b/docs/images/gallery/precision_recall_binary.png differ diff --git a/docs/images/gallery/precision_recall_multiclass.png b/docs/images/gallery/precision_recall_multiclass.png new file mode 100644 index 000000000..b9e3835ec Binary files /dev/null and b/docs/images/gallery/precision_recall_multiclass.png differ diff --git a/docs/images/gallery/prediction_error.png b/docs/images/gallery/prediction_error.png new file mode 100644 index 000000000..a7e16c323 Binary files /dev/null and b/docs/images/gallery/prediction_error.png differ diff --git a/docs/images/gallery/radviz.png b/docs/images/gallery/radviz.png new file mode 100644 index 000000000..d158884f8 Binary files /dev/null and b/docs/images/gallery/radviz.png differ diff --git a/docs/images/gallery/rank1d_shapiro.png b/docs/images/gallery/rank1d_shapiro.png new file mode 100644 index 000000000..aae7a05dd Binary files /dev/null and b/docs/images/gallery/rank1d_shapiro.png differ diff --git a/docs/images/gallery/rank2d_covariance.png b/docs/images/gallery/rank2d_covariance.png new file mode 100644 index 000000000..e385c96f5 Binary files /dev/null and b/docs/images/gallery/rank2d_covariance.png differ diff --git a/docs/images/gallery/residuals.png b/docs/images/gallery/residuals.png new file mode 100644 index 000000000..3cb152a46 Binary files /dev/null and b/docs/images/gallery/residuals.png differ diff --git a/docs/images/gallery/rfecv_sklearn_example.png b/docs/images/gallery/rfecv_sklearn_example.png new file mode 100644 index 000000000..6e323f7d9 Binary files /dev/null and b/docs/images/gallery/rfecv_sklearn_example.png differ diff --git a/docs/images/gallery/rocauc_binary.png b/docs/images/gallery/rocauc_binary.png new file mode 100644 index 000000000..3328eb327 Binary files /dev/null and b/docs/images/gallery/rocauc_binary.png differ diff --git a/docs/images/gallery/rocauc_multiclass.png b/docs/images/gallery/rocauc_multiclass.png new file mode 100644 index 000000000..42434391d Binary files /dev/null and b/docs/images/gallery/rocauc_multiclass.png differ diff --git a/docs/images/gallery/scatter.png b/docs/images/gallery/scatter.png new file mode 100644 index 000000000..3751a780d Binary files /dev/null and b/docs/images/gallery/scatter.png differ diff --git a/docs/images/gallery/silhouette.png b/docs/images/gallery/silhouette.png new file mode 100644 index 000000000..8cc2f4b9c Binary files /dev/null and b/docs/images/gallery/silhouette.png differ diff --git a/docs/images/gallery/validation_curve.png b/docs/images/gallery/validation_curve.png new file mode 100644 index 000000000..7c4d1bf6b Binary files /dev/null and b/docs/images/gallery/validation_curve.png differ diff --git a/docs/images/matplotlib_anatomy.png b/docs/images/matplotlib/anatomy.png similarity index 100% rename from docs/images/matplotlib_anatomy.png rename to docs/images/matplotlib/anatomy.png diff --git a/docs/images/matplotlib_pbpython_example.png b/docs/images/matplotlib/pbpython_example.png similarity index 100% rename from docs/images/matplotlib_pbpython_example.png rename to docs/images/matplotlib/pbpython_example.png diff --git a/docs/images/matplotlib_single.png b/docs/images/matplotlib/single.png similarity index 100% rename from docs/images/matplotlib_single.png rename to docs/images/matplotlib/single.png diff --git a/docs/images/quickstart/bikeshare_ols_residuals.png b/docs/images/quickstart/bikeshare_ols_residuals.png deleted file mode 100644 index 05e6005f0..000000000 Binary files a/docs/images/quickstart/bikeshare_ols_residuals.png and /dev/null differ diff --git a/docs/images/quickstart/bikeshare_rank2d.png b/docs/images/quickstart/bikeshare_rank2d.png deleted file mode 100644 index 6f1c5a559..000000000 Binary files a/docs/images/quickstart/bikeshare_rank2d.png and /dev/null differ diff --git a/docs/images/quickstart/bikeshare_ridge_alphas.png b/docs/images/quickstart/bikeshare_ridge_alphas.png deleted file mode 100644 index d831e9e50..000000000 Binary files a/docs/images/quickstart/bikeshare_ridge_alphas.png and /dev/null differ diff --git a/docs/images/quickstart/bikeshare_ridge_prediction_error.png b/docs/images/quickstart/bikeshare_ridge_prediction_error.png deleted file mode 100644 index f012da42f..000000000 Binary files a/docs/images/quickstart/bikeshare_ridge_prediction_error.png and /dev/null differ diff --git a/docs/images/quickstart/temp_feelslike_jointplot.png b/docs/images/quickstart/temp_feelslike_jointplot.png deleted file mode 100644 index db70dbe8e..000000000 Binary files a/docs/images/quickstart/temp_feelslike_jointplot.png and /dev/null differ diff --git a/docs/images/readme/affiliates_ddl.png b/docs/images/readme/affiliates_ddl.png new file mode 100644 index 000000000..6dfc88b48 Binary files /dev/null and b/docs/images/readme/affiliates_ddl.png differ diff --git a/docs/images/readme/affiliates_numfocus.png b/docs/images/readme/affiliates_numfocus.png new file mode 100644 index 000000000..299e45f7e Binary files /dev/null and b/docs/images/readme/affiliates_numfocus.png differ diff --git a/docs/images/readme/banner.png b/docs/images/readme/banner.png new file mode 100644 index 000000000..916526abd Binary files /dev/null and b/docs/images/readme/banner.png differ diff --git a/docs/images/readme/readme_imgs.py b/docs/images/readme/readme_imgs.py new file mode 100755 index 000000000..c246c2049 --- /dev/null +++ b/docs/images/readme/readme_imgs.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 + +import os +import sys +import argparse +import numpy as np +import matplotlib.pyplot as plt + +from yellowbrick.regressor import AlphaSelection +from yellowbrick.regressor import ResidualsPlot +from yellowbrick.regressor import PredictionError +from yellowbrick.cluster import KElbowVisualizer +from yellowbrick.cluster import SilhouetteVisualizer +from yellowbrick.cluster import InterclusterDistance +from yellowbrick.classifier import PrecisionRecallCurve +from yellowbrick.classifier import ClassPredictionError +from yellowbrick.classifier import DiscriminationThreshold +from yellowbrick.datasets import load_spam, load_concrete, load_game + + +from sklearn.cluster import KMeans +from sklearn.datasets import make_blobs +from sklearn.naive_bayes import GaussianNB +from sklearn.preprocessing import OneHotEncoder +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split +from sklearn.linear_model import LogisticRegression, Lasso, LassoCV, Ridge + + +FIGSIZE = (20, 4) + +IMAGES = os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) +YB_LOGO_PATH = os.path.join(IMAGES, "yb-fc.png") + + +def tts_plot(viz, X, y, test_size=0.20, random_state=42, score=True, finalize=True): + """ + Helper function to plot model visualizers with train_test_split + """ + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=test_size, random_state=random_state + ) + + viz.fit(X_train, y_train) + if score: + viz.score(X_test, y_test) + if finalize: + viz.finalize() + + return viz + + +def class_prediction_error(ax=None): + data = load_game(return_dataset=True) + X, y = data.to_numpy() + + X = OneHotEncoder().fit_transform(X).toarray() + + viz = ClassPredictionError(GaussianNB(), ax=ax) + return tts_plot(viz, X, y) + + +def confusion_matrix(ax=None): + data = load_spam(return_dataset=True) + X, y = data.to_pandas() + + viz = PrecisionRecallCurve(LogisticRegression(), ax=ax) + return tts_plot(viz, X, y) + + +def discrimination_threshold(ax=None): + data = load_spam(return_dataset=True) + X, y = data.to_pandas() + + viz = DiscriminationThreshold(RandomForestClassifier(n_estimators=10), ax=ax) + return tts_plot(viz, X, y, score=False) + + +def classification_visualizers(saveto=None): + _, (axa, axb, axc) = plt.subplots(nrows=1, ncols=3, figsize=FIGSIZE) + + class_prediction_error(axa) + confusion_matrix(axb) + discrimination_threshold(axc) + + plt.tight_layout(pad=1.5) + + if saveto is not None: + plt.savefig(saveto) + else: + plt.show() + + +def residuals_plot(ax=None): + data = load_concrete(return_dataset=True) + X, y = data.to_pandas() + + viz = ResidualsPlot(Ridge(), ax=ax) + return tts_plot(viz, X, y) + + +def prediction_error(ax=None): + data = load_concrete(return_dataset=True) + X, y = data.to_pandas() + + viz = PredictionError(Lasso(), ax=ax) + return tts_plot(viz, X, y) + + +def alpha_selection(ax=None): + data = load_concrete(return_dataset=True) + X, y = data.to_pandas() + + alphas = np.logspace(-10, 1, 400) + viz = AlphaSelection(LassoCV(alphas=alphas), ax=ax) + return tts_plot(viz, X, y) + + +def regression_visualizers(saveto=None): + _, (axa, axb, axc) = plt.subplots(nrows=1, ncols=3, figsize=FIGSIZE) + + residuals_plot(axa) + prediction_error(axb) + alpha_selection(axc) + + plt.tight_layout(pad=1.5) + + if saveto is not None: + plt.savefig(saveto) + else: + plt.show() + + +def intercluster_distance(ax=None): + X, y = make_blobs(centers=12, n_samples=1000, n_features=16, shuffle=True) + + viz = InterclusterDistance(KMeans(9), ax=ax) + viz.fit(X) + viz.finalize() + + return viz + + +def k_elbow(ax=None): + X, y = make_blobs(centers=12, n_samples=1000, n_features=16, shuffle=True) + + viz = KElbowVisualizer(KMeans(), k=(4, 12), ax=ax, locate_elbow=False) + viz.fit(X) + viz.finalize() + + return viz + + +def silhouette(ax=None): + X, y = make_blobs(centers=12, n_samples=1000, n_features=16, shuffle=True) + + viz = SilhouetteVisualizer(KMeans(9), ax=ax) + viz.fit(X) + viz.finalize() + + return viz + + +def clustering_visualizers(saveto=None): + _, (axa, axb, axc) = plt.subplots(nrows=1, ncols=3, figsize=FIGSIZE) + + intercluster_distance(axa) + k_elbow(axb) + silhouette(axc).ax.get_legend().remove() + + plt.tight_layout(pad=1.5) + + if saveto is not None: + plt.savefig(saveto) + else: + plt.show() + + +def yb_logo(path=YB_LOGO_PATH, ax=None): + """ + Reads the YB image logo from the specified path and writes it to the axes. + """ + # Load image + with open(path, "rb") as fobj: + img = plt.imread(fobj, format="png") + + if ax is None: + _, ax = plt.subplots() + + # Draw image + ax.imshow(img, interpolation="nearest") + + # Remove spines, ticks, grid, and other marks + ax.grid(False) + ax.set_xticks([]) + ax.set_yticks([]) + for pos in ["right", "top", "bottom", "left"]: + ax.spines[pos].set_visible(False) + + return ax + + +def full_image(saveto=None, center_logo=False): + _, axes = plt.subplots(nrows=3, ncols=3, figsize=(21, 14)) + + # Top row: classifiers + class_prediction_error(axes[0][0]) + confusion_matrix(axes[0][1]) + discrimination_threshold(axes[0][2]) + + # Middle row: regressors + residuals_plot(axes[1][0]) + alpha_selection(axes[1][2]) + + if center_logo: + yb_logo(ax=axes[1][1]) + else: + prediction_error(axes[1][1]) + + # Bottom row: clusterers + intercluster_distance(axes[2][0]) + k_elbow(axes[2][1]) + silhouette(axes[2][2]).ax.get_legend().remove() + + plt.tight_layout(pad=1.5) + + if saveto is not None: + plt.savefig(saveto) + else: + plt.show() + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description="generates images for the README.md banner" + ) + + parser.add_argument( + "-c", + "--classifiers", + type=str, + metavar="PATH", + default="classifiers.png", + help="path to save the classifiers banner image", + ) + + parser.add_argument( + "-r", + "--regressors", + type=str, + metavar="PATH", + default="regressors.png", + help="path to save the regressors banner image", + ) + + parser.add_argument( + "-C", + "--clusterers", + type=str, + metavar="PATH", + default="clusterers.png", + help="path to save the clusterers banner image", + ) + + parser.add_argument( + "-b", + "--banner", + type=str, + metavar="PATH", + default="", + help="make full banner image and save to disk", + ) + + parser.add_argument( + "-y", + "--yb", + action="store_true", + help="replace middle image of banner with logo", + ) + + args = parser.parse_args() + + if args.banner: + full_image(args.banner, args.yb) + sys.exit(0) + + if args.classifiers: + classification_visualizers(args.classifiers) + + if args.regressors: + regression_visualizers(args.regressors) + + if args.clusterers: + clustering_visualizers(args.clusterers) diff --git a/docs/images/tutorial/modelselect_bagging_classifier.png b/docs/images/tutorial/modelselect_bagging_classifier.png index 8edcb5625..4d0a7a643 100644 Binary files a/docs/images/tutorial/modelselect_bagging_classifier.png and b/docs/images/tutorial/modelselect_bagging_classifier.png differ diff --git a/docs/images/tutorial/modelselect_extra_trees_classifier.png b/docs/images/tutorial/modelselect_extra_trees_classifier.png index 68e1405de..226e7cccb 100644 Binary files a/docs/images/tutorial/modelselect_extra_trees_classifier.png and b/docs/images/tutorial/modelselect_extra_trees_classifier.png differ diff --git a/docs/images/tutorial/modelselect_kneighbors_classifier.png b/docs/images/tutorial/modelselect_kneighbors_classifier.png index e978edc3c..4c8d0d57d 100644 Binary files a/docs/images/tutorial/modelselect_kneighbors_classifier.png and b/docs/images/tutorial/modelselect_kneighbors_classifier.png differ diff --git a/docs/images/tutorial/modelselect_linear_svc.png b/docs/images/tutorial/modelselect_linear_svc.png index 1fec34311..db98db518 100644 Binary files a/docs/images/tutorial/modelselect_linear_svc.png and b/docs/images/tutorial/modelselect_linear_svc.png differ diff --git a/docs/images/tutorial/modelselect_logistic_regression.png b/docs/images/tutorial/modelselect_logistic_regression.png index 0468dea79..665d0126e 100644 Binary files a/docs/images/tutorial/modelselect_logistic_regression.png and b/docs/images/tutorial/modelselect_logistic_regression.png differ diff --git a/docs/images/tutorial/modelselect_logistic_regression_cv.png b/docs/images/tutorial/modelselect_logistic_regression_cv.png index ce541c63c..1900f7798 100644 Binary files a/docs/images/tutorial/modelselect_logistic_regression_cv.png and b/docs/images/tutorial/modelselect_logistic_regression_cv.png differ diff --git a/docs/images/tutorial/modelselect_nu_svc.png b/docs/images/tutorial/modelselect_nu_svc.png index 1e5feb1c9..ebc4cd533 100644 Binary files a/docs/images/tutorial/modelselect_nu_svc.png and b/docs/images/tutorial/modelselect_nu_svc.png differ diff --git a/docs/images/tutorial/modelselect_random_forest_classifier.png b/docs/images/tutorial/modelselect_random_forest_classifier.png index 5c49ab188..acce5fa7d 100644 Binary files a/docs/images/tutorial/modelselect_random_forest_classifier.png and b/docs/images/tutorial/modelselect_random_forest_classifier.png differ diff --git a/docs/images/tutorial/modelselect_sgd_classifier.png b/docs/images/tutorial/modelselect_sgd_classifier.png index f75a94f79..153c029b6 100644 Binary files a/docs/images/tutorial/modelselect_sgd_classifier.png and b/docs/images/tutorial/modelselect_sgd_classifier.png differ diff --git a/docs/images/tutorial/modelselect_svc.png b/docs/images/tutorial/modelselect_svc.png index 2d51721e5..16206c69f 100644 Binary files a/docs/images/tutorial/modelselect_svc.png and b/docs/images/tutorial/modelselect_svc.png differ diff --git a/docs/images/visualizers.png b/docs/images/visualizers.png deleted file mode 100644 index 342b6ba86..000000000 Binary files a/docs/images/visualizers.png and /dev/null differ diff --git a/docs/images/yb-bw.png b/docs/images/yb-bw.png new file mode 100644 index 000000000..ef5a5e158 Binary files /dev/null and b/docs/images/yb-bw.png differ diff --git a/docs/images/yb-fc.png b/docs/images/yb-fc.png new file mode 100644 index 000000000..57d118252 Binary files /dev/null and b/docs/images/yb-fc.png differ diff --git a/docs/images/yb-lego.png b/docs/images/yb-lego.png new file mode 100644 index 000000000..fd3c36c50 Binary files /dev/null and b/docs/images/yb-lego.png differ diff --git a/docs/index.rst b/docs/index.rst index 8fb3ebee9..7ca743ad2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,16 +7,48 @@ Yellowbrick: Machine Learning Visualization =========================================== -.. image:: images/visualizers.png +.. image:: images/readme/banner.png -Yellowbrick is a suite of visual diagnostic tools called "Visualizers" that extend the Scikit-Learn API to allow human steering of the model selection process. In a nutshell, Yellowbrick combines scikit-learn with matplotlib in the best tradition of the scikit-learn documentation, but to produce visualizations for *your* models! For more on Yellowbrick, please see the :doc:`about`. +Yellowbrick extends the Scikit-Learn API to make model selection and hyperparameter tuning easier. Under the hood, it's using Matplotlib. -If you're new to Yellowbrick, checkout the :doc:`quickstart` or skip ahead to the :doc:`tutorial`. Yellowbrick is a rich library with many Visualizers being added on a regular basis. For details on specific Visualizers and extended usage head over to the :doc:`api/index`. Interested in contributing to Yellowbrick? Checkout the :ref:`contributing guide ` . If you've signed up to do user testing, head over to the :doc:`evaluation` (and thank you!). +Recommended Learning Path +------------------------- + +1. Check out the :doc:`quickstart`, try the :doc:`tutorial`, and check out the :doc:`oneliners`. + +2. Use Yellowbrick in your work, referencing the :doc:`api/index` for assistance with specific visualizers and detailed information on optional parameters and customization options. + +3. Star us on `GitHub `_ and follow us on `Twitter (@scikit_yb) `_ so that you'll hear about new visualizers as soon as they're added. + +Contributing +------------ + +Interested in contributing to Yellowbrick? Yellowbrick is a welcoming, inclusive project and we would love to have you. +We follow the `Python Software Foundation Code of Conduct `_. + +No matter your level of technical skill, you can be helpful. We appreciate bug reports, user testing, feature requests, bug fixes, product enhancements, and documentation improvements. + +Check out the :doc:`contributing/index` guide! + +If you've signed up to do user testing, head over to the :doc:`evaluation`. + +Please consider joining the `Google Groups Listserv `_ listserve so you can respond to questions. + +Thank you for your contributions! + +Concepts & API +-------------- Visualizers ----------- +The primary goal of Yellowbrick is to create a sensical API similar to Scikit-Learn. + +Visualizers are the core objects in Yellowbrick. +They are similar to transformers in Scikit-Learn. +Visualizers can wrap a model estimator - similar to how the "ModelCV" (e.g. `RidgeCV `_, `LassoCV `_) methods work. + -Visualizers are estimators (objects that learn from data) whose primary objective is to create visualizations that allow insight into the model selection process. In Scikit-Learn terms, they can be similar to transformers when visualizing the data space or wrap an model estimator similar to how the "ModelCV" (e.g. `RidgeCV `_, `LassoCV `_) methods work. The primary goal of Yellowbrick is to create a sensical API similar to Scikit-Learn. Some of our most popular visualizers include: +Some of our most popular visualizers include: Feature Visualization ~~~~~~~~~~~~~~~~~~~~~ @@ -26,8 +58,6 @@ Feature Visualization - :doc:`Radial Visualization `: separation of instances around a circular plot - :doc:`api/features/pca`: projection of instances based on principal components - :doc:`api/features/manifold`: high dimensional visualization with manifold learning -- :doc:`api/features/importances`: rank features by importance or linear coefficients for a specific model -- :doc:`api/features/rfecv`: find the best subset of features based on importance - :doc:`Joint Plots `: direct data visualization with feature selection Classification Visualization @@ -36,7 +66,7 @@ Classification Visualization - :doc:`api/classifier/class_prediction_error`: shows error and support in classification - :doc:`api/classifier/classification_report`: visual representation of precision, recall, and F1 - :doc:`ROC/AUC Curves `: receiver operator characteristics and area under the curve -- :doc:`api/classifier/prcurve`: precision vs recall for different probability thresholds +- :doc:`api/classifier/prcurve`: precision vs recall for different probability thresholds - :doc:`Confusion Matrices `: visual description of class decision making - :doc:`Discrimination Threshold `: find a threshold that best separates binary classes @@ -46,6 +76,7 @@ Regression Visualization - :doc:`api/regressor/peplot`: find model breakdowns along the domain of the target - :doc:`api/regressor/residuals`: show the difference in residuals of training and test data - :doc:`api/regressor/alphas`: show how the choice of alpha influences regularization +- :doc:`api/regressor/influence`: show the influence of instances on linear regression Clustering Visualization ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -59,6 +90,8 @@ Model Selection Visualization - :doc:`api/model_selection/validation_curve`: tune a model with respect to a single hyperparameter - :doc:`api/model_selection/learning_curve`: show if a model might benefit from more data or less complexity +- :doc:`api/model_selection/importances`: rank features by importance or linear coefficients for a specific model +- :doc:`api/model_selection/rfecv`: find the best subset of features based on importance Target Visualization ~~~~~~~~~~~~~~~~~~~~ @@ -73,20 +106,32 @@ Text Visualization - :doc:`Term Frequency `: visualize the frequency distribution of terms in the corpus - :doc:`api/text/tsne`: use stochastic neighbor embedding to project documents - :doc:`api/text/dispersion`: visualize how key terms are dispersed throughout a corpus +- :doc:`api/text/umap_vis`: plot similar documents closer together to discover clusters +- :doc:`api/text/postag`: plot the counts of different parts-of-speech throughout a tagged corpus -... and more! Visualizers are being added all the time; be sure to check the examples (or even the `develop branch `_) and feel free to contribute your ideas for new Visualizers! +... and more! Visualizers are being added all the time. Check the examples (or even the `develop branch `_). Feel free to contribute your ideas for new Visualizers! Getting Help ------------ -Yellowbrick is a welcoming, inclusive project in the tradition of matplotlib and scikit-learn. Similar to those projects, we follow the `Python Software Foundation Code of Conduct `_. Please don't hesitate to reach out to us for help or if you have any contributions or bugs to report! +Can't get someting to work? Here are places you can find help. + +1. The docs (you're here!). +2. `Stack Overflow `_. If you ask a question, please tag it with "yellowbrick". +3. The Yellowbrick `Google Groups Listserv `_. +4. You can also Tweet or direct message us on Twitter `@scikit_yb `_. + + +Find a Bug? +----------- + +Check if there's already an open `issue `_ on the topic. If needed, file an `issue `_. -The primary way to ask for help with Yellowbrick is to post on our `Google Groups Listserv `_. This is an email list/forum that members of the community can join and respond to each other; you should be able to receive the quickest response here. Please also consider joining the group so you can respond to questions! You can also ask questions on `Stack Overflow `_ and tag them with "yellowbrick". Or you can add issues on GitHub. You can also tweet or direct message us on Twitter `@scikit_yb `_. Open Source ----------- -The Yellowbrick `license `_ is an open source `Apache 2.0 `_ license. Yellowbrick enjoys a very active developer community; please consider joining them and `contributing `_! +The Yellowbrick `license `_ is an open source `Apache 2.0 `_ license. Yellowbrick enjoys a very active developer community; please consider :doc:`contributing/index`! Yellowbrick is hosted on `GitHub `_. The `issues `_ and `pull requests `_ are tracked there. @@ -94,23 +139,23 @@ Yellowbrick is hosted on `GitHub NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/matplotlib.rst b/docs/matplotlib.rst index 82484c081..552b486ea 100644 --- a/docs/matplotlib.rst +++ b/docs/matplotlib.rst @@ -5,14 +5,14 @@ Effective Matplotlib Yellowbrick generates visualizations by wrapping `matplotlib `_, the most prominent Python scientific visualization library. Because of this, Yellowbrick is able to generate publication-ready images for a variety of GUI backends, image formats, and Jupyter notebooks. Yellowbrick strives to provide well-styled visual diagnostic tools and complete information. However, to customize figures or roll your own visualizers, a strong background in using matplotlib is required. -With permission, we have included part of `Chris Moffitt's `_ `Effectively Using Matplotlib `_ as a crash course into Matplotlib terminology and usage. For a complete example, please visit his excellent post on creating a visual sales analysis! Additionally we recommend `Nicolas P. Rougier's Matplotlib tutorial `_ for an in-depth dive. +With permission, we have included part of `Chris Moffitt's `_ `Effectively Using Matplotlib `_ as a crash course into Matplotlib terminology and usage. For a complete example, please visit his excellent post on creating a visual sales analysis! This post was also adapated for matplotlib's documentation, `The Lifecycle of a Plot `_, which gives another good perspective. Additionally we recommend `Nicolas P. Rougier's Matplotlib tutorial `_ for an in-depth dive. Figures and Axes ---------------- This graphic from the `matplotlib faq is gold `_. Keep it handy to understand the different terminology of a plot. -.. image:: images/matplotlib_anatomy.png +.. image:: images/matplotlib/anatomy.png Most of the terms are straightforward but the main thing to remember is that the ``Figure`` is the final image that may contain 1 or more axes. The ``Axes`` represent an individual plot. Once you understand what these are and how to access them through the object oriented API, the rest of the process starts to fall into place. @@ -64,7 +64,7 @@ Therefore you have complete control of the style and customization of a Yellowbr Creating a Custom Plot ---------------------- -.. image:: images/matplotlib_pbpython_example.png +.. image:: images/matplotlib/pbpython_example.png The first step with any visualization is to plot the data. Often the simplest way to do this is using the standard pandas plotting function (given a ``DataFrame`` called ``top_10``): @@ -173,7 +173,7 @@ For this example, we’ll draw a line showing an average and include labels show # Hide the legend ax.legend().set_visible(False) -.. image:: images/matplotlib_single.png +.. image:: images/matplotlib/single.png While this may not be the most exciting plot it does show how much power you have when following this approach. diff --git a/docs/oneliners.rst b/docs/oneliners.rst new file mode 100644 index 000000000..274978d89 --- /dev/null +++ b/docs/oneliners.rst @@ -0,0 +1,347 @@ +.. -*- mode: rst -*- + +Oneliners +========= + +Yellowbrick's quick methods are visualizers in a single line of code! + +Yellowbrick is designed to give you as much control as you would like over the plots you create, offering parameters to help you customize everything from color, size, and title to preferred evaluation or correlation measure, optional bestfit lines or histograms, and cross validation techniques. To learn more about how to customize your visualizations using those parameters, check out the :doc:`api/index`. + +But... sometimes you just want to build a plot with a single line of code! + +On this page we'll explore the Yellowbrick quick methods (aka "oneliners"), which return a fully fitted, finalized visualizer object in only a single line. + +.. note:: This page illustrates oneliners for some of our most popular visualizers for feature analysis, classification, regression, clustering, and target evaluation, but is not a comprehensive list. Nearly every Yellowbrick visualizer has an associated quick method! + +Feature Analysis +---------------- + +Rank2D +~~~~~~ + +The ``rank1d`` and ``rank2d`` plots show pairwise rankings of features to help you detect relationships. More about :doc:`api/features/rankd`. + +.. plot:: + :context: close-figs + :alt: Rank2D Quick Method + + from yellowbrick.features import rank2d + from yellowbrick.datasets import load_credit + + + X, _ = load_credit() + visualizer = rank2d(X) + +.. plot:: + :context: close-figs + :alt: Rank1D Quick Method + + from yellowbrick.features import rank1d + from yellowbrick.datasets import load_energy + + + X, _ = load_energy() + visualizer = rank1d(X, color="r") + + +Parallel Coordinates +~~~~~~~~~~~~~~~~~~~~ + +The ``parallel_coordinates`` plot is a horizontal visualization of instances, disaggregated by the features that describe them. More about :doc:`api/features/pcoords`. + +.. plot:: + :context: close-figs + :alt: Parallel Coordinates Quick Method + + from sklearn.datasets import load_wine + from yellowbrick.features import parallel_coordinates + + + X, y = load_wine(return_X_y=True) + visualizer = parallel_coordinates(X, y, normalize="standard") + + +Radial Visualization +~~~~~~~~~~~~~~~~~~~~ + +The ``radviz`` plot shows the separation of instances around a unit circle. More about :doc:`api/features/radviz`. + +.. plot:: + :context: close-figs + :alt: Radviz Quick Method + + from yellowbrick.features import radviz + from yellowbrick.datasets import load_occupancy + + + X, y = load_occupancy() + visualizer = radviz(X, y, colors=["maroon", "gold"]) + + +PCA +~~~ + +A ``pca_decomposition`` is a projection of instances based on principal components. More about :doc:`api/features/pca`. + +.. plot:: + :context: close-figs + :alt: PCA Quick Method + + from yellowbrick.datasets import load_spam + from yellowbrick.features import pca_decomposition + + + X, y = load_spam() + visualizer = pca_decomposition(X, y) + + +Manifold +~~~~~~~~ + +The ``manifold_embedding`` plot is a high dimensional visualization with manifold learning, which can show nonlinear relationships in the features. More about :doc:`api/features/manifold`. + +.. plot:: + :context: close-figs + :alt: Manifold Quick Method + + from sklearn.datasets import load_iris + from yellowbrick.features import manifold_embedding + + + X, y = load_iris(return_X_y=True) + visualizer = manifold_embedding(X, y) + + +Classification +-------------- + +Class Prediction Error +~~~~~~~~~~~~~~~~~~~~~~ + +A ``class_prediction_error`` plot illustrates the error and support in a classification as a bar chart. More about :doc:`api/classifier/class_prediction_error`. + +.. plot:: + :context: close-figs + :alt: Class Prediction Error Quick Method + + from yellowbrick.datasets import load_game + from sklearn.preprocessing import OneHotEncoder + from sklearn.ensemble import RandomForestClassifier + from yellowbrick.classifier import class_prediction_error + + + X, y = load_game() + X = OneHotEncoder().fit_transform(X) + visualizer = class_prediction_error( + RandomForestClassifier(n_estimators=10), X, y + ) + + +Classification Report +~~~~~~~~~~~~~~~~~~~~~ + +A ``classification_report`` is a visual representation of precision, recall, and F1 score. More about :doc:`api/classifier/classification_report`. + +.. plot:: + :context: close-figs + :alt: Classification Report Quick Method + + from yellowbrick.datasets import load_credit + from sklearn.ensemble import RandomForestClassifier + from yellowbrick.classifier import classification_report + + + X, y = load_credit() + visualizer = classification_report( + RandomForestClassifier(n_estimators=10), X, y + ) + + +Confusion Matrix +~~~~~~~~~~~~~~~~ + +A ``confusion_matrix`` is a visual description of per-class decision making. More about :doc:`api/classifier/confusion_matrix`. + +.. plot:: + :context: close-figs + :alt: Confusion Matrix Quick Method + + from yellowbrick.datasets import load_game + from sklearn.preprocessing import OneHotEncoder + from sklearn.linear_model import RidgeClassifier + from yellowbrick.classifier import confusion_matrix + + + X, y = load_game() + X = OneHotEncoder().fit_transform(X) + visualizer = confusion_matrix(RidgeClassifier(), X, y, cmap="Greens") + + +Precision Recall +~~~~~~~~~~~~~~~~ + +A ``precision_recall_curve`` shows the tradeoff between precision and recall for different probability thresholds. More about :doc:`api/classifier/prcurve`. + +.. plot:: + :context: close-figs + :alt: Precision Recall Quick Method + + from sklearn.naive_bayes import GaussianNB + from yellowbrick.datasets import load_occupancy + from yellowbrick.classifier import precision_recall_curve + + + X, y = load_occupancy() + visualizer = precision_recall_curve(GaussianNB(), X, y) + + +ROCAUC +~~~~~~ + +A ``roc_auc`` plot shows the receiver operator characteristics and area under the curve. More about :doc:`api/classifier/rocauc`. + +.. plot:: + :context: close-figs + :alt: ROCAUC Quick Method + + from yellowbrick.classifier import roc_auc + from yellowbrick.datasets import load_spam + from sklearn.linear_model import LogisticRegression + + + X, y = load_spam() + visualizer = roc_auc(LogisticRegression(), X, y) + + +Discrimination Threshold +~~~~~~~~~~~~~~~~~~~~~~~~ + +A ``discrimination_threshold`` plot can help find a threshold that best separates binary classes. More about :doc:`api/classifier/threshold`. + +.. plot:: + :context: close-figs + :alt: Discrimination Threshold Quick Method + + from yellowbrick.classifier import discrimination_threshold + from sklearn.linear_model import LogisticRegression + from yellowbrick.datasets import load_spam + + X, y = load_spam() + visualizer = discrimination_threshold( + LogisticRegression(multi_class="auto", solver="liblinear"), X, y + ) + + +Regression +---------- + +Residuals Plot +~~~~~~~~~~~~~~ + +A ``residuals_plot`` shows the difference in residuals between the training and test data. More about :doc:`api/regressor/residuals`. + +.. plot:: + :context: close-figs + :alt: Residuals Quick Method + + from sklearn.linear_model import Ridge + from yellowbrick.datasets import load_concrete + from yellowbrick.regressor import residuals_plot + + + X, y = load_concrete() + visualizer = residuals_plot( + Ridge(), X, y, train_color="maroon", test_color="gold" + ) + +Prediction Error +~~~~~~~~~~~~~~~~ + +A ``prediction_error`` helps find where the regression is making the most errors. More about :doc:`api/regressor/peplot`. + +.. plot:: + :context: close-figs + :alt: Prediction Error Quick Method + + from sklearn.linear_model import Lasso + from yellowbrick.datasets import load_bikeshare + from yellowbrick.regressor import prediction_error + + + X, y = load_bikeshare() + visualizer = prediction_error(Lasso(), X, y) + + +Cooks Distance +~~~~~~~~~~~~~~ + +A ``cooks_distance`` plot shows the influence of instances on linear regression. More about :doc:`api/regressor/influence`. + +.. plot:: + :context: close-figs + :alt: Cooks Distance Quick Method + + from sklearn.datasets import load_diabetes + from yellowbrick.regressor import cooks_distance + + + X, y = load_diabetes(return_X_y=True) + visualizer = cooks_distance(X, y) + + +Clustering +---------- + +Silhouette Scores +~~~~~~~~~~~~~~~~~ + +A ``silhouette_visualizer`` can help you select ``k`` by visualizing silhouette coefficient values. More about :doc:`api/cluster/silhouette`. + +.. plot:: + :context: close-figs + :alt: Silhouette Scores Quick Method + + from sklearn.cluster import KMeans + from yellowbrick.datasets import load_nfl + from yellowbrick.cluster import silhouette_visualizer + + X, y = load_nfl() + visualizer = silhouette_visualizer(KMeans(5, random_state=42), X) + + +Intercluster Distance +~~~~~~~~~~~~~~~~~~~~~ + +A ``intercluster_distance`` shows size and relative distance between clusters. More about :doc:`api/cluster/icdm`. + +.. plot:: + :context: close-figs + :alt: ICDM Quick Method + + from yellowbrick.datasets import load_nfl + from sklearn.cluster import MiniBatchKMeans + from yellowbrick.cluster import intercluster_distance + + + X, y = load_nfl() + visualizer = intercluster_distance(MiniBatchKMeans(5, random_state=777), X) + + +Target Analysis +--------------- + +ClassBalance +~~~~~~~~~~~~ + +The ``class_balance`` plot can make it easier to see how the distribution of classes may affect the model. More about :doc:`api/target/class_balance`. + +.. plot:: + :context: close-figs + :alt: ClassBalance Quick Method + + from yellowbrick.datasets import load_game + from yellowbrick.target import class_balance + + + X, y = load_game() + visualizer = class_balance(y, labels=["draw", "loss", "win"]) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 957e95c6c..96553f382 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -5,16 +5,16 @@ Quick Start If you're new to Yellowbrick, this guide will get you started and help you include visualizers in your machine learning workflow. Before we begin, however, there are several notes about development environments that you should consider. -Yellowbrick has two primary dependencies: `scikit-learn `_ and `matplotlib `_. If you do not have these Python packages, they will be installed alongside Yellowbrick. Note that Yellowbrick works best with scikit-learn version 0.18 or later and matplotlib version 2.0 or later. Both of these packages require some C code to be compiled, which can be difficult on some systems, like Windows. If you're having trouble, try using a distribution of Python that includes these packages like `Anaconda `_. +Yellowbrick has two primary dependencies: `scikit-learn `_ and `matplotlib `_. If you do not have these Python packages, they will be installed alongside Yellowbrick. Note that Yellowbrick works best with scikit-learn version 0.20 or later and matplotlib version 3.0.1 or later. Both of these packages require some C code to be compiled, which can be difficult on some systems, like Windows. If you're having trouble, try using a distribution of Python that includes these packages like `Anaconda `_. Yellowbrick is also commonly used inside of a `Jupyter Notebook `_ alongside `Pandas `_ data frames. Notebooks make it especially easy to coordinate code and visualizations; however, you can also use Yellowbrick inside of regular Python scripts, either saving figures to disk or showing figures in a GUI window. If you're having trouble with this, please consult matplotlib's `backends documentation `_. -.. NOTE:: Jupyter, Pandas, and other ancillary libraries like NLTK for text visualizers are not installed with Yellowbrick and must be installed separately. +.. NOTE:: Jupyter, Pandas, and other ancillary libraries like the Natural Language Toolkit (NLTK) for text visualizers are not installed with Yellowbrick and must be installed separately. Installation ------------ -Yellowbrick is compatible with Python 2.7 or later, but it is preferred to use Python 3.5 or later to take full advantage of all functionality. The simplest way to install Yellowbrick is from PyPI_ with pip_, Python's preferred package installer. +Yellowbrick is a Python 3 package and works well with 3.4 or later. The simplest way to install Yellowbrick is from PyPI_ with pip_, Python's preferred package installer. .. code-block:: bash @@ -23,7 +23,7 @@ Yellowbrick is compatible with Python 2.7 or later, but it is preferred to use P .. _PyPI: https://pypi.python.org/pypi/yellowbrick .. _pip: https://docs.python.org/3/installing/ -Note that Yellowbrick is an active project and routinely publishes new releases with more visualizers and updates. In order to upgrade Yellowbrick to the latest version, use pip as follows. +Note that Yellowbrick is an active project and routinely publishes new releases with more visualizers and updates. In order to upgrade Yellowbrick to the latest version, use ``pip`` as follows. .. code-block:: bash @@ -37,12 +37,13 @@ If you're using Anaconda, you can take advantage of the `conda `_ installing matplotlib on Linux with Anaconda. If you're having trouble please let us know on GitHub. +If you're having trouble with installation, please let us know on GitHub. -Once installed, you should be able to import Yellowbrick without an error, both in Python and inside of Jupyter notebooks. Note that because of matplotlib, Yellowbrick does not work inside of a virtual environment without jumping through some hoops. +Once installed, you should be able to import Yellowbrick without an error, both in Python and inside of Jupyter notebooks. Note that because of matplotlib, Yellowbrick does not work inside of a virtual environment on macOS without jumping through some hoops. Using Yellowbrick ----------------- + The Yellowbrick API is specifically designed to play nicely with scikit-learn. The primary interface is therefore a ``Visualizer`` -- an object that learns from data to produce a visualization. Visualizers are scikit-learn `Estimator `_ objects and have a similar interface along with methods for drawing. In order to use visualizers, you simply use the same workflow as with a scikit-learn model, import the visualizer, instantiate it, call the visualizer's ``fit()`` method, then in order to render the visualization, call the visualizer's ``poof()`` method, which does the magic! For example, there are several visualizers that act as transformers, used to perform feature analysis prior to fitting a model. The following example visualizes a high-dimensional data set with parallel coordinates: @@ -63,9 +64,14 @@ The ``poof()`` method finalizes the drawing (adding titles, axes labels, etc) an visualizer.poof(outpath="pcoords.png") -The extension of the filename will determine how the image is rendered. In addition to the .png extension, .pdf is also commonly used. +The extension of the filename will determine how the image is rendered. In addition to the ``.png`` extension, ``.pdf`` is also commonly used for high-quality publication ready images. + +.. NOTE:: -.. NOTE:: Data input to Yellowbrick is identical to that of scikit-learn: a dataset, ``X``, which is a two-dimensional matrix of shape ``(n,m)`` where ``n`` is the number of instances (rows) and ``m`` is the number of features (columns). The dataset ``X`` can be a Pandas DataFrame, a NumPy array, or even a Python list of lists. Optionally, a vector ``y``, which represents the target variable (in supervised learning), can also be supplied as input. The target ``y`` must have length ``n`` -- the same number of elements as rows in ``X`` and can be a Pandas Series, NumPy array, or Python list. + Data input to Yellowbrick is identical to that of scikit-learn. Datasets are + usually described with a variable ``X`` (sometimes referred to simply as data) and an optional variable ``y`` (usually referred to as the target). The required data ``X`` is a table that contains instances (or samples) which are described by features. ``X`` is therefore a *two-dimensional matrix* with a shape of ``(n, m)`` where ``n`` is the number of instances (rows) and ``m`` is the number of features (columns). ``X`` can be a Pandas DataFrame, a NumPy array, or even a Python lists of lists. + + The optional target data, ``y``, is used to specify the ground truth in supervised machine learning. ``y`` is a vector (a one-dimensional array) that must have length ``n`` -- the same number of elements as rows in ``X``. ``y`` can be a Pandas Series, a Numpy array, or a Python list. Visualizers can also wrap scikit-learn models for evaluation, hyperparameter tuning and algorithm selection. For example, to produce a visual heatmap of a classification report, displaying the precision, recall, F1 score, and support for each class in a classifier, wrap the estimator in a visualizer as follows: @@ -105,26 +111,37 @@ These quick functions give you slightly less control over the machine learning w Walkthrough ----------- -Consider a regression analysis as a simple example of the use of visualizers in the machine learning workflow. Using a `bike sharing dataset `_ based upon the one uploaded to the `UCI Machine Learning Repository `_, we would like to predict the number of bikes rented in a given hour based on features like the season, weather, or if it's a holiday. +Let's consider a regression analysis as a simple example of the use of visualizers in the machine learning workflow. Using a bike sharing dataset based upon the one uploaded to the `UCI Machine Learning Repository `_, we would like to predict the number of bikes rented in a given hour based on features like the season, weather, or if it's a holiday. -.. note:: We have updated the dataset from the UCI ML repository to make it a bit easier to load into Pandas; make sure you download the `Yellowbrick version of the dataset `_. +.. NOTE:: We have updated the dataset from the UCI ML repository to make it a bit easier to load into Pandas; make sure you download the Yellowbrick version of the dataset using the ``load_bikeshare`` method below. Please also note that Pandas is required to follow the supplied code. Pandas can be installed using ``pip install pandas`` if you haven't already installed it. -After downloading the dataset and unzipping it in your current working directory, we can load our data as follows: +We can load our data using the ``yellowbrick.datasets`` module as follows: .. code-block:: python import pandas as pd + from yellowbrick.datasets import load_bikeshare + + X, y = load_bikeshare() + print(X.head()) - data = pd.read_csv('bikeshare.csv') - X = data[[ - "season", "month", "hour", "holiday", "weekday", "workingday", - "weather", "temp", "feelslike", "humidity", "windspeed" - ]] - y = data["riders"] +This prints out the first couple lines of our dataset which looks like:: -The machine learning workflow is the art of creating *model selection triples*, a combination of features, algorithm, and hyperparameters that uniquely identifies a model fitted on a specific data set. As part of our feature selection, we want to identify features that have a linear relationship with each other, potentially introducing covariance into our model and breaking OLS (guiding us toward removing features or using regularization). We can use the Rank2D_ visualizer to compute Pearson correlations between all pairs of features as follows: + season year month hour holiday weekday workingday weather temp \ + 0 1 0 1 0 0 6 0 1 0.24 + 1 1 0 1 1 0 6 0 1 0.22 + 2 1 0 1 2 0 6 0 1 0.22 + 3 1 0 1 3 0 6 0 1 0.24 + 4 1 0 1 4 0 6 0 1 0.24 -.. _Rank2D: http://www.scikit-yb.org/en/latest/api/yellowbrick.features.html#module-yellowbrick.features.rankd + feelslike humidity windspeed + 0 0.2879 0.81 0.0 + 1 0.2727 0.80 0.0 + 2 0.2727 0.80 0.0 + 3 0.2879 0.75 0.0 + 4 0.2879 0.75 0.0 + +The machine learning workflow is the art of creating *model selection triples*, a combination of features, algorithm, and hyperparameters that uniquely identifies a model fitted on a specific data set. As part of our feature selection, we want to identify features that have a linear relationship with each other, potentially introducing covariance into our model and breaking OLS (guiding us toward removing features or using regularization). We can use the :doc:`api/features/rankd` visualizer to compute Pearson correlations between all pairs of features as follows: .. code-block:: python @@ -134,21 +151,45 @@ The machine learning workflow is the art of creating *model selection triples*, visualizer.fit_transform(X) visualizer.poof() -.. image:: images/quickstart/bikeshare_rank2d.png +.. plot:: + :include-source: False + :context: close-figs + :alt: Rank2D of Bikeshare Features + + import matplotlib.pyplot as plt + from yellowbrick.features import Rank2D + from yellowbrick.datasets import load_bikeshare + + X, y = load_bikeshare() + visualizer = Rank2D(algorithm="pearson") + visualizer.fit_transform(X) + visualizer.poof() + plt.tight_layout() This figure shows us the Pearson correlation between pairs of features such that each cell in the grid represents two features identified in order on the x and y axes and whose color displays the magnitude of the correlation. A Pearson correlation of 1.0 means that there is a strong positive, linear relationship between the pairs of variables and a value of -1.0 indicates a strong negative, linear relationship (a value of zero indicates no relationship). Therefore we are looking for dark red and dark blue boxes to identify further. -In this chart, we see that the features ``temp`` and ``feelslike`` have a strong correlation and also that the feature ``season`` has a strong correlation with the feature ``month``. This seems to make sense; the apparent temperature we feel outside depends on the actual temperature and other airquality factors, and the season of the year is described by the month! To dive in deeper, we can use the `JointPlotVisualizer `_ to inspect those relationships. +In this chart, we see that the features ``temp`` and ``feelslike`` have a strong correlation and also that the feature ``season`` has a strong correlation with the feature ``month``. This seems to make sense; the apparent temperature we feel outside depends on the actual temperature and other airquality factors, and the season of the year is described by the month! To dive in deeper, we can use the :doc:`api/features/jointplot` (``JointPlotVisualizer``) to inspect those relationships. .. code-block:: python from yellowbrick.features import JointPlotVisualizer - visualizer = JointPlotVisualizer(feature='temp', target='feelslike') - visualizer.fit(X['temp'], X['feelslike']) + visualizer = JointPlotVisualizer(columns=['temp', 'feelslike']) + visualizer.fit_transform(X, y) visualizer.poof() -.. image:: images/quickstart/temp_feelslike_jointplot.png +.. plot:: + :include-source: False + :context: close-figs + :alt: JointPlot of temp vs feelslike + + from yellowbrick.features import JointPlotVisualizer + from yellowbrick.datasets import load_bikeshare + + X, y = load_bikeshare() + visualizer = JointPlotVisualizer(columns=['temp', 'feelslike']) + visualizer.fit_transform(X, y) + visualizer.poof() This visualizer plots a scatter diagram of the apparent temperature on the y axis and the actual measured temperature on the x axis and draws a line of best fit using a simple linear regression. Additionally, univariate distributions are shown as histograms above the x axis for temp and next to the y axis for feelslike. The ``JointPlotVisualizer`` gives an at-a-glance view of the very strong positive correlation of the features, as well as the range and distribution of each feature. Note that the axes are normalized to the space between zero and one, a common technique in machine learning to reduce the impact of one feature over another. @@ -176,9 +217,29 @@ At this point, we can train our model; let's fit a linear regression to our mode visualizer.score(X_test, y_test) visualizer.poof() -.. image:: images/quickstart/bikeshare_ols_residuals.png +.. plot:: + :include-source: False + :context: close-figs + :alt: ResidualsPlot of a simple LinearRegression -The residuals plot shows the error against the predicted value (the number of riders), and allows us to look for heteroskedasticity in the model; e.g. regions in the target where the error is greatest. The shape of the residuals can strongly inform us where OLS (ordinary least squares) is being most strongly affected by the components of our model (the features). In this case, we can see that the lower predicted number of riders results in lower model error, and conversely that the the higher predicted number of riders results in higher model error. This indicates that our model has more noise in certain regions of the target or that two variables are colinear, meaning that they are injecting error as the noise in their relationship changes. + from yellowbrick.datasets import load_bikeshare + from yellowbrick.regressor import ResidualsPlot + from sklearn.linear_model import LinearRegression + from sklearn.model_selection import train_test_split + + X, y = load_bikeshare() + + # Create training and test sets + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.1 + ) + + visualizer = ResidualsPlot(LinearRegression()) + visualizer.fit(X_train, y_train) + visualizer.score(X_test, y_test) + visualizer.poof() + +The residuals plot shows the error against the predicted value (the number of riders), and allows us to look for heteroskedasticity in the model; e.g. regions in the target where the error is greatest. The shape of the residuals can strongly inform us where OLS (ordinary least squares) is being most strongly affected by the components of our model (the features). In this case, we can see that the lower predicted number of riders results in lower model error, and conversely that the the higher predicted number of riders results in higher model error. This indicates that our model has more noise in certain regions of the target or that two variables are colinear, meaning that they are injecting error as the noise in their relationship changes. The residuals plot also shows how the model is injecting error, the bold horizontal line at ``residuals = 0`` is no error, and any point above or below that line indicates the magnitude of error. For example, most of the residuals are negative, and since the score is computed as ``actual - expected``, this means that the expected value is bigger than the actual value most of the time; e.g. that our model is primarily guessing more than the actual number of riders. Moreover, there is a very interesting boundary along the top right of the residuals graph, indicating an interesting effect in model space; possibly that some feature is strongly weighted in the region of that model. @@ -198,7 +259,22 @@ Along with generating the residuals plot, we also measured the performance by "s visualizer.fit(X, y) visualizer.poof() -.. image:: images/quickstart/bikeshare_ridge_alphas.png +.. plot:: + :include-source: False + :context: close-figs + :alt: AlphaSelection for L2 Regularization using RidgeCV + + import numpy as np + from yellowbrick.datasets import load_bikeshare + from sklearn.linear_model import RidgeCV + from yellowbrick.regressor import AlphaSelection + + X, y = load_bikeshare() + + alphas = np.logspace(-10, 1, 200) + visualizer = AlphaSelection(RidgeCV(alphas=alphas)) + visualizer.fit(X, y) + visualizer.poof() When exploring model families, the primary thing to consider is how the model becomes more *complex*. As the model increases in complexity, the error due to variance increases because the model is becoming more overfit and cannot generalize to unseen data. However, the simpler the model is the more error there is likely to be due to bias; the model is underfit and therefore misses its target more frequently. The goal therefore of most machine learning is to create a model that is *just complex enough*, finding a middle ground between bias and variance. @@ -218,8 +294,27 @@ We can now train our final model and visualize it with the ``PredictionError`` v visualizer.score(X_test, y_test) visualizer.poof() -.. image:: images/quickstart/bikeshare_ridge_prediction_error.png +.. plot:: + :include-source: False + :context: close-figs + :alt: PredictionError for L2 Regularization using Ridge + + from yellowbrick.datasets import load_bikeshare + from sklearn.linear_model import Ridge + from yellowbrick.regressor import PredictionError + from sklearn.model_selection import train_test_split + + X, y = load_bikeshare() + # Create training and test sets + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.1 + ) + + visualizer = PredictionError(Ridge(alpha=3.181)) + visualizer.fit(X_train, y_train) + visualizer.score(X_test, y_test) + visualizer.poof() The prediction error visualizer plots the actual (measured) vs. expected (predicted) values against each other. The dotted black line is the 45 degree line that indicates zero error. Like the residuals plot, this allows us to see where error is occurring and in what magnitude. diff --git a/docs/requirements.txt b/docs/requirements.txt index bc5a0d332..0ac9e363c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,11 +1,11 @@ # Library Dependencies -matplotlib>=1.5.1,!=3.0.0 -scipy>=0.19 -scikit-learn>=0.19 +matplotlib>=3.0.2,!=3.1.1 +scipy>=1.0.0 +scikit-learn>=0.20 numpy>=1.13.0 cycler>=0.10.0 # Documentation Dependencies -Sphinx>=1.7.5 -sphinx-rtd-theme>=0.4.0 -numpydoc>=0.8.0 +Sphinx>=2.1 +sphinx-rtd-theme>=0.4.3 +numpydoc>=0.9.0 diff --git a/docs/teaching.rst b/docs/teaching.rst index a9cd2063d..b59eabf44 100644 --- a/docs/teaching.rst +++ b/docs/teaching.rst @@ -18,7 +18,7 @@ The following slide deck presents an approach to teaching students about the mac .. raw:: html - + Teachers are welcome to `download the slides `_ via SlideShare as a PowerPoint deck, and to add them to their course materials to assist in teaching these important concepts. diff --git a/docs/tutorial.py b/docs/tutorial.py index b49f67962..b1f98544a 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -1,111 +1,89 @@ #!/usr/bin/env python # Generate the classification report images for the tutorial -import os -import pandas as pd import matplotlib.pyplot as plt -from yellowbrick.classifier import ClassificationReport - from sklearn.pipeline import Pipeline -from sklearn.base import BaseEstimator, TransformerMixin -from sklearn.preprocessing import LabelEncoder, OneHotEncoder - from sklearn.svm import LinearSVC, NuSVC, SVC from sklearn.neighbors import KNeighborsClassifier +from sklearn.preprocessing import OneHotEncoder, LabelEncoder from sklearn.linear_model import LogisticRegressionCV, LogisticRegression, SGDClassifier -from sklearn.ensemble import BaggingClassifier, ExtraTreesClassifier, RandomForestClassifier - - -DATA = os.path.join( - os.path.dirname(__file__), "..", "examples", "data", "mushroom", "mushroom.csv" +from sklearn.ensemble import ( + BaggingClassifier, + ExtraTreesClassifier, + RandomForestClassifier, ) +from yellowbrick.datasets import load_mushroom +from yellowbrick.classifier import ClassificationReport + ESTIMATORS = { - LinearSVC: "images/tutorial/modelselect_linear_svc.png", - NuSVC: "images/tutorial/modelselect_nu_svc.png", - SVC: "images/tutorial/modelselect_svc.png", - SGDClassifier: "images/tutorial/modelselect_sgd_classifier.png", - KNeighborsClassifier: "images/tutorial/modelselect_kneighbors_classifier.png", - LogisticRegressionCV: "images/tutorial/modelselect_logistic_regression_cv.png", - LogisticRegression: "images/tutorial/modelselect_logistic_regression.png", - BaggingClassifier: "images/tutorial/modelselect_bagging_classifier.png", - ExtraTreesClassifier: "images/tutorial/modelselect_extra_trees_classifier.png", - RandomForestClassifier: "images/tutorial/modelselect_random_forest_classifier.png", + "SVC": {"model": SVC(gamma="auto"), "path": "images/tutorial/modelselect_svc.png"}, + "NuSVC": { + "model": NuSVC(gamma="auto"), + "path": "images/tutorial/modelselect_nu_svc.png", + }, + "LinearSVC": { + "model": LinearSVC(), + "path": "images/tutorial/modelselect_linear_svc.png", + }, + "SGD": { + "model": SGDClassifier(max_iter=100, tol=1e-3), + "path": "images/tutorial/modelselect_sgd_classifier.png", + }, + "KNN": { + "model": KNeighborsClassifier(), + "path": "images/tutorial/modelselect_kneighbors_classifier.png", + }, + "LR": { + "model": LogisticRegression(solver="lbfgs"), + "path": "images/tutorial/modelselect_logistic_regression.png", + }, + "LRCV": { + "model": LogisticRegressionCV(cv=3), + "path": "images/tutorial/modelselect_logistic_regression_cv.png", + }, + "Bags": { + "model": BaggingClassifier(), + "path": "images/tutorial/modelselect_bagging_classifier.png", + }, + "XTrees": { + "model": ExtraTreesClassifier(n_estimators=100), + "path": "images/tutorial/modelselect_extra_trees_classifier.png", + }, + "RF": { + "model": RandomForestClassifier(n_estimators=100), + "path": "images/tutorial/modelselect_random_forest_classifier.png", + }, } - -class EncodeCategorical(BaseEstimator, TransformerMixin): - """ - Encodes a specified list of columns or all columns if None. - """ - - def __init__(self, columns=None): - self.columns = [col for col in columns] - self.encoders = None - - def fit(self, data, target=None): - """ - Expects a data frame with named columns to encode. - """ - # Encode all columns if columns is None - if self.columns is None: - self.columns = data.columns - - # Fit a label encoder for each column in the data frame - self.encoders = { - column: LabelEncoder().fit(data[column]) - for column in self.columns - } - return self - - def transform(self, data): - """ - Uses the encoders to transform a data frame. - """ - output = data.copy() - for column, encoder in self.encoders.items(): - output[column] = encoder.transform(data[column]) - - return output - - -def load_data(path=DATA): - dataset = pd.read_csv(path) - features = ['shape', 'surface', 'color'] - target = ['target'] - - X = dataset[features] - y = dataset[target] - - y = LabelEncoder().fit_transform(y.values.ravel()) - - return X, y - - -def visual_model_selection(X, y, estimator, path): +def visualize_model(X, y, estimator, path, **kwargs): """ Test various estimators. """ - model = Pipeline([ - ('label_encoding', EncodeCategorical(X.keys())), - ('one_hot_encoder', OneHotEncoder()), - ('estimator', estimator) - ]) + y = LabelEncoder().fit_transform(y) + model = Pipeline([("one_hot_encoder", OneHotEncoder()), ("estimator", estimator)]) _, ax = plt.subplots() # Instantiate the classification model and visualizer - visualizer = ClassificationReport(model, ax=ax, classes=['edible', 'poisonous']) + visualizer = ClassificationReport( + model, + classes=["edible", "poisonous"], + cmap="YlGn", + size=(600, 360), + ax=ax, + **kwargs + ) visualizer.fit(X, y) visualizer.score(X, y) visualizer.poof(outpath=path) -if __name__ == '__main__': - X, y = load_data() +if __name__ == "__main__": + X, y = load_mushroom() - for clf, path in ESTIMATORS.items(): - visual_model_selection(X, y, clf(), path) + for clf in ESTIMATORS.values(): + visualize_model(X, y, clf["model"], clf["path"]) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 80dc12f9f..ebebd930e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -3,10 +3,7 @@ Model Selection Tutorial ======================== -In this tutorial, we are going to look at scores for a variety of -`Scikit-Learn `__ models and compare them using -visual diagnostic tools from `Yellowbrick `__ -in order to select the best model for our data. +In this tutorial, we are going to look at scores for a variety of `Scikit-Learn `__ models and compare them using visual diagnostic tools from `Yellowbrick `__ in order to select the best model for our data. The Model Selection Triple -------------------------- @@ -29,112 +26,54 @@ The Yellowbrick library is a diagnostic visualization platform for machine learn About the Data -------------- -This tutorial uses a modified version of the mushroom dataset_ from -the `UCI Machine Learning Repository `__. -Our objective is to predict if a mushroom is poisonous or edible based on -its characteristics. +This tutorial uses the mushrooms data from the Yellowbrick :doc:`api/datasets/index` module. Our objective is to predict if a mushroom is poisonous or edible based on its characteristics. -.. _dataset: https://github.com/rebeccabilbro/rebeccabilbro.github.io/blob/master/data/agaricus-lepiota.txt +.. NOTE:: The YB version of the mushrooms data differs from the mushroom dataset from the `UCI Machine Learning Repository `__. The Yellowbrick version has been deliberately modified to make modeling a bit more of a challenge. -The data include descriptions of hypothetical samples corresponding to -23 species of gilled mushrooms in the Agaricus and Lepiota Family. Each -species was identified as definitely edible, definitely poisonous, or of -unknown edibility and not recommended (this latter class was combined -with the poisonous one). +The data include descriptions of hypothetical samples corresponding to 23 species of gilled mushrooms in the Agaricus and Lepiota Family. Each species was identified as definitely edible, definitely poisonous, or of unknown edibility and not recommended (this latter class was combined with the poisonous one). -Our file, "agaricus-lepiota.txt," contains information for 3 nominally -valued attributes and a target value from 8124 instances of mushrooms -(4208 edible, 3916 poisonous). +Our data contains information for 3 nominally valued attributes and a target value from 8124 instances of mushrooms (4208 edible, 3916 poisonous). -Let's load the data with Pandas. +Let's load the data: .. code:: python - import os - import pandas as pd + from yellowbrick.datasets import load_mushroom - names = [ - 'class', - 'cap-shape', - 'cap-surface', - 'cap-color' - ] - - mushrooms = os.path.join('data','agaricus-lepiota.txt') - dataset = pd.read_csv(mushrooms) - dataset.columns = names - dataset.head() - -= ========= ========= =========== ========= -. class cap-shape cap-surface cap-color -= ========= ========= =========== ========= -0 edible bell smooth white -1 poisonous convex scaly white -2 edible convex smooth gray -3 edible convex scaly yellow -4 edible bell smooth white -= ========= ========= =========== ========= + X, y = load_mushroom() + print(X[:5]) # inspect the first five rows -.. code:: python +.. parsed-literal:: - features = ['cap-shape', 'cap-surface', 'cap-color'] - target = ['class'] + shape surface color + 0 convex smooth yellow + 1 bell smooth white + 2 convex scaly white + 3 convex smooth gray + 4 convex scaly yellow - X = dataset[features] - y = dataset[target] Feature Extraction ------------------ -Our data, including the target, is categorical. We will need to change -these values to numeric ones for machine learning. In order to extract -this from the dataset, we'll have to use Scikit-Learn transformers to -transform our input dataset into something that can be fit to a model. -Luckily, Sckit-Learn does provide a transformer for converting -categorical labels into numeric integers: -`sklearn.preprocessing.LabelEncoder `__. -Unfortunately it can only transform a single vector at a time, so we'll -have to adapt it in order to apply it to multiple columns. +Our data, including the target, is categorical. We will need to change these values to numeric ones for machine learning. In order to extract this from the dataset, we'll have to use scikit-learn transformers to transform our input dataset into something that can be fit to a model. Luckily, scikit-learn does provide transformers for converting categorical labels into numeric integers: +`sklearn.preprocessing.LabelEncoder `__ and `sklearn.preprocessing.OneHotEncoder `__. + +We'll use a combination of scikit-learn's ``Pipeline`` object (`here's `__ a great post on using pipelines by `Zac Stewart `__), ``OneHotEncoder``, and ``LabelEncoder``: .. code:: python - from sklearn.base import BaseEstimator, TransformerMixin - from sklearn.preprocessing import LabelEncoder, OneHotEncoder + from sklearn.pipeline import Pipeline + from sklearn.preprocessing import OneHotEncoder, LabelEncoder + # Label-encode targets before modeling + y = LabelEncoder().fit_transform(y) - class EncodeCategorical(BaseEstimator, TransformerMixin): - """ - Encodes a specified list of columns or all columns if None. - """ - - def __init__(self, columns=None): - self.columns = [col for col in columns] - self.encoders = None - - def fit(self, data, target=None): - """ - Expects a data frame with named columns to encode. - """ - # Encode all columns if columns is None - if self.columns is None: - self.columns = data.columns - - # Fit a label encoder for each column in the data frame - self.encoders = { - column: LabelEncoder().fit(data[column]) - for column in self.columns - } - return self - - def transform(self, data): - """ - Uses the encoders to transform a data frame. - """ - output = data.copy() - for column, encoder in self.encoders.items(): - output[column] = encoder.transform(data[column]) - - return output + # One-hot encode columns before modeling + model = Pipeline([ + ('one_hot_encoder', OneHotEncoder()), + ('estimator', estimator) + ]) Modeling and Evaluation ----------------------- @@ -174,314 +113,121 @@ diagnostics from the Yellowbrick library). from sklearn.metrics import f1_score from sklearn.pipeline import Pipeline + from sklearn.svm import LinearSVC, NuSVC, SVC + from sklearn.neighbors import KNeighborsClassifier + from sklearn.preprocessing import OneHotEncoder, LabelEncoder + from sklearn.linear_model import LogisticRegressionCV, LogisticRegression, SGDClassifier + from sklearn.ensemble import BaggingClassifier, ExtraTreesClassifier, RandomForestClassifier - def model_selection(X, y, estimator): + models = [ + SVC(gamma='auto'), NuSVC(gamma='auto'), LinearSVC(), + SGDClassifier(max_iter=100, tol=1e-3), KNeighborsClassifier(), + LogisticRegression(solver='lbfgs'), LogisticRegressionCV(cv=3), + BaggingClassifier(), ExtraTreesClassifier(n_estimators=300), + RandomForestClassifier(n_estimators=300) + ] + + + def score_model(X, y, estimator, **kwargs): """ Test various estimators. """ - y = LabelEncoder().fit_transform(y.values.ravel()) + y = LabelEncoder().fit_transform(y) model = Pipeline([ - ('label_encoding', EncodeCategorical(X.keys())), - ('one_hot_encoder', OneHotEncoder()), - ('estimator', estimator) + ('one_hot_encoder', OneHotEncoder()), + ('estimator', estimator) ]) # Instantiate the classification model and visualizer - model.fit(X, y) + model.fit(X, y, **kwargs) expected = y predicted = model.predict(X) - # Compute and return the F1 score (the harmonic mean of precision and recall) - return (f1_score(expected, predicted)) - -.. code:: python - - # Try them all! - from sklearn.svm import LinearSVC, NuSVC, SVC - from sklearn.neighbors import KNeighborsClassifier - from sklearn.linear_model import LogisticRegressionCV, LogisticRegression, SGDClassifier - from sklearn.ensemble import BaggingClassifier, ExtraTreesClassifier, RandomForestClassifier - -.. code:: python - - model_selection(X, y, LinearSVC()) - - - - -.. parsed-literal:: - - 0.65846308387744845 - - - -.. code:: python - - model_selection(X, y, NuSVC()) - - - - -.. parsed-literal:: - - 0.63838842388991346 - - - -.. code:: python - - model_selection(X, y, SVC()) - - - - -.. parsed-literal:: - - 0.66251459711950167 - - - -.. code:: python - - model_selection(X, y, SGDClassifier()) - - - - -.. parsed-literal:: - - 0.69944182052382997 - - - -.. code:: python - - model_selection(X, y, KNeighborsClassifier()) - - - - -.. parsed-literal:: - - 0.65802139037433149 - - - -.. code:: python - - model_selection(X, y, LogisticRegressionCV()) - - - - -.. parsed-literal:: - - 0.65846308387744845 - - - -.. code:: python - - model_selection(X, y, LogisticRegression()) - - - - -.. parsed-literal:: - - 0.65812609897010799 - - - -.. code:: python - - model_selection(X, y, BaggingClassifier()) - + # Compute and return F1 (harmonic mean of precision and recall) + print("{}: {}".format(estimator.__class__.__name__, f1_score(expected, predicted))) + for model in models: + score_model(X, y, model) .. parsed-literal:: - 0.687643484132343 - - - -.. code:: python - - model_selection(X, y, ExtraTreesClassifier()) - - - - -.. parsed-literal:: - - 0.68713648045448383 - - - -.. code:: python - - model_selection(X, y, RandomForestClassifier()) - - - - -.. parsed-literal:: - - 0.69317131158367451 - + SVC: 0.6624286455630514 + NuSVC: 0.6726016476215785 + LinearSVC: 0.6583804143126177 + SGDClassifier: 0.5582697992842696 + KNeighborsClassifier: 0.6581185045215279 + LogisticRegression: 0.6580434509606933 + LogisticRegressionCV: 0.6583804143126177 + BaggingClassifier: 0.6879633373770051 + ExtraTreesClassifier: 0.6871364804544838 + RandomForestClassifier: 0.687643484132343 Preliminary Model Evaluation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Based on the results from the F1 scores above, which model is performing -the best? +Based on the results from the F1 scores above, which model is performing the best? Visual Model Evaluation ----------------------- -Now let's refactor our model evaluation function to use Yellowbrick's -``ClassificationReport`` class, a model visualizer that displays the -precision, recall, and F1 scores. This visual model analysis tool -integrates numerical scores as well as color-coded heatmaps in order to -support easy interpretation and detection, particularly the nuances of -Type I and Type II error, which are very relevant (lifesaving, even) to -our use case! - -**Type I error** (or a **"false positive"**) is detecting an effect that -is not present (e.g. determining a mushroom is poisonous when it is in -fact edible). +Now let's refactor our model evaluation function to use Yellowbrick's ``ClassificationReport`` class, a model visualizer that displays the precision, recall, and F1 scores. This visual model analysis tool integrates numerical scores as well as color-coded heatmaps in order to support easy interpretation and detection, particularly the nuances of Type I and Type II error, which are very relevant (lifesaving, even) to our use case! -**Type II error** (or a **"false negative"**) is failing to detect an -effect that is present (e.g. believing a mushroom is edible when it is -in fact poisonous). +**Type I error** (or a **"false positive"**) is detecting an effect that is not present (e.g. determining a mushroom is poisonous when it is in fact edible). -.. note:: When running in a Jupyter Notebook, be sure to add the following line at the top of the notebook: ``%matplotlib notebook``. This will ensure the figures are rendered correctly. For those running this code with a Python script, the figure should appear in a secondary window. +**Type II error** (or a **"false negative"**) is failing to detect an effect that is present (e.g. believing a mushroom is edible when it is in fact poisonous). .. code:: python - import matplotlib.pyplot as plt - from sklearn.pipeline import Pipeline from yellowbrick.classifier import ClassificationReport - def visual_model_selection(X, y, estimator): + def visualize_model(X, y, estimator, **kwargs): """ Test various estimators. """ - y = LabelEncoder().fit_transform(y.values.ravel()) + y = LabelEncoder().fit_transform(y) model = Pipeline([ - ('label_encoding', EncodeCategorical(X.keys())), - ('one_hot_encoder', OneHotEncoder()), - ('estimator', estimator) + ('one_hot_encoder', OneHotEncoder()), + ('estimator', estimator) ]) - # Create a new figure to draw the classification report on - _, ax = plt.subplots() - # Instantiate the classification model and visualizer visualizer = ClassificationReport( - model, ax=ax, classes=['edible', 'poisonous'] + model, classes=['edible', 'poisonous'], + cmap="YlGn", size=(600, 360), **kwargs ) visualizer.fit(X, y) visualizer.score(X, y) - - # Note that to save the figure to disk, you can specify an outpath - # argument to the poof method! visualizer.poof() - -.. code:: python - - visual_model_selection(X, y, LinearSVC()) - - - -.. image:: images/tutorial/modelselect_linear_svc.png - - -.. code:: python - - visual_model_selection(X, y, NuSVC()) - - - -.. image:: images/tutorial/modelselect_nu_svc.png - - -.. code:: python - - visual_model_selection(X, y, SVC()) + for model in models: + visualize_model(X, y, model) .. image:: images/tutorial/modelselect_svc.png +.. image:: images/tutorial/modelselect_nu_svc.png -.. code:: python - - visual_model_selection(X, y, SGDClassifier()) - - +.. image:: images/tutorial/modelselect_linear_svc.png .. image:: images/tutorial/modelselect_sgd_classifier.png - -.. code:: python - - visual_model_selection(X, y, KNeighborsClassifier()) - - - .. image:: images/tutorial/modelselect_kneighbors_classifier.png - -.. code:: python - - visual_model_selection(X, y, LogisticRegressionCV()) - - - -.. image:: images/tutorial/modelselect_logistic_regression_cv.png - - -.. code:: python - - visual_model_selection(X, y, LogisticRegression()) - - - .. image:: images/tutorial/modelselect_logistic_regression.png - -.. code:: python - - visual_model_selection(X, y, BaggingClassifier()) - - +.. image:: images/tutorial/modelselect_logistic_regression_cv.png .. image:: images/tutorial/modelselect_bagging_classifier.png - -.. code:: python - - visual_model_selection(X, y, ExtraTreesClassifier()) - - - .. image:: images/tutorial/modelselect_extra_trees_classifier.png - -.. code:: python - - visual_model_selection(X, y, RandomForestClassifier()) - - - .. image:: images/tutorial/modelselect_random_forest_classifier.png diff --git a/examples/README.md b/examples/README.md index d798ae2cb..745980248 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,66 +1,43 @@ -# Yellowbrick Examples +# Yellowbrick Examples -[![Visualizers](../docs/images/visualizers.png)](../docs/images/visualizers.png) +[![Visualizers](../docs/images/readme/banner.png)](../docs/images/readme/banner.png) -Welcome to the yellowbrick examples directory! This directory contains a gallery of visualizers and their application to classification, regression, clustering, and other machine learning techniques with Scikit-Learn. Examples have been submitted both by the Yellowbrick team and also users like you! The result is a rich gallery of tools and techniques to equip your machine learning with visual diagnostics and visualizer workflows! +Welcome to the yellowbrick examples directory! This directory contains a gallery of visualizers and their application to classification, regression, clustering, and other machine learning techniques with scikit-learn. Examples have been submitted both by the Yellowbrick team and also users like you! The result is a rich gallery of tools and techniques to equip your machine learning with visual diagnostics and visualizer workflows! -## Getting Started +## Getting Started -The notebook to explore first is the `examples.ipynb` Jupyter notebook. This notebook contains the executable examples from the tutorial in the documentation. However, before you can successfully run this notebook, you must first download the sample datasets. To download the samples run the downloader script: +The notebook to explore first is the `examples.ipynb` Jupyter notebook. This notebook contains the executable examples from the tutorial in the documentation. You can run the notebook as follows: ``` -$ python download.py +$ jupyter notebook examples.ipynb ``` -This should create a directory called `examples/data`, which in turn will contain CSV or text datasets. There are two primary problems that the download script may have: first, you may get the error `"The requests module is required to download data"`. To fix this problem: +If you don't have jupyter installed, or other dependencies, you may have to `pip install` them. -``` -$ pip install requests -``` - -The second problem may be `"Download signature does not match hardcoded signature!"` This problem means that the file you're trying to download has changed. Either download a more recent version of Yellowbrick, or use the URLs in the `download.py` script to fetch the data manually. If there are any other problems, please notify us via [GitHub Issues](https://github.com/DistrictDataLabs/yellowbrick/issues). - -Once the example data has been downloaded, you can run the examples notebook as follows: - -``` -$ jupyter notebook examples.ipynb -``` - -If you don't have jupyter installed, or other dependencies, you may have to `pip install` them. - -## Organization +## Organization The examples directory contains many notebooks, folders and files. At the top level you will see the following: -- examples.ipynb: a notebook with executable versions of the tutorial visualizers -- download.py: a script to download the example data sets -- palettes.ipynb: a visualization of the Yellowbrick palettes -- data: a directory containing the example datasets. +- examples.ipynb: a notebook with executable versions of the tutorial visualizers +- palettes.ipynb: a visualization of the Yellowbrick palettes +- regression.ipynb: a notebook exploring the regression model visualizers. -In addition to these files and directory, you will see many other directories, whose names are the GitHub usernames of their contributors. You can explore these user submitted examples or submit your own! +In addition to these files and directory, you will see many other directories, whose names are the GitHub usernames of their contributors. You can explore these user submitted examples or submit your own! -### Contributing +### Contributing To contribute an example notebook of your own, perform the following steps: -1. Fork the repository into your own account -2. Checkout the develop branch (see [contributing to Yellowbrick](http://www.scikit-yb.org/en/latest/about.html#contributing) for more. -3. Create a directory in the repo, `examples/username` where username is your GitHub username. -4. Create a notebook in that directory with your example. See [user testing](http://www.scikit-yb.org/en/latest/evaluation.html) for more. -5. Commit your changes back to your fork. -6. Submit a pull-request from your develop branch to the Yellowbrick develop branch. -7. Complete the code review steps with a Yellowbrick team member. - -That's it -- thank you for contributing your example! - -A couple of notes. First, please make sure that the Jupyter notebook you submit is "run" -- that is it has the output saved to the notebook and is viewable on GitHub (empty notebooks don't serve well as a gallery). Second, please do not commit datasets, but instead provide instructions for downloading the dataset. You can create a downloader utility similar to ours. - -One great tip, is to create your PR right after you fork the repo; that way we can work with you on the changes you're making and communicate about how to have a very successful contribution! +1. Fork the repository into your own account +2. Checkout the develop branch (see [contributing to Yellowbrick](http://www.scikit-yb.org/en/latest/about.html#contributing) for more. +3. Create a directory in the repo, `examples/username` where username is your GitHub username. +4. Create a notebook in that directory with your example. See [user testing](http://www.scikit-yb.org/en/latest/evaluation.html) for more. +5. Commit your changes back to your fork. +6. Submit a pull-request from your develop branch to the Yellowbrick develop branch. +7. Complete the code review steps with a Yellowbrick team member. -### User Examples +That's it -- thank you for contributing your example! -In this section we want to thank our examples contributors, and describe their notebooks so that you can find an example similar to your application! +A couple of notes. First, please make sure that the Jupyter notebook you submit is "run" -- that is it has the output saved to the notebook and is viewable on GitHub (empty notebooks don't serve well as a gallery). Second, please do not commit datasets, but instead provide instructions for downloading the dataset. You can create a downloader utility similar to ours. -- [bbengfort](https://github.com/bbengfort): visualizing text classification -- [rebeccabilbro](https://github.com/rebeccabilbro): visualizing book reviews data -- [nathan](https://github.com/ndanielsen/): visualizing the Iris dataset +One great tip, is to create your PR right after you fork the repo; that way we can work with you on the changes you're making and communicate about how to have a very successful contribution! diff --git a/examples/Sangarshanan/comparing_corpus_visualizers.ipynb b/examples/Sangarshanan/comparing_corpus_visualizers.ipynb new file mode 100644 index 000000000..ab17641f9 --- /dev/null +++ b/examples/Sangarshanan/comparing_corpus_visualizers.ipynb @@ -0,0 +1,242 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comparing Corpus Visualizers on Yellowbrick\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "##### Import all the necessary Libraries\n", + "\n", + "from yellowbrick.text import TSNEVisualizer\n", + "from sklearn.feature_extraction.text import TfidfVectorizer\n", + "from sklearn.feature_extraction.text import CountVectorizer\n", + "from yellowbrick.text import UMAPVisualizer\n", + "from yellowbrick.datasets import load_hobbies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UMAP vs T-SNE\n", + "\n", + "Uniform Manifold Approximation and Projection (UMAP) is a dimension reduction technique that can be used for visualisation similarly to t-SNE, but also for general non-linear dimension reduction. The algorithm is founded on three assumptions about the data\n", + "\n", + "1. The data is uniformly distributed on a Riemannian manifold;\n", + "2. The Riemannian metric is locally constant (or can be approximated as such);\n", + "3. The manifold is locally connected.\n", + "\n", + "From these assumptions it is possible to model the manifold with a fuzzy topological structure. The embedding is found by searching for a low dimensional projection of the data that has the closest possible equivalent fuzzy topological structure.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "corpus = load_hobbies()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Writing a Function to quickly Visualize Corpus \n", + "\n", + "Which can then be used for rapid comparison" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def visualize(dim_reduction,encoding,corpus,labels = True,alpha=0.7,metric=None):\n", + " if 'tfidf' in encoding.lower():\n", + " encode = TfidfVectorizer()\n", + " if 'count' in encoding.lower():\n", + " encode = CountVectorizer()\n", + " docs = encode.fit_transform(corpus.data)\n", + " if labels is True:\n", + " labels = corpus.target\n", + " else:\n", + " labels = None\n", + " if 'umap' in dim_reduction.lower():\n", + " if metric is None:\n", + " viz = UMAPVisualizer()\n", + " else:\n", + " viz = UMAPVisualizer(metric=metric)\n", + " if 't-sne' in dim_reduction.lower():\n", + " viz = TSNEVisualizer(alpha = alpha)\n", + " viz.fit(docs,labels)\n", + " viz.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Quickly Comparing Plots by Controlling \n", + "\n", + "- The Dimensionality Reduction technique used \n", + "- The Encoding Technique used \n", + "- The dataset to be visualized \n", + "- Whether to differentiate Labels or not \n", + "- Set the alpha parameter\n", + "- Set the metric for UMAP " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualize('t-sne','tfidf',corpus)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualize('t-sne','count',corpus,alpha = 0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualize('t-sne','tfidf',corpus,labels =False)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualize('umap','tfidf',corpus)" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualize('umap','tfidf',corpus,labels = False)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualize('umap','count',corpus,metric= 'cosine')" + ] + } + ], + "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.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/balavenkatesan/testing.py b/examples/balavenkatesan/testing.py index 0793d55ed..35a67117f 100644 --- a/examples/balavenkatesan/testing.py +++ b/examples/balavenkatesan/testing.py @@ -18,31 +18,64 @@ def load_adm_sat_school_data(return_X_y=False): n_features = int(temp[1]) target_names = np.array(temp[2:]) - - df = pd.read_csv("./merged_adm_sat_data.csv", sep=",", usecols=(0, 1, 2, 3), skiprows=0) + df = pd.read_csv( + "./merged_adm_sat_data.csv", sep=",", usecols=(0, 1, 2, 3), skiprows=0 + ) data = np.empty((n_samples, n_features), dtype=int) target = np.ma.empty((n_samples,), dtype=int) for index, row in df.iterrows(): - data[index] = np.asarray([df.iloc[index][0], df.iloc[index][1], df.iloc[index][2]], dtype=np.float) + data[index] = np.asarray( + [df.iloc[index][0], df.iloc[index][1], df.iloc[index][2]], dtype=np.float + ) target[index] = np.asarray(df.iloc[index][3], dtype=np.int) - feature_names = np.array(['ACT_AVG','SAT_AVG','GRAD_DEBT','REGION']) + feature_names = np.array(["ACT_AVG", "SAT_AVG", "GRAD_DEBT", "REGION"]) if return_X_y: return data, target - return datasets.base.Bunch(data=data, target=target, - target_names=target_names, - DESCR='School Data set', - feature_names=feature_names) + return datasets.base.Bunch( + data=data, + target=target, + target_names=target_names, + DESCR="School Data set", + feature_names=feature_names, + ) + def show_plot(X, y, n_neighbors=10, h=0.2): # Create color maps - cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF','#FFAAAA', '#AAFFAA', '#AAAAFF','#FFAAAA', '#AAFFAA', '#AAAAFF','#AAAAFF']) - cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#0000FF','#FF0000','#FF0000','#FF0000','#FF0000','#FF0000','#FF0000','#FF0000',]) - - for weights in ['uniform', 'distance']: + cmap_light = ListedColormap( + [ + "#FFAAAA", + "#AAFFAA", + "#AAAAFF", + "#FFAAAA", + "#AAFFAA", + "#AAAAFF", + "#FFAAAA", + "#AAFFAA", + "#AAAAFF", + "#AAAAFF", + ] + ) + cmap_bold = ListedColormap( + [ + "#FF0000", + "#00FF00", + "#0000FF", + "#FF0000", + "#FF0000", + "#FF0000", + "#FF0000", + "#FF0000", + "#FF0000", + "#FF0000", + ] + ) + + for weights in ["uniform", "distance"]: # we create an instance of Neighbours Classifier and fit the data. clf = neighbors.KNeighborsClassifier(n_neighbors, weights=weights) clf.fit(X, y) @@ -52,8 +85,7 @@ def show_plot(X, y, n_neighbors=10, h=0.2): # point in the mesh [x_min, x_max]x[y_min, y_max]. x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1 y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1 - xx, yy = np.meshgrid(np.arange(x_min, x_max, h), - np.arange(y_min, y_max, h)) + xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]) # Put the result into a color plot @@ -65,20 +97,21 @@ def show_plot(X, y, n_neighbors=10, h=0.2): plt.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold) plt.xlim(xx.min(), xx.max()) plt.ylim(yy.min(), yy.max()) - plt.title("3-Class classification (k = %i, weights = '%s')" - % (n_neighbors, weights)) + plt.title( + "3-Class classification (k = %i, weights = '%s')" % (n_neighbors, weights) + ) plt.show() -if __name__ == '__main__': +if __name__ == "__main__": school = load_adm_sat_school_data() X = school.data[:, :2] # we only take the first two features. y = school.target - #show_plot(X,y,3) + # show_plot(X,y,3) model = neighbors.KNeighborsClassifier(10) - model.fit(X,y) + model.fit(X, y) model.predict(X) - #visualizer = KnnDecisionBoundariesVisualizer(model, classes=school.target_names, features=school.feature_names[:2]) + # visualizer = KnnDecisionBoundariesVisualizer(model, classes=school.target_names, features=school.feature_names[:2]) visualizer = KnnDecisionBoundariesVisualizer(model) - visualizer.fit_draw_poof(X, y) \ No newline at end of file + visualizer.fit_draw_poof(X, y) diff --git a/examples/bbengfort/classifiers.ipynb b/examples/bbengfort/classifiers.ipynb new file mode 100644 index 000000000..ac0590abf --- /dev/null +++ b/examples/bbengfort/classifiers.ipynb @@ -0,0 +1,435 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Classifiers and Class Labels\n", + "\n", + "This notebook explores how the classification score visualizers handle constraints when it comes to different types of models, data, class labeling schemes, and other parameters. In particular we explore the following:\n", + "\n", + "Target Types:\n", + "\n", + "- binary\n", + "- multiclass (3 classes)\n", + "\n", + "Target Encoding:\n", + "\n", + "- integers\n", + "- labels \n", + "\n", + "Labeling:\n", + "\n", + "- list of classes\n", + "- LabelEncoder\n", + "- dict encoding\n", + "- list of more classes than values in y" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Ensure we're importing the development version of Yellowbrick\n", + "import sys\n", + "sys.path.append(\"../..\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Use inline so that we can run the notebook multiple times\n", + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Import all of the Yellowbrick classifiers\n", + "from yellowbrick.classifier import *\n", + "from yellowbrick.exceptions import YellowbrickError\n", + "from yellowbrick.datasets import load_game, load_occupancy\n", + "\n", + "# Import scikit-learn utilities\n", + "from sklearn.pipeline import Pipeline\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.naive_bayes import MultinomialNB, GaussianNB\n", + "from sklearn.model_selection import train_test_split as tts\n", + "from sklearn.preprocessing import OneHotEncoder, LabelEncoder\n", + "from sklearn.datasets import make_classification\n", + "\n", + "from collections import namedtuple\n", + "from functools import partial\n", + "\n", + "Dataset = namedtuple(\"Dataset\", \"X,y,classes,encoder\")\n", + "Split = namedtuple(\"Split\", \"train,test\")\n", + "\n", + "\n", + "make_binary = partial(make_classification,\n", + " n_samples=500,\n", + " n_features=20,\n", + " n_informative=8,\n", + " n_redundant=2,\n", + " n_classes=2,\n", + " n_clusters_per_class=3,\n", + " )\n", + "\n", + "make_multiclass = partial(make_classification, \n", + " n_samples=500,\n", + " n_features=20,\n", + " n_informative=8,\n", + " n_redundant=2,\n", + " n_classes=6,\n", + " n_clusters_per_class=3,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Select the parameters to run against all models \n", + "# Then restart kernel and run all\n", + "MODEL = LogisticRegression(solver='lbfgs', multi_class='auto')\n", + "IS_FITTED = False\n", + "DATASET = \"multiclass\" \n", + "TARGET = \"integers\"\n", + "ENCODER = \"labelencoder\"\n", + "USE_PANDAS = False\n", + "\n", + "\n", + "def make_dataset(name=DATASET, target=TARGET, encoder=ENCODER, use_pandas=USE_PANDAS):\n", + " loader = {\n", + " 'game': load_game, \n", + " 'occupancy': load_occupancy,\n", + " 'binary': make_binary,\n", + " 'multiclass': make_multiclass,\n", + " }.get(name)\n", + " \n", + " if name in {'game', 'occupancy'}:\n", + " dataset = loader(return_dataset=True)\n", + " labels = sorted(dataset.meta['labels'].items(), key=lambda i: i[1])\n", + "\n", + " if use_pandas:\n", + " X, y = dataset.to_pandas()\n", + " else:\n", + " X, y = dataset.to_numpy()\n", + " else:\n", + " X, y = loader()\n", + " labels = zip(list('abcdefghijk'), np.unique(y))\n", + " \n", + " if name == 'game':\n", + " X = OneHotEncoder().fit_transform(X)\n", + " \n", + " # game target is string encoded, occupancy is integer encoded\n", + " if target == \"integers\":\n", + " if y.dtype.kind != 'i':\n", + " y = LabelEncoder().fit_transform(y)\n", + " elif target == \"labels\":\n", + " if y.dtype.kind == 'i':\n", + " rv = {i[1]: i[0] for i in labels.items()}\n", + " y = np.array([rv[yi] for yi in y])\n", + " else:\n", + " raise ValueError(f\"unknown target type '{target}', use integers or labels\")\n", + " \n", + " c, le = None, None\n", + " X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, shuffle=True, stratify=y)\n", + " \n", + " if encoder == 'list':\n", + " c = [l[0] for l in labels]\n", + " elif encoder == 'labelencoder':\n", + " le = LabelEncoder().fit([l[0] for l in labels])\n", + " elif encoder == 'dict':\n", + " le = {l[1]: l[0] for l in labels}\n", + " elif encoder is None:\n", + " c, le = None, None\n", + " else:\n", + " raise ValueError(f\"unknown encoder type '{encoder}', see make_dataset for choices\")\n", + " \n", + " return Dataset(Split(X_train, X_test), Split(y_train, y_test), c, le)\n", + " \n", + "\n", + "def visualize(visualizer, model=MODEL, is_fitted=IS_FITTED, score=True):\n", + " if is_fitted:\n", + " # This includes both auto and True; fit the model manually if you want the exception raised\n", + " model = model.fit(dataset.X.train, dataset.y.train)\n", + " _, ax = plt.subplots(figsize=(9,6)) \n", + " \n", + " try:\n", + " oz = visualizer(model, ax=ax, classes=dataset.classes, encoder=dataset.encoder, is_fitted=is_fitted)\n", + " oz.fit(dataset.X.train, dataset.y.train)\n", + "\n", + " if score:\n", + " oz.score(dataset.X.test, dataset.y.test)\n", + "\n", + " oz.finalize()\n", + " except YellowbrickError as e:\n", + " print(e)\n", + " except Exception as e:\n", + " print(\"A NON YB ERROR OCCURRED:\")\n", + " print(e)\n", + " return oz\n", + " \n", + " \n", + " \n", + "dataset = make_dataset()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = visualize(ClassPredictionError)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.43" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "oz.score_" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ClassificationReport(ax=,\n", + " classes=None,\n", + " cmap=,\n", + " encoder=LabelEncoder(), force_model=False, is_fitted=False,\n", + " model=None, support=None)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualize(ClassificationReport)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConfusionMatrix(ax=,\n", + " classes=None,\n", + " cmap=,\n", + " encoder=LabelEncoder(), fontsize=None, force_model=False,\n", + " is_fitted=False, model=None, percent=False, sample_weight=None)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualize(ConfusionMatrix)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PrecisionRecallCurve(ap_score=True,\n", + " ax=,\n", + " classes=None, encoder=LabelEncoder(), fill_area=True,\n", + " fill_opacity=0.2, force_model=False, is_fitted=False,\n", + " iso_f1_curves=False, iso_f1_values={0.2, 0.4, 0.6, 0.8},\n", + " line_opacity=0.8, micro=True, model=None, per_class=False)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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TA3w6RVHkd7M148UXERERCQmIzVAeTDMuWP7E/C7l5A+XrSO/JIKKkOR1htcjDF+4CFgBFABXGStXRL3/G+AM6MP37PW3pqstoiIiEjDPPbTA2qVvTh7OT58XPdOHm3SU2PlfTrk8IuDBuHz1VyzOnmS2eMyGciw1h5gjBkP3AOcBGCM6QecDewPRIAvjTFTrbVzk9geERER2QXlwTB+n48vl21MKE9PCeDzQbvMtFhZj3ZZHNq/a9LakszgcjDwHwBr7XRjzNi451YDx1hrwwDGmFSgPIltERERkV101ui+vDn/R+6eNDpWdvlr3xAMR3j060UJdVP8fv572UTaZVT3zPjw4fc3Tq+Mz6k560wjMcY8AbxmrX0/+ngV0M9aG4qr4wPuBtpYay/d3r7y8vL6AMuXbCknGElOe0VERKThXliwGVtYzhmmI1WZ5MWFBaQFfPhIDCmdMlP48yE9SK89oV3fMWPGrNiZ4yazx6UIaBP32F8jtGQATwHFwOUN2eGA/v0hkLrjitIkFixcwJDBQ5q7GRKl89Gy6Hy0PDonjeu2wbXLpq74EoC92mbSMSsdgB+3lFISCtPH7EP3dlkAVFRUMH/+/F06bjKDy1fAJODl6BiXeVVPRHta3gQ+sdb+JYltEBERkSby8GnjWVm4jaFdq+d7eXz6Ymau2tRox0hmcJkKHGmMmQb4gPONMVcDS4AAMAFIN8YcG61/g7X26yS2R0RERJIoOy0lIbQkQ9KCi7U2AlxWo3hh3M8ZyTq2iIiItAxfLd9IWsDPSU99SrsM9+6jDml+rh+5awFHE9CJiIhI0kQch/JQmIDPhz/TLdudaV8UXERERCRp/j55HN+v38Ih/eLmdgkHYfOaXdpfrfuSRERERBpLx6z0xNCymxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQzFFxERETEMxRcRERExDMUXERERMQz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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualize(PRCurve)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ROCAUC(ax=,\n", + " classes=None, encoder=LabelEncoder(), force_model=False, is_fitted=False,\n", + " macro=True, micro=True, model=None, per_class=True)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualize(ROCAUC)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "multiclass format is not supported\n" + ] + }, + { + "data": { + "text/plain": [ + "DiscriminationThreshold(argmax='fscore',\n", + " ax=,\n", + " cv=0.1, exclude=None, fbeta=1.0, force_model=None,\n", + " is_fitted=False, model=None, n_trials=50,\n", + " quantiles=array([0.1, 0.5, 0.9]), random_state=None)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiAAAAFoCAYAAABuakCAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAQi0lEQVR4nO3cf6jldZ3H8deMo3egxn4gtBIsFeSH4JLBWI6u5QbpZiRI9EcYxApWUrBtBq6ykAVtS5Rb9IdEG7L/7BJCiLaFEsSyNirJNRYv5VsmMCIqKkwr6k7OzP5x79BBZu45d5zzvsd7Hw8YuN/z/Z5z3vC5d87zfs+53z0nTpwIAECnvds9AACw+wgQAKCdAAEA2gkQAKCdAAEA2gkQAKDdTAEyxrh0jPE/p7j92jHGo2OMh8cYHzzr0wEAO9LUABlj3JLka0n2P+/2c5N8McnVSa5M8qExxqvmMSQAsLPMcgbkx0nec4rb35DkSFU9XVVHk3wvydvO5nAAwM60b9oBVfWNMcZrTrHr/CTPTGz/LsnLNnuslZWVpSRvTvLzJMdmHxMAWEDnJLkwyaMHDx5c28odpwbIJp5NcmBi+0CS3065z5uTPPgCnhMAWDxvzfo7ITN7IQHyoySvH2O8Msnvs/72yxem3OfnSXLRRRflvPPOewFPzdm0urqa5eXl7R6DDdZjsViPxWNNFsfRo0fz5JNPJhuv71ux5QAZY1yf5KVV9dUxxs1JHsj6Z0nuqqqfTbn7sSQ577zzsrS0tNWnZo6sx2KxHovFeiwea7JwtvyxipkCpKqeSnJo4+v/mrj9m0m+udUnBQB2NxciAwDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDaCRAAoJ0AAQDa7Zt2wBhjb5I7k1ycZC3JjVV1ZGL/J5Jcn+R4ks9W1T1zmhUA2CFmOQNyXZL9VXVZkluT3HFyxxjj5Uk+luSyJFcn+dI8hgQAdpZZAuSKJPcnSVU9kuSSiX1/SPKTJC/Z+Hf8bA8IAOw8swTI+Umemdg+NsaYfOvmp0l+mOSxJF8+i7MBADvU1M+AJHk2yYGJ7b1V9dzG19ckuTDJaze2HxhjHK6q72/2gKurq1selPlaWVnZ7hGYYD0Wi/VYPNbkxW+WADmc5Nokd48xDiV5fGLf00n+mGStqk6MMX6b5OXTHnB5eTlLS0tnMi9zsLKykoMHD273GGywHovFeiwea7I41tbWzvikwiwBck+Sq8YYDyXZk+SGMcbNSY5U1X1jjHckeWSMcTzJ95J854wmAQB2jakBUlXHk9z0vJufmNh/e5Lbz/JcAMAO5kJkAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtBMgAEA7AQIAtNs37YAxxt4kdya5OMlakhur6sjE/muS3J5kT5KVJB+tqhPzGRcA2AlmOQNyXZL9VXVZkluT3HFyxxjjQJLPJ3l3VV2a5KkkF8xhTgBgB5klQK5Icn+SVNUjSS6Z2Hd5kseT3DHGeDDJL6vqV2d9SgBgR5n6FkyS85M8M7F9bIyxr6qey/rZjrcneVOS3yd5cIzxcFU9udkDrq6unum8zMnKysp2j8AE67FYrMfisSYvfrMEyLNJDkxs792IjyT5TZJHq+oXSTLG+N+sx8imAbK8vJylpaUzGJd5WFlZycGDB7d7DDZYj8ViPRaPNVkca2trZ3xSYZa3YA4neVeSjDEOZf0tl5MeS7I8xrhgjLEvyaEkPzyjSQCAXWOWMyD3JLlqjPFQ1v/S5YYxxs1JjlTVfWOM25I8sHHs3VXl/RUAYFNTA6Sqjie56Xk3PzGx/+tJvn6W5wIAdjAXIgMA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2u2bdsAYY2+SO5NcnGQtyY1VdeQUx3wryb1V9ZV5DAoA7ByznAG5Lsn+qrosya1J7jjFMZ9J8oqzORgAsHPNEiBXJLk/SarqkSSXTO4cY7w3yfGTxwAATDP1LZgk5yd5ZmL72BhjX1U9N8ZYTnJ9kvcm+eSsT7q6urq1KZm7lZWV7R6BCdZjsViPxWNNXvxmCZBnkxyY2N5bVc9tfP2BJK9O8t0kr0lydIzxVFVtejZkeXk5S0tLZzAu87CyspKDBw9u9xhssB6LxXosHmuyONbW1s74pMIsAXI4ybVJ7h5jHEry+MkdVXXLya/HGJ9K8otp8QEAMEuA3JPkqjHGQ0n2JLlhjHFzkiNVdd9cpwMAdqSpAVJVx5Pc9LybnzjFcZ86SzMBADucC5EBAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO0ECADQToAAAO32TTtgjLE3yZ1JLk6yluTGqjoysf/jSd63sfntqvr0PAYFAHaOWc6AXJdkf1VdluTWJHec3DHGeF2S9ye5PMmhJFePMd44j0EBgJ1jlgC5Isn9SVJVjyS5ZGLfT5O8s6qOVdWJJOcm+dNZnxIA2FGmvgWT5Pwkz0xsHxtj7Kuq56rqz0l+PcbYk+TzSX5QVU/OY1AAYOeYJUCeTXJgYntvVT13cmOMsT/JXUl+l+Qjszzp6urqVmakwcrKynaPwATrsVisx+KxJi9+swTI4STXJrl7jHEoyeMnd2yc+bg3yXer6nOzPuny8nKWlpa2OitzsrKykoMHD273GGywHovFeiwea7I41tbWzvikwiwBck+Sq8YYDyXZk+SGMcbNSY4kOSfJlUmWxhjXbBx/W1U9fEbTAAC7wtQAqarjSW563s1PTHy9/6xOBADseC5EBgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC0EyAAQDsBAgC02zftgDHG3iR3Jrk4yVqSG6vqyMT+Dyb5cJLnknymqv57TrMCADvELGdArkuyv6ouS3JrkjtO7hhj/FWSf0jyN0n+Lsm/jjGW5jEoALBzzBIgVyS5P0mq6pEkl0zse0uSw1W1VlXPJDmS5I1nfUoAYEeZ+hZMkvOTPDOxfWyMsa+qnjvFvt8ledkmj3VOkhw9enSrczJna2tr2z0CE6zHYrEei8eaLIaJ1/NztnrfWQLk2SQHJrb3bsTHqfYdSPLbTR7rwiR58skntzIjDVZXV7d7BCZYj8ViPRaPNVk4Fyb58VbuMEuAHE5ybZK7xxiHkjw+se/7Sf5ljLE/yVKSNyTZ7Lvi0SRvTfLzJMe2MigAsHDOyXp8PLrVO+45ceLEpgdM/BXMG5PsSXJDknclOVJV9238FcyHsv55ks9W1Te2OgQAsLtMDRAAgLPNhcgAgHYCBABoJ0AAgHaz/BXMGXEJ98Uyw3p8PMn7Nja/XVWf7p9y95i2HhPHfCvJvVX1lf4pd5cZfkauSXJ71j+Mv5Lko1XlQ3RzMsN6fCLJ9UmOZ/0PIO7ZlkF3mTHGpUk+V1V/+7zbr03yyay/pt9VVf8+7bHmeQbEJdwXy2br8bok709yeZJDSa4eY7ii7Xyddj0mfCbJK1qn2t02+xk5kOTzSd5dVZcmeSrJBdsx5C6y2Xq8PMnHklyW5OokX9qWCXeZMcYtSb6WZP/zbj83yRezvhZXJvnQGONV0x5vngHiEu6LZbP1+GmSd1bVsY3f6M5N8qf+EXeVzdYjY4z3Zv03u/v7R9u1NluTy7N+DaQ7xhgPJvllVf2qf8RdZbP1+EOSnyR5yca/4+3T7U4/TvKeU9z+hqxfmuPpqjqa5HtJ3jbtweYZIKe8hPtp9k27hDsv3GnXo6r+XFW/HmPsGWN8IckPqsrlaufrtOsxxljO+qnlT27HYLvYZv9nXZDk7Un+Kck1Sf5xjHFR83y7zWbrkaz/4vTDJI8l+XLnYLvVxnW+/nyKXWf0mj7PADmbl3DnhdtsPbJxNdv/3DjmI82z7UabrccHkrw6yXeT/H2Sm8cY7+wdb1fabE1+k+TRqvpFVf0+yf8meVP3gLvMZutxTdavvvnaJH+d5Loxxlua5+Mvzug1fZ4BcjjrV0zNaS7h/tYxxv4xxssy/RLuvHCnXY8xxp4k9yb5v6r6cFW5TP78nXY9quqWqrp040Ne/5Hk36rKWzHzt9n/WY8lWR5jXLDxW/ihrP/2zfxsth5PJ/ljkrWq+lPWX+xe3j4hJ/0oyevHGK8cY5yX9bdfHp52p7n9FUySe5JcNcZ4KBuXcB9j3Jy/XML9y0kezHoE/fPGNxHzc9r1yPq1/K9MsrTxSf8kua2qpn4DccY2/fnY3tF2rWn/Z92W5IGNY++uKr80zde09XhHkkfGGMez/pmD72zjrLvSGOP6JC+tqq9urM0DWX9Nv6uqfjbt/i7FDgC0cyEyAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2gkQAKCdAAEA2v0/KnfaJ4RqHpYAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualize(DiscriminationThreshold, score=False)" + ] + } + ], + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/bbengfort/cooks_distance.ipynb b/examples/bbengfort/cooks_distance.ipynb new file mode 100644 index 000000000..c352805c2 --- /dev/null +++ b/examples/bbengfort/cooks_distance.ipynb @@ -0,0 +1,2004 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Cooks Distance\n", + "\n", + "See more at [Cook's Distance](https://en.wikipedia.org/wiki/Cook%27s_distance). A very good description of influence and leverage can be found at [Linear Regression in Python, Outliers / Leverage Detect](https://songhuiming.github.io/pages/2016/11/27/linear-regression-in-python-outliers-leverage-detect/) (this post should make it into the Yellowbrick documentation in some form). \n", + "\n", + "A good description of outlier detection that also includes Cook's Distance is [How to Make Your Machine Learning Models Robust to Outliers](https://heartbeat.fritz.ai/how-to-make-your-machine-learning-models-robust-to-outliers-44d404067d07). Original motivating post is at [How do you check the quality of your regression model in Python?](https://towardsdatascience.com/how-do-you-check-the-quality-of-your-regression-model-in-python-fa61759ff685) - source code for this post is at [\n", + "Visual analytics and diagnostics of model fit for linear regression](https://github.com/tirthajyoti/Machine-Learning-with-Python/blob/master/Regression/Regression_Diagnostics.ipynb).\n", + "\n", + "The statsmodels source code for Cook's Distance is at:\n", + "\n", + "- [Outliers Influence](http://www.statsmodels.org/0.6.1/_modules/statsmodels/stats/outliers_influence.html)\n", + "- [Linear Model](https://www.statsmodels.org/dev/_modules/statsmodels/regression/linear_model.html)\n", + "- [NumPy Linear Algebra](https://docs.scipy.org/doc/numpy-1.13.0/reference/routines.linalg.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib notebook\n", + "\n", + "import scipy as sp\n", + "import numpy as np\n", + "import pandas as pd \n", + "import matplotlib.pyplot as plt \n", + "\n", + "# Note: statsmodels requires scipy 1.2\n", + "import statsmodels.formula.api as sm\n", + "\n", + "from sklearn.datasets import make_regression\n", + "from sklearn.linear_model import LinearRegression\n", + "from statsmodels.stats.outliers_influence import OLSInfluence as influence\n", + "\n", + "from yellowbrick.base import Visualizer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Random Test Data\n", + "\n", + "For the purpose of generating tests for the visualizer, I'm using random test data. The compressive concrete strength dataset would also be good for integration testing." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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", + " \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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
X0X1X2X3X4X5X6X7X8X9X10X11X12X13y
0-0.0790990.774174-0.389897-1.009309-0.0823650.6373090.2369021.618119-0.520738-0.2108910.654572-0.366485-0.8592110.14597842.341339
1-0.8963111.928183-0.5387420.5881170.724919-0.8566560.842047-1.796532-0.5345442.421266-0.1152431.585129-0.1034420.169937-64.162092
20.479350-0.544794-0.1807320.899390-0.8744540.9077071.0580481.2662990.033375-0.2419880.161981-0.7708800.785022-2.043742-63.616222
3-0.7931361.422011-1.109729-1.4342481.3923040.622966-0.4724420.075291-0.077669-1.725702-0.0756340.147481-0.918953-0.566963-84.649787
40.6708280.1496580.1169480.011745-2.450315-0.529813-1.586794-0.337791-1.360943-1.209053-0.0292310.909747-0.142993-1.622250-166.153271
\n", + "
" + ], + "text/plain": [ + " X0 X1 X2 X3 X4 X5 X6 \\\n", + "0 -0.079099 0.774174 -0.389897 -1.009309 -0.082365 0.637309 0.236902 \n", + "1 -0.896311 1.928183 -0.538742 0.588117 0.724919 -0.856656 0.842047 \n", + "2 0.479350 -0.544794 -0.180732 0.899390 -0.874454 0.907707 1.058048 \n", + "3 -0.793136 1.422011 -1.109729 -1.434248 1.392304 0.622966 -0.472442 \n", + "4 0.670828 0.149658 0.116948 0.011745 -2.450315 -0.529813 -1.586794 \n", + "\n", + " X7 X8 X9 X10 X11 X12 X13 \\\n", + "0 1.618119 -0.520738 -0.210891 0.654572 -0.366485 -0.859211 0.145978 \n", + "1 -1.796532 -0.534544 2.421266 -0.115243 1.585129 -0.103442 0.169937 \n", + "2 1.266299 0.033375 -0.241988 0.161981 -0.770880 0.785022 -2.043742 \n", + "3 0.075291 -0.077669 -1.725702 -0.075634 0.147481 -0.918953 -0.566963 \n", + "4 -0.337791 -1.360943 -1.209053 -0.029231 0.909747 -0.142993 -1.622250 \n", + "\n", + " y \n", + "0 42.341339 \n", + "1 -64.162092 \n", + "2 -63.616222 \n", + "3 -84.649787 \n", + "4 -166.153271 " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Make Test Dataset\n", + "X, y = make_regression(\n", + " n_samples=100, n_features=14, n_informative=6, bias=1.2, noise=49.8, tail_strength=0.6, random_state=637\n", + ")\n", + "\n", + "# Convert to a DataFrame for statsmodels\n", + "data = pd.DataFrame(X)\n", + "data.columns = [f\"X{i}\" for i in range(X.shape[1])]\n", + "data[\"y\"] = y\n", + "data.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Statsmodels Results\n", + "\n", + "For comparison to the custom computation, this section computes the statsmodels OLS model and Cook's Distance values. In a later section we will compare this to scikit-learn linear regression and the Yellowbrick cooks function." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " OLS Regression Results \n", + "==============================================================================\n", + "Dep. Variable: y R-squared: 0.881\n", + "Model: OLS Adj. R-squared: 0.862\n", + "Method: Least Squares F-statistic: 45.14\n", + "Date: Sun, 09 Jun 2019 Prob (F-statistic): 2.75e-33\n", + "Time: 16:26:30 Log-Likelihood: -513.54\n", + "No. Observations: 100 AIC: 1057.\n", + "Df Residuals: 85 BIC: 1096.\n", + "Df Model: 14 \n", + "Covariance Type: nonrobust \n", + "==============================================================================\n", + " coef std err t P>|t| [0.025 0.975]\n", + "------------------------------------------------------------------------------\n", + "Intercept -9.9575 5.033 -1.978 0.051 -19.965 0.050\n", + "X0 101.3951 4.852 20.897 0.000 91.748 111.042\n", + "X1 7.3318 5.241 1.399 0.165 -3.089 17.752\n", + "X2 -3.2906 4.839 -0.680 0.498 -12.912 6.331\n", + "X3 -3.7461 5.237 -0.715 0.476 -14.158 6.666\n", + "X4 28.0759 4.432 6.335 0.000 19.265 36.887\n", + "X5 3.0478 5.184 0.588 0.558 -7.259 13.355\n", + "X6 3.3205 4.966 0.669 0.505 -6.552 13.193\n", + "X7 15.3175 4.014 3.816 0.000 7.336 23.299\n", + "X8 49.7685 5.584 8.912 0.000 38.666 60.872\n", + "X9 14.7939 4.486 3.298 0.001 5.874 23.713\n", + "X10 -0.6941 5.275 -0.132 0.896 -11.183 9.795\n", + "X11 -10.1851 4.921 -2.070 0.042 -19.969 -0.402\n", + "X12 -8.5387 5.249 -1.627 0.108 -18.976 1.898\n", + "X13 25.9747 4.656 5.579 0.000 16.717 35.232\n", + "==============================================================================\n", + "Omnibus: 0.261 Durbin-Watson: 2.027\n", + "Prob(Omnibus): 0.878 Jarque-Bera (JB): 0.418\n", + "Skew: 0.093 Prob(JB): 0.811\n", + "Kurtosis: 2.744 Cond. No. 2.30\n", + "==============================================================================\n", + "\n", + "Warnings:\n", + "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n" + ] + } + ], + "source": [ + "# Compute an OLS model \n", + "cols = data.columns\n", + "model = sm.ols(formula=f\"{cols[-1]} ~ {' + '.join(cols[:-1])}\", data=data)\n", + "model = model.fit()\n", + "\n", + "print(model.summary())" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute the influence to get Cook's distance\n", + "inf = influence(model)\n", + "\n", + "# cooks_distance is an attribute of incluence, here C, not sure about P (p-value maybe?)\n", + "C, P = inf.cooks_distance" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support.' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
')\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " event.shiftKey = false;\n", + " // Send a \"J\" for go to next cell\n", + " event.which = 74;\n", + " event.keyCode = 74;\n", + " manager.command_mode();\n", + " manager.handle_keydown(event);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "class CooksDistance(Visualizer):\n", + " \n", + " def fit(self, X, y):\n", + " # Leverage is computed as the diagonal of the projection matrix of X \n", + " # TODO: whiten X before computing leverage\n", + " self.leverage_ = (X * np.linalg.pinv(X).T).sum(1)\n", + " \n", + " # Compute the MSE\n", + " rank = np.linalg.matrix_rank(X)\n", + " df = X.shape[0] - rank\n", + " \n", + " resid = y - LinearRegression().fit(X, y).predict(X)\n", + " mse = np.dot(resid, resid) / df \n", + " \n", + " resid_studentized_internal = resid / np.sqrt(mse) / np.sqrt(1-self.leverage_)\n", + " \n", + " self.distance_ = resid_studentized_internal**2 / X.shape[1]\n", + " self.distance_ *= self.leverage_ / (1 - self.leverage_)\n", + "\n", + " self.p_values_ = sp.stats.f.sf(self.distance_, X.shape[1], df)\n", + " \n", + " self.draw()\n", + " return self\n", + " \n", + " \n", + " def draw(self):\n", + " self.ax.stem(self.distance_, markerfmt=\",\", label=\"influence\")\n", + " self.ax.axhline(4/len(self.distance_), c='r', ls='--', lw=1, label=\"$\\frac{4}{n}$\")\n", + " \n", + " def finalize(self):\n", + " self.ax.legend()\n", + " self.ax.set_xlabel(\"instance\")\n", + " self.ax.set_ylabel(\"influence\")\n", + " self.ax.set_title(\"Cook's Distance Outlier Detection\")\n", + " \n", + " \n", + "viz = CooksDistance().fit(X, y)\n", + "viz.finalize()" + ] + } + ], + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/download.py b/examples/download.py deleted file mode 100644 index ec82a6685..000000000 --- a/examples/download.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python -# download -# Downloads the example datasets for running the examples. -# -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort -# Created: Wed May 18 11:54:45 2016 -0400 -# -# Copyright (C) 2016 District Data Labs -# For license information, see LICENSE.txt -# -# ID: download.py [1f73d2b] benjamin@bengfort.com $ - -""" -Downloads the example datasets for running the examples. -""" - -########################################################################## -## Imports -########################################################################## - -import os -import sys -import hashlib -import zipfile - -try: - import requests -except ImportError: - print(( - "The requests module is required to download data --\n" - "please install it with pip install requests." - )) - sys.exit(1) - - -########################################################################## -## Links and MD5 hash of datasets -########################################################################## - -DATASETS = { - 'concrete': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/concrete.zip', - 'signature': 'b9ea5f26a7bb272a040e2f1a993b26babbf8dc4a04ab8198bb315ca66d71f10d', - }, - 'energy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/energy.zip', - 'signature': '19fb86f3bcdde208eed46944172cb643ef6a7d58da103fb568fae43205ed89d3', - }, - 'credit': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/credit.zip', - 'signature': '4a91339c69f55e18f3f48004328fbcb7868070b618208fed099920427b084e5e', - }, - 'occupancy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/occupancy.zip', - 'signature': '429cfe376dc9929a1fa528da89f0e1626e34e19695f3f555d8954025bbc522b8', - }, - 'mushroom': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/mushroom.zip', - 'signature': '884c43cb70db35d211c67b1cf6a3683b2b4569393d2789d5c07840da4dc85ba8', - }, - 'hobbies': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/hobbies.zip', - 'signature': '415c8f68df1486d5d84a1d1757a5aa3035aef5ad63ede5013c261d622fbd29d8', - }, - 'game': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/game.zip', - 'signature': 'b1bd85789a014a898daa34cb5f89ceab6d2cd6488a2e572187e34aa4ec21a43b', - }, - 'bikeshare': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/bikeshare.zip', - 'signature': 'a9b440f65549746dff680c92ff8bdca3c7265f09db1cf09e708e6e26fc8aba44', - }, - 'spam': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/spam.zip', - 'signature': '65be21196ba3d8448847409b70a67d761f873f30719c807600eb516d7aef1de1', - }, -} - - -########################################################################## -## Download functions -########################################################################## - -def sha256sum(path, blocksize=65536): - """ - Computes the SHA256 signature of a file to verify that the file has not - been modified in transit and that it is the correct version of the data. - """ - sig = hashlib.sha256() - with open(path, 'rb') as f: - buf = f.read(blocksize) - while len(buf) > 0: - sig.update(buf) - buf = f.read(blocksize) - return sig.hexdigest() - - -def download_data(url, path='data', signature=None, extract=True): - """ - Downloads the zipped data set specified at the given URL, saving it to - the output path specified. This function verifies the download with the - given signature (if supplied) and extracts the zip file if requested. - """ - # Create the output directory if it does not exist - if not os.path.exists(path): - os.mkdir(path) - - # Get the name of the file from the URL - name = os.path.basename(url) - dlpath = os.path.join(path, name) - - # Fetch the response in a streaming fashion and write it to disk. - response = requests.get(url, stream=True) - with open(dlpath, 'wb') as f: - for chunk in response.iter_content(65536): - f.write(chunk) - - # If verify, compare the signature - if signature is not None: - dlsignature = sha256sum(dlpath) - if signature != dlsignature: - raise ValueError( - "Download signature does not match hardcoded signature!" - ) - - # If extract, extract the zipfile. - if extract: - zf = zipfile.ZipFile(dlpath) - zf.extractall(path) - - -def download_all(path='data', verify=True, extract=True): - """ - Downloads all the example datasets. If verify is True then compare the - download signature with the hardcoded signature. If extract is True then - extract the contents of the zipfile to the given path. - """ - for name, meta in DATASETS.items(): - url = meta['url'] - signature = meta['signature'] if verify else None - - download_data(url, path=path, signature=signature, extract=extract) - - -if __name__ == '__main__': - path='data' - download_all(path) - print("Downloaded datasets to {}".format(os.path.abspath(path))) diff --git a/examples/mike-curry00/Silhouette_Plot_Data.ipynb b/examples/mike-curry00/Silhouette_Plot_Data.ipynb new file mode 100644 index 000000000..5988e2220 --- /dev/null +++ b/examples/mike-curry00/Silhouette_Plot_Data.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visuals for NFL Receiver Clusters\n", + "- 2018 regular season statistics for all players targeted at least once.\n", + "- Data sourced from [Pro-Football-Reference](https://www.pro-football-reference.com/years/2018/receiving.htm)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import matplotlib as mpl\n", + "import matplotlib.pyplot as plt\n", + "from sklearn.cluster import KMeans\n", + "from yellowbrick.cluster import KElbowVisualizer, SilhouetteVisualizer\n", + "import yellowbrick.datasets as ybdata\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "nfl_df = (ybdata.load_nfl(return_dataset=True)\n", + " .to_dataframe()\n", + " .query('Tgt >= 20'))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "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", + " \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", + " \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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
RkPlayerIdTmAgeGGSTgtRecCtch_Rate...FirstTeamAllProC_posCB_posDT_posFB_posQB_posRB_posT_posTE_posWR_pos
01Michael ThomasThomMi05NOR2516161471250.850...1000000001
12Zach ErtzErtzZa00PHI2816161561160.744...0000000010
23DeAndre HopkinsHopkDe00HOU2616161631150.706...1000000001
34Julio JonesJoneJu02ATL2916161701130.665...0000000001
45Adam ThielenThieAd00MIN2816161531130.739...0000000001
\n", + "

5 rows × 29 columns

\n", + "
" + ], + "text/plain": [ + " Rk Player Id Tm Age G GS Tgt Rec Ctch_Rate \\\n", + "0 1 Michael Thomas ThomMi05 NOR 25 16 16 147 125 0.850 \n", + "1 2 Zach Ertz ErtzZa00 PHI 28 16 16 156 116 0.744 \n", + "2 3 DeAndre Hopkins HopkDe00 HOU 26 16 16 163 115 0.706 \n", + "3 4 Julio Jones JoneJu02 ATL 29 16 16 170 113 0.665 \n", + "4 5 Adam Thielen ThieAd00 MIN 28 16 16 153 113 0.739 \n", + "\n", + " ... FirstTeamAllPro C_pos CB_pos DT_pos FB_pos QB_pos RB_pos \\\n", + "0 ... 1 0 0 0 0 0 0 \n", + "1 ... 0 0 0 0 0 0 0 \n", + "2 ... 1 0 0 0 0 0 0 \n", + "3 ... 0 0 0 0 0 0 0 \n", + "4 ... 0 0 0 0 0 0 0 \n", + "\n", + " T_pos TE_pos WR_pos \n", + "0 0 0 1 \n", + "1 0 1 0 \n", + "2 0 0 1 \n", + "3 0 0 1 \n", + "4 0 0 1 \n", + "\n", + "[5 rows x 29 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nfl_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "features = ['Rec', 'Yds', 'TD', 'Fmb', 'Ctch_Rate']\n", + "X = nfl_df[features].values" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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6tWLLl26ANCxY0c++ugjatWqxcGDB9m5cyfbtm3j1FNPZc+ePbzxxhvcfffdx8Xw9ttvM2PGDLKysti+fTu33XYbAN999x3t27cvNO7s7Gw2bNjA2LFjGTduHEeOHDlm/auvvhpS++fOnUv9+vVJTEw8psemMDExMRw6dIg///nPXHzxxXTv3p2ZM2fSoUMH+vXrB5CXSM2YMaPQ5yYzM5M9e/bw29/+Nu85LOz5L6ktaWlpx22X68CBA4wZM4Z7772XWrVqhfQ8FHT48GGWL1/O/fffT1JSEo8++igTJ07kmmuuYc6cOcyZM4fatWtzyy23MHXqVIYNG8aoUaO47rrrqFGjBp07dyY6unTFiJiYGKZOncrevXv57W9/S0ZGBp06dWLu3LnUqVPnuNdoz549RcZyopRshEFRCYcSDZHyFUpPxDPPPFPiNhdeeCH3339/SOdcuXIlt956K6mpqcct79ChA2vWrKF169bExcUBXlmkc+fOAKxateqYXoeMjAw6duxIdHQ0UVFRTJkyhZSUFFavXo2ZkZOTQ7t27Y45zzvvvMOSJUuYNGkSCQkJXHHFFZx88sns3LmT2rVrExtb+J/91atX0717d/bs2UNMTMxxyUaoPRtfffUVH330EfPmzSMzM5P9+/dzxx13MH78+OP2rVGjBu+99x5z587lgQceICUlhW3bthXaI1XUc/Ptt9+SlJSU166inv+S2lJUz0Z2djZjxoxhyJAhDBgwAPB6ALZs2ZK3zdatW2nSpEmRywGaNm1K06ZN89o2aNAgJk6cyGeffUbLli2pX78+AAMGDGDx4sUMGzaM1NTUvHZMmDAh71jFnacwderU4fTTT2f+/Pl06tSJr776iq+++orzzjvvmNeof//+RcZyopRshEnBhCMuJpq7+ycGGZKEwY033siOHTuYPHly0KFIhGjcuDGffvopI0aMIDo6GjOjU6dOeeM1VqxYwcaNG8nKyiI7O5unn36ae+65B/A+RHIvELdhwwamTp2a9y08Ojqajz76iFdeeYWtW7fywgsvcPvttx93fjPjlFNOISEhgVmzZrF48WI6derE6tWri62/r1y5klNOOYWhQ4dy0003HXfsUHs2br/99rx9Fy5cyAsvvJCXaFx99dX85S9/yftgXLt2LW3btiU5OZlVq1aRlZVFo0aNWLlyZd7xdu7cSf369Yt8bubOnYtzrsTnP/94hVB7No4ePcp9991H+/btueaaa/KWd+/enbVr17JhwwaaNGnC9OnT+etf/0q7du0KXQ7QqFEjmjZtypo1a2jfvj0LFiygQ4cONG/enPT0dA4ePEj16tVZsGBBXrlkx44dNGjQgE2bNjF79mzeeOONYs+f386dO4mNjaVOnTocOnSIzz77jOuvvz7vNTr33HPp1avXMa9Renp6kbGcKCUbYZSbcLydlsE3Ow4x2zYxpFurgKOSsrRgwQKysrKCDkMiyIgRI1i4cCGDBw+mevXqnHzyyYwfPx4zo3v37nzzzTcMGDCAyy67jEOHDvGb3/yGnj17AjBs2DA++eQThgwZQnx8PH/84x+pV68eALGxsfTt25fY2FgSEhI4dOhQXqkhv+HDh3PTTTfx7rvv0qdPH1q1akXNmjVp3749u3bt4qKLLmLcuHGceuqpx+yXG1+7du244447ePzxx+nbty/VqlUrk+flyJEjrF+/nrp16+Yte+aZZ1i8eDE1a9akY8eOPProoxw+fJjbb7+d5ORkYmNjGTNmDP379y/yucnIyKBHjx4lPv8/RVpaGlOnTqVTp0553+7Hjh3LOeecwwMPPMCoUaPIyclhxIgRnHzyyQBFLge4//77ueOOO8jOzqZVq1Y89thj1K1bl4EDB3LJJZcQGxtLly5duPTSSwFvbNHu3buJjY3lwQcfzBtQGhsbW+R5rr/+eh599FF27drF3XffTU5ODkePHmXQoEGFvl/yS0pKKjKWExX1U0YaV1ZpaWltge8SExOJj48vs+NOmjWfa2ev5Ze92jNpZJ8yO26kKq72WdlU1cuVV6XXGKpee6Hs25yRkcGbb76Z14sTaaraaxyO9mZmZrJ06VKAdr169Vqbf52mvpaDbg1q0LpeAtOWbeBQdtnNARcRqSg6deoUsYmGhJ+SjXIQFRVFalIb9h7KZrZtCjocERGRcqVko5ykJrUBYMqSdQFHIiIiUr40QLSc9G7VgLb1E5i2dCOHsnOoXi0m6JCkDPTu3TvvGgkiIlI49WyUk6ioKFJ6tGFfZjazVEqpNJ5//nnuu+++oMMQEYloSjbKUWrPtgBM/nptoHGIiIiUJyUb5ahXy/q0rZ/Au8s3cjD7cNDhSBmYNGlSsbfsFhERJRvlypuV0pb9mYeZtVKllMpgwoQJIV9ZUUSkqlKyUc5yZ6VMTtesFBERqRqUbJSzU1vWp139WrynUoqIiFQRSjbKWe4FvvZnHmamSikiIlIFKNkIQGpPv5TytUopIiJS+SnZCMApLerToUFtlVJERKRKULIRgKioKFKSWnMg6zDvr1AppSJbtGgR//73v4MOQ0QkoinZCEhqUlsAJqevDTQOOTFxcXFUq1Yt6DBERCKako2A9GxRj44NvVLKj1kqpVRUGRkZrF+/PugwREQimpKNgHillDb8mJXD+yu/Dzoc+YlSU1O59957gw5DRCSiKdkIUN4FvjQrRUREKjElGwFKal6PkxvWZvoKlVJERKTyUrIRoPyllBkrVEoREZHKSclGwPIu8KV7pYiISCWlZCNgPZrVo1OjOkxfvpEDmdlBhyMiIlLmlGwELPdeKQezc5ihe6VUOE888QRjx44NOgwRkYimZCMCpOTNSlkbbCBSaueeey6nnnpq0GGIiEQ0JRsRoHuzk3CN6jBjxffsVylFREQqGSUbESAqKorUnl4pZfpyzUqpSM4//3xuuummoMMQEYloSjYiRG4pZcoSzUqpSH744Qd2794ddBgiIhFNyUaESGx6Ep0b12HGcpVSRESkclGyESG8WSltOXQ4h/eWbww6HBERkTITG64DO+dqAS8B9YB44GFgC/BP4CiwxMxG+9veCaT6yx82sxnOubrAq0BdYD8w0sx2OufOB/4I5AAzzOwR/xh/A37uH+MWM1vknGsFvAzEAJuBK80sM1xtPlEpSa155IMlTElfz2WntAs6HBERkTIRzp6NXwFmZv2AFOAJ4HG8RKAPUNc5N9g51w64DDgLuAiY4JyLAW4FPjazs4C3gLv84z4JjAD6AAOcc12dc+cAJ5vZGcB1/jYA44CnzawvsAq4NoztPWHdmp5ElyZ1eX/F9+w7pFKKiIhUDuFMNrYDDfzf6wE7gXZmtshf9i5wPtAPeN/MsszsB2Ad0BXoD7ydf1vnXHtgp5ltMLMjwAx/u/7AOwBmtgKo55yrA5wLTCtwvoiVe4EvlVIqjssuu4wLLrgg6DBERCJa2JINM3sNaO2cWwXMA+4AduXbZBvQDGgK/FDC8tJsi/97UyAhX9kkd9uIlneBL90rpUK45557uPrqq4MOQ0QkooVzzMYvgfVmNsg5l4TXS7En3yZRRexa2PLSbFvaYxxn6dKloW4asrS0tJC3bVc3nveXb2Te51+QUC2mzGMpD6Vpb2VQ1doLVa/NVa29UPXarPaGT9iSDbwxFbMAzCzdOVcDqJZvfQtgk//jiljeFC9BKbis4LZZBZY3xxsQut85V8PMDubbtkSJiYnEx8eH1soQpKWl0atXr5C3v2p7LA/PXsLGuEaMPLXiDRQtbXsrsvvvv5+tW7cyceLEoEMpV1XpNYaq116oem1We09cZmZmkV/WwzlmYxVwOoBzrg2wD1jhnDvLXz8cmAl8BCQ75+Kcc83xkoLlwGy8GSrgDQidaWZrgTrOubbOuVi8AaWz/Z8U/1ynApvMbB/wob9v3jHC19yyo3ulVBzTpk1j/vz5QYchIhLRwtmz8SzwgnPuE/88N+JNfX3WORcNLDSzDwGcc8/hjes4Cow2syPOuSeBV5xz84HdwC/9444G/uP//rqZZQAZzrk059xnwBHgt/76B4GXnHM34A08nRTG9paZrk1PolvTusyyTew9lEWd6nFBhyQiIvKThS3ZMLP9wC8KWdW3kG2fAp4qZP+LC9l2HnBGIcvvLmTZZqBCThVITWrLQ7PSeXfZRq7o1T7ocERERH4yXUE0QmlWioiIVBZKNiJUlyZ1SWx6ErNWeqUUERGRikrJRgRL7dmGrJwjTFumC3xFqjZt2tC0adOSNxQRqcKUbESwlB65s1JUSolU06ZNY/z48UGHISIS0ZRsRLDOTerSvdlJzLZN7DmoUoqIiFRMSjYiXGqSSimRbMaMGXz22WdBhyEiEtGUbES4/81KWRtsIFKoe+65h3/84x9BhyEiEtGUbEQ417guSc3rMds2s1ulFBERqYCUbFQAKUltyM45wrRlG4IORUREpNSUbFQA/7tXimaliIhIxaNkowLo1KgOPZvX44MMlVJERKTiUbJRQeSWUqYuVSlFREQqFiUbFYTulRKZZsyYwd/+9regwxARiWhKNiqIkxvV4ZQW9fnANrHrx8ygwxFfixYtaNSoUdBhiIhENCUbFUhKUmsOHznKOyqlRIzdu3ezb9++oMMQEYloSjYqkNxSyhSVUiLGOeecw+jRo4MOQ0QkoinZqEA6NqzDqS3r82HGZnaqlCIiIhWEko0KJjWpjVdK+UalFBERqRiUbFQweaWUJSqliIhIxaBko4Jp36A2vVrWZ07GZnYcUClFREQin5KNCig1qa0/K2V90KGIiIiUSMlGBZSS1BrQvVIiwf3338+1114bdBgiIhEtNugApPTaNahN71YN+GjVFnYcyKRBQnzQIVVZKSkppKWlBR2GiEhEU89GBZWa1IacI0d5+xuVUkREJLIp2aigRvTwSym6wFegLrvsMn7/+98HHYaISERTGaWCategNqe1asDcVVvYvv8QDWtVDzqkKmnFihVkZWUFHYaISERTz0YFlldK0b1SREQkginZqMDybjv/9dpgAxERESmGko0KrE39WvysdQM+Xr2VH/YfCjocERGRQinZqOBSk9pqVoqIiEQ0JRsVXO6sFN12Phj9+/end3vKmzYAACAASURBVO/eQYchIhLRNBulgmtTvxant27I3FVb2bbvII1r1wg6pCplwoQJuqiXiEgJ1LNRCaT2bMORo5qVIiIikUnJRiUwood/23ndK6XcPfnkk7z++utBhyEiEtGUbFQCresl8PM2Dfl4tVdKkfLz/PPP8+677wYdhohIRFOyUUmkJnmllLe+USlFREQii5KNSiKvlJK+NthAREREClCyUUm0qpfAmW0b8cnqbWxVKUVERCKIko1KJCWvlKILfImISORQslGJ5F3gS7NSyk3NmjWpXl133BURKY4u6lWJtDwpgT5tG/HJmq1s2XuQpnV0ga9wW7BggS7qJSJSAvVsVDIpSW04ehTeWqJSioiIRAYlG5XMCP+281OWqJRSHhYtWsTy5cuDDkNEJKKpjFLJtKhbk7PaNWbemq1s3vsjzerUDDqkSm3UqFFkZWVx5ZVXBh2KiEjEUs9GJZSS1FqlFBERiRhKNiqh4T3aEBWl286LiEhkULJRCeWWUuZ/t41Ne34MOhwREanilGxUUqmalSIiIhFCyUYlNbxHa6+UolkpIiISMM1GqaSa1alJX7+U8v2eH2lRV7NSwmHSpEmsWLEi6DBERCKaejYqsdSktn4pRb0b4dKzZ086deoUdBgiIhFNyUYllltKmax7pYiISIBURqnEmtapwdntm/DJ6q0qpYRJ7969OXToEEuXLg06FBGRiKWejUou1b98+Zu65kZYZGdnk5OTE3QYIiIRLaw9G865K4DfAYeBB4AlwMtADLAZuNLMMv3tbgWOABPN7HnnXDXgRaANkANcY2ZrnHNJwD+Bo8ASMxvtn+tOINVf/rCZzXDO1QVeBeoC+4GRZrYznG2ONJd0b83Nb3/B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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "elbow_visualizer = KElbowVisualizer(KMeans(random_state=42), k=(2,10), timings=False)\n", + "elbow_visualizer.fit(X)\n", + "elbow_visualizer.size = (600, 400)\n", + "elbow_visualizer.poof()" + ] + }, + { + "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": [ + "model = KMeans(4, random_state=42)\n", + "silhouette_visualizer = SilhouetteVisualizer(model, colors='yellowbrick')\n", + "\n", + "silhouette_visualizer.fit(X)\n", + "elbow_visualizer.size = (600, 400)\n", + "silhouette_visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "venv" + }, + "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/examples/rebeccabilbro/barchart_colors.ipynb b/examples/rebeccabilbro/barchart_colors.ipynb new file mode 100644 index 000000000..c09e85a60 --- /dev/null +++ b/examples/rebeccabilbro/barchart_colors.ipynb @@ -0,0 +1,383 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Coloring bars\n", + "\n", + "Demo of using color params for barchart-based visualizers" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sklearn/externals/joblib/__init__.py:15: DeprecationWarning: sklearn.externals.joblib is deprecated in 0.21 and will be removed in 0.23. Please import this functionality directly from joblib, which can be installed with: pip install joblib. If this warning is raised when loading pickled models, you may need to re-serialize those models with scikit-learn 0.21+.\n", + " warnings.warn(msg, category=DeprecationWarning)\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "from sklearn.naive_bayes import GaussianNB\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.preprocessing import LabelEncoder\n", + "from sklearn.datasets import make_classification\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.model_selection import StratifiedKFold\n", + "from sklearn.ensemble import GradientBoostingClassifier\n", + "from sklearn.feature_extraction.text import CountVectorizer\n", + "from sklearn.preprocessing import LabelEncoder, OrdinalEncoder\n", + "\n", + "from yellowbrick.features import Rank1D\n", + "from yellowbrick.datasets import load_game\n", + "from yellowbrick.datasets import load_hobbies\n", + "from yellowbrick.datasets import load_concrete\n", + "from yellowbrick.datasets import load_occupancy\n", + "from yellowbrick.text import FreqDistVisualizer\n", + "from yellowbrick.model_selection import CVScores\n", + "from yellowbrick.features import FeatureImportances\n", + "from yellowbrick.contrib.missing import MissingValuesBar\n", + "from yellowbrick.target import ClassBalance, FeatureCorrelation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Rank1D\n", + "\n", + "has a `color` param now" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYUAAAEFCAYAAAAMk/uQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAZ4klEQVR4nO3deZgdZZn38W8ISQdkE3HAoING4UY2cVohLCFBWcRxBMlEYuA1iSOEkYiIGTKCI4qoCBHnBRkWASPKABOi4wYEVFZRlkYNEbhRBAYR2QQJDtlI5o+qLo5Nd6fXU+nk+7muvvqc2s5dT3fXr556zqketmrVKiRJAliv7gIkSWsOQ0GSVDEUJEkVQ0GSVDEUJEkVQ0GSVFm/7gK0ZoqIscAXgVdRnDw8AszKzF9HxATgq5m50wC91nuB/TLz2H5s4zPAMcCj5aRhwCbAd4BPZGaf3nsdETdQ7OuVHaZfCFyemT/qa809fP0DgK8BTwD7ZOYLDfP2Bv6d4u94CTAzM+/sZBsPAUuBFxom/yEz393Hmt4AzMnMiX1ZX2s2Q0EvExEtwA+AAzLzrnLaEcDV5QFhQGXm94DvDcCmrsjMme1PIuKVwEJgQfk1YDLzwwO5vW5MBr6Wmad2Mu9bwIcy8ycR8T7gG8COXWzn8M4Co4+2AWKAtqU1jKGgzmwIbAZs1DDtUuA5YHj5fKOIuBzYHhgFHJmZN0fEdsA55bqjgV8Ch2XmkohYQXFmuy/wCuDEzPx2REwD/jEz31Oemf+p3O65FGf65wKvpzj7/0ZmntHD/diy3JdnACLiQ8AMYCSwOXBaZp5bvv77gJXAtsAy4IOZuah9QxGxPvCfwHJgKvAj4KvAncCPgauA3cvtnpSZV0TEhsB5wFjgWeAegMyc1lhkRIwAzgTeCbwI3AZ8HDgaOAR4ISI2zcx/6bB/w4FXlo83pugt9EpEbF3ux98CIyh6P18o551Yvv4oip/XLIrwvhDYOiIWULTnoszcqFzn9e3Py3b9p3LdP2fmvhHxT8BHKHqfT1P0bu4rez1nlvu0CvhiZs7v7f6o/xxT0Mtk5jPACcA1EfG7iPgmMB34UWYuKxd7LfCVzNwVOB/4TDn9SIoD9x7Am4A3AH9fzhsO/CkzW4H3AxdHxKs7KeGZzNwhM8+mCKPrM3NnYC/giIiY3EXph0XELyPi/oh4GjgbmJGZt0fERmVt787MtwKHAac3rDse+Gh5SeynQOMBeCQwj+ISzhGZuaLD644BFmTmbsDshu3+G8WJ1/bAfsBbu6j7UxQB+pbyaz3gjDL8vkfRzh0DAeBDwCUR8XvgP4CZnSzT7tKybdq/di2nfxO4uPyZ7AbsFxHvj4htyprHZ+YuwEnAKZn5IvBh4IHMPLCb12u3IzChDITxFIE6rvwZnA58u1zus8CZZR0fAt7Rg21rEBgK6lRmnklxpn0s8BjFwe4XEbFpucgDmXlb+fiXwN+Uj2cDT0bECRRn+KP56x7HV8vtLwTuBvbp5OVvBoiIV1AEwTnlOn8G5gIHdVH2FWVI7QTMpzhDvbpc93ngPcDfR8TnKA5yjXW1Zebvy8d3UZzxt/sysD/wuS7GJpZT9BQ6rvtu4KLMXJmZz1Fc3unMQcB5mbk8M1dShFlX+whARGxJMdYwPjNfCxwBXFm2WWcOz8xdG75+WS47HvhcRPwS+DlFj2HXzHyY4gB+eEScRtFr2aiLbXdnYbnvUJwcvAm4tXy904HNI2Jz4L+AcyLiUqAVOLEPr6UBYCjoZSJir4j4l8xcnJk/yMwTKM74VlIcHKE4ELZbRXFpB+Ay4CjgYeArFAfJYQ3LNp5lr0dxuaSj5xvmD+swbz2KyxxdKnszMykuqZxe7tNrKcJrG+AWirPzRo2DsI37A8XZ9LkUB+HOLCsP5h3XXdFhO53tK7z873C1+wiMAx5uHyfIzP+m+Jm8eTXrNRpe1rdne1hQXOr6QkT8HXArxWD9tcCXePnPAl7eViM7zH++4fFw4JsNr/V3wNsoeobnAzsD1wEHAgsbTkDURIaCOvMk8KnyOm+711Cced+9mnUPpLjMcAXFAWN3XhqHAPggQHnQ2R64sasNZeZiirPXY8p1Ni3Xv251O1AGwz8DM8rXelu5X6dm5gKKXgMRMbzrrVRup7gU9KaIOLIHy7f7ITA9ItYrxxemULRJRwuAoyNiRESsR7G/q9vHhcBO5RgOEbE7xfjJ/T0trjyD/zlwfLmNzSgunR1M0YO7s+wx3kgxttDeVit4KbSeBUZGxA7l8/d185LXAh+IiNeUz4+mGI8hIm4F3pqZcylOKjbjpfESNZGhoJfJzPspDgJfKMcU7qHo3h+Vmbma1U8EvhMRd1IMst5Iccmg3V4RcRdwMcUA9DOr2d7hwDsj4m6Kg/N8iktIPdmPWyjGJL5KcZD9PZAR8QuKyyRPdqitu20tAaYBZ0TEG3uyDsVbepdQBOmPKMYk/reT5U4F/kjRk7mX4oD7sdXUcz/FQXV+RCwE/j9waMOlmp6aAowt2/c24LLMvJSix7dF+bNvozjj3zwiNgZ+DbwYEbdTvPngBIp3pt1B56HXXvMCih7HdWXNU8qaV5XbOKX82VwPfDYzH+rlvmgADPPW2WqWiFgFvDozn6q7lmYoB8Sfy8yryh7AfODazDy35tKkLtlTkAbPIuCkclB1EfAHirdzSmssewqSpIo9BUlSZch+ormtra0FeDvFe+i7equfJOmvDad4N+Edra2tSzvOHLKhQBEIN9ddhCQNUeMoPrPzV4ZyKDwGsN122zFyZMfPy6w7Fi1axE47DcjNSocs28A2ANsAetYGy5Yt4/7774fyGNrRUA6FFwFGjhxJS0tL3bXUal3ff7ANwDYA2wB61QadXnZ3oFmSVDEUJEkVQ0GSVDEUJEkVQ0GSVDEUJEkVQ0GSVBnKn1MAYNa8+Sxevnz1C67NFi5a/TJrO9vANoC1pg3mTp9a22vbU5AkVQwFSVLFUJAkVQwFSVLFUJAkVQwFSVLFUJAkVdbIUIiIURHx4brrkKR1zRoZCsBWgKEgSU222k80R8QGwNeBbYCRwHHANGBbilD5VGbeEBF3AzcBuwD3AY8D+wBLgXcDGwIXAa8qN31sZt4dEb8BfgpEuc5E4CRgh4j4dGaeMjC7KklanZ70FI4GHsrMPYDJwN7AU5m5D3AwcE653MbAf2bmOIp/CH1rucxIYEfgRODHmbkvcBRwbrneGODfyu2/Gng78HngHgNBkpqrJ/c+CuBqgMz8TUS8DhgXEbu3byMitigf31V+fxa4p3z8DDAK2Bl4R0QcVk7fvPz+VGY+Uj5+pFxWklSDnvQU7qU4eycixlD0Fi7LzAnAQcA84E/lsqu62c59wFfK9d4PfKubdVb2sDZJ0gDqyYH3fGBMRNwIXEIRBNuXz28FHs7MlT3YzueB90fEDcA1QHe3M3wCGBkRX+rBdiVJA2S1l48ycwkwpcPk2zpZ7vUNj8c2PD6kYbHGx+3zt2p4PLlh1q6rq02SNLC8RCNJqhgKkqSKoSBJqhgKkqSKoSBJqvTkw2trtDmTJtLS0lJ3GbVpa2ujtbW17jJqZRvYBmAbDBR7CpKkiqEgSaoYCpKkiqEgSaoM+YHmWfPms3j58rrLqNfC7m4jtY6wDWwD+Ks2mDt9ao2FDF32FCRJFUNBklQxFCRJFUNBklQxFCRJFUNBklRpWihExISIuLxZrydJ6j17CpKkyqB9eC0itgO+DqygCJ8LGubNBA4FXgE8BbwPGA5cAowGHgH2yczRg1WfJOnlBrOnsD9wO7AfcDKwKUBErAe8CtgvM3enCKa3A0cBD2bmXsBngC0HsTZJUicGMxQuAp4FrgFmUvQYyMyVwDLgsoi4CHgtMAJ4M3Brucx9wJODWJskqRODGQoHAzdn5juBecBsgIjYBTgkMw8DPlrWMAxYBOxRLvNGYItBrE2S1InBvCHencA3IuJTFOMFZwO7Ab8F/hIRPy2Xe4xiHOEiYG5E3AQ8DCwZxNokSZ0YtFDIzAeAvbuY/Y6OEyJiT+CizLw2IrYF9hys2iRJnVuTbp39O4pxhpMpxhiOqbkeSVrnrDGhkJl/BPatuw5JWpf54TVJUsVQkCRVDAVJUmWNGVPoqzmTJtLS0lJ3GbVpa2ujtbW17jJqZRvYBmAbDBR7CpKkiqEgSaoYCpKkiqEgSaoM+YHmWfPms3j58rrLqNfCRXVXUD/bYK1qg7nTp9ZdwjrLnoIkqWIoSJIqhoIkqWIoSJIqhoIkqWIoSJIqa0woRMTciHhX3XVI0rpsjQkFSVL9Bv3DaxGxCXAhsBkwGjgHGAZMBVYCd2TmseXiMyLiBGBT4J8z8/bBrk+S9JJm9BTeBFyemQcABwDHA9OBmZm5B3BvRLSHU1tmvgM4G5jWhNokSQ2acZuLx4HjIuJQ4DlgBEUozIqINwA/o+g5ALSV3/8IbNiE2iRJDZrRU/gE8LPMPAKYRxEARwJHZ+Z44K3AnuWyq5pQjySpC83oKXwfODsiJgPPAiuAe4CbI2Ix8ChwG0XvQZJUo0EPhcy8Htipk1nnd3g+rWGda4BrBrEsSVInfEuqJKliKEiSKoaCJKliKEiSKoaCJKky5P9H85xJE2lpaam7jNq0tbXR2tpadxm1sg1sAw0cewqSpIqhIEmqGAqSpIqhIEmqDPmB5lnz5rN4+fK6y6jXwkV1V1A/28A2gHWiDeZOnzqo27enIEmqGAqSpIqhIEmqGAqSpIqhIEmqGAqSpIqhIEmqNDUUImLziJjSzNeUJPVcs3sKuwDvbfJrSpJ6qF+faI6INuAg4BngaWBCZt4VEXcBC4C3Aa8CfpWZ04GTgLdExFHA1cAFwAbAC8BRwHDg++W2rsrM0/tTnySpd/rbU/gucCCwN/AgsF9E7AA8BDyTmftTBMPYiNga+Dzwk8y8AJgDnJWZE8rHp5Xb3Ao4wECQpObr772Pvk1x9v8/5fdjKYLmMmD3iLgMeB7YCBjRYd2dgRMjYjYwDGi/gdGDmbmsn3VJkvqgXz2FzFwEjAF2A66iOPgfDCwDXpeZHwBOpLhENAxY2fCa9wGzy57CDGBeOX1lf2qSJPXdQAw03wA8mZkrgRuBJ4DbgDERcRNwJfA7YDTwALBzRBwHzAJOjogbgUuAhQNQiySpH/p96+zMnN3w+JMNs97exSpvbnh8YCfzx/a3JklS3/jhNUlSxVCQJFUMBUlSxVCQJFUMBUlSpd/vPqrbnEkTaWlpqbuM2rS1tdHa2lp3GbWyDWwDsA0Gij0FSVLFUJAkVQwFSVLFUJAkVYb8QPOsefNZvHz56hdcmy1cVHcF9bMNbAMY8DaYO33qgG5vKLCnIEmqGAqSpIqhIEmqGAqSpIqhIEmqGAqSpMqgh0JE3BAR2/dynZ0jYp/BqkmS1Lk1tacwEdih7iIkaV3T5w+vRcR2wNeBFRThcgHwQWAlsBVwQWae07D8a4FzgVHAa4BPZeZ/R8TngX3LWuYD3wKmAcsi4q7MvL2vNUqSeqc/PYX9gduB/YCTgU2BrYH3AmOBj0fE3zQsvz3w5czcHzgKOKacfjgwBRgHPJuZjwJzgTMNBElqrv6EwkXAs8A1wEyKHsOtmbk0M18AFgFvbFj+MWBGRHwTOBoYUU4/HDgNWABs1o96JEn91J9QOBi4OTPfCcwDZgO7RsTwiNgQ2BH4TcPynwMuycz/B1wPDIuIFmAS8AGKS0jTImIbiktQa+p4hySttfpz4L0TOCUifkJx5n82xdn/1cDNwKmZ+VTD8vOAORFxE8Wlpy0ycynwJ+DnFEFxLfA/QBswMyL27Ud9kqRe6vNAc2Y+AOzd/jwiJgC7ZebkDstNKB/eB1zWyXZOAU7pMPmH5ZckqYm8RCNJqgzY/1PIzBuAGwZqe5Kk5rOnIEmqGAqSpIqhIEmqDPn/0Txn0kRaWlrqLqM2bW1ttLa21l1GrWwD2wBsg4FiT0GSVDEUJEkVQ0GSVDEUJEmVIT/QPGvefBYvX153GfVauKjuCupnG9gGsM60wdzpUwdt2/YUJEkVQ0GSVDEUJEkVQ0GSVDEUJEkVQ0GSVDEUJEkVQ0GSVOnxh9ciYgPg68A2wEjgOGAGMAYYDpyZmVdExHjgZIrA2QiYAiwDvg88DVwFPA9MBVYCd2TmsRHxOuACYAPgBeCozHxkIHZSktQzvekpHA08lJl7AJOB8cCTmbknsB9wakRsAewIHJGZE4BvA5PK9bcCDsjM04HpwMxyW/dGxPrAHOCscr05wGn93TlJUu/0JhQC+BlAZv4GeA1wU/l8MXAP8EbgUeCsiJgL7AuMKNd/MDOXlY+nA8dExI0UPY9hwM7AiRFxA/BpYMs+75UkqU96Ewr3Am8HiIgxwAeAceXzjSkO6g8CXwOmZ+Y04A8UB3woLhW1OxI4OjPHA28F9gTuA2aXPYUZwLw+7ZEkqc96c0O884GLy7P74cC7KM72b6EYB/hsZj4REd8Cbo6IvwCPA6M72dbd5TKLKXoWtwGzgHMjYlS5vY/1dackSX3T41DIzCUUg8aNbu9kueO72MTYhmUuBC7sMP93wIE9rUeSNPB8S6okqWIoSJIqhoIkqWIoSJIqhoIkqTLk/0fznEkTaWlpqbuM2rS1tdHa2lp3GbWyDWwDsA0Gij0FSVLFUJAkVQwFSVLFUJAkVYb8QPOsefNZvHx53WXUa+Giuiuon21gG8A60QZzp08d1O3bU5AkVQwFSVLFUJAkVQwFSVLFUJAkVQwFSVLFUJAkVQb9cwoRsT5wHRDAJzPzG4P9mpKkvmnGh9dGA5tk5ugmvJYkqR+aEQrnAdtGxPnAL4D7gNnAMmAMcHlmfj4iXgdcAGwAvAAclZmPNKE+SVKpGWMKHwHuAR5rmLYNMBEYC5xQTpsDnJWZE8rHpzWhNklSg7rufXR3Zq4AVkTEC+W0nYETI2I2MAxYx29oJEnNV9e7j1Z1Mu0+YHbZU5gBzGtqRZKkNeouqbOAcyNiFMW4wsdqrkeS1jmDHgqZ+RDF2EGjGxrmb1V+/x1w4GDXI0nqmh9ekyRVDAVJUsVQkCRVDAVJUsVQkCRV1qS3pPbJnEkTaWlpqbuM2rS1tdHa2lp3GbWyDWwDsA0Gij0FSVLFUJAkVQwFSVLFUJAkVYb8QPOsefNZvHwdv6HqwkV1V1C/dbAN5k6fWncJWgvZU5AkVQwFSVLFUJAkVQwFSVLFUJAkVQwFSVLFUJAkVQwFSVJl0D+8FhGbABcCmwGjgXOAtvL7YuAJYElmTouIjwJTgFXA5Zl51mDXJ0l6STN6Cm+iOMAfABwAHA+cB0zLzHcADwBExA7AYcDewDjgkIiIJtQnSSo14zYXjwPHRcShwHPACGB0Zv66nH8zMBnYCdgG+HE5/ZXAtkA2oUZJEs3pKXwC+FlmHgHMA4YBj5Q9A4Cx5fcEfg3sm5kTgLnAwibUJ0kqNaOn8H3g7IiYDDwLrABmAhdHxPPAMuDRzPxVRPwYuCUiWoDbgUebUJ8kqTTooZCZ11NcGqpExDHAP2TmkxFxKkUwkJlnAGcMdk2SpM7Vdevsx4Fry57CnwHvASxJa4BaQiEzrwSurOO1JUld88NrkqSKoSBJqhgKkqTKkP8fzXMmTaSlpaXuMmrT1tZGa2tr3WXUyjaQBo49BUlSxVCQJFUMBUlSxVCQJFUMBUlSxVCQJFUMBUlSxVCQJFWG8ofXhgMsW7as7jpqt3Tp0rpLqJ1tYBuAbQCrb4OGY+bwzuYPW7Vq1QCX1BxtbW17U/wrT0lS741rbW29pePEodxTuAMYBzwGvFhzLZI0VAwHXkNxDH2ZIdtTkCQNPAeaJUkVQ0GSVDEUJEkVQ0GSVDEUJEmVIfGW1IhYD/gP4C3AUuDDmfnbhvlHAjOAFcCpmfmDWgodJD3Y/48Dk8unV2XmZ5tf5eBaXRs0LPND4LuZeV7zqxxcPfg9OAg4GRgGtAHHZOZa9fbCHrTBJ4ApwErgC5n5nVoKbYKI2B34UmZO6DD9H4BPUxwPL87Mr/Vmu0Olp3AIMCoz9wD+Ffhy+4yI2Ao4FtgLOBD4YkSsbf+fs7v9HwMcDuwJjAUOiIhdaqlycHXZBg1OBV7Z1Kqaq7vfg42BM4D3ZObuwEPAFnUUOci6a4PNgI8BewAHAP9eS4VNEBEnABcCozpMHwF8hWL/xwNHRcSWvdn2UAmFvYFrADLz58DbGubtBvw0M5dm5p+B3wJr20Gxu/1/BHhXZr5YnhWOAJY0v8RB110bEBH/SHF2eE3zS2ua7tpgT+Bu4MsRcTPweGY+2fwSB113bfAX4GHgFeXXyqZX1zwPAId2Mv3NwG8z85nMXAbcAuzTmw0PlVDYBPhzw/MXI2L9LuYtBjZtVmFN0uX+Z+byzHwqIoZFxBzgF5l5fy1VDq4u2yAidqK4ZPDpOgprou7+DrYA9gVmAwcBx0XEdk2urxm6awMoTpLuAe4CzmpmYc2UmfOB5Z3M6vfxcKiEwnPAxg3P18vMFV3M2xh4tlmFNUl3+09EjAIuLZf5SJNra5bu2uCDwNbAT4BpwPER8a7mltcU3bXB08AdmfnHzHweuAnYtdkFNkF3bXAQxe0b3gD8LXBIROzW5Prq1u/j4VAJhZ8C7waIiLEU3eR2twPjImJURGxK0X1a1PwSB1WX+x8Rw4DvAr/KzBmZubbeB6rLNsjMEzJz93LAbS5wZmaujZeRuvs7uAvYKSK2KM+cx1KcMa9tumuDZ4AXgKWZuYTiYLhZ0yus173AthGxeUSMpLh09LPebGBIvPsI+A6wf0TcSvHOiukRcTzFtbPvRcRZFHdMXQ84qfyFWJt0uf8UN7caD7SU7z4B+GRm9uoXYQjo9neg3tKaZnV/B58EFpTL/ldmrm0nR7D6NtgP+HlErKS4nn5djbU2TURMATbKzAvK9lhAcTy8ODMf7c22vCGeJKkyVC4fSZKawFCQJFUMBUlSxVCQJFUMBUlSxVCQJFUMBUlS5f8A6Sz9yqPH6RYAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "X, y = load_concrete()\n", + "\n", + "visualizer = Rank1D(algorithm='shapiro', color=[\"cadetblue\"])\n", + "\n", + "visualizer.fit(X, y)\n", + "visualizer.transform(X)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## FreqDistVisualizer\n", + "\n", + "has a `color` param now" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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Rd1bpN966y2OP/54jj/+eJ588sk66y644AKee+45AN544w1+8pOfcOqpp/L886v9kL+sqqurN+hWj5SSkpIGd0souIuIFLChQ4fy4IOrnno7depUWrRowcMPP8wBBxzApEmT6mx/7733cv311zN+/Hjuvvvu2uXjxo1j/vz5ta//8Ic/cMcddzB69GhGjhy5wfaJrwlFRUW5N0pQkUhEZA0477zzGDx4MFtssQWDBw+mX79+zJw5k8cee4yioiLOP/98evfuzeTJk3nggQeoqqriF7/4BXvuuSdXX301y5YtY+nSpdx6660AzJw5kzPPPJPq6mpuuOEGNtlkE6699lpuuummOvvde++9MTMWLVoEwIcffsg+++wDwG677caIESPqbL/LLruwaNEiKioqam9TdnfmzJnDQQcdVLvdypUr2XLLLQHYdNNN+eyzz9hqq62a5sOLmdOxR6Om1+WLwpxLreBq7nM69sj4T0RkbTnyyCN56qmnqKqqwt3ZcssteeKJJ5g4cSJjxozhtttuY8WKFdxzzz089NBDjB8/nvfff59PPvmEM888k9GjR3PEEUfwwgsvAFBWVsaYMWMYOHAgd955J61bt64X2AF69+5d5/WOO+7ISy+FCc1eeeUVli9fXmf9FltswcUXX8yRRx5Jnz59qKio4Pbbb+eyyy6rs13Lli358MMPWbJkCW+//Xa9dGTtUs1dRGQN6N27N+effz5du3Zlv/3245NPPsHMageMbbrppnz99ddsttlmlJWVUVZWxnnnncenn37KmDFjeOyxx1iwYAE9eoSKyu67705RURHdunXjk08+yTsfvXr14vXXX+fUU0+lR48etG/fvs76O++8k8mTJ9OuXTvOPvtsOnbsyGeffcYFF1zAZ599xtNPP82OO+7Ib3/7W37729/Svn17unXrRrt27Rr181pXLF++nCuvvJJ58+axcuVKrrrqKh5++GE+/fRTqqqqOPvsszn88MM544wz6NChA9988w1HHHEEzz//PEuXLuWrr76if//+9OnTh4MPPpgpU6ZQVlbGLbfcwvbbb0+vXr245JJLqKmpoaKigmuvvZauXbt+53wruIuIrAGpAP7ggw9y+eWX07ZtW2bPnk1VVRUVFRV8/vnntG/fnvnz57NixQoqKyu58sor6dSpE4cccgiHH3441113XW167g7AjBkz2G677fLOx5w5c9h7770ZNGgQEyZMoHPnznXWt23blpYtW1JaWspGG23ErrvuyuOPPw7AHXfcQdeuXdlmm20YPXo0999/P1VVVfTv359OnTo1wqe07nn44YfZYostGDZsGHPnzuXJJ5+kQ4cO3HLLLSxZsoTjjjuOfffdFwitM4ceeiiPPvoo3377LQ888AALFy7khBNOyHif/4wZM2jfvj033XQT77//PsuWLWuUfCu4i4isIUcddRS3334722+/fe3rU045hcrKSgYMGEBpaSkXXHABZ5xxBjU1NVx88cUUFRVx3XXXMW7cONq1a0eLFi3o2rUry5cv58wzz6SmpoYbb7yRpUuXpu1zT+rcuTN/+MMfGDFiBNtvvz3XXXcdK1asYNCgQQwbNoxf/epXnH322ZSUlLD//vvTpUuXtOl07NiRk046ibKyMgYOHNjon9W64oMPPuDAAw8EYNttt+WLL75gv/32A2CjjTaiS5cutS0n8ULW3nvvTXFxMZtssglt27Zl4cKFddKtqakB4MADD2Tu3LlcdNFFNGvWjAsvvLBR8q3gLiKyhvTs2ZOePXvWvj7ttNM47bTT6mxz+OGHc/jhh9dZ9vTTT9dLKzUoLi5TYD/uuONq/27VqhX33XdfnfU1NTVss802QAg2qWCWdPHFF9f+fcQRR3DEEUek3a6QdOnShZkzZ9K7d28++eQT/v73v1NaWsqhhx7KkiVLmD17du3Awvio9n//+98ALFiwgCVLlrDxxhtTWlrK/Pnz2XLLLXnvvffo0qUL06dPp1OnTowaNYo333yTW2+9lbFjx37nfCu4i4gIp5xyytrOwjrp5JNP5qqrruL000+nqqqK++67j/Hjx3PKKadQUVHBgAED2Hjjjeu9b8GCBfz0pz9l8eLFDB48mJKSEs4991zOO+88tthiC9q2bQvAzjvvzMCBA5kwYQKVlZX079+/UfKt4C4isoErKipi0003XdvZyMuavnWtrKyMP/7xj3WWpXsQTrK2vffee9e7wyA1OVDSAw880Ag5ravgboUTERHZ0Cm4i4isR4455pgGv2fo0KG108YuX76c/v37c8YZZ/Czn/2Mb775hhUrVjBgwABOOeUUrr/++trBXinPPfccJ554IscffzyPPvpoxmVPP/00xx57LGeccQYzZsz4jke6/jruuOPq1drXNAV3EZECtXLlSgYOHMizzz5bu+zxxx9nr732YuzYsRx22GFMnDiRJ598kl133ZUJEyawbNkyZs2aVSedO+64gzFjxjBhwgRGjRpFVVVV2mXDhg1j7NixDB8+nGHDhq3pwy1YycJWPtTnLiKyBpx00kmMGzeOV199lbvuuouHH36Yu+++m1122YVPP/203jS0J554Im3atOGAAw6goqKC5557jh133LE2vYEDBzJ//nyKi4u55ZZb+Pbbb5k4cSKXX3557TYrV67k+OOPr3OL1pFHHklxcajXpZ40duyxx1JVVUVVVRULFy5ko402qpP3UaNG0bJlSyorK6mpqaG4uLjesq+++ootttiCNm3aAKzWk8ySiouLWbFiBaWlpaudRiGoqqpq8Geg4C4isgbstddezJgxg+nTp/Ptt9+yfPlyysvLOfbYY7nrrruYOHEiFRUVnHjiiRx88MEsWLCA4cOHU1paykUXXcSkSZNwdwYNGsTixYv5+OOPGTNmDLNnz2bx4sV06dKlTmCHcNvbfvvtR3l5ee2y1q1bA2EymwkTJjBu3DggBNKjjjqK1q1b07FjxzrppEaD33DDDfTr14+ioqJ6yzp06MD8+fNZuHAhK1eu5D//+Q8rVqz4TsG9WbNmfPvttyxbtoySkpLVeoDK+qympqa20NXQz1HN8iIia8CBBx7Ia6+9xkcffcQRRxzBSy+9RIsWLZg/f369aWi//PLL2iA7d+5cdthhB4qKith5551p0aIFbdq04dxzz+VXv/oVI0eOpKSkpEF5effdd7n00ksZNmxY7S1ZRUVFTJ48mX79+jFy5Mg621dXVzN48GBKS0s555xz0i4rLi7miiuuYMCAAQwbNow999yTli1bfufPrU2bNpSWlm5wgR3Cd1JaWlrbGtIQqrmLiKwB3bt35+6772azzTbjhz/8Iddeey0nnngiW2yxRdppaFNN5507d8bdqa6u5qOPPqKiooL58+fz0Ucfce+99zJ16lQefvhhrrjiirzyMW/ePC6//HKGDx/O1ltvDcCECRNo3bo1Rx99NC1btqzdd8rNN99MmzZt6gwSS7fsnXfeYfz48SxcuJDBgwc3WkDWM90bTp+YiMgakJqrfa+99qJbt27MnTuXXr16sckmm9SbhrZ58+a17+vYsSOHHXYYJ5xwAjvssAMtWrSgY8eOzJkzhxNOOIEWLVpwzTXX8NFHH9Xrc0/n/vvvZ+nSpVx99dVAeKDN0UcfzWWXXcakSZPYaKONuPHGG5k3bx4PPPAA559/PmPHjmWPPfbgjDPOAMKz3JPL7rrrLpo1a0a/fv1o2bIlQ4YMaZoPUvJStDqj8BpDeXn5tsCH3bp1q31mcGPI9mjXQn1ur4ism1588cWMU7mu61auXMmIESMYMGBAo6S3Pn8W66KKiorUXQ3bde/efW5yvfrcRUSknurq6nrz3sv6I69meTO7EjgaKAXuAv4JjAZqgFlAf3evNrPBwBFAJXCJu7/eFJkWEZGmlXqmvKyfcgZ3M+sF7AfsD7QCLgNuBa5x9xfM7B7gGDP7COgJ7ANsBfwF2LuJ8i0isl559NFHWbRoEWedddYa3e9HH33EoEGDqK6u5vDDD6+z/xkzZnDzzTcDsHjxYoqLixkyZEi9ZfFZ6V5//XWuuuqqNXoM0nD5NMv3AWYCjwF/AyYD3Qm1d4ApQG/gAOAZd69x94+BZmbWMU16IiKyhowbN46f//znTJw4kSeeeIKlS5fWrtttt90YO3YsY8eOpUuXLlx11VVpl0F4uMnNN9+8WrOlyZqXT7P8JsA2wJHAdsATQLG7p77hxUA7oC3wZex9qeVfZEs8Oc3hd9U+y7r4RA4iIk3tyy+/rA2mFRUVTJ06lWeffZbKykpuuukmmjVrxu9+9zuWLVvGsmXLGDp0KJ9//jkPPPAAK1as4Ntvv+X666+nc+fOXHvttXz11Vd8/fXXXH311SxfvpyRI0dyzz33cMkll3DWWWexYMECVq5cSd++fWvzsMMOO/DFF1/w9ddfU1VVxcqVK+sEeAjPHq+oqKBr166165LLNttsMwYNGsSLL75Y7/35mDt3bu0EOtL08gnuXwLvufsKwM1sOaHZPaUN8DWwKPo7uTyrRh8tn2Vd9+7dG20/IiK5LF26tDaglZWVseWWWzJ06FDuu+8+Xn31VXbZZRfOPvts9t13X0aPHs306dPZaaedqK6uZvz48ZSXlzNu3DguvPBC+vbtS58+fXjuuef4xz/+wRVXXMHzzz/PlVdeyU477cT++++fNg+bb745V1xxBSNHjqRHjx60b1+/CjRp0iTOP//8OsE3uaxv375Mnz6d5s2br1aQ3nbbbXUNbkSx0fJp5dMs/xJwmJkVmVlnoDUwNeqLB+gLTANeBvqYWbGZbU2o3S/4TrkXESkgZgZAhw4dWL58OR06dGDSpEkMGjSIadOmUVVVBYSpaouKith111354IMPaNeuHS+88AKXX345TzzxRO12Z5xxBk8//TQnn3xyxn3edtttPPjggzz77LMsXLiwXgvmypUrmTt3LrvuumvWZbJ+yRnc3X0y8CbwOqHPvT9wKXCtmb1KGEH/iLuXE4L8q4TBdP2bKtMiIoVgzJgxHHLIIdx4441su+22tcvfe+89AGbOnMm2227LY489xnbbbcdNN93EHnvsUdvvffPNN3PllVcydOjQjPvYaKONaNWqFcXFxWy88cYsWbKkznp3r/NAmkzLZP2S161w7p5uyqOeabYbAgz5blkSEdkw9OzZk+uuu45x48bRrl07WrRoQdeuXfniiy8488wzqaqq4sYbb2Tx4sVceumlPP/883Tq1ImioiIef/xxOnXqxFlnncUHH3zAE088QWlpKStXruSoo46q3cevf/1rBg4cSHFxMV26dKFHjx5MnTqVb775huOOO46PP/6Yzp0718lXumWyftngZqjTDHYisqaszqxs06dP57nnnqudHrYpzJs3j5deeokTTzyxyfaRpBnqGpdmqBMRWUvW1dvGmjdvTr9+/dbY/qqrq9fYviTQg2NERJpIRUUFFRUVDWqd3Geffdhnn32aMFfUe157U5s5cybbbbfdGt3nhk7BXUSkiRxyyCFMmTKFTTfdlHbt2m1wzySvrKxk3rx5dOrUia222ir3G6TRKLiLiDSRkpISjjzySJYtW8Y333yztrOzxpWUlGBm9Z4PL01PwV1EpIm1atWKVq1are1syAZExSkREZECo+AuIiJSYBTcRURECoyCu4iISIFRcBcRESkwCu4iIiIFRsFdRESkwCi4i4iIFBgFdxERkQKj4C4iIlJgFNxFREQKjIK7iIhIgVFwFxERKTAK7iIiIgVGj3xNY07HHhnXdfli2hrMiYiISMOp5i4iIlJgFNxFREQKjIK7iIhIgVFwFxERKTB5Dagzs/8DFkUvPwRGALcDlcAz7n6tmRUDdwG7AxXAue7+fuNnWURERLLJGdzNrAVQ5O69YsveAvoBHwB/N7M9ge2AFu7+IzPbF/gjcEyT5FpEREQyyqfmvjvQysyeibYfApS5+xwAM3sa6A1sDjwF4O6vmdkPmiTHIiIiklU+wX0ZcAtwH7Aj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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "corpus = load_hobbies()\n", + "y = LabelEncoder().fit_transform(corpus.target)\n", + "\n", + "vectorizer = CountVectorizer(stop_words='english')\n", + "docs = vectorizer.fit_transform(corpus.data)\n", + "features = vectorizer.get_feature_names()\n", + "visualizer = FreqDistVisualizer(\n", + " features=features, orient='v', size=(600, 300), color=[\"crimson\"]\n", + ")\n", + "visualizer.fit(docs)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CVScores\n", + "\n", + "has a `color` param now" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "X, y = load_occupancy()\n", + "\n", + "cv = StratifiedKFold(n_splits=12, random_state=42)\n", + "visualizer = CVScores(\n", + " GaussianNB(), cv=cv, scoring='f1_weighted', color=\"goldenrod\"\n", + ")\n", + "\n", + "visualizer.fit(X, y)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## FeatureImportances\n", + "\n", + "has a `colors` and a `colormap` param now; \n", + "`colors` is for `stack==False` and `colormap` is for `stack==True`" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "X, y = load_occupancy()\n", + "\n", + "model = RandomForestClassifier(n_estimators=10)\n", + "colors = [\"lightpink\", \"pink\", \"hotpink\", \"crimson\", \"orchid\"]\n", + "viz = FeatureImportances(model, colors=colors)\n", + "viz.fit(X, y)\n", + "viz.poof()" + ] + }, + { + "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": [ + "X, y = load_game()\n", + "X = OrdinalEncoder().fit_transform(X)\n", + "y = LabelEncoder().fit_transform(y)\n", + "\n", + "model = LogisticRegression(multi_class=\"auto\", solver=\"liblinear\")\n", + "viz = FeatureImportances(model, stack=True, relative=False, colormap=\"viridis\")\n", + "viz.fit(X, y)\n", + "viz.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MissingValuesBar" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No handles with labels found to put in legend.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY8AAAETCAYAAADOPorfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAgAElEQVR4nO3df3xcdZ3v8Veo/SG3BQV0S9m9NKuXt/JLMHhdsLUsrg8FBPxVt0tRWuFiV11dcW91e0UsWnF3FWoBBVlprUW7sKIrUvkhrRqqZcnULkTkw6UGrpemK6KFVmhqSfaP8w09TSfJnMmkM0nezzzyaOac73zP50zSec/5zpnzberp6cHMzKyIA+pdgJmZjTwODzMzK8zhYWZmhTk8zMysMIeHmZkV5vAwM7PCXlDvAqxxSBoHfBg4l+xvYwJwK/DJiOgahu29BrggIhYUvN+XgTcD34iI/5NbPg9YDnw6Ij6ZW94EbAaeiYhjJS0AXhQRn6ui5jXA30XEg0Xv26efZiCA5oh4vM+6+4FPRcQtA9y/B3hJRPxmKHUMUuOngMMi4oND6ONFwGXAqUA30ANcHRFfHeR+p6Z2x1a7bRtePvKwvC8DJwNviIgTgNcAAv55mLZ3DPDHVdzvfcDMfHDk/D9gbp9lM4EDe29ExLXVBEe67xlDDY7UTwdwJzAvv1zSycCLgH8b6jbqTdIk4EfA/wdenf6m3gr8vaQL6lqcDZmPPAx4/pXwXODwiHgaICJ+n16ln5LaHAxcA5xA9gry+8CiiNjd95Vw723gWGAJ8Mv080TgA8AjZK9ID5a0PCLm96nnGOBq4NC0rS9ExEpJrUAT8H1J74+I1j678gDwJ5JOiYifpGXnA6vIjlb2ekUt6a+BBcAuYCfwvoh4cIDljwLvBCaX26+IWCfpJWRHQC8DngS2Au0R8ak+tX4JWCbpsxHR+2ndi4DrIuI5SUelx3syMA3YBPxlROzMPU7zgHdGxFv63pY0AfgHYBYwDvgZ8KGIeLq//WNfr5T0Y+CQdP/3A8cDq4EjI6Jb0oHAo8CxEfHr3H3/EtgREf/YuyAiHpP0LrKj2n5/z/kCJK1Ij9/n+95Ov49vAGemPi4FXge0AH8Azo6ILandCuANwH8H/iUiFpbZX6uQjzys16uBn/cGR6+I2JobPllG9mR4HHAS8Crg7yro+7VkTwonAl8lG5L5FfBJoLVMcLwA+C5wVUQcD5wOfFbSyRExMzX78zLB0Wsl8O7U14FkRx63922UhumWAm+OiNcAXwFm9Le8kv1Ky5eRPZavBGaTwreM28mCcFaq52DgHOD6tP5/AV+LiJOBlwPNZE+Slfo4sBtoiYhXAVuAzxXYP9J230H2O28CPhER68n+Dt6c2swB7u4THJD9jazv22FEbIyIDQP9ngvsI8CktH8fTfvyxXT7V+x9ZDc5/f2cAvxNesFkVXJ4WK9uBv97OJ1sHLonvQdybVo2mMciYlP6eSPZq9iBHEX2hHALQERsAb7FnierwdwIvF3SeOBtZE9Qu/s2iojngJuBn0i6GngK+Gp/ywvs1xlkT2JERCfwr+WKjIhussfwvWnRecCa3JPwx4AnJC0kG1KcRnYUUqm3kIXRzyRtIhsyOrrA/gHcEhFPpCOj5cAb0/JryMINsmHEL5e572B/U0P9Pff6Vvp3M7A1Iv4jdzv/t/ZvaTuPA79m8L9DG4DDw3r9O9kQxZT8QklHSLpN0gvZ9+/lAGB87nZTus+EPu2ezf3c09tuAOX+Lvtuq18RsZXsyfwMsiGrFQO0PQ84i2wY7WPALQMt76O//drN3vv43ADl3gCcKekgsifjq3Prvkk2jPUYcGXap76PXd/HM//YjwM+HBEnpPcb/ifZkFul+9e39iayoSDIAnqGpD8ne0X/4zL33QD8Wd+Fks6W9E9U/nseaB8B8idz/IH+Ff07tAE4PAx4/tXYjcAN6YmM9O+XgCcj4lngDuADkpokTSR7YrsrdfEE2TAFwNsr3OxuygdCALskvT3VMY1s6OSuMm37s5JsGOPgiGgv10DSYZJ+RbZ/S4FPAK/qb3mBbd8GXJC2cSjZ0U/ZK5BGxJNkZ7QtBp6LiA251W8CLouIf0n3fy1ZIOQ9ARwraVIaBjort+4O4IOSJkg6gGw47PKC+3e2pBenoa6LyN7nIiKeIXsf6Qayo6dyvkX2ntbCdH8k/SlwBfALKv89P/+3JekwsmFIqzOHh+W9H3iQbDhjE3Bvun1hWv8h4KVkb0o/QPaff0lu3TWSNgInAp0VbO+nwCskfTu/MCL+QDbE8uF02uoPyJ5E1xXYl++QPSF+vb8G6c39zwB3SyoBnwMu7G95gW1/hGy/HiB7An0MeGaA9teQnSJ9dZ/li4BvS2oje4L+Edl7EHl3puUPAa1kv5denyZ7I/tnZL/HJuCjBffvQeB7qd9tqW2v5WR/DyvL3I+I2AX8BdlZdQ+k3+W3gM9ExA0Ffs9XAYdLCrIXOD/sp1bbj5p8SXaz2pL0fuBnEfHTdITWClwaEd+vc2k1kz478zGyM67+ut712P7nU3XNau9B4Ko0VDMBuHk0BUfyS7LhpLPrXYjVh488zMysML/nYWZmhY36YatSqTSR7DIbnQx8yqSZme0xDjgcuK+lpWWfa9uN+vAgC47+PolsZmYDmwnc03fhWAiPToCjjjqKCRP6fraosbS3t3PssY1/EVHXWVuus7ZcZ23s2rWLhx9+GPo57X4shMdzABMmTGDixIn1rmVQI6FGcJ215jpry3XWVNnhfr9hbmZmhTk8zMysMIeHmZkV5vAwM7PCHB5mZlbYgGdbSZoO3E82j0CvtRFxWZGNSLoIWJ6uojlkkt4N/G+ySWxWRER/E9mYmdkwqORU3Qcj4tQhbmcR2WWbhxwe6Xr+nyabNnUb8ANJd0fEo0Pt28zMKlP15zwkXU72ycNxwBURcbOkWWQT0B9ANl3muanNVGC1pKXAgoiYk/rYGhFT04T2h6bvM4GFffvObfpPgf+IiN+mPu4jm63s0YHqbaaZzoqmmKijlnoXUKFhqLOt1Fb7ToFSqTQs/daa66wt1zn8KgmPoyX9MHd7LnA80BwRMyRNAjZIuots0pfzImKLpEXA7IhYIukSYA5lpqTMWRsRV0o6vVzfEbEttfu/wDGS/gjYDrwBeLjAPlsDammpfSKVSqVh6bfWXGdtuc7a6Orqor297CScQJXDVpLmAi25UBkPTAceB5ZJ2gEcAawfpO/8HMKR/j2un743AUTE7yR9hGxGsifJ3o/5TQX7YWZmNVLt2VYPAetSqJwG3ARsJpsjeX5EzAO2sCccutO2dpJdpRFJRwKH5PrsHqRv0v1eQPZ+x0zgXcArGDykzMyshqoNj1uBHZJagRLQExHbgVVAq6T1wBRgWmrfCqxJbbdJuhdYDHQU6BuAiNidftxINpfxsjQns5mZ7ScDDlulM5j2eZ8iInqAi8ss32dZWn5+7uY5ZdbPG6zvPu0Xk4WPmZnVgT8kaGZmhTk8zMysMIeHmZkV5vAwM7PCHB5mZlbYmAmPDjroafCvtlJb3WuoV51mNrKMmfAwM7PacXiYmVlhDg8zMyvM4WFmZoU5PMzMrDCHh5mZFTZS5zCfC3wUeA64ISK+XIt+zcysMiNuDvPk82SzFu4AHpS0OiJ+V6O+zcxsECNxDnPIjoYOBnaTTTjlT5mZme1HI3EOc4B2somifg/c0mddWc0000lnBbtbR407nfHeXOeQtJXa9llWKpXqUElxrrO2Rkqd5Yy4OcwlHU92dNJMNmy1StLsMkcnZg2ppWXvVCuVSvssa0Sus7Yavc6uri7a29v7XT/i5jAHngKeBZ6NiOeAXwMvrnI/zMysCiNxDvPHgOuAeyTdA7wIWFHlfpiZWRVG6hzm1wLXDtTGzMyGjz8kaGZmhTk8zMysMIeHmZkV5vAwM7PCHB5mZlbYmAkPz2HuOhvly2w0GDPhYWZmtePwMDOzwhweZmZWmMPDzMwKc3iYmVlhDg8zMyvM4WFmZoUNeFVdSdPJpnzdmFu8NiIuK7IRSRcByyNiyHOYS5oKrM4tOgH4eLrSrpmZ7QdVzSRYhUXASmDI4RERW4FTASSdDCwhm4TKzMz2k0rCoyxJlwMzgXHAFRFxs6RZwKVkw2GTgXNTm6nAaklLgQURMSf1sTUipkpaARyavs8EFvbtu8z2m4CrgLlpRkEzM9tPKgmPo3PziQPMBY4HmiNihqRJwAZJdwHHAOdFxBZJi4DZEbFE0iXAHMpMLJWzNiKulHR6ub4jYluf9mcBP4+I2LerfTXTTCedlTStn8adznhvo6TOtlLb/qmjAqVSqd4lVMR11tZIqbOcqoatJM0FWnKhMh6YDjwOLJO0AzgCWD9I3025n3tD4Lh++t7U577nAV+soH6zslpaGiMFS6VSw9QyENdZW41eZ1dXF+3t7f2ur/Zsq4eAdSlUTgNuAjaTvfcwP00ru4U94dCdtrUTOBxA0pHAIbk+uwfpu6+TgJ9UWb+ZmQ1BteFxK7BDUitQAnoiYjuwCmiVtB6YAkxL7VuBNantNkn3AouBjgJ9P0/SS4Cn03znZma2nw04bBURj1LmfYr0pH1xmeX7LEvLz8/dPKfM+nmD9d2n/RNkp+iamVkd+EOCZmZWmMPDzMwKc3iYmVlhDg8zMyvM4WFmZoWNmfDooIOeBv9qK7XVvYaxVKeZVW/MhIeZmdWOw8PMzApzeJiZWWEODzMzK8zhYWZmhTk8zMysMIeHmZkVNuBVdSVNB+4HNuYWr42Iy4psRNJFwPKIGPIc5qm/1wBXkM0XspVs9sKdtejbzMwGV9VMglVYBKwEhhweae7y64F3RsQjki4EjmTPTIRmZjbMKgmPsiRdDswExgFXRMTNkmYBl5INh00Gzk1tpgKrJS0FFkTEnNTH1oiYKmkFcGj6PhNY2Lfv3KaPAp4EPiLpWOC2SucxNzOz2qgkPI7OzScOMBc4HmiOiBmSJgEbJN0FHEM2hLRF0iJgdkQskXQJMIcyE0vlrI2IKyWdXq7viNiW2h0GnAJ8EHgE+J6ktohYO9BONNNMJ50V7G4dNe50xntznbXlOmvLdT6vrdQ2bH1XNWwlaS7QkguV8cB04HFgmaQdwBHA+kH6bsr93Hv0cFw/fW9Kt58EHomIX6Rabiebz3zA8DAzG2taWqpPqK6uLtrb2/tdX+3ZVg8B61KonAbcBGwmey9ifppWdgt7wqE7bWsncDiApCOBQ3J9dg/Sd69fApMlvTzdngn8vMr9MDOzKlQbHrcCOyS1AiWgJyK2A6uAVknrgSnAtNS+FViT2m6TdC+wGOgo0DcAEbELuAD4hqT7gF9FxG1V7oeZmVWhqadndF+aulQqTQc6zjr2LDonNvh7HmZmNTSUqQdyw1bNLS0tj/Zd7w8JmplZYQ4PMzMrzOFhZmaFOTzMzKwwh4eZmRU2ZsKjgw56GvyrrdRW9xpcp+t0naOnzuE0ZsLDzMxqx+FhZmaFOTzMzKwwh4eZmRXm8DAzs8IcHmZmVpjDw8zMChtwMihJ04H7gY25xWsj4rIiG5F0EbA8IoY8h3nq7yPAhcATadH7PBWtmdn+U9VMglVYBKwEahIeZBM4viciSjXqz8zMChhwPo905LE6IvaZe1zS5WSz+I0DroiImyXNAi4lGw6bDJyb2lwD3A4sBRZExJzUx9aImCppBXBo+j4TWNi37z7b/gXZ7IFTgdsi4vL+9qFW83kM51zAZmYNrOx8HpUceRydm08cYC5wPNAcETMkTQI2SLoLOAY4LyK2SFoEzI6IJZIuAeYA+4RQztqIuFLS6eX6johtubaryQLpaeDbkt4SEd+rYF+qNpS5gCtVKpX2y3aGynXWluusLddZG4PNYV7VsJWkuUBLLlTGA9OBx4FlknYARwDrB+m7Kfdz73sWx/XT96a07SZgaUQ8lW7fBpwIDGt4mJnZHtWebfUQsC6FymnATcBm4HpgfkTMA7awJxy607Z2AocDSDoSOCTXZ/cgffc6CGiXNDkFyWlkc52bmdl+Um143ArskNRK9sTdExHbgVVAq6T1wBRgWmrfCqxJbbdJuhdYDHQU6BuAdMSxCFiX+v15RKypcj/MzKwKAw5bRcSjlHmfIiJ6gIvLLN9nWVp+fu7mOWXWzxus7z7tvw58faA2ZmY2fPwhQTMzK8zhYWZmhTk8zMysMIeHmZkV5vAwM7PCKvmQ4KjQQQcTmVjvMszMRgUfeZiZWWEODzMzK8zhYWZmhTk8zMysMIeHmZkV5vAwM7PCHB5mZlbYgJ/zSNPQ3g9szC1eGxGXFdmIpIuA5RFRqznMe/v9CvDbiPh4Lfs1M7OBVTWTYBUWASuBmoWHpPeRzTr4o1r1aWZmlan6E+aSLgdmAuOAKyLiZkmzgEvJhsMmA+emNlOB1ZKWAgsiYk7qY2tETJW0Ajg0fZ8JLOzbd59tnwK8FrgOeEUl9TbTTCed1e7u/tG40xnvrUHqbCu1DdqmVBoZk0y6ztpyncOvkvA4OjefOMBc4HigOSJmSJoEbJB0F3AMcF5EbJG0CJgdEUskXQLMoczEUjlrI+JKSaeX6zsitgFIOpwsoN4GvKvg/too0tIycIqVSqVB2zQC11lbrrM2urq6aG9v73d9VcNWkuYCLblQGQ9MBx4HlknaARwBrB+k76bcz5H+Pa6fvjel27OBw8imtZ0KHCjpoYhYUcG+mJlZDVR7ttVDwLoUKqcBNwGbgeuB+Wla2S3sCYfutK2dwOEAko4EDsn12T1I3wBExLKIaEnrPwd8w8FhZrZ/VRsetwI7JLUCJaAnIrYDq4BWSeuBKcC01L6V7EihBGyTdC+wGOgo0LeZmTWIAYetIuJRyrxPERE9wMVllu+zLC0/P3fznDLr5w3Wdz/9rqiknZmZ1ZY/JGhmZoU5PMzMrDCHh5mZFebwMDOzwhweZmZW2JgJjw466Gnwr7ZSW91rGEl1mln9jJnwMDOz2nF4mJlZYQ4PMzMrzOFhZmaFOTzMzKwwh4eZmRU2Iucwl/QO4ONAD3BjRHyxFv2amVllRtwc5pLGkc3jcRKwA3hQ0o0R8Zuh9m1mZpUZcXOYR8Rzkl4ZEbslvTS12VXtfpiZWXEjbg5zgBQcbweuAW4Dfj/YTjTTTCedFexu/bTRRqlUqncZFXGdteU6a8t1Dr+ROId51jjiFknfAVYA7wGWV7AvDa+lpaXeJQyqVCq5zhpynbXlOmujq6uL9vb2ftePuDnMJR0k6UeSJkZEN9lRRzdmZrbfjLg5zCPiaeBG4MeS7iE742pVlfthZmZVGJFzmEfEV4CvDNTGzMyGjz8kaGZmhTk8zMysMIeHmZkV5vAwM7PCHB5mZlbYmAmPkTCHuZnZSDFmwsPMzGrH4WFmZoU5PMzMrDCHh5mZFebwMDOzwhweZmZWmMPDzMwKG/CqupKmA/cDG3OL10bEZUU2IukiYHlEDHkO89TfXwF/C+wGHgDen+b2MDOz/aCqmQSrsAhYCQw5PCS9EPgMcFxEPCPpm8BbgO8OtW8zM6tMJeFRlqTLgZnAOOCKiLhZ0izgUrLhsMnAuanNVGC1pKXAgoiYk/rYGhFTJa0ADk3fZwIL+/ad23QXcEpEPJPbh53V7oeZmRVXSXgcnZtPHGAucDzQHBEzJE0CNki6CzgGOC8itkhaBMyOiCWSLgHmUGZiqZy1EXGlpNPL9R0R2wDS8NR/Akj6G7KQumuwnWimmU46K9jdOmrc6Yz3NkLqbKONUqlU7zIq4jpry3UOv6qGrSTNBVpyoTIemA48DiyTtAM4Alg/SN9NuZ8j/XtcP31vym3/AOAfgaOAd6TZB8320dLS+ElXKpVcZw25ztro6uqivb293/XVnm31ELAuhcppwE3AZuB6YH6aVnYLe8KhO21rJ3A4gKQjgUNyffa+4d1f33nXAZOAt+aGr8zMbD+pNjxuBXZIagVKQE9EbAdWAa2S1gNTgGmpfSuwJrXdJuleYDHQUaBvACS9GriA7AhlraQfSnpblfthZmZVGHDYKiIepcz7FGmY6OIyy/dZlpafn7t5Tpn18wbrO7d+I/58iplZXflJ2MzMCnN4mJlZYQ4PMzMrzOFhZmaFOTzMzKywMRMeHXTQ0+BfbaW2utcwmuo0s+EzZsLDzMxqx+FhZmaFOTzMzKwwh4eZmRXm8DAzs8IcHmZmVpjDw8zMChvwqrqSpgP3Axtzi9dGxGVFNiLpImB5RAx5DvNcnweSzSB4QUQ8VKt+zcxscFXNJFiFRcBKoCbhIekk4Frgj2vRn5mZFVNJeJQl6XJgJjAOuCIibpY0C7iUbDhsMnBuajMVWC1pKbAgIuakPrZGxFRJK4BD0/eZwMK+fffZ/ETgbcDXq63fzMyqV0l4HJ2bTxxgLnA80BwRMyRNAjZIugs4BjgvIrZIWgTMjoglki4B5lBmYqmctRFxpaTTy/UdEdt6G0bEegBJFe9oM8100llx+7po3OmM9+Y6n9dWaqtJP6VSqSb9DDfXWVsjpc5yqhq2kjQXaMmFynhgOvA4sEzSDuAIYP0gfTflfo7073H99L2pglrN9quWlqEnVKlUqkk/w8111laj19nV1UV7e3u/66s92+ohYF0KldOAm4DNwPXA/DSt7Bb2hEN32tZO4HAASUcCh+T67B6kbzMzaxDVhsetwA5JrUAJ6ImI7cAqoFXSemAKMC21bwXWpLbbJN0LLAY6CvRtZmYNYsBhq4h4lDLvU0RED3BxmeX7LEvLz8/dPKfM+nmD9d1Pv6dW0s7MzGrLHxI0M7PCHB5mZlaYw8PMzApzeJiZWWEODzMzK2zMhEcHHfQ0+Fdbqa3uNbjOYl9mY9WYCQ8zM6sdh4eZmRXm8DAzs8IcHmZmVpjDw8zMCnN4mJlZYQ4PMzMrbMCr6kqaDtwPbMwtXhsRlxXZiKSLgOURUas5zM8CPgnsBm6IiOtr0a+ZmVWmqpkEq7AIWAkMOTwkjQeuBF4D/B5YL+m7EfGfQ+3bzMwqU0l4lCXpcmAmMA64IiJuljQLuJRsOGwycG5qMxVYLWkpsCAi5qQ+tkbEVEkrgEPT95nAwr595zb9SuCRiPhd6uMe4PVAvo2ZmQ2jSsLj6Nx84gBzgeOB5oiYIWkSsEHSXcAxwHkRsUXSImB2RCyRdAkwhzITS+WsjYgrJZ1eru+I2JbaHQQ8lbvfduDgwXaimWY66axgd+uocacz3tsYrrOt1Fb7Tsnmsx4JXGdtjZQ6y6lq2ErSXKAlFyrjgenA48AySTuAI4D1g/TdlPs50r/H9dP3pnT7abIpbntNAbZhth+0tNQ+kUql0rD0W2uus7Yavc6uri7a29v7XV/t2VYPAetSqJwG3ARsBq4H5qdpZbewJxy607Z2AocDSDoSOCTXZ/cgfff6BfA/JB0iaQLZkNVPq9wPMzOrQrXhcSuwQ1IrUAJ6ImI7sApolbSe7IhgWmrfCqxJbbdJuhdYDHQU6BuAdMbWxcAdZKFxQ0Q8XuV+mJlZFQYctoqIRynzPkVE9JA9gfddvs+ytPz83M1zyqyfN1jffdrfShYyZmZWB/6QoJmZFebwMDOzwhweZmZWmMPDzMwKc3iYmVlhYyY8Ouigp8G/2kptda/BdQ78ZWaZMRMeZmZWOw4PMzMrzOFhZmaFOTzMzKwwh4eZmRXm8DAzs8IcHmZmVpjDw8zMCqt6DvMRZBzArl276l1HRbq6uupdQkVcZ225ztpynUOXe84cV259U0/P6P7UbKlUmkE2GZWZmRU3s6Wl5Z6+C8fCkcd9wEygE3iuzrWYmY0U48imDb+v3MpRf+RhZma15zfMzcysMIeHmZkV5vAwM7PCHB5mZlaYw8PMzAob1afqSjoA+BLwKqALuDAiHqlvVXuT9FrgHyLiVEkvB1YAPUA78IGI6K5zfeOBG4DpwETgM8CDNF6d44DrAaW6FgA7abA6e0l6KVAC3gjspgHrlLQReDrd7ACuA75IVu+dEbG4XrXlSfp74GxgAtn/9x/RYI+npHnAvHRzEnACcCoN+HhWarQfebwVmBQRJwMfB75Q53r2Imkh8M9kf0wAVwCfiIiZQBNwTr1qyzkPeDLV9GbgahqzzrMAIuJ1wCeAJTRmnb2BfB3wbFrUcHVKmgQ0RcSp6Xs+cC1wLjADeK2kE+taJCDpVOAU4HXALOBPaMDHMyJW9D6WZC8aPkQDPp5FjPbwmAHcDhARG4CT6lvOPjYDb8/dbiF71QTwfeAv9ntF+7oZuCT93ET2Kqnh6oyI7wAXpZtHAttowDqTz5M9cWxJtxuxzlcBB0q6U9JaSa8HJkbE5ojoAe6gMep8E/AA8G3gVuB7NObjCYCkk4BjgNU05uNZsdEeHgcBT+VuPyepYYbqIuJbwB9yi5rSHxLAduDg/V/V3iJiR0RslzQF+FeyV/UNVydAROyW9DXgKuBGGrDONHzxRETckVvccHUCz5CF3JvIhgCXp2W9GqXOw8heFM4mq/NG4IAGfDx7LQIWkz03PZ1b3mh1Dmq0h8fTwJTc7QMiYne9iqlAflx2Ctmr57qT9CfAOuDrEfENGrROgIg4HziK7P2PF+ZWNUqd7wXeKOmHZOPeK4GX5tY3Sp0PA6sioiciHiZ7EXZIbn2j1PkkcEdE7IqIIHufK/8k3Ch1IulFgCJiHfs+NzVMnZUa7eGxHjgDQNKfkR3eNrKfpTFcgNNpgAs6Svoj4E7gYxFxQ1rciHW+O71xCtkr5G6grdHqjIjXR8SsNPa9CXgP8P1Gq5Ms5L4AIGkacCDwe0kvk9REdkTSCHXeA7xZUlOq878Bdzfg4wnweuBugIh4GtjVgI9nxRpmCGeYfJvsVd5PyMbr59e5nsF8FLhe0gTgF2TDRPW2CHgxcImk3vc+Pgwsa7A6bwGWS/oxMB74W7LaGu3xLKcRf+9fBVZIuofsrKX3kgXyjWQXzLszIu6tY30ARMT30vsx/072YvgDZGeGNdrjCdmZgL/M3e4dZmuYx7MIXxjRzMwKG+3DVmZmNgwcHmZmVpjDw8zMCnN4mJlZYQ4PMzMrbLSfqmu230g6CLic7BpLu4HfAR+NiHFBwrkAAAGSSURBVI013MZi4AcRMaI+E2Cjj488zGogXcF5DfBb4ISIOAG4jOwDgIfWcFOzyD4XYFZX/pyHWQ1IegPZJVFenr/8t6QzgDbgQrIrFD9H9on9hWRXgP1hRExPbT8FEBGfktRJ9uG2GWRHMe8CZpJdcnwr8LaIaPQrJtgo5iMPs9o4Ebiv77wREbGG7MJ9Z5Nd7fVE4OVkny4eyFTg7og4Efgx8MGIWEkKIgeH1ZvDw6w2uskugVPOacA3I+LZdGHOG4A3VNDn7enfdva+KKFZ3Tk8zGqjDXh1usjd8yR9ln2DoonsZJUe9g6c8flGEbEz/di3nVndOTzMaqMV+DVwaZoSF0lvIrsY51LgryS9MM0nM5/sEvfbgBdLeomkiWQzNQ5mNz5L0hqAw8OsBtLkQ2cDLwPaJd0PfAw4IyK+RjbDXRvwc+Ax4KqIeAr4J+A+4AdkV4YdzO3AtZJOqf1emFXOZ1uZmVlhPvIwM7PCHB5mZlaYw8PMzApzeJiZWWEODzMzK8zhYWZmhTk8zMyssP8CqikVts4JvzUAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Make a classification dataset\n", + "X, y = make_classification(\n", + " n_samples=400, n_features=10, n_informative=2, n_redundant=3,\n", + " n_classes=2, n_clusters_per_class=2, random_state=854\n", + ")\n", + "\n", + "# Assign NaN values\n", + "X[X > 1.5] = np.nan\n", + "features = [\"Feature {}\".format(str(n)) for n in range(10)]\n", + "\n", + "visualizer = MissingValuesBar(features=features, color=\"lime\")\n", + "\n", + "visualizer.fit(X)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ClassBalance" + ] + }, + { + "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": [ + "X, y = load_game()\n", + "\n", + "visualizer = ClassBalance(\n", + " labels=[\"draw\", \"loss\", \"win\"],\n", + " colormap=\"copper\"\n", + ")\n", + "\n", + "visualizer.fit(y)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## FeatureCorrelation" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYUAAAETCAYAAADZHBoWAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAf9UlEQVR4nO3deZhcVZ3G8W8ICfuOilFE2V5AQLBZAgIJyD6yjIpEQEkEO8g2qAxBQAkIygjCI8tAYgKICmgQFJBVIBDZUyAkLD/WKMOOAoImhJDMH+fU5VLp7vSSrqruvJ/nyZOquttbVV33d8+5dU8NmDdvHmZmZgCLNTqAmZk1DxcFMzMruCiYmVnBRcHMzAouCmZmVnBRMDOzgotCE5A0T9I0SX8p/ZvQg/VtLumChZmxGeXXbdUFzPMfkk7Ot/eUdHZ90oGkQyQdm28fLOnQfHuspHO7sb5zJY1dyDG7mmEzSTN6uI6fS2pZOInmW/cESTsuYJ52X39JMyRt1hvZ+orFGx3ACttHxGsLaV2fBj6+kNbV120OrAwQEVcDV9drwxFRLszbANPrte0mtxMwrjdWHBEH98Z6FyUuCk1O0vrAz4BVgIHA2RFxoaTFgLOAocBywADgYOBvwMnACpIuAn4BnBsRG+b1Da/ez0edWwEfBR6OiAMkHQ98idSKnAEcGhEvSPoicAIwF3gP+O+IuKONvN8DDgTmAE8CIyPiTUnfB76aH38CODwiXpI0GfgHsB5wft52+f4l+flvBAwCbsnbnlPa5jJ53nVJBeAtYD9gReAQYKCkN3OeL0fEFyR9PC/zyfza/SIiTpf0ybyN64At8/qOj4jf1DzPq4A/RsQESUOBu4G1IuKZ/BquAPwbWDWvb09gJ0kz8yrWk3Rbfu1fBkZExIs121gemAB8Bngxv3Z/ztM+BpwLfCK/LpdHxI9y/tuByXm5Afm1npKXa+/9nZyfw+fyOqcAB0bEXEnfAr4NvAlMq8nYpfUBPwSGAL+W9PWIuLe0rruAMyPiinz/tJz/5Lbe34iIdv5+zo2IKyQdB+wNLAksAxwdEVflza0v6Y68vgdz7rdqntsepL/5wfm9PDoi7qafc/dR87itpvvow5IWB64Ajo2IFmAYcHTeCW1J+nBtFREbkHb+x0bEc8APgCkRMaoT210D+GwuCF8n7Xy3iIhNSDvGajfW6aQPzmbA94HhtSuStCcwMmfaEHgWOFzSKGA3YPOI2Jh0xHxxadHXI2KDiDinjftnAZX8/Dcl7WS/U7Pp3YA3ImJoRKwL3E/aEd4LXAD8JiKOr1nm18BtEbERacd1gKQRedqawI0RsQUwBvhJG6/bVcCu+fauwEtAtdtiL9L7BkDeEV0NnBUR55W28ZWIWA94nVTQa50EzCTt8PYBVJr2S+DC/LpsAewo6St52idy/k2AY4HfSBq0gPcXYC3S+7oRsAMwTNImwFhgu4jYHJhdnbk768vvwwvA/uWCkP2c9PeDpIHAAXl9bb6/peVq/36QtAbp/RiW/+aOJxWXqrVJBWQjUuE5oRxE0jrAj4DdI2JToBW4Mh+A9GtuKTSP+bqPJG1A+mBdKBX7g6WATSPifEknAKMlVT98HzjS6aR7SkfdXyDtYKbm7Q0Els7TLgeukvRH4Gba3lHuCEyKiNcBIuI7+Xn8FrgoIv6V5/sZcLykwfn+lJr1lO9/AdhC0kH5/lK1G81Hhc9IOoL0YR9OOkptU/5gfw7YOS//pqSLSTufe4B3STs4gAfI3U81rgHOzIV7F+AUUkvgWuAjpB3X7u1lAG6OiFfz7YeAD7cxz47AURExD3g1t06q+YcBK0v6YZ53WWAT4D7STvLS/Nyul/QesDEdv78A10TEXOAtSU/l5/1Z4KaIeCnPM573i2F31teR3wJnSFotb/epiHgSeHIB72/t3w8R8VdJBwL7S1qb1KJetjTLldXXP7eoTycdAFTtRGrF3VL67M3N239oAc+jT3NRaG4DSUdIm1QfkPQR4E1J/0Hauf4U+APwOOnIqtY80pFQ1eCa6W/XbO9/IuL8vK0lgJUAIuJ4SRNJO9KRwLGSWvKHvmpO3l4164qkLpzaFulipL+9aq63a6bXZtonIh4rrfMDA3bl7o1WUnfKpaTuhE/RvsX44GtSfWxQvj279LxqXz8AIuJ1SQ8Ce5C6ii4htdD2Bq6KiHmlnUlb3i3dbnMbbTxeLd4D8+NbR8S/AfIJ91mkltQcPmgxUpdfu+9vNrN0u7rt9jJUc3R1fe2KiH9JmkTq+tuK1HLozPtb+/eDpM+SPhdnATeRutTOL83yXun2AD74flSf2y0RsW9pnauTWjn9mruPmlsAsyQdAMUf5XSghXQkc03+QN5P2hkNzMvN4f0d3KvAJ3J31IA8X3tuBA7OfdmQmtu/lLR4/sbJMvnk6aHA+qVtVP0J+GJp+bGkrp4bgVGlpveRwB0R8U4nXoMbgW9LGpB3Olfzwa4DSEfqF0fERNJrtgdtvxYA5L7je4DDACStAHyd1ALqiqtIXQy35HUGqbvmijbmnS9HJ9wAHCRpMUkrkbqliIh/5vzVltiKwJ3V6cCHJO2ap+1B2uFNo533dwEZbgZ2zudgIHfvZN1ZH3T8WlS7kLYGfpcf6+j9bc92wNSIOJNUEMqfD4A9Ja2Uu6lagetrlr+V9LzXA5C0O/Aw6fxEv+ai0MQiYjbpg36wpIdJRzzfj4g7SX3lw/LjdwNPA5/KJ6DvJp3IvCoiHiV902MqaUfyYhubqpoAXAvcI+kRUpfDyNy9dBRwqaQHgEnAN2p36hFxHXARcKekacBqpL7ciaSCcZ+kx0hdA/t38mU4knSScBrpQzmN+buuziB1o/2FdFL3AVIzn3x/T0nn1CyzP/D5nPM+0g7o4k5mqvo9qZ+/WkxuJO3s7mpj3uuBI5VOxHfWWNIO/XFSd1X5JO9+wNCc/17gsoj4dZ42C/iapIdIr//eEfEe7by/HQWIiGnAMaRulKl8cKfY5fVlvyed59i5je1VSEXjdxExKz/c0fvbnsuAVSU9ClRIrYmVJS2Xpz+as08D3gBOq8nxCKlYXJ5fxx8Ce5a6QPutAR4626z/yN8+mh4Ryy5oXrO2uKVgZmYFtxTMzKzgloKZmRX67FdSK5XKEqQhDF7kg18vMzOz9g0kXYNxf0tLy3zfAOyzRYFUEOa7aMXMzDplW/KwKWV9uSi8CLDuuusyeHDt9Vjzmz59OhtuuGGvh+oOZ+u6Zs0FztYdzZoLmjdbd3PNnj2bJ554Atr5enpfLgrvAQwePJgllliiUwt0dr5GcLaua9Zc4Gzd0ay5oHmz9TBXm93uPtFsZmYFFwUzMyu4KJiZWcFFwczMCi4KZmZWcFEwM7OCi4KZmRX68nUKtggbP7pCGia/WTlb13Uu17iprb2cY9HmloKZmRVcFMzMrOCiYGZmBRcFMzMruCiYmVnBRcHMzAouCmZmVnBRMDOzgouCmZkVev2KZknLAxOAFYEhwHmkSxfPA94CXgFmRcRISUcA+wHzgMsj4uzezmdmZu+rR0thbdIOfmdgZ+A7wAXAyIjYAXgaQNIGwL7ANqQflN5bkuqQz8zMsnqMffQycJSkLwL/BAYBQyLikTx9CjAC2BBYA7glP74SsA4QdchoZmbUp6XwXeDuiDgAmAQMAJ7LLQOAofn/AB4Bto+I4cDFwMN1yGdmZlk9WgrXAOdIGgG8AcwBDgculPQ2MBt4PiIeknQL8GdJSwD3Ac/XIZ+ZmWW9XhQi4jZS11BB0mHAHhHxqqRTSIWBiDgdOL23M5mZWdsa9XsKLwM35ZbCm8CBDcphZmYlDSkKEXEFcEUjtm1mZu3zxWtmZlZwUTAzs4KLgpmZFRp1otmsR1rHtdDS0tLoGG2qVCrO1kXNmmtR5JaCmZkVXBTMzKzgomBmZgUXBTMzK/hEcx81erPxddxapY7b6pzWcT4padYb3FIwM7OCi4KZmRVcFMzMrOCiYGZmBRcFMzMruCiYmVmh17+SKmlx4GZAwPci4he9vU0zM+ueelynMARYPiKG1GFbZmbWA/UoChcA60gaBzwIPA6MIf0u85rA5RFxqqTVgfHAUsBMoDUinqtDPjMzy+pxTuFQ4FHgxdJjawBfAoYCx+THzgDOjojh+fZpdchmZmYljRrmYlpEzAHmSJqZH9sIOE7SGGAA8G6DspmZLbIa9e2jeW089jgwJrcURgOT6prIzMyaakC8o4HzJS1JOq/wXw3OY2a2yOn1ohARM0jnDsoml6avlv9/Btilt/OYmVn7fPGamZkVXBTMzKzgomBmZgUXBTMzK7gomJlZoZm+kmpdMG5qa122U6lUaGlpvt9DrlSa73ejzfoDtxTMzKzgomBmZgUXBTMzK7gomJlZwSea+6jRm42v49aa76Ru67jmO/lt1h+4pWBmZgUXBTMzK7gomJlZwUXBzMwKLgpmZlZwUTAzs4KLgpmZFVwUzMys0OmL1yQtBVwErAEMBo4CRgNrAgOBMyPiN5KGASeSCs6ywH7AbOAa4O/AdcDbwIHAXOD+iDhS0urAeGApYCbQGhHPLYwnaWZmndOVlsIhwIyI2AoYAQwDXo2IrYEdgVMkrQp8GjggIoYDVwL75OVXA3aOiJ8Ao4DD87oek7Q4cAZwdl7uDOC0nj45MzPrmq4UBQF3A0TEk8BHgTvy/beAR4G1gOeBsyVdDGwPDMrLPxsRs/PtUcBhkm4ntTwGABsBx0maDPwA+Ei3n5WZmXVLV4rCY8DmAJLWBL4KbJvvL0faqT8L/BwYFREjgRdIO3xIXUVV3wQOiYhhwKbA1sDjwJjcUhgNTOrWMzIzs27ryoB444AL89H9QGBX0tH+n0nnAU6KiFck/QqYIulfwMvAkDbWNS3P8xapZXEvcDRwvqQl8/r+q7tPyszMuqfTRSEiZpFOGpfd18Z832lnFUNL80wAJtRMfwbYpbN5zMxs4fNXUs3MrOCiYGZmBRcFMzMruCiYmVnBRcHMzAr+jeY+atzU1rpsp1Kp0NLSfL+HXKk03+9Gm/UHbimYmVnBRcHMzAouCmZmVnBRMDOzgk80W580fnQFaOaTzc7Wdb2fq15f0OjL3FIwM7OCi4KZmRVcFMzMrOCiYGZmBRcFMzMruCiYmVnBRcHMzAq9XhQkTZa0XheX2UjSdr2VyczM2tasLYUvARs0OoSZ2aKm21c0S1oXuAiYQyou44GvA3OB1YDxEXFeaf6PA+cDSwIfBU6IiN9LOhXYPmf5HfArYCQwW9IDEXFfdzOamVnX9KSlsBNwH7AjcCKwAvAxYE9gKPBtSR8uzb8e8NOI2AloBQ7Lj+8P7AdsC7wREc8DFwNnuiCYmdVXT4rCROAN4AbgcFKL4a6IeCciZgLTgbVK878IjJb0S+AQYFB+fH/gNOBGYMUe5DEzsx7qSVHYC5gSEZ8HJgFjgE0kDZS0NPBp4MnS/D8ELomIrwG3AQMkLQHsA3yV1IU0UtIapC6oZj3fYWbWb/VkxzsVOFnSraQj/3NIR//XA1OAUyLitdL8k4AzJN1B6npaNSLeAf4B3EMqFDcBfyMNl3i4pO17kM/MzLqo2yeaI+JpYJvqfUnDgS0iYkTNfMPzzceBy9pYz8nAyTUP/zH/MzOzOnIXjZmZFRbaj+xExGRg8sJan5mZ1Z9bCmZmVnBRMDOzgouCmZkVFto5BbN6ah3XQktLS6NjtKlSqThbFzVrrkWRWwpmZlZwUTAzs4KLgpmZFVwUzMyssMieaB692fhGR6hRaXSADjRfttZxPilp1hvcUjAzs4KLgpmZFVwUzMys4KJgZmYFFwUzMyu4KJiZWaGuRUHSypL2q+c2zcys8+rdUtgY2LPO2zQzs07q0cVrkirAbsDrwN+B4RHxgKQHgBuBzYBVgIciYhRwPPAZSa3A9cB4YClgJtAKDASuyeu6LiJ+0pN8ZmbWNT1tKfwB2AXYBngW2FHSBsAM4PWI2IlUGIZK+hhwKnBrRIwHzgDOjojh+fZpeZ2rATu7IJiZ1V9Ph7m4knT0/7f8/5GkQnMZsKWky4C3gWWBQTXLbgQcJ2kMMAB4Nz/+bETM7mEuMzPrhh61FCJiOrAmsAVwHWnnvxcwG1g9Ir4KHEfqIhoAzC1t83FgTG4pjAYm5cfn9iSTmZl138I40TwZeDUi5gK3A68A9wJrSroDuAJ4BhgCPA1sJOko4GjgREm3A5cADy+ELGZm1gM9HiU1IsaUbn+vNGnzdhZZv3R7lzamD+1pJjMz6x5fvGZmZgUXBTMzK7gomJlZwUXBzMwKLgpmZlZYZH+jedzU1kZHKFQqFVpamvM3h5s1W6XSfL8bbdYfuKVgZmYFFwUzMyu4KJiZWcFFwczMCovsiWZr3+jNxtc80nwndVvHNd/Jb7P+wC0FMzMruCiYmVnBRcHMzAouCmZmVnBRMDOzgouCmZkVmqYoSLpY0q6NzmFmtihrmqJgZmaN1+sXr0laHpgArAgMAc4DBgAHAnOB+yPiyDz7aEnHACsA34qI+3o7n5mZva8eLYW1gcsjYmdgZ+A7wCjg8IjYCnhMUrU4VSJiB+AcYGQdspmZWUk9hrl4GThK0heBfwKDSEXhaEmfAu4mtRzg/fEUXgKWrkM2MzMrqUdL4bvA3RFxADCJVAC+CRwSEcOATYGt87zz6pDHzMzaUY+WwjXAOZJGAG8Ac4BHgSmS3gKeB+4ltR7MzKyBer0oRMRtwIZtTBpXc39kaZkbgBt6MZaZmbXBX0k1M7OCi4KZmRVcFMzMrOCiYGZmBRcFMzMr+DeabT7jprYWtyuVCi0tzfd7yJVK8/1utFl/4JaCmZkVXBTMzKzgomBmZgUXBTMzK/hEs/VJ40dXeH9Q3WbUtWzlk/tmjeSWgpmZFVwUzMys4KJgZmYFFwUzMyu4KJiZWcFFwczMCi4KZmZWqFtRkDRc0uX12p6ZmXWdWwpmZlbotSuaJa0LXATMIRWf8aVphwNfBJYBXgP+ExgIXAIMAZ4DtouIIb2Vz8zM5tebLYWdgPuAHYETgRUAJC0GrALsGBFbkgrT5kAr8GxEfA4YC3ykF7OZmVkberMoTATeAG4ADie1GIiIucBs4DJJE4GPA4OA9YG78jyPA6/2YjYzM2tDbxaFvYApEfF5YBIwBkDSxsDeEbEvcETOMACYDmyV51kLWLUXs5mZWRt6c5TUqcAvJJ1AOl9wDrAF8BTwL0l35vleJJ1HmAhcLOkO4K/ArF7MZmZmbei1ohARTwPbtDN5h9oHJG0NTIyImyStA2zdW9nMzKxtzfR7Cs+QzjOcSDrHcFiD85iZLXKapihExEvA9o3OYWa2KPPFa2ZmVnBRMDOzgouCmZkVmuacgllXtI5roaWlpdEx2lSpVJo2m9mCuKVgZmYFFwUzMyu4KJiZWcFFwczMCj7RbH3S+NEVoNLoGB1wtq5r1lzQjNlax/XOlxncUjAzs4KLgpmZFVwUzMys4KJgZmYFFwUzMyu4KJiZWaEpi4KkJSUd3OgcZmaLmqYsCsBqgIuCmVmdLfDiNUlLARcBawCDgaOAkcA6pKJyQkRMljQNuAPYGHgceBnYDngH2B1YGpgIrJJXfWRETJP0JHAnoLzMl4DjgQ0k/SAiTl44T9XMzBakMy2FQ4AZEbEVMALYBngtIrYD9gLOy/MtB1waEdsC2wJ35XkGA58GjgNuiYjtgVbg/LzcmsD38/o/BGwOnAo86oJgZlZfnRnmQsD1ABHxpKTVgW0lbVldh6RV8+0H8v9vAI/m268DSwIbATtI2jc/vnL+/7WIeC7ffi7Pa2ZmDdCZlsJjpKN3JK1Jai1cFhHDgd2AScA/8rzzOljP48BZebmvAL/qYJm5ncxmZmYLUWd2vOOANSXdDlxCKgTr5ft3AX+NiLmdWM+pwFckTQZuAKZ3MO8rwGBJ/9OJ9ZqZ2UKywO6jiJgF7Ffz8L1tzPfJ0u2hpdt7l2Yr365OX610e0Rp0iYLymZmZguXu2jMzKzgomBmZgUXBTMzK7gomJlZwUXBzMwK/o1m65Nax7XQ0tI7v1HbU5VKxdm6qFlzQfNmq1R653ej3VIwM7OCi4KZmRVcFMzMrOCiYGZmBRcFMzMruCiYmVnBRcHMzAouCmZmVujLF68NBJg9e3anF3jnnXd6LUxPOVvXNWsucLbuaNZc0LzZupOrtM8c2Nb0AfPmdfRjac2rUqlsA0xpdA4zsz5q25aWlj/XPtiXWwr3A9sCLwLvNTiLmVlfMRD4KGkfOp8+21IwM7OFzyeazcys4KJgZmYFFwUzMyu4KJiZWcFFwczMCn35K6kdkrQU8Cvgw8BbwIER8WrNPKcCOwLzgGMjYnITZTsd2Ib0Ho2PiJ83Q64839rAVRGxUR0yLQb8L/AZ4B3g4Ih4qjT9m8BoYA5wSkRc29uZOpMrz/Mh4E5g44iYVY9cnckm6dvAiHz3uog4qYmyHQaMJH0mz4iI3zZDrtI8fwT+EBEX1CNXZ7JJ+hlpX/FWfmiviHizu9vrzy2FbwHTImJb4BLghPJESZsCQ/O/EcDPmijb9sDaEbEV6c0eI2mlRufK2b4GXA58qA55APYGlsyvxbHAT0tZVgOOBD4H7AL8WNISjc6Vs+0C3ASsVqc8ncomaU1gf2Br0t/+zpI2bpJsq5L+BrcGPg/8VNKARucqOQWox+ew1oKytQC7RMTw/K/bBQH6d1HYBrgh376e1CIoRMSDpBdyHrAG8EazZAPuBr6Rb88jXWzybhPkAngdGFaHLFVFpoi4B9isNG0L4M6IeCd/EJ4C6rWD6ygXwFzS6/ePOuUp6yjbc8CuEfFe/tsfBNStFdNRtoh4DdgkIt4lFdNZOWNDcwFI+jLpPb1h/kV7XbvZcitiHWC8pDslfaPtVXRev+g+knQQ8O2ah18GqhXzLWCF2uUiYk7uQjoSOKJZsuWuhlmSBgG/IHUfvd3oXDnbtXn5hRmnI8uXMgG8J2nxiJjTxrQ2MzcgFxFxM9T1depUtrzDfS0fgZ8OPBgRTzRDNig+k4cDJwFnN0MuSRsC+wFfBn5Qx0wLzAYsA5wDnEk6eLxN0tSIeLi7G+sXRSEiJgITy49JuhJYLt9djnZaAhFxvKTTgHskTYmIp5shW+4uugKYHBE/XpiZepKrAf7J+5kAFqvuQNqYVs/MHeVqtA6zSVoSuJBURA9tpmwAEXGupPHA9ZK2j4jbGpzr68DHgFuBTwKzJc2IiHq1GjrK9m/gZxHxbwBJt5LOPXS7KPTn7qM7gd3z7d2oGTxP0g6Szst3Z5G6Z+Y2SbalgFuACyPih3XKtMBcDVJkkjQUmFaadh+wraQlJa0ArA9Mb4JcjdZuttxC+APwUESMjoh6jxvWUTZJujJnfJd0UrXun8naXBFxTERsGRHDgYuBM+tYEDrMBqwL3ClpYO5Z2AZ4oCcb67djH0lamtT18lFgNrBfRLwk6SekI/AKcC6pD3ogMLEe3/DpZLbPAScCfyktNioinm1kroi4rzTvSxHR6ydRS9+82BgYAIwifUCeioir87ePWkkHOD+KiN/1dqbO5CrNNwNYr0HfPpovG+lv/TLgntIi34uIuxudLb+fJ5IOSOYB10fEyc2QqzTfWOClBn37qL3X7L+Br5AK6SU9zdZvi4KZmXVdf+4+MjOzLnJRMDOzgouCmZkVXBTMzKzgomBmZoV+cfGa9S+SPgk8ATxK+mriYOAF0tdy/6+B0dolaXngx6QhQOaQhgP5bkT06Dvj7WxrLEBEjO1gnpOAP0XEFEkTgAsiYurCzmL9j1sK1qxeiIhNImLTiPg0MJV0OX/Tyd8jv440ztEmEbEJcDLpitxVGhRrGOmaBCLiYBcE6yy3FKyvuAPYE0DS5sBZwNLAa8DoiHhW0jDg1Pz4SsAxETFJ0sXAKsDawDGkHeZOwHukYZBPyhfu/Zw0RMBc0rDNl0gaCewKrAysCdwUEbVDQ2wPDAFOjIi5ABFxm6RR5B2zpOOAA/I2b8o5VicNdPYa6ar6XwEHAqsC15BG7h2X55tLusjsT+UN53GCvkYaA2cusC+wOWnQtAmS/pNUTMdGxOQOclxFuhp8U9IYWPtERCMG87MGc0vBml6+fH9f0uX8g4EJpKutP0saRrh6JfoRpLHmPwscxAcHL/t7RKxPGhNmt4j4DGmI5nXyWEBj8zwbAjsAY0tDSm8NfIl0Rekekmp/R2JT4P5qQaiKiOsi4hVJu5MKWkued23gkOrTAw6IiOqItB8HNo2I40hF4cKIaMnLj5NUjIGTu6z2Bobn3L8HDo2IS0gtq4MjojyMREc5PkMavmFD0vhR+2OLJLcUrFkNkVQd5mMJ0jhHx5LGelkLuLo0Auny+f8DgC9I2of0WwHLltZ3b/7/eWCmpDuBa4ETImKWpB1IhYSIeE3SH4DhpMHI7oqItwAkPUNqNZTNJQ0/0J4dgMsiYmZex4WkFsEfgVciYkZp3gdKg53tCKwnqTrUw6D83Mk5/ylpP2CEpHVJLZry0ChdzfFgnm96G8/RFhEuCtasXsh98x8gaXXgmeo0SQOBj+TJU4DbgMmkAQUvLS06E4qhmbckdSHtDtydu51qW80DeP/zUR63aB7zF4CpwKGSBpTH/5f0I+DmBax7Zs208v2BwA7VbhxJQ0hdO3uXXovJpDG8rgdeIrUA2tOT52iLCHcfWV/zOLCypG3z/W8Al0pamdSK+EFEXAfsTO7PL1P6xb3bgTsi4mjSN5xEGhb5oDzPqqQd7+ROZpoCvAKcmItU9ZfXRuX13wp8VdJSkhbPj3dmOOhbyUNbS9qA1PW1dGn65qRB0c4itYR2Kz3nOcx/0NfdHLYIcVGwPiUi3gH2If1U48Ok7o+D8tH0BOARSQ+Sfmd6aUnL1Cz/IOmX7aZLegCYQTrKPplUbKaRTmqf2tmvk+bWwZ6krp3pOdcYYPeIeDn/MNG1pBbFI8Bf6dw3qY4Ahub1/Qb4WrUbK7sJWEzSo6RRT2cAn8rTbgAukLR1KWd3c9gixKOkmplZwS0FMzMruCiYmVnBRcHMzAouCmZmVnBRMDOzgouCmZkVXBTMzKzw/2JBGA4Rz/MAAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "X, y = load_concrete(return_dataset=True).to_pandas()\n", + "\n", + "# Create a list of the feature names\n", + "features = [\n", + " \"cement\", \"slag\", \"ash\", \"water\", \"splast\", \"coarse\", \"fine\", \"age\"\n", + "]\n", + "\n", + "# Instantiate the visualizer\n", + "visualizer = FeatureCorrelation(labels=features, color=\"rebeccapurple\")\n", + "\n", + "visualizer.fit(X, y)\n", + "visualizer.poof()" + ] + } + ], + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/rebeccabilbro/check_is_fitted.ipynb b/examples/rebeccabilbro/check_is_fitted.ipynb new file mode 100644 index 000000000..c64681a6f --- /dev/null +++ b/examples/rebeccabilbro/check_is_fitted.ipynb @@ -0,0 +1,854 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from sklearn.cluster import KMeans\n", + "from sklearn.ensemble import RandomForestRegressor\n", + "from sklearn.model_selection import train_test_split as tts\n", + "from sklearn.linear_model import LogisticRegression, Lasso, LassoCV\n", + "\n", + "from yellowbrick.cluster import *\n", + "from yellowbrick.features import FeatureImportances\n", + "from yellowbrick.classifier import ROCAUC, DiscriminationThreshold\n", + "from yellowbrick.classifier import ClassPredictionError, ConfusionMatrix\n", + "from yellowbrick.datasets import load_occupancy, load_energy, load_credit\n", + "from yellowbrick.classifier import ClassificationReport, PrecisionRecallCurve\n", + "from yellowbrick.regressor import PredictionError, ResidualsPlot, AlphaSelection" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Check if fitted on Classifiers" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "X, y = load_occupancy(return_dataset=True).to_numpy()\n", + "X_train, X_test, y_train, y_test = tts(X, y, test_size=0.20)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "unfitted_model = LogisticRegression(solver='lbfgs')\n", + "fitted_model = unfitted_model.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ClassPredictionError(fitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ClassPredictionError(unfitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYwAAAEYCAYAAABPzsEfAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAgAElEQVR4nO3deXhU1f3H8fcECGBYLWBxRap+RRGswbJIsVpRUbS0amvVohTqRuu+UK11qVpbS1utdUXrLm5FUBSLoj8VRDBUBZcvxJW6IYjsScjy++Pc0DElyQGGTJh8Xs/jY+beyZlz54T7mXPuuWdSVVVViIiI1Ccv2xUQEZEtgwJDRESiKDBERCSKAkNERKIoMEREJIoCQ0REojTPdgUaOzPrBsxz9zYZKu9I4CB3P6OO5xwO9HX330Y+/zJgNPBxsikFtAMmAOe6e6ObO21mTwLnuftbGSpvO+AqoBCoBEqAq919YrK/Cujs7osz8XpJmeOA8e7+jJldDJwCPAtUVG/fiDJ3Bv7k7keZ2bbAI+4+IEP1vRMYDHyRbMoD2gA3u/sfM/EaEXW4LXm9ooZ4PcksBUYDc/dJwKR6nrYvsPUGPB/gQXf/ZfUDM+sIvAE8nfzXqLj7YZkqy8w6AzOA3wAj3L3KzHoDU81stbtPzdRrpXP3UWkPRwLHuftLm1jsToAl5X8CZCQs0vzF3f9U/cDMdgTeNrNJ7v5Ohl9rfQYDtzTA68hmoMDYBGbWHvg7sDdQBTwFXOTu5WZ2GPAHwqfN14CDgIHA94Cj3X2omf2IcJKrTJ53PlAKnAo0M7NlwIK0538TuBnYPfmdm939+lqqtw2wFbA0qWsP4DrgG0Az4Hp3vyPZN4ZwwlsBvAAMc/duySfSrYFvAU8AlyTHtH9Sxr+BM9x9uZmdltS7jPDp/hR3f6uO7R8kx/WqmZ0MnJG8B58Dv3T3+cnrLwf2AnYA3gGOdfeVNY71dOAld7+neoO7v25mRwFf1WizAuAmYLfk2FYQTvS+vvZw9xfq2P48cANwDLA9cLuZ/RY4DbjB3R8xs6HAlYRP86uAU5O6XQQMA1oBBcB5hA8G44DtzOxpQo9lnru3MbMWwJ+B7yd1eAU4291XJO/lncm+HQkfHi4gzvbJ/1ck788AQhsXJMd7mbs/YWYnAT9NjmM7Qm/2RHf/xMy2T97TboTe7V3ufm3SO38ReDvZ9yKwLXCfmQ1391ci6yiNhK5hbJrrgSWEE1ofoDdwnpl9A7gHOMHd9waeI/wjq+la4HR370M4GX8v+Ud0M+Ef/cU1nn8jMN/ddwf6Ayeb2S7Jvp+Y2WtmNt/MlgB/I5ycZ5lZc+ARYIy7FxJO+OeZWT8zOwQ4idCrKQTa1njNrdx9T3e/EBgDlAOF7t4b+AS4xsyaAX8FDnX3fYFbgYG1bU8v3MwOBC4ADkjKvB94zMxSyVMKgUOBHoSTzTHreR/7ANNrbnT3F919bo3NQ4Cv3L2fu+8GzAaqe2b/0x71bK9+nZ8k78Xx7v5g2rFtA9wLnOTuvZJyrjGznQgfIPZPtl8MXOHuFcAo4F13P6RGvX+THH/v5L+8pLxqbdz9u4Qeya+Soa31OTv5O3nXzBYT3vuh7v5x0iv9B/Azd98HOBK4KemFAOwHjHb3PYAiwt8/wH3Ac+6+V/KcE8zs2GTf9sDv3H03dx+Z9j4pLLZACoxNM4TwSbLK3UsJJ/ohwCDgLXd/HcDd7yJ8Uq5pPDAhGQvvCNQ3jnwQ4aSLuy9z957uXpzsezAJp57Ao4RPiE8l+3Yj9BLuMLPXgP8DWgPfBg4DHnb3r5JrHX+v8ZrpQyxDgR8A/07KGQbskZzoHgZmmNkNwDLg9tq21yj/0KTuXyTHdSchXLsl+6e4e6m7rwXmkgzV1VBJ5N+yuz8C3GlmvzKz6wgn/+rrU7W1x4a2U7X9CD2E15LX/qe7D3H3D4ETgePN7BpCD6y+a2RDCD3Kte5eSfhAMCRt/8TkNT4GFrH+9wnCkNTeQC9gJuG9eyHZ1x/oSgjs14AnCT3nXsn+f7n7/OTn24BDkh7bfiR/N+6+jNDbqa5bOfByPccmWwgFxqap+f7lAS0I/0hSNfZV1vzlpAexH/Aq4VP+y2ZWV5uUE/4BA2Bm3c2sXY0yywifmNvy3xNbM8Kn6r2r/wP6ET5N1qxrRY3XTB/+aQacmVbGd4Cjk9c9ATgCKAYuBP5Z1/Y06zveFOF9BFiTtr2K/31fIZz4+tXcaGanmNk5NbadRgit1YTezAPVZdbWHhvRTtVqtlfKzHqZ2T6Eay7tgH8RhoDWd1zpavtbqxbzPq3j7quAnxF6JNXvUTPg7fX8nVRfAyuv8foVyf9rvlZ63UrdvRzJCQqMTfM0MDo5EbQETgamEoZHdjOzXgDJWHoHvn7yaJ6MPRe4+82Ecfge/Ddw0k8G1Z4BRiS/354wI2fXmk9KQuM04JTk5ORAiZmdkPzuDsA8wnDPZOCopDwI1zJqm1X1NPBLM8tPTpi3Ab83s05mthBY4u5/JQyf9K5t+3rK/Ely4RozG0EY5ism3i3A98zs+OqhLDMrBK4g9ErSHQLc6e63J+/LEYTrRbW2Rx3tVJ9XgB5mtmfy+AeEIapBwKvu/mdCb28Y4WQNtbf908CpZtYiee9HE/7WNpq7LwXOBS5NZpnNBHY1s0EAZrY34RratsmvfD95HoRe0ePuviL5vdHJ77QHhtdRt9qOT7YACow4BWa2ssZ/exEu1HYhnJTmEk5AV7n7l4QLhHeb2RzCSaqc8KkWgORT11nA/clzHgZ+ngxtPQscaWZ/q1GPXxJOQG8QQun3Xsv0xGS2zn2Ei7JrCSerUcnv/gu4xN2nu/s0won/ZTN7FWifXs8afgd8QLjY/Rbhk+W5HqaqXgk8a2ZFwDXAqNq216jnVOAvwDQze5MwVDM0GXaJkrzf3wOOAuaZ2VzCEMlI/98ZUn8iBOlrhPd5DrBLPe1R2/b66vU5cDxwV/J65wDHEno1nczsLcK1gJXA1mbWFngTqDCzWXz9k/uVwGeECRRvE066Z8a+R3XU8T5Cz2lsMix4FHCtmb1OuA73s2QIDeA/wD1mVn0R+6xk+/GEMJkLzCIMid5Zy0s+BjxoZgdvat2l4aW0vHnmJcNEvyHMMFmdfMqfDGzrjeyeCDPrAwzwZLZVMoTTN7mQKwJAMkvqaHcfmu26SPZoWu1m4GGaaRkw28zWEj7h/7ixhUViPnChhamtVcBHhKE1EZGvUQ9DRESi6BqGiEiOM7O+Fm40rbn9CDObbWYvm9kv6isnoz2MoqKiloQbwD7lf6dniojkomaE+1dmFxYW1jsZIkZRUdHWhGnX9VleWFj4ZV1PMLMLCFOoV7l7v7TtLQgTKPYlrEIwnTDh5PPaysr0NYx9Cbf/i4g0Nd/l6ze6bpSioqKty5atXJLfPmq906VFRUW71BMa7wI/Isx6S9cDKE6mV2NmLxGmfD9cW0GZDoxPAaaPvIySRXWGnmTRme9PA+C6nQ/Mck2kLmqnLcNpPoX58+dDcv7LgHb57dswfeTllCxaUuuTWnX5BvvdfmlHQk+k1hOuuz9qYV2v/3kdwuoL1VYQptXXKtOBUQFQsuhL1nyasVWkJcNatmwJoDZq5NROW4b8/PzqHzM6DF+yaMnmbvvlfH3tuLbUWKyzJk2rFRFpmt4m3Nm/NeHm0UGEG1trpcAQEWlCzOw4wurGtyY36j5NmDF7R7J4Za0UGCIiOc7dPyBZoNPd70/b/jjweGw5ug9DRESiKDBERCSKAkNERKIoMEREJIoCQ0REoigwREQkigJDRESiKDBERCSKAkNERKIoMEREJIoCQ0REoigwREQkigJDRESiKDBERCSKAkNERKIoMEREJIoCQ0REoigwREQkir6iVUSkEdoXqKpjf6qhKpJGPQwREYmiwBARkSgKDBERiaLAEBGRKAoMERGJosAQEZEoCgwREYmiwBARkSgKDBERiaLAEBGRKAoMERGJosAQEZEoCgwREYmiwBARkSgKDBERiaLvwxARyVFmlgfcCPQGSoFR7l6ctv9c4DigErja3SfUVZ56GCIiuWsY0Mrd+wNjgLHVO8ysA3Am0B84GPhrfYUpMEREctdAYAqAu88E+qTtWwV8CBQk/1XWV5gCQ0Qkd7UDlqU9rjCz9EsRC4G3gDnA9fUVpsAQEcldy4G2aY/z3L08+XkI0BXYGdgRGGZm36mrMAWGiEjumg4cBmBm/YC5afuWAmuAUncvAb4COtRVmGZJiYjkrgnAYDObAaSAEWZ2DlDs7pPM7CBgpplVAi8BU+sqTIEhIpKj3L0SOLXG5nfS9l8KXBpbngJDRKQR6r5NCc1Sq2vdX9GlhMUNWB9oqoGRSnH4jZexTW+jorSMSaN+w9J3P1q3e78LfkHPnx5O6fKVTP/jOBZMfp4WW7Xm8Jsuo8PO29MsvwVP/ep3LPvoU44e/+d1v/fNvXvwzJixFN0yPhtHlXs2op06dNueYXddA6kUyz78hMdPvoTyNSUc+teL2WHgPpStWAXA+B+cTunyldk6styyEe3Uboeu/PCeP5JKpVjz5TIePe5cyteUANC8dSt+NvUfTBp5MUv8vWwdlaxHVGCYWV/gD+7+vc1bnYax+7CDaN4qnzsGHMt2fXtz8NgxPDjsdAC69NyNnscNZVzfYwAYOWM870+byYDzR7Jo3gIeO/FCuuxlfLP37nwyey53HTAcgO377c2BV53NnNseytpx5ZqNaafB157PqzePZ94DT/DtkUfT/5wRvHjVTXQt3JN7DxnFmiVLs3lIOWlj2qn/2Sfx5oNP8epN93PglWexz8ijmXXDvXQt7MnQmy+n3fbbZPOQpBb1zpIyswuAcUCrzV+dhrHjwEKKp7wIwMevvM62fXqu29epx7f48PlZVJSWUVFaxpIFH7JNL+Nbhwykomwtx08Zx6BLTqf46Re/VuaQv13C5NMuo6qy3ntfJNLGtFPnPXah+KkXAFg4fQ47DiyEVIqtd92JI269ghEvPcDeI47KyvHkqo1pp89ee5vWHdsB0LJdGyrWhpmezVvm8+APR7P4HfUsGqOYabXvAj/a3BVpSC3btaF02X+HI6oqKkg1awbAornOjoP6kN+mgNZbd2CHAd8mv6A1W3XqSOuO7bjv0FHMf3waB//pwnW/v9sRB7LozQUsmf9+gx9LLtuYdvrstbexIw8EwI78Pi0KWpNfsBWz/nYv/zzhfO47dBT7nn4cXfayrBxTLtqYdlr+n8/Y95fHc9q8J9hlyCDeengKAAtnzGH5fz7LynFI/eoNDHd/FFjbAHVpMKXLV5LftmDd41ReHlUVFQAsfuc9Zt9wH8dPGceQGy7h41deZ/XipaxZ8hU+aRoA8x9/7mufonqdcCRzbtVQVKZtTDv969w/sNuRB3Lic3dTVVXF6sVLWbt6Da9cdzfla0ooW7mK96fN5Ju9d8/WYeWcjWmnwddewMSTfs1NPYcy5cyrGHb3H7JVfdkATfLGvYXT57DrYYMA2K5vbz6fO3/dvq06dSS/bQH/GPhTJp96Ke126MqieQv46KUidj1sfwB2GrQvi95ct+Aj2/bpycIZcxr2IJqAjWmn7oMHMO2iv3DXAcOpqqjgvakz+MZu3fj59AdI5eWR17w5Ow7ch0/nvJmtw8o5G9NOa5Yup2TZCgBWfLJo3fCUNG5NcpbU2xOm0n3wfvx8+gOQSjFxxEX0O/skviz+iPmPT6Nzj+6MmvUIFWVrmXr+H6mqrOTFq2/hyHFX8vMZ46lcW86E4WFIaqtOHTXbZjPZmHZa4u/zo/v+RHlpGV+8uYAnR19BZXk5b9wzkZEzH6Jy7Vpev3siX7xVXH8FJMrGtNNTv/odh93wW1LN8kilUjw5+opsH4ZESFVVVdX7JDPrBox39351Pa+oqKgb8P6zR5zBmk8beoawxLq0ygG4PKVx/MZM7bRlGFPyBvPmzQPYubCw8INNLa/6PNpp1Mk0W7So1udVdOnC4nG3Zux1Y0T1MNz9A6DOsBARkdzWJK9hiIjIhlNgiIhIFAWGiIhEUWCIiEgUBYaIiERRYIiISBQFhoiIRFFgiIhIFAWGiIhEUWCIiEgUBYaIiERRYIiISBQFhoiIRGmS34chItLYdd6rDS2XldS6v7R9Gxr6SyTUwxARkSgKDBERiaLAEBGRKAoMERGJosAQEZEoCgwREYmiwBARkSgKDBERiaLAEBGRKAoMERGJoqVBRERylJnlATcCvYFSYJS7F6ftHwJcCqSAImC0u1fVVp56GCIiuWsY0Mrd+wNjgLHVO8ysLXAtMNTd+wIfAJ3qKkyBISKSuwYCUwDcfSbQJ23fAGAuMNbMXgQ+d/cv6ipMgSEikrvaAcvSHleYWfWliE7AAcCFwBDgLDPbra7CFBgiIrlrOdA27XGeu5cnPy8BZrv7Z+6+EngB2LuuwhQYIiK5azpwGICZ9SMMQVWbA/Q0s05Jr6Mf8FZdhWmWlIhI7poADDazGYSZUCPM7Byg2N0nmdmvgaeT5z7k7vPqKkyBISKSo9y9Eji1xuZ30vaPB8bHlqchKRERiaLAEBGRKAoMERGJosAQEZEoCgwREYmiWVIiIo1Qqm8HUqWVte9v2aEBaxOohyEiIlEUGCIiEkWBISIiURQYIiISRYEhIiJRFBgiIhJFgSEiIlEUGCIiEkWBISIiUTbLnd5nvj+Nli1bbo6iJYMurfJsV0EiqJ0at9LS0mxXocFslsC4bucDWfPp4s1RtGRA9Qno8pRluSZSF7XTlmFMyRvZrkKD0ZCUiIhEUWCIiEgUBYaIiERRYIiISBQFhoiIRFFgiIhIFAWGiIhEUWCIiEgUBYaIiERRYIiISBQFhoiIRNksa0mJiMimSe3cjlRFZe37m7VrwNoE6mGIiEgUBYaIiERRYIiISBQFhoiIRFFgiIhIFM2SEhHJUWaWB9wI9AZKgVHuXrye50wGJrr7zXWVpx6GiEjuGga0cvf+wBhg7HqecyXQMaYwBYaISO4aCEwBcPeZQJ/0nWZ2NFBZ/Zz6KDBERHJXO2BZ2uMKM2sOYGY9geOA38YWpmsYIiK5aznQNu1xnruXJz8PB7YDpgHdgDIz+8Dda+1tKDBERHLXdOAI4CEz6wfMrd7h7hdU/2xmlwGf1RUWoMAQEcllE4DBZjYDSAEjzOwcoNjdJ21oYQoMEZEc5e6VwKk1Nr+znuddFlOeLnqLiEgUBYaIiERRYIiISBQFhoiIRFFgiIhIFAWGiIhEUWCIiEgUBYaIiERRYIiISBTd6S0i0hh13wXyVte+v3IrWNVw1QH1MEREJJICQ0REoigwREQkigJDRESiKDBERCSKAkNERKIoMEREJIoCQ0REoigwREQkigJDRESiKDBERCSKAkNERKIoMEREJErTXK02leLwGy9jm95GRWkZk0b9hqXvfrRu934X/IKePz2c0uUrmf7HcSyY/Dwdum3PsLuugVSKZR9+wuMnX0L5mhL6nzOCnscNpaqyipeuvpl3HnsmiweWYzLYTvuMOobCU46lsrycF668iQWTn8/eceWaDLZTdXnHTb4Vn/gsRbeMz9JByfrUGxhmlgfcCPQGSoFR7l68uSu2Oe0+7CCat8rnjgHHsl3f3hw8dgwPDjsdgC49d6PncUMZ1/cYAEbOGM/702Yy+NrzefXm8cx74Am+PfJo+p8zglk33EvfM4dz/S4Hk1/QmlNee0yBkUGZaqc54x7mO2f8jNv6HEXzVi0Z8dL9vDd1OhVla7N5eDkjU+304lU3AXDglWfRumO7rB2P1C5mSGoY0Mrd+wNjgLGbt0qb344DCyme8iIAH7/yOtv26bluX6ce3+LD52dRUVpGRWkZSxZ8yDa9jM577ELxUy8AsHD6HHYcWMjaVWv46sNPyC9oTYuC1lRVVmXleHJVptppu+/0YuH0f1NRtpbS5Sv5svgjtum1e1aOKRdlqp0Aehx1CFWVVevKk8YlJjAGAlMA3H0m0Gez1qgBtGzXhtJlK9c9rqqoINWsGQCL5jo7DupDfpsCWm/dgR0GfJv8gtZ89trb2JEHAmBHfp8WBa0BWL7wU05/azKnzJnArOvvbviDyWGZaqdQzop15ZStWEXL9m0a9mByWKbaqfOeu7LXcUN57rfXZeU4pH4x1zDaAcvSHleYWXN3L99MddrsSpevJL9twbrHqbw8qioqAFj8znvMvuE+jp8yjmUffcLHr7zO6sVL+de5f2DIDZew94gfseDJF1i9eCm7DBlEm65duG7n7wNwwtO389H0OXwye25WjivXZKqdapaT37aAkq9W/M/rycbJVDv1Hj6Mttttw4nT7qJDt+2oKFvLVx98zLtPq7fRWMT0MJYDbdN/Z0sOCwhd4F0PGwTAdn178/nc+ev2bdWpI/ltC/jHwJ8y+dRLabdDVxbNW0D3wQOYdtFfuOuA4VRVVPDe1BmULF1G+ZqSdd3tkq9W0KqDxl4zJVPt9PGsN9jxu4U0a5lPy3Zt6NzjWyyaN7+2l5UNlKl2eubCa7m934+564DhvHbnBF7+850Ki0YmpocxHTgCeMjM+gFb/MfntydMpfvg/fj59AcglWLiiIvod/ZJfFn8EfMfn0bnHt0ZNesRKsrWMvX8P1JVWckSf58f3fcnykvL+OLNBTw5+goqy8v5ZPZcRs58iKrKSha+NIf3pk7P9uHljEy206zr72HEi/eTyksx7eK/UFFalu3DyxmZaidp/FJVVXVfqE2bJdULSAEj3P2d9T23qKioG/D+s0ecwZpPF2e4qpIpl1Y5AJenLMs1kbqonbYMY0reYN68eQA7FxYWfrCp5VWfR/cseIKWeatrfV5p5Va8uWpoxl43Rr09DHevBE5tgLqIiEgjpju9RUQkStO801tEpJFLddyVVIvaby5NrW0BqxqwQigwRERyVn0rdZjZ2cCxycMn3f3yusrTkJSISO6qdaUOM+sOHA8MAPoBB5tZr7oKU2CIiOSuulbqWAgc6u4V7l4FtABK6ipMQ1IiIrmr1pU63H0tsNjMUsC1wL/dvc47WtXDEBHJXXWu1GFmrYD7kuecXl9hCgwRkdw1HTgMoOZKHUnPYiLwuruf4u4V9RWmISkRkdw1ARhsZjNIVuows3OAYqAZsD/Q0syGJM//tbu/XFthCgwRkRxVy0od6Us7tdqQ8jQkJSIiURQYIiISRYEhIiJRFBgiIhJFgSEiIlEUGCIiEkWBISIiURQYIiISRYEhIiJRFBgiIhJFgSEiIlEUGCIiEkWBISIiURQYIiISRcubi4g0Rh0MWtaxvxT4T0NVJlAPQ0REoigwREQkigJDRESiKDBERCSKAkNERKIoMEREJIoCQ0REoigwREQkigJDRESiKDBERCSKAkNERKIoMEREJIoCQ0REoigwREQkigJDRESiKDBERCSKAkNERKIoMEREJIoCQ0REoug7vUVEcpSZ5QE3Ar0J3wI+yt2L0/b/AjgFKAeudPcn6ipPPQwRkdw1DGjl7v2BMcDY6h1m9k3gDGA/4BDg92bWsq7CMt3DaAZwmk8hPz8/w0VLppSWlgIwpuSNLNdE6qJ22jKUlZVV/9gsk+WuXbtp+xMDgSkA7j7TzPqk7fsOMN3dS4FSMysGegGzayss04HRFWD+/PkZLlZEpNHrCrybgXKWA0vd6Rjx3KXJ82vTDliW9rjCzJq7e/l69q0A2tf1YpkOjNnAd4FPgYoMly0i0hg1I4RFrZ/MN0RhYeGXRUVFuxBO6PVZXlhY+GVd+4G2aY/zkrBY3762wFd1vVhGA6OwsLAUeCmTZYqIbAEy0bNYJwmBuoIg1nTgCOAhM+sHzE3bNwu4ysxaAS2BHsC8ugpLVVVVZaBOIiLS2KTNkuoFpIARwGFAsbtPSmZJnUyYAHW1uz9aV3kKDBERiaJptSIiEkWBISIiURQYIiISRYEhIiJRFBiSs8xsDzPTemmNSDJrR7ZQarwMMLOMLgkgm87MjgEuBgoVGtlnZueYWUd3r1RobLk0rXYTmVle8o8gRVib5WN3/0+269VUJe1wJXAFcCqwK3APUJR2h6s0IDNrBzwJvAz83t2/rP53k+WqyQZS0m8CM2uWFhaPEFaCvNDMhmW5ak2Wu1cRblK6H7gJKAaGo55GgzOzPDM7DxgEtCIsoX2NmW2tnsaWSQ22Cdy9IgmL84AXgYOAN4B+ZnZUVivXBJlZCwB3P4KwJs6jhLtcFwDHA/01fNigrgAGADOAW4AbgC+AqxUaWyY11kao8Uf+XcIXkJS4ewkwEVgIDDSzLtmoX1OUDHGsNbPOZraTu48kBMU/CaHxKfADoEU269nE3AN8C7iTMCT4cfLzZ8D11dc0slc92VDqom+gZBiqumfRi7CY17nAaDOb5+4vmdmDQGt3X5TVyjYRZpZKPq12BR4H3jKzFu7+UzMbCzwLfB9ok4S6NIxiwjDUnkDntG0PAj8kLHgnWxBd9N4ISQ/jCUL3uj9wNtAJOA24yN2nZbF6TZKZtQceIFy3mA/cTgjz0cDvgJvdfWH2atg0mVlnoDvwF2Bs9eJ2SaDHfQWQNBoakopkZt3TLpqOBd509xOBUYSx2lnAdcCqLFWxyalxPaISmED4QpgxwPXAt4G73f1ihUV2uPsX7v4K4d/I78zsyGS7wmILpCGpCGZ2INDe3d9LNn0IlAG4+wtmNgno4+73ZKuOTU1yzaLCzLYlrPdfTOhRHALcQfgwNBe4Jnu1lGruPsXM1pLh742QhqUhqQ1gZmcCbwHdgJ2AVwlfkXg9cJq7z8he7Zqe5Evs7wPmAA+5+2wzuxZoAxwADHP3d7JZR5FcoiGpOqxnCmYbwifYDwlDT32AC4DzFBYNJ22W2gjg3+5+fhIW+xO+dvJR4BCFhUhmaUiqFtUX5ZKT003AHHe/yszOAvYHnnP3Z8ysg7vX+T24khlpdwenkk3FhC+1L3D3VcB+wEfu/kzWKimSwzQkVQ8zmwD8H/AOsBXgwLHAtoTptMuSu4tlM0pbgqUrYebTG0AB8GPgaULv70DC0KBnr6YiuUs9jBrM7GdAgbvfbGbbA22BycCfCRfsjiUsNdFFPRcrsnIAAALvSURBVIuGkRYWnYCnCLPUjiH0MO4lDK22B0519/nZq6lIblMPoxZmdrW7X2RmVwMfAa8D84BJwHBN02wYaWGxNWGa7B7u/jczewl4jDBUqPteRBqAAiNRfQd32uOJQJW7DzOzfYCjgYOB37r7k9mqZ1NkZt8g9PIeBc4ClhBmQfUHfk7o8a3S0KDI5qUhKb42pz+PMEX2C8KQxx1mNtndDzezKsLUzdeyWtkmJmmTYwhrQD1GmMZ8MWFpidHA8e6+Mns1FGk61MNIk/Qq3gSmJDfktQbuIlyv+F5WK9eEJdcuTga+QVjQDsL3XMxxd90IJtJAmvR9GMkCgtU/70AYgrooCYs9gVvc/cfAr7JWScHdFwO3Af8hrNe12t0fVliINKwmGxjJNYv07tUnwPLkIjfAaqBDsm7/3IavoaRz9y8Id3XPI3zXhYg0sCY5JJUsh12VjI8/QrjHYg/CukOnAh2ArsCV7v549moqNdWcnCAiDafJXfSuDovk4d2E70p4kLDa7A8IgbEnsNzdF2SnllIbhYVI9jSpwFjPF88vICwmeCtwJuFLXszdi7JRPxGRxqzJBEbat7KlgHGE5T46E+4avp2w4umTwInZq6WISOPVJC5617jAfS9Q6e53A38nLPfRDrgfGKP7LERE1i/nL3qnLS2RR1hl9krCTWDHuXtxMp12NdBZy2GLiNQupwMjbTZUCpgIfEpYZfZwwpcf/cTd389mHUVEthQ5PSSVNgx1CbDE3U8BjiRMpe0BPJ7czS0iIvXI6cAAMLP2QGugi5n1TkLkfsKidUe4+5qsVlBEZAuR00NS1cysI+HrPLsTptEOBy5x96lZrZiIyBYk53sYAO6+lHCT3iLCFyDd5u5T09eSEhGRujWJHka15Et4RgA7Are7+xtZrpKIyBajSfQwqrn7l4TlsRcAn2e5OiIiW5Qm1cOopgXsREQ2XJMMDBER2XBNakhKREQ2ngJDRESiKDBERCSKAkNERKL8PwQlPgjAFJJkAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ClassificationReport(fitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ClassificationReport(unfitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "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": [ + "oz = ConfusionMatrix(fitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "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": [ + "oz = ConfusionMatrix(unfitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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PDDZ+PfsRZt0mfsN6sn5e7pGz3fr7Xds0jhq03/gQB5+/AkAEokEo049hVlmAJxy2ukc96vjASgvL+e4o4/imGOPo3OXLmmPS0TahjaTHDJl+ldfce7oM1m7Zg3nnn8Be++zL7845CBemPQK4268gWh2NgsX/kTR0iJuGHszQ7bdlmeefor33n2H0nXr6NixE3fecw+vv/oqL02ciOclGH3uebw4YQK33XU3AKNOPZnb7rybbt271xpDWVkZFRXl5Oal3t66auVKYrEKcnLXH/f19Onc8cdb8bwE3br34OZb/8hvf30eV117Hf0HDOD58c9SVFTEUUeP5He/uYAOHTuy5957M+nFF5nw0iRCoRC33jyW3UaMoE+fvtx+6zg8Dzp07MCYG8dSWFiYEouItB5tJjmcv6dr0ln+xsrLy+PeB/7CiuXLOe3kE9lzr73XG79Fr15cM+Z6Xnj+OV54/jmuvOZaVq1axYMPP0o4HOb8c8/m22++AaB9+/bc/ef78DyPO269leJVq1i6dCkdO3ZKSQzz5szh7FFn+M+IhSOcePIp9O27FQD/+NuTvPHaayxetIhuPbpz7Q03UlBQsN78N994PeNuu50BAwby4gsTmDd37ga3sWhZEU+NH080ms2M775j2tSp7LDjjnz+2adcevkVnHn6qVx/400MGDiIF1+YwJOPP8ZvLrxoUxSviGRIm0kOmbLTLrsQCoXo3KUL7dq1Y9XKleuNHxz0Kd+jZ0+++uILwuEw0WiUKy/7A/n5+SxZvJhYzH/aeav+/QAIhUIcfsQRvPH6a/z03/8y8phjUtab3KxUU2Wz0nfffsuVl13KVlv1S5lmWVERAwYMBGDkMcemjPeSbh3t3bt31fWTY449jlcmvcSyoiL23W9/srKymD93LreMHQtALFZRlaREpPXS3Uob6bvgrL+oaCklJSV07NRp/QlC63+cZca/33uXP95xJ5ddeRWJhFd1IA6Hqr+Oo0b+knfeepNpUz9nz733aVJs2263HaPOGs0Vl11KIpFYb1y37t358YcfAHji0Ud47913yM7JpqhoKQAzZ3xXvQnh6rh2GzECmzmDl158gZHH+kllq379uHHcOB5+/AkuuvgS9t533ybFKyIth2oOG6m0rJRzzhrFupISrr7uekKhUJ3T9+nbl9y8PEadejIAXbt1ZemSpSnTde/Rg/yCAnbYcShZWU3/mkYecyxvvfEGzz37DMefeFLV8KuvG8P1111DOBSma7dunHTqaWRHs7ll7Fh6btGT7t171Lq8UCjEzw86mE8+nkKfPn0BuPKa67juqquIx2MQCjHmhpuaHK+ItAwhrxU8eTp16tR+wDy69K5+TWg8xsCuhW26G4kLLzifSy+/PKWZprR0Hbm5eRmKqvlVlJczp2g1RFKTpF4HWU1lUU1l4Ut6TWj/YcOGzW/MvGpWaoFKS0s56Vf/R/8BA9R+LyIZoWalFig3N5enxz+X6TBEZDOmmoOIiKRQchARkRRKDiIikkLJQUREUuiC9CbyxGOP8vTf/8bLb7xFTk5Ok5bx+WefcvmllzBgwEBCISgtK+Pww4/ghJNPbnJcdfUQO+nFiXTo0IF99z+gyctP9sLzzzHhueeIZEUYfc657LPvfuuNn/LRZO69+y7y8vL42Z57Mfrc85j04kRefuklAMrKy5g1cyZv/+s/FLZvv0liEpGmaVPJ4fuh29Q6vMtvL6bz6PMA+Om8UZRMmZwyTd7w3dny0b8DsOLJRym6649s/dWsBq/7tVde4eBDD+PN11/jqJG/bEL0vl13272qV9fy8nJ+eeQv+MWRRzb5YFlXD7EbE2dNRUVLeeapp/jHs+MpKyvjrNNOZcQePyM7eA4lkUhw05jreOixJ9iyTx+uvuJyvpg2laNG/rIqjlvG3sTRI3+pxCDSArSp5JApn3/2KX369OG4Xx3PNVdezt777sdZp5+W0ntp9+7dufXmseQXFNC5cxdysrO54eZxG1zu2rVriYQjRLIinD3qDDp37syqVau47a67+OO4cfz4ww94XoLzf3shw3fdjff/828e+ssDeB4MHjKEq68bw5GHHcILk17hww/e58nHHiUrK4tu3bpzy+138NCDf6Fr164c96vjuev22/jyi2kAHHr4LzjplFMZc/VVKb3KbtVvKy664IL14tx1t91wgwczdOedyc7OJjs7mz59+/L9LGO77XcAYOWKFRS2b8+WffoAsNPOO/PltGnsvMswAL779hvmzpnDlddcm46vSEQaqU0lh4ac6fd+8PF6p+l0+ll0Ov2sBq934oQJjDz2WPr17092djb/XbCArbfZJqX30tNOOoGbxt3KwEGDuO/ee1i6eHHKsj779BO/t9VwiKysLC678iry8/0eVQ85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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = PrecisionRecallCurve(fitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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PDDZ+PfsRZt0mfsN6sn5e7pGz3fr7Xds0jhq03/gQB5+/AkAEokEo049hVlmAJxy2ukc96vjASgvL+e4o4/imGOPo3OXLmmPS0TahjaTHDJl+ldfce7oM1m7Zg3nnn8Be++zL7845CBemPQK4268gWh2NgsX/kTR0iJuGHszQ7bdlmeefor33n2H0nXr6NixE3fecw+vv/oqL02ciOclGH3uebw4YQK33XU3AKNOPZnb7rybbt271xpDWVkZFRXl5Oal3t66auVKYrEKcnLXH/f19Onc8cdb8bwE3br34OZb/8hvf30eV117Hf0HDOD58c9SVFTEUUeP5He/uYAOHTuy5957M+nFF5nw0iRCoRC33jyW3UaMoE+fvtx+6zg8Dzp07MCYG8dSWFiYEouItB5tJjmcv6dr0ln+xsrLy+PeB/7CiuXLOe3kE9lzr73XG79Fr15cM+Z6Xnj+OV54/jmuvOZaVq1axYMPP0o4HOb8c8/m22++AaB9+/bc/ef78DyPO269leJVq1i6dCkdO3ZKSQzz5szh7FFn+M+IhSOcePIp9O27FQD/+NuTvPHaayxetIhuPbpz7Q03UlBQsN78N994PeNuu50BAwby4gsTmDd37ga3sWhZEU+NH080ms2M775j2tSp7LDjjnz+2adcevkVnHn6qVx/400MGDiIF1+YwJOPP8ZvLrxoUxSviGRIm0kOmbLTLrsQCoXo3KUL7dq1Y9XKleuNHxz0Kd+jZ0+++uILwuEw0WiUKy/7A/n5+SxZvJhYzH/aeav+/QAIhUIcfsQRvPH6a/z03/8y8phjUtab3KxUU2Wz0nfffsuVl13KVlv1S5lmWVERAwYMBGDkMcemjPeSbh3t3bt31fWTY449jlcmvcSyoiL23W9/srKymD93LreMHQtALFZRlaREpPXS3Uob6bvgrL+oaCklJSV07NRp/QlC63+cZca/33uXP95xJ5ddeRWJhFd1IA6Hqr+Oo0b+knfeepNpUz9nz733aVJs2263HaPOGs0Vl11KIpFYb1y37t358YcfAHji0Ud47913yM7JpqhoKQAzZ3xXvQnh6rh2GzECmzmDl158gZHH+kllq379uHHcOB5+/AkuuvgS9t533ybFKyIth2oOG6m0rJRzzhrFupISrr7uekKhUJ3T9+nbl9y8PEadejIAXbt1ZemSpSnTde/Rg/yCAnbYcShZWU3/mkYecyxvvfEGzz37DMefeFLV8KuvG8P1111DOBSma7dunHTqaWRHs7ll7Fh6btGT7t171Lq8UCjEzw86mE8+nkKfPn0BuPKa67juqquIx2MQCjHmhpuaHK+ItAwhrxU8eTp16tR+wDy69K5+TWg8xsCuhW26G4kLLzifSy+/PKWZprR0Hbm5eRmKqvlVlJczp2g1RFKTpF4HWU1lUU1l4Ut6TWj/YcOGzW/MvGpWaoFKS0s56Vf/R/8BA9R+LyIZoWalFig3N5enxz+X6TBEZDOmmoOIiKRQchARkRRKDiIikkLJQUREUuiC9CbyxGOP8vTf/8bLb7xFTk5Ok5bx+WefcvmllzBgwEBCISgtK+Pww4/ghJNPbnJcdfUQO+nFiXTo0IF99z+gyctP9sLzzzHhueeIZEUYfc657LPvfuuNn/LRZO69+y7y8vL42Z57Mfrc85j04kRefuklAMrKy5g1cyZv/+s/FLZvv0liEpGmaVPJ4fuh29Q6vMtvL6bz6PMA+Om8UZRMmZwyTd7w3dny0b8DsOLJRym6649s/dWsBq/7tVde4eBDD+PN11/jqJG/bEL0vl13272qV9fy8nJ+eeQv+MWRRzb5YFlXD7EbE2dNRUVLeeapp/jHs+MpKyvjrNNOZcQePyM7eA4lkUhw05jreOixJ9iyTx+uvuJyvpg2laNG/rIqjlvG3sTRI3+pxCDSArSp5JApn3/2KX369OG4Xx3PNVdezt777sdZp5+W0ntp9+7dufXmseQXFNC5cxdysrO54eZxG1zu2rVriYQjRLIinD3qDDp37syqVau47a67+OO4cfz4ww94XoLzf3shw3fdjff/828e+ssDeB4MHjKEq68bw5GHHcILk17hww/e58nHHiUrK4tu3bpzy+138NCDf6Fr164c96vjuev22/jyi2kAHHr4LzjplFMZc/VVKb3KbtVvKy664IL14tx1t91wgwczdOedyc7OJjs7mz59+/L9LGO77XcAYOWKFRS2b8+WffoAsNPOO/PltGnsvMswAL779hvmzpnDlddcm46vSEQaqU0lh4ac6fd+8PF6p+l0+ll0Ov2sBq934oQJjDz2WPr17092djb/XbCArbfZJqX30tNOOoGbxt3KwEGDuO/ee1i6eHHKsj779BO/t9VwiKysLC678iry8/0eVQ85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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = PrecisionRecallCurve(unfitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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SbccR2Ehx+JvRelYmM1mrNa8vUqQ3da25ume8oD0ZHuPxWLh+++/59y5c1y6dImSJUve8javXr2KxWK5pQ91eHj4LcdRWMix+FtROxZJSUlcu3YtT8vRZPlXqbUekmd7ySMWs1w/z0+xsbHs27ePJk2aYLFY+O9//0vJkiUpVqzYLW87JSUFi8VCUFDQLW0nOTlZurWc5Fj8ragdCz8/P+Lj40lJScmzFoR824pMORwOOnbsyG+//ca2bduoXLkyYWFhebZ9u92e581gIYoyi8Vyy1206fnUX6dDxjnkG5PJxODBgzl8+DBlypTxdjhCiBzk9S3kPpUcZBCcZ61cuZL58+ezZMkSbDYbPXr08HZIQggvcacqqygiNm7cyLZt2/jpp5+8HUq+2LlzJ02aNCEiIoKIiAg6d+7M888/77rL5eLFi4wYMYKIiAh69+7NSy+9xLlz51yv3717N8888wwRERF06dKFRYsWub3vTz/9lIcffpg1a9Zku97y5cuZPHnyzb3BLCQkJDB06FB69+5N//79uXjxYo6veeONN3jssccyLIuIiODIkSOux4mJiRnKsS9dupQnnniCiIgIevbsyc6dO2865uPHj/PII49k+twvv/xCt27d6NmzJzNmzACMbssxY8bQo0cPIiIiOH5cbrzMLZ9qOcj9SnnL4XC4JuEBGDt2LEOGDKFGjRr5HssrX0fz+Z7c/wGnjbvITNd6tzPpkYbZvr5x48ZMnTrV9fill17iu+++o23btgwZMoQ+ffrQunVrALZt28bAgQOJiori1KlTjBs3jtmzZ1OmTBkSEhJ48sknCQsLo0WLFlntzmXdunVMmzYNpVQu3m3eWLx4MTVr1mTo0KGsXr2amTNnMmrUqCzXv3btGtHR0dSsWZOdO3dy33335biP1atXs3XrVubNm4fNZuPkyZP861//YsWKFZQqVSpX8X755ZcsWLAgyyT2+uuv8/777xMWFsaAAQOIiYnh8OHDJCUlsXTpUn755RfeeecdPvxQpqXJDR9LDiIvjRw5ko8//pivvvqKf/7zn4SGhhIaGurtsLwmKSmJs2fPEhoayv79+ylevLgrMQA0bdqUKlWqsGvXLnbv3s1jjz3muh4TEBDAnDlzbrj76vfffycyMpLU1FRMJhOjRo1iz549xMTE8NprrzF16lTXhf6EhARGjhzJqVOn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UOdNp06dIj4+npSUFE6ePJlp3azrzZ49m0uXLvHpp59iNpvZu3cvzz77LN9++22uB6d+8sknrFy5ksDAwBueS05OZsKECXz++ecEBgbSq1cvWrVqxddff03NmjUZOnQoq1evZubMmYwaNSpX+70Vkhzy2KJFi3j11Vc5evQoEyZMAJDEUEAtX76cTZs2kZCQwLlz53jyySfZuHEjv/76K6+88gqtW7emWbNmbN26lT179vD2229jt9spX748kydPpn///pQqVYq//vqLjz/+mMjISH7//XdSU1N55plnaN8+YyHB2NhYXnvtNa5evcrZs2fp3bs37dq144knnmDNmjWYTCbGjh1LkyZNqFKlCuPGjQOgRIkSvP3228TExDB58mRsNhvdu3cnICCARYsWkZKSgslkYtKkSQQFBfHmm2+yf/9+ypQpwx9//MGHH36IxWJh9OjRJCYm4u/vz1tvvXVDMb70kpKSOHv2LKGhoezfv5/ixYu7EgNA06ZNqVKlCrt27WL37t089thjrtIgAQEBzJkzh6CgoAzbPHbsGKNGjSI5OZmAgACmTp3KpEmTaN++PS1atGDz5s2sWbOGd955h5YtW1KtWjXCw8PZtGkTX331FUFBQcyZMweLxULbtm1z9X6++OILHnzwQQICAvjss88YMWJEjp+PpUuXsnz5clftprp16/L5559nSAxxcXEMGpSxEMR99913Q4uzSpUqvP/++7zyyis37OfIkSNUqVLFVZiwYcOG7Nq1i+joaPr16wdAixYtmDlzZo4x5yVJDnkgKSnJVXK6d+/enDx5ksGDB3s7LJ/jzpl+C9Ujw+PMRsLWvO0f1LztH27tMy4ujrlz57J69WrmzZvHsmXL2LlzJwsWLMjwZThmzBjee+89wsPDiYqKctXN79ixI23atOHTTz+lVKlSTJ48mdjYWDp37kzjxo0z1PM5fvw4HTp04KGHHuLPP/90ddEopdi9ezf16tVj586dREZG0rt3b95++22qV69OVFQUs2fPpmnTpiQmJhIVFQXARx99xMcff0xgYCBjxoxh+/btlCxZksuXL/P5559z8eJFHnroIQAmTpxIREQE999/P9u3b2fy5MlMmTKF9DxdoXbixIkMGDCAFi1asHHjxhuKFKZ3+vRpli9fTsmSJbHZbKxbt47HHnuMVatWMXfuXN58880c308a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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ROCAUC(fitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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SbccR2Ehx+JvRelYmM1mrNa8vUqQ3da25ume8oD0ZHuPxWLh+++/59y5c1y6dImSJUve8javXr2KxWK5pQ91eHj4LcdRWMix+FtROxZJSUlcu3YtT8vRZPlXqbUekmd7ySMWs1w/z0+xsbHs27ePJk2aYLFY+O9//0vJkiUpVqzYLW87JSUFi8VCUFDQLW0nOTlZurWc5Fj8ragdCz8/P+Lj40lJScmzFoR824pMORwOOnbsyG+//ca2bduoXLkyYWFhebZ9u92e581gIYoyi8Vyy1206fnUX6dDxjnkG5PJxODBgzl8+DBlypTxdjhCiBzk9S3kPpUcZBCcZ61cuZL58+ezZMkSbDYbPXr08HZIQggvcacqqygiNm7cyLZt2/jpp5+8HUq+2LlzJ02aNCEiIoKIiAg6d+7M888/77rL5eLFi4wYMYKIiAh69+7NSy+9xLlz51yv3717N8888wwRERF06dKFRYsWub3vTz/9lIcffpg1a9Zku97y5cuZPHnyzb3BLCQkJDB06FB69+5N//79uXjxYo6veeONN3jssccyLIuIiODIkSOux4mJiRnKsS9dupQnnniCiIgIevbsyc6dO2865uPHj/PII49k+twvv/xCt27d6NmzJzNmzACMbssxY8bQo0cPIiIiOH5cbrzMLZ9qOcj9SnnL4XC4JuEBGDt2LEOGDKFGjRr5HssrX0fz+Z7c/wGnjbvITNd6tzPpkYbZvr5x48ZMnTrV9fill17iu+++o23btgwZMoQ+ffrQunVrALZt28bAgQOJiori1KlTjBs3jtmzZ1OmTBkSEhJ48sknCQsLo0WLFlntzmXdunVMmzYNpVQu3m3eWLx4MTVr1mTo0KGsXr2amTNnMmrUqCzXv3btGtHR0dSsWZOdO3dy33335biP1atXs3XrVubNm4fNZuPkyZP861//YsWKFZQqVSpX8X755ZcsWLAgyyT2+uuv8/777xMWFsaAAQOIiYnh8OHDJCUlsXTpUn755RfeeecdPvxQpqXJDR9LDiIvjRw5ko8//pivvvqKf/7zn4SGhhIaGurtsLwmKSmJs2fPEhoayv79+ylevLgrMQA0bdqUKlWqsGvXLnbv3s1jjz3muh4TEBDAnDlzbrj76vfffycyMpLU1FRMJhOjRo1iz549xMTE8NprrzF16lTXhf6EhARGjhzJqVOn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UOdNp06dIj4+npSUFE6ePJlp3azrzZ49m0uXLvHpp59iNpvZu3cvzz77LN9++22uB6d+8sknrFy5ksDAwBueS05OZsKECXz++ecEBgbSq1cvWrVqxddff03NmjUZOnQoq1evZubMmYwaNSpX+70Vkhzy2KJFi3j11Vc5evQoEyZMAJDEUEAtX76cTZs2kZCQwLlz53jyySfZuHEjv/76K6+88gqtW7emWbNmbN26lT179vD2229jt9spX748kydPpn///pQqVYq//vqLjz/+mMjISH7//XdSU1N55plnaN8+YyHB2NhYXnvtNa5evcrZs2fp3bs37dq144knnmDNmjWYTCbGjh1LkyZNqFKlCuPGjQOgRIkSvP3228TExDB58mRsNhvdu3cnICCARYsWkZKSgslkYtKkSQQFBfHmm2+yf/9+ypQpwx9//MGHH36IxWJh9OjRJCYm4u/vz1tvvXVDMb70kpKSOHv2LKGhoezfv5/ixYu7EgNA06ZNqVKlCrt27WL37t089thjrtIgAQEBzJkzh6CgoAzbPHbsGKNGjSI5OZmAgACmTp3KpEmTaN++PS1atGDz5s2sWbOGd955h5YtW1KtWjXCw8PZtGkTX331FUFBQcyZMweLxULbtm1z9X6++OILHnzwQQICAvjss88YMWJEjp+PpUuXsnz5clftprp16/L5559nSAxxcXEMGpSxEMR99913Q4uzSpUqvP/++7zyyis37OfIkSNUqVLFVZiwYcOG7Nq1i+joaPr16wdAixYtmDlzZo4x5yVJDnkgKSnJVXK6d+/enDx5ksGDB3s7LJ/jzpl+C9Ujw+PMRsLWvO0f1LztH27tMy4ujrlz57J69WrmzZvHsmXL2LlzJwsWLMjwZThmzBjee+89wsPDiYqKctXN79ixI23atOHTTz+lVKlSTJ48mdjYWDp37kzjxo0z1PM5fvw4HTp04KGHHuLPP/90ddEopdi9ezf16tVj586dREZG0rt3b95++22qV69OVFQUs2fPpmnTpiQmJhIVFQXARx99xMcff0xgYCBjxoxh+/btlCxZksuXL/P5559z8eJFHnroIQAmTpxIREQE999/P9u3b2fy5MlMmTKF9DxdoXbixIkMGDCAFi1asHHjxhuKFKZ3+vRpli9fTsmSJbHZbKxbt47HHnuMVatWMXfuXN58880c308a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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ROCAUC(unfitted_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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NPnQhz7EkiVL+PrXv84555zDddddx/79+/n0pz/N9773PX784x/z6KOP4vF4+OY3v0ksNjgjYf369XzhC19gxYoVPPjgg2Sz2YFrv/71ryktLeW+++4jGo1yzTXXcM455wCwYsUK7rzzTu69914ee+wx2dsoEm688cZCmyCZhgQ8JVSE6jnSvYeWnkZqI3NRlEERcNXWkTl8GCsWQwuFxkjp5CgqcZiunHXWWaiqSkVFBeFwmL179zJnzhwAdu3axfPPP88f//hHAHp6emhqamLBggV4vU438ROf+MSQ9L761a/yk5/8hHvuuYdVq1YNWUK/d+9eVq9eDUAwGGTevHk0NTkbcPWfC1BTU0N7e/vUFloyaZSVldHZOfWHt0iKC587SFVoNke693C0Zx/JTGzIaml3/UxSe3aTaT06JeJQVAPS08+p5NA/T729vZ1oNEp5eTlqrps3d+5cbrzxRh544AHuu+8+rrrqKmbNmkVjY+PA6u5bb72Vo0ePDqT3m9/8hi996Uv84he/YOfOnWzatGng2rx58wbeR6NRdu3aRX19/akqqkQiOUVoqk5VuAGX5qEr3kJ02Gppz2ynATpVM5aKSxym6Vz89vZ23ve+93HzzTfzhS98AS1vm49bbrmFP/7xj1x//fV84AMfYMGCBZSVlfHBD36Q9773vbz73e9myZIlVFdXD9wjhOC6667jhhtuoKysbMiWzu9617vo6enh2muv5YYbbuAjH/kI5eXlp7S8Eonk1BDyllLiryadTdARPTTkmmfmLABS+xunJO+icitNT2lw3Er5rqGnn3564HVpaSn/8R//MeKea665hmuuuWZI2LPPPgvAunXrWLdu3ZBrX/va1wZe33XXXQSGLZl/4IEHBl5fe+21J1EKSaG47LLLCm2CZJrid0co8VfT1neQ9r5DZKvS6JqzdY47Jw7pg5M7S7Ofouo5TF95kEhOnoceeqjQJkimKbrmojxQC0BvsoNEZvBsafcMx52cPnxo1HsnnPeUpDpFTEev0vDWv0Ryolx77bVSICTHpDQwA0110ZfsJJWJEfKWAXlrHY4cmZJ8i6rnMA21QSKZMI8//nihTZBMY/yeECFvGYl0L33JroHw/p5Dpr0NawqOLigucZiOXQeJRCKZQgKeCGFfBQCdsWYs2wRA9XrRy8rJdrRj9o48UnSiTJlbSQih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2T05E3jvg9t/w97fvz2glzv+6vmc9sD13R5vbGzk29/+NnV1dUydOpUPPviA1atXc8MNN3DvvfcyZcoUVq1aRVVVFbfccgu//vWvee655zAMg8svv5wlS5Zw5513cvnll3PeeefxxhtvsGbNGpYtW8YLL7xAeXk5pmkyd+5cvvOd77S79g033EBpaSm1tbU88sgjfO9736O+vp7Dhw+zePFiLrzwQp5++mk8Hg/Tp08nFArx8MMP43K5mDBhAj/4wQ/weDwD6i+hf5SXl0sIDSFlAv5S6kJVNEVqcAUccWioh5KR/covq2oOVjyzqg4rV65EKcXKlSu58sore5y1vGPHDtasWcPKlSv5zW9+w8svv8zOnTu7TFtTU8MjjzxCeXk5q1atoqKignXr1nVKt2jRIsrLy9m7dy+f+cxnWLFiBU8++STl5eUEg0GuuuqqREiP73//+zz66KM89dRTBINBnn766QHzg5Aat912W7pNEHKA1oV/aporMf15AFgpjFbKqppDPBzt9thpD1zf41v+YPDJJ59w7rnnAjBnzpwuZxe3dqJv376dAwcOJN4Qa2tr2bNnT5dp9+7dS3V1NTfddBNg11D27t3Lpz71qXbpJ02aBMCoUaP45S9/yUsvvURhYSHRaHs/VVdXc/jwYW699VYAQqFQYh0JQRBygxH59tIB9U1HKM6zxSHeHOp3flklDtGmcLpNaIdSivXr13PRRRehtW4X0qOyspIpU6awZcsWgsEgkydPZurUqfz85z/HMAzKy8tRSrF27VoqKyuBtgWIjjvuOMaOHcuKFSvweDz84Q9/4JRTTul0fcMwAHtluNmzZ7N48WLefvttXn/99cTxeDxOSUkJZWVlPPbYYwQCAV555RXy8/OHwkWCIAwRRXn2Og714epEzSGVeQ5ZJQ6xpki6TWjHNddcw913383111/PuHHjEvuXLFnCfffdx7hx4xILAZ188smcddZZXHfddUQiEU499VSCwSDXXHMNd911F6tXr2bixImAHaVz6dKl3HDDDcRiMcaPH89ll13WrR0LFy7k/vvvZ82aNQQCAVwuF5FIhBkzZvDAAw8wZcoU7r77bm666SYsy6KgoKDdcqpCehnoZWSF4UlRnt230BiuwcyzlwqNh5r6nZ+RDXMH1q9fPxHYNeaomwkXzQIyL/BeOBzmsssuS6xbPZgM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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = DiscriminationThreshold(fitted_model)\n", + "oz.fit(X, y)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYcAAAETCAYAAADd6corAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAgAElEQVR4nOy9d5wcxZm4/3T35LBhNmuVU0lCCQkZAcYY2ZhogzkwRmcwnG3Mz/Zh7MOAjSO+wxgnHM7xfg74DE5gH3fYhzHBYBEOBMqilaVVWK02T57p8P2je2dng6SVtKvZEfV8PrszXV1d9XbPTL1V71v1lmLbNhKJRCKRFKOWWgCJRCKRjD+kcpBIJBLJEKRykEgkEskQpHKQSCQSyRCkcpBIJBLJEKRykEgkEskQPKUWQHJ8CCG+A7zFPZwH7ATS7vFZQAqo03W9fQzq/jmwQdf1rx/DNTcAV+m6ftkw5zYAH9N1/Zlh6rkAOATYgBfYDnxI1/U2IcQut8xXjlDvMuADuq7fPMy5dwA/AdqAt+i6nh6c50Tu63gQQtwNbNN1/YEj5Pk8sFbX9f8aYf5ngClAj5ukAX7gX490XakQQkwAfq/r+tmlluWNjFQOZYqu67f0vXcbyX8sbiSFECWQakz4VrESEkJ8A/g+cNUIrz8NmHiYc+8FfqLr+r+emIijh67rnx9BthXApmPID/ApXdd/33cghDgDWCWE+IOu6/Fjl3Ts0HV9PyAVQ4mRyuHU5ktCiOVADfA1Xdf/3e3pfgAIAz26rp8vhPgA8BEcM2MHTi/+dSHEm4Fv4vQ0beAruq4/7JZ9thDieaAB2ACs1HU9KYQ4F/gaEAJywGd1Xf/fYqGEEPOAn7p5XndlGSlPAvcNThRC3ATcApjAQeBjOCOpu4FKIcTPdF2/sSj/p4ArgLQQohL4jHuvb3PLeAn4hK7rcVf5vgQsBD6j6/ofRiKoEOI04Hs4z98GvtHXUxdC3InzOcSBZ4ErdF2fWjwqE0J8CXg3znPsAG4ArgTOAL4mhDCBy4vynwl8B+d55oDbdF1/6jDiTQeSQNaV553AZwEfzqjzNl3XXxBChIAfAsuBbvqV0g2Dnwvwf+79TsYZ5f1a1/V7hBAe4LvAm125dgA3ApnDpNe69xQRQniP8rn83D03GfiNruu3H/2TkYwE6XM4tdmh6/pSnAbmG+4PDZze9FtdxXAe8H7gXF3XT8dpeB9x830J+KZbxj/h9Fj7aAbeDszG6ZlfKYSoAX4PfFzX9YVuuf8phJg2SK5f4fTYFwLfxjF5HBUhRBC4Hnh6UPoK4HbgfF3XFwEPAn8E9gKfB54rVgwAuq5/DXgUZ2TyKZyGcQKwyP1TcZRcHxt0XZ97DIrB45b/Xfc+LwbuEUKcJYS4EKehXwYsBaLDXD8JuBVYpuv6GcBfgDN1Xf934BWckcAfivJ73Xu+W9f1+cCHgG8LIfp+418TQqwRQuwSQhzE+U68Tdf1nBBiFnAPcIn7HbgJeEQIEQY+h9OJnIPzeZ8+SNTi5/JL4Kfu9+VNwNuFEO/BMXO+FVjontuBo1AOl17M0T6XiK7r5+KMNP55mO+a5DiRyuHU5kH3dQ2OjbnCPV6n63qv+/5SYCbwvBBiDY5yiAkhYsBvgX8XQvwKpxH7TFHZf9R1PaXruokzcqgHzsSxf78EoOv6RmAVTgMAgKtAFgIPuHlWudcfjk+4jdoa4GWcHvSnB+W5CKfXeMgt8+c4ymvqkR7OIC4Gfqjrel7XdQunR3tx0fnnjqEscJRmQNf1R1yZ9gMPu7JeAvxO1/VuXddt4N+HuX4fsBZ4VQjxdWCNrut/PEJ9CwBT1/XH3PpW67q+wL0XcJTJYhyFtAc4pOv6a+65C4Am4En3Of8KsHC+F5cA/7+u65b7nfnFoHqfA3AVyXnAl90yXsTpzS8G1uP2+oUQXwYe1nX9+SOkF3O0z+W/3Pvdh+M7ih3hGUmOAakcTm3yAG4DBKC4r4miPBrwS13XF7uNxxIcs0WXrus/wml0ngAuBNa5JphC2S62W/Zw3ycVx8RQnLdYFgDjCPfwrT7ZdF2fr+v6R3VdTwzKM1y9yqB6j8bgMgbLPbjOYy2vuEyDgfdvDs7oNoTn4YwwOoBvCSG+fYT6DPqfLQBCiPnuCKa43EPANcAHhRBXu8ka8GTRc16MY0baMAJZE0VlKMDZg8q4R9f1bpxe/23u9b8RQnzicOmDyj/a51I8iaDveygZBaRykPwFuFYI0eQe34xj18f1KZzu9sRvAqqA6iOU9aJzmXiTe/1pODOqnunLoOt6J7Aa+KCbZwmOAjoRHgeuEULUuWXeiNOgbsNp3EaiJB4HbhZCeF1TzEdxlOLxogM5IcSVrkwTgH9wy3wM+IciRfsBhjbsi3Aa5826rn8F+BZOQ8ph7kkHbCHEBe71S4CnGOY3ruv6DuDfcBRO2M33DiHEHPfaS4B1QMCV9UYhhOr6H1YOltUtsxfn8/+kW0YVzqjxciHEZTjfqed1Xf8izqhx0eHSBxU92p+LZIRI5fAGR9f1x4GvAk8IIdbh/PivdEcbtwN3CyFew7Hzf0nX9V1HKKsduBr4rhBiPY5Z60Zd17cMynot8F43z+eAzSd4D0/gNJ5PCSE24vg6LnN73y8Ac4QQR/MV/CvQimOC24zT+H58hCJcJIRIFP3t1XU9j+Pw/rj7XP+K4w942nUS/wR4QQjxClCJ4wQuvqe1OGa9V9w8/wT09ar/G/i6EOL9RfmzOM7qL7hmnR/ifI65w8j8dbfOz7nmv5uAXwsh1gJfBt6l63oS+AqO43i9ew9tg2UtYiWw3P1cXwIe0nX9V8CfgY3ABvdezga+eIT0Yk7kc5GcAIoM2S2RnFzcaaRn67r+Hff4kzjO5mtKK9lQhBDvBXp1Xf+T23N/GPiLrus/KLFokjFGTmWVSE4+W4A73Om3No6D+KbSinRYNgA/EkLcgzPN9WngP0orkuRkIEcOEolEIhnCmPochBBnukv3B6e/UwjxshDiBSHEh8ZSBolEIpEcO2M2chBC3A5cByR1XV9elO7FcSwtw1mhuQrHeXjwcGWtXr3a7+Y/wDDT/iQSiUQyLBrOGpaXly5dmj2WC8fS57AdZ/bELwelz8VZKNUFIIT4O850x98doaxlHPsiJIlEIpE4nAv8/VguGDPloOv6w0KIqcOcqqA/OiQ4sWUqh8lXzAGAigYvmgcsG7qS++lKtmIPnXLtYh/haEQJIzw38KxtWxx5NHZsI7XhcmcyaQKB4DGVM3zhNql8nLyZOfGyJMfMgV2dNE0dHwt6vaqfkK8Cj+ZFUTRURUVVNFTFg6ZqaKqXymA90WCMsL+SiL8aRRm/6802bNjA/PnzSy1GSdnVmeDanz/NT94xDdw29FgoxWylXgbGkoniBPQ6EibA5DqB3+93kwaHYBkfmJaBaRlHUFonzto1a1i0cPER89i2BdhYRzEb2rZNzsyAm28kyra/SHuQIhz+2gFl2gPPDH/l0HIOp3B1XUeI2Ucu4zDVG2Z22PRjuY/+tKHXZI00OSONZZnYWO5n0vf8bLSeHcxqnl641rL78wzGsk3Subhbn10ooyCObdPT00NlZQV9n2Rf+gD57b53zmdn2SaGlSeTT9KZ2Ytlj8xq69MCeDU/qqo57z0B94yCzxMgFp5AyBfFo3qJBmJEAjX4vSFU5eQtrepvK96YtMQ7aUsXgg8cszm+FMphMzDLjd2TwDEpjXhfgPGOpnrQ1LF9rJriw+8ZhZGDS7gQcqn82Kt10FBZnrHW3r4kRmdn54jz27Z9xE7Hq6tfZcm8JX2ZsQ6jaPpV2kBlZ2FhWxaGZZAzkmSNFKlsnKyRJm9liKc7yJkZ0rleet33lmESt4bew56OjUVHCtFANUFfBVF/jOpwI9FADT5vAL8WxKsFCHjDeD1v7MZ8tNnefmKR2E+achBCrMSJoPhjd9HP4zizpX7qBs2SDGJwb9m2LOx8HiuZIN/ejpXPYefz2Pk8GAa2kcfK5TB7e7EzGWzLcrqplvOjx3J7poPSsO3+95aFmUphJZNYqSRWOu1eZzvXFf05PdFBabYNpomVyWClUtiW6Zwzzf7RiW1jGyYUri/c4YA8hSFK8XNw061sBiudwUomWOcPFOUbdI09qMzicqD//ovrwwZFHXBt8XV2cb5CHYPKPWw+VxbLeX0lFnLzKE5UoD5TjaKgoDjHfekFM47imHQK+Z1ztmGwxutxzT1K/zWFyxQUVUPx+VD9ftRAEDQNbBtF01C8XtA0573Hg+L1oqgaPp8PfyhI2O9FqapEmzYZu2km+aAGQT+m11EsKAqoCvmQl5QnR97IYNoGnYn9JDLdxDOdtLHL2bqpCFXRCPurHLOW6kVVNSL+aqrCjdSEm4kGq1EVDwoKHu1YwmUdH86IykZBcR/h8ZvPktk8NmBaNpqqOH+KQjJnsKc7STpvkswaaKqCR1XxeVQ8qoJPU/F5NCzLpiOVLVgAPKqKadlE/B5CPg8Bj4ZhWrSnslQGvET8XmrDftqTWVbtbDuh5zCmysENtbDcff9gUfp/44QAOCZ6n3kKr1X6yUq2bWOlkpi9ccx4L2a8F6u3FyMedxrlfB4rm8XOZrANA9u0wDKdBtg0nQbTtNxzBrZhuI27gW2azqthOI2+afbnsfp7gmtKeP/jAkVB8flBUchrKoUWUKHovVL0UnzefaMcPk0ZLl9R3YOvUVRl4HkGNSrKYJkUaOvB1zzJzVCsxAYeD1ag9jCKD8DM5VA9HveUqyj79KV7bOUN7EQCO5fDzueGV8CjgM/vxx8KYns9VAaDKNWVWGE/ZjSAUeH+hXwYQQ9GQCPt205vwIvt82B7NfbXhLF8HrBt1GAInzdI0BuloWIaddFJ+L0hvJqfoC+CaYVJGxbpvIlh2agKtMRzVLb3Fhp4VVFQ3WefMUzyptPh6c3k6c7kyRlm4fPKmRaGOXDU5Xzk/cpCdY8B8qaFqjppHlVFU526MnmTrDG0vUpk83SncyRyBsmswfaOOC/ubse2bSJ+LxG/h4jPeY36vbQlMrzS0oFhWWiqQl04gOoqme50ju50jolVIZY013DmlFpiIR+GZbOnO3lCn2FZrZA2k0lU40gBPEcX27bJbt9Kas1r5FoPkD/YihWPYyYTTk94lFA8XvC4vbWiXpsaDBbS0DzOe49GOpsjVFHh5NWcNDQNRdVAVVFDIVSvF1S10HtUFNXtaar9jVlfD1NVQFELvVHF40Xx+1H9ARSfD9RBvdgBDavS3wgWeqw4svoDKKprYx5yXf+Py4nnqhXuDxRs0xzUW2ZgPZqGoijs2LGdadNnOPX0nVeHsWtbllMmoGgaaiTsPK9hP5AR9BQHjyqK6lSGq38Ybn3wISZdcw2GZQ0v8zGydctWps6e5dyX5t6bZWFlhtn9tHiwY5pY2QykU47SME2wLRTLHVEaeaxMBjuTwezuIn9gP2ZHh9ORsSynV1v0ZyUTWL092JkMZHNYnd2wuwVwTAU+92+kWF4NoyZCvi5Kb22U9pooyUgNbVoDh5QYbbEJ6NGJpJWA8/0GkskkoTVdhy0zY5h0pnL0ZpzQU4rifBv7FIiqKEWDNqWgHABypoltg0dT8KoqHk3F6/b8vZrT83deVRQF2hIZAh6NoFfjQG+avT1DQ1MFvc75fT0pcuZQc2Bt2E9lwEfesjgYT5MzLGygOuhjek2U3V0J/qu7hUc3tgwwPE6OHr+prqyUw1hiGwZmIo7R2YnRdpBc6wGSL71A/mBrIY8aCqFFK/DU1aNFI6iRKFokihaNooYjTqPs97sNqx/F53eH5iqoauFHq6hKoTFEVfsbPVV1lILX09+wAygKqms6Ufx+dH0LU+aIwrHiOczH2Gc6su1C46N4vGiRyGEbI7VPZr8fRVXJmxapnKOQ7XwOO3e4OG7uj8gfIGtDzqLQiFu2Tb5o1GPb0JXKYlqDGtfDNMqOrb2v7XHeW7ZN3rRYTzXbp0x1flAFJzFYxQcUdZ7do763pmljFRy8A+srutxtTPuOFGyFATn6Ovd508LIW4VTbYkMrfE0hmljWBaG1T9JwF58FS/qplvuwOcz8P4HS12c1i9HPB4jsjE3KL3/nganD6wngG0HwLb60y13xGs73kxbNZ3dEmI4ysP9XimuIgEwLIusaWGh9JsjLZNQOoE/myaQTePPpghnU/jyWfxGFn8ug8/I4c9n8Zp51LxBdbIbxTKxUajMJKjv7aKmdR/hojso9jQZPi97qxpYE5vB+saZ9Maa2VTRQNIf4nCEfBpVgX41ZbmfsV30atm4MxApfG4+j4qCQt60yOTzGJaNYVrkrcFP3UFR+p+1osCCpirqIwHCPk/BDLR4QjU+j6PQc4ZJImeQyBr0ZHJoqsKc+sqC4rJcs5dp2fg0p/1I5gxW7+3gt2t205PJFepbPrX2sPd/NMoifMbq1aunAjubd+/EOwojh9zeFlLr15LWN2MlElipFPlDbUNGA4rXS3jpMiLLz8bbOAFvff2Qspzergc8HggE3AbSj+L1ofi8RXm8qIEA+APYPj+224tWA4H+wtzesGU7DYllO42NZdv0ZPJk8o58mzZtYt68ecd0zzY2XakclmsLzxoWqbxBKmeSMQxMy8YwbfK2TSprkDVMTNv5sZuW80U0bRvT7SmaFoWyLBvylkV3Okc6b7rpFF5td9aUbUPWsOhIZcmZJjnDImdaWFZf41+kBAo/0lOX6K8/R/y9Xy61GGNOv8tEKYRk6DPLFLtTVNtJD2gKQU3Bryn4FZsAJn4MvEaOWKKdqfFWmox2GtVOgrkkwUNdBA504t/biZod2D7kaurpOuftJE5bQnLWPKyaeuxQCK+qEvAeZuR4nNhug523LOe35P52YiE/lm2TyhmoqkLUP9Bv4tFUNEXB73H8CdlhzFq4zynk9ZDKG05/z20rirFsG8uyOZjI8NimvXxo2TSEFgeYtnTp0l3Hcj9lpRxSr6zBzh6+53pYLAu1ZRfKvj1oG9ai7dpWOGVrmtNg19RhVVZDRSVWbT12VTXWlOlQHcMOhrFjtdiRCKY/SN4fdD4pj+PAs3HsiOZheg4jwXZ7wlnDImOYjqMqZ5DOG+RNu7/hBNra2vBFK8m4PVS7qPfalsiwoyOOOaiB7ns1bItM3uQERD1hwq4jzas5jjfNHX4XhvD0m5CKG5bCe9f261EVzGyGiqgzM3qAib+ojIFp/UdFfloGpw7nLjjs+UI+pWBW6KMy4KOpIohXU9zGsd9f8fX3nMftv/3bgEIK5Q8qvLiOwfX2vdm3dy8TJ046/LWDEpRh6xk+j6aqFLtV7MK/PreG6zBVVPwetd8sM8prIZz2KgdmFs1IEvTkUcwkdqaVlNkCLbsI6gcI7OwgsrMH79YWlLy7L5XPjzp5Ktq0Gfjmnob37HPRps2AugYs19Fr27idoP7eeV/HxXI7QoVzbkPsd3v8pu2MIHKmhWFZeFSVqN+LqkBdJICiON99r6pSHfK59+Moh8FYlk0q74we+nwm1UEfHk0dIMu+nhTtySwHelMkskM7ztV+lbpMOxyHcigrs1I8a2Bm8kfPaNuo8R7821/Hv3Uz3padaMn+jbyyU2eRPv1N5KbOwooOnMZpBUNYFdWY0ShmTT1WdJj1eX2tdM7Etg3ylk3WMNnX7dgLbZyhYY8raypn0J3JOb1zdwiayBn0ZvJ0JLP9DrJjehqHt6eGfBoBj4aqKAXnmILjMFMVhWCFRtDrKdg5/R6tMItCVRRU1cnXd1w8y0JTlYIzrK8B6MtTFfQR9npc51+R466vXAU0VSHoHb2v3c6dO5k2bdrRM45TpsQio1ZWNuilqWLkU5z7FNnhvnfF/caKgHegcnDPqapSMFn1mT0c23t/5mKTXl/eoNczrGWzX2X3y9iXYgN+j4rftd9XB50Gtid9yF0Qu4S46OTAki0c6N0FCoQPZmj8207Cm1ph9z7M7Vuwtunkn/gTyg+/TfC0BYROX0rd5VcQffN5qONkbYSqKq5zeujsLMX9bWrA1FiEqbEIth1j3YEuNrX2DMjrVY9fOZeVcjgaWncngXWrCa57BU9n/3w5M1pJ6vQzyU2dSW7SNKwqZ1WqFY5g1DViVlRh+/xYkQrHPHQYDMtiR3uCHZ1xOlM5dnTE2dmRIDvMEHAkeFWFmrCfmrDfnbqmEvBohS9/yOch5NWcH0hfzxno6OhganMjYZ9n4KQZnJ74tJpI4YdaasI+T6FnNRwnKmZ3QGNKdfgwZTtKS2GgecqjKoS8ngF1a6qCz3XijkQkTVUKZgnHJHDsJoofLlzIuxdMLhyf6MLJV/PtLFkwCY86cPRyOMbzCudjoSpUT8RfxaG44/SOTliOkq7CDsc5yE62v2cuHns+E7pDNOzOQctBrC07MDZsJvXaalKvrab95/9B1WWXU/+BmwjOmYunrh7Vdyxu89KiKAqLJsSYURPllZYODvSm8WoqgSO0Z0ej/JWDaRLY+BqB19cTeH09ALbHQ0bMJzdpKtm5izCrawBnVGDWNmKFwljhiJN+lB9IIpvn7zvbWLOvi81tPQNMRwrQXBmiOuTDq6k0RAJE/F4UnJkMVUG/MzvDo1IT8uNxe2oe194Z9h3f49+5M8O0yY6jqXianqJAYJhGqm8O9dHwqIpzL+586z6CXg++YYa+R8PvUakIjO0PbHXiAEunDfUFlQN/e+aZUS0v4FFHdVRWTng0Hw2V00hkOknlevEoAaY1zmFa3SJaOjbR2rODPdVxuhpjVJ+9jFjyHEIHE3h2H4SN2+l98i90P/oHep96gvAZZxKcdxqRZW8ifMaZ+KdOLRtFGvF7ecv0BrKmiaoomLkcm3uPOXIGUObKQevqoPIPv8K3dxcA+cZmUsveTGbuQuxAEBQVK1pBZt5irFD4mKcLvrDrEL94ZXvBljelOsyMmijzGiuJBf1MrA4ROoEfY8CrUeH3FvRT39Q3cBxP9ZFAwewDFBr/16wOli6eCjjDT0l5cuutt3L//feXWoxTBlVRqQjWUhGsZY/WRsRfTTLbw/T6xUyqmYt+4EU6kweI08keRWHi3NlMmL4YT20lTee+mfgTf6X3ySeIP/Mk8WeepG8JmW/iJEKLTid63vlUXXQp/mnTxrWyUFWFoBulIWse/xTpslUOntZ9xH72XdR8jvS8xSTPPh+jqRk0L0Zdg2M+qqw+LrtFPJPn9+t28+TWVvwelasWTmZRc4xpI7QP9/Xm+4j6vTREA1QGfFQGvFQFfa5t//i+YCdyrWT88MADD0jlMEZoipf6iimYlkF3qo2edBvzJ55HzsjQnTrIjkNr2Nulc1ALsPDcc0i25wgELqTqne8it2Mn2T27MA61kW3ZQ3b7drofe5Tuxx6l5dO3ERRzCC87k8oLL6HirSvwVJRv+JkjUZ7KwbapfPTXqPkcPZddTfr05aAoGLX1ZOcuxh6hU6k9kWFfb5rW3jSt8TS9mTz7e1Ps7U5hAxMqgnz8LXNprhx+rrTPo1Id9OHTHIduU0WQioCXqoBPNt4SyThAUz3URCYQCVTTnToIdFNfMYWaSDP7unR2ta9nc/vLLJy0AhpiGAc6CQY8VMyaVXCO24ZBdsd2UhvWkVq/jvSmjaQ3baT9Fz8ltPh0Gj76cSovuAhv7fGvKRiPlKVyCKxfjbd1H5k5C0gvOQvb6yO9aFnB0Xw0DvSm+fVrO1m9d2jAML+mIuorWDqxhgtmNxWmmQW9GrGQn1jIR8idjtZUERx2GppEIhlf+D1BGiqmYph5DvbuJGukmFxzGoaZY2+Xzqu7H0c0nkn1jGZSM5pRO+PEUgGMnbsBCMwWBGYLYldejdHTQ3bbFnqefILUmtfY+eF/InLmWTR+4lNUXXTJiFfIj3fKSjlUP/ADlAP70HqcaZyppWdj+wPOa2j4GSvFZA2T363dzRP6AUzbZlZtlIUTqmmMBmmMBqkMeqkIeAvOW6+mMqkqRGM0OKpTDiUSgI0bNx49k2RU8WheJlTNpDfTQUdiP9PqFuPR/OxuX8/6vc8wuWYeU2sXYsWi9NYFqZ97EUpPEiubwejqxIrHQVHwLF1GaMkZJF5cRc+fHyPxwip2bFhPw0dvof6DH8bX2FTqWz1hyko5KNkMGAbZmXNIvulccjPnkl6w9KiKwbAsntl2kD+/vo+D8Qz1kQDvPX0qyybVDOtYqgh4mVQVZnpNZNh5xhLJaLB27Vqamsq/ESk3FEWlMliHV/XTFt/N5Jp5VIcb2bz/efZ0bMK2babUzidnptkX30HAHyYYjRKduABN9WAmk2R3bCN3YD/Rs95M5Myz6f3rX+j8/W84cO+/knj+7zT+8yeoeOsK1ODohdY/2ZSVcuj80CcwM/0btOSmTD+iKSmVM3hpTzt/3XKA3V1JNFXhHaKJa0+fhneQOUhVFKbVRJhcFaYhGhjXsxEkpwYrV648pv0cJKNLyF9BkzaTgz07iAZiLJh4HmtbnqKlczNdyVYm1cylOtSIjUU6H6c71Up1qImKUC2hBYsILViE0dFBav1aKt9xEcHT5tP2kx8Qf/YZkq++QsPHPs6E2z49MEROGVFWymEwZqxu2HTbttnY2sNPXtxKR8pRJudOr+eaxVOpCvbPu1cVhUlVISZWhamPBEY91opEIhnf+D1BJlTPprV7O/hg2bRL2NL6Cofiu9m8/3kUVBoqpzIxNoeQr4KO5D66Uq1EAzFqIs14amqInnc+ma1bUDSN5s9+ie4//w/djz3Kga/eg9nVxcQv/htaNHp0YcYZ5ascFBWzsnpAkmXbPLm1lf/ZuJeOVBZFgcvmTeQtM+qZUDFwxpFXU3nbrEaqQ+NjubxEIikNHtVLY9UMWru3kwPmTjiLKdl5HOjZTmfiAK09O2jt2UFNZCIz6hcT8EboSR9CQaEiVIdH9RKcLfDW1tH79JNUv/MKgvPm0/rtr9P2o+9jJhJM+dr9aGU25bVslYNZFeuPWY+zkvkHz29h7f4uAh6N5VNquXhuMzNqBmrsSVVhKgJe6U+QlJxvfvObpRZB4uJRvUyomkVHch/xTCchfyUz6pcwvc6iPbGPvZ2v05HYS2/6EJNic3XdPewAACAASURBVImFJ7ibBbUTC08gGqjBE4tRecGF9D73NwIzZjLh05/nwNfvpeNXD2AbJs2f+Rz+6TPKxmRdtsrBqO0PmZDKG3z7udfZfLCHBU1VfPis2QPMR82VIWrDfhqiQWrCcqQgGR/ccMMNpRZBUoSqatRFJxP0RmlP7MWyTRRFpS46idrIRPZ3b2F72xp2HHL+Iv5qptefjmVb5MwMtZGJaBUVVJz/NhIvrgJgwh13sf+r/0rnb36F4tGo/8BNRN60vMR3OjLKVjn0+Rs2H+zhu39/nd5MnsUTqvnkW+cNWJ1cEfBy1tS6IQ5oiaTUxGIx6ZAeh0QC1YT8laSyPbTF9wA2iqLQXC2oi07mULyF7tRBOhL7WN/yNDPql2DbFmFfJUFfFC0UouL8t5PesA6ACbffxf57v0zHr3+Ff/oMPLEaAjNnlfYmR0BZKgfb48GKROlIZvn2c5tJ502uXDCZS+Y1F4LQNVUEmV4ToSEalIpBIpEcE6qiEglUo6oah+ItmJYTft/nCdJcPZvm6tl0JVvZsO9ZtrWtJp2P41F9NFcLPJoXRVEILViEt76R+PN/p/5D/x8Hvn4v7b/4KYGp0/E2NI57J3VZKgcrWkXOtPjKk+tJZA2uP2M67xATAPB7NM6cUnvYkBcSiUQyUkK+CqbUnIZlm6SycTL5BL2ZDsCmOtzI0qkXsWHvs+zr2kLYX0XQF6W+Ykrhem9DA9Gz3wyWReScc0mseo6O3z6Et76e6FtXjGv/Q1kqB7Oqmmd3tNEaz/C2WY1cMNtZSBT0alwwu4mwdDRLyoALL7yw1CJIRoiqaEQCVYW//d3bAZuQr4LTms9l7Z4n2dL6Mj5PEBub+uiUQsPvbWgg+uZzncVz27bS85c/E1l+Nr5Jk8e1eaks7S1WKMLT21rRVIV3L5iM4m62ctbUOqkYJGXDQw89VGoRJMdBwBuhNjIRv8exToT9lcyf+BYURWHLgZfoTh2ktWc7htW/a6W3oZHw6adT8773g21z8PvfIf73ZzHj8VLdxlEpS+WQ8vrZ051kRk20MCtpxaxGGqLlu1Rd8sbj2muvLbUIkuOkIljDhKqZRPzV7nEtU2rmkzMzbNj7LIlstxsFtp/gvPlEzz6HqssuxzjURvt//oLk6pdLIf6IKEvlsC3lbAQ+s9Zx6EyviQyYuiqRlAOPP/54qUWQnACKolJfMYVowAnhMyk2l/qKKcQzHaxreYqO+F7yZn+4H0XTiCw/h9g/vAf/1GkkXnye+AvPk23ZU6pbOCJlpxzyqDyxqwOAWbVR/B6NJRNrSiyVRCJ5o1IdakJVNBRFYXbjmdRFJxPPdLKzfR2H4i3Ydv8e857KSkILFhJ77z8C0PHQL0m88n8YHR2lEv+wlJVy+EJnhJvbK3i5pYOZtVEWNceYVReVU1UlEknJ8GheaiOTAGcK7Jym5QS8EQ727qIr2UpPun1A/sBs4exP/ablZHdsp+fPfyK+6jnMVKoU4h+WsmpV47ZCHoWzp9bx6bfNJ+TzMLuuvOKVSCR9yAVwpw6RQBUBr7Pni6KoTK1dgG1bbD34Ml3JAxhmv3NaUVUqVlxA/c0fQQ1H6Hz4N2T37qX3r38hf+hQqW5hCGWlHL5ZE+c/lsb4yDkCv0djSnUYv0dGUpWUJz//+c9LLYJkFKmLTkJTPO77ycTCE+hOHWRX+3ra4ruxbbuQV/V6qbrgYmre+4/Y2SyH/uOHGD3dJF56AduyDlfFSaWslAOAVVFVeF8tndCSMuaTn/xkqUWQjCJezU9ztSDojaIoCnOaluPTArR0bqYnfYjeQeYlT3U1sfeuJLR0GZktrzsRXJNJcntbSnQHAyk75WAWKYeoXNMgkUjGER7NS33FFDTFg0fzMaV2AZZtsqt9HZ3JA+SMzID8kSVn0PjPnyA49zRSa14l/vdnSW/eOGCUUSrKSzmoGnawPyxGRUAqB4lEMr7QVA+xiBPOp7FyGiFfJQd7dhLPdHCwd9eAvKrfT2DGDOo+cBOKz0/n739D7sABcrt3DS34JFNWysHy9ZuRNFUhKHduk5QxDz74YKlFkIwR0UCMsL8KRVGZXr8YgF3t68mbGdK5gauiAzNn421opOqyd2HFe+l+9I+k1q8rue+hrJSD7evfi6Ei4B3XQaskkqOxaNGiUosgGUNqIxNRFY1YuIloIEZncj+ZfJJ4pmtAPi0aJXr2m6m86BK0WA29zzxJvq2N7M4dJZLcobyUg7d/5CBXREvKndNOO63UIkjGEE31UBl0NiVrqpoJwIHu7aRzvUN8Ct6GBsLzF1D1jouxczl6n3qC9MYNWPn8kHJPFmOmHIQQqhDih0KIF4QQzwghZg46/y9CiNVCiJeFEO8eSZnFI4eqgFQOEolkfFMZqsOj+qiLTsajemnt2UHeypHODw24F5gzj8qLLkYNh+l58i8YPT2kN64vgdQOYzlyuAII6Lp+FnAn8I2+E0KIKuDjwFnAO4D7R1Kg7ZMjB4lEUj6oikpVqAFN9dBQOY28maEjsY+e1NDFbqrPR+WKC6hYcQFWIkH878+S2bq1ZCunx1I5vBn4XwBd118Ezig6lwR2A2H3b0Sel2LlIGcqScqd66+/vtQiSE4C0UA1AW+Ypso+09JW0vk4eTM3JK8nFqP+gzeheL30PP4nbMMgu2P7yRbZkWUMy64AeoqOTSGER9d1wz1uATYBGvCVkRS4v62dnpSBqsAmq+MN7ZBevXp1qUUYN5Trs7juuutGXfZyfRZjwXh6FoadJWF24FeidKfa2LJ9Ewd2dRJQK4fktdGwFy7GWP0yOx/7b5S2g5DJnvT2biyVQy9QvEmqWqQYLgaagGnu8eNCiFW6rv/fkQpsam4mFqunIuDljHkTR1/iMmH16tUsXbq01GKMC8r5WZx//vk8/fTTo1ZeOT+L0Wa8PQvbttnTsZFwj8brB55HjaSZ1NTApNi8YRv9Q3t3s2vNq3iee5qJF15E5cRmfI1Nx1xvNptlw4YNxyXzWJqVVgGXAAghlgPFnpUuIA1kdV3PAN1A1ZASBuM+xIhcGS05BVi7dm2pRZCcJBRFoSJYS23U2UGutWcHqVycZLZn2PwVb7uAqgsvwejoIP7sM6Q3bzzJEo+tcvgDkBFCPA98C/iEEOKTQoh36br+HPAy8KIQ4gVgC/DEUUvsUw6+stz6WiKRvIGpCjXg94SYGJuDZZu09uygK7l/wH4PffiaJ1J9+ZUoHi89Tz5B/mAb+ba2kyrvmLWyuq5bwM2Dkl8vOv8F4AvHVKirHMJSOUhOARobG0stguQkoigKlcFaGiqmsvPQWlp7djApNpd0PkHIVzEkb2jRIsJnLiex6jnSG9bjnzwZb339SZO3vBbBua8BGTZDcgqwadOmUosgOcmE/VV4ND+1kYlk8gkSmU6S2e5h8wZmzKLygosA6Pqf/yK3f/9JDalRVsoBnJGD31NmYkskw3DvvfeWWgTJSUZTPQS9EeoqJgPQFt9DPNM1rO9BDQapfPsFhJacQXbbFuLP/Q2zq2tIvrGivFpZ16kvN/iRnArcd999pRZBUgLC/iqqw414VC+H4nuwbYuu5IFh8wbnzaf2+hsBiP/9b+QPnTy/Q5kpB3fkIPeMlkgkZUrYX4mqeKiJTiRnpOlJHyI3TLRWcFZNV5xzLgExh4z+OomXXzppcpZXK+sqB+lzkEgk5YqmeogGYtRHpwBwKL4HgN5Mx7D5/dOmEzn7XAB6/voXjJO093h5KQcUNFVBU8tMbIlkGJ566qlSiyApETXhCcQizXi1AIfie7Bsi2S2Z1jntBoIUHXRJSgeL8mXXiDfPjQu01hQXq2sokh/g0QiKXtUVaMmPIH6iikYZo623l2ATXdqeJ9C9KxzCMyaTW5vC9k9u06OjCelllHCVuRMJcmpw4oVK0otgqSERAJVTIrNRVFUdndsxLItskYayzKH5PXEYgTc/T+S/3dy/A7l1dIqCgE5cpBIJKcAqqJRE5lAU+UMsvkkB3t2AjYZIzls/vASJ7B1euMGrNzQiK6jLt+Y1zCqKHJ1tEQiOWWI+KuZVDMPVdHY07ERyzIPG28petabAchs33pS1juUl3JQZNA9yanD7bffXmoRJCUm4I0Q8kaZUDWLrJHiYO8uktlurGHiLQVmzMRT30B2x3byncPPbBpNyks5oMige5JThjvvvLPUIkhKjKIohP2VNFfPBhT2d2/FtAw6E/uH5NWqqwnMFlipFOn1Yx/Rt7yUg6IQlTvASU4R5s2bV2oRJOOAkL8KvzdEbaSZZLabeKaDeKZzSLRWRVEIL14CQGL1y2MuV3kpB+lzkJxCtLa2lloEyTgg5Is624hWOduI7u/eho1FJj907+jouecBkN6wAdswhpwfTcpKOaiagleGzpBIJKcYddHJxMIT8HtCdCT2YdkmqdxQx3TFW96K4vOT3b4Vo3v4aK6jRVm1tD45jVVyCrFo0aJSiyAZJ3g1PxXBWmoiEzGtPD2pQ/SmOzCs/IB8WjRKYNYscvv2ktu/d0xlKivl4JXKQXIKMZr7R0vKn2ggRm20GYD2xF5sLLqSQ02PoQWLwLZJvfbqmMpTVsrBp0rlIDl1uPXWW0stgmQc4fMEqItOwaP66Ejsw7Zt4plOLHvgiunwm5YDkFy3ZkzlKSvvrtc7VJcZhoF1EndHGi/kRnGFpKqqeDxl9VU4JXjggQe4//77Sy2GZBxRGawlFplAW+8uEplOosEasvk0QV+kkCd6jrsYbvPmMZWlvEYOgxqweDw+qo1kuTBjxoxRLS+XyxGPD40lL5FITi6RQDV1UWeXuPbEPgCyg8JpBMVcFJ+P7K4dmKmhM5pGi7LqLmpFM5UMw0DTNEKhUAklKg35fB6fzzdq5fl8PlKpFIZhyBGERFJCVEVjcmwum/evoiOxl2l1C8nkByoHxeMhMHsO6c0bMdoPoU2eMjayjEmpY0RxuG7LsmRDNopomvaGNM+Vko0bN5ZaBMk4pCJYS3W4kVSul1Sul0w+iW3bA/KEFi0G0yQ5hk7pslIOPhmue8xQ3F32JCePtWvHPgSCpPwIeMPURiYB0B7fi2Wb5M3sgDzhM94EQGqNVA4AeOUOcJJTiJUrV5ZaBMk4RFFUJsXmoSke9nVtwbTyZPKJAXmCYg7AmG78U1atraae2r3bj33sY4c99+Mf/5h169adRGkkEkmpqA430BybTd7M0Na7e8j2od7GJgCMtuF3jhsNysporx7B9HH7f6/m92t3j2p9Vy2awn3vXDqqZR6J733ve4c9d9NNN500OSQSSWkJ+iI0Vc5kT8cmDvbupqlqFoaVx6M6gUd9rnLId7RjWxbKGFhVyko5jEceeeQR/vrXv5JMJunq6uKjH/0o3/3ud5k6dSper5e7776bu+66iy53c47PfvazCCH43e9+x0MPPYRlWaxYsYJbbrmFc845h1WrVvGrX/2KP/7xj6iqyoIFC/jsZz/LnXfeySWXXMJZZ53FXXfdRWtrK6ZpcuONN3LJJZdw3XXXMWfOHLZu3UoikeDb3/42zc3NJX46kiPxzW9+s9QiSMYpqqJRGaqnMlhHT/oQOSNNKttDRbDWOR+NogSDmN3dWKkUWiRylBKPnbJSDkfymd73zqUntZdfTDqd5mc/+xmdnZ1cffXVmKbJRz7yEebNm8fXvvY1li9fzsqVK9m1axef/vSn+d73vsdPfvITHn30Ufx+P9/4xjdIJvunqz3yyCN84QtfYOHChTz44IMYRdEXf/Ob31BdXc39999PIpHgyiuvZPlyZ8XkwoULueuuu/jWt77FY489Jkcb45wbbrih1CJIxjEhX5RYeAI96UN0p1qpCNYWlIOiKHjrGzC7urCSyTFRDmXlcxivHodly5ahqiq1tbVUVFTQ2dnJtGnTANiyZQsPP/ww1113HZ/73Ofo6emhpaWFWbNmEQgEUBSF2267jXA4XCjvK1/5Cg8++CDve9/72L9//4BpbNu3b2fJEiemeyQSYcaMGbS0tAD9+wM0NjaSzQ6c3SAZf8RisVKLIBnHhH1VVIcbAehMtpIz0gPO+5omYMZ7MXqH31b0RCkv5TBOp1v2zVdvb28nkUhQU1OD6toAp0+fzg033MAvf/lL7r//ft71rncxefJkduzYUVjdfcstt3Dw4MFCeb/97W/50pe+xH/+53+yefNmXnvttcK5GTNmFI4TiQRbtmxh4sSJJ+tWJRLJScLr8VMdanLCeMf3kjcz5M3+iBDepglg2+T2toxJ/eWlHEotwGFob2/n/e9/PzfddBNf+MIX0LT+xXo333wzf/7zn7nuuuv44Ac/yKxZs4jFYnzoQx/ife97H9dccw3z5s2joaGhcI0QgpUrV3L99dcTi8UGhHZ+z3veQ09PD9deey3XX389H/vYx6ipqTmp9yuRSE4OkUA1jZXTMW2DQ/GWAaMH3yRnLURm29YxqbusfA7jVT0sW7aM2267rXD81FNPFd5XV1fz/e9/f8g1V155JVdeeeWAtFWrVgFw9dVXc/XVVw84d++99xbe33333QPMUAC//OUvC++vvfba47gLycnmwgsvLLUIknFOxF9FXXQyuzs20OWalsL+SgCCcxwzcub1TWNSd3mNHManbpBIjouHHnqo1CJIxjlej59YeAI+LUB36iCpbG/hXHCuqxx2bMceg9A35aUcSi3AMFx55ZUDRg0SyUiRIzzJSKgM1VEVaiBvZulMHSjsDtc3csjv3YuVSBypiOOirJSDRHIq8fjjj5daBEkZEPRFqXJnLXWnDpLNO2G6tWgUb9MEcvv3jsmMpbJSDuN1tpJEIpGMFaqi0lg5HXCUQ3EQvoCYg9nTQ3b36EaHgDF0SAshVOD7wCIgC3xQ1/VtRecvBr6AYy1aDXxU13V7uLL6kKpBIpG8EamLTsLvCdGbbidnZArpofkLiT/zFOkN66hwd4gbLcZy5HAFENB1/SzgTuAbfSeEEFHga8Bluq6fCewCao9WoBw4SE4lOjs7Sy2CpEwI+SqIBKrJm1mS2a7+9AULAUiPwYylsZzK+mbgfwF0XX9RCHFG0bmzgfXAN4QQ04H/0HX90NEK3LRpEyFv/xqCGTNmkM/nR1fqEnLppZfyyCOPcM899/COd7yDc84557B5i8NtjAb5fJ7t27ePapkni9WrV5dahOPiscce49JLLx3VMsv1WYwFp9qzMLJOX37T9jV07nFC6thu//7Qmtdof+kllFHcAG0slUMFUOwlMYUQHl3XDZxRwvnAYiABPCeEeEHX9S1HKnD+aadRHXXm9/etLu7bLvPlnX9iV/vohrSeWruQZdMuGdUyj4SqqoTDYTweD4FAYMhahj6SyeRhzx0vuVyOBQsWjOr2oyeD1atXs3RpaWJqnSgXXHABn//850etvHJ+FqPNqfgstD3drNnTQqTaz+JZi9BUD+acObz64RsIxOPMmToVb9FiWoBsNsuGDRuOq76xVA69QLToWHUVA0AH8LKu660AQohncRTFEZXDeOSRRx7h4YcfxrIsrrvuOn7xi1+gqipLly7ltttuo7OzkzvuuIN4PI5t23z1q18lEAjwxS9+kWw2y6FDh7j11lt5+9vfXupbkUgk45hY2AnTncx2kzMyBH0RtHAY74QJ5PfvxehoH6IcToSxVA6rgHcCvxVCLMcxI/XxKjBfCFELdAPLgZ8crcAjzVZaNu2Sk9rLL6aiooKvfOUrrFy5kocffphgMMinPvUpVq1axdNPP82KFSu49tprefXVV1m3bh21tbXceOONnHnmmbz66qt897vflcpBIpEckapQPaqikcx2kzczBHEisQZmzib+7DOkt28jOO+0UatvLJXDH4ALhBDP40w0ulEI8Ulgm67rjwohPg30TfT+ra7rxzf2GQdMmzaNPXv20NnZWQiTnUwm2bNnDzt37uSqq64CYMmSJSxZsoStW7fygx/8gN///vcoijIgJLfkjcODDz5YahEkZYTfEyLkrySZ7SaTT1ERdNKDc+c5ymHTBnjn5aNW35gpB13XLeDmQcmvF53/NfDrYylzvM5WUlWViRMn0tTUxE9/+lO8Xi+P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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = DiscriminationThreshold(unfitted_model)\n", + "oz.fit(X, y)\n", + "oz.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Check if fitted on Feature Visualizers*\n", + "Just the ones that inherit from `ModelVisualizer`" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/rbilbro/pyjects/my_yb/yellowbrick/features/importances.py:159: YellowbrickWarning: detected multi-dimensional feature importances but stack=False, using mean to aggregate them.\n", + " ), YellowbrickWarning)\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "viz = FeatureImportances(fitted_model)\n", + "viz.fit(X, y)\n", + "viz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/rbilbro/pyjects/my_yb/yellowbrick/features/importances.py:159: YellowbrickWarning: detected multi-dimensional feature importances but stack=False, using mean to aggregate them.\n", + " ), YellowbrickWarning)\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "viz = FeatureImportances(unfitted_model)\n", + "viz.fit(X, y)\n", + "viz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: Not sure how to deal with Recursive Feature Elimination" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Check if fitted on Regressors" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "X, y = load_energy(return_dataset=True).to_numpy()\n", + "X_train, X_test, y_train, y_test = tts(X, y, test_size=0.20)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "unfitted_nonlinear_model = RandomForestRegressor(n_estimators=10)\n", + "fitted_nonlinear_model = unfitted_nonlinear_model.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "unfitted_linear_model = Lasso()\n", + "fitted_linear_model = unfitted_linear_model.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = PredictionError(unfitted_linear_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAQ4AAAEVCAYAAAARoAv9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAgAElEQVR4nOydd5wUVda/n6pOkyMwAww5FNGEiASHQYK6KipBXAOYdVlFRXfVF/U14M91ddk1rKCiAutrFtQFyVERRNqApCJnZpgZJvd0qqrfHx2YGSZPT+Q+nw/aXeHWrZ6ub597zr3nSIZhIBAIBDVBbuwOCASC5ocQDoFAUGOEcAgEghojhEMgENQYIRwCgaDGCOEQCAQ1xtzYHWjJKIrSGdgP/F5iswS8pqrq+3VsezHwhaqq8xRF+RVIU1U1t4JjY4FFqqpe7n9f6fE17MezwJ+B42V2LVNV9Ym6tl/JdS8AvgTygHGqqh6qRRtpwJuqqvYLbe9aPkI46p9iVVUvCLxRFKU9sF1RlK2qqm4LxQVKtl8B8cAlNTi+pnyqquoDIW6zKsYCa1VVvbuBrytACEeDo6rqcUVR9gI9FUW5CLgLiATyVFUdoSjKXcBUfMPIbOABVVV3K4rSDpgPtAMOA20CbSqKYgCtVVXNUhTlSWAK4AX2ArcDHwDhfktjgH9f4PingT/6t+3xXy9dUZR1wCZgKNAR+A6YoqqqXpP79bdzGugFzAbGl3m/yP//zvissfmqqr7it9a+A3b59w1XVfWkv81b/J+RSVGUcFVVb6niPoLXU1X1jWr2+1Lg74ANaAusVFX1LkVRzMAbwDDADRwA7gCc5W1XVbVQUZTrgf8FTEA+MF1V1S01+RybGsLH0cAoijIY6A786N/UF9+wYYSiKMPxPfSXqap6Ib4v7kL/cf8GNquq2heYhu9BKNv2WHxCMdhvfh8EHsD3xS5WVfUCVVW1EsffAVwFDFRV9TxgOzCvRJPdgDSgP3A5MLyC25qkKMqvZf5dUWJ/jqqqfUo8tCXf/x8+y6E/PpG6VVGUm/zHpQAvqKraMyAaAKqq/h8wB5+lc0s17qPs9avDQ8AzqqoOAvoAYxVFGQAM9n8m56mqOgCfQJxX0XZFUXr5+zre37dngK8VRYmpQV+aHMLiqH8Cv/Tg+7yzgFtUVT2qKArANlVV8/37r8YnKj/49wEkKIqSAIwCHgNQVXWfoihryrnWKOBzVVVz/MdNh6CvpTyuAj5QVbXI//41YIaiKFb/+//6LYwCRVH2AQkVtFPVUOW78t4rihKJTyzG+PubpyjKPH+/NuOzHjZV0m5176Ps9avDFOAPiqL8Dz6RjgCigN8ADfhRUZTlwJeqqm5RFCWugu1TgdWqqh7w3+MaRVFO4bP81taiX00CIRz1TykfRzkUlnhtAv6jqurjAIqiyPiGJjmAgc+UD+Atpy2v/zj858cBcZVcu6zFKeP7TgSuU1xiX9nr14TCCt7L5bQpAxb/a5eqquXdZ1mquo+y168O3+ETiWXAZ8AgQFJVNVdRlPPxCd7lwKeKoryuquo/y9sOeCror6Wc7c0GMVRpWqwA/qgoSlv/+/uB1f7Xy4B7ARRF6QiMKOf8VcC4Embws8B0fIJiUhSl7EO6HLjD/8sPviHQBlVVXSG4lypRVbUAn2XxZwhGfyYDK2vYVEjvQ1GUeOBi4HFVVRcC7fFZgiZFUa7B9zf5QVXVZ4EFwPkVbQfWAGMURenqb/tyoANnhqrNEmFxNCFUVV2uKMrLwEpFUXR8jrRxqqoaiqL8GfhAUZRdwDHg13LO/1ZRlD7ARv9QZwdwD+AAfgZ2KYoytMQp7+H7Em/xWzf7gFtq0fVJiqIMK7PtiKqqY6tx7i3Av/1+Cis+n8c8oFMNrl+X++itKEpZi6Q98BLws6Io2fiGlxvxicdcfEOj7f7zcvB9xkfL266q6iH/cGWh37HqAK5VVTWvBvfX5JDEsnqBQFBTxFBFIBDUGCEcAoGgxgjhEAgENaZZOkftdrsNGAicxBc7FwgEocWEb8bsTwMGDDgrOtUshQOfaNRmUo9AIKgZlwHfl93YXIXjJEDPnj2xWq1VHVtntm/fTr9+LXcBpbi/5kuo780wDCRJwu12s2fPHvA/a2VprsKhAVitVmw2W4NcsKGu01iI+2u+hOre7HY7WVlZjBkzpuTmcl0BzVU4BAJBCLHb7axc6Zuw26tXL5KTkys9vsUJh9frRddrtPK7Wrjd7pC32ZSozv3JsozZ3OK+Muc8JUVj9OjRdOrUCZer8tn6LSocW1BQUC8PeLdu3ULeZlOiuvfndrspKCio594IGpKyojFgwIBqnddifj68Xi8mk4mIiIiQt+3xeBrECdtYVPf+rFYrDocDr9crLI8WQG1FA1qQxaHruvgyNwAmk6lehoKChkXTNH7/3ZcKt6aiAS3I4hA0DJJU25QcgqaEyWRi0qRJHDx4kD59+tT4/BZjcQgEgqo5fPgwgRXx4eHhtRINEMIRUhYuXMirr75apzY+/PDDs7YdPXqUK6+8kscff5wXX3yREydOkJuby3//+986XUtwbmG32/n4449Zvnw5dU2nIYSjiTF79uyzttntdtLS0nj55ZeZMWMG7dq1Q1VV1qwpL+2oQHA2JR2hrVu3rvOQs0ULR0JCQoX/5s2bFzxu3rx5lR5bE3799VemTJnC+PHjWbduHQBbtmzhj3/8I7feeitPPvkkHo+HgwcPctNNN3Hrrbdy8803c/LkSWbPnk1eXh7PPvtssL0TJ04wZ84cli1bxkcffcRtt93G/v37mTNnDps3b+bTTz8NwSclaMnUJXpSEcI5GmLCw8N55513OH36NBMnTuSyyy7j6aef5qOPPiIxMZF//etfLFq0CI/Hw3nnncdf/vIXtm7dSkFBAX/605/48MMPSwlHu3btuPfeezlw4AA333wzS5cuBeD+++/nk08+YdKkSY10p4LmQH2IBrRw4Th9+nS1jrv99tu5/fbbK9xfVFRU4b6yDBgwAEmSSExMJDo6mpycHE6dOsXDDz8MgNPpZMiQIUydOpV3332Xu+++m+joaB555JFqX0MgqA47d+6sF9GAFi4cjUEgNp6ZmYnD4SA+Pp7k5GTeeustoqOjWb16NREREaxevZoBAwbwwAMPsHjxYubOnctLL71UbaeVLMtiPoWgUrp27Urbtm3p169fSEUDhHCEHKfTyeTJk3E4HDz//POYTCZmzJjBvffei2EYREZG8ve//52ioiIef/xxZs+eja7rPPnkk4Bv+vdjjz1WZXSmY8eO7Nmzh3nz5lVqLQnOPQJL48PCwrj11lsxmUwhv0azzHJut9s7Awf79esXXFIcWKNSH1PDi4qKiIyMrPrAZkpN7q8+P+f6wm63h/wXt6lQ9t7sdjsZGRlcddVVdYqcuFwutm/fDtBlwIABh8ruFxaHQNBCKOkI7dOnD507d663a7XocKxAcK5QNnpSn6IBQjgEgmZPfYVcK6PRhiqKorQB7MBofLVN5+ErbLwd+LO/SrpAIKiE3bt3c/ToUaDhRAMayeJQFMUCvM2ZauizgKdUVb0MX4Xx6xqjXwJBc0LTNA4cOAA0rGhA41kcrwJzgCf97wcA6/2vlwJjgEVVNeL3+gbp1q0bHo8ndL0sQU0mgTVHqnt/Ho+H/fv313NvQo/dbm/sLtQLo0aN4vjx40DD3mODC4eiKLcDmf7K7AHhkFRVDcSFC4DY6rTV1MKxGzZs4OTJk6Wmgd94443MmjWLlJSUal/P5XLxzTffMHHiRBYuXEhsbCwjR47kww8/5NZbb63VPVRGTcOx/fv3F+HYRuTQoUN07NgRWZax2+1MmDAh5NcoEY4tl8YYqtwJjFYUZR1wAbAAaFNifzSQ2wj9qjOpqakhWTuSmZnJ559/DsC4ceMYOXIkUP7KWcG5hd1u55NPPmHp0qV1XhpfFxrc4lBVNTXw2i8e9wOvKIqSpqrqOuAqYG0orvW3v/2twn1XXnklF1xwAeBb0bps2bIKj33wwQerdb2FCxdy4MABTCYT3333HcnJyeTk5AC+RMozZswIvn/qqadQFIUxY8Zw0UUXcfDgQRITE3njjTeYM2cO+/bt480338QwDFq1akVubm5w5WxBQQHXXnstaWlp7N+/n5dffpl33nmnuh+LoJlSMnqSnJzcqNnYmko49lHgOUVRNgFW4ItG7k+t2b59Oz/99BNffPFFcGo5wJw5c7j00kv5z3/+wwsvvBBcAXv06FEeeughPv30U06fPs3vv//O/fffT/fu3XnggQeC7f7pT38iNjaWZ599lokTJ7Jokc8F9MUXX9SLqSpoWjR0yLWqagGNOnNUVdW0Em+Hh7r9J554olrHXXDBBUHrozxq4hg9efIkw4cPR5ZloqKi6NmzJwB79uxh8+bNwWXxeXl5AMTHx9O2bVsA2rZtW2U9C4BBgwYxc+ZMTp8+zcaNG5k+fXq1+ydofjS0aBw7doxHHnmk0udHTDkPMSkpKWzbtg1d13E6nezbtw/wrVQcO3Ys1157LdnZ2UEfRnnmZkUrXwNjWkmSGDt2LDNnzmTo0KFYLJZ6vCNBY7Jr164GFY0NGzZw1113VVkxoKkMVVoMvXv3JjU1lQkTJjB9+nQSExMBX+KdpUuXctttt3H33XfTo0ePCttITEzE4/HwyiuvlNoeWDkLPqfpihUrxDClhdOlSxfatWtX76JhGAavv/4648aNIzs7m0svvbTqE5rbv61bt3beunWr4XQ6jQAul8twuVxGfVBYWFgv7daF9PR0Y/LkySFpqyb3V5+fc32xdevWxu5CjdF1Pfha07QKjwvFveXl5Rm33XabER8fb8THxxszZ840ioqKjK1btxpbt27tbJTzDAqLoxmyYsUK7r77bqZNm9bYXRHUA3a7nSVLlgSHq7Jcv4/pxo0bWbx4MTExMXz00UfMmDGjyhwewsfRDBkzZgxjxoxp7G4I6oGSjtC+ffvSpUuX4D6XVyO7yEVipG/S44l8BycKXPTzatjMtU/Wc9VVV/Hiiy9yxRVX0LVr12qdI4RDIGgASj70FT3kZaMnAdHQdJ3XN+xi3f4Msgqd5Dk9ZDtcuL0GmuYlZUsmdwzsxsPD+2CqhnXi8Xh44YUXuP7667nooosAX7i/JgjhEAjKUN5DXp0HvzxKPvSB89O6JTEttXeph7yykOustTtYtP0oNrOJzEIXx/IceHUDi0nGLMGJXAezf9iDLEk8kta30v5kZGRw5513smnTJr799ls2bdpUq6icEA6BwE/gIV+9N51TBU7aRIcxokcyGAYbDpyq9MGviNc37OKbHceQJQmb2US+08Pnvx3Gq+n8ZWR/oGLR0HSdWet2MmvDLoo9OmYZXB4dzTCQJPDqOmaThCRJ5Dk9fLvzBHdd2oOYsPLXEW3evJk777yT9PR0kpOT+fe//13rUL4QDoHAz7827GLORpU8pwePZnAop5AfD2cSG26lU3wUNrOJQpeXb3YcA6jy193l1Vi3PwNZkjAMg2O5DnKcbjyawcHTu0CSeDi1Fzt37gTOtjRe37CLRb8fxenRMUmg6QZOr4YBmGQJA18CG6dXx6tp/Hgkk4nz1nNlr3alhM0wDN555x2efvppvF4vQ4YM4b333iMpKanWn5UQDoEA30M+78d9ZDvcSIAsgVc3KHJ7cXp1OsRFIvsn68mSxLr9GUwd1qvSYUt2kYvsIhc2s4ljuQ6yipxIkoRJ8j3si34/ilmWmHrjjRw4cIDevXuX6s+6/RnYzDIWk4SmG0iAJIFh+ARDAjyagYbPArGaZdyafpawPfroo8HKhVOnTuV///d/6zxpUIRjBQ1CvtPNz8eyyXdWvgaisTiR7+BEvpOS83gNw/fP7dVwebVSx592+EShMhIjbSRG2tANgxynu9QsYXNBFlYZ1u3PAJO5lGjAGdGRJYm4cKtPKCQJiyxj+DtnlmU0fyfNkkRCuA1ZkoLCFujz5ZdfTlRUFO+//z4zZ84MyUxjYXE0IKtWrWLdunUUFhYyYcIEhg0b1thdqnfcXi83zt/Aj0eycLg1IqwmBnVsxWdTUrFWMa25QTGC/wkiSf5feN+7UvsSImzBsGhF2Mwm0rol8flvh/FoBiZ/E0bGIawnd1Og5eJVLiK7yEW72IhS5wZEp9DlJSXOlyslt9iNxSxjkiHCasHl1XF7NWwWmbZR4aTEnWkj/cTxYLvXXHMNgwcPDs5iDgXC4qgHPvnkE4YOHcrYsWMZNWoUX331FeDL1jRz5kyee+45vv3221q3v2HDBq644gpGjx5d4XL6AwcOcN111wX/XXTRRUFzdd68eVx99dVcc801TJ8+vdTCuieffJLBgwdzzTXX1Lp/Jblx/gbW78/A7dUxyxJur876/RncOH9DSNoPFe1iI2gbG14qx4UEmCQJqyxjM595VHTDIK1bUrWiK9NSe3NDvw6EW2R0QM48TPjJ3cSEWbDEJpAYGVauAAVERzd8Q5QOcZH0TY6jT1IcT485n8PPjGfzw1dxYZtwBnZoRceEKCRJwtB1Tqz4mH2vP8rh3WcS8YRSNEAIR72wZ88eHnjgAb755htmzZrFSy+9VGr/7NmzueWWW2rVtqZpPP/888ydO5clS5awePHi4EK6knTt2pWvv/6ar7/+moULFxIeHs7o0aPJyMhgwYIFfPnllyxevBhN01i+fHnwvHHjxjF37txa9a0s+U43Px7JCvoGAsiSxI9HsprUsMVmNnHHwG4kRNqQZQnNAFmWaBcTzsgeycSEWXBrGlE2M2P7pjAttXfVjQImWeYvI/szfXgfOnuyaJW9j9gwC7HKhYS371apAE1L7c3YvilE2cy4NY2YMAsTz+/I9LQ+2MwmlDaxDGsfEzzeW5TP/vkvkbHhazB01F07Q/LZlEcTshUbh9rG5ytDVdXgzM6UlJTgmNIwDF599VVSU1Pp27dyj3xFbNu2jU6dOtGhQwcArr76alavXk337t0rPGfTpk106NCB9u3bk5GRgaZpOJ1OzGYzTqeT1q1bB48dOHAgx44dq1XfyrIvqwCHW8Msn70CuNitsS+rgItSQvtLWBceHt4HWZJYvTedzMJiWkf5RGNaam+8ulGn70lajJON+Qc5ZJYxde5PUrdewbBuRZhkmUfS+jJ1WK8Kr32TkkCHonD+u24ju+e/gicvm4iYOD6c9z5paWk17md1OWeFo7oTc2rDnj176NKlC4Zh8OGHHwYr0f/nP/9h06ZNFBQUcPjwYf74xz+WOu/mm28uN/fH448/zpAhQwDfBJ7k5OTgvqSkJLZt21Zpf5YsWRIceiQlJXHnnXcyYsQIbDYbQ4cOZfDgwXW634ro3iqaCKsJt/fsFAHhVhPdW0XXy3VrS2UPqknmLD9Eddm9ezerV63i0s6teeLOm+io9K2RANnMpgqvbZIlEo/Y2TnnGTxuNxcNGMD8efNo3759rfpaXc5Z4Sg7Macm8fnKOHnyJEVFRdx7771kZGSgKEow9eDkyZOZPHlyhed+9NFHtb5uRbjdbtasWcOjjz4K+BIIrV69mtWrVxMdHc1DDz3EkiVLuPHGG0N+7ZgwK4M6tmK9fy5DAN0wGNSxVYUTlRqbyh7U2tClSxdSUlLo3bt3yJfG5+Tk8Mwzz+B2u7nrrruYOXNmMIF3fXJOCkfJiTklqW58vjL27NnDxRdfzIIFC8jLy+Oaa67hl19+Ca4JqIzqWBxJSUmkp6cH92VkZFQ6kWfDhg307duXVq1aAfDDDz+QkpJCQkIC4Fswt3Xr1noRDoDPpqQGoyrFbo3wElGVlo7hrxpvs9m4+eab62WVa3x8PHPmzCE3N5ebbrop5O1XxDkpHCUn5pQlEJ+v7S+Oqqr06dMHgNjYWK655hrWr19fLeGojsXRv39/Dh06xNGjR0lKSmLJkiX84x//qPD4JUuWcPXVVwfft2vXjt9++43i4mLCwsLYtGlTML1hfWA1m/nqrsvJd7rZl1VA91bRTdbSCCV2u50TJ05w9dVXI8tySEVj9erVHD9+PGi9XnnllSFru7o0Rl0VE/AuoOALkd8PWIDFwF7/YbNVVf20vvpQMkZelurE5ytDVVVSU8/8ml5++eW8+OKLQT9HXTGbzTzzzDPcfffdaJrG+PHjS2UTu+eee5g5cyZJSUk4HA5++OEHnn/++eD+888/nyuuuIIbbrgBs9k38WjcuHHB/dOnT2fLli3k5OSQmprKgw8+yMSJE+vc75gwa5NyhNYnZZfGV3epelXouh6M0plMpkatFSOVjFs3BIqiXA+MVVX1TkVR0oBHgP8CsaqqVvzTWQK73d4ZOFiXgkz/XLcj6OMIoBsGY/umnOXjqEnBouZITQsyQf0UvqovGrIgU30lFs7Ly+P+++9n+fLlSJLE448/zmOPPcYvv/xSL/dWoiBTlwEDBhwqu78x6qp8pSjKYv/bTviKLw0AFEVRrsNndTysqmpBffYjEAZbtz+D0w4XCRG2KsNjAkFl1Jdo7Nixg8mTJ3Pw4EHi4uJ45513GDVqVEjari0NbnEEUBRlPnADMAFoD2xTVdWuKMoMIF5V1ccqOjdgcZTd3q1btxrPw3d5NU473CREWEM2j6Ml01xrx9Y3u3fvZsuWLQBccskl9OrVKyTtbt68mZkzZ+JyuejevTvPPPNMsJxGA9E0LI4AqqpOURTlceBHYIiqqsf9uxYBb1SnjVDUjo0EEqqoVCuGKmdwuVyidmwZdF1n9+7dtG3bNuTZyBMTE3n11VcZN24cr776KuHh4aX219e9VVU7tjGco7cBKaqqvgQ4AB1YqCjKg6qqbgFGAjUuuy3LMm63u1l9oZsjmqaJz7gMsiwzceJEDhw4EBJLIzs7m4SEBCRJonPnznz33Xe0b9++UUs+lqUxLI6FwAeKomzAF015GDgKvKEoigdIB+6taaNms5ni4mIcDgcmkymkH7LH46myJF5zpjr3ZxgGmqahaVqVxXrOFQ4cOEDnzp2RZRmr1RoS0di4cSN33nknDz30EFOnTgV8yxaaGo3hHC0CypttNLSubUdHR+P1esutglYX9u/fT//+/UPaZlOiOvcnSRJWq1WIhp+AI7R3796MHTu2zj9UhmHw73//m+eeew5N01izZg33339/vZdGqC0t7ltQX1/slm6et/T7CyUloycpKSl1Fo2CggKmTZvG119/DcDDDz/MjBkzmqxoQAsUDkHTILDqOMpmptDlDenq44aivJXToQ65qqrK5MmT2bt3L9HR0bz11lulZvo2VYRwCEJKYNXx2n3pbD+ZS7FXI9xipl9yHCO6h2b1cX1T0crpYVHFrF61CgjdPI2//vWv7N27l169erFgwYJK0yM0JYRwCEJKYNXx8VwHucU+h2uxR2NXRi4FLg9Qt9XHZamPfCrlrZz+bO1m1mfs5NLOrUMacn3zzTeZNWsWL7zwAlFRUSFpsyEQwiEIGYFVxwCni104PRpew8AwwOnxpfVfuy+9TquPA5SugVJMm+gzSXfqYtFUtHI6LLEt6RmHSLt8ZJ1E49SpU8yfP5/HHnsMSZLo0KED//znP2vdXmMhhEMQMgKrjg3DIM/pQS8xKdnQDTILnOwwjDqtPg7wr/U7mf3DnmANlIM5Rew+lYduGDw6ol+d7yEgbIGl8bLZjLn3pXRUam8tbdmyhTvuuIOTJ08SHR3N/fffX+u2GpumPdgUNCsCq47TC4pLiQb4lkF7dQOHx5e3sy5kFhYze+Mesotc6Love7iuG5wucvHBT/vPKmVQGS6vxok8R/CcKJuZSKsZ3TAoOraP3B0/YvjD+4lR4bVaOW0YBnPnzuXaa6/l5MmTDBo0iOuvv77G7TQlhMUhCBk2s4lhnVuzas/Jcve7NZ1wv8+gNjk5AqUWvj9wihynx5eFXJaItJr9xYok0vOLOZHnoEti5WkJyzpAEyJ8tUtcHp29WfkUHNmH5fhOYsMshCd3wpyQxIXt42vcZ4fDwfTp0/nss88AuO+++3j++edDUtukMRHCIQgp487vxLMrtiFRukpJ4H2XxKha5zuZOH8DK9UTeDRfywErpsjtJcoa+CpLZUuglEtJB6jVbGLT4Uzyij2YTTLmzMMYh7fjNKCgTU/yveEkFLlYuSedX47nVDs37cmTJ7nxxhvZsWMHERERvPbaa4wfP75W997UEMIhCCmyJBHm9w94NOOMYkiAYTC4c+taOUbznW7W7UvHW85ibq9uoPuap310GIkRNk7kOSqMtJR1gB7NKSTP6UGSJLT0g8hHd/r626kvRpvOODxeojQLYTXMTZuQkIDNZqNbt27Mnz8/mBmuJSCEQxBSAoWNjuc6AAOvrgdLFiZG2vifUbWbur8zIxeHWwsWWi6Lw+1FliDb4WToa0uJCbfSyj//Ymhk6TNKOkB1wyDH4cYwQMo8hOnoTnTA274PRmJHJE3HajKTW+ymfWxEqRKL5UWHAqUnIiMjsdlsfPjhh0RERBATE0NLQjhHBSElUNgoMdJGhNVEuMVEtM1Mh7hIHkvrQ4S1dmP7uDBrhaIBEGExEWU14/JoHM8vJrPQGbQOPlFPlzo24MQFn1Xk1Q3QdaQcXxJob0of9Nad0A3Q/UaTRzeCQyQov3bs6dOnmTRpEvfcc09wvVRycnKLEw0QFoegHqissFFtaRcbgcUk4dbKl44+ybHsySwIru8oaSHYM4pwebWgdRAor/jNjmNYTBIWs4zhlvF0HYBckIURl1y6cUnCIktYTGecJ2Vz0/72229MmTKFI0eOkJiYyOHDh+nSpUut77epI4RDEHKqU4GsphS6vERYzLg1z9nXk8Dj1UsVdg5YCDazRL5bO2vuSEDElm7+lXCThVxANpvR45KDVo1JkjBJoOkGrSJtQZ9I2dqx//d//8djjz2Gy+XiwgsvZP78+U1yKXwoEcIhqDdCWdgoymbGYpIJM5vw6jq6YSBLkq8osyRhs5iwmCR0/wSSkhZCjNV0ViTHJMukRjtx6EcY17UTr52IIqvIjcujoRk6kiFhNsuEmWUGdmyFhEROcenctE6nkyeeeIIFCxYAMGXKFF566SXCwsJCcs9NGSEcgmaBz/kPSuQAACAASURBVOIw4fZq2MxmnzNTCkxl901rjw+zklXkBEkiLtwaFJYBSZFnWTyBVa4mWWJgX4V7O4bx1fajaLqBxeQb7ri8Gjf068BfRvYvd03MW+++y4IFC7DZbLzyyivceuutDf65NBZCOATNgsRIG/3axrE7I58cpxuPZmCWJdrHRmAz+YQCDKwWGcmA2HArUTazP6pSXKqt8pbGX+B3ZpbOen+mKn151tN9993Htm3beOCBBzj//PPr/0NoQgjhEDQLbGYTI7onU+Dy0p4IPJoetAzG9k0p5U8BSlkHdvuZFLYV5dOojl9G13Xmzp3LxIkTiY+Px2q18u677zbE7Tc5hHAImg1la+HEhFlKzeIsaRGU51tRVbXKJDwV+WXy8/OZOnUq3377LWvWrOHjjz9uUsmDGxohHIJmQ12jNV27dqVTp0707NmzRkvjd+7cyeTJkzlw4ACxsbHccccd57RoQNOpHesE5vnfbwf+rKpqaDMOC1oMNY3WBIqOWSwWJk2aVKNcnl9++SUPPfQQDoeDvn37smDBghY9P6O6NMbM0WsBVFUdCjwFvAjMAp5SVfUyfBP1rmuEfglaIHa7nfXr16NpvmXz1RUNwzB48sknueeee3A4HEyaNInly5cL0fDT4MKhqupXnKmbUrJ27Hr/tqVA4xbGFLQIAo7QI0eOcOjQoRqdK0kSsixjsVh49dVXeeutt4iICM2clJZAU6kdO09V1Xb+7ZcDd6qqWmFQvKLasQJBgNrWci1ZDdDr9XLo0KFGTSDs1nTyXBqxNhNWk1ytfZWdUwuadO3YkgUxo/FZIVVSsnZsfVLftUcbm5Z2f3a7naNHjwZruQJV3p9hGMyePZv33nuPFStWkJiYCMCgQYPqpY+BzGNI0C4m4uxVthVkWg9ElgL7Dp/KplObRNK6JfHnYQr//l4t95zycodUlui5udSO3aooSpqqquuAq4C1Dd0vQcugvHkaJedxlEdhYSEPPfQQixYtAmDZsmXccsst9dI/Tdf51/qdfPDTfk7mFQMS7WLCuH1Qdx4u8YCXl2k9kAcEOJOESJaD+9bvzyDP6Qmek+/08Plvh/FqOn8Z2b9UHyoSpeomem4qtWN3Ae8qimL1v/6iEfolaObous7evXuB6tc92bt3L5MnT0ZVVaKionjzzTcZO3ZsvfXx9Q27mP3DHk4XufwhXYPj+cXM2agi40sOVFGm9cCK48Drsvx4JItebWIxDINjuY7gDNuDp3eBJDE9rQ8mWa5UlKpbuqIp1Y4d3tB9EbQsZFlm/PjxHDhwAEVRqjx+8eLFTJ06lcLCQnr27MmCBQvo2bNnvfXP5dVYvTc9mG0sgATkOT2s3psenKNSMtN6STILizEMieiw0nlNPJpOsVvDoxmcKigmq8iJ5F/d6/TqLPr9KGZZYuqwXmeJkm4YeDQ9eP3qzI0RiXwEzZ59+/YFw60Wi6VaorFr1y4mT55MYWEhY8eOZeXKlSEXjbIZ1LOLXJwqKC6VECiARzfILHQGhw6BqfO6YeDyauj+IEbrqHDaRIcF97l1A93wLcwLt5owyZDjdCNJEobh22eWwGaWWbc/gxN5Dl92eMPA6dE4nFPI9vRctqfnsflwJi+t+h2tGkXbxcxRQbMm4NPo2bMnN9xwQ6lf8oDzz62d/SD07t2b6dOnEx8fz9SpU0M6E7QiH8J9Q3rSJjqcgzlFweX/ASyyROuosKCjMrVrm1J1YywmidgwC38a0hMkiTkbVfKcHpweL2EOjdgwC5d0bEVmoRO3V8er6XgNHd2AMLOJ47kOPNE2dMMgr9hNhj9DmlfXsZhM2MwysiSxdl86sWEWpg6uPJIkhEPQINRHqcaSjtBOnToFH/6SD25moRPdVcz4gjDSYl3IkhT0fTz11FMh6UdZKvMhjOyRzO5TeSV8HL7p0rFhFkb2SD7z2UiSr+SD72XwNZIEhnEmjWKJ18O6tAYDtp/Mxa3rPuepScZikskqcmK1yCz87TDFXg2P1593RJLw6Dp4DVrFRmCWfZbJ3ZdUPtFNCIegXqmOB782ohIQDU03uHhoKj369g9mNn/r+918vf0oJ/KKyXG6cbnc7H1zDn/7/nPaJrVh3bp1tGrVql7utzLH5rr9GXx822XohsEHP+0nPd8fVYn2RVUCoVaXV2PD/gw6xkf5/Q8+i0OWJNb6naOd/Pty8vKJj41BliS+P5jJf24ZxryfDnA8vxi5RKUIQ5IwdFh/4BQd4yLRdYOiPAfg0yJZkoLT+E87XOQ43JXepxAOQb1S2a/vtNTetQoL2u12VqxYwY+Hs8hr04P3NqRTvOYrwi1meifFcDinCJdbI9vhAs2DvOEjjD2+yWDJ5w2q1+TBlTk2Tztc5BZ7eHREPx64rHeF8zhKtuH73M6IUGahE8MwiA6zBi2KgEiddrjYl1VAXIQVj66TW+zGoxtYZF++kkirmVMFxUSHWemUEEWB24tXM3xGDODVDExmiYQIG/ERVjIruU8hHII6UekkoirCitkOJ98dyMQsy9UOC+7Zs4eVK1fy4+EsTsZ3JceWRK4/guDyutmVkU92kRPNMLAW5cDyd5CzjoHZgjH8ZkypV2PIoRkqlUfAsVno8p61r2SCY5vZVGG1ucraaB3lc4wWe84uc5kQYaN7q2haRdoIM5toHxtRyloJt5iC58qSFMyYJiFh9qdaDORTtZoq/4yEcAhqRXWGIOX9+hrAsdwisotcbDyYidnk+wKnxEX41odUUrMEoEuXLqR07ER+cTwRbTpzID036CuQgAKXB7Ms496/DdYvAJcDI6YV0pX3YmndgUK3JyRFryuiZAb1siHPkgmOa9vGyB6+DOwVtR8TZi11bsBaKe/clDjfZ5DjdBNts5TKb+L1nJ0UuiRCOAS1YtbaHSzafhSb2VShtVDeL+ex3CKyilxISBiGjq7jyxMKdIiPBM7ULCn5cAeqxlssFkb8YSxzsjaAVjqzOfiqusWFWXBoXnA5oFN/9LRbMUVEE+cv0lTbEpTVpWzCoZIJjkPZxrr9GZzOM4IpEgP7qn2uw0Xv5FiGdW7NTRd1oU10eFDYzrZ1SiOEQ1AjNF1n1rqdzNqwi2KPjqWExVDWWij7y6kbBrnFbjAM4iOt5Ds9aLpPEHKcbtobvjbK1iyx2+0cOnSI6667DrPZTCt/2DLf6SmV2dzQdSxmE50SonD2uRh3ZCR6ux6YDGgVaaNdbES1f/XrQijKQ1TVRmDf2k0/MWLwwFL7qntuXaJcYgKYoEa8vmEXi34/itOjY5JA1w2yipwcy/V56MtWOJuW2puxfVOIsplxuL3+GiVhdIyLJC7cGqxh4tF8sxfLmvSB6MnevXs5eNC3IDogSODLbG4YBkbWUfj0BSJPH0ECHhvRlycmj+eSjm3oEGOhd1Is1/VNqVNRqJoSSDhUF6GqrA2b2UTrCEuF7Vd1bl36JiwOQbUJODttZhmLSULz/9KXtBjKWgslf/1O5Dt48MstQcdeSpxvaJJb7EYyDOLCraUqvm3e8hP/XbqMCIuZK68YQ48ePYLtBo5Zuy+dn9d8j2Plh74Iyq8rGH7taP40pCcRVgvTUnuX+6tcnlO3PuaatFSEcAiqTUlnZ1y41e+r8OHRfFOj07qlVPgL1yUhmpE9koNDFwnoEBdJ25hwRnRP5slR/bGZTWi6zmPvfs6qVatwuDVSzhtIREEYF+h60PFqkmWmDu7OkW/e5/tlHwDQJ+1KYkbdxqo9J/nl+OnguL7kr3J5Tt3UbklgGGw4cKrWq0XPNYRwCKpNSWdnSWvBoxuEW2Ru6NehyqFAZY47kyzj8mo8NOcz1q5Z7Suy1OtCpKQuZzlejx8/zu23347dbsdqtTLmroc52u5Cin3TKcl3eoLnpJaIepY3r2TORhUD36Sq2q4WPdcQwiGoNmWdnR3iImkfG4HLq3ND/w785fJ+VbZRkeNO03X+uW4HK9UTfLfmR8hzoHfsS465FfG5RaTERQYdr2YJbrjhBvbt20dKSgrvvvc+L/6Wz/FTeeQ4XLg1A6tJIj7Cxtp9GVx6nk85yptXohsGeU4Phv91wIlb09Wi5xpCOAQ1IhShRjg7U3nAEjh8uhBnpwswFWajxyXhcGtous/ZmhQdFgzTzpw5k3feeYe3334blzmc35ct5VSBM7iwq9gDRW4Nr6aTp/iuU3ZeiW4YFLm8uL06SBJOj5eMAif5Lg9eHcwyvLTqd54ec54YspRBCIegRtRHJXqXV+PbTT9jRCaS7/Igm00YcUlIgFfXsSGTk5dHq6y9JEaOAWDMmDGMHj0aSZLId7p9q2B1HcMgGKnRNJ2jeQ5M/i2BoVaB03Mm0Y1Xp9jjBQl+P5mLy6sjy2CWZCxWU3C1qBiylEbI6DlI2TwRtSEUocYA6zZu5vCW9WRv+wGvZmCW5GAtFAMwsk/g/exvqAv+jn3Lj8HzAjNGs4tcaIaBXkI0AugGPPXD8WCf07olcSS3iKwiJ7puYJIl3zoNHVyajiz71qG6dR0kgqtF6/JZtUSExXEOUdE08aGRjZPpHnzzNLZu3ECE1YStdVssZhlZArzgNXTM+39B+v5j8Lro2asXSUlJZzci+UTB6S0/Ac3eHCf5TjcxYVbuG9KT93/ch9kk49EDSW5MaIaB26tj4Fst6lur4fN3lDeT9VxHCMc5REUrVY/GSlwysOH7E5jcZZIlRo0ahd0dQ3yug6wiJzYZrJu/Rtq2BoB+l41m6UfvExkZeVY77WIiSIiwkuc8e32FDLh1g31ZBVyUkkhusYfYcCutosLwaAa6obMzIx+LBJpuEGY2YZJ9oeLApLTEBpim3twQQ5VzhMpWqtozihrcFC+bjfzlu8Yztm8KvZJiiDWKkRa/jrRtDZJs4oq7HmLNwo/KFQ3wWQx3XNL9TLIbziS+sZokws0murfyRVYCIuATT9+qXItJ8h8rB0UDwGKSMMlSg0xTb240RnkEC/A+0BmwATOBo8BiYK//sNmqqn7a0H1ryVSWJyLfrTWoKR5YGg+ls5EHnK7b1H1M+uxFLG2SmPvee1w2dEiVbT4xsh+f/HIQ9VQ+huEbblhk3wzXvonhxIT5iiyVDSkHlpdnFjlJjglHwjc3xa3ptI0O5/pqzE05F2mMocqtQLaqqrcpipIA/Ao8D8xSVfUfjdCfc4LKcjzEWE0Naop37dqVbt260bVr16BoBJyhNrOJgX0VPv7oIzp37ly+T6McTLKMffrVjP9gPT8eycLl0YiwmRnUsRWP9yud96JsSLlXUgx9pFh0A3KKXfRsHcPFHRL56+V9ibBayrvcOU9jCMfnnKmb4neDMQBQFEW5Dp/V8bCqqgWN0LcWS2U5HgYkRTaIKR5YGm82m5kwYUIwKlJUVMT06dPp27cv06ZNA2pXQc1qNvPfe0aS73SzL6uA7q2iiQmznlWQqaKQslirUn1qXDtWUZRxqqourOuFFUWJBr4B3sU3ZNmmqqpdUZQZQLyqqo9VdK6oHVs7NN3gE/U09owi8t0aMVYTA5IiuUlJwCSHLst3eezevZvjx4+TlpaGqUR2qePHj/Pcc89x8OBBIiMjWbBgQb2m9hPUmNrVjlUUJRZ4WVXV+/2b7lYU5U5gqqqqR2rTE0VROgCLgLdUVf1IUZQ4VVUD9WIXAW9Upx1RO7bmXDLw7FWg9X1/gVquAPHx8cFVrsuWLWPatGnk5+fTo0cP5s+fX+3i0DW9fkv5+5Wlvu6tqtqx1YmqbATeCrxRVfUPwAJgtaIoTyiKUiObTlGUJGAF8Liqqu/7Ny9XFOUS/+uRQOXFPgV1IpSTt6qibPSkR48eaJrGiy++yM0330x+fj7XXHMNK1eurBfRENQP1RGOT/DVdw2iqupnwEVAO+BnRVGG1eCa/wPEA08rirJOUZR1wHTgn/7XQ/FFWgTNnPIKQAM8++yz/OMf/0CWZZ599lnmz58vhifNjCqHKqqqzlQUpVQ8SlGUfsAQIAZoD3yrKMon+Jyajiraewh4qJxdQ6vda0GTpyLRALjvvvtYtWoVL7/8MqmpqY3VRUEdqNYEMFVVdwVeK4qSiy8yMhBY4/9/HLAbUWVegC96cuDAAeCMaKxatQrdX5M0JSWFjRs3CtFoxtQmHNtDVdXyarXMUhTl7rp2SND8kSSJG264gYMHD9KhQwemTZvGhx9+yNNPP80jjzwC+CrLC5ovNRaOCkQjwA116IugmbNnzx66du2K2WzGbDYTFhbGH/7wB3799VfCwsJo27ZtY3dRECJCKvuqqqqhbE/QfLDb7SxcuJBFixZhGAZr1qwhLS2NX3/9lU6dOrFs2TJuuummxu6mIEQIe1FQZ0o6Qrt06cKsWbOYOHEiOTk5jB49mrVr13Leeec1ci8FoUQIh6BOlI2enHfeeaxYsQKAxx9/nI8//pi4uLjG7KKgHhD5OAS1pqKQ6wcffMDOnTsZNWpUY3ZPUI8Ii0NQK/bu3RsUDZPJxJtvvhkMt7Zr106IRgtHWByCWtGlSxe6dOnC8uXL+eqrrwCYMGECV199dSP3TNAQCOEQ1IjA0vhTp07x3nvvsXXrViwWCy+//DJ/+MMfGrt7ggZCCIeg2tjtdvbv309ycjL33HMPmZmZtGvXjnnz5nHxxRc3dvcEDYgQDkG1CDhCjxw5wmeffYau66SmpjJ37lxatWrV2N0TNDBCOARVUjJ6MmXKFI4cOcLAgQOZMWMGZrP4Cp2LiL+6oFLsdjuffPIJERERjB07lgEDBrBo0SIsFpGL81xGhGMFFWK323nzzTdZsGABmzZt4oILLgAQoiEQFoegfDweD6+++irLly8HoEOHDrjdbsLDwxu5Z4KmgLA4BGdx6tQpxo8fz/Lly5FlmRdeeIG5c+cK0RAEERaHoBRffvklTz/9NOnp6bRp04b333+fIUOqLogkOLcQwiEIYrfbmTt3Lunp6VxyySV88MEHIoeGoFyEcAiAMyHXyy67jIEDB/L0008LJ6igQppK7didwDzAALYDf1ZVVW/ovp2LHDx4kEceeYS0tDQiIiK44oorWmwNEkHoaAznaKB27GXAlcCbwCzgKf82CbiuEfp1zrFixQpSU1PZvn0769evPysbuUBQEY0hHJ8DT/tfl6wdu96/bSkg1mTXI5qm8dJLL3HTTTdRVFRESkoKL7zwghANQbVp8KGKqqqFEKwd+wXwFPCqqqqBIrYFQGx12qqsRF2oKVu4uLmSn5/P3/72N3766SckSWL48OHccMMNREZGtph7LA9xb6GlUZyj5dSO/XuJ3dFAbvlnlkbUjq0ZhYWFXHbZZRw+fJiEhATmzp3L8OHD+fnnn1vE/VVES/n7lUdj1Y5tDOdooHbsA6qqrvZv/kVRlDRVVdcBVwFrG7pf5wJRUVEMGTIEwzBYtGgRXbp0aewuCZopjWFxlKwdG/B1PAS8riiKFdiFqAgXMlwuF0ePHqV79+7Y7XZSUlKYMGECbre7sbsmaMY0ho+jotqxwxu6Ly2dY8eOcfvtt3Py5Elee+01tm7diiRJXHXVVSiK0tjdEzRjxFqVFsr69esZMWIEP//8M5qmsXTpUuDsAtACQW0QM0dbGIZh8NprrzFz5kx0Xefiiy9m8ODBhIeHC9EQhAwhHC2I/Px8/vznP7NkyRIApk+fTq9evdi3b58QDUFIEcLRgti6dStLliwhJiaGt99+myuuuAJN0zh06BDdunVr7O4JWhBCOBoBl1cju8hFYqQNm9kUsnYvv/xyZs2aRceOHRk2bBjgK5YkREMQaoRw1DMur4aakcvh3CIGdkjk458PsW5/RlA40rolMS21Nya5Zn5ql1fjcHYeb7zyNyZcP5bhfqHo378/K1euJCMjg0mTJiHXsF2BoDoI4agnNF3n2WU/84+1u3D51/lKgM0kM7BDAjaziUKXl292HAPgkbS+1W73Xxt28e6qrWQsfBMpfT+ffLGIv7zzGSMSNFavWgVAz549hWgI6g0hHPVAsdtNtxe/IqPQVWq7ATg1na3HTjOoU+vg9mW7T3LrxV3xaEaVw5d/bdjFS/9ZhHfZXOTifPSIWFzDJ/PGV6v4XjvJkC5thCNUUO8I4agHLv7n0rNEoyTFXh2XpnGqwEmOw0Why0u/l/9LpM1Mv+Q4RnQvf/ji9Hh548230NZ+imzoaMndKR4xBQpyOL37F36wmOh20aVccOGF9X2LgnMcYcuGmMzCYvZnFVR53MHsArKKXDjcGpph4NF1covd7D6Vx8Lfj/DXb7aS7yw9LXzaI9PJX/MxkqHj6Xc5jiunYricWI7txADcKb350RnN6xt21dPdCQQ+hHCEmJ+PnUbTjSqPczjd4BcMi8mEBBi6zpGcQrYcyeaN71V6/r+vuP69Nbi9XgCGjxwN1jCcl9+Ba9D1GJIJI7oVWlwy3pQ+WJK6YDPLrNufgcur1fOdCs5lhHCEmItSEjCbJKQqjvMYEpIkYZZlbGbfn6HIo+HRwTAAJIo9Gut/2cGN8zcAMOHaP5A89VXoehHoOgaALOPpfAF6604kRPpSDKTnOziR56ivWxQIhHCEmtZR4XRvFV2hcEhAQpiZQR1b0a9tHJE2MxKg6zqa31DRDANN8+L5YRF8/Bxr128gx+HEZjZx12Xn0broJLYDP4HmxQAkSSI2zARIbE/PZV9WEQ8u3MI/1+1A00XqVkHoEcJRD2x5+Cp6JcWcJR4RMkTbzAztmsQ1fVOQJYn4MCuGYVDy8ZachUSumEPY76sAA3f2SWau2Iam6xzfuxPjyA4shTmEFWVjM0l0jo8kISKM7CInmm6QGGml2KPxzY5jwt8hqBdEVKUeCLda+f2v13E8t4AJ8zawPysftwbhVhODOrbisympwYjJ2n3p7EjXKXB6KETDlHmYiDXvIzty0cOicKTdjta2B9tP5vHYu1+wbMVKoqxm2va7GHPbrmTkO7BaZE4VODGbZOLCraTERQKgG/D19qPcenFXWkeJKmyC0CGEox5pHxfNpoevJt/pZl9WAd1bRRMTZg3ufyStL1OH9SK7yEWe08Xge2cgbfwcSdfwtu6MY8QdGJFxSMDxvTs4fmQnEhCjXEhkSncAOiX4hkURFjPRNguyJKEbBtvTc8l3edA0g74vf8OQzq35bEoqVrP4kwvqjhiqNAAxYVYuSkksJRoBbGYT7WIjiJO8WO1LkHQNV69hFF31IEZkHABy5mFy1F9waXop0QhQ5PEQZT1TPGlHei65xR4MA2QZvJrO+v0ZQSery6txIs8hIi+CWiN+fpoIbdu0ZsKjzzJv/S+4ug08s8MwkIpycHk1ug8YjDm5a4ldBsdyHRS4PYSZTRS6vcTYLOQ53UiShGEYWGQZSfJFeTYfzuSlVdvYfDir1FqZoZFVh48FgpIIi6MRWbVqFW+//Xbw/aO3jcfbfWDpgyQJrdN5FHe+mNTBg9CNMw/5sVwHmUVOom0WuiRGEx9u5bTDhVcH8IlGINQLkFfs4cttRyh0eUutlflEPV3PdypoaQjhqCP5Tjc/H8s+a5ZnZei6zt///ncmTZrEjBkz+PXXXwHwaL7YiiyBOTcdk65hkiVMJhPemERuuqAzY/umEGUz4/R6KXB7aB0VRkpcJBLQIS6Sfm3jscgSEWYTYRYTkuSL7RiGARJEWkvXg5UlCXtGkRi2CGpEow1VFEUZBLysqmqaoigXAouBvf7ds1VV/bSx+lYd3F4vN87fwObDmRS5NSKtJi7tVLUDMjc3l/vvv58VK1YgSRJPPPEE5513Hm6vlwcXbkEzwJR5GNOxnehRCeg9LsGQZCxmmQibOehQ3ZGeywNfbiHcUnpBnNUkE2Wz4NG0Ur8KmmEQabFgls+eYZLv9uUHaRcbEaqPR9DCaayCTH8FbgOK/JsGALNUVf1HY/SnNkyct56Ve06iGQa64XM4Lt99gonz1vP13SPLPef3339nypQpHDp0iPj4eN5++21GjfJVu7zxg3X8dCQLs180APS4ZHQkbCaJLvFRtIvxPdg2s4m+yXG0jrJR6PKedZ3BnVoBsPVYNsVujXCriSEpvtW4Ds/ZlkWM1URiZP0XthK0HBrL4tgPjAP+438/AFAURbkOn9XxsKqqVa4Ua6wSkIVuL6v3nMSjlZ64pRkGi3edYNl3P9A6wvcgujWdPJfGnt/s/L+ZL+B2u+nRowfPPPMM8fHx2O12Ct1eNu5Px8g4iPX4TjTAm9IHrXUnJCDeKjO6XRjbf/u1VJ+62bxsyMxHls5YEbphcH5KDLf0TqSwXxTHCjykRFuIspr5v13ZbMg++/jUlJiz2m5piBKQoaVRhENV1S8VRelcYtMWYK6qqnZFUWYA/ws8VlU7jVUCcvOhU7j1PVQ0mfvP606wZ8Y4Xt+wK5jtK8rbBkt4JBMmTuTVV14hLCwsePzPx7JxZ6zBdGwXBhLelN5orTsF9984oDuvjB1w1jL7Cy7Ug9c47XCREFF5RrGKjh8aWdyi83eIEpA1p8mVgKyARaqqBurFLgLeaMzOVEV8hK3SRWzH8or5fyt+Y/lve7BGxmIzm/CYY+l034t0u7R/KdEAMBdkYT2+E5du4E7pg9SmE6YS0ZPWkbZyhcAky6UmkVWVBKii41vyr7GgfmgqUZXliqJc4n89EmjS3+TOCVFE2SrWXM2AL5auYs+bj5OxflFwuy0modwl7/2UHnTv2Qt3+94YbXyWhiRJGEBsuIXNR7IqjXoEJpFVN/FxTY8XCMrSVITjT8A/FUVZBwwFZjZudyrHZjbx4LBe5e80DMK2r+Hoh3/DW5RP4WEVo8QK1dMOF9lFvuxguq7j8mqkFzj54H+mEpnS3bdS1jCQgLhwC/2S40udIxA0BRptqKKq6iHgUv/rn/EJRrPh6SvOZ86mPWQWnZm/IXmchH/3EZbDvwGQlHodbUdNQioxzEiIsJEYaeOnrVt58+s1nE45n2ynDlRfBwAAEJ5JREFUTrhFJjHCRu+kGNxeg3CrCbP/vMA5AkFToan4OJoVmu5zMl7cIZE1+9JxeQ3k3HQi17yPnJdBVHQ0l931GPtjumJIZ5L66IZBWrcktv/2Ky/O/Zjdmfm4iiLJssbj1nQ03eBwThEd4yOIskWWOkcMKwRNCSEcteD1Dbv4ZscxZEliUMfWONxejqyfiysvg9YduyJdcQ/LXHE4T+ZhkqBVpJV+beMZ0T2ZYVHFLFu+kkM5hbja9SLDHIvHP7dCliQ0w+BEngPdMOjXNj4YJREImhJCOGqIy6uxbn8GkiRxNLeI3GI3Hs3AnHoLcuRSPGkTyfXKSBiEmWV0w0BH4rKuSaRGO1m5chUOjxc69iPTlIDLUyKoG4ikSD4H7H9uGVbuilqBoLFpKs7RZkN2kc9RefjYCTJXf4bXqyFLoIXHkD9wHBlOIzg0MfxCUOjysHjd9yxbvgKAa6+6EmdCh9KiUQKX12BfZkG5s0IFgqZAixWO2iw+qw5x4RYy9u4ke8FzSL8sx21fjtOrB40Fj6ajGwZOj0aRx0uR20t+sZu9Bw9T5PYyevRoLrzoIkyShFzOuhHwLXJzaXqlIV+BoDFpcd/MwOKzH49k4XBrRJRI11fX7FeGYXD3Uy+R/fEbyLqGltQNb49B6LqOYehYzTIezaDYq6FpRnBlqiRLuDr2x+iRwIABAziR5yAuwkp0oZmcYs9Z1zHLEpFW8/9v796DoyrzNI5/zzndnU5CyIXbgJGIgbzDRQqDyo4uF0VGcGvUGmcZL+OCMsNs6ZbZcmqcxUvtVu3M1o7FuFNeVildR0k5M+4w65S7Ciu7IuDq6NhgrSHscYAQlBByUXLr+zln/zjdMYEGukNC0offp4qq7k7O6fcF+un3vO9535feWFIuVcSY5LkWx+qXdrHz4HHiSRufrhFPDl79aqjC4TDfXf99tj3/c7AtrHnLiN54H3ZhCRoOuq4RNHSSttM/h8X5vAUr6QZDRXEhjdEAsaTFhOICJhYXMG9qOYV+g4ENDx2YVlrE3K+UyRCsGLM8FRzd0TjvH+kYNIkL3NGK9490DPmypb2jk9prlvLqb7fg+AJoX19HYMlqCv1+igM+iv0+nNQM2TSjvRn/4Y/wH/wQHBucL2/+KvAZLKueggZMLSlE1wbvw9IVibPk0kkyBCvGLE8Fx4GOHsLxzLdmR+IWB7LYmjGTba0WfXoBBROmYnzrQexLa0lYNn6fgc/QsXHvt0hv4Ga0N+NLT40vn4rlaLT2RCgNBvpbEfcvmc1NcyvpjSVI2o67PwqgadAXT7L7UNuQyirE+eCpPo6ZE0soChjEk6eOVhQGDGZOLMn6XJZl0dfXR0FRMXs7osz4dh0tvQmOxyAaS6JpEHAcai+qIBy3aO+NcKwnOig00lPjAaJJi8srK/pbEYaus+5PZvHTtxooCRg4jtsnomvuDm8ffOp27EofhxiLPNXiGB8MsGj6xEHrcoLbGlg0fWLWH8LOzk5Wr17NmjVraOsO0xVPcixh0BJ1iKe2XrQdiCRsjvdE+faCKi6pKIG2zKEBbofnmiuqB72P2dbNiUiCcMIinLAJJyxiSRvHcc6phSTESPNUcADU33kNV1VNxG/oWLZDwKeztHoK/7pmSVbH7927l2uvvZYdO3bQ0NBAT/sxSvwGrd1RkqlrEUN3h1J9uoah69y/dA6LyhwCRzOHBrjBMa108KZIb5pHSdoOlgM27qzamGUTTVg5t5CEOJ88ExyWbfNPb+/jOy//D32xJJdfVMG6RdU0/ugmfrfuuqyGYuvr67nxxhv57LPPqK2tZceOHcxRNdSUB4lZFif1ueLTddp6o7R0h/nJ7SuYXFWdMTQAbNvh6XfM/uexpMWv9h7uH1HRUn8cIG47XHGafViEGAs8Exzp+SPppf/jls2eo1/w4gcHz3psNBqlrq6Ouro6YrEYa9eu5fXXX6eyshKAb1xaht9w+ybSF0HprQccxwIH/D4f//3YDymurD7l/EGfTsDn478+ae0feWnpCnOsK0JR4MsFhNMdpIausfHm2nP+OxFipHgiONLzRzINw2ZaOOdk9fX11NfXEwwGeeqpp3j88ccHLUk4pdjPjAnFFPkNiv0+igM+gj4dWpsYfzjEhKAbKtPKillYOYEiv06h32BcwKA06CfoM/AbGr3xBJ19MSzb5oXf/5HeWILe+JejKroGBT6dcQG/bNUoxjRPBEd6/kgm2SyCc88993DnnXeybds27rjjjlN+HjB07lp4KaWFAXyGhu2A1t5MsPX/mF/icPRIM1+Eo/zNf4T4+NgXhBM2kYRFOG4RS1jYjkNZYYCJxe66Gk/s2s+upjYcvpzXBm6HazRp4zfoX9FciLHIE19rE1IfyEyTwjItgmPbNps2beLWW29l8uTJGIbBk09mXubUsm1e3t+JGdaxbRsdGHfiUyq+OMSlF0/kwbV/zoZ3j/HWS3vpS02PT/dV2LidnUUF7lJ9y6qnALiza3E7QzMJx2VymxjbPNHiSN+JmWkY9uRFcLq6urjrrrt4+OGHWb9+ff8M1tP5+c5GfvNJJ+8f6eBEJEG85SDJpo+pKi/i0e/dwWP7wrx9oJXIgP1K0mfUcC8/DE3nz746jfuXzO5vHTV93nva94wmbZo+l6FYMXZ5IjgAvn91DUurJ1PoN4hbFuMKfNw0t3LQIjiNjY0sX76crVu3Ulpayn333dc/ES2TWNLihQ8O0B5O0hVJEG05RKypgZ5Ykve1r3DRrNm8f6QDTdMybpWQvhQJGDq31c7A0HUmFBdQVhigO3rq5LY0y4ET4eGd1SvEcMr7S5X0Mn7p/UvKCgMsnzWVB6+bS9GAfVK3bNlCXV0dkUiEefPmsXnzZi655JIznrulK8zhzj4SqQTwh0+gAYnKOXw+bhqPvLGXcNxC5/StFgcIJ5L8ek8TP1x+GQU+gysurmDXgdYzvndlmfRxiLFr1FocSqlFqVXNUUrNVEq9o5TarZR6RimVdblOHoaNJCx2HjzOpnc/Adyp8Bs2bGD9+vVEIhFuu+02tm3bdtbQAIhZFknHbUtomkZy+mXEq6/EnlSFA3zS0UOhX4eTJqkN5ADjC/y8c7i9f3TnwevmMbkkeJoj3H+Utl5Z1VyMXaMSHKm9Y58H0p+ex4FHTNNcjNs1cHM254lbZx+G1TSNkpIS/H4/Gzdu5Omnn6aoKLtv8wKfge/EcUhNjUfTccZP7L/fIhJPsmBaBY7jkGlNHh0o9OtMLy8eNLpTFPCzbtEsjAzHaEBFUUDuGhVj2ljaO3Zn6vFW4Ou4O7qd0Xt7/pfmtk4CGXY56+iIsuO9PzCpyM/1119PTU0NM2bMYM+ePVkX8uPGRko/+wjbN574rEWgue+jAQEDgnacDfMn8bd9vXx4vI9oaphEA4I+DRyH8oBBuK+PQp9Os7mPY4Z7juVlDv9S4qe5O/HljV8aBAyN2WUB/rjv46zLORy8vpubl+t3Ie8dq5mmme4o6AFKsznP12rnU2WGBw3DOo5D+7tv0PHeVuas28H0iy4C4KqrrjrdaTIKhUK0Hj3KlVVT2BWvwDYMHMed9m5oGtPGF/LNKxRLr57L21fDF+EoP97+MVv3t9DSHcZv6JQHA1SWFeEAN82t5GtXzR30Ho2XL+DWX+zkgyOdRJPDu1pZrnX16t6q4O36Xeh7xw4clCgBTpzuFwcKGO4wbHqrAisW5cirz3Ki4fcAvLV9O2vXrs25MKFQiO3btwPwyHdv55f7O3nzaJRj3RE0DaaWFnL3ldWDRmzKi4L87OYr+ftVCR57q4EPP/2crmickqD/tFscBHw+/v17y+mOxjnQ0cPMiSUyP0XkhbESHHuVUstM03wbWAXsyPbA9Afyjff20PjiY8TajxIIFvLsM//MLTdn1VUyyMDQWLFiBQsXLkTXQvzj7Qto6Q6Dwxn3XS0K+Pm7lZcTS1pZbQQN7nIAtZUTci6rEKNlrATHD4DnlFIBYD+wJdsDDV1nZs8hzGceJtbby6yaGuo3b6ampibnQhw+fPiU0Egr8BnMqMi+wzK9sbMQXjRW9o79BFg6lPMcPHiQu+++G9u2ueWWW3jiiScYN27ckMpUVVXF/PnzmTJlimeviYUYDmOlxTFk1dXVPPTQQxQUFHDvvfee8U7Q07EsC8Mw0DSNVatWDekcQlxI8j44AB544IEhHxsKhdi3bx+rV68mGAxKaAiRhbyeq9LWdm4rgac7QltaWjh06NAwlUoI78vr4CgvLx/ysSePnsyZM2e4iiWE5+V1cPj9/rP/UgaZhlyFENnL6+AYCgkNIc7dBRUcjuP094tIaAgxdJ4YVcmWpmmsXLmS2bNnZzWtXgiR2QXR4mhsbCQSiQBueEhoCHFuPB8coVCI1157jVdeeQXLOvM2CUKI7Hg6OAZ2hF522WUYxpknmwkhsuPZ4JDREyFGjieDQ0JDiJHlueBobm6W0BBihHluOHb69OksWLCASZMmSWgIMUI8ExwDp8bfcMMNMstViBHkiUuVUChEfX39oHs1hBAjJ++DI90R2traSlNT02gXR4gLQl4HR0NDg0yNF2IU5HVw7N69G5DREyHOt7wODpDQEGI05OuoigGwePFi5s2bRyw28hs0n4/3GE1Sv/w1EnWLx+PphxnnaWiO42R6fUwLhUJ/Cuwe7XIIcQFYvHDhwndOfjFfWxx/ABYDxwCZ8irE8DOAqbiftVPkZYtDCDG68r5zVAhx/klwCCFyJsEhhMiZBIcQImcSHEKInOXrcOyIU0otAn5qmuYypdRM4EXAARqA+0zTtEezfEOllPIDLwCXAAXAj4FGPFA/pZQBPAco3Lr8JRDFA3UbSCk1GQgBK4Ako1A/aXFkoJR6EHgeCKZeehx4xDTNxYAG3DxaZRsG3wE6U3VZCTyFd+r3DQDTNK8BHgF+gnfqBvQH/yYgknppVOonwZHZQeCbA54vBHamHm8Frj/vJRo+vwEeTT3WcL+xPFE/0zR/B6xPPa0CTuCRug2wEXgWaEk9H5X6SXBkYJrmb4HEgJc00zTTd8r1AKXnv1TDwzTNXtM0e5RSJcAW3G9mL9UvqZR6CXgSeBkP1U0ptRZoN03zPwe8PCr1k+DIzsBrxhLcb7K8pZS6GNgB1Jum+Us8Vj/TNNcANbj9HYUDfpTvdbsHWKGUehtYAGwGJg/4+XmrnwRHdvYqpZalHq8ijyfYKaWmAG8CPzJN84XUy56on1LqLqXUhtTTMG4gfuiFugGYprnENM2lpmkuAz4C/gLYOhr1k1GV7PwAeE4pFQD24zbx89VDQDnwqFIq3ddRBzzhgfr9G/ALpdQuwA/8NW59vPJvl8mo/N+USW5CiJzJpYoQImcSHEKInElwCCFyJsEhhMiZBIcQImcSHEKInElwCCFyJsEhRoxSarJSqksppQ94batS6lujWS5x7iQ4xIgxTbMNaAXmASilVgOOaZpeu3vzgiO3nIuRthu4Wil1GPgH3MVnRJ6T4BAjbTdwHTAXeME0zaZRLo8YBhIcYqTtxl2lqgWoHeWyiGEifRxipDUDAeCvTNNMnO2XRX6Q4BAjrQ54xTTNnWf9TZE35FJFjAil1FeBV3FbHDL86jGyHocQImdyqSKEyJkEhxAiZxIcQoicSXAIIXImwSGEyJkEhxAiZxIcQoic/T8Aat9bpDq6agAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = PredictionError(fitted_linear_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AObPn8+SJUu4/fbbByWPpG8mlhQe1ckAkpGDVDZDwPPPP8+f//xnhBDceuut/PWvf6Wjo4MDBw6waNEiFi1alFZQ06ZN4/XXXyeRSLBr1y5uv/12rrrqqi7n27x5M1OnTsVxHH73u9/xta99Lb3vmWee4e2336ajo4OdO3fy2c9+Nr1v0aJFhEKhHvLdf//9nHbaaenH9fX1jBs3Lv24srKSNWvW9Pn+Xn75ZS677LL045kzZ/LYY4/R0tKC3++npqamS3v6W2+9FUVRuPbaawdkyUkkkpHPsCgbwzBOBh42TXOhYRgnAi8BW5K7nzBN83+HQ65sUlRUxBNPPMH69eu59NJLueCCC6ivr+dzn/scixYt6nJsR0cHTz/9NDt27OCOO+7oomzq6uoIhUJ84QtfoL6+HsMwuPvuu9P7b7zxRm688cZeZfjDH/6Q8fcVj8d5/fXXueeee9Lbqqurue2227j11lvJy8tj1qxZqMl41x//+EcqKytpamri85//PNOmTWP+/PkZl0sikQyObK3PQ65sDMO4D/gckLrFngc8aprmI0Mty1AydepUAMrLy3n66adZsmQJhYWFWJbV41jDMAAYP358j+aimzdv5uMf/zi//e1vaWtr47LLLuPDDz/kpJNO6leGgVo2lZWV7N+/P/24vr6eysrKXs9ZU1PDnDlzKC8v77L96quv5uqrrwbg0UcfTT8/9W9ZWRnnn38+a9askcpGIskRsrk+D4dlUwtcBTyTfDwPMAzD+BSu9vyqaZqjru1v6s7+f/7nfzjhhBNYtGgR77zzDitWrOhx7KHatZimyezZswEoLi7msssuY8WKFQNSNgO1bI477jh27NjB7t27qays5OWXX+aRR3r/rr388stceumlPbY3NTVRVlbGvn37WLJkCc899xzhcBghBIWFhYTDYd58803uvPPOAckkkUiGhKytz0OubEzT/LNhGFM6bXoXeNI0zdWGYXwT+BbQb2R93bp1XR5XV1eTSLhVw+FwOGPypgiHw4RC2oCOjcViJBKJtBXR+fGpp57KD3/4Q/76178SCARQVZWWlpb0MbGY2+8qFAoRi8UQQnSxRjZs2MBpp52W3nbqqafy4x//uEdq8pFy3333ccsttyCE4PLLL6eqqopQKMTdd9/Ngw8+yNixY2lsbOTNN9/k/vvv72Ex3XnnnbS1taHr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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ResidualsPlot(unfitted_linear_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AObPn8+SJUu4/fbbByWPpG8mlhQe1ckAkpGDVDZDwPPPP8+f//xnhBDceuut/PWvf6Wjo4MDBw6waNEiFi1alFZQ06ZN4/XXXyeRSLBr1y5uv/12rrrqqi7n27x5M1OnTsVxHH73u9/xta99Lb3vmWee4e2336ajo4OdO3fy2c9+Nr1v0aJFhEKhHvLdf//9nHbaaenH9fX1jBs3Lv24srKSNWvW9Pn+Xn75ZS677LL045kzZ/LYY4/R0tKC3++npqamS3v6W2+9FUVRuPbaawdkyUkkkpHPsCgbwzBOBh42TXOhYRgnAi8BW5K7nzBN83+HQ65sUlRUxBNPPMH69eu59NJLueCCC6ivr+dzn/scixYt6nJsR0cHTz/9NDt27OCOO+7oomzq6uoIhUJ84QtfoL6+HsMwuPvuu9P7b7zxRm688cZeZfjDH/6Q8fcVj8d5/fXXueeee9Lbqqurue2227j11lvJy8tj1qxZqMl41x//+EcqKytpamri85//PNOmTWP+/PkZl0sikQyObK3PQ65sDMO4D/gckLrFngc8aprmI0Mty1AydepUAMrLy3n66adZsmQJhYWFWJbV41jDMAAYP358j+aimzdv5uMf/zi//e1vaWtr47LLLuPDDz/kpJNO6leGgVo2lZWV7N+/P/24vr6eysrKXs9ZU1PDnDlzKC8v77L96quv5uqrrwbg0UcfTT8/9W9ZWRnnn38+a9askcpGIskRsrk+D4dlUwtcBTyTfDwPMAzD+BSu9vyqaZqjru1v6s7+f/7nfzjhhBNYtGgR77zzDitWrOhx7KHatZimyezZswEoLi7msssuY8WKFQNSNgO1bI477jh27NjB7t27qays5OWXX+aRR3r/rr388stceumlPbY3NTVRVlbGvn37WLJkCc899xzhcBghBIWFhYTDYd58803uvPPOAckkkUiGhKytz0OubEzT/LNhGFM6bXoXeNI0zdWGYXwT+BbQb2R93bp1XR5XV1eTSLhVw+FwOGPypgiHw4RC2oCOjcViJBKJtBXR+fGpp57KD3/4Q/76178SCARQVZWWlpb0MbGY2+8qFAoRi8UQQnSxRjZs2MBpp52W3nbqqafy4x//uEdq8pFy3333ccsttyCE4PLLL6eqqopQKMTdd9/Ngw8+yNixY2lsbOTNN9/k/vvv72Ex3XnnnbS1taHr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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ResidualsPlot(fitted_linear_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ResidualsPlot(unfitted_nonlinear_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = ResidualsPlot(fitted_nonlinear_model)\n", + "oz.fit(X_train, y_train)\n", + "oz.score(X_test, y_test)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "unfitted_cv_model = LassoCV(alphas=[.01,1,10], cv=3)\n", + "fitted_cv_model = unfitted_cv_model.fit(X, y)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = AlphaSelection(unfitted_cv_model)\n", + "oz.fit(X, y)\n", + "oz.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "oz = AlphaSelection(fitted_cv_model)\n", + "oz.fit(X, y)\n", + "oz.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Check if fitted on Clusterers" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "X, _ = load_credit(return_dataset=True).to_numpy()" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "unfitted_cluster_model = KMeans(6)\n", + "fitted_cluster_model = unfitted_cluster_model.fit(X)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: Not sure how to deal with K-Elbow and prefitted models...\n", + "\n", + "# visualizer = KElbowVisualizer(unfitted_cluster_model, k=(4,12))\n", + "# visualizer.fit(X)\n", + "# visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "# visualizer = KElbowVisualizer(fitted_cluster_model, k=(4,12))\n", + "# visualizer.fit(X)\n", + "# visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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jdva2sqcOR3PjF865SwQczGXVwyAuUqpjzGHhHe8FvOAcMstt3DHHXfwzDPPRL2/8zpuvvlmXnjhBXK5HJZlcdZZZzFlyhQuvfRSTjjhBB5++GEaGhpoamri/vvv5yc/6XhRt033yiuvMGnSJOrq6nj22WeZMWMGrtv9ovXuu+/yq1/9iiAIWLNmDVOnTuVLX/pSp68t7MlTaOkXxuuLh3oKK/rCwVyFvXoK2wKiOnhLiIGqnAXiRMwlEb8ZXlWtmjId7dcXHnvsMfbff39++9vfcuihh0adDgDvvPMOzz77LPfddx+WZfGPf/yDs846i8cee4wf/OAHfZbHU089xahRo6irq+PRRx9ll116cm14uP322/n617/OiBEj8H2fiy++mK233pqJEyeu09L3PA/bttv34CmM7xeGgIQQ5VHOAnEb5mL3f8FcivHEroaXemT8+M4fP/NMOO00c/u44+DPnZzWado0uP9+c/uWW+DSS2HevB6Fffnllxk7dixHHXUUZ555JrvvvjvHHHMMv/vd77Asi4suuoiddtqJsWPHcskllwBQW1vLZZddxltvvcVVV11FLBbjyCOPJJFIcM8995DL5Vi1ahW33347dXV1zJo1izlz5tDQ0MBHH33ETTfdhOM4nHfeebS2thKPx7n44ovXGZ6pqqpiwYIFPPTQQ8yYMYMtttiChx56KPwYjuPCCy9c53188MEHnHTSScybN48dd9yRww47jHnz5nHHHXe0b3g96aSTCIKA6667josuugiA888/nzPOOIN0Os3NN9/MypXmMrdf+cpX+Pjjj/nggw+46aabmDFjBsuXL+f666/nu9/9Lg888ABvv/02vu9z8MEHs9tuu7F8+XKGDx+O4zgMHz6clpYWDjnkEKZMmcJvfvMb4vE4a9as4eyzz2bBggVks1nOO+88pkyZwtlnn82HH35IPp/nhBNOYP/99+e4446jvr6e5cuXc/PNN3PhhRfywQcf4Ps+3/72t5k6dSpCiN4pW4EILwF6dLnm35cefPBBjjjiCJqamvA8j/nz56OU4tVXX2Wbbbbh5Zdf5pxzzuHoo4/msssuY9KkSTz44IPceuutTJ8+ndbWVh588EEAfvazn3HzzTeTTCY59dRT+ctf/kIqlWLZsmU89NBDLF26lC98wVzC9oorruC4445jt91248UXX+Sqq67i6quvbs9r2LBh3HTTTdx9993ccMMNJBIJvvOd77Dvvvt2+j5aW1u58cYbefrppznvvPM47LDDuPXWWznppJMYN24cr7zyCnfddRdHH22+tsKpGmzbJplM8uSTT7Ljjjsyc+ZMFi9ezNVXX83111/P008/zdlnn41Sij/84Q/84he/4OWXX6atrY1HH32U1tZWjjzySI444giWL1/O6NHmMsbXXXcdd9xxB5dffjnz589n5syZnHXWWdx///2MGjWKn/zkJ8ybN4/nnnuON998k/r6eq666ipWrlzJoYceyrRp0wCYOXMm++yzD/feey91dXVcdtllfPLJJxx77LH89re/LdtyMWBFdB4uMfAMrAPletLiv+uu7l9z8snmXw8sX76cP/3pTyxdupS77rqLlStXcvfdd3PkkUfyyCOPsGTJEvbcc09c1+Xdd99l1qxZAGSzWcaHPZ4JEya0z2/IkCGcddZZpNNp/vWvf5HL5Xjvvffad4Wsr6+nqakJgLlz5/Lzn/+cW2+9tX1/+mIffPABmUyGSy+9lHw+z2uvvcbpp5+OUoq2tjb+85//sGLFivYV/IgRI3j//fdxHIdYLMbQoUNZsWIF06dPb2/RP/bYY2y11Vak02l23HFHAJLJJJMmTeKee+5h7ty5vPqquQLk6tWrGTFiBJ7nUVVVRTKZbN82MHfuXN58802OO+44AHK5HB999FF77q2trbz55pucdtppnHbaaSxbtoyzzz6bBx54gPfee48ZM2YAMH78eI4//nhmzZrF9OnTAchkMkycOJH58+ev8/nOnTuXlpYWXn/99faYS5cupb6+vkffdcV4882oMxADxMAqEBF47LHHOOywwzjrLHOp4dWrV7PXXntx9tlnc+WVV7J48WIuuOACwKyorrjiCkaOHElLSwtLliwBzK6TAJ9++ik//elPee655wA4/PDDyefzNDU18dhjj3HEEUewZMkS3nvvPf79738zYsQIDjvsMDbffHM++OADXn/9dd566y183yefz/Piiy/yzDPP8J3vfAfXdVm1ahXxeJx58+axZs0aFi1axMqVK3EchxUrVpDNZvn000/J5/M4jsPYsWMZPnw4K1asYPPNN+fpp59mwoQJpNNpli5dSj6fZ9WqVXz44YcANDU1ceCBB3LAAQfw8ccft/eKLMtaZ+Ox7/s0NTUxdepULr74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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# NOTE: Silhouette Scores doesn't have a quick method\n", + "visualizer = SilhouetteVisualizer(unfitted_cluster_model)\n", + "visualizer.fit(X)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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jdva2sqcOR3PjF865SwQczGXVwyAuUqpjzGHhHe8FvOAcMstt3DHHXfwzDPPRL2/8zpuvvlmXnjhBXK5HJZlcdZZZzFlyhQuvfRSTjjhBB5++GEaGhpoamri/vvv5yc/6XhRt033yiuvMGnSJOrq6nj22WeZMWMGrtv9ovXuu+/yq1/9iiAIWLNmDVOnTuVLX/pSp68t7MlTaOkXxuuLh3oKK/rCwVyFvXoK2wKiOnhLiIGqnAXiRMwlEb8ZXlWtmjId7dcXHnvsMfbff39++9vfcuihh0adDgDvvPMOzz77LPfddx+WZfGPf/yDs846i8cee4wf/OAHfZbHU089xahRo6irq+PRRx9ll116cm14uP322/n617/OiBEj8H2fiy++mK233pqJEyeu09L3PA/bttv34CmM7xeGgIQQ5VHOAnEb5mL3f8FcivHEroaXemT8+M4fP/NMOO00c/u44+DPnZzWado0uP9+c/uWW+DSS2HevB6Fffnllxk7dixHHXUUZ555JrvvvjvHHHMMv/vd77Asi4suuoiddtqJsWPHcskllwBQW1vLZZddxltvvcVVV11FLBbjyCOPJJFIcM8995DL5Vi1ahW33347dXV1zJo1izlz5tDQ0MBHH33ETTfdhOM4nHfeebS2thKPx7n44ovXGZ6pqqpiwYIFPPTQQ8yYMYMtttiChx56KPwYjuPCCy9c53188MEHnHTSScybN48dd9yRww47jHnz5nHHHXe0b3g96aSTCIKA6667josuugiA888/nzPOOIN0Os3NN9/MypXmMrdf+cpX+Pjjj/nggw+46aabmDFjBsuXL+f666/nu9/9Lg888ABvv/02vu9z8MEHs9tuu7F8+XKGDx+O4zgMHz6clpYWDjnkEKZMmcJvfvMb4vE4a9as4eyzz2bBggVks1nOO+88pkyZwtlnn82HH35IPp/nhBNOYP/99+e4446jvr6e5cuXc/PNN3PhhRfywQcf4Ps+3/72t5k6dSpCiN4pW4EILwF6dLnm35cefPBBjjjiCJqamvA8j/nz56OU4tVXX2Wbbbbh5Zdf5pxzzuHoo4/msssuY9KkSTz44IPceuutTJ8+ndbWVh588EEAfvazn3HzzTeTTCY59dRT+ctf/kIqlWLZsmU89NBDLF26lC98wVzC9oorruC4445jt91248UXX+Sqq67i6quvbs9r2LBh3HTTTdx9993ccMMNJBIJvvOd77Dvvvt2+j5aW1u58cYbefrppznvvPM47LDDuPXWWznppJMYN24cr7zyCnfddRdHH22+tsKpGmzbJplM8uSTT7Ljjjsyc+ZMFi9ezNVXX83111/P008/zdlnn41Sij/84Q/84he/4OWXX6atrY1HH32U1tZWjjzySI444giWL1/O6NHmMsbXXXcdd9xxB5dffjnz589n5syZnHXWWdx///2MGjWKn/zkJ8ybN4/nnnuON998k/r6eq666ipWrlzJoYceyrRp0wCYOXMm++yzD/feey91dXVcdtllfPLJJxx77LH89re/LdtyMWBFdB4uMfAMrAPletLiv+uu7l9z8snmXw8sX76cP/3pTyxdupS77rqLlStXcvfdd3PkkUfyyCOPsGTJEvbcc09c1+Xdd99l1qxZAGSzWcaHPZ4JEya0z2/IkCGcddZZpNNp/vWvf5HL5Xjvvffad4Wsr6+nqakJgLlz5/Lzn/+cW2+9tX1/+mIffPABmUyGSy+9lHw+z2uvvcbpp5+OUoq2tjb+85//sGLFivYV/IgRI3j//fdxHIdYLMbQoUNZsWIF06dPb2/RP/bYY2y11Vak02l23HFHAJLJJJMmTeKee+5h7ty5vPqquQLk6tWrGTFiBJ7nUVVVRTKZbN82MHfuXN58802OO+44AHK5HB999FF77q2trbz55pucdtppnHbaaSxbtoyzzz6bBx54gPfee48ZM2YAMH78eI4//nhmzZrF9OnTAchkMkycOJH58+ev8/nOnTuXlpYWXn/99faYS5cupb6+vkffdcV4882oMxADxMAqEBF47LHHOOywwzjrLHOp4dWrV7PXXntx9tlnc+WVV7J48WIuuOACwKyorrjiCkaOHElLSwtLliwBzK6TAJ9++ik//elPee655wA4/PDDyefzNDU18dhjj3HEEUewZMkS3nvvPf79738zYsQIDjvsMDbffHM++OADXn/9dd566y183yefz/Piiy/yzDPP8J3vfAfXdVm1ahXxeJx58+axZs0aFi1axMqVK3EchxUrVpDNZvn000/J5/M4jsPYsWMZPnw4K1asYPPNN+fpp59mwoQJpNNpli5dSj6fZ9WqVXz44YcANDU1ceCBB3LAAQfw8ccft/eKLMtaZ+Ox7/s0NTUxdepULr74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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualizer = SilhouetteVisualizer(fitted_cluster_model)\n", + "visualizer.fit(X)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualizer = InterclusterDistance(unfitted_cluster_model)\n", + "visualizer.fit(X)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "visualizer = InterclusterDistance(fitted_cluster_model)\n", + "visualizer.fit(X)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Check if fitted on Model Selection Visualizers\n", + "\n", + "_NOTE: Not sure how to proceed with multi-model visualizers -- is already fitted a real use case here?_" + ] + } + ], + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/rebeccabilbro/tsne_resolve_colors.ipynb b/examples/rebeccabilbro/tsne_resolve_colors.ipynb new file mode 100644 index 000000000..def562d7b --- /dev/null +++ b/examples/rebeccabilbro/tsne_resolve_colors.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## TSNE Resolve Colors Bug Documentation\n", + "\n", + "Jerome Massot [reports](https://github.com/DistrictDataLabs/yellowbrick/pull/658) that there is a bug in TSNE that means that colors that are passed in on instantiation do not affect the colors of the plot.\n", + "\n", + "In this example, we'll validate that the bug exists, and that his proposed solution works" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys \n", + "\n", + "# Modify the path \n", + "sys.path.append(\"..\")\n", + "\n", + "import pandas as pd\n", + "import yellowbrick as yb\n", + "import matplotlib.pyplot as plt " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Validate Bug" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from download import download_all \n", + "from sklearn.datasets.base import Bunch\n", + "\n", + "## The path to the test data sets\n", + "FIXTURES = os.path.join(os.getcwd(), \"data\")\n", + "\n", + "## Dataset loading mechanisms\n", + "datasets = {\n", + " \"hobbies\": os.path.join(FIXTURES, \"hobbies\")\n", + "}\n", + "\n", + "\n", + "def load_data(name, download=True):\n", + " \"\"\"\n", + " Loads and wrangles the passed in text corpus by name.\n", + " If download is specified, this method will download any missing files. \n", + " \"\"\"\n", + " \n", + " # Get the path from the datasets \n", + " path = datasets[name]\n", + " \n", + " # Check if the data exists, otherwise download or raise \n", + " if not os.path.exists(path):\n", + " if download:\n", + " download_all() \n", + " else:\n", + " raise ValueError((\n", + " \"'{}' dataset has not been downloaded, \"\n", + " \"use the download.py module to fetch datasets\"\n", + " ).format(name))\n", + " \n", + " # Read the directories in the directory as the categories. \n", + " categories = [\n", + " cat for cat in os.listdir(path) \n", + " if os.path.isdir(os.path.join(path, cat))\n", + " ]\n", + " \n", + " \n", + " files = [] # holds the file names relative to the root \n", + " data = [] # holds the text read from the file \n", + " target = [] # holds the string of the category \n", + " \n", + " # Load the data from the files in the corpus \n", + " for cat in categories:\n", + " for name in os.listdir(os.path.join(path, cat)):\n", + " files.append(os.path.join(path, cat, name))\n", + " target.append(cat)\n", + " \n", + " with open(os.path.join(path, cat, name), 'r') as f:\n", + " data.append(f.read())\n", + " \n", + " \n", + " # Return the data bunch for use similar to the newsgroups example\n", + " return Bunch(\n", + " categories=categories,\n", + " files=files,\n", + " data=data,\n", + " target=target,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.feature_extraction.text import TfidfVectorizer\n", + "\n", + "corpus = load_data('hobbies')\n", + "tfidf = TfidfVectorizer()\n", + "\n", + "docs = tfidf.fit_transform(corpus.data)\n", + "labels = corpus.target" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from yellowbrick.text import TSNEVisualizer\n", + "\n", + "tsne = TSNEVisualizer(colors=[\"purple\",\"blue\",\"orchid\",\"indigo\",\"plum\",\"navy\"])\n", + "tsne.fit(docs, labels)\n", + "tsne.poof()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Validate Solution" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from collections import defaultdict\n", + "\n", + "from yellowbrick.draw import manual_legend\n", + "from yellowbrick.text.base import TextVisualizer\n", + "from yellowbrick.style.colors import resolve_colors\n", + "from yellowbrick.exceptions import YellowbrickValueError\n", + "\n", + "from sklearn.manifold import TSNE\n", + "from sklearn.pipeline import Pipeline\n", + "from sklearn.decomposition import TruncatedSVD, PCA\n", + "\n", + "##########################################################################\n", + "## Quick Methods\n", + "##########################################################################\n", + "\n", + "def tsne(X, y=None, ax=None, decompose='svd', decompose_by=50, classes=None,\n", + " colors=None, colormap=None, alpha=0.7, **kwargs):\n", + " \"\"\"\n", + " Display a projection of a vectorized corpus in two dimensions using TSNE,\n", + " a nonlinear dimensionality reduction method that is particularly well\n", + " suited to embedding in two or three dimensions for visualization as a\n", + " scatter plot. TSNE is widely used in text analysis to show clusters or\n", + " groups of documents or utterances and their relative proximities.\n", + "\n", + " Parameters\n", + " ----------\n", + "\n", + " X : ndarray or DataFrame of shape n x m\n", + " A matrix of n instances with m features representing the corpus of\n", + " vectorized documents to visualize with tsne.\n", + "\n", + " y : ndarray or Series of length n\n", + " An optional array or series of target or class values for instances.\n", + " If this is specified, then the points will be colored according to\n", + " their class. Often cluster labels are passed in to color the documents\n", + " in cluster space, so this method is used both for classification and\n", + " clustering methods.\n", + "\n", + " ax : matplotlib axes\n", + " The axes to plot the figure on.\n", + "\n", + " decompose : string or None\n", + " A preliminary decomposition is often used prior to TSNE to make the\n", + " projection faster. Specify `\"svd\"` for sparse data or `\"pca\"` for\n", + " dense data. If decompose is None, the original data set will be used.\n", + "\n", + " decompose_by : int\n", + " Specify the number of components for preliminary decomposition, by\n", + " default this is 50; the more components, the slower TSNE will be.\n", + "\n", + " classes : list of strings\n", + " The names of the classes in the target, used to create a legend.\n", + "\n", + " colors : list or tuple of colors\n", + " Specify the colors for each individual class\n", + "\n", + " colormap : string or matplotlib cmap\n", + " Sequential colormap for continuous target\n", + "\n", + " alpha : float, default: 0.7\n", + " Specify a transparency where 1 is completely opaque and 0 is completely\n", + " transparent. This property makes densely clustered points more visible.\n", + "\n", + " kwargs : dict\n", + " Pass any additional keyword arguments to the TSNE transformer.\n", + "\n", + " Returns\n", + " -------\n", + " ax : matplotlib axes\n", + " Returns the axes that the parallel coordinates were drawn on.\n", + " \"\"\"\n", + " # Instantiate the visualizer\n", + " visualizer = TSNEVisualizer(\n", + " ax, decompose, decompose_by, classes, colors, colormap, alpha, **kwargs\n", + " )\n", + "\n", + " # Fit and transform the visualizer (calls draw)\n", + " visualizer.fit(X, y, **kwargs)\n", + " visualizer.transform(X)\n", + "\n", + " # Return the axes object on the visualizer\n", + " return visualizer.ax\n", + "\n", + "\n", + "##########################################################################\n", + "## TSNEVisualizer\n", + "##########################################################################\n", + "\n", + "class TSNEVisualizer(TextVisualizer):\n", + " \"\"\"\n", + " Display a projection of a vectorized corpus in two dimensions using TSNE,\n", + " a nonlinear dimensionality reduction method that is particularly well\n", + " suited to embedding in two or three dimensions for visualization as a\n", + " scatter plot. TSNE is widely used in text analysis to show clusters or\n", + " groups of documents or utterances and their relative proximities.\n", + "\n", + " TSNE will return a scatter plot of the vectorized corpus, such that each\n", + " point represents a document or utterance. The distance between two points\n", + " in the visual space is embedded using the probability distribution of\n", + " pairwise similarities in the higher dimensionality; thus TSNE shows\n", + " clusters of similar documents and the relationships between groups of\n", + " documents as a scatter plot.\n", + "\n", + " TSNE can be used with either clustering or classification; by specifying\n", + " the ``classes`` argument, points will be colored based on their similar\n", + " traits. For example, by passing ``cluster.labels_`` as ``y`` in ``fit()``, all\n", + " points in the same cluster will be grouped together. This extends the\n", + " neighbor embedding with more information about similarity, and can allow\n", + " better interpretation of both clusters and classes.\n", + "\n", + " For more, see https://lvdmaaten.github.io/tsne/\n", + "\n", + " Parameters\n", + " ----------\n", + "\n", + " ax : matplotlib axes\n", + " The axes to plot the figure on.\n", + "\n", + " decompose : string or None, default: ``'svd'``\n", + " A preliminary decomposition is often used prior to TSNE to make the\n", + " projection faster. Specify ``\"svd\"`` for sparse data or ``\"pca\"`` for\n", + " dense data. If None, the original data set will be used.\n", + "\n", + " decompose_by : int, default: 50\n", + " Specify the number of components for preliminary decomposition, by\n", + " default this is 50; the more components, the slower TSNE will be.\n", + "\n", + " labels : list of strings\n", + " The names of the classes in the target, used to create a legend.\n", + " Labels must match names of classes in sorted order.\n", + "\n", + " colors : list or tuple of colors\n", + " Specify the colors for each individual class\n", + "\n", + " colormap : string or matplotlib cmap\n", + " Sequential colormap for continuous target\n", + "\n", + " random_state : int, RandomState instance or None, optional, default: None\n", + " If int, random_state is the seed used by the random number generator;\n", + " If RandomState instance, random_state is the random number generator;\n", + " If None, the random number generator is the RandomState instance used\n", + " by np.random. The random state is applied to the preliminary\n", + " decomposition as well as tSNE.\n", + "\n", + " alpha : float, default: 0.7\n", + " Specify a transparency where 1 is completely opaque and 0 is completely\n", + " transparent. This property makes densely clustered points more visible.\n", + "\n", + " kwargs : dict\n", + " Pass any additional keyword arguments to the TSNE transformer.\n", + " \"\"\"\n", + "\n", + " # NOTE: cannot be np.nan\n", + " NULL_CLASS = None\n", + "\n", + " def __init__(self, ax=None, decompose='svd', decompose_by=50,\n", + " labels=None, classes=None, colors=None, colormap=None,\n", + " random_state=None, alpha=0.7, **kwargs):\n", + "\n", + " # Visual Parameters\n", + " self.alpha = alpha\n", + " self.labels = labels\n", + " self.colors = colors\n", + " self.colormap = colormap\n", + " self.random_state = random_state\n", + "\n", + " # Fetch TSNE kwargs from kwargs by popping only keys belonging to TSNE params\n", + " tsne_kwargs = {\n", + " key: kwargs.pop(key)\n", + " for key in TSNE().get_params()\n", + " if key in kwargs\n", + " }\n", + " self.transformer_ = self.make_transformer(decompose, decompose_by, tsne_kwargs)\n", + "\n", + " # Call super at the end so that size and title are set correctly\n", + " super(TSNEVisualizer, self).__init__(ax=ax, **kwargs)\n", + "\n", + " def make_transformer(self, decompose='svd', decompose_by=50, tsne_kwargs={}):\n", + " \"\"\"\n", + " Creates an internal transformer pipeline to project the data set into\n", + " 2D space using TSNE, applying an pre-decomposition technique ahead of\n", + " embedding if necessary. This method will reset the transformer on the\n", + " class, and can be used to explore different decompositions.\n", + "\n", + " Parameters\n", + " ----------\n", + "\n", + " decompose : string or None, default: ``'svd'``\n", + " A preliminary decomposition is often used prior to TSNE to make\n", + " the projection faster. Specify ``\"svd\"`` for sparse data or ``\"pca\"``\n", + " for dense data. If decompose is None, the original data set will\n", + " be used.\n", + "\n", + " decompose_by : int, default: 50\n", + " Specify the number of components for preliminary decomposition, by\n", + " default this is 50; the more components, the slower TSNE will be.\n", + "\n", + " Returns\n", + " -------\n", + "\n", + " transformer : Pipeline\n", + " Pipelined transformer for TSNE projections\n", + " \"\"\"\n", + "\n", + " # TODO: detect decompose by inferring from sparse matrix or dense or\n", + " # If number of features > 50 etc.\n", + " decompositions = {\n", + " 'svd': TruncatedSVD,\n", + " 'pca': PCA,\n", + " }\n", + "\n", + " if decompose and decompose.lower() not in decompositions:\n", + " raise YellowbrickValueError(\n", + " \"'{}' is not a valid decomposition, use {}, or None\".format(\n", + " decompose, \", \".join(decompositions.keys())\n", + " )\n", + " )\n", + "\n", + " # Create the pipeline steps\n", + " steps = []\n", + "\n", + " # Add the pre-decomposition\n", + " if decompose:\n", + " klass = decompositions[decompose]\n", + " steps.append((decompose, klass(\n", + " n_components=decompose_by, random_state=self.random_state)))\n", + "\n", + " # Add the TSNE manifold\n", + " steps.append(('tsne', TSNE(\n", + " n_components=2, random_state=self.random_state, **tsne_kwargs)))\n", + "\n", + " # return the pipeline\n", + " return Pipeline(steps)\n", + "\n", + " def fit(self, X, y=None, **kwargs):\n", + " \"\"\"\n", + " The fit method is the primary drawing input for the TSNE projection\n", + " since the visualization requires both X and an optional y value. The\n", + " fit method expects an array of numeric vectors, so text documents must\n", + " be vectorized before passing them to this method.\n", + "\n", + " Parameters\n", + " ----------\n", + " X : ndarray or DataFrame of shape n x m\n", + " A matrix of n instances with m features representing the corpus of\n", + " vectorized documents to visualize with tsne.\n", + "\n", + " y : ndarray or Series of length n\n", + " An optional array or series of target or class values for\n", + " instances. If this is specified, then the points will be colored\n", + " according to their class. Often cluster labels are passed in to\n", + " color the documents in cluster space, so this method is used both\n", + " for classification and clustering methods.\n", + "\n", + " kwargs : dict\n", + " Pass generic arguments to the drawing method\n", + "\n", + " Returns\n", + " -------\n", + " self : instance\n", + " Returns the instance of the transformer/visualizer\n", + " \"\"\"\n", + "\n", + " # Store the classes we observed in y\n", + " if y is not None:\n", + " self.classes_ = np.unique(y)\n", + " elif y is None and self.labels is not None:\n", + " self.classes_ = np.array([self.labels[0]])\n", + " else:\n", + " self.classes_ = np.array([self.NULL_CLASS])\n", + "\n", + " # Fit our internal transformer and transform the data.\n", + " vecs = self.transformer_.fit_transform(X)\n", + " self.n_instances_ = vecs.shape[0]\n", + "\n", + " # Draw the vectors\n", + " self.draw(vecs, y, **kwargs)\n", + "\n", + " # Fit always returns self.\n", + " return self\n", + "\n", + " def draw(self, points, target=None, **kwargs):\n", + " \"\"\"\n", + " Called from the fit method, this method draws the TSNE scatter plot,\n", + " from a set of decomposed points in 2 dimensions. This method also\n", + " accepts a third dimension, target, which is used to specify the colors\n", + " of each of the points. If the target is not specified, then the points\n", + " are plotted as a single cloud to show similar documents.\n", + " \"\"\"\n", + " # Resolve the labels with the classes\n", + " labels = self.labels if self.labels is not None else self.classes_\n", + " if len(labels) != len(self.classes_):\n", + " raise YellowbrickValueError((\n", + " \"number of supplied labels ({}) does not \"\n", + " \"match the number of classes ({})\"\n", + " ).format(len(labels), len(self.classes_)))\n", + "\n", + "\n", + " # Create the color mapping for the labels.\n", + " self.color_values_ = resolve_colors(\n", + " n_colors=len(labels), colormap=self.colormap, colors=self.colors)\n", + " colors = dict(zip(labels, self.color_values_))\n", + "\n", + " # Transform labels into a map of class to label\n", + " labels = dict(zip(self.classes_, labels))\n", + "\n", + " # Expand the points into vectors of x and y for scatter plotting,\n", + " # assigning them to their label if the label has been passed in.\n", + " # Additionally, filter classes not specified directly by the user.\n", + " series = defaultdict(lambda: {'x':[], 'y':[]})\n", + "\n", + " if target is not None:\n", + " for t, point in zip(target, points):\n", + " label = labels[t]\n", + " series[label]['x'].append(point[0])\n", + " series[label]['y'].append(point[1])\n", + " else:\n", + " label = self.classes_[0]\n", + " for x,y in points:\n", + " series[label]['x'].append(x)\n", + " series[label]['y'].append(y)\n", + "\n", + " # Plot the points\n", + " for label, points in series.items():\n", + " self.ax.scatter(\n", + " points['x'], points['y'], c=colors[label],\n", + " alpha=self.alpha, label=label\n", + " )\n", + "\n", + " def finalize(self, **kwargs):\n", + " \"\"\"\n", + " Finalize the drawing by adding a title and legend, and removing the\n", + " axes objects that do not convey information about TNSE.\n", + " \"\"\"\n", + " self.set_title(\n", + " \"TSNE Projection of {} Documents\".format(self.n_instances_)\n", + " )\n", + "\n", + " # Remove the ticks\n", + " self.ax.set_yticks([])\n", + " self.ax.set_xticks([])\n", + "\n", + " # Add the legend outside of the figure box.\n", + " if not all(self.classes_ == np.array([self.NULL_CLASS])):\n", + " box = self.ax.get_position()\n", + " self.ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])\n", + " manual_legend(\n", + " self, self.classes_, self.color_values_,\n", + " loc='center left', bbox_to_anchor=(1, 0.5)\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tsne = TSNEVisualizer(colors=[\"purple\",\"blue\",\"orchid\",\"indigo\",\"plum\",\"navy\"])\n", + "tsne.fit(docs, labels)\n", + "tsne.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/regression.ipynb b/examples/regression.ipynb index 180153532..6ceb97962 100644 --- a/examples/regression.ipynb +++ b/examples/regression.ipynb @@ -20,7 +20,7 @@ "import matplotlib as mpl \n", "import matplotlib.pyplot as plt \n", "\n", - "from sklearn.linear_model import Lasso, LassoCV, Ridge, RidgeCV\n", + "from sklearn.linear_model import Lasso, LassoCV, Ridge\n", "from sklearn.model_selection import cross_val_predict, train_test_split\n", "\n", "from yellowbrick.datasets import load_concrete\n", @@ -63,28 +63,10 @@ "outputs": [], "source": [ "# Use Yellowbrick to load the concrete dataset\n", - "data = load_concrete()\n", - "\n", - "# Save the data in a Pandas DataFrame\n", - "df = pd.DataFrame(data['data'], columns=data['feature_names'], dtype='float')" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# Save feature names as a list and target variable as a string\n", - "feature_names = ['cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age']\n", - "target_name = 'strength'\n", - "\n", - "# Get the X and y data from the DataFrame \n", - "X = df[feature_names]\n", - "y = df[target_name]\n", + "X, y = load_concrete()\n", "\n", "# Create the train and test data \n", - "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)" + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)" ] }, { @@ -100,12 +82,12 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGACAYAAABBbw0iAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvhp/UCwAAIABJREFUeJzsvXl8VPW9//88y8xkm6yEJBD2JchiABdcUVEUKyJuVcHaX1vx2tZ66be2LojctoobtrfaW3ptbetSl+q1SKutVaviQqkCCgQhyBYSkkDINpn1bL8/hhmyTDIzyWSZ5PN8PPoombN9PmfG83md9ypZlmUhEAgEAoFAMASR+3sAAoFAIBAIBP2FEEICgUAgEAiGLEIICQQCgUAgGLIIISQQCAQCgWDIIoSQQCAQCASCIYsQQgKBQCAQCIYsQggJhgwlJSVcfvnlXHHFFSxevJhLLrmEq6++mu3bt3f7nCtWrODjjz/u8Pn27duZN29et89bWVnJrFmz4jpm7dq1nH/++dx9993dvu4TTzzBGWecwRVXXMEVV1zBokWLmDdvHg8++CChShtXXHEFzc3NHY596qmnuOuuu7p97fbMmzePmTNn4na723z+5z//mZKSEv7+97/Hdb5Yx1dSUkJ9fX1c5xYIBMmL2t8DEAj6kqeffprc3Nzw30899RT3338/L730UrfO98ADDyRqaD3mlVdeYc2aNZx66qk9Os9XvvIV7rvvvvDfTU1NLFq0iHPOOYdzzz2X1157radDjZmcnBzeeustFi9eHP7sz3/+M8OGDeuzMQgEgsGNEEKCIYuu61RXV5OVlRX+bO3atfzjH//ANE1GjhzJqlWrKCgo4B//+Adr165FkiQUReFHP/oRp512Gl/72tdYunQpCxYs4Pnnn+fpp58mIyODyZMnh8/5xBNP0NDQEBYXrf/+7LPPePTRRwkEAhw9epSzzjqL1atXtxnn3r17WbFiBYFAAMuyuOaaa1i6dGmbfZYvX05tbS0rVqzgP//zP5k9ezb/9V//RVVVFZZlsXjxYm6++WYqKytZunQpEyZMoKqqimeffZbhw4d3eZ/q6urw+Xzh+1RSUsLGjRtxOp3cf//9fPzxx+Tl5ZGXl4fT6QTg4MGD3HPPPTQ1NZGfn49lWSxatIirrrqKLVu2sGbNGrxeL5Ik8b3vfY8LLrgg4rUXLVrE+vXrw0KoqqoKj8fD+PHjw/t8+umnPPLII3i9Xmw2G8uXL2fu3Llomtbp+FwuFw888ADl5eVomsaZZ57Jj370I1RVPBIFgqGG+K9eMKT4+te/jiRJ1NfX43A4uOCCC3jwwQcBWLduHeXl5bz88suoqspLL73Evffey29+8xseeeQR1qxZw8yZM/nwww/ZtGkTp512Wvi8X3zxBb/85S957bXXyM/Pb2NR6YpnnnmG22+/nTlz5uB2u7nwwgvZsWMH2dnZ4X2eeuop5s2bxy233MLRo0dZvXo1N9xwA7J8wrP93//938ybN481a9YwY8YMbrzxRi688EK+8Y1v4HK5WLp0KUVFRZSWllJTU8Njjz3WqeXojTfeYPPmzfh8PhobG5k6dSo//vGPOfnkk9vs9/zzz3PgwAFef/11dF3nxhtvDAuNH/3oR1xxxRUsWbKEvXv3cvXVV7No0SKampq4++67eeqppyguLqa2tpavfvWrlJSUMGLEiA5jOe+88/jTn/7EkSNHGD58OK+99hqLFy/mzTffBKChoYHbb7+dtWvXUlpayp49e7jxxht55ZVX+Oc//9np+FavXs20adN46KGHMAyDu+66i9///vcsW7Yspu9NIBAMHoQQEgwpQq6xnTt3smzZMmbNmkVeXh4A7777Ltu3b+fqq68GwDRNvF4vAJdddhm33XYb5513HmeffXaHBXPjxo2cffbZ5OfnA3Ddddfx4YcfRh3PQw89xIYNG/j1r3/Nvn378Pl8eDyeNkJo/vz53HnnnWzbto0zzzyTe++9t40Iao/H42HLli387ne/A8DpdHLVVVexYcMGSktLUVWVmTNndnp8yDUWCAT46U9/yp49e5g7d26H/TZu3MjChQux2+3Y7XYuv/xydu/eTVNTE9u2beO5554DYMKECZxxxhkAfPbZZxw9epTvfve74fNIksTu3bsjCiGbzcaCBQv461//yje/+U3eeOMNnnvuubAQ2rZtG6NHj6a0tBSASZMmMXv2bP797393Oj6A9957j+3bt/PKK68A4PP5Or0fAoFgcCOEkGBIMnXqVO6++27uvfdeSktLKS4uxjRNbr75ZpYsWQJAIBCgqakJgO9///tcc801fPjhh7z66qs8+eSTvPrqq+HzSZJE67Z9iqJ0uk3TtPC/ly5dypQpUzj33HO59NJL+fzzz2nf/u+CCy7gzTff5OOPP2bjxo38z//8Dy+++CKjR4+OODfTNDucwzRNdF0HwG63x+QCstvtrFy5kquvvppHH32UVatWdbl/aM6h/490PwzDYMKECbz88svhbbW1tW3ittqzePFiVq1axcyZMxk/fnwbkWiaZof9LcsKzzXS+ELH/eIXv2DChAkANDc3I0lSl/MTCASDE5E1JhiyLFy4kJkzZ4Zjcs455xxeeeUVWlpaAPjFL37Bj370I3RdZ968eXg8Hm644QZWrVrF3r172yy2Z511Fh999BE1NTVAMKA3RE5ODmVlZViWhcfjCVuKmpqa2LFjB3fccQcXX3wxtbW1VFRUdFjcf/CDH/DGG29w2WWXsWrVKjIyMqiuru50XhkZGZSWlvLHP/4RCMbDrFu3jrPOOivue2S321m1ahUvvfQSZWVlbbade+65rFu3Dr/fj9/v54033ghff/bs2WGheOjQITZu3IgkScycOZODBw/yySefAEGX4iWXXMKRI0c6HUNpaSk+n4+f//znXHnllR227d+/n23btgGwZ88ePvnkE04//fROxwfB7/oPf/gDlmURCAT49re/HbZgCQSCoYWwCAmGNCtXrmTRokV88MEHXHvtteGYFUmSKCoq4qGHHkJVVe655x7uuOMOVFVFkiRWr16N3W4Pn6ekpIQf/vCHfP3rXyc9Pb1NPE3o/BdffDEFBQXMmjULy7LIysrilltu4corryQ7O5ucnBxmz57NwYMHGTVqVPj473znO6xYsYKXXnoJRVG46KKLOP3007uc15o1a/jJT37Cq6++SiAQ4PLLL+eqq66iqqoq7nt06qmncvnll/PTn/6UF154Ifz59ddfT0VFBQsXLiQ7O5sxY8aEtz388MOsWLGC559/noKCAoqLi0lJSSE3N5fHH3+cRx55BL/fj2VZPPLII4wcObLLMVxxxRX88Y9/5Nxzz23zeW5uLr/4xS/46U9/is/nQ5IkHnzwQcaNG8fo0aM7Hd+KFSt44IEHuPzyy9E0jbPOOoubb7457nsjEAiSH8lqb0MXCASCHrJ27VouvvhiJkyYgMvlYtGiRfzmN79h4sSJ/T00gUAgaIOwCAkEgoQzduxYvv/97yPLMoZhsGzZMiGCBALBgERYhAQCgUAgEAxZRLC0QCAQCASCIYsQQgKBQCAQCIYs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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGACAYAAABBbw0iAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzsnXl8VOW9/9/nnNmSyUoICZCA7GDAIO6IUVDRe5VFcQO0/bVVuyHFalUE5baKuNaKbem111pb9erVWrWtr24USVFERBSJQJA9mISsk2T2Oef8/hhmmEkmyUwyM8kkz/v16qtmzvY85wzzfM53lXRd1xEIBAKBQCAYhMh9PQCBQCAQCASCvkIIIYFAIBAIBIMWIYQEAoFAIBAMWoQQEggEAoFAMGgRQkggEAgEAsGgRQghgUAgEAgEgxYhhASDhkmTJjFv3jwWLFjAwoULueKKK1i0aBGff/55j8+5atUqPvjggw6ff/7558yZM6fH562qquLMM8+M6ZgNGzZwySWXsHLlyh5f99lnn+X8889nwYIFLFiwgPnz5zNnzhzWrVtHoNLGggULaGlp6XDs888/z3333dfja7dnzpw5TJ8+HbvdHvb5H//4RyZNmsRf//rXmM4X7fgmTZpEY2NjTOcWCASpi6GvByAQJJMXX3yRIUOGBP9+/vnnefjhh3nttdd6dL61a9fGa2i95o033uDJJ5/k7LPP7tV5/vM//5MHH3ww+LfNZmP+/PnMmjWLiy66iLfffru3Q42a3Nxc/vGPf7Bw4cLgZ3/84x8ZOnRo0sYgEAgGNkIICQYtPp+P6upqsrOzg59t2LCBv//972iaxsiRI1mzZg0FBQX8/e9/Z8OGDUiShKIo3HPPPZxzzjnccsstLF26lCuvvJJXXnmFF198kYyMDCZOnBg857PPPktTU1NQXIT+/emnn/LEE0/g8Xioq6tj5syZPPLII2HjPHDgAKtWrcLj8aDrOtdddx1Lly4N22fFihXU1tayatUqfvCDHzBjxgz+67/+i+PHj6PrOgsXLuTWW2+lqqqKpUuXMm7cOI4fP87vf/97hg0b1uV9qq+vx+VyBe/TpEmT2Lp1K5mZmTz88MN88MEH5OXlkZeXR2ZmJgBHjhzh/vvvx2azkZ+fj67rzJ8/n2uvvZZPPvmEJ598EqfTiSRJ3HHHHcyePTvitefPn88777wTFELHjx/H4XAwduzY4D4ff/wxjz/+OE6nE6PRyIoVKygrK8Pr9XY6vtbWVtauXUtlZSVer5cLLriAe+65B4NB/CQKBIMN8a9eMKj4+te/jiRJNDY2YjabmT17NuvWrQPgrbfeorKyktdffx2DwcBrr73G6tWr+fWvf83jjz/Ok08+yfTp09myZQvbtm3jnHPOCZ53z549/PznP+ftt98mPz8/zKLSFb/73e9Yvnw55513Hna7nUsvvZTdu3eTk5MT3Of5559nzpw53H777dTV1fHII4+wePFiZPmUZ/tnP/sZc+bM4cknn2TatGncfPPNXHrppXzjG9+gtbWVpUuXMnz4cEpLS6mpqeGpp57q1HL07rvvsmPHDlwuF83NzZx++un8+Mc/5owzzgjb75VXXuHw4cP85S9/wefzcfPNNweFxj333MOCBQtYsmQJBw4cYNGiRcyfPx+bzcbKlSt5/vnnKSoqora2lhtuuIFJkyYxYsSIDmO5+OKL+b//+z9OnDjBsGHDePvtt1m4cCF/+9vfAGhqamL58uVs2LCB0tJS9u/fz80338wbb7zBv/71r07H98gjj1BSUsKjjz6Kqqrcd999vPDCC9x2221RPTeBQDBwEEJIMKgIuMa++OILbrvtNs4880zy8vIA2LRpE59//jmLFi0CQNM0nE4nAFdddRXLli3j4osv5sILL+ywYG7dupULL7yQ/Px8AG688Ua2bNnS7XgeffRRysvL+dWvfsXBgwdxuVw4HI4wIXT55Zdz7733smvXLi644AJWr14dJoLa43A4+OSTT/jNb34DQGZmJtdeey3l5eWUlpZiMBiYPn16p8cHXGMej4eHHnqI/fv3U1ZW1mG/rVu3cvXVV2MymTCZTMybN499+/Zhs9nYtWsXL730EgDjxo3j/PPPB+DTTz+lrq6O73//+8HzSJLEvn37Igoho9HIlVdeyZ///Ge++c1v8u677/LSSy8FhdCuXbsYNWoUpaWlAEyYMIEZM2bw0UcfdTo+gPfee4/PP/+cN954AwCXy9Xp/RAIBAMbIYQEg5LTTz+dlStXsnr1akpLSykqKkLTNG699VaWLFkCgMfjwWazAXDnnXdy3XXXsWXLFt58802ee+453nzzzeD5JEkitG2foiidbvN6vcH/Xrp0KZMnT+aiiy7i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vJYSIZFLwG63ExISQlhYGLfddhu33HIL9913X7mH7l2olStXEhcXB8D48ePJy8s763tTU1P5+uuvL2j7bdu2Lfe6sLCQzp07n/Y527dvJzw8/IK2JSJndnJfERYWxqBBg4iOjubw4cN8//33zJgx46zr/vLLL0yfPv2MZe+99x7vvfcecOG/xw0bNrBkyZLTtmMV3vd4S5FKcvnll5ORkeF5PX/+fJKSkli4cOFFb/vVV189Z3lWVhY9e/a8qM8ICgpiwIABfPLJJ4wdO9azfPXq1dx+++0XtW0R+dvJfYVpmjz77LNER0fz7rvvcu211551vdzc3LNOUzFy5Mh/XZ/du3dXyHaqKiVCIpWkW7dubNiwAYAbbriBjh07smfPHt59912+/PJLli5ditvtpkOHDsycORN/f39Wr17Nyy+/TFBQEFdccQW1atXyrP/WW2/RqFEjZs2axTfffIOfnx+TJk3C4XCwa9cu4uPjefHFFwkICCAxMZE//viDgIAAEhISaN++PXa7nZiYGIqLi+nUqdMZ63z77beTkpLiSYROnDjBF198QWxsLADPPfccW7Zs4c8//6R+/fosWLCARo0aedYvmxj5oYceKlfvJk2a8Mwzz7B9+3ZcLhcRERGeB/aJeDPDMHjooYe4/vrreeutt8jMzCQtLY0lS5awatUqbDYbHTt2ZPbs2SQnJ2O325k1axY333wzc+fOxe1207p1a5o2bQr8/dtLSEjgu+++o379+syZM4fg4GDuueceoqKi6NmzJ3a7ndGjR7N48WLS09MBCA4OJjc317Odzz//nOeffx63282VV17J7NmzadiwITfccAPDhg1j06ZNHD9+nJSUFEJCQipnB54HDY2JVAKn08maNWvo0qWLZ1m/fv1Yu3YtBQUFLFu2jPT0dDIyMrjssst4/fXXycvLY968ebzzzju8//775eYvKpOWlkZxcTFr1qxhyZIlLFy4kCFDhhASEkJycjJt27YlNjaWmJgYVq1aRVJSkudprklJSURERJCRkVGuXifr0aMHR48e5aeffgJg/fr19OrVi7p165KTk8NPP/1Eeno6a9eupVmzZnz00UfntT+WLVsGwKpVq1i+fDmfffbZBQ/liVhVjRo1aN68OQ0bNgSgpKSERYsWsWLFClauXIlhGOTl5REfH09ISAgzZ84E4MCBAyxdupSUlJTTttm9e3cyMjIYMGAATz755Fk/u1WrVkRGRhIZGVnuzG9+fj5PPPEECxcu5KOPPqJLly7Mnj3bU16vXj2WL19OZGSkZ8qkqkpnhEQukd9//52wsDCg9HHzHTt2ZOrUqZ7ysrMw27ZtIycnhxEjRgClSVP79u359ttv6dy5s6czHDp0KFu3bi33GVlZWYwYMQKbzUajRo345JNPypUXFRWxa9cupk2b5llWXFzMkSNH2L59O/Pnzwdg2LBhxMfHnxaDYRiEh4fz8ccfEx0dTUZGhufMTfPmzYmNjeWDDz4gOzubHTt20KxZs/PaN2WTz5bFU1xczN69e6v9vFUiFcUwDAICAgDw9fWlc+fODB8+nBtvvJG77rqLxo0bc+DAgXLrlE1ueqqAgACGDRsGQFhYGM8///wF1+e7776jY8eOnjNNd955J4sXL/aU9+3bF4DWrVuzbt26C97+paRESOQSOfUaoVP5+/sD4HK5GDx4sCcRKSoqwuVysWXLFtxut+f9Z5rB+tRlOTk5NGnSxPPa7XZTo0aNcvX47bffqFevHlB6PQKUdrqGYZyxnuHh4YwdO5ZRo0aRnZ3tmYBz165dTJ06lTFjxjBo0CBsNhunzuBjGEa5GMpm9Xa5XMTExDBw4EAACgoKPMN+It7O4XCQnZ1Nfn6+Z9lLL73Ejh072LhxI+PGjWPevHmnrVeWOJ3KZvt7MMg0zXL9RtlvtqSk5Jx1Ovl3XLbeyeuU9Wdn60eqEg2NiVQxPXv2JDMzk/z8fEzTJDExkaVLl9K1a1d27txJXl4ebrebTz/99LR1u3fvzpo1azBNk/z8fO6++24cDgc+Pj64XC5q167NVVdd5UmENm/ezF133QVAnz59+PDDDwFYt24dDofjjPULDg4mODiY1NRUwsLCPB1dVlYWPXr0YOTIkbRq1YrNmzfjcrnKrVu/fn32798PlP6jPHToEAC9evVi2bJlOJ1OioqKGDVqFDt37qyAvSlSvbndbhYsWECnTp08Z1gLCgoYPHgwbdq04eGHH+b6669n7969+Pj4/GMCA6VnXD/77DMAVqxYQZ8+fYDyv8/169d73n+m7Xbq1ImdO3dit9sBeP/99y/6hozKojNCIlXMNddcQ1RUFPfeey9ut5t27drxwAMP4O/vT3x8PGPGjKFmzZq0atXqtHVHjRpFcnKy57R3QkICQUFB9O3bl5kzZ5KSksLcuXNJTEzktddew8/Pj+eeew7DMHjiiSeIiYkhPT2da6+9lsDAwLPWMSIigscff5zMzEzPsiFDhhAVFcXQoUPx8/Ojbdu2nk7y5PesXbuWIUOG0KFDB9q3bw9AZGQkOTk5hIeHU1JSQkRERLXtVEUu1snD6GV9wPz589m7dy8ADRo0IDIykuHDh1OzZk2aNGlCeHg4TqeTY8eOERMTw/Dhw8+6/Tp16rB+/XpeeOEFGjduzFNPPQXAuHHjiIuLY8WKFdx4442e93fv3p3Y2FjPsDxAw4YNmT17NlFRUTidToKDg895rVFVptnnRURExGtpaExERES8lhIhERER8VpKhERERMRrKRESERERr6VESERERLyWEiERERHxWkqERERExGspERIRERGv9X8wI+THyeeDogAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -133,12 +115,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAGACAYAAACDX0mmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvhp/UCwAAIABJREFUeJzsvXlglNW9//96lpnJvgIJkLDKIotBXFCrqLiAikhdagVrf+0Vr221l35r3RC5bRE3bK/aW3r12lZrXa7WIrV2UauiSKkCsgQhyJYQspJtMuuz/f4YZpgkM8lMMkkmyXn5h2Se7TznmXnO+3w+n/P5SJZlWQgEAoFAIBAkIXJ/N0AgEAgEAoEgGkKoCAQCgUAgSFqEUBEIBAKBQJC0CKEiEAgEAoEgaRFCRSAQCAQCQdIihIpAIBAIBIKkRQgVgSBJmDJlCldffTXXXHMNixcvZv78+Vx33XXs2rWr2+dcsWIFn3zySYfPd+3axbx587p93qNHj3L66afHdcy6deu46KKLuO+++7p93aeffppzzjmHa665hmuuuYZFixYxb948Hn74YYKZFq655hpaWlo6HPvcc89x7733dvva7Zk3bx6zZs3C5XK1+fyPf/wjU6ZM4a9//Wtc54u1fVOmTKGhoSGucwsEAxm1vxsgEAhO8vzzz5OXlxf6+7nnnmP16tW8+uqr3TrfQw89lKim9ZjXX3+dtWvXcuaZZ/boPFdeeSUPPvhg6O/m5mYWLVrE+eefzwUXXMCbb77Z06bGTG5uLu+88w6LFy8OffbHP/6RYcOG9VkbBILBjhAqAkGSous6VVVVZGdnhz5bt24df//73zFNk9GjR7Nq1SoKCgr4+9//zrp165AkCUVRuPvuuznrrLP4xje+wdKlS1mwYAEvvfQSzz//PBkZGUyePDl0zqeffprGxsbQ4B/+9+eff87jjz+O3++nrq6O8847jzVr1rRp54EDB1ixYgV+vx/Lsrj++utZunRpm32WL19OTU0NK1as4D/+4z+YPXs2//mf/0llZSWWZbF48WJuvfVWjh49ytKlS5k4cSKVlZX87ne/Y8SIEZ32U319PV6vN9RPU6ZMYfPmzWRmZrJ69Wo++eQT8vPzyc/PJzMzE4AjR45w//3309zczPDhw7Esi0WLFnHttdeybds21q5di8fjQZIk7rzzTi6++OKI1160aBEbNmwICZXKykrcbjcTJkwI7fPZZ5/x2GOP4fF4sNlsLF++nLlz56JpWtT2OZ1OHnroIcrKytA0jXPPPZe7774bVRWvbMHQQ3zrBYIk4pvf/CaSJNHQ0IDD4eDiiy/m4YcfBmD9+vWUlZXx2muvoaoqr776Kg888ADPPvssjz32GGvXrmXWrFl8/PHHbNmyhbPOOit03i+++IJf/OIXvPnmmwwfPryNRaIzXnjhBb7//e8zZ84cXC4Xl1xyCbt37yYnJye0z3PPPce8efO47bbbqKurY82aNdx0003I8knP8n/9138xb9481q5dy8yZM7n55pu55JJL+Na3voXT6WTp0qWMHDmSkpISqqureeKJJ6JaXt5++222bt2K1+ulqamJadOm8eMf/5jTTjutzX4vvfQShw8f5s9//jO6rnPzzTeHhMDdd9/NNddcw5IlSzhw4ADXXXcdixYtorm5mfvuu4/nnnuOoqIiampq+NrXvsaUKVMYNWpUh7ZceOGF/N///R+1tbWMGDGCN998k8WLF/O3v/0NgMbGRr7//e+zbt06SkpK2L9/PzfffDOvv/46//jHP6K2b82aNUyfPp1HHnkEwzC49957+c1vfsOyZctiem4CwWBCCBWBIIkIun727NnDsmXLOP3008nPzwfg/fffZ9euXVx33XUAmKaJx+MB4KqrruKOO+7gwgsv5Ctf+UqHAW3z5s185StfYfjw4QDceOONfPzxx12255FHHmHjxo386le/4uDBg3i9Xtxudxuhctlll3HPPfewc+dOzj33XB544IE2IqU9brebbdu28etf/xqAzMxMrr32WjZu3EhJSQmqqjJr1qyoxwddP36/n5/+9Kfs37+fuXPndthv8+bNLFy4ELvdjt1u5+qrr2bfvn00Nzezc+dOXnzxRQAmTpzIOeecA8Dnn39OXV0d3/ve90LnkSSJffv2RRQqNpuNBQsW8NZbb/Htb3+bt99+mxdffDEkVHbu3MmYMWMoKSkBYNKkScyePZt//etfUdsH8MEHH7Br1y5ef/11ALxeb9T+EAgGO0KoCARJyLRp07jvvvt44IEHKCkpoaioCNM0ufXWW1myZAkAfr+f5uZmAH7wgx9w/fXX8/HHH/PGG2/wzDPP8MYbb4TOJ0kS4WW9FEWJuk3TtNC/ly5dytSp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q64N2xUqIiIicu2w9akfERERCWwqVERERCRgqVARERGRgKVCRURERAKWChUREREJWCpURGyupqaG6OhokpKSeOKJJxg1ahSTJk1qdkOvy7V27VqysrIAmDJlCrW1tRf92cLCQr7//vvLOn6fPn2abTc0NNC/f/8Wz7Nz504cDsdlHUtEAosKFRGha9eulJWVUVpayoYNG4iOjvb3P7pS7733HrfddttFx6uqqvB6vVf0HBERETz22GNs2LCh2f7S0lLGjBlzRccWkbalQkVEWhg4cCCHDh0CYPjw4cyaNYu4uDjq6uooLS3F4XCQlJREdna2v91AaWkpcXFxjBkzhi1btviPNXz4cGpqajh79izZ2dnExcUxevRoNm7cSGlpKXv27CEnJ4f9+/fjdDqZNGkSDoeD1NRUfv75Z+D8X31SU1NJSkpiwYIF/zrnMWPGsH79ev/22bNn2bJlCwkJCQAsWbKElJQU4uLiGDduHMePH2/2+KVLlzZrWto0b6/Xy5tvvonD4SAxMZFVq1ZdabwichlUqIhIMx6Ph/Lych544AH/vmHDhrFp0yZOnjxJSUkJxcXFlJWVcfPNN/P+++9TW1vL4sWLWb16NZ988kmzvidNioqKaGxspLy8nA8//JB3332X+Ph4oqOjyc/Pp0+fPmRmZpKRkcG6devIy8vz390zLy+P5ORkysrKms3rn2JiYjh16hQHDx4E4Msvv2Tw4MF07twZp9PJwYMHKS4uZtOmTURGRvLFF19cUh4lJSUArFu3jjVr1vDVV19d9qkqEfnf2bopoYic98cff5CUlAScv3X8fffdx5w5c/zj999/PwA7duzA6XSSkpICnC9q+vXrxw8//ED//v255ZZbAEhISKCysrLZc1RVVZGSkkJQUBC33npri9M0LpeLPXv2MG/ePP++xsZG6uvr2blzJ++88w4AiYmJ5OTktHgNlmXhcDhYv349M2fOpKyszH978x49epCZmcmnn35KdXU1u3btIjIy8pKyaWrS2PR6Ghsb2b9/v216uYi0NRUqIuK/RuViwsLCAPB6vYwcOdJfKLhcLrxeL9u3b8fn8/l/PiSk5UfLhfucTie33367f9vn89GuXbtm8zh27BhdunQBoKnbh2VZ/hbyF3I4HEyePJm0tDSqq6v9TeP27NnDnDlzmDhxInFxcQQFBXFh9xDLspq9hqYOsF6vl4yMDEaMGAHAyZMn6dChw0WzEpHWpVM/InLJBg0aREVFBXV1dRhjyM3N5aOPPmLAgAHs3r2b2tpafD4fGzdubPHYBx98kPLycowx1NXV8cwzz+B2uwkODsbr9dKxY0fuuOMOf6Gybds2nn76aQAeeughPv/8cwA2b96M2+3+1/l169aNbt26UVhYSFJSkr+gqaqqIiYmhtTUVHr16sW2bdtaXMB74403cuDAAQB+/PFH/zUsgwcPpqSkBI/Hg8vlIi0tjd27d7dCmiJyKfQXFRG5ZHfffTfp6elMmDABn89H3759eeGFFwgLCyMnJ4eJEyfSvn17evXq1eKxaWlp5Ofnk5iYCMD8+fOJiIhg6NChLFy4kIKCAhYtWkRubi4rV64kNDSUJUuWYFkWCxYsICMjg+LiYu69917Cw8MvOsfk5GReeeUVKioq/Pvi4+NJT08nISGB0NBQ+vTpQ01NTbPHxcfHs2nTJuLj47nnnnvo168fAOPGjcPpdOJwODh37hzJyckMGjSoNeIUkUug7skiIiISsHTqR0RERAKWChUREREJWCpUREREJGCpUBEREZGApUJFREREApYKFREREQlYKlREREQkYKlQERERkYD1H6Gc/ivcW+meAAAAAElFTkSuQmCC\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAGACAYAAACDX0mmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzsvXl4VOXZ+P8558yWfQMSIEEJu4DgiiJERUVb2epWBdS3Vq1tkWJfa0VUvq3iivUV2+JrX2tVtFqXqq3+ulEhRVARUSRCwg6BJGSdJDOZ5Sy/P4YZMskkmUkmk8nk+VwXl2bOMs95zpxz38+9SoZhGAgEAoFAIBDEIXJfD0AgEAgEAoGgI4SiIhAIBAKBIG4RiopAIBAIBIK4RSgqAoFAIBAI4hahqAgEAoFAIIhbhKIiEAgEAoEgbhGKikAQJ4wbN465c+cyf/58FixYwOWXX87VV1/N119/3e1zrlixgs2bN7f7/Ouvv2bWrFndPm95eTlnnHFGRMesXbuWiy66iOXLl3f7e5999lnOO+885s+fz/z585k3bx6zZs3i0UcfxV9pYf78+TQ2NrY79oUXXuDee+/t9ne3ZdasWUydOhWHwxH0+Z///GfGjRvH3/72t4jOF+74xo0bR11dXUTnFgj6M6a+HoBAIDjJSy+9RHZ2duDvF154gYcffpg33nijW+dbtWpVtIbWY9566y1Wr17N2Wef3aPzfPvb3+bBBx8M/G2325k3bx4zZsxg5syZvPfeez0dathkZWXxz3/+kwULFgQ++/Of/8ygQYNiNgaBINERiopAEKeoqkpFRQUZGRmBz9auXcs//vEPdF1n+PDhrFy5ktzcXP7xj3+wdu1aJElCURTuuecezjnnHG688UYWLVrEFVdcwWuvvcZLL71EamoqY8eODZzz2Wefpb6+PiD8W//95Zdf8uSTT+LxeKiurmb69Ok88sgjQePct28fK1aswOPxYBgG11xzDYsWLQraZ9myZVRVVbFixQp+8pOfcOaZZ/L//t//4+jRoxiGwYIFC7j11lspLy9n0aJFjBo1iqNHj/LKK68wZMiQTueppqYGl8sVmKdx48axZcsW0tLSePjhh9m8eTM5OTnk5OSQlpYGwKFDh7jvvvuw2+0MHjwYwzCYN28eV111FV988QWrV6+mpaUFSZK48847ufjii0N+97x583j//fcDisrRo0dxOp0UFhYG9vn888954oknaGlpwWw2s2zZMoqKivB6vR2Or6mpiVWrVlFWVobX6+X888/nnnvuwWQSr2zBwEP86gWCOOLmm29GkiTq6uqwWq1cfPHFPProowC8++67lJWV8eabb2IymXjjjTe4//77+d3vfscTTzzB6tWrmTp1Kps2beLTTz/lnHPOCZx3165d/PrXv+a9995j8ODBQRaJznj55ZdZunQp06ZNw+FwcMkll7Bz504yMzMD+7zwwgvMmjWL22+/nerqah555BFuuOEGZPmkZ/l//ud/mDVrFqtXr2by5MksXryYSy65hO9973s0NTWxaNEihg4dypQpU6isrOSpp57q0PLy4Ycfsm3bNlwuFw0NDZx22mn84he/4PTTTw/a77XXXuPgwYN88MEHqKrK4sWLA4rAPffcw/z581m4cCH79u3j6quvZt68edjtdpYvX84LL7xAfn4+VVVVXHfddYwbN45hw4a1G8uFF17In/70J44fP86QIUN47733WLBgAX//+98BqK+vZ+nSpaxdu5YpU6awZ88eFi9ezFtvvcW///3vDsf3yCOPMHHiRB577DE0TePee+/lxRdf5LbbbgvrvgkEiYRQVASCOMLv+vnmm2+47bbbOOOMM8jJyQHgo48+4uuvv+bqq68GQNd1WlpaALjyyitZsmQJF154IRdccEE7gbZlyxYuuOACBg8eDMB3v/tdNm3a1OV4HnvsMYqLi3nuuefYv38/LpcLp9MZpKhcdtll/PznP2fHjh2cf/753H///UFKSlucTidffPEFv//97wFIS0vjqquuori4mClTpmAymZg6dWqHx/tdPx6Ph4ceeog9e/ZQVFTUbr8tW7YwZ84cLBYLFouFuXPnUlpait1uZ8eOHaxbtw6AUaNGcd555wHw5ZdfUl1dzY9//OPAeSRJorS0NKSiYjabueKKK/jrX//KLbfcwocffsi6desCisqOHTsYMWIEU6ZMAWDMmDGceeaZfPbZZx2OD2DDhg18/fXXvPXWWwC4XK4O50MgSHSEoiIQxCGnnXYay5cv5/7772fKlCnk5+ej6zq33norCxcuBMDj8WC32wG46667uOaaa9i0aRPvvPMOzz//PO+8807gfJIk0bqtl6IoHW7zer2B/1+0aBHjx49n5syZ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MoYMGcLy5csxmUzous5TTz3Fc889x969e/n1r3/NkiVLgs5RVlbGyJEjMQyDdevWcddddwHwyiuvsGXLFpqamjh06BA33HBD0HELFy7E4XAEfabrOsuXL2f69OkAVFVVkZeXF9iem5vLjh072l1HeXk52dnZLF++nN27dzNx4kRWrFgR1J26vLycXbt2MWXKlMC5brnlFi6++GKsVisXXHBBwsTRRCII+9I11FvZJ/FOOPE5OSnWdnMSq/L3kRCtrJXWREsp6G4xtVD0luIfrfN2pvR/uX17t8cn6ByhqMSAN998k6ysLB555BHq6+tZvHgxCxcu5PTTT+dnP/sZn3/+OU1NTdxxxx0BF09rKioqcDgc3H777VRVVTFu3DjuvPNOAG666SZuuummDr/7tddea/eZw+HoVkdpVVX55ptveOCBB5gyZQoPP/wwzz//PMuWLQucd+nSpdx3332kpqYCYLfbWb9+PevXryctLY2f/OQnvPfee8yfPz/i7483whGEuWm2uHENDbQeJJ3F52QlWXh12342HTje7r7EawBytMu5R0t4R9pluMPz9JKCGM3zDlSlv68RikoMKCsrY9u2bQErhaqqXHLJJbz11lvceuutpKWlBSwkHR1/9tln8/LLL2O325kzZw7bt2/nzDPP7PK7w7Go5ObmUllZGdheVVVFbm5uu3Pl5eWRl5cXsJZcccUVPP/884DParR06VLmzp0bcFEBbN68mfz8fLKzswGYPXs227dvTwhFJZxA1f7gGkpUOovPkSWJD3cdDXlffjRjfFwGIPc0a6U10RTe4bilwrEo9paC2BvnHWhKf18jFJUYUFhYSF5eHnfccQcul4u1a9fyxRdfcNZZZ7FkyRL++te/8n//93/ceeed6CHKL5eWlnLaaacBkJGRwZw5c9i4cWNYiko4FpXJkydz8OBBjhw5Qm5uLh988AFPPdU+GHnw4MHk5eWxf/9+CgsL2bJlC6NGjcIwDFasWEFhYSHf+973go4ZNmwYX331FS0tLdhsNrZs2cKkSZO6HHd/oKtAVSDuXAgDjVCm+hmnDuY/B6o7vS89DUD2tytAgmHpPa+h0pruZq20JprCuzO31OD0QtYUl4ZlUeytDLWBnvmWCPR9WsIA4Prrr2f//v0sXryY66+/nuHDhzNp0iTWrFnDTTfdxOuvv87ixYvJycnB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WgoqIiIi4rUUVERERMRrKaiIiIiI11JQEREREa+loCIiIiJeS0FFREREvNZNd8E383/28Whubr7ixzQ1adfUS6kmbake7akmbake7akmbake7V1JTVr/n5uX7NPVyjAv1+Klzp8/zy+//NLR3RAREZHraODAgXTp0qXd8ZsuqLjdbhwOBwEBARiG0dHdERERkWtgmiZOp5Pg4GBstvZnpNx0QUVERESsQyfTioiIiNdSUBERERGvpaAiIiIiXktBRURERLzWTXcdlStx4MABVqxYQUFBAVVVVWRkZGAYBnfffTfLli37x7OKfZHT6SQzM5MTJ07Q3NzM3LlzGTBggGXrAeByucjKyqKyshLDMHj11VcJDAy0dE0AamtrSUhI4P3338ff39/y9bDb7YSEhADQp08fnnzySV577TX8/PyIjIwkJSWlg3t4461Zs4avvvoKp9NJUlISERERlh0nmzZtYvPmzcDF64QcPnyYgoICS48Rp9NJRkYGJ06cwGazkZOTc/0+S0wfs3btWnPixInm5MmTTdM0zdmzZ5vl5eWmaZrmkiVLzB07dnRk926ojRs3mrm5uaZpmmZ9fb35yCOPWLoepmmaZWVlZkZGhmmaplleXm7OmTPH8jVpbm42X3jhBXPcuHHm0aNHLV+Pv/76y4yPj29zLC4uzqyqqjLdbrf53HPPmYcOHeqg3nWM8vJyc/bs2abL5TIbGhrM/Px8y4+TVtnZ2WZhYaHlx0hZWZmZmppqmqZp7tq1y0xJSbluY8Tn4m9oaCirVq3y3D506BAREREAREVF8e2333ZU1264xx57jBdffBG4uE7dz8/P0vUAePTRR8nJyQHg5MmTdO3a1fI1ycvLY8qUKfTs2ROw9t8MwM8//8yFCxeYOXMmU6dOpaKigubmZkJDQzEMg8jISMvVZNeuXQwcOJB58+YxZ84cxowZY/lxAvDjjz9y9OhRJkyYYPkx0q9fP1wuF263m4aGBvz9/a/bGPG5qZ/o6Giqq6s9t03T9FwYLjg4mPPnz3dU12644OBgABoaGkhNTWX+/Pnk5eVZth6t/P39SU9Pp6ysjPz8fHbv3m3ZmmzatInbbruN0aNHs3btWsDafzMAt9xyC88++yyTJ0/m+PHjzJo1i65du3rag4OD+f333zuwhzdefX09J0+eZPXq1VRXVzN37lzLjxO4OB02b948GhoaPFOFYM0xEhQUxIkTJxg/fjz19fWsXr2aioqK6zJGfC6oXOrv82EOh6PNB44VnDp1innz5pGcnExsbCzLly/3tFmxHq3y8vJYtGgRiYmJbfaisFpNPv30UwzDYM+ePRw+fJj09HTq6uo87VarB1z8Zti3b18Mw6Bfv3506dKFP//809NuxZp0796dsLAwOnXqRFhYGIGBgZw+fdrTbsWanDt3jsrKSkaOHElDQwMOh8PTZsV6rF+/nsjISBYuXMipU6eYNm0aTqfT034tNfG5qZ9LDRkyhL179wLw9ddfM3z48A7u0Y1z9uxZZs6cSVpaGk888QRg7XoAFBcXs2bNGgA6d+6MYRiEh4dbtiYbNmzgo48+oqCggMGDB5OXl0dUVJRl6wGwceNG3nzzTQBqamq4cOECQUFB/Pbbb5imya5duyxXk2HDhvHNN99gmqanJqNGjbL0OKmoqGDUqFEAhISEEBAQYOkx0rVrV88+Pd26daOlpeW6/b/xyUvoV1dX89JLL1FUVERlZSVLlizB6XQSFhZGbm4ufn5+Hd3FGyI3N5fS0lLCwsI8x1555RVyc3MtWQ+AxsZGFi9ezNmzZ2lpaWHWrFn079/fsmPk75555hmys7Ox2WyWrkdzczOLFy/m5MmTGIbBokWLsNlsvP7667hcLiIjI1mwYEFHd/OGe+utt9i7dy+mabJgwQL69Olj6XGybt06/P39mT59OgD79++39BhxOBxkZmZy5swZnE4nU6dOJTw8/LqMEZ8MKiIiIuIbfH7qR0RERG5eCioiIiLitRRURERExGspqIiIiIjXUlARERERr6WgImJx1dXVhIeHEx8fz+OPP86ECROYMWNGmwt6Xa1NmzaRkZEBwKxZs6ipqbnsffPz8/nuu++u6vkHDRrU5nZDQwNDhw5t9zr79u3Dbrdf1XOJiHdRUBERevbsSUlJCcXFxWzdupXw8HDPnkjX6t1336VXr16Xba+oqMDlcl3Ta4SEhPCvf/2LrVu3tjleXFzMpEmTrum5RaRjKaiISDvDhw/n+PHjAIwdO5b58+cTHR1NbW0txcXF2O124uPjyczM9GxBUFxcTHR0NJMmTWLnzp2e5xo7dizV1dU0NTWRmZlJdHQ0EydOZNu2bRQXF3Pw4EGysrI4cuQIVVVVzJgxA7vdTlJSEj/99BNw8VefpKQk4uPjWbp06T/2edKkSWzZssVzu6mpiZ07dxIbGwvAypUrSUxMJDo6milTpnDmzJk2j1+1alWbDU1b++1yuXjjjTew2+3ExcWxfv36ay2viFwFBRURacPpdFJaWsqDDz7oORYVFcX27dupq6ujqKiIwsJCSkpK6NGjB++99x41NTWsWLGCDRs28PHHH7fZ96RVQUEBjY2NlJaW8sEHH/DOO+8QExNDeHg4ubm5DBo0iPT0dNLS0ti8eTM5OTmeq3vm5OSQkJBASUlJm379XUREBOfOnePYsWMAfPHFF4wcOZJu3bpRVVXFsWPHKCwsZPv27YSGhvL5559fUT2KiooA2Lx5Mxs3buTLL7+86qkqEfnv+fymhCLyn/3xxx/Ex8cDFy8hf//997Nw4UJP+wMPPADA3r17qaqqIjExEbgYaoYMGcL333/P0KFDuf322wGIjY2lvLy8zWtUVFSQmJiIzWbjjjvuaDdN43A4OHjwIIsXL/Yca2xspL6+nn379vH2228DEBcXR1ZWVrv3YBgGdrudLVu2kJqaSklJiefy5n379iU9PZ1PPvmEyspK9u/fT2ho6BXVpnXDxtb309jYyJEjRyy3l4tIR1FQERHPOSqXExgYCIDL5WL8+PGeoOBwOHC5XOzZswe32+25v79/+4+WS49VVVVx5513em673W46derUph+nT5+me/fuALTu9mEYhmfr+EvZ7XZmzpxJcnIylZWVnk3jDh48yMKFC5k+fTrR0dHYbDYu3T3EMIw276F151eXy0VaWhrjxo0DoK6ujqCgoMvWSkSuL039iMgVGzFiBGVlZdTW1mKaJtnZ2Xz44YcMGzaMAwcOUFNTg9vtZtu2be0e+9BDD1FaWoppmtTW1vL000/T3NyMn58fLpeLLl26cNddd3mCyu7du3nqqacAePjhh/nss88A2LFjB83Nzf/Yv969e9O7d2/y8/OJj4/3BJqKigoiIiJISkpiwIAB7N69u90JvLfeeitHjx4F4IcffvCcwzJy5EiKiopwOp04HA6Sk5M5cODAdaimiFwJ/aIiIlfsnnvuISUlhWnTpuF2uxk8eDDPP/88gYGBZGVlMX36dDp37syAAQPaPTY5OZnc3Fzi4uIAWLJkCSEhIYwePZply5aRl5fH8uXLyc7OZt26dQQEBLBy5UoMw2Dp0qWkpaVRWFjIfffdR3Bw8GX7mJCQwMsvv0xZWZnnWExMDCkpKcTGxhIQEMCgQYOorq5u87iYmBi2b99OTEwM9957L0OGDAFgypQpVFVVYbfbaWlpISEhgREjRlyPcorIFdDuySIiIuK1NPUjIiIiXktBRURERLyWgoqIiIh4LQUVERER8VoKKiIiIuK1FFRERETEaymoiIiIiNdSUBERERGv9W956ijw32hFYQAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -168,12 +150,12 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXsAAAGCCAYAAAABo2PaAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzsnXd8U/X6x98nSdM9oNCBBWTIKhuRCpVxFZCrDBEQUBBRcSteuRdBwAGoqIiiF1Hvz4WKIjKu6zpQKHsEEMqqlNVSKKN0pkma5Pz+qIlturKbpN/364Uvc3JyznNOmuf7nOf7PJ+vJMuyjEAgEAgCGkV9GyAQCAQCzyOcvUAgEDQAhLMXCASCBoBw9gKBQNAAEM5eIBAIGgDC2QsEAkEDQDh7gcNkZ2fTsWNHRo4caf03YsQIVq9e7fKxH3jgAdasWQPAyJEjKSwsrHHfoqIiJk+ebH1d1/6O8NZbb5GSklLpGkeOHMlrr73mluPXxJEjR7jpppu47bbbyM7OduoYO3fu5NZbb3WzZQJ/R1XfBgj8k5CQENavX299nZuby6233krnzp3p0KGDW85R8fjVUVBQwMGDB+3e31H+/ve/M2/ePLcesy42bNhAnz59WLhwoVfPKwh8hLMXuIX4+HhatmzJqVOnOHz4MKtXr6a0tJSIiAhWrFjBV199xcqVKzGbzcTExDB37lzatGlDbm4uTz/9NBcuXKBZs2ZcvnzZesz27duzfft2GjduzLvvvsvatWtRqVS0bNmSl19+mVmzZqHT6Rg5ciRr1qyhU6dO1v3//e9/891336FUKmnVqhVz586ladOmTJo0ie7du7N3717OnTtHr169WLRoEQqFYw+5kyZNIjo6mhMnTjBhwgR++umnSq8HDx7Mc889x9mzZ5FlmVGjRnHfffeRnZ3NnXfeSZs2bTh79iwrVqwgLi4OgP/+97+sXLkSk8mETqdj8eLFtV5HxfNNmjTJLrv379/Pq6++isFg4OLFi/Tt25cXX3wRo9HI/Pnz2bt3L0FBQSQlJfHSSy8RHBxc7fbw8HB++eUX3n77bUwmExEREcyaNYuuXbs6dB8FXkQWCBwkKytL7t69e6Vte/fulXv37i3n5OTIX3/9tdy7d2+5qKhIlmVZ3rlzpzxx4kRZq9XKsizLmzdvlocNGybLsiw//PDD8pIlS2RZluVTp07J3bt3l7/++mtZlmW5Xbt28uXLl+VffvlFHjJkiJyfny/Lsiy/+OKL8rJly6rYYdl/9erV8h133CGXlJTIsizLS5culadOnSrLsizfdddd8uOPPy6bTCa5qKhITk1Nlbdv317lGpcuXSr36dNHHjFiRKV/aWlp1uPMmjXLur/t6zvvvFP+4IMPZFmW5cLCQnn48OHyt99+K2dlZcnt2rWTd+/eXe29Xbp0qfz888/LsizXeR0Vz1eRHTt2yLfccku17z355JPyjh07ZFmW5eLiYrlPnz7ywYMH5d27d8s333yzbDabZVmW5VdeeUXWaDQ1bj9+/Ljct29f+cyZM7Isy/K2bdvkfv36Wb9zge8hInuBU1giagCTyUSjRo149dVXSUxMBMqj8oiICAA2btzI6dOnGT9+vPXzBQUF5Ofns23bNmbOnAlAy5Yt6dOnT5Vzbd++nZtvvpno6GgAZs2aBVBjTjstLY3Ro0cTFhYGwOTJk1m+fDkGgwGAQYMGoVAoiIiIoGXLlhQUFFR7nLrSONdee221r7VaLXv37uWDDz4AIDIyktGjR5OWlka3bt1QqVR07969xuPaex2257eHl19+mbS0NJYvX86JEyfQ6XRotVo6dOiAUqlk7NixpKamMnToULp27UphYWG12z/77DNSUlJo3rw5ANdffz2NGzcmPT2dlJQUh+0SeB7h7AVOYZuzt8XioADMZjMjR47kn//8p/X1hQsXiI6ORpIk5AryTCpV1T9JpVKJJEnW14WFhbVOxMo2ck9msxmj0VjJdgu253eEitdY8bXZbK7VBrVaXe112lLXddie3x7uvPNOOnTowA033MCwYcP4/fffkWWZqKgo1q9fz969e9mxYwfTp09n8uTJTJkypdrt1dkvy3Il+wS+hajGEXicfv368d1333HhwgUAVq5cyd133w3ADTfcwJdffglATk4OO3furPL5vn378vPPP1NcXAyUV8p89NFHqFQqTCZTFaeYmprKmjVr0Gq1AKxYsYLevXujVqs9do0ViYiIoFu3bnz22WdAedXQunXr6Nu3r0PHcfd1FBQUkJ6ezowZMxgyZAi5ubmcOXMGs9nMb7/9xpQpU+jRowePPfYYo0aN4ujRozVuT0lJYevWrWRlZQHlT1/nzp2jW7duTtkm8Dwishd4nBtuuIH7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\n", "text/plain": [ "
" ] @@ -205,12 +187,12 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -249,7 +231,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.6.7" } }, "nbformat": 4, diff --git a/examples/walkthrough.ipynb b/examples/walkthrough.ipynb new file mode 100644 index 000000000..2cd3bac52 --- /dev/null +++ b/examples/walkthrough.ipynb @@ -0,0 +1,135 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Walkthrough\n", + "\n", + "This notebook contains the code for the [walkthrough in the quickstart guide](http://www.scikit-yb.org/en/latest/quickstart.html#walkthrough). We've purposefully omitted the text of the guide so that you can follow along in code using this notebook as a template! The scikit-yb developers also use this notebook to verify that the quickstart code is correct, so if this code doesn't match what's on the guide, please leave us a note on our [GitHub Issues](https://github.com/DistrictDataLabs/yellowbrick/issues)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from yellowbrick.datasets import load_bikeshare\n", + "\n", + "X, y = load_bikeshare()\n", + "print(X.head())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from yellowbrick.features import Rank2D\n", + "\n", + "visualizer = Rank2D(algorithm=\"pearson\")\n", + "visualizer.fit_transform(X)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from yellowbrick.features import JointPlotVisualizer\n", + "\n", + "visualizer = JointPlotVisualizer(feature='temp', target='feelslike')\n", + "visualizer.fit_transform(X['temp'], X['feelslike'])\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from yellowbrick.regressor import ResidualsPlot\n", + "from sklearn.linear_model import LinearRegression\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "# Create training and test sets\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.1\n", + ")\n", + "\n", + "visualizer = ResidualsPlot(LinearRegression())\n", + "visualizer.fit(X_train, y_train)\n", + "visualizer.score(X_test, y_test)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from sklearn.linear_model import RidgeCV\n", + "from yellowbrick.regressor import AlphaSelection\n", + "\n", + "alphas = np.logspace(-10, 1, 200)\n", + "visualizer = AlphaSelection(RidgeCV(alphas=alphas))\n", + "visualizer.fit(X, y)\n", + "visualizer.poof()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.linear_model import Ridge\n", + "from yellowbrick.regressor import PredictionError\n", + "\n", + "visualizer = PredictionError(Ridge(alpha=3.181))\n", + "visualizer.fit(X_train, y_train)\n", + "visualizer.score(X_test, y_test)\n", + "visualizer.poof()" + ] + } + ], + "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.6.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/paper/figures/figures.py b/paper/figures/figures.py index 6279dc18b..13c1741bf 100644 --- a/paper/figures/figures.py +++ b/paper/figures/figures.py @@ -31,8 +31,8 @@ ) # Quick reference dataset objects -Dataset = namedtuple('Dataset', 'X,y') -Split = namedtuple('Split', 'train,test') +Dataset = namedtuple("Dataset", "X,y") +Split = namedtuple("Split", "train,test") def _make_dataset(X, y, split=False): @@ -58,8 +58,8 @@ def load_concrete(split=False): path = os.path.join(DATA, "concrete", "concrete.csv") data = pd.read_csv(path) - X = data[['cement', 'slag', 'ash', 'water', 'splast', 'coarse', 'fine', 'age']] - y = data['strength'] + X = data[["cement", "slag", "ash", "water", "splast", "coarse", "fine", "age"]] + y = data["strength"] return _make_dataset(X, y, split) @@ -81,7 +81,7 @@ def feature_analysis(fname="feature_analysis.png"): """ # Create side-by-side axes grid - _, axes = plt.subplots(ncols=2, figsize=(18,6)) + _, axes = plt.subplots(ncols=2, figsize=(18, 6)) # Draw RadViz on the left data = load_occupancy(split=False) @@ -130,7 +130,7 @@ def regression(fname="regression.png"): def classification(fname="classification.png"): # Create side-by-side axes grid - _, axes = plt.subplots(ncols=2, figsize=(18,6)) + _, axes = plt.subplots(ncols=2, figsize=(18, 6)) # Add ClassificationReport to the reft data = load_spam(split=True) @@ -153,11 +153,11 @@ def classification(fname="classification.png"): def clustering(fname="clustering.png"): # Create side-by-side axes grid - _, axes = plt.subplots(ncols=2, figsize=(18,6)) + _, axes = plt.subplots(ncols=2, figsize=(18, 6)) X, y = make_blobs(centers=7) # Add K-Elbow to the left - oz = KElbowVisualizer(MiniBatchKMeans(), k=(3,12), ax=axes[0]) + oz = KElbowVisualizer(MiniBatchKMeans(), k=(3, 12), ax=axes[0]) oz.fit(X, y) oz.finalize() @@ -171,9 +171,10 @@ def clustering(fname="clustering.png"): plt.tight_layout() plt.savefig(path) + def hyperparameter_tuning(fname="hyperparameter_tuning.png"): # Create side-by-side axes grid - _, axes = plt.subplots(ncols=2, figsize=(18,6)) + _, axes = plt.subplots(ncols=2, figsize=(18, 6)) # Load the concrete dataset data = load_concrete(split=False) @@ -187,7 +188,7 @@ def hyperparameter_tuning(fname="hyperparameter_tuning.png"): oz.finalize() # Add LearningCurve to the right - oz = LearningCurve(RandomForestRegressor(), scoring='r2', ax=axes[1]) + oz = LearningCurve(RandomForestRegressor(), scoring="r2", ax=axes[1]) oz.fit(data.X, data.y) oz.finalize() @@ -197,8 +198,7 @@ def hyperparameter_tuning(fname="hyperparameter_tuning.png"): plt.savefig(path) - -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser( description="generate visualizations for JOSS paper" ) diff --git a/paper/paper.bib b/paper/paper.bib index cd1ed1f5c..59409d8fb 100644 --- a/paper/paper.bib +++ b/paper/paper.bib @@ -5,7 +5,7 @@ @article{zenodo Gray, Larry and others}, title = {Yellowbrick}, - month = Jul, + month = Nov, year = 2018, doi = {10.5281/zenodo.1206239}, url = {https://doi.org/10.5281/zenodo.1206239} @@ -44,7 +44,7 @@ @misc{scipy title = {{SciPy}: Open source scientific tools for {Python}}, year = {2001--}, url = "http://www.scipy.org/", - note = {[Online; accessed ]} + note = {[Online; accessed 2018-07-30]} } @article{kumar2016model, @@ -55,7 +55,8 @@ @article{kumar2016model number = {4}, pages = {17--22}, year = {2016}, - publisher = {ACM} + publisher = {ACM}, + doi = {10.1145/2935694.2935698} } @article{liu_wang_liu_zhu_2017, @@ -90,7 +91,8 @@ @inproceedings{kapoor2010interactive booktitle = {Proceedings of the SIGCHI Conference on Human Factors in Computing Systems}, pages = {1343--1352}, year = {2010}, - organization = {ACM} + organization = {ACM}, + doi = {10.1145/1753326.1753529} } @article{rajaraman2008more, diff --git a/requirements.txt b/requirements.txt index b7f02d62b..6e0f3680c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,29 +1,28 @@ ## Dependencies -matplotlib>=1.5.1,!=3.0.0 +matplotlib>=2.0.2,!=3.0.0 scipy>=1.0.0 scikit-learn>=0.20 numpy>=1.13.0 cycler>=0.10.0 -## Testing Requirements (uncomment for development) -#pytest>=3.4.1 -#pytest-cov>=2.5.1 -#pytest-flakes>=2.0.0 -#pytest-spec>=1.1.0 -#coverage>=4.4.1 -#requests>=2.18.3 -#six==1.11.0 +## Optional Dependencies (uncomment to use) +#umap-learn>=0.3 -## Python 2 Testing Requirements -#mock>=2.0.0 +## Testing Requirements (pip install -r tests/requirements.txt) +#pytest>=4.2.0 +#pytest-cov>=2.6.1 +#pytest-flakes>=4.0.0 +#pytest-spec>=1.1.0 +#coverage>=4.5.2 -## Optional Testing Dependencies (uncomment for development) +## Optional Testing Dependencies (pip install -r tests/requirements.txt) #nltk>=3.2 #pandas>=0.20 +#umap-learn>=0.3 -## Documentation (uncomment to build documentation) -#Sphinx>=1.7.5 -#sphinx-rtd-theme>=0.4.0 +## Documentation (pip install -r docs/requirements.txt) +#Sphinx>=1.8.3 +#sphinx-rtd-theme>=0.4.2 #numpydoc>=0.8.0 ## Build Requirements (uncomment for deployment) diff --git a/setup.cfg b/setup.cfg index 3308f57b3..505034391 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] description-file = DESCRIPTION.txt +license_file = LICENSE.txt [wheel] universal = 1 @@ -12,17 +13,30 @@ tests = True test=pytest [tool:pytest] -addopts = --verbose --cov=yellowbrick --flakes --spec +# TODO: add --spec and --verbose back to addopts +addopts = --cov=yellowbrick --flakes python_files = tests/* flakes-ignore = __init__.py UnusedImport __init__.py ImportStarUsed test_*.py ImportStarUsed test_*.py ImportStarUsage + conftest.py UnusedVariable examples/* ALL tests/checks.py ALL + docs/_build ALL spec_header_format = {class_name} ({path}) filterwarnings = + once::UserWarning once::DeprecationWarning once::PendingDeprecationWarning + ignore::sklearn.exceptions.ConvergenceWarning ignore::FutureWarning + +[flake8] +# match black maximum line length +max-line-length = 88 +per-file-ignores = + __init__.py:F401 + test_*.py:F405,F403 + conftest.py:F841 \ No newline at end of file diff --git a/setup.py b/setup.py index bf144208f..06f23e4f4 100755 --- a/setup.py +++ b/setup.py @@ -2,10 +2,10 @@ # setup # Setup script for installing yellowbrick # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Wed May 18 14:33:26 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt and NOTICE.md # # ID: setup.py [c4f3ba7] benjamin@bengfort.com $ @@ -29,69 +29,88 @@ ## Package Information ########################################################################## +## Basic information ## Basic information NAME = "yellowbrick" DESCRIPTION = "A suite of visual analysis and diagnostic tools for machine learning." -AUTHOR = "Rebecca Bilbro, Benjamin Bengfort" -EMAIL = "info@districtdatalabs.com" -MAINTAINER = "Benjamin Bengfort" +AUTHOR = "The scikit-yb developers" +EMAIL = "yellowbrick@googlegroups.com" +MAINTAINER = "The scikit-yb developers" LICENSE = "Apache 2" -REPOSITORY = "https://github.com/districtdatalabs/yellowbrick" +REPOSITORY = "https://github.com/DistrictDataLabs/yellowbrick" PACKAGE = "yellowbrick" +URL = "http://scikit-yb.org/" ## Define the keywords -KEYWORDS = ('visualization', 'machine learning', 'scikit-learn', 'matplotlib', 'data science') +KEYWORDS = ( + "visualization", + "machine learning", + "scikit-learn", + "matplotlib", + "data science", +) ## Define the classifiers ## See https://pypi.python.org/pypi?%3Aaction=list_classifiers CLASSIFIERS = ( - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: Apache Software License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Software Development', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Scientific/Engineering :: Visualization', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Visualization", ) ## Important Paths -PROJECT = os.path.abspath(os.path.dirname(__file__)) +PROJECT = os.path.abspath(os.path.dirname(__file__)) REQUIRE_PATH = "requirements.txt" VERSION_PATH = os.path.join(PACKAGE, "version.py") -PKG_DESCRIBE = "DESCRIPTION.rst" +PKG_DESCRIBE = "DESCRIPTION.md" ## Directories to ignore in find_packages -EXCLUDES = ( - "tests", "bin", "docs", "fixtures", "register", "notebooks", "examples", +EXCLUDES = ( + "tests", + "bin", + "docs", + "fixtures", + "register", + "notebooks", + "examples", + "binder", + "paper", ) ########################################################################## ## Helper Functions ########################################################################## + def read(*parts): """ Assume UTF-8 encoding and return the contents of the file located at the absolute path from the REPOSITORY joined with *parts. """ - with codecs.open(os.path.join(PROJECT, *parts), 'rb', 'utf-8') as f: + with codecs.open(os.path.join(PROJECT, *parts), "rb", "utf-8") as f: return f.read() def get_version(path=VERSION_PATH): """ - Reads the __init__.py defined in the VERSION_PATH to find the get_version - function, and executes it to ensure that it is loaded correctly. + Reads the python file defined in the VERSION_PATH to find the get_version + function, and executes it to ensure that it is loaded correctly. Separating + the version in this way ensures no additional code is executed. """ namespace = {} exec(read(path), namespace) - return namespace['get_version'](short=True) + return namespace["get_version"](short=True) def get_requires(path=REQUIRE_PATH): @@ -101,9 +120,19 @@ def get_requires(path=REQUIRE_PATH): """ for line in read(path).splitlines(): line = line.strip() - if line and not line.startswith('#'): + if line and not line.startswith("#"): yield line + +def get_description_type(path=PKG_DESCRIBE): + """ + Returns the long_description_content_type based on the extension of the + package describe path (e.g. .txt, .rst, or .md). + """ + _, ext = os.path.splitext(path) + return {".rst": "text/x-rst", ".txt": "text/plain", ".md": "text/markdown"}[ext] + + ########################################################################## ## Define the configuration ########################################################################## @@ -113,21 +142,30 @@ def get_requires(path=REQUIRE_PATH): "version": get_version(), "description": DESCRIPTION, "long_description": read(PKG_DESCRIBE), + "long_description_content_type": get_description_type(PKG_DESCRIBE), + "classifiers": CLASSIFIERS, + "keywords": KEYWORDS, "license": LICENSE, "author": AUTHOR, "author_email": EMAIL, + "url": URL, "maintainer": MAINTAINER, "maintainer_email": EMAIL, - "url": REPOSITORY, + "project_urls": { + "Documentation": URL, + "Download": "{}/tarball/v{}".format(REPOSITORY, get_version()), + "Source": REPOSITORY, + "Tracker": "{}/issues".format(REPOSITORY), + }, "download_url": "{}/tarball/v{}".format(REPOSITORY, get_version()), "packages": find_packages(where=PROJECT, exclude=EXCLUDES), - "install_requires": list(get_requires()), - "classifiers": CLASSIFIERS, - "keywords": KEYWORDS, + "package_data": {"yellowbrick": ["datasets/manifest.json"]}, "zip_safe": False, - "scripts": [], - "setup_requires":["pytest-runner"], - "tests_require":["pytest"], + "entry_points": {"console_scripts": []}, + "install_requires": list(get_requires()), + "python_requires": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", + "setup_requires": ["pytest-runner"], + "tests_require": ["pytest"], } @@ -135,5 +173,5 @@ def get_requires(path=REQUIRE_PATH): ## Run setup script ########################################################################## -if __name__ == '__main__': +if __name__ == "__main__": setup(**config) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..f22f58eb0 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,94 @@ +# Yellowbrick Tests + +*Welcome to the Yellowbrick tests!* + +If you're looking for information about how to use Yellowbrick, for our contributor's guide, for examples and teaching resources, for answers to frequently asked questions, and more, please visit the latest version of our documentation at [www.scikit-yb.org](https://www.scikit-yb.org/). + +## Running Yellowbrick Tests + +To run the tests locally, first install the tests-specific requirements with `pip` using the `requirements.txt` file in the `tests` directory: + +``` +$ pip install -r tests/requirements.txt +``` + +The required dependencies for the test suite include testing utilities and libraries such as `pandas` and `nltk` that are not included in the core dependencies. + +Tests can then be run as follows from the project `root`: + +```bash +$ make test +``` + +The Makefile uses the `pytest` runner and testing suite as well as the coverage library. + +## Adding a Test for Your Visualizer + +The `tests` package mirrors the yellowbrick package in structure and also contains several helper methods and base functionality. To add a test to your visualizer, find the corresponding file to add the test case, or create a new test file in the same place you added your code. + +### Visual Tests + +The primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. + +Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. To assist with this, we have two primary helpers, `VisualTestCase` and the `yellowbrick.datasets` module. + +Leverage these helpers to create your tests as follows: + +```python +import pytest + +from tests.base import VisualTestCase +from yellowbrick.datasets import load_occupancy + + +class MyVisualizerTests(VisualTestCase): + + def test_my_visualizer(self): + """ + Test MyVisualizer on a real dataset + """ + # Load the data using the Yellowbrick datasets module + X, y = load_occupancy() + + try: + visualizer = MyVisualizer() + visualizer.fit(X) + visualizer.finalize() + except Exception as e: + pytest.fail("my visualizer didn't work") +``` + +### Image Comparison Tests + +Writing an image-based comparison test is only a little more difficult than the simple test case presented above. We have adapted `matplotlib`'s image comparison test utility into an easy to use assert method: `self.assert_images_similar(visualizer)` + +The main consideration is that you must specify the “baseline” (i.e. expected) image in the `tests/baseline_images/` folder structure. + +For example, let's say you create your tests in `tests/test_regressor/test_myvisualizer.py` as follows: + +```python +from tests.base import VisualTestCase +... + def test_my_visualizer_output(self): + ... + visualizer = MyVisualizer() + visualizer.fit(X) + visualizer.finalize() + self.assert_images_similar(visualizer) +``` + +The first time this test is run, there will be no baseline image to compare against, so the test will fail. Alternatively, if you are making a correction to the existing test `test_my_visualizer_output`, and the correction modifies the resulting test image, the test may also fail to match the existing baseline image. The solution is to first run the tests, then copy the new output images to the correct subdirectory under source code revision control (with `git add`). When rerunning the tests, they should now pass! + +We have a helper script, `tests/images.py` to clean up and manage baseline images automatically. It is run using the ``python -m`` command to execute a module as main, and it takes as an argument the path to **your** test file. To copy the figures as above: + +```bash +$ python -m tests.images tests/test_regressor/test_myvisualizer.py +``` + +This will move all related test images from `actual_images` to `baseline_images` on your behalf (note you'll have had to already run the tests at least once to generate the images). You can also clean up images from both actual and baseline as follows: + +```bash +$ python -m tests.images -C tests/test_regressor/test_myvisualizer.py +``` + +This is useful particularly if you're stuck trying to get an image comparison to work. For more information on the images helper script, use `python -m tests.images --help`. diff --git a/tests/__init__.py b/tests/__init__.py index eb9ab5155..4f88b2007 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,7 +4,7 @@ # Author: Rebecca Bilbro # Created: Wed May 18 10:48:46 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [0c5ba04] benjamin@bengfort.com $ @@ -17,31 +17,30 @@ ## Imports ########################################################################## -import unittest import matplotlib ## IMPORTANT! Set matplotlib to use the Agg backend before imported anywhere! -matplotlib.use('Agg') +matplotlib.use("Agg") ########################################################################## ## Test Constants ########################################################################## -EXPECTED_VERSION = "0.9.1" +EXPECTED_VERSION = "1.0" ########################################################################## ## Initialization Tests ########################################################################## -class InitializationTests(unittest.TestCase): +class TestInitialization(object): def test_sanity(self): """ Test that tests work by confirming 7-3 = 4 """ - self.assertEqual(7-3, 4, "The world went wrong!!") + assert 7 - 3 == 4, "The world went wrong!!" def test_import(self): """ @@ -58,6 +57,7 @@ def test_version(self): """ try: import yellowbrick as yb - self.assertEqual(yb.__version__, EXPECTED_VERSION) + + assert yb.__version__ == EXPECTED_VERSION except ImportError: self.fail("Could not import the yellowbrick library!") diff --git a/tests/base.py b/tests/base.py index 449def1ef..09ae206b6 100644 --- a/tests/base.py +++ b/tests/base.py @@ -15,19 +15,32 @@ ########################################################################## import os +import sys import inspect -import unittest import matplotlib as mpl import matplotlib.pyplot as plt from matplotlib import ticker -from matplotlib import rcParams - from matplotlib.testing.compare import compare_images from yellowbrick.exceptions import ImageComparisonFailure +########################################################################## +## Environment +########################################################################## + + +def is_windows_or_conda(): + """ + Simple detection mechanism to determine if the tests are running in a + win32 or Anaconda/Miniconda environment. + """ + is_windows = sys.platform == "win32" + is_conda = os.path.exists(os.path.join(sys.prefix, "conda-meta")) + return is_windows or is_conda + + ########################################################################## ## Module Constants ########################################################################## @@ -36,51 +49,49 @@ TESTS = os.path.dirname(__file__) ACTUAL_IMAGES = os.path.join(TESTS, "actual_images") BASELINE_IMAGES = os.path.join(TESTS, "baseline_images") +IS_WINDOWS_OR_CONDA = is_windows_or_conda() ########################################################################## ## Visual Test Case ########################################################################## -class VisualTestCase(unittest.TestCase): - @classmethod - def setUpClass(klass): - """ - In order for tests to pass on Travis-CI we must use the 'Agg' - matplotlib backend. This setup function ensures that all tests - that do visual work setup the backend correctly. +class VisualTestCase(object): + """ + The visual test case class ensures that all tests inside of the class + can execute image similarity tests inside of a clean matplotlib global + figure. + """ - Note: + def setup_method(self): """ - klass._backend = mpl.get_backend() - super(VisualTestCase, klass).setUpClass() + Before a visual test case method is run, ensure that the previous + figure is closed and the current axes are cleared. - def setUp(self): - """ - Assert tthat the backend is 'Agg' and close all previous plots + See: https://docs.pytest.org/en/latest/xunit_setup.html """ # Reset the matplotlib environment - plt.cla() # clear current axis - plt.clf() # clear current figure - plt.close("all") # close all existing plots - - # Travis-CI does not have san-serif - rcParams['font.family'] = 'DejaVu Sans' - - # Assert that the backend is agg - self.assertEqual(self._backend, 'agg') - super(VisualTestCase, self).setUp() - - def assert_images_similar(self, visualizer=None, ax=None, tol=0.01, **kwargs): + plt.cla() # clear current axis + plt.clf() # clear current figure + plt.close("all") # close all existing plots + + # Travis-CI does not have san-serif so ensure standard fonts are used. + # Note that this must be set before each test otherwise it will be reset by + # the Yellowbrick styles. + mpl.rcParams["font.family"] = "DejaVu Sans" + + def assert_images_similar( + self, visualizer=None, ax=None, tol=0.01, windows_tol=None, **kwargs + ): """Accessible testing method for testing generation of a Visualizer. Requires the placement of a baseline image for comparison in the tests/baseline_images folder that corresponds to the module path of the VisualTestCase being evaluated. The name of the image corresponds to - the unittest function where "self.assert_images_similar" is called. + the test function where "self.assert_images_similar" is called. - For example, calling "assert_images_similar" in the unittest + For example, calling "assert_images_similar" in the test function "test_class_report" in tests.test_classifier.test_class_balance would require placement a baseline image at: @@ -92,18 +103,22 @@ def assert_images_similar(self, visualizer=None, ax=None, tol=0.01, **kwargs): actual_images/ - visualizer : yellowbrick visualizer + visualizer : yellowbrick visualizer, default: None An instantiated yellowbrick visualizer that has been fitted, transformed and had all operations except for poof called on it. ax : matplotlib Axes, default: None The axis to plot the figure on. - tol : float + tol : float, default: 0.01 The tolerance (a color value difference, where 255 is the maximal difference). The test fails if the average pixel difference is greater than this value. + windows_tol: float, default: None + Similar to the tol parameter, but targeted for testing on a + windows environment. + kwargs : dict Options to pass to the ImageComparison class. """ @@ -112,7 +127,12 @@ def assert_images_similar(self, visualizer=None, ax=None, tol=0.01, **kwargs): # Build and execute the image comparison compare = ImageComparison( - inspect.stack(), visualizer=visualizer, ax=ax, tol=tol, **kwargs + inspect.stack(), + visualizer=visualizer, + ax=ax, + tol=tol, + windows_tol=windows_tol, + **kwargs ) compare() @@ -124,6 +144,7 @@ def assert_images_similar(self, visualizer=None, ax=None, tol=0.01, **kwargs): ## Image Comparison Test ########################################################################## + class ImageComparison(object): """ An image comparison wraps a single ``assert_images_similar`` statement to @@ -153,6 +174,9 @@ class ImageComparison(object): difference. The test fails if the average pixel difference is greater than this value. + windows_tol : float, default: 0.01 + The tolerance (tol) parameter for the windows operating system environment. + ext : string, default: ".png" The file extension to save the actual and baseline images as. @@ -164,19 +188,34 @@ class ImageComparison(object): remove_title : bool, default: True Remove the title since different OS may have varying fonts. + remove_labels : bool, default: True + Remove the x and y labels since different OS may have varying fonts. + + remove_legend : bool, default: True + Remove the legend since different OS may have varying fonts. + Raises ------ ValueError : at least one of visualizer or ax must be specified. """ - def __init__(self, stack, visualizer=None, ax=None, tol=0.01, ext=".png", - remove_ticks=True, remove_title=True, remove_legend=False): + def __init__( + self, + stack, + visualizer=None, + ax=None, + tol=0.01, + windows_tol=0.01, + ext=".png", + remove_ticks=True, + remove_title=True, + remove_labels=True, + remove_legend=True, + ): # Ensure we have something to draw on if visualizer is None and ax is None: - raise ValueError( - "at least one of visualizer or ax must be specified" - ) + raise ValueError("at least one of visualizer or ax must be specified") # Save the ax being drawn on self.ax = ax or visualizer.ax @@ -188,10 +227,8 @@ def __init__(self, stack, visualizer=None, ax=None, tol=0.01, ext=".png", # FrameInfo(frame, filename, lineno, function, code_context, index) self.test_func_name = frame[3] - if not self.test_func_name.startswith('test'): - raise ValueError( - "{} is not a test function".format(self.test_func_name) - ) + if not self.test_func_name.startswith("test"): + raise ValueError("{} is not a test function".format(self.test_func_name)) # Find the relative path to the Yellowbrick tests to compute the # module name for storing images in the actual and baseline dirs. @@ -199,11 +236,17 @@ def __init__(self, stack, visualizer=None, ax=None, tol=0.01, ext=".png", module_path = os.path.relpath(frame[1], root) self.test_module_path = os.path.splitext(module_path)[0] + # Set the error tolerance depending on the os + if os.name == "nt" and windows_tol is not None: + self.tol = windows_tol + else: + self.tol = tol + # Save other image comparison properties - self.tol = tol self.ext = ext self.remove_ticks = remove_ticks self.remove_title = remove_title + self.remove_labels = remove_labels self.remove_legend = remove_legend def __call__(self): @@ -271,7 +314,11 @@ def cleanup(self): except AttributeError: continue - if self.remove_legend: + if self.remove_labels: + self.ax.set_xlabel("") + self.ax.set_ylabel("") + + if self.remove_legend and self.ax.get_legend() is not None: self.ax.legend_.remove() def save(self): @@ -302,18 +349,18 @@ def compare(self): # Ensure we have an image to compare against (common failure) if not os.path.exists(expected): raise ImageComparisonFailure( - 'baseline image does not exist:\n{}'.format(os.path.relpath(expected)) + "baseline image does not exist:\n{}".format(os.path.relpath(expected)) ) - # Perform the comparison err = compare_images(expected, actual, self.tol, in_decorator=True) # Raise image comparison failure if not close if err: - for key in ('actual', 'expected'): + for key in ("actual", "expected"): err[key] = os.path.relpath(err[key]) - raise ImageComparisonFailure(( - "images not close (RMS {rms:0.3f})" - "\n{actual}\n\tvs\n{expected}" - ).format(**err)) + raise ImageComparisonFailure( + ( + "images not close (RMS {rms:0.3f})" "\n{actual}\n\tvs\n{expected}" + ).format(**err) + ) diff --git a/tests/baseline_images/test_base/test_draw_visualizer_grid.png b/tests/baseline_images/test_base/test_draw_visualizer_grid.png index 074c416ed..fc1735ca9 100644 Binary files a/tests/baseline_images/test_base/test_draw_visualizer_grid.png and b/tests/baseline_images/test_base/test_draw_visualizer_grid.png differ diff --git a/tests/baseline_images/test_base/test_draw_with_cols.png b/tests/baseline_images/test_base/test_draw_with_cols.png index 6188b4773..8a396f981 100644 Binary files a/tests/baseline_images/test_base/test_draw_with_cols.png and b/tests/baseline_images/test_base/test_draw_with_cols.png differ diff --git a/tests/baseline_images/test_base/test_draw_with_rows.png b/tests/baseline_images/test_base/test_draw_with_rows.png index 2c9af4d8b..fc82c6bd5 100644 Binary files a/tests/baseline_images/test_base/test_draw_with_rows.png and b/tests/baseline_images/test_base/test_draw_with_rows.png differ diff --git a/tests/baseline_images/test_classifier/test_class_balance/test_class_report.png b/tests/baseline_images/test_classifier/test_class_balance/test_class_report.png deleted file mode 100644 index 99aa71a13..000000000 Binary files a/tests/baseline_images/test_classifier/test_class_balance/test_class_report.png and /dev/null differ diff --git a/tests/baseline_images/test_classifier/test_class_prediction_error/test_class_prediction_error_quickmethod.png b/tests/baseline_images/test_classifier/test_class_prediction_error/test_class_prediction_error_quickmethod.png index 0aff817b3..44ccb4622 100644 Binary files a/tests/baseline_images/test_classifier/test_class_prediction_error/test_class_prediction_error_quickmethod.png and b/tests/baseline_images/test_classifier/test_class_prediction_error/test_class_prediction_error_quickmethod.png differ diff --git a/tests/baseline_images/test_classifier/test_class_prediction_error/test_integration_class_prediction_error_.png b/tests/baseline_images/test_classifier/test_class_prediction_error/test_integration_class_prediction_error_.png deleted file mode 100644 index 0cc3c3c7e..000000000 Binary files a/tests/baseline_images/test_classifier/test_class_prediction_error/test_integration_class_prediction_error_.png and /dev/null differ diff --git a/tests/baseline_images/test_classifier/test_class_prediction_error/test_numpy_integration.png b/tests/baseline_images/test_classifier/test_class_prediction_error/test_numpy_integration.png new file mode 100644 index 000000000..6163791df Binary files /dev/null and b/tests/baseline_images/test_classifier/test_class_prediction_error/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_class_prediction_error/test_pandas_integration.png b/tests/baseline_images/test_classifier/test_class_prediction_error/test_pandas_integration.png new file mode 100644 index 000000000..6163791df Binary files /dev/null and b/tests/baseline_images/test_classifier/test_class_prediction_error/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png b/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png index cb2627f17..0b02c243d 100644 Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png and b/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png differ diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png b/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png index e8cc21ae8..42829ff4c 100644 Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png and b/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png differ diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_numpy_integration.png b/tests/baseline_images/test_classifier/test_classification_report/test_numpy_integration.png new file mode 100644 index 000000000..1689d989f Binary files /dev/null and b/tests/baseline_images/test_classifier/test_classification_report/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png b/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png index 92a9fe7a4..1689d989f 100644 Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png and b/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png b/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png index aef0f8cbc..62b525b6e 100644 Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png and b/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png differ diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_support_count_class_report.png b/tests/baseline_images/test_classifier/test_classification_report/test_support_count_class_report.png index 31917cbc9..051058361 100644 Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_support_count_class_report.png and b/tests/baseline_images/test_classifier/test_classification_report/test_support_count_class_report.png differ diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_support_percent_class_report.png b/tests/baseline_images/test_classifier/test_classification_report/test_support_percent_class_report.png index e6a39c38d..5eb75929c 100644 Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_support_percent_class_report.png and b/tests/baseline_images/test_classifier/test_classification_report/test_support_percent_class_report.png differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_class_filter_eg_zoom_in.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_class_filter_eg_zoom_in.png deleted file mode 100644 index d25e29d54..000000000 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_class_filter_eg_zoom_in.png and /dev/null differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_confusion_matrix.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_confusion_matrix.png index 19cd37aa2..7bb3d23ef 100644 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_confusion_matrix.png and b/tests/baseline_images/test_classifier/test_confusion_matrix/test_confusion_matrix.png differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_extra_classes.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_extra_classes.png deleted file mode 100644 index 8dce1720e..000000000 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_extra_classes.png and /dev/null differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_fontsize.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_fontsize.png index dcf208634..594f8f2ef 100644 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_fontsize.png and b/tests/baseline_images/test_classifier/test_confusion_matrix/test_fontsize.png differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_no_classes_provided.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_no_classes_provided.png index 13ac38f7c..e366a7234 100644 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_no_classes_provided.png and b/tests/baseline_images/test_classifier/test_confusion_matrix/test_no_classes_provided.png differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_one_class.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_one_class.png deleted file mode 100644 index c8fbe9cca..000000000 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_one_class.png and /dev/null differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_pandas_integration.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_pandas_integration.png index 657b2c678..87b947763 100644 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_pandas_integration.png and b/tests/baseline_images/test_classifier/test_confusion_matrix/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_percent_mode.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_percent_mode.png index c31609fcc..7b0acd674 100644 Binary files a/tests/baseline_images/test_classifier/test_confusion_matrix/test_percent_mode.png and b/tests/baseline_images/test_classifier/test_confusion_matrix/test_percent_mode.png differ diff --git a/tests/baseline_images/test_classifier/test_confusion_matrix/test_quick_method.png b/tests/baseline_images/test_classifier/test_confusion_matrix/test_quick_method.png new file mode 100644 index 000000000..326d33380 Binary files /dev/null and b/tests/baseline_images/test_classifier/test_confusion_matrix/test_quick_method.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_binary_decision.png b/tests/baseline_images/test_classifier/test_prcurve/test_binary_decision.png index 6ae4fc789..ddff9660d 100644 Binary files a/tests/baseline_images/test_classifier/test_prcurve/test_binary_decision.png and b/tests/baseline_images/test_classifier/test_prcurve/test_binary_decision.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability.png b/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability.png index 951ed3b3b..68ef5f09f 100644 Binary files a/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability.png and b/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability_decision.png b/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability_decision.png index 0dd8b102c..8395a9158 100644 Binary files a/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability_decision.png and b/tests/baseline_images/test_classifier/test_prcurve/test_binary_probability_decision.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_custom_iso_f1_scores.png b/tests/baseline_images/test_classifier/test_prcurve/test_custom_iso_f1_scores.png new file mode 100644 index 000000000..8e3cf2a1a Binary files /dev/null and b/tests/baseline_images/test_classifier/test_prcurve/test_custom_iso_f1_scores.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_decision.png b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_decision.png index e05a1bc9b..f886a9f46 100644 Binary files a/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_decision.png and b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_decision.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability.png b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability.png index ea7734e76..b27b94c85 100644 Binary files a/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability.png and b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability_with_class_labels.png b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability_with_class_labels.png new file mode 100644 index 000000000..d28eded67 Binary files /dev/null and b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability_with_class_labels.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_pandas_integration.png b/tests/baseline_images/test_classifier/test_prcurve/test_pandas_integration.png new file mode 100644 index 000000000..fdbfea5d7 Binary files /dev/null and b/tests/baseline_images/test_classifier/test_prcurve/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_quick_method.png b/tests/baseline_images/test_classifier/test_prcurve/test_quick_method.png index c9bec66f6..a0ceab7ab 100644 Binary files a/tests/baseline_images/test_classifier/test_prcurve/test_quick_method.png and b/tests/baseline_images/test_classifier/test_prcurve/test_quick_method.png differ diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_quick_method_with_test_set.png b/tests/baseline_images/test_classifier/test_prcurve/test_quick_method_with_test_set.png new file mode 100644 index 000000000..4462ffa95 Binary files /dev/null and b/tests/baseline_images/test_classifier/test_prcurve/test_quick_method_with_test_set.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_binary_decision.png b/tests/baseline_images/test_classifier/test_rocauc/test_binary_decision.png index 548ecdb5a..3d813ea40 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_binary_decision.png and b/tests/baseline_images/test_classifier/test_rocauc/test_binary_decision.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability.png b/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability.png index d839dfafa..6da7abe38 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability.png and b/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability_decision.png b/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability_decision.png index 5f6a39544..68a7f131f 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability_decision.png and b/tests/baseline_images/test_classifier/test_rocauc/test_binary_probability_decision.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_multiclass_rocauc.png b/tests/baseline_images/test_classifier/test_rocauc/test_multiclass_rocauc.png index 09f9cb975..244d17c49 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_multiclass_rocauc.png and b/tests/baseline_images/test_classifier/test_rocauc/test_multiclass_rocauc.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_pandas_integration.png b/tests/baseline_images/test_classifier/test_rocauc/test_pandas_integration.png new file mode 100644 index 000000000..4751eeb60 Binary files /dev/null and b/tests/baseline_images/test_classifier/test_rocauc/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_classes.png b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_classes.png index 253c0e143..f1fda54fd 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_classes.png and b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_classes.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro.png b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro.png index a04d729be..d3a200f63 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro.png and b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro_no_micro.png b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro_no_micro.png index 092254631..fc46463e0 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro_no_micro.png and b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_macro_no_micro.png differ diff --git a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_micro.png b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_micro.png index 8f45199a8..1dfbe770c 100644 Binary files a/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_micro.png and b/tests/baseline_images/test_classifier/test_rocauc/test_rocauc_no_micro.png differ diff --git a/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold.png b/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold.png index aeb6d54ca..a709220d5 100644 Binary files a/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold.png and b/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold.png differ diff --git a/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold_alt_args.png b/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold_alt_args.png index 0c73c5e0f..4ef0de49e 100644 Binary files a/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold_alt_args.png and b/tests/baseline_images/test_classifier/test_threshold/test_binary_discrimination_threshold_alt_args.png differ diff --git a/tests/baseline_images/test_classifier/test_threshold/test_numpy_integration.png b/tests/baseline_images/test_classifier/test_threshold/test_numpy_integration.png new file mode 100644 index 000000000..661aaf2ea Binary files /dev/null and b/tests/baseline_images/test_classifier/test_threshold/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_threshold/test_pandas_integration.png b/tests/baseline_images/test_classifier/test_threshold/test_pandas_integration.png index 438e7a90a..661aaf2ea 100644 Binary files a/tests/baseline_images/test_classifier/test_threshold/test_pandas_integration.png and b/tests/baseline_images/test_classifier/test_threshold/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_classifier/test_threshold/test_quick_method.png b/tests/baseline_images/test_classifier/test_threshold/test_quick_method.png index ebf541f64..97b869729 100644 Binary files a/tests/baseline_images/test_classifier/test_threshold/test_quick_method.png and b/tests/baseline_images/test_classifier/test_threshold/test_quick_method.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_calinski_harabasz_metric.png b/tests/baseline_images/test_cluster/test_elbow/test_calinski_harabasz_metric.png new file mode 100644 index 000000000..071d1b8e0 Binary files /dev/null and b/tests/baseline_images/test_cluster/test_elbow/test_calinski_harabasz_metric.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_calinski_harabaz_metric.png b/tests/baseline_images/test_cluster/test_elbow/test_calinski_harabaz_metric.png deleted file mode 100644 index cedba48d3..000000000 Binary files a/tests/baseline_images/test_cluster/test_elbow/test_calinski_harabaz_metric.png and /dev/null differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_distortion_metric.png b/tests/baseline_images/test_cluster/test_elbow/test_distortion_metric.png index 0876dcb86..11d937448 100644 Binary files a/tests/baseline_images/test_cluster/test_elbow/test_distortion_metric.png and b/tests/baseline_images/test_cluster/test_elbow/test_distortion_metric.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png b/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png index 764411681..95a602a93 100644 Binary files a/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png and b/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png b/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png index 5fe2017a2..d9342b40b 100644 Binary files a/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png and b/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_locate_elbow.png b/tests/baseline_images/test_cluster/test_elbow/test_locate_elbow.png new file mode 100644 index 000000000..1ebd4b25b Binary files /dev/null and b/tests/baseline_images/test_cluster/test_elbow/test_locate_elbow.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_quick_method.png b/tests/baseline_images/test_cluster/test_elbow/test_quick_method.png new file mode 100644 index 000000000..bfc4d46f7 Binary files /dev/null and b/tests/baseline_images/test_cluster/test_elbow/test_quick_method.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_silhouette_metric.png b/tests/baseline_images/test_cluster/test_elbow/test_silhouette_metric.png index c0220b6e7..0b22bbe71 100644 Binary files a/tests/baseline_images/test_cluster/test_elbow/test_silhouette_metric.png and b/tests/baseline_images/test_cluster/test_elbow/test_silhouette_metric.png differ diff --git a/tests/baseline_images/test_cluster/test_elbow/test_timings.png b/tests/baseline_images/test_cluster/test_elbow/test_timings.png index b6dc5f622..540741d2f 100644 Binary files a/tests/baseline_images/test_cluster/test_elbow/test_timings.png and b/tests/baseline_images/test_cluster/test_elbow/test_timings.png differ diff --git a/tests/baseline_images/test_cluster/test_icdm/test_affinity_tsne_no_legend.png b/tests/baseline_images/test_cluster/test_icdm/test_affinity_tsne_no_legend.png index 0d0933d52..718577028 100644 Binary files a/tests/baseline_images/test_cluster/test_icdm/test_affinity_tsne_no_legend.png and b/tests/baseline_images/test_cluster/test_icdm/test_affinity_tsne_no_legend.png differ diff --git a/tests/baseline_images/test_cluster/test_icdm/test_kmeans_mds.png b/tests/baseline_images/test_cluster/test_icdm/test_kmeans_mds.png index 12cc087d3..609730905 100644 Binary files a/tests/baseline_images/test_cluster/test_icdm/test_kmeans_mds.png and b/tests/baseline_images/test_cluster/test_icdm/test_kmeans_mds.png differ diff --git a/tests/baseline_images/test_cluster/test_icdm/test_quick_method.png b/tests/baseline_images/test_cluster/test_icdm/test_quick_method.png index 08388f3fa..c807f2d62 100644 Binary files a/tests/baseline_images/test_cluster/test_icdm/test_quick_method.png and b/tests/baseline_images/test_cluster/test_icdm/test_quick_method.png differ diff --git a/tests/baseline_images/test_cluster/test_silhouette/test_colormap_as_colors_silhouette.png b/tests/baseline_images/test_cluster/test_silhouette/test_colormap_as_colors_silhouette.png new file mode 100644 index 000000000..b8ac7f86a Binary files /dev/null and b/tests/baseline_images/test_cluster/test_silhouette/test_colormap_as_colors_silhouette.png differ diff --git a/tests/baseline_images/test_cluster/test_silhouette/test_colormap_silhouette.png b/tests/baseline_images/test_cluster/test_silhouette/test_colormap_silhouette.png new file mode 100644 index 000000000..c4189cadc Binary files /dev/null and b/tests/baseline_images/test_cluster/test_silhouette/test_colormap_silhouette.png differ diff --git a/tests/baseline_images/test_cluster/test_silhouette/test_colors_silhouette.png b/tests/baseline_images/test_cluster/test_silhouette/test_colors_silhouette.png new file mode 100644 index 000000000..c47f335a6 Binary files /dev/null and b/tests/baseline_images/test_cluster/test_silhouette/test_colors_silhouette.png differ diff --git a/tests/baseline_images/test_cluster/test_silhouette/test_integrated_kmeans_silhouette.png b/tests/baseline_images/test_cluster/test_silhouette/test_integrated_kmeans_silhouette.png index 8de5e3a96..81647ec4e 100644 Binary files a/tests/baseline_images/test_cluster/test_silhouette/test_integrated_kmeans_silhouette.png and b/tests/baseline_images/test_cluster/test_silhouette/test_integrated_kmeans_silhouette.png differ diff --git a/tests/baseline_images/test_cluster/test_silhouette/test_integrated_mini_batch_kmeans_silhouette.png b/tests/baseline_images/test_cluster/test_silhouette/test_integrated_mini_batch_kmeans_silhouette.png index 5c9a89bb6..d2f717148 100644 Binary files a/tests/baseline_images/test_cluster/test_silhouette/test_integrated_mini_batch_kmeans_silhouette.png and b/tests/baseline_images/test_cluster/test_silhouette/test_integrated_mini_batch_kmeans_silhouette.png differ diff --git a/tests/baseline_images/test_cluster/test_silhouette/test_quick_method.png b/tests/baseline_images/test_cluster/test_silhouette/test_quick_method.png new file mode 100644 index 000000000..7ad3a33bd Binary files /dev/null and b/tests/baseline_images/test_cluster/test_silhouette/test_quick_method.png differ diff --git a/tests/baseline_images/test_contrib/test_classifier/test_boundaries/test_real_data_set_viz.png b/tests/baseline_images/test_contrib/test_classifier/test_boundaries/test_real_data_set_viz.png index 17934f217..d898a0e35 100644 Binary files a/tests/baseline_images/test_contrib/test_classifier/test_boundaries/test_real_data_set_viz.png and b/tests/baseline_images/test_contrib/test_classifier/test_boundaries/test_real_data_set_viz.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy.png b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy.png index e78b7b429..01dd7f82e 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy.png and b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target.png b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target.png index 1f37cc733..9f23f28bb 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target.png and b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target_with_labels.png b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target_with_labels.png index 75d04ce36..9f23f28bb 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target_with_labels.png and b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_numpy_with_y_target_with_labels.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_pandas.png b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_pandas.png index 8a0f764d5..1fb2c743f 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_pandas.png and b/tests/baseline_images/test_contrib/test_missing/test_bar/test_missingvaluesbar_pandas.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy.png b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy.png index 3ae92423d..3b6d089e2 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy.png and b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy_with_y_targets.png b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy_with_y_targets.png index 569624b93..edadc8f09 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy_with_y_targets.png and b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_numpy_with_y_targets.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas.png b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas.png index ddc0eb338..c82ecefaf 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas.png and b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas.png differ diff --git a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas_with_y_targets.png b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas_with_y_targets.png index 3b4d52b43..0d293e939 100644 Binary files a/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas_with_y_targets.png and b/tests/baseline_images/test_contrib/test_missing/test_dispersion/test_missingvaluesdispersion_with_pandas_with_y_targets.png differ diff --git a/tests/baseline_images/test_contrib/test_scatter/test_scatter_image.png b/tests/baseline_images/test_contrib/test_scatter/test_scatter_image.png index 93839a259..561d91d88 100644 Binary files a/tests/baseline_images/test_contrib/test_scatter/test_scatter_image.png and b/tests/baseline_images/test_contrib/test_scatter/test_scatter_image.png differ diff --git a/tests/baseline_images/test_contrib/test_scatter/test_scatter_image_fail.png b/tests/baseline_images/test_contrib/test_scatter/test_scatter_image_fail.png deleted file mode 100644 index 04cde26de..000000000 Binary files a/tests/baseline_images/test_contrib/test_scatter/test_scatter_image_fail.png and /dev/null differ diff --git a/tests/baseline_images/test_draw/test_horizontal_bar_stack.png b/tests/baseline_images/test_draw/test_horizontal_bar_stack.png new file mode 100644 index 000000000..f7317dedd Binary files /dev/null and b/tests/baseline_images/test_draw/test_horizontal_bar_stack.png differ diff --git a/tests/baseline_images/test_draw/test_labels_horizontal.png b/tests/baseline_images/test_draw/test_labels_horizontal.png new file mode 100644 index 000000000..efa9982f6 Binary files /dev/null and b/tests/baseline_images/test_draw/test_labels_horizontal.png differ diff --git a/tests/baseline_images/test_draw/test_labels_vertical.png b/tests/baseline_images/test_draw/test_labels_vertical.png new file mode 100644 index 000000000..eecd0202e Binary files /dev/null and b/tests/baseline_images/test_draw/test_labels_vertical.png differ diff --git a/tests/baseline_images/test_draw/test_manual_legend.png b/tests/baseline_images/test_draw/test_manual_legend.png index f0682256a..65bf8b975 100644 Binary files a/tests/baseline_images/test_draw/test_manual_legend.png and b/tests/baseline_images/test_draw/test_manual_legend.png differ diff --git a/tests/baseline_images/test_draw/test_single_row_bar_stack.png b/tests/baseline_images/test_draw/test_single_row_bar_stack.png new file mode 100644 index 000000000..e4475de34 Binary files /dev/null and b/tests/baseline_images/test_draw/test_single_row_bar_stack.png differ diff --git a/tests/baseline_images/test_draw/test_vertical_bar_stack.png b/tests/baseline_images/test_draw/test_vertical_bar_stack.png new file mode 100644 index 000000000..8148df34f Binary files /dev/null and b/tests/baseline_images/test_draw/test_vertical_bar_stack.png differ diff --git a/tests/baseline_images/test_features/test_importances/test_integration_coef.png b/tests/baseline_images/test_features/test_importances/test_integration_coef.png deleted file mode 100644 index 67ca51bc1..000000000 Binary files a/tests/baseline_images/test_features/test_importances/test_integration_coef.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_importances/test_integration_feature_importances.png b/tests/baseline_images/test_features/test_importances/test_integration_feature_importances.png deleted file mode 100644 index 9879e019c..000000000 Binary files a/tests/baseline_images/test_features/test_importances/test_integration_feature_importances.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_importances/test_integration_quick_method.png b/tests/baseline_images/test_features/test_importances/test_integration_quick_method.png deleted file mode 100644 index c388bff47..000000000 Binary files a/tests/baseline_images/test_features/test_importances/test_integration_quick_method.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_importances/test_multi_coefs_stacked.png b/tests/baseline_images/test_features/test_importances/test_multi_coefs_stacked.png deleted file mode 100644 index 7293b6dce..000000000 Binary files a/tests/baseline_images/test_features/test_importances/test_multi_coefs_stacked.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_continuous_y.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_continuous_y.png new file mode 100644 index 000000000..2bc44d0c5 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_continuous_y.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_continuous_y_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_continuous_y_hist.png new file mode 100644 index 000000000..ad00ec273 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_continuous_y_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_discrete_y.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_discrete_y.png new file mode 100644 index 000000000..f4df04e2b Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_discrete_y.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_discrete_y_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_discrete_y_hist.png new file mode 100644 index 000000000..14c6ac802 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_index_discrete_y_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_int_index_numpy_no_y.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_int_index_numpy_no_y.png new file mode 100644 index 000000000..f4df04e2b Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_int_index_numpy_no_y.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_int_index_numpy_no_y_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_int_index_numpy_no_y_hist.png new file mode 100644 index 000000000..14c6ac802 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_int_index_numpy_no_y_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_str_index_pandas_no_y.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_str_index_pandas_no_y.png new file mode 100644 index 000000000..2bc44d0c5 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_str_index_pandas_no_y.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_double_str_index_pandas_no_y_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_double_str_index_pandas_no_y_hist.png new file mode 100644 index 000000000..ad00ec273 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_double_str_index_pandas_no_y_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_none_x.png b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x.png new file mode 100644 index 000000000..f4df04e2b Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_hist.png new file mode 100644 index 000000000..14c6ac802 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_y.png b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_y.png new file mode 100644 index 000000000..0ab687b5a Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_y.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_y_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_y_hist.png new file mode 100644 index 000000000..abc472a97 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_none_x_y_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_single_int_index_numpy.png b/tests/baseline_images/test_features/test_jointplot/test_columns_single_int_index_numpy.png new file mode 100644 index 000000000..5d30c2762 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_single_int_index_numpy.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_single_int_index_numpy_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_single_int_index_numpy_hist.png new file mode 100644 index 000000000..9d2788c43 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_single_int_index_numpy_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_single_str_index_pandas.png b/tests/baseline_images/test_features/test_jointplot/test_columns_single_str_index_pandas.png new file mode 100644 index 000000000..6ae4778a5 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_single_str_index_pandas.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_columns_single_str_index_pandas_hist.png b/tests/baseline_images/test_features/test_jointplot/test_columns_single_str_index_pandas_hist.png new file mode 100644 index 000000000..0e04db612 Binary files /dev/null and b/tests/baseline_images/test_features/test_jointplot/test_columns_single_str_index_pandas_hist.png differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_jointplot_has_no_errors.png b/tests/baseline_images/test_features/test_jointplot/test_jointplot_has_no_errors.png deleted file mode 100644 index ec2dfd703..000000000 Binary files a/tests/baseline_images/test_features/test_jointplot/test_jointplot_has_no_errors.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_jointplot/test_jointplot_integrated_has_no_errors.png b/tests/baseline_images/test_features/test_jointplot/test_jointplot_integrated_has_no_errors.png deleted file mode 100644 index 3eb043bdb..000000000 Binary files a/tests/baseline_images/test_features/test_jointplot/test_jointplot_integrated_has_no_errors.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_manifold/test_manifold_classification.png b/tests/baseline_images/test_features/test_manifold/test_manifold_classification.png index d78bf790f..b144c8480 100644 Binary files a/tests/baseline_images/test_features/test_manifold/test_manifold_classification.png and b/tests/baseline_images/test_features/test_manifold/test_manifold_classification.png differ diff --git a/tests/baseline_images/test_features/test_manifold/test_manifold_classification_3d.png b/tests/baseline_images/test_features/test_manifold/test_manifold_classification_3d.png new file mode 100644 index 000000000..bc1b5ffea Binary files /dev/null and b/tests/baseline_images/test_features/test_manifold/test_manifold_classification_3d.png differ diff --git a/tests/baseline_images/test_features/test_manifold/test_manifold_pandas.png b/tests/baseline_images/test_features/test_manifold/test_manifold_pandas.png index c28d5137c..40abd6462 100644 Binary files a/tests/baseline_images/test_features/test_manifold/test_manifold_pandas.png and b/tests/baseline_images/test_features/test_manifold/test_manifold_pandas.png differ diff --git a/tests/baseline_images/test_features/test_manifold/test_manifold_regression.png b/tests/baseline_images/test_features/test_manifold/test_manifold_regression.png index 5b0ecae7e..d30fa174b 100644 Binary files a/tests/baseline_images/test_features/test_manifold/test_manifold_regression.png and b/tests/baseline_images/test_features/test_manifold/test_manifold_regression.png differ diff --git a/tests/baseline_images/test_features/test_manifold/test_manifold_regression_3d.png b/tests/baseline_images/test_features/test_manifold/test_manifold_regression_3d.png new file mode 100644 index 000000000..9550de9b5 Binary files /dev/null and b/tests/baseline_images/test_features/test_manifold/test_manifold_regression_3d.png differ diff --git a/tests/baseline_images/test_features/test_manifold/test_manifold_single.png b/tests/baseline_images/test_features/test_manifold/test_manifold_single.png index 716dd2073..4ff3243a8 100644 Binary files a/tests/baseline_images/test_features/test_manifold/test_manifold_single.png and b/tests/baseline_images/test_features/test_manifold/test_manifold_single.png differ diff --git a/tests/baseline_images/test_features/test_manifold/test_manifold_single_3d.png b/tests/baseline_images/test_features/test_manifold/test_manifold_single_3d.png new file mode 100644 index 000000000..6881c983b Binary files /dev/null and b/tests/baseline_images/test_features/test_manifold/test_manifold_single_3d.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_biplot_2d.png b/tests/baseline_images/test_features/test_pca/test_biplot_2d.png index 3e2b58f6c..551dd59e2 100644 Binary files a/tests/baseline_images/test_features/test_pca/test_biplot_2d.png and b/tests/baseline_images/test_features/test_pca/test_biplot_2d.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_biplot_3d.png b/tests/baseline_images/test_features/test_pca/test_biplot_3d.png index 0eafad7f5..93b79f1a1 100644 Binary files a/tests/baseline_images/test_features/test_pca/test_biplot_3d.png and b/tests/baseline_images/test_features/test_pca/test_biplot_3d.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_colorbar.png b/tests/baseline_images/test_features/test_pca/test_colorbar.png new file mode 100644 index 000000000..000145934 Binary files /dev/null and b/tests/baseline_images/test_features/test_pca/test_colorbar.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_colorbar_heatmap.png b/tests/baseline_images/test_features/test_pca/test_colorbar_heatmap.png new file mode 100644 index 000000000..af198ba5e Binary files /dev/null and b/tests/baseline_images/test_features/test_pca/test_colorbar_heatmap.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_continuous.png b/tests/baseline_images/test_features/test_pca/test_continuous.png new file mode 100644 index 000000000..609714cef Binary files /dev/null and b/tests/baseline_images/test_features/test_pca/test_continuous.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_discrete.png b/tests/baseline_images/test_features/test_pca/test_discrete.png new file mode 100644 index 000000000..eb66353c9 Binary files /dev/null and b/tests/baseline_images/test_features/test_pca/test_discrete.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_heatmap.png b/tests/baseline_images/test_features/test_pca/test_heatmap.png new file mode 100644 index 000000000..e3c564521 Binary files /dev/null and b/tests/baseline_images/test_features/test_pca/test_heatmap.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_pca_decomposition_quick_method.png b/tests/baseline_images/test_features/test_pca/test_pca_decomposition_quick_method.png index 38ae6a50e..60db53d0f 100644 Binary files a/tests/baseline_images/test_features/test_pca/test_pca_decomposition_quick_method.png and b/tests/baseline_images/test_features/test_pca/test_pca_decomposition_quick_method.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_scale_false_2d.png b/tests/baseline_images/test_features/test_pca/test_scale_false_2d.png index 7b478681f..83c7fa2f7 100644 Binary files a/tests/baseline_images/test_features/test_pca/test_scale_false_2d.png and b/tests/baseline_images/test_features/test_pca/test_scale_false_2d.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_scale_false_3d.png b/tests/baseline_images/test_features/test_pca/test_scale_false_3d.png index cfc4858dc..6b18070fa 100644 Binary files a/tests/baseline_images/test_features/test_pca/test_scale_false_3d.png and b/tests/baseline_images/test_features/test_pca/test_scale_false_3d.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_scale_true_2d.png b/tests/baseline_images/test_features/test_pca/test_scale_true_2d.png index 3d8902cde..141ce961b 100644 Binary files a/tests/baseline_images/test_features/test_pca/test_scale_true_2d.png and b/tests/baseline_images/test_features/test_pca/test_scale_true_2d.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_scale_true_3d.png b/tests/baseline_images/test_features/test_pca/test_scale_true_3d.png index 17b3540f4..77c70c987 100644 Binary files a/tests/baseline_images/test_features/test_pca/test_scale_true_3d.png and b/tests/baseline_images/test_features/test_pca/test_scale_true_3d.png differ diff --git a/tests/baseline_images/test_features/test_pca/test_single.png b/tests/baseline_images/test_features/test_pca/test_single.png new file mode 100644 index 000000000..baf50882c Binary files /dev/null and b/tests/baseline_images/test_features/test_pca/test_single.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_alpha.png b/tests/baseline_images/test_features/test_pcoords/test_alpha.png index bbf9bb84c..dc7cc9659 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_alpha.png and b/tests/baseline_images/test_features/test_pcoords/test_alpha.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_alpha_fast.png b/tests/baseline_images/test_features/test_pcoords/test_alpha_fast.png index 8c5e2fab2..0544ca9f7 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_alpha_fast.png and b/tests/baseline_images/test_features/test_pcoords/test_alpha_fast.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_labels.png b/tests/baseline_images/test_features/test_pcoords/test_labels.png index a27856219..a55d0b401 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_labels.png and b/tests/baseline_images/test_features/test_pcoords/test_labels.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_labels_fast.png b/tests/baseline_images/test_features/test_pcoords/test_labels_fast.png index 4dd06ca2a..6172c7042 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_labels_fast.png and b/tests/baseline_images/test_features/test_pcoords/test_labels_fast.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_normalized_l2.png b/tests/baseline_images/test_features/test_pcoords/test_normalized_l2.png index 16f4132f0..ef1cfd2cb 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_normalized_l2.png and b/tests/baseline_images/test_features/test_pcoords/test_normalized_l2.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_normalized_l2_fast.png b/tests/baseline_images/test_features/test_pcoords/test_normalized_l2_fast.png index a08cb3f4e..91d6b5609 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_normalized_l2_fast.png and b/tests/baseline_images/test_features/test_pcoords/test_normalized_l2_fast.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax.png b/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax.png index c94ba5be9..48ba143bf 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax.png and b/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax_fast.png b/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax_fast.png index e0af88231..cc21f860c 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax_fast.png and b/tests/baseline_images/test_features/test_pcoords/test_normalized_minmax_fast.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_numpy_integration_fast.png b/tests/baseline_images/test_features/test_pcoords/test_numpy_integration_fast.png new file mode 100644 index 000000000..3ea42d18e Binary files /dev/null and b/tests/baseline_images/test_features/test_pcoords/test_numpy_integration_fast.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_numpy_integration_sampled.png b/tests/baseline_images/test_features/test_pcoords/test_numpy_integration_sampled.png new file mode 100644 index 000000000..2ea193b1f Binary files /dev/null and b/tests/baseline_images/test_features/test_pcoords/test_numpy_integration_sampled.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_fast.png b/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_fast.png index e8eb1338f..3ea42d18e 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_fast.png and b/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_fast.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_sampled.png b/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_sampled.png index 41da2a3b7..2ea193b1f 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_sampled.png and b/tests/baseline_images/test_features/test_pcoords/test_pandas_integration_sampled.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_parallel_coords.png b/tests/baseline_images/test_features/test_pcoords/test_parallel_coords.png index ecb9b2557..a55d0b401 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_parallel_coords.png and b/tests/baseline_images/test_features/test_pcoords/test_parallel_coords.png differ diff --git a/tests/baseline_images/test_features/test_pcoords/test_parallel_coords_fast.png b/tests/baseline_images/test_features/test_pcoords/test_parallel_coords_fast.png index c4fd16d03..6172c7042 100644 Binary files a/tests/baseline_images/test_features/test_pcoords/test_parallel_coords_fast.png and b/tests/baseline_images/test_features/test_pcoords/test_parallel_coords_fast.png differ diff --git a/tests/baseline_images/test_features/test_projection/test_3d_continuous_plot.png b/tests/baseline_images/test_features/test_projection/test_3d_continuous_plot.png new file mode 100644 index 000000000..19b50f61f Binary files /dev/null and b/tests/baseline_images/test_features/test_projection/test_3d_continuous_plot.png differ diff --git a/tests/baseline_images/test_features/test_projection/test_colorbar_false.png b/tests/baseline_images/test_features/test_projection/test_colorbar_false.png new file mode 100644 index 000000000..8b1288b13 Binary files /dev/null and b/tests/baseline_images/test_features/test_projection/test_colorbar_false.png differ diff --git a/tests/baseline_images/test_features/test_projection/test_continuous_plot.png b/tests/baseline_images/test_features/test_projection/test_continuous_plot.png new file mode 100644 index 000000000..445753ae2 Binary files /dev/null and b/tests/baseline_images/test_features/test_projection/test_continuous_plot.png differ diff --git a/tests/baseline_images/test_features/test_projection/test_continuous_when_target_discrete.png b/tests/baseline_images/test_features/test_projection/test_continuous_when_target_discrete.png new file mode 100644 index 000000000..6d3bfad7e Binary files /dev/null and b/tests/baseline_images/test_features/test_projection/test_continuous_when_target_discrete.png differ diff --git a/tests/baseline_images/test_features/test_projection/test_discrete_3d.png b/tests/baseline_images/test_features/test_projection/test_discrete_3d.png new file mode 100644 index 000000000..b2170bc74 Binary files /dev/null and b/tests/baseline_images/test_features/test_projection/test_discrete_3d.png differ diff --git a/tests/baseline_images/test_features/test_projection/test_discrete_plot.png b/tests/baseline_images/test_features/test_projection/test_discrete_plot.png new file mode 100644 index 000000000..6fcd54ff5 Binary files /dev/null and b/tests/baseline_images/test_features/test_projection/test_discrete_plot.png differ diff --git a/tests/baseline_images/test_features/test_projection/test_single_plot.png b/tests/baseline_images/test_features/test_projection/test_single_plot.png new file mode 100644 index 000000000..febcfd9b1 Binary files /dev/null and b/tests/baseline_images/test_features/test_projection/test_single_plot.png differ diff --git a/tests/baseline_images/test_features/test_radviz/test_integrated_radiz_pandas_classes_features.png b/tests/baseline_images/test_features/test_radviz/test_integrated_radiz_pandas_classes_features.png deleted file mode 100644 index 98a11a9f0..000000000 Binary files a/tests/baseline_images/test_features/test_radviz/test_integrated_radiz_pandas_classes_features.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_radviz/test_integrated_radiz_with_pandas.png b/tests/baseline_images/test_features/test_radviz/test_integrated_radiz_with_pandas.png deleted file mode 100644 index 98607bc74..000000000 Binary files a/tests/baseline_images/test_features/test_radviz/test_integrated_radiz_with_pandas.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_numpy_classes_features.png b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_numpy_classes_features.png new file mode 100644 index 000000000..185306988 Binary files /dev/null and b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_numpy_classes_features.png differ diff --git a/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_pandas_classes_features.png b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_pandas_classes_features.png new file mode 100644 index 000000000..185306988 Binary files /dev/null and b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_pandas_classes_features.png differ diff --git a/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_with_numpy.png b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_with_numpy.png new file mode 100644 index 000000000..4804bd985 Binary files /dev/null and b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_with_numpy.png differ diff --git a/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_with_pandas.png b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_with_pandas.png new file mode 100644 index 000000000..cf2d44dbd Binary files /dev/null and b/tests/baseline_images/test_features/test_radviz/test_integrated_radviz_with_pandas.png differ diff --git a/tests/baseline_images/test_features/test_radviz/test_radviz.png b/tests/baseline_images/test_features/test_radviz/test_radviz.png index fdbe4aeb3..5c644ad52 100644 Binary files a/tests/baseline_images/test_features/test_radviz/test_radviz.png and b/tests/baseline_images/test_features/test_radviz/test_radviz.png differ diff --git a/tests/baseline_images/test_features/test_radviz/test_radviz_alpha.png b/tests/baseline_images/test_features/test_radviz/test_radviz_alpha.png index d53445692..13677c268 100644 Binary files a/tests/baseline_images/test_features/test_radviz/test_radviz_alpha.png and b/tests/baseline_images/test_features/test_radviz/test_radviz_alpha.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank1d_integrated.png b/tests/baseline_images/test_features/test_rankd/test_rank1d_integrated_numpy.png similarity index 50% rename from tests/baseline_images/test_features/test_rankd/test_rank1d_integrated.png rename to tests/baseline_images/test_features/test_rankd/test_rank1d_integrated_numpy.png index 50cea16e4..dd611de43 100644 Binary files a/tests/baseline_images/test_features/test_rankd/test_rank1d_integrated.png and b/tests/baseline_images/test_features/test_rankd/test_rank1d_integrated_numpy.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank1d_integrated_pandas.png b/tests/baseline_images/test_features/test_rankd/test_rank1d_integrated_pandas.png new file mode 100644 index 000000000..dd611de43 Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank1d_integrated_pandas.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank1d_orientation.png b/tests/baseline_images/test_features/test_rankd/test_rank1d_orientation.png new file mode 100644 index 000000000..8b3b016df Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank1d_orientation.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank1d_random.png b/tests/baseline_images/test_features/test_rankd/test_rank1d_random.png deleted file mode 100644 index e44d850f9..000000000 Binary files a/tests/baseline_images/test_features/test_rankd/test_rank1d_random.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank1d_shapiro.png b/tests/baseline_images/test_features/test_rankd/test_rank1d_shapiro.png new file mode 100644 index 000000000..9c78a6219 Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank1d_shapiro.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank2d_covariance.png b/tests/baseline_images/test_features/test_rankd/test_rank2d_covariance.png new file mode 100644 index 000000000..f4ee0001a Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank2d_covariance.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank2d_integrated.png b/tests/baseline_images/test_features/test_rankd/test_rank2d_integrated_numpy.png similarity index 98% rename from tests/baseline_images/test_features/test_rankd/test_rank2d_integrated.png rename to tests/baseline_images/test_features/test_rankd/test_rank2d_integrated_numpy.png index 5ca032037..da30d76fd 100644 Binary files a/tests/baseline_images/test_features/test_rankd/test_rank2d_integrated.png and b/tests/baseline_images/test_features/test_rankd/test_rank2d_integrated_numpy.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank2d_integrated_pandas.png b/tests/baseline_images/test_features/test_rankd/test_rank2d_integrated_pandas.png new file mode 100644 index 000000000..da30d76fd Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank2d_integrated_pandas.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank2d_kendalltau.png b/tests/baseline_images/test_features/test_rankd/test_rank2d_kendalltau.png new file mode 100644 index 000000000..ee18a6627 Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank2d_kendalltau.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank2d_pearson.png b/tests/baseline_images/test_features/test_rankd/test_rank2d_pearson.png new file mode 100644 index 000000000..3c3497c78 Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank2d_pearson.png differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank2d_random.png b/tests/baseline_images/test_features/test_rankd/test_rank2d_random.png deleted file mode 100644 index cccb32171..000000000 Binary files a/tests/baseline_images/test_features/test_rankd/test_rank2d_random.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_rankd/test_rank2d_spearman.png b/tests/baseline_images/test_features/test_rankd/test_rank2d_spearman.png new file mode 100644 index 000000000..8b79bab35 Binary files /dev/null and b/tests/baseline_images/test_features/test_rankd/test_rank2d_spearman.png differ diff --git a/tests/baseline_images/test_features/test_rfecv/test_pandas_integration.png b/tests/baseline_images/test_features/test_rfecv/test_pandas_integration.png deleted file mode 100644 index c542e6100..000000000 Binary files a/tests/baseline_images/test_features/test_rfecv/test_pandas_integration.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_rfecv/test_quick_method.png b/tests/baseline_images/test_features/test_rfecv/test_quick_method.png deleted file mode 100644 index 85cef8a7d..000000000 Binary files a/tests/baseline_images/test_features/test_rfecv/test_quick_method.png and /dev/null differ diff --git a/tests/baseline_images/test_features/test_rfecv/test_rfecv_classification.png b/tests/baseline_images/test_features/test_rfecv/test_rfecv_classification.png deleted file mode 100644 index a6a2ffdc7..000000000 Binary files a/tests/baseline_images/test_features/test_rfecv/test_rfecv_classification.png and /dev/null differ diff --git a/tests/baseline_images/test_meta/test_random_visualizer.png b/tests/baseline_images/test_meta/test_random_visualizer.png index 53e9fe65a..f3f893197 100644 Binary files a/tests/baseline_images/test_meta/test_random_visualizer.png and b/tests/baseline_images/test_meta/test_random_visualizer.png differ diff --git a/tests/baseline_images/test_meta/test_random_visualizer_increased_tolerance.png b/tests/baseline_images/test_meta/test_random_visualizer_increased_tolerance.png index 4cf89a0fb..da6f804ae 100644 Binary files a/tests/baseline_images/test_meta/test_random_visualizer_increased_tolerance.png and b/tests/baseline_images/test_meta/test_random_visualizer_increased_tolerance.png differ diff --git a/tests/baseline_images/test_meta/test_random_visualizer_not_close.png b/tests/baseline_images/test_meta/test_random_visualizer_not_close.png index c9b9b1ff4..ab01c119a 100644 Binary files a/tests/baseline_images/test_meta/test_random_visualizer_not_close.png and b/tests/baseline_images/test_meta/test_random_visualizer_not_close.png differ diff --git a/tests/baseline_images/test_model_selection/test_cross_validation/test_classifier.png b/tests/baseline_images/test_model_selection/test_cross_validation/test_classifier.png index 1e2b6fd5d..7940183d1 100644 Binary files a/tests/baseline_images/test_model_selection/test_cross_validation/test_classifier.png and b/tests/baseline_images/test_model_selection/test_cross_validation/test_classifier.png differ diff --git a/tests/baseline_images/test_model_selection/test_cross_validation/test_numpy_integration.png b/tests/baseline_images/test_model_selection/test_cross_validation/test_numpy_integration.png new file mode 100644 index 000000000..49261aefe Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_cross_validation/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_cross_validation/test_pandas_integration.png b/tests/baseline_images/test_model_selection/test_cross_validation/test_pandas_integration.png index d3b00b845..49261aefe 100644 Binary files a/tests/baseline_images/test_model_selection/test_cross_validation/test_pandas_integration.png and b/tests/baseline_images/test_model_selection/test_cross_validation/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_cross_validation/test_quick_method.png b/tests/baseline_images/test_model_selection/test_cross_validation/test_quick_method.png index 6e32d4d37..6027d5528 100644 Binary files a/tests/baseline_images/test_model_selection/test_cross_validation/test_quick_method.png and b/tests/baseline_images/test_model_selection/test_cross_validation/test_quick_method.png differ diff --git a/tests/baseline_images/test_model_selection/test_cross_validation/test_regression.png b/tests/baseline_images/test_model_selection/test_cross_validation/test_regression.png index cf63f267e..2d4464b17 100644 Binary files a/tests/baseline_images/test_model_selection/test_cross_validation/test_regression.png and b/tests/baseline_images/test_model_selection/test_cross_validation/test_regression.png differ diff --git a/tests/baseline_images/test_model_selection/test_importances/test_integration_coef.png b/tests/baseline_images/test_model_selection/test_importances/test_integration_coef.png new file mode 100644 index 000000000..894ceff20 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_importances/test_integration_coef.png differ diff --git a/tests/baseline_images/test_model_selection/test_importances/test_integration_feature_importances.png b/tests/baseline_images/test_model_selection/test_importances/test_integration_feature_importances.png new file mode 100644 index 000000000..ce10311b4 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_importances/test_integration_feature_importances.png differ diff --git a/tests/baseline_images/test_model_selection/test_importances/test_integration_quick_method.png b/tests/baseline_images/test_model_selection/test_importances/test_integration_quick_method.png new file mode 100644 index 000000000..780184bb0 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_importances/test_integration_quick_method.png differ diff --git a/tests/baseline_images/test_model_selection/test_importances/test_multi_coefs_stacked.png b/tests/baseline_images/test_model_selection/test_importances/test_multi_coefs_stacked.png new file mode 100644 index 000000000..4419a2747 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_importances/test_multi_coefs_stacked.png differ diff --git a/tests/baseline_images/test_model_selection/test_learning_curve/test_classifier.png b/tests/baseline_images/test_model_selection/test_learning_curve/test_classifier.png index d45011096..d60fe234f 100644 Binary files a/tests/baseline_images/test_model_selection/test_learning_curve/test_classifier.png and b/tests/baseline_images/test_model_selection/test_learning_curve/test_classifier.png differ diff --git a/tests/baseline_images/test_model_selection/test_learning_curve/test_clusters.png b/tests/baseline_images/test_model_selection/test_learning_curve/test_clusters.png index 0975c2712..8814d17f1 100644 Binary files a/tests/baseline_images/test_model_selection/test_learning_curve/test_clusters.png and b/tests/baseline_images/test_model_selection/test_learning_curve/test_clusters.png differ diff --git a/tests/baseline_images/test_model_selection/test_learning_curve/test_numpy_integration.png b/tests/baseline_images/test_model_selection/test_learning_curve/test_numpy_integration.png new file mode 100644 index 000000000..5aa360fdb Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_learning_curve/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_learning_curve/test_pandas_integration.png b/tests/baseline_images/test_model_selection/test_learning_curve/test_pandas_integration.png index 6b2a2814a..5aa360fdb 100644 Binary files a/tests/baseline_images/test_model_selection/test_learning_curve/test_pandas_integration.png and b/tests/baseline_images/test_model_selection/test_learning_curve/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_learning_curve/test_quick_method.png b/tests/baseline_images/test_model_selection/test_learning_curve/test_quick_method.png index 7a4980fa2..b4f80a803 100644 Binary files a/tests/baseline_images/test_model_selection/test_learning_curve/test_quick_method.png and b/tests/baseline_images/test_model_selection/test_learning_curve/test_quick_method.png differ diff --git a/tests/baseline_images/test_model_selection/test_learning_curve/test_regressor.png b/tests/baseline_images/test_model_selection/test_learning_curve/test_regressor.png index f324fc2b5..07d4d9d34 100644 Binary files a/tests/baseline_images/test_model_selection/test_learning_curve/test_regressor.png and b/tests/baseline_images/test_model_selection/test_learning_curve/test_regressor.png differ diff --git a/tests/baseline_images/test_model_selection/test_rfecv/test_numpy_integration.png b/tests/baseline_images/test_model_selection/test_rfecv/test_numpy_integration.png new file mode 100644 index 000000000..2d91dc140 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_rfecv/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_rfecv/test_pandas_integration.png b/tests/baseline_images/test_model_selection/test_rfecv/test_pandas_integration.png new file mode 100644 index 000000000..2d91dc140 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_rfecv/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_rfecv/test_quick_method.png b/tests/baseline_images/test_model_selection/test_rfecv/test_quick_method.png new file mode 100644 index 000000000..553c18edc Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_rfecv/test_quick_method.png differ diff --git a/tests/baseline_images/test_model_selection/test_rfecv/test_rfecv_classification.png b/tests/baseline_images/test_model_selection/test_rfecv/test_rfecv_classification.png new file mode 100644 index 000000000..6fde68771 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_rfecv/test_rfecv_classification.png differ diff --git a/tests/baseline_images/test_model_selection/test_rfecv/test_rfecv_step.png b/tests/baseline_images/test_model_selection/test_rfecv/test_rfecv_step.png new file mode 100644 index 000000000..7d20edc32 Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_rfecv/test_rfecv_step.png differ diff --git a/tests/baseline_images/test_model_selection/test_validation_curve/test_classifier.png b/tests/baseline_images/test_model_selection/test_validation_curve/test_classifier.png index e04a81472..fd2652a81 100644 Binary files a/tests/baseline_images/test_model_selection/test_validation_curve/test_classifier.png and b/tests/baseline_images/test_model_selection/test_validation_curve/test_classifier.png differ diff --git a/tests/baseline_images/test_model_selection/test_validation_curve/test_numpy_integration.png b/tests/baseline_images/test_model_selection/test_validation_curve/test_numpy_integration.png new file mode 100644 index 000000000..d5d10835b Binary files /dev/null and b/tests/baseline_images/test_model_selection/test_validation_curve/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_validation_curve/test_pandas_integration.png b/tests/baseline_images/test_model_selection/test_validation_curve/test_pandas_integration.png index b5fc0f6b2..d5d10835b 100644 Binary files a/tests/baseline_images/test_model_selection/test_validation_curve/test_pandas_integration.png and b/tests/baseline_images/test_model_selection/test_validation_curve/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_model_selection/test_validation_curve/test_quick_method.png b/tests/baseline_images/test_model_selection/test_validation_curve/test_quick_method.png index 685e71bc6..c62bb473c 100644 Binary files a/tests/baseline_images/test_model_selection/test_validation_curve/test_quick_method.png and b/tests/baseline_images/test_model_selection/test_validation_curve/test_quick_method.png differ diff --git a/tests/baseline_images/test_model_selection/test_validation_curve/test_regression.png b/tests/baseline_images/test_model_selection/test_validation_curve/test_regression.png index 52fd5295e..9cc41cf86 100644 Binary files a/tests/baseline_images/test_model_selection/test_validation_curve/test_regression.png and b/tests/baseline_images/test_model_selection/test_validation_curve/test_regression.png differ diff --git a/tests/baseline_images/test_regressor/test_alphas/test_similar_image.png b/tests/baseline_images/test_regressor/test_alphas/test_similar_image.png index 8b5c6406e..ab4bc45d4 100644 Binary files a/tests/baseline_images/test_regressor/test_alphas/test_similar_image.png and b/tests/baseline_images/test_regressor/test_alphas/test_similar_image.png differ diff --git a/tests/baseline_images/test_regressor/test_influence/test_cooks_distance.png b/tests/baseline_images/test_regressor/test_influence/test_cooks_distance.png new file mode 100644 index 000000000..b28b5e428 Binary files /dev/null and b/tests/baseline_images/test_regressor/test_influence/test_cooks_distance.png differ diff --git a/tests/baseline_images/test_regressor/test_influence/test_cooks_distance_quickmethod.png b/tests/baseline_images/test_regressor/test_influence/test_cooks_distance_quickmethod.png new file mode 100644 index 000000000..7e3f08e9b Binary files /dev/null and b/tests/baseline_images/test_regressor/test_influence/test_cooks_distance_quickmethod.png differ diff --git a/tests/baseline_images/test_regressor/test_influence/test_numpy_integration.png b/tests/baseline_images/test_regressor/test_influence/test_numpy_integration.png new file mode 100644 index 000000000..0ddac4f19 Binary files /dev/null and b/tests/baseline_images/test_regressor/test_influence/test_numpy_integration.png differ diff --git a/tests/baseline_images/test_regressor/test_influence/test_pandas_integration.png b/tests/baseline_images/test_regressor/test_influence/test_pandas_integration.png new file mode 100644 index 000000000..0ddac4f19 Binary files /dev/null and b/tests/baseline_images/test_regressor/test_influence/test_pandas_integration.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_lines.png b/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_lines.png index 0621deca8..83979f5e6 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_lines.png and b/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_lines.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_shared_limits.png b/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_shared_limits.png index 70088ef7f..15f728b82 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_shared_limits.png and b/tests/baseline_images/test_regressor/test_residuals/test_peplot_no_shared_limits.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_pred_error.png b/tests/baseline_images/test_regressor/test_residuals/test_pred_error.png deleted file mode 100644 index 8a2b8fb8c..000000000 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_pred_error.png and /dev/null differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_prediction_error.png b/tests/baseline_images/test_regressor/test_residuals/test_prediction_error.png index c12fe075f..b984640ff 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_prediction_error.png and b/tests/baseline_images/test_regressor/test_residuals/test_prediction_error.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_prediction_error_numpy.png b/tests/baseline_images/test_regressor/test_residuals/test_prediction_error_numpy.png new file mode 100644 index 000000000..f65d9341b Binary files /dev/null and b/tests/baseline_images/test_regressor/test_residuals/test_prediction_error_numpy.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_prediction_error_pandas.png b/tests/baseline_images/test_regressor/test_residuals/test_prediction_error_pandas.png index 334fa8073..ef9e0ae8f 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_prediction_error_pandas.png and b/tests/baseline_images/test_regressor/test_residuals/test_prediction_error_pandas.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_resid_plots.png b/tests/baseline_images/test_regressor/test_residuals/test_resid_plots.png deleted file mode 100644 index 0143bf10f..000000000 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_resid_plots.png and /dev/null differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot.png b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot.png index a66872dda..498498898 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot.png and b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_no_histogram.png b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_no_histogram.png index a9f309123..f637c067e 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_no_histogram.png and b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_no_histogram.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_numpy.png b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_numpy.png new file mode 100644 index 000000000..fadd802b9 Binary files /dev/null and b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_numpy.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_pandas.png b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_pandas.png index 9c21534af..fadd802b9 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_pandas.png and b/tests/baseline_images/test_regressor/test_residuals/test_residuals_plot_pandas.png differ diff --git a/tests/baseline_images/test_regressor/test_residuals/test_residuals_quick_method.png b/tests/baseline_images/test_regressor/test_residuals/test_residuals_quick_method.png index 3ec28a8be..03a928039 100644 Binary files a/tests/baseline_images/test_regressor/test_residuals/test_residuals_quick_method.png and b/tests/baseline_images/test_regressor/test_residuals/test_residuals_quick_method.png differ diff --git a/tests/baseline_images/test_style/test_colors/test_integrated_yb_colormap.png b/tests/baseline_images/test_style/test_colors/test_integrated_yb_colormap.png new file mode 100644 index 000000000..565847cc8 Binary files /dev/null and b/tests/baseline_images/test_style/test_colors/test_integrated_yb_colormap.png differ diff --git a/tests/baseline_images/test_target/test_binning/test_balancedbinningreference.png b/tests/baseline_images/test_target/test_binning/test_balancedbinningreference.png deleted file mode 100644 index e359f6e40..000000000 Binary files a/tests/baseline_images/test_target/test_binning/test_balancedbinningreference.png and /dev/null differ diff --git a/tests/baseline_images/test_target/test_binning/test_numpy_bins.png b/tests/baseline_images/test_target/test_binning/test_numpy_bins.png new file mode 100644 index 000000000..6eb431663 Binary files /dev/null and b/tests/baseline_images/test_target/test_binning/test_numpy_bins.png differ diff --git a/tests/baseline_images/test_target/test_binning/test_pandas_bins.png b/tests/baseline_images/test_target/test_binning/test_pandas_bins.png new file mode 100644 index 000000000..6eb431663 Binary files /dev/null and b/tests/baseline_images/test_target/test_binning/test_pandas_bins.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_binary_balance.png b/tests/baseline_images/test_target/test_class_balance/test_binary_balance.png index 096b59688..1d48f35f7 100644 Binary files a/tests/baseline_images/test_target/test_class_balance/test_binary_balance.png and b/tests/baseline_images/test_target/test_class_balance/test_binary_balance.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_binary_compare.png b/tests/baseline_images/test_target/test_class_balance/test_binary_compare.png index 69837fd7f..4ec0e2427 100644 Binary files a/tests/baseline_images/test_target/test_class_balance/test_binary_compare.png and b/tests/baseline_images/test_target/test_class_balance/test_binary_compare.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_multiclass_balance.png b/tests/baseline_images/test_target/test_class_balance/test_multiclass_balance.png index edce080ba..4a45f5817 100644 Binary files a/tests/baseline_images/test_target/test_class_balance/test_multiclass_balance.png and b/tests/baseline_images/test_target/test_class_balance/test_multiclass_balance.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_multiclass_compare.png b/tests/baseline_images/test_target/test_class_balance/test_multiclass_compare.png index 7b051203f..bff3d1c4b 100644 Binary files a/tests/baseline_images/test_target/test_class_balance/test_multiclass_compare.png and b/tests/baseline_images/test_target/test_class_balance/test_multiclass_compare.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_numpy_occupancy_balance.png b/tests/baseline_images/test_target/test_class_balance/test_numpy_occupancy_balance.png new file mode 100644 index 000000000..f4115587b Binary files /dev/null and b/tests/baseline_images/test_target/test_class_balance/test_numpy_occupancy_balance.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_numpy_occupancy_compare.png b/tests/baseline_images/test_target/test_class_balance/test_numpy_occupancy_compare.png new file mode 100644 index 000000000..0ab35b080 Binary files /dev/null and b/tests/baseline_images/test_target/test_class_balance/test_numpy_occupancy_compare.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_balance.png b/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_balance.png index 01cefff44..f4115587b 100644 Binary files a/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_balance.png and b/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_balance.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_compare.png b/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_compare.png index 992914346..0ab35b080 100644 Binary files a/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_compare.png and b/tests/baseline_images/test_target/test_class_balance/test_pandas_occupancy_compare.png differ diff --git a/tests/baseline_images/test_target/test_class_balance/test_quick_method.png b/tests/baseline_images/test_target/test_class_balance/test_quick_method.png index 26a60fb35..d19850334 100644 Binary files a/tests/baseline_images/test_target/test_class_balance/test_quick_method.png and b/tests/baseline_images/test_target/test_class_balance/test_quick_method.png differ diff --git a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_classification.png b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_classification.png index eae06d1d7..facea8b7e 100644 Binary files a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_classification.png and b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_classification.png differ diff --git a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_regression.png b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_regression.png index d0c4c759a..c39aa1463 100644 Binary files a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_regression.png and b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_mutual_info_regression.png differ diff --git a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_pearson.png b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_pearson.png index 0b8a87322..46de93cdf 100644 Binary files a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_pearson.png and b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_integrated_pearson.png differ diff --git a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_quick_method.png b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_quick_method.png index 0b8a87322..46de93cdf 100644 Binary files a/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_quick_method.png and b/tests/baseline_images/test_target/test_feature_correlation/test_feature_correlation_quick_method.png differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_annotate_docs.png b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_annotate_docs.png new file mode 100644 index 000000000..381bfc6a1 Binary files /dev/null and b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_annotate_docs.png differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_color_by_class.png b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_color_by_class.png new file mode 100644 index 000000000..6575bc1fb Binary files /dev/null and b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_color_by_class.png differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_generator_input.png b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_generator_input.png new file mode 100644 index 000000000..679520992 Binary files /dev/null and b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_generator_input.png differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_ignore_case.png b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_ignore_case.png new file mode 100644 index 000000000..679520992 Binary files /dev/null and b/tests/baseline_images/test_text/test_dispersion/test_dispersion_plot_ignore_case.png differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_annotate_docs.png b/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_annotate_docs.png deleted file mode 100644 index 76c4c936f..000000000 Binary files a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_annotate_docs.png and /dev/null differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_color_words_by_class.png b/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_color_words_by_class.png deleted file mode 100644 index 935b91f98..000000000 Binary files a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_color_words_by_class.png and /dev/null differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_generator_input.png b/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_generator_input.png deleted file mode 100644 index 05c17a9e5..000000000 Binary files a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_generator_input.png and /dev/null differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_ignore_case.png b/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_ignore_case.png deleted file mode 100644 index 05c17a9e5..000000000 Binary files a/tests/baseline_images/test_text/test_dispersion/test_dispersionplot_ignore_case.png and /dev/null differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_integrated_dispersion_plot.png b/tests/baseline_images/test_text/test_dispersion/test_integrated_dispersion_plot.png new file mode 100644 index 000000000..809830a18 Binary files /dev/null and b/tests/baseline_images/test_text/test_dispersion/test_integrated_dispersion_plot.png differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_integrated_dispersionplot.png b/tests/baseline_images/test_text/test_dispersion/test_integrated_dispersionplot.png deleted file mode 100644 index 11b347da2..000000000 Binary files a/tests/baseline_images/test_text/test_dispersion/test_integrated_dispersionplot.png and /dev/null differ diff --git a/tests/baseline_images/test_text/test_dispersion/test_quick_method.png b/tests/baseline_images/test_text/test_dispersion/test_quick_method.png new file mode 100644 index 000000000..809830a18 Binary files /dev/null and b/tests/baseline_images/test_text/test_dispersion/test_quick_method.png differ diff --git a/tests/baseline_images/test_text/test_freqdist/test_integrated_freqdist.png b/tests/baseline_images/test_text/test_freqdist/test_integrated_freqdist.png index f0368cd5e..43d09b8c9 100644 Binary files a/tests/baseline_images/test_text/test_freqdist/test_integrated_freqdist.png and b/tests/baseline_images/test_text/test_freqdist/test_integrated_freqdist.png differ diff --git a/tests/baseline_images/test_text/test_postag/test_frequency_mode.png b/tests/baseline_images/test_text/test_postag/test_frequency_mode.png new file mode 100644 index 000000000..7e57a2f13 Binary files /dev/null and b/tests/baseline_images/test_text/test_postag/test_frequency_mode.png differ diff --git a/tests/baseline_images/test_text/test_postag/test_quick_method.png b/tests/baseline_images/test_text/test_postag/test_quick_method.png new file mode 100644 index 000000000..50dd117ff Binary files /dev/null and b/tests/baseline_images/test_text/test_postag/test_quick_method.png differ diff --git a/tests/baseline_images/test_text/test_postag/test_spacy_tagged.png b/tests/baseline_images/test_text/test_postag/test_spacy_tagged.png new file mode 100644 index 000000000..c002661e5 Binary files /dev/null and b/tests/baseline_images/test_text/test_postag/test_spacy_tagged.png differ diff --git a/tests/baseline_images/test_text/test_postag/test_stack_frequency_mode.png b/tests/baseline_images/test_text/test_postag/test_stack_frequency_mode.png new file mode 100644 index 000000000..3746f30cf Binary files /dev/null and b/tests/baseline_images/test_text/test_postag/test_stack_frequency_mode.png differ diff --git a/tests/baseline_images/test_text/test_postag/test_stack_mode.png b/tests/baseline_images/test_text/test_postag/test_stack_mode.png new file mode 100644 index 000000000..281fd171b Binary files /dev/null and b/tests/baseline_images/test_text/test_postag/test_stack_mode.png differ diff --git a/tests/baseline_images/test_text/test_postag/test_word_tagged.png b/tests/baseline_images/test_text/test_postag/test_word_tagged.png new file mode 100644 index 000000000..50dd117ff Binary files /dev/null and b/tests/baseline_images/test_text/test_postag/test_word_tagged.png differ diff --git a/tests/baseline_images/test_text/test_postag/test_wordpunct_tagged.png b/tests/baseline_images/test_text/test_postag/test_wordpunct_tagged.png new file mode 100644 index 000000000..b36fce746 Binary files /dev/null and b/tests/baseline_images/test_text/test_postag/test_wordpunct_tagged.png differ diff --git a/tests/baseline_images/test_text/test_tsne/test_integrated_tsne.png b/tests/baseline_images/test_text/test_tsne/test_integrated_tsne.png index 59e831b36..39d843eb5 100644 Binary files a/tests/baseline_images/test_text/test_tsne/test_integrated_tsne.png and b/tests/baseline_images/test_text/test_tsne/test_integrated_tsne.png differ diff --git a/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne.png b/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne.png index 2341ba7a4..035d620ff 100644 Binary files a/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne.png and b/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne.png differ diff --git a/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne_class_labels.png b/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne_class_labels.png index 2341ba7a4..035d620ff 100644 Binary files a/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne_class_labels.png and b/tests/baseline_images/test_text/test_tsne/test_make_classification_tsne_class_labels.png differ diff --git a/tests/baseline_images/test_text/test_tsne/test_no_target_tsne.png b/tests/baseline_images/test_text/test_tsne/test_no_target_tsne.png index 08eb3ca82..62e844b0f 100644 Binary files a/tests/baseline_images/test_text/test_tsne/test_no_target_tsne.png and b/tests/baseline_images/test_text/test_tsne/test_no_target_tsne.png differ diff --git a/tests/baseline_images/test_text/test_tsne/test_visualizer_with_pandas.png b/tests/baseline_images/test_text/test_tsne/test_visualizer_with_pandas.png index 25477dbf0..aaffd6dc7 100644 Binary files a/tests/baseline_images/test_text/test_tsne/test_visualizer_with_pandas.png and b/tests/baseline_images/test_text/test_tsne/test_visualizer_with_pandas.png differ diff --git a/tests/baseline_images/test_text/test_umap/test_integrated_umap.png b/tests/baseline_images/test_text/test_umap/test_integrated_umap.png new file mode 100644 index 000000000..156a4e7ae Binary files /dev/null and b/tests/baseline_images/test_text/test_umap/test_integrated_umap.png differ diff --git a/tests/baseline_images/test_text/test_umap/test_make_classification_umap.png b/tests/baseline_images/test_text/test_umap/test_make_classification_umap.png new file mode 100644 index 000000000..d16d8459c Binary files /dev/null and b/tests/baseline_images/test_text/test_umap/test_make_classification_umap.png differ diff --git a/tests/baseline_images/test_text/test_umap/test_make_classification_umap_class_labels.png b/tests/baseline_images/test_text/test_umap/test_make_classification_umap_class_labels.png new file mode 100644 index 000000000..d16d8459c Binary files /dev/null and b/tests/baseline_images/test_text/test_umap/test_make_classification_umap_class_labels.png differ diff --git a/tests/baseline_images/test_text/test_umap/test_no_target_umap.png b/tests/baseline_images/test_text/test_umap/test_no_target_umap.png new file mode 100644 index 000000000..87a244cdc Binary files /dev/null and b/tests/baseline_images/test_text/test_umap/test_no_target_umap.png differ diff --git a/tests/baseline_images/test_text/test_umap/test_visualizer_with_pandas.png b/tests/baseline_images/test_text/test_umap/test_visualizer_with_pandas.png new file mode 100644 index 000000000..f566446de Binary files /dev/null and b/tests/baseline_images/test_text/test_umap/test_visualizer_with_pandas.png differ diff --git a/tests/checks.py b/tests/checks.py index 851509e25..64ec00f14 100644 --- a/tests/checks.py +++ b/tests/checks.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Mon May 22 11:18:06 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: checks.py [4131cb1] benjamin@bengfort.com $ @@ -18,6 +18,7 @@ ########################################################################## import sys + sys.path.append("..") import numpy as np @@ -35,6 +36,7 @@ ## Checking runable ########################################################################## + def check_visualizer(Visualizer): """ Check if visualizer adheres to Yellowbrick conventions. @@ -53,6 +55,7 @@ def check_visualizer(Visualizer): ## Generate the specific per-visualizer checking ########################################################################## + def _yield_all_checks(name, Visualizer): """ Composes the checks required for the specific visualizer. @@ -125,6 +128,7 @@ def _yield_text_checks(name, Visualizer): ## Checking Functions ########################################################################## + def check_instantiation(name, Visualizer, args, kwargs): # assert that visualizers can be passed an axes object. ax = plt.gca() @@ -135,7 +139,7 @@ def check_instantiation(name, Visualizer, args, kwargs): def check_estimator_api(name, Visualizer): X = np.random.rand((5, 10)) - y = np.random.randint(0,2, 10) + y = np.random.randint(0, 2, 10) # Ensure fit returns self. viz = Visualizer() @@ -143,8 +147,9 @@ def check_estimator_api(name, Visualizer): assert viz == self -if __name__ == '__main__': +if __name__ == "__main__": import sys + sys.path.append("..") from yellowbrick.classifier import * @@ -154,12 +159,24 @@ def check_estimator_api(name, Visualizer): from yellowbrick.text import * visualizers = [ - ClassBalance, ClassificationReport, ConfusionMatrix, ROCAUC, - KElbowVisualizer, SilhouetteVisualizer, - ScatterVisualizer, JointPlotVisualizer, Rank2D, RadViz, ParallelCoordinates, - AlphaSelection, ManualAlphaSelection, - PredictionError, ResidualsPlot, - TSNEVisualizer, FreqDistVisualizer, PosTagVisualizer + ClassBalance, + ClassificationReport, + ConfusionMatrix, + ROCAUC, + KElbowVisualizer, + SilhouetteVisualizer, + ScatterVisualizer, + JointPlotVisualizer, + Rank2D, + RadViz, + ParallelCoordinates, + AlphaSelection, + ManualAlphaSelection, + PredictionError, + ResidualsPlot, + TSNEVisualizer, + FreqDistVisualizer, + PosTagVisualizer, ] for visualizer in visualizers: diff --git a/tests/conftest.py b/tests/conftest.py index 8e39b0261..7fd1b4b69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,13 @@ # tests.conftest -# Global definitions for Yellowbrick PyTest +# Global definitions for Yellowbrick pytest # # Author: Benjamin Bengfort # Created: Fri Mar 02 11:53:55 2018 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: conftest.py [] benjamin@bengfort.com $ +# ID: conftest.py [957cd53] benjamin@bengfort.com $ """ Global definitions for Yellowbrick PyTest @@ -18,13 +18,51 @@ ########################################################################## import os +import matplotlib as mpl from pytest_flakes import FlakesItem + +########################################################################## +## Configure tests +########################################################################## + + +def pytest_configure(config): + """ + This function is called by pytest for every plugin and conftest file + after the command line arguments have been passed but before the + session object is created and all of the tests are created. It is used + to set a global configuration before all tests are run. + + Yellowbrick uses this function primarily to ensure that the matplotlib + environment is setup correctly for all tests. + """ + # This is redundant with the line in tests/__init__.py but ensures that + # the backend is correctly set across all tests and plugins. + mpl.use("Agg") + + # Travis-CI does not have san-serif so ensure standard fonts are used. + # TODO: this is currently being reset before each test; needs fixing. + mpl.rcParams["font.family"] = "DejaVu Sans" + + ########################################################################## ## PyTest Hooks ########################################################################## + +def docline(obj): + """ + Returns the first line of the object's docstring or None if + there is no __doc__ on the object. + """ + if not obj.__doc__: + return None + lines = list(filter(None, obj.__doc__.split("\n"))) + return lines[0].strip() + + def pytest_itemcollected(item): """ A reporting hook that is called when a test item is collected. @@ -38,7 +76,7 @@ def pytest_itemcollected(item): """ # Ignore Session and PyFlake tests that are generated automatically - if not hasattr(item.parent, 'obj') or isinstance(item, FlakesItem): + if not hasattr(item.parent, "obj") or isinstance(item, FlakesItem): return # Collect test objects to inspect @@ -49,12 +87,13 @@ def pytest_itemcollected(item): # or class name, and the docstring of the test case, then set the nodeid # so that pytest-spec will correctly parse the information. path = os.path.relpath(str(item.fspath)) - prefix = parent.__doc__ or getattr(parent, '__name__', parent.__class__.__name__) - suffix = node.__doc__.strip() if node.__doc__ else node.__name__ + prefix = docline(parent) or getattr(parent, "__name__", parent.__class__.__name__) + suffix = docline(node) or node.__name__ # Add parametrize or test generation id to distinguish it in output - if item._genid: + # TODO: this is broken with pytest 4.2 (no attribute _genid) + if hasattr(item, "_genid") and item._genid: suffix += " ({})".format(item._genid) if prefix or suffix: - item._nodeid = '::'.join((path, prefix.strip(), suffix.strip())) + item._nodeid = "::".join((path, prefix.strip(), suffix.strip())) diff --git a/tests/dataset.py b/tests/dataset.py deleted file mode 100644 index 5354d912a..000000000 --- a/tests/dataset.py +++ /dev/null @@ -1,264 +0,0 @@ -# tests.dataset -# Helper functions for tests that utilize downloadable datasets. -# -# Author: Benjamin Bengfort -# Created: Thu Oct 13 19:55:53 2016 -0400 -# -# Copyright (C) 2016 District Data Labs -# For license information, see LICENSE.txt -# -# ID: dataset.py [8f4de77] benjamin@bengfort.com $ - -""" -Helper functions for tests that utilize downloadable datasets. -""" - -########################################################################## -## Imports -########################################################################## - -import io -import os -import shutil -import hashlib -import zipfile -import numpy as np - -from collections import namedtuple -from sklearn.datasets.base import Bunch - - -try: - import requests -except ImportError: - requests = None - -try: - import pandas as pd -except ImportError: - pd = None - - -# Helpers for fixtures -Dataset = namedtuple('Dataset', 'X,y') -Split = namedtuple('Split', 'train,test') - - -########################################################################## -## Fixtures -########################################################################## - -DATASETS = { - 'concrete': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/concrete.zip', - 'signature': 'b9ea5f26a7bb272a040e2f1a993b26babbf8dc4a04ab8198bb315ca66d71f10d', - 'type': 'numpy', - }, - 'energy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/energy.zip', - 'signature': '19fb86f3bcdde208eed46944172cb643ef6a7d58da103fb568fae43205ed89d3', - 'type': 'numpy', - }, - 'credit': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/credit.zip', - 'signature': '4a91339c69f55e18f3f48004328fbcb7868070b618208fed099920427b084e5e', - 'type': 'numpy', - }, - 'occupancy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/occupancy.zip', - 'signature': '429cfe376dc9929a1fa528da89f0e1626e34e19695f3f555d8954025bbc522b8', - 'type': 'numpy', - }, - 'mushroom': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/mushroom.zip', - 'signature': '884c43cb70db35d211c67b1cf6a3683b2b4569393d2789d5c07840da4dc85ba8', - 'type': 'pandas', - }, - 'hobbies': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/hobbies.zip', - 'signature': '415c8f68df1486d5d84a1d1757a5aa3035aef5ad63ede5013c261d622fbd29d8', - 'type': 'corpus', - }, - 'game': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/game.zip', - 'signature': 'b1bd85789a014a898daa34cb5f89ceab6d2cd6488a2e572187e34aa4ec21a43b', - 'type': 'pandas', - }, - 'bikeshare': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/bikeshare.zip', - 'signature': 'a9b440f65549746dff680c92ff8bdca3c7265f09db1cf09e708e6e26fc8aba44', - 'type': 'numpy', - }, - 'spam': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/spam.zip', - 'signature': '65be21196ba3d8448847409b70a67d761f873f30719c807600eb516d7aef1de1', - 'type': 'numpy', - }, -} - -FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures") - - -########################################################################## -## Test Cases that Require Download -########################################################################## - -class DatasetMixin(object): - """ - Mixin for unittest.TestCase class to download datasets from S3 for - testing real world machine learning visual diagnostics. - """ - - @staticmethod - def sha256sum(path, blocksize=65536): - """ - Computes the SHA256 signature of a file to verify that the file has not - been modified in transit and that it is the correct version of the data. - """ - sig = hashlib.sha256() - with open(path, 'rb') as f: - buf = f.read(blocksize) - while len(buf) > 0: - sig.update(buf) - buf = f.read(blocksize) - return sig.hexdigest() - - - @staticmethod - def download_data(url, path=FIXTURES, signature=None, extract=True): - """ - Downloads the zipped data set specified at the given URL, saving it to - the output path specified. This function verifies the download with the - given signature (if supplied) and extracts the zip file if requested. - """ - if requests is None: - raise ImportError( - "The requests module is required to download data --\n" - "please install it with pip install requests." - ) - - # Create the output directory if it does not exist - if not os.path.exists(path): - os.mkdir(path) - - # Get the name of the file from the URL - name = os.path.basename(url) - dlpath = os.path.join(path, name) - - # Fetch the response in a streaming fashion and write it to disk. - response = requests.get(url, stream=True) - with open(dlpath, 'wb') as f: - for chunk in response.iter_content(65536): - f.write(chunk) - - # If verify, compare the signature - if signature is not None: - dlsignature = DatasetMixin.sha256sum(dlpath) - if signature != dlsignature: - raise ValueError( - "Download signature does not match hardcoded signature!" - ) - - # If extract, extract the zipfile. - if extract: - zf = zipfile.ZipFile(dlpath) - zf.extractall(path) - - - @staticmethod - def download_all(path=FIXTURES, verify=True, extract=True): - """ - Downloads all the example datasets. If verify is True then compare the - download signature with the hardcoded signature. If extract is True then - extract the contents of the zipfile to the given path. - """ - for name, meta in DATASETS.items(): - url = meta['url'] - signature = meta['signature'] if verify else None - - DatasetMixin.download_data( - url, path=path, signature=signature, extract=extract - ) - - @staticmethod - def remove_all(fixtures=FIXTURES): - """ - Removes all the downloaded datasets as clean up - """ - shutil.rmtree(fixtures) - - @staticmethod - def load_data(name, fixtures=FIXTURES): - """ - Loads the numpy matrix from the specified data set, downloads it if - it hasn't already been downloaded. - """ - # Just in case this is a corpus data set, then do that instead. - if DATASETS[name]['type'] == 'corpus': - return DatasetMixin.load_corpus(name, fixtures) - - if DATASETS[name]['type'] == 'pandas': - return DatasetMixin.load_pandas(name, fixtures) - - path = DatasetMixin._lookup_path(name, fixtures) - return np.genfromtxt(path, dtype=float, delimiter=',', names=True) - - @staticmethod - def load_corpus(name, fixtures=FIXTURES): - """ - Loads a sklearn Bunch with the corpus and downloads it if it hasn't - already been downloaded. Used to test text visualizers. - """ - path = DatasetMixin._lookup_path(name, fixtures, ext=None) - - # Read the directories in the directory as the categories. - categories = [ - cat for cat in os.listdir(path) - if os.path.isdir(os.path.join(path, cat)) - ] - - files = [] # holds the file names relative to the root - data = [] # holds the text read from the file - target = [] # holds the string of the category - - # Load the data from the files in the corpus - for cat in categories: - for name in os.listdir(os.path.join(path, cat)): - files.append(os.path.join(path, cat, name)) - target.append(cat) - - with io.open(os.path.join(path, cat, name), 'r', encoding='UTF-8', errors='ignore') as f: - data.append(f.read()) - - # Return the data bunch for use similar to the newsgroups example - return Bunch( - categories=categories, - files=files, - data=data, - target=target, - ) - - @staticmethod - def load_pandas(name, fixtures=FIXTURES): - """ - Loads a pandas Dataframe with the specified - """ - if pd is None: - raise ImportError("pandas is required to load this dataset") - - path = DatasetMixin._lookup_path(name, fixtures) - return pd.read_csv(path) - - @staticmethod - def _lookup_path(name, fixtures=FIXTURES, ext=".csv"): - """ - Looks up the path to the dataset, downloading it if necessary - """ - if ext is None: - path = os.path.join(fixtures, name) - else: - path = os.path.join(fixtures, name, "{}{}".format(name, ext)) - - if not os.path.exists(path): - DatasetMixin.download_all(path=fixtures) - return path diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 000000000..449ed631b --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,25 @@ +# tests.fixtures +# Helpers for pytest fixtures and data related testing. +# +# Author: Zijie (ZJ) Poh +# Created: Wed Feb 13 13:24:24 2019 -0400 +# +# Copyright (C) 2019 The scikit-yb developers. +# For license information, see LICENSE.txt +# +# ID: fixtures.py [eb9f8cc] 8103276+zjpoh@users.noreply.github.com $ + +""" +Helpers for pytest fixtures and data related testing. +""" + +########################################################################## +## Imports and Module Variables +########################################################################## + +from collections import namedtuple + + +## Used for wrapping an dataset into a single variable. +Dataset = namedtuple("Dataset", "X,y") +Split = namedtuple("Split", "train,test") diff --git a/tests/images.py b/tests/images.py index f4c945565..d0d46a11e 100644 --- a/tests/images.py +++ b/tests/images.py @@ -4,10 +4,10 @@ # Author: Benjamin Bengfort # Created: Fri Mar 02 21:51:56 2018 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: images.py [] benjamin@bengfort.com $ +# ID: images.py [ab37b18] benjamin@bengfort.com $ """ Helper utility to manage baseline images for image comparisons. Usage: @@ -37,6 +37,7 @@ ## Helper Methods ########################################################################## + def relpath(path): """ Compute the path relative to the test directory. @@ -100,7 +101,7 @@ def list_images(path): bname, bext = os.path.splitext(name) output.append(bname) - output.append("-"*len(bname)) + output.append("-" * len(bname)) # Handle base path base_path = os.path.join(BASELINE, path, name) @@ -117,7 +118,7 @@ def list_images(path): output.append(" - no actual image") # Handle diff path - diff_path = os.path.join(ACTUAL, path, '{}-failed-diff{}'.format(bname, bext)) + diff_path = os.path.join(ACTUAL, path, "{}-failed-diff{}".format(bname, bext)) if diff_path in diffs: output.append(" - {}".format(os.path.relpath(diff_path))) @@ -132,10 +133,10 @@ def sync(path): Move all non-diff images from actual to baseline """ for fname in os.listdir(os.path.join(ACTUAL, path)): - if fname.endswith('-diff.png'): + if fname.endswith("-diff.png"): continue - if fname.endswith('.png'): + if fname.endswith(".png"): src = os.path.join(ACTUAL, path, fname) dst = os.path.join(BASELINE, path, fname) shutil.copy2(src, dst) @@ -146,6 +147,7 @@ def sync(path): ## Main Method ########################################################################## + def main(args): # Get directories relative to test dir test_dirs = list(map(relpath, args.test_dirs)) @@ -170,28 +172,28 @@ def main(args): sync(path) -if __name__ == '__main__': +if __name__ == "__main__": args = { - ('-C', '--clear'): { - 'action': 'store_true', - 'help': 'clear actual, diffs, and baseline images for test', + ("-C", "--clear"): { + "action": "store_true", + "help": "clear actual, diffs, and baseline images for test", }, - ('-L', '--list'): { - 'action': 'store_true', - 'help': 'list images images for tests and exit', + ("-L", "--list"): { + "action": "store_true", + "help": "list images images for tests and exit", }, - 'test_dirs' : { - 'metavar': 'DIR', 'nargs': '+', - 'help': 'directories to move images from actual to baseline', + "test_dirs": { + "metavar": "DIR", + "nargs": "+", + "help": "directories to move images from actual to baseline", }, } - # Create the parser and add the arguments parser = argparse.ArgumentParser( description="utility to manage baseline images for comparisons", - epilog="report any issues on GitHub" + epilog="report any issues on GitHub", ) for pargs, kwargs in args.items(): diff --git a/tests/rand.py b/tests/rand.py index 15e2533ac..78b930d85 100644 --- a/tests/rand.py +++ b/tests/rand.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Wed Mar 21 17:51:15 2018 -0400 # -# ID: random.py [] benjamin@bengfort.com $ +# ID: rand.py [cc69b3c] davidwaterman@gmail.com $ """ A visualizer that draws a random scatter plot for testing. @@ -26,6 +26,7 @@ ## Random Visualizer ########################################################################## + class RandomVisualizer(Visualizer): """ Creates random scatter plots as a testing utility. @@ -49,16 +50,13 @@ class RandomVisualizer(Visualizer): Used to specify the seed of the random state to ensure tests work. """ - def __init__(self, ax=None, n_samples=100, n_blobs=3, - random_state=None, **kwargs): + def __init__(self, ax=None, n_samples=100, n_blobs=3, random_state=None, **kwargs): super(RandomVisualizer, self).__init__(ax=ax, **kwargs) if isinstance(random_state, (int, float)) or random_state is None: random_state = np.random.RandomState(random_state) - self.set_params( - n_samples=n_samples, n_blobs=n_blobs, random_state=random_state, - ) + self.set_params(n_samples=n_samples, n_blobs=n_blobs, random_state=random_state) def generate(self): """ @@ -79,8 +77,8 @@ def generate(self): def fit(self, *args, **kwargs): X, c = self.generate() - x = X[:,0] - y = X[:,1] + x = X[:, 0] + y = X[:, 1] self.draw(x, y, c) return self @@ -89,7 +87,7 @@ def draw(self, x, y, c): colors = resolve_colors(self.n_blobs) for i in np.arange(self.n_blobs): - mask = c==i + mask = c == i label = "c{}".format(i) self.ax.scatter(x[mask], y[mask], label=label, c=colors[i]) @@ -103,7 +101,7 @@ def finalize(self): return self.ax -if __name__ == '__main__': +if __name__ == "__main__": r = RandomVisualizer() r.fit() - r.poof(outpath='test.png') + r.poof(outpath="test.png") diff --git a/tests/requirements.txt b/tests/requirements.txt index 64bf01d3a..6ac14d583 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,22 +1,20 @@ # Library Dependencies -matplotlib>=1.5.1,!=3.0.0 -scipy>=0.19 -scikit-learn>=0.19 +matplotlib>=2.0.2,!=3.0.0,!=3.1.1 +scipy>=1.0.0 +scikit-learn>=0.20 numpy>=1.13.0 cycler>=0.10.0 # Testing Requirements -pytest>=3.4.1,<4.2.0 -pytest-cov>=2.5.1 -pytest-flakes>=2.0.0 -pytest-spec>=1.1.0 -coverage>=4.4.1 -requests>=2.18.3 -six==1.11.0 - -# Python 2 Testing Requirements -mock>=2.0.0 +pytest>=4.2.0, !=4.6.0 +pytest-cov>=2.6.1 +pytest-flakes>=4.0.0 +#pytest-spec>=1.1.0 +coverage>=4.5.2 # Optional Testing Dependencies nltk>=3.2 +# spacy>=2.0.18 pandas>=0.20 +umap-learn==0.3 +numba==0.42 diff --git a/tests/test_base.py b/tests/test_base.py index c599ee2e4..a05894fcc 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,11 +1,14 @@ # tests.test_base.py # Assertions for the base classes and abstract hierarchy. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Author: Neal Humphrey # Created: Sat Oct 08 18:34:30 2016 -0400 # +# Copyright (C) 2016 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: test_base.py [83131ef] benjamin@bengfort.com $ """ @@ -16,24 +19,28 @@ ## Imports ########################################################################## -import sys import pytest import matplotlib.pyplot as plt from yellowbrick.base import * from yellowbrick.base import VisualizerGrid +from yellowbrick.datasets import load_occupancy +from yellowbrick.exceptions import YellowbrickWarning from yellowbrick.exceptions import YellowbrickValueError -from tests.base import VisualTestCase +from unittest.mock import patch +from unittest.mock import MagicMock from tests.rand import RandomVisualizer +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase +from sklearn.svm import LinearSVC from sklearn.datasets import make_classification - ########################################################################## ## Base Cases ########################################################################## + class TestBaseClasses(VisualTestCase): """ Tests for the high-level API of Yellowbrick and base classes @@ -51,6 +58,38 @@ def test_visualizer_ax_property(self): assert viz._ax == "foo" assert viz.ax == "foo" + def test_visualizer_fig_property(self): + """ + Test the fig property on the Visualizer + """ + viz = Visualizer() + assert viz._fig is None + assert viz.fig is not None + + viz.fig = "foo" + assert viz._fig == "foo" + assert viz.fig == "foo" + + def test_size_property(self): + """ + Test the size property on the base Visualizer + """ + fig = plt.figure(figsize=(1, 2)) + viz = Visualizer() + + assert viz._size is None + assert viz.size is not None + + fsize = fig.get_size_inches() * fig.get_dpi() + assert all(viz.size) == all(fsize) + + viz.size = (1080, 720) + assert viz._size == (1080, 720) + assert viz.size == (1080, 720) + + fsize = fig.get_size_inches() * fig.get_dpi() + assert all(viz.size) == all(fsize) + def test_visualizer_fit_returns_self(self): """ Assert that all visualizers return self @@ -73,58 +112,133 @@ def test_finalize_interface(self): viz = Visualizer() assert viz.finalize() is viz.ax - def test_size_property(self): + @patch("yellowbrick.base.plt") + def test_poof_show_interface(self, mock_plt): """ - Test the size property on the base Visualizer + Test poof calls plt.show and other figure finalization correctly """ - fig = plt.figure(figsize =(1,2)) - viz = Visualizer() - assert viz._size is None - assert viz.size is not None + class CustomVisualizer(Visualizer): + pass - fsize = fig.get_size_inches() * fig.get_dpi() - assert all(viz.size) == all(fsize) + _, ax = plt.subplots() + viz = CustomVisualizer(ax=ax) + viz.finalize = MagicMock() + assert viz.poof() is ax - viz.size = (1080, 720) - assert viz._size == (1080, 720) - assert viz.size == (1080, 720) + viz.finalize.assert_called_once_with() + mock_plt.show.assert_called_once_with() + mock_plt.savefig.assert_not_called() + + @patch("yellowbrick.base.plt") + def test_poof_savefig_interface(self, mock_plt): + """ + Test poof calls plt.savefig and other figure finalization correctly + """ + + class CustomVisualizer(Visualizer): + pass + + _, ax = plt.subplots() + viz = CustomVisualizer(ax=ax) + viz.finalize = MagicMock() + assert viz.poof(outpath="test.png") is ax + + viz.finalize.assert_called_once_with() + mock_plt.show.assert_not_called() + mock_plt.savefig.assert_called_once_with("test.png") + + @patch("yellowbrick.base.plt") + def test_poof_warns(self, mock_plt): + """ + Test poof issues a warning when no axes has been modified + """ + + class CustomVisualizer(Visualizer): + pass + + with pytest.warns(YellowbrickWarning): + viz = CustomVisualizer() + assert viz.poof() is not None + + +########################################################################## +## ScoreVisualizer Cases +########################################################################## - fsize = fig.get_size_inches() * fig.get_dpi() - assert all(viz.size) == all(fsize) + +class MockVisualizer(ScoreVisualizer): + """ + Mock for a downstream score visualizer + """ + + def fit(self, X, y): + super(MockVisualizer, self).fit(X, y) + + +class TestScoreVisualizer(VisualTestCase): + """ + Tests for the ScoreVisualizer + """ + + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + model = LinearSVC().fit(X, y) + classes = ["unoccupied", "occupied"] + + with patch.object(model, "fit") as mockfit: + oz = MockVisualizer(model, classes=classes) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = MockVisualizer(model, classes=classes, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = MockVisualizer(model, classes=classes, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) ########################################################################## ## Visual Grid Cases ########################################################################## + +@pytest.mark.filterwarnings("ignore:Matplotlib is currently using agg") class TestVisualizerGrid(VisualTestCase): """ Tests for the VisualizerGrid layout class """ @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) def test_draw_visualizer_grid(self): """ Draw a 4 visualizers grid with default options """ - visualizers = [ - RandomVisualizer(random_state=(1+x)**2) - for x in range(4) - ] + visualizers = [RandomVisualizer(random_state=(1 + x) ** 2) for x in range(4)] X, y = make_classification(random_state=78) grid = VisualizerGrid(visualizers) grid.fit(X, y) - grid.poof() + # poof is required here (do not replace with finalize)! + assert grid.poof() is not None self.assert_images_similar(grid) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) def test_draw_with_rows(self): """ @@ -139,12 +253,14 @@ def test_draw_with_rows(self): grid = VisualizerGrid(visualizers, nrows=2) grid.fit(X, y) - grid.poof() + # poof is required here (do not replace with finalize)! + assert grid.poof() is not None self.assert_images_similar(grid) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) def test_draw_with_cols(self): """ @@ -159,7 +275,8 @@ def test_draw_with_cols(self): grid = VisualizerGrid(visualizers, ncols=2) grid.fit(X, y) - grid.poof() + # poof is required here (do not replace with finalize)! + assert grid.poof() is not None self.assert_images_similar(grid) diff --git a/tests/test_bestfit.py b/tests/test_bestfit.py index 8641b4a91..178a6db26 100644 --- a/tests/test_bestfit.py +++ b/tests/test_bestfit.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Sun Jun 26 19:27:39 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_bestfit.py [56236f3] benjamin@bengfort.com $ @@ -35,49 +35,50 @@ ## Best fit tests ########################################################################## -class BestFitTests(VisualTestCase): +class TestBestFit(VisualTestCase): def test_bad_estimator(self): """ Test that a bad estimator name raises a value error. """ - fig, axe = plt.subplots() + fig, ax = plt.subplots() X, y = ANSCOMBE[1] - with self.assertRaises(YellowbrickValueError): - draw_best_fit(X, y, axe, 'pepper') + with pytest.raises(YellowbrickValueError): + draw_best_fit(X, y, ax, "pepper") def test_ensure_same_length(self): """ Ensure that vectors of different lengths raise """ - fig, axe = plt.subplots() + fig, ax = plt.subplots() X = np.array([1, 2, 3, 5, 8, 10, 2]) y = np.array([1, 3, 6, 2]) - with self.assertRaises(YellowbrickValueError): - draw_best_fit(X, y, axe, 'linear') + with pytest.raises(YellowbrickValueError): + draw_best_fit(X, y, ax, "linear") - with self.assertRaises(YellowbrickValueError): - draw_best_fit(X[:,np.newaxis], y, axe, 'linear') + with pytest.raises(YellowbrickValueError): + draw_best_fit(X[:, np.newaxis], y, ax, "linear") - @pytest.mark.filterwarnings('ignore') - def testdraw_best_fit(self): + @pytest.mark.filterwarnings("ignore") + def test_draw_best_fit(self): """ Test that drawing a best fit line works. """ - fig, axe = plt.subplots() + fig, ax = plt.subplots() X, y = ANSCOMBE[0] - self.assertEqual(axe, draw_best_fit(X, y, axe, 'linear')) - self.assertEqual(axe, draw_best_fit(X, y, axe, 'quadratic')) + assert ax == draw_best_fit(X, y, ax, "linear") + assert ax == draw_best_fit(X, y, ax, "quadratic") ########################################################################## ## Estimator tests ########################################################################## -class EstimatorTests(VisualTestCase): + +class TestEstimator(VisualTestCase): """ Test the estimator functions for best fit lines. """ @@ -89,12 +90,11 @@ def test_linear(self): X, y = ANSCOMBE[0] X = np.array(X) y = np.array(y) - X = X[:,np.newaxis] + X = X[:, np.newaxis] model = fit_linear(X, y) - self.assertIsNotNone(model) - self.assertIsInstance(model, LinearRegression) - + assert model is not None + assert isinstance(model, LinearRegression) def test_quadratic(self): """ @@ -103,11 +103,11 @@ def test_quadratic(self): X, y = ANSCOMBE[1] X = np.array(X) y = np.array(y) - X = X[:,np.newaxis] + X = X[:, np.newaxis] model = fit_quadratic(X, y) - self.assertIsNotNone(model) - self.assertIsInstance(model, Pipeline) + assert model is not None + assert isinstance(model, Pipeline) def test_select_best(self): """ @@ -116,17 +116,17 @@ def test_select_best(self): X, y = ANSCOMBE[1] X = np.array(X) y = np.array(y) - X = X[:,np.newaxis] + X = X[:, np.newaxis] model = fit_select_best(X, y) - self.assertIsNotNone(model) - self.assertIsInstance(model, Pipeline) + assert model is not None + assert isinstance(model, Pipeline) X, y = ANSCOMBE[3] X = np.array(X) y = np.array(y) - X = X[:,np.newaxis] + X = X[:, np.newaxis] model = fit_select_best(X, y) - self.assertIsNotNone(model) - self.assertIsInstance(model, LinearRegression) + assert model is not None + assert isinstance(model, LinearRegression) diff --git a/tests/test_classifier/__init__.py b/tests/test_classifier/__init__.py index c82b7f039..c366aa959 100644 --- a/tests/test_classifier/__init__.py +++ b/tests/test_classifier/__init__.py @@ -1,6 +1,12 @@ -#Backend must be set before first use. -# Setting backend here allows us to run tests just in this folder, without running the whole yellowbrick.tests folder -# This command will have no effect if backend has already been set previously. -import matplotlib -matplotlib.use('Agg') +# tests.test_classifier +# Tests for the classifier visualizers +# +# ID: __init__.py [5388065] neal@nhumphrey.com $ +""" +Tests for the classifier visualizers +""" + +########################################################################## +## Imports +########################################################################## diff --git a/tests/test_classifier/conftest.py b/tests/test_classifier/conftest.py index 799ac3f11..deea5363f 100644 --- a/tests/test_classifier/conftest.py +++ b/tests/test_classifier/conftest.py @@ -1,10 +1,13 @@ # tests.test_classifier.conftest # Provides fixtures for the classification tests module. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Mar 23 18:07:00 2018 -0400 # -# ID: conftest.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: conftest.py [1e04216] benjamin@bengfort.com $ """ Provides fixtures for the classification tests module. @@ -16,47 +19,97 @@ import pytest -from tests.dataset import Dataset, Split +from tests.fixtures import Dataset, Split +from yellowbrick.exceptions import NotFitted +from sklearn.exceptions import NotFittedError from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split as tts +########################################################################## +## Assertion Helpers +########################################################################## + +ATTRS = ["classes_", "class_count_", "score_"] + + +def assert_not_fitted(estimator, attrs=ATTRS, X_test=None): + """ + Check that the estimator is not fitted by ensuring it does not have + any of the attributes specified in attrs. If X_test is specified, + it is passed to predict, which must also raise a NotFitted exception. + """ + __traceback_hide__ = True + for attr in attrs: + msg = "model is fitted, has {} attribute".format(attr) + assert not hasattr(estimator, attr), msg + + if X_test is not None: + with pytest.raises((NotFitted, NotFittedError)): + estimator.predict(X_test) + + +def assert_fitted(estimator, attrs=ATTRS, X_test=None): + """ + Check that the estimator is fitted by ensuring it does have the attributes + passed in attrs. If X_test is specified, it is passed to predict which + must not raise a NotFitted exception. + """ + __traceback_hide__ = True + for attr in attrs: + msg = "model is not fitted, does not have {} attribute".format(attr) + assert hasattr(estimator, attr), msg + + if X_test is not None: + try: + estimator.predict(X_test) + except (NotFitted, NotFittedError): + pytest.fail("estimator not fitted raised from predict") + + ########################################################################## ## Fixtures ########################################################################## -@pytest.fixture(scope='class') + +@pytest.fixture(scope="class") def binary(request): """ Creates a random binary classification dataset fixture """ X, y = make_classification( - n_samples=500, n_features=20, n_informative=8, n_redundant=2, - n_classes=2, n_clusters_per_class=3, random_state=87 + n_samples=500, + n_features=20, + n_informative=8, + n_redundant=2, + n_classes=2, + n_clusters_per_class=3, + random_state=87, ) - X_train, X_test, y_train, y_test = tts( - X, y, test_size=0.2, random_state=93 - ) + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=93) dataset = Dataset(Split(X_train, X_test), Split(y_train, y_test)) request.cls.binary = dataset -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def multiclass(request): """ Creates a random multiclass classification dataset fixture """ X, y = make_classification( - n_samples=500, n_features=20, n_informative=8, n_redundant=2, - n_classes=6, n_clusters_per_class=3, random_state=87 + n_samples=500, + n_features=20, + n_informative=8, + n_redundant=2, + n_classes=6, + n_clusters_per_class=3, + random_state=87, ) - X_train, X_test, y_train, y_test = tts( - X, y, test_size=0.2, random_state=93 - ) + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=93) dataset = Dataset(Split(X_train, X_test), Split(y_train, y_test)) request.cls.multiclass = dataset diff --git a/tests/test_classifier/test_base.py b/tests/test_classifier/test_base.py new file mode 100644 index 000000000..1455f47d2 --- /dev/null +++ b/tests/test_classifier/test_base.py @@ -0,0 +1,249 @@ +# tests.test_classifier.test_base +# Tests for the base classification visualizers +# +# Author: Benjamin Bengfort +# Created: Wed Jul 31 11:21:28 2019 -0400 +# +# Copyright (C) 2019 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_base.py [da729da] benjamin@bengfort.com $ + +""" +Tests for the base classification visualizers +""" + +########################################################################## +## Imports +########################################################################## + +import pytest +import numpy as np +import numpy.testing as npt + +from yellowbrick.classifier.base import * +from sklearn.naive_bayes import GaussianNB +from sklearn.preprocessing import LabelEncoder +from sklearn.linear_model import LinearRegression +from .conftest import assert_fitted, assert_not_fitted +from yellowbrick.exceptions import YellowbrickTypeError + + +########################################################################## +## Test Classification Score Visualizer +########################################################################## + + +@pytest.mark.usefixtures("binary", "multiclass") +class TestClassificationScoreVisualizer(object): + """ + Test the ClassificationScoreVisualizer base functionality + """ + + def test_fit_score(self): + """ + Ensure correct fit and score behavior + """ + oz = ClassificationScoreVisualizer(GaussianNB()) + assert_not_fitted(oz, X_test=self.binary.X.test) + assert oz.fit(self.binary.X.train, self.binary.y.train) is oz + assert 0.0 <= oz.score(self.binary.X.test, self.binary.y.test) <= 1.0 + assert_fitted(oz, X_test=self.binary.X.test) + + def test_class_counts(self): + """ + Test class and class counts identification + """ + oz = ClassificationScoreVisualizer(GaussianNB()) + oz.fit(self.multiclass.X.train, self.multiclass.y.train) + + unique, counts = np.unique(self.multiclass.y.train, return_counts=True) + npt.assert_array_equal(oz.classes_, unique) + npt.assert_array_equal(oz.class_counts_, counts) + + def test_force_estimator(self): + """ + Test that an estimator can be forced through + """ + with pytest.raises(YellowbrickTypeError): + ClassificationScoreVisualizer(LinearRegression()) + + try: + ClassificationScoreVisualizer(LinearRegression(), force_model=True) + except YellowbrickTypeError as e: + pytest.fail("type error was raised incorrectly: {}".format(e)) + + def test_score_with_fitted_estimator(self): + """ + Assert fitted estimator can be scored without fit but warns + """ + model = GaussianNB().fit(self.binary.X.train, self.binary.y.train) + + # NOTE that the wrapper will pass a call down to `classes_` + oz = ClassificationScoreVisualizer(model) + assert_not_fitted(oz, ["class_counts_", "score_"]) + + msg = "could not determine class_counts_" + with pytest.warns(YellowbrickWarning, match=msg): + oz.score(self.binary.X.test, self.binary.y.test) + assert_fitted(oz, ["classes_", "class_counts_", "score_"]) + + def test_score_without_fitted_estimator(self): + """ + Assert score without fitted estimator raises NotFitted + """ + oz = ClassificationScoreVisualizer(GaussianNB()) + assert_not_fitted(oz) + + with pytest.raises(NotFitted): + oz.score(self.binary.X.test, self.binary.y.test) + assert_not_fitted(oz) + + def test_colors_property(self): + """ + Test that a unique color per class is created after fit + """ + oz = ClassificationScoreVisualizer(GaussianNB()) + + with pytest.raises(NotFitted, match="cannot determine colors before fit"): + oz.colors + + oz.fit(self.multiclass.X.train, self.multiclass.y.train) + assert len(oz.colors) == len(oz.classes_) + + def test_decode_labels_warning(self): + """ + Assert warning is issued and encoder is used with multiple decoding params + """ + with pytest.warns( + YellowbrickWarning, match="both classes and encoder specified" + ): + oz = ClassificationScoreVisualizer( + GaussianNB(), + classes=["a", "b", "c"], + encoder={0: "foo", 1: "bar", 2: "zap"}, + ) + encoded = oz._decode_labels([0, 1, 2]) + npt.assert_array_equal(encoded, ["foo", "bar", "zap"]) + + def test_decode_labels_from_numeric(self): + """ + Test that a numeric y can be decoded using classes and encoder + """ + classes = np.array(["a", "b", "c", "d", "e"]) + y = np.random.randint(0, 5, 100) + decoded = classes[y] + + oz = ClassificationScoreVisualizer(GaussianNB, classes=classes) + npt.assert_array_equal(oz._decode_labels(y), decoded) + + encoder = dict(zip(range(len(classes)), classes)) + oz = ClassificationScoreVisualizer(GaussianNB, encoder=encoder) + npt.assert_array_equal(oz._decode_labels(y), decoded) + + encoder = LabelEncoder().fit(decoded) + oz = ClassificationScoreVisualizer(GaussianNB, encoder=encoder) + npt.assert_array_equal(oz._decode_labels(y), decoded) + + def test_decode_labels_from_strings(self): + """ + Test that string y can be decoded using classes and encoder + """ + classes = np.array(["a", "b", "c", "d", "e"]) + decoded = classes[np.random.randint(0, 5, 100)] + y = np.array([v.upper() for v in decoded]) + + oz = ClassificationScoreVisualizer(GaussianNB, classes=classes) + npt.assert_array_equal(oz._decode_labels(y), decoded) + + encoder = {c.upper(): c for c in classes} + oz = ClassificationScoreVisualizer(GaussianNB, encoder=encoder) + npt.assert_array_equal(oz._decode_labels(y), decoded) + + class L2UTransformer(object): + def transform(self, y): + return np.array([yi.upper() for yi in y]) + + def inverse_transform(self, y): + return np.array([yi.lower() for yi in y]) + + oz = ClassificationScoreVisualizer(GaussianNB, encoder=L2UTransformer()) + npt.assert_array_equal(oz._decode_labels(y), decoded) + + def test_decode_labels_unknown_class(self): + """ + Ensure a human-understandable error is raised when decode fails + """ + classes = np.array(["a", "b", "c", "d", "e"]) + y = classes[np.random.randint(0, 5, 100)] + + # Remove class "c" from the known array labels + classes = np.array(["a", "b", "d", "e"]) + + oz = ClassificationScoreVisualizer(GaussianNB, classes=classes) + with pytest.raises(ModelError, match="could not decode"): + npt.assert_array_equal(oz._decode_labels(y), decoded) + + encoder = dict(zip(classes, range(len(classes)))) + oz = ClassificationScoreVisualizer(GaussianNB, encoder=encoder) + with pytest.raises(ModelError, match="cannot decode class 'c' to label"): + npt.assert_array_equal(oz._decode_labels(y), decoded) + + encoder = LabelEncoder().fit(classes[np.random.randint(0, 4, 100)]) + oz = ClassificationScoreVisualizer(GaussianNB, encoder=encoder) + with pytest.raises(ModelError, match="could not decode"): + npt.assert_array_equal(oz._decode_labels(y), decoded) + + def test_labels(self): + """ + Check visualizer can return human labels correctly + """ + classes = np.array(["a", "b", "c", "d", "e"]) + y = classes[np.random.randint(0, 5, 100)] + + oz = ClassificationScoreVisualizer(GaussianNB, classes=classes) + npt.assert_array_equal(oz._labels(), classes) + + encoder = dict(zip(range(len(classes)), classes)) + oz = ClassificationScoreVisualizer(GaussianNB, encoder=encoder) + npt.assert_array_equal(oz._labels(), classes) + + encoder = LabelEncoder().fit(y) + oz = ClassificationScoreVisualizer(GaussianNB, encoder=encoder) + npt.assert_array_equal(oz._labels(), classes) + + def test_labels_warning(self): + """ + Assert warning and encoder is used with multiple decoding params for labels + """ + with pytest.warns( + YellowbrickWarning, match="both classes and encoder specified" + ): + oz = ClassificationScoreVisualizer( + GaussianNB(), + classes=["a", "b", "c"], + encoder={0: "foo", 1: "bar", 2: "zap"}, + ) + labels = oz._labels() + npt.assert_array_equal(labels, ["foo", "bar", "zap"]) + + def test_labels_encoder_no_classes(self): + """ + Assert warning and None returned if encoder doesn't have classes + """ + + class L2UTransformer(object): + def transform(self, y): + return np.array([yi.upper() for yi in y]) + + oz = ClassificationScoreVisualizer(GaussianNB(), encoder=L2UTransformer()) + with pytest.warns(YellowbrickWarning, match="could not determine class labels"): + assert oz._labels() is None + + def test_dict_labels_sorted(self): + """ + Ensure dictionary encoder labels are returned sorted + """ + le = {3: "a", 2: "c", 1: "b"} + oz = ClassificationScoreVisualizer(GaussianNB(), encoder=le) + npt.assert_array_equal(oz._labels(), ["b", "c", "a"]) diff --git a/tests/test_classifier/test_class_prediction_error.py b/tests/test_classifier/test_class_prediction_error.py index ae5743aa5..1c08e9899 100644 --- a/tests/test_classifier/test_class_prediction_error.py +++ b/tests/test_classifier/test_class_prediction_error.py @@ -1,12 +1,12 @@ # tests.test_classifier.test_class_prediction_error # Testing for the ClassPredictionError visualizer # -# Author: Benjamin Bengfort -# Author: Rebecca Bilbro +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Author: Larry Gray # Created: Tue May 23 13:41:55 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_rocauc.py [] benjamin@bengfort.com $ @@ -22,74 +22,114 @@ import pytest import matplotlib.pyplot as plt -from tests.dataset import DatasetMixin -from yellowbrick.classifier.class_prediction_error import * from yellowbrick.exceptions import ModelError +from yellowbrick.datasets import load_occupancy +from yellowbrick.classifier.class_prediction_error import * from sklearn.svm import LinearSVC from sklearn.ensemble import RandomForestClassifier -from sklearn.datasets import make_multilabel_classification, make_classification +from sklearn.datasets import make_multilabel_classification +from unittest.mock import patch from tests.base import VisualTestCase -########################################################################## -## Data -########################################################################## -X, y = make_classification( - n_classes=4, n_informative=3, n_clusters_per_class=1, random_state=42 -) +try: + import pandas as pd +except ImportError: + pd = None + ########################################################################## ## Tests ########################################################################## -class ClassPredictionErrorTests(VisualTestCase, DatasetMixin): +class TestClassPredictionError(VisualTestCase): + """ + Test ClassPredictionError visualizer + """ + + @pytest.mark.filterwarnings("ignore:could not determine class_counts_") + def test_numpy_integration(self): + """ + Assert no errors during class prediction error integration with NumPy arrays + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + classes = ["unoccupied", "occupied"] + + model = LinearSVC(random_state=42) + model.fit(X, y) + visualizer = ClassPredictionError(model, classes=classes) + visualizer.score(X, y) + visualizer.finalize() + + # AppVeyor and Linux conda fail due to non-text-based differences + # AppVeyor fails with RMS 13.161 - 13.289 (python - miniconda) + self.assert_images_similar(visualizer, tol=12.5, windows_tol=13.3) - def test_integration_class_prediction_error_(self): + @pytest.mark.filterwarnings("ignore:could not determine class_counts_") + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_pandas_integration(self): """ - Assert no errors occur during class prediction error integration + Assert no errors during class prediction error integration with Pandas """ - model = LinearSVC() + X, y = load_occupancy(return_dataset=True).to_pandas() + classes = ["unoccupied", "occupied"] + + model = LinearSVC(random_state=42) model.fit(X, y) - visualizer = ClassPredictionError(model, classes=["A", "B", "C", "D"]) + visualizer = ClassPredictionError(model, classes=classes) visualizer.score(X, y) - self.assert_images_similar(visualizer) + visualizer.finalize() + + # AppVeyor and Linux conda fail due to non-text-based differences + # AppVeyor fails with RMS 13.161 - 13.289 (python - miniconda) + self.assert_images_similar(visualizer, tol=12.5, windows_tol=13.3) def test_class_prediction_error_quickmethod(self): """ Test the ClassPreditionError quickmethod """ + X, y = load_occupancy(return_dataset=True).to_numpy() + fig = plt.figure() ax = fig.add_subplot() clf = LinearSVC(random_state=42) - g = class_prediction_error(clf, X, y, ax, random_state=42) + viz = class_prediction_error(clf, X, y, ax=ax, random_state=42) - self.assert_images_similar(ax=g) + # Not sure why the tolerance must be so high for this + # Failing on travis with RMS 9.544 + # AppVeyor and Linux conda fail due to non-text-based differences: RMS 12.961 + self.assert_images_similar(viz, tol=13, windows_tol=13) + @pytest.mark.filterwarnings("ignore:could not determine class_counts_") def test_classes_greater_than_indices(self): """ - Assert error when y and y_pred contain zero values for - one of the specified classess + A model error should be raised when there are more classes in fit than score """ - model = LinearSVC() + X, y = load_occupancy(return_dataset=True).to_numpy() + classes = ["unoccupied", "occupied", "partytime"] + + model = LinearSVC(random_state=42) model.fit(X, y) - with self.assertRaises(ModelError): - visualizer = ClassPredictionError( - model, classes=["A", "B", "C", "D", "E"] - ) + with pytest.raises(ModelError): + visualizer = ClassPredictionError(model, classes=classes) visualizer.score(X, y) def test_classes_less_than_indices(self): """ Assert error when there is an attempt to filter classes """ - model = LinearSVC() + X, y = load_occupancy(return_dataset=True).to_numpy() + classes = ["unoccupied"] + + model = LinearSVC(random_state=42) model.fit(X, y) - with self.assertRaises(NotImplementedError): - visualizer = ClassPredictionError(model, classes=["A"]) + with pytest.raises(NotImplementedError): + visualizer = ClassPredictionError(model, classes=classes) visualizer.score(X, y) @pytest.mark.skip(reason="not implemented yet") @@ -106,7 +146,7 @@ def test_class_type(self): X, y = make_multilabel_classification() model = RandomForestClassifier() model.fit(X, y) - with self.assertRaises(YellowbrickValueError): + with pytest.raises(YellowbrickValueError): visualizer = ClassPredictionError(model) visualizer.score(X, y) @@ -114,10 +154,36 @@ def test_score_returns_score(self): """ Test that ClassPredictionError score() returns a score between 0 and 1 """ + X, y = load_occupancy(return_dataset=True).to_numpy() + # Create and fit the visualizer - visualizer = ClassPredictionError(LinearSVC()) + visualizer = ClassPredictionError(LinearSVC(random_state=42)) visualizer.fit(X, y) # Score the visualizer s = visualizer.score(X, y) assert 0 <= s <= 1 + + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + model = RandomForestClassifier().fit(X, y) + classes = ["unoccupied", "occupied"] + + with patch.object(model, "fit") as mockfit: + oz = ClassPredictionError(model, classes=classes) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ClassPredictionError(model, classes=classes, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ClassPredictionError(model, classes=classes, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) diff --git a/tests/test_classifier/test_classification_report.py b/tests/test_classifier/test_classification_report.py index 1badee928..8f6e3570a 100644 --- a/tests/test_classifier/test_classification_report.py +++ b/tests/test_classifier/test_classification_report.py @@ -1,10 +1,13 @@ # tests.test_classifier.test_classification_report # Tests for the classification report visualizer # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Sun Mar 18 16:57:27 2018 -0400 # +# Copyright (C) 208 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: test_classification_report.py [] benjamin@bengfort.com $ """ @@ -20,11 +23,12 @@ import yellowbrick as yb import matplotlib.pyplot as plt +from yellowbrick.datasets import load_occupancy from yellowbrick.classifier.classification_report import * from pytest import approx +from unittest.mock import patch from tests.base import VisualTestCase -from tests.dataset import DatasetMixin from sklearn.svm import LinearSVC from sklearn.naive_bayes import GaussianNB @@ -43,8 +47,9 @@ ## Test for Classification Report ########################################################################## + @pytest.mark.usefixtures("binary", "multiclass") -class ClassificationReportTests(VisualTestCase, DatasetMixin): +class TestClassificationReport(VisualTestCase): """ ClassificationReport visualizer tests """ @@ -62,14 +67,12 @@ def test_binary_class_report(self): self.assert_images_similar(viz, tol=40) assert viz.scores_ == { - 'precision': {0: approx(0.7446808), 1: approx(0.8490566)}, - 'recall': {0: approx(0.8139534), 1: approx(0.7894736)}, - 'f1': {0: approx(0.7777777), 1: approx(0.8181818)} - } - - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + "precision": {0: approx(0.7446808), 1: approx(0.8490566)}, + "recall": {0: approx(0.8139534), 1: approx(0.7894736)}, + "f1": {0: approx(0.7777777), 1: approx(0.8181818)}, + } + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_multiclass_class_report(self): """ Correctly generates report for multi-class with LogisticRegression @@ -83,21 +86,33 @@ def test_multiclass_class_report(self): self.assert_images_similar(viz, tol=11.0) assert viz.scores_ == { - 'precision': { - 0: 0.5333333333333333, 1: 0.5, 2: 0.45, - 3: 0.4, 4: 0.4, 5: 0.5882352941176471 - }, 'recall': { - 0: 0.42105263157894735, 1: 0.5625, 2: 0.6428571428571429, - 3: 0.3157894736842105, 4: 0.375, 5: 0.625 - }, 'f1': { - 0: 0.47058823529411764, 1: 0.5294117647058824, - 2: 0.5294117647058824, 3: 0.35294117647058826, - 4: 0.38709677419354843, 5: 0.6060606060606061 - }} - - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + "precision": { + 0: 0.5333333333333333, + 1: 0.5, + 2: 0.45, + 3: 0.4, + 4: 0.4, + 5: 0.5882352941176471, + }, + "recall": { + 0: 0.42105263157894735, + 1: 0.5625, + 2: 0.6428571428571429, + 3: 0.3157894736842105, + 4: 0.375, + 5: 0.625, + }, + "f1": { + 0: 0.47058823529411764, + 1: 0.5294117647058824, + 2: 0.5294117647058824, + 3: 0.35294117647058826, + 4: 0.38709677419354843, + 5: 0.6060606060606061, + }, + } + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") @pytest.mark.skipif(pd is None, reason="test requires pandas") def test_pandas_integration(self): """ @@ -106,21 +121,52 @@ def test_pandas_integration(self): _, ax = plt.subplots() # Load the occupancy dataset from fixtures - data = self.load_data('occupancy') - target = 'occupancy' - features = [ - "temperature", "relative_humidity", "light", "C02", "humidity" - ] + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + + # Create train/test splits + splits = tts(X, y, test_size=0.2, random_state=4512) + X_train, X_test, y_train, y_test = splits - # Create instances and target - X = pd.DataFrame(data[features]) - y = pd.Series(data[target].astype(int)) + classes = ["unoccupied", "occupied"] + + # Create classification report + model = GaussianNB() + viz = ClassificationReport(model, ax=ax, classes=classes) + viz.fit(X_train, y_train) + viz.score(X_test, y_test) + + self.assert_images_similar(viz, tol=5.0) + + # Ensure correct classification scores under the hood! + assert viz.scores_ == { + "precision": { + "unoccupied": 0.999347471451876, + "occupied": 0.8825214899713467, + }, + "recall": { + "unoccupied": 0.9613935969868174, + "occupied": 0.9978401727861771, + }, + "f1": {"unoccupied": 0.9800031994880819, "occupied": 0.9366447034972124}, + } + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_numpy_integration(self): + """ + Test with NumPy arrays + """ + _, ax = plt.subplots() + + # Load the occupancy dataset from fixtures + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() # Create train/test splits splits = tts(X, y, test_size=0.2, random_state=4512) X_train, X_test, y_train, y_test = splits - classes = ['unoccupied', 'occupied'] + classes = ["unoccupied", "occupied"] # Create classification report model = GaussianNB() @@ -128,28 +174,33 @@ def test_pandas_integration(self): viz.fit(X_train, y_train) viz.score(X_test, y_test) - self.assert_images_similar(viz, tol=43.0) + self.assert_images_similar(viz, tol=5.0) # Ensure correct classification scores under the hood! assert viz.scores_ == { - 'precision': { - 'unoccupied': 0.999347471451876, - 'occupied': 0.8825214899713467 - }, 'recall': { - 'unoccupied': 0.9613935969868174, - 'occupied': 0.9978401727861771 - }, 'f1': { - 'unoccupied': 0.9800031994880819, - 'occupied': 0.9366447034972124 - }} + "precision": { + "unoccupied": 0.999347471451876, + "occupied": 0.8825214899713467, + }, + "recall": { + "unoccupied": 0.9613935969868174, + "occupied": 0.9978401727861771, + }, + "f1": {"unoccupied": 0.9800031994880819, "occupied": 0.9366447034972124}, + } def test_quick_method(self): """ Test the quick method with a random dataset """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=27 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=27, ) _, ax = plt.subplots() @@ -164,8 +215,8 @@ def test_isclassifier(self): """ message = ( - 'This estimator is not a classifier; ' - 'try a regression or clustering score visualizer instead!' + "This estimator is not a classifier; " + "try a regression or clustering score visualizer instead!" ) with pytest.raises(yb.exceptions.YellowbrickError, match=message): @@ -177,20 +228,18 @@ def test_support_count_class_report(self): """ _, ax = plt.subplots() - viz = ClassificationReport(LinearSVC(random_state=42), ax=ax, - support='count') + viz = ClassificationReport(LinearSVC(random_state=42), ax=ax, support="count") viz.fit(self.binary.X.train, self.binary.y.train) viz.score(self.binary.X.test, self.binary.y.test) self.assert_images_similar(viz, tol=40) assert viz.scores_ == { - 'precision': {0: approx(0.7446808), 1: approx(0.8490566)}, - 'recall': {0: approx(0.8139534), 1: approx(0.7894736)}, - 'f1': {0: approx(0.7777777), 1: approx(0.8181818)}, - 'support': {0: approx(0.42999999999999999), - 1: approx(0.56999999999999995)} - } + "precision": {0: approx(0.7446808), 1: approx(0.8490566)}, + "recall": {0: approx(0.8139534), 1: approx(0.7894736)}, + "f1": {0: approx(0.7777777), 1: approx(0.8181818)}, + "support": {0: approx(0.42999999999999999), 1: approx(0.56999999999999995)}, + } def test_support_percent_class_report(self): """ @@ -198,28 +247,28 @@ def test_support_percent_class_report(self): """ _, ax = plt.subplots() - viz = ClassificationReport(LinearSVC(random_state=42), ax=ax, - support='percent') + viz = ClassificationReport(LinearSVC(random_state=42), ax=ax, support="percent") viz.fit(self.binary.X.train, self.binary.y.train) viz.score(self.binary.X.test, self.binary.y.test) self.assert_images_similar(viz, tol=40) assert viz.scores_ == { - 'precision': {0: approx(0.7446808), 1: approx(0.8490566)}, - 'recall': {0: approx(0.8139534), 1: approx(0.7894736)}, - 'f1': {0: approx(0.7777777), 1: approx(0.8181818)}, - 'support': {0: approx(0.42999999999999999), - 1: approx(0.56999999999999995)} - } + "precision": {0: approx(0.7446808), 1: approx(0.8490566)}, + "recall": {0: approx(0.8139534), 1: approx(0.7894736)}, + "f1": {0: approx(0.7777777), 1: approx(0.8181818)}, + "support": {0: approx(0.42999999999999999), 1: approx(0.56999999999999995)}, + } def test_invalid_support(self): """ Ensure that bad support arguments raise exception """ - with pytest.raises(YellowbrickValueError, - match="'foo' is an invalid argument for support, use None, " \ - "True, False, 'percent', or 'count'"): + with pytest.raises( + YellowbrickValueError, + match="'foo' is an invalid argument for support, use None, " + "True, False, 'percent', or 'count'", + ): ClassificationReport(LinearSVC(), support="foo") def test_score_returns_score(self): @@ -232,3 +281,27 @@ def test_score_returns_score(self): s = viz.score(self.binary.X.test, self.binary.y.test) assert 0 <= s <= 1 + + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + model = LinearSVC().fit(X, y) + classes = ["unoccupied", "occupied"] + + with patch.object(model, "fit") as mockfit: + oz = ClassificationReport(model, classes=classes) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ClassificationReport(model, classes=classes, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ClassificationReport(model, classes=classes, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) diff --git a/tests/test_classifier/test_confusion_matrix.py b/tests/test_classifier/test_confusion_matrix.py index 5d0d1e5ba..4e96f434f 100644 --- a/tests/test_classifier/test_confusion_matrix.py +++ b/tests/test_classifier/test_confusion_matrix.py @@ -2,9 +2,12 @@ # Tests for the confusion matrix visualizer # # Aithor: Neal Humphrey -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue May 03 11:05:11 2017 -0700 # +# Copyright (C) 2017 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: test_confusion_matrix.py [] benjamin@bengfort.com $ """ @@ -16,38 +19,39 @@ ########################################################################## import sys -import six import pytest import yellowbrick as yb import numpy.testing as npt import matplotlib.pyplot as plt +from yellowbrick.exceptions import ModelError +from yellowbrick.datasets import load_occupancy from yellowbrick.classifier.confusion_matrix import * -from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset, Split +from unittest.mock import patch +from tests.fixtures import Dataset, Split +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase from sklearn.svm import SVC -from sklearn.datasets import load_digits from sklearn.naive_bayes import GaussianNB from sklearn.preprocessing import LabelEncoder from sklearn.tree import DecisionTreeClassifier -from sklearn.datasets import make_classification from sklearn.linear_model import LogisticRegression from sklearn.linear_model import PassiveAggressiveRegressor from sklearn.model_selection import train_test_split as tts +from sklearn.datasets import load_digits, make_classification try: import pandas as pd except ImportError: pd = None - ########################################################################## ## Fixtures ########################################################################## -@pytest.fixture(scope='class') + +@pytest.fixture(scope="class") def digits(request): """ Creates a fixture of train and test splits for the sklearn digits dataset @@ -59,24 +63,21 @@ def digits(request): ) # Set a class attribute for digits - request.cls.digits = Dataset( - Split(X_train, X_test), Split(y_train, y_test) - ) + request.cls.digits = Dataset(Split(X_train, X_test), Split(y_train, y_test)) ########################################################################## ## Test Cases ########################################################################## + @pytest.mark.usefixtures("digits") -class ConfusionMatrixTests(VisualTestCase, DatasetMixin): +class TestConfusionMatrix(VisualTestCase): """ - ConfusionMatrix visualizer tests + Test ConfusionMatrix visualizer """ - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_confusion_matrix(self): """ Integration test on digits dataset with LogisticRegression @@ -84,28 +85,32 @@ def test_confusion_matrix(self): _, ax = plt.subplots() model = LogisticRegression(random_state=93) - cm = ConfusionMatrix(model, ax=ax, classes=[0,1,2,3,4,5,6,7,8,9]) + cm = ConfusionMatrix(model, ax=ax, classes=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) cm.fit(self.digits.X.train, self.digits.y.train) cm.score(self.digits.X.test, self.digits.y.test) self.assert_images_similar(cm, tol=10) # Ensure correct confusion matrix under the hood - npt.assert_array_equal(cm.confusion_matrix_, np.array([ - [38, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 35, 0, 0, 0, 0, 0, 0, 2, 0], - [ 0, 0, 39, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 38, 0, 1, 0, 0, 2, 0], - [ 0, 0, 0, 0, 40, 0, 0, 1, 0, 0], - [ 0, 0, 0, 0, 0, 27, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 1, 29, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 35, 0, 1], - [ 0, 2, 0, 0, 0, 0, 0, 0, 32, 0], - [ 0, 0, 0, 0, 0, 0, 0, 1, 1, 35]])) + npt.assert_array_equal( + cm.confusion_matrix_, + np.array( + [ + [38, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 35, 0, 0, 0, 0, 0, 0, 2, 0], + [0, 0, 39, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 38, 0, 1, 0, 0, 2, 0], + [0, 0, 0, 0, 40, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 27, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 29, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 35, 0, 1], + [0, 2, 0, 0, 0, 0, 0, 0, 32, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 35], + ] + ), + ) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_no_classes_provided(self): """ Integration test on digits dataset with GaussianNB, no classes @@ -120,17 +125,23 @@ def test_no_classes_provided(self): self.assert_images_similar(cm, tol=10) # Ensure correct confusion matrix under the hood - npt.assert_array_equal(cm.confusion_matrix_, np.array([ - [36, 0, 0, 0, 1, 0, 0, 1, 0, 0], - [ 0, 31, 0, 0, 0, 0, 0, 1, 3, 2], - [ 0, 1, 34, 0, 0, 0, 0, 0, 4, 0], - [ 0, 1, 0, 33, 0, 2, 0, 2, 3, 0], - [ 0, 0, 0, 0, 36, 0, 0, 5, 0, 0], - [ 0, 0, 0, 0, 0, 27, 0, 0, 0, 0], - [ 0, 0, 1, 0, 1, 0, 28, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 36, 0, 0], - [ 0, 3, 0, 1, 0, 1, 0, 4, 25, 0], - [ 1, 2, 0, 0, 1, 0, 0, 8, 3, 22]])) + npt.assert_array_equal( + cm.confusion_matrix_, + np.array( + [ + [36, 0, 0, 0, 1, 0, 0, 1, 0, 0], + [0, 31, 0, 0, 0, 0, 0, 1, 3, 2], + [0, 1, 34, 0, 0, 0, 0, 0, 4, 0], + [0, 1, 0, 33, 0, 2, 0, 2, 3, 0], + [0, 0, 0, 0, 36, 0, 0, 5, 0, 0], + [0, 0, 0, 0, 0, 27, 0, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 28, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 36, 0, 0], + [0, 3, 0, 1, 0, 1, 0, 4, 25, 0], + [1, 2, 0, 0, 1, 0, 0, 8, 3, 22], + ] + ), + ) def test_fontsize(self): """ @@ -161,18 +172,25 @@ def test_percent_mode(self): self.assert_images_similar(cm, tol=10) # Ensure correct confusion matrix under the hood - npt.assert_array_equal(cm.confusion_matrix_, np.array([ - [16, 0, 0, 0, 0, 22, 0, 0, 0, 0], - [ 0, 11, 0, 0, 0, 26, 0, 0, 0, 0], - [ 0, 0, 10, 0, 0, 29, 0, 0, 0, 0], - [ 0, 0, 0, 6, 0, 35, 0, 0, 0, 0], - [ 0, 0, 0, 0, 11, 30, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 27, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 9, 21, 0, 0, 0], - [ 0, 0, 0, 0, 0, 29, 0, 7, 0, 0], - [ 0, 0, 0, 0, 0, 32, 0, 0, 2, 0], - [ 0, 0, 0, 0, 0, 34, 0, 0, 0, 3]])) + npt.assert_array_equal( + cm.confusion_matrix_, + np.array( + [ + [16, 0, 0, 0, 0, 22, 0, 0, 0, 0], + [0, 11, 0, 0, 0, 26, 0, 0, 0, 0], + [0, 0, 10, 0, 0, 29, 0, 0, 0, 0], + [0, 0, 0, 6, 0, 35, 0, 0, 0, 0], + [0, 0, 0, 0, 11, 30, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 27, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 9, 21, 0, 0, 0], + [0, 0, 0, 0, 0, 29, 0, 7, 0, 0], + [0, 0, 0, 0, 0, 32, 0, 0, 2, 0], + [0, 0, 0, 0, 0, 34, 0, 0, 0, 3], + ] + ), + ) + @pytest.mark.xfail(reason="class filtering is not currently supported") def test_class_filter_eg_zoom_in(self): """ Test filtering classes zooms in on the confusion matrix. @@ -180,53 +198,26 @@ def test_class_filter_eg_zoom_in(self): _, ax = plt.subplots() model = LogisticRegression(random_state=93) - cm = ConfusionMatrix(model, ax=ax, classes=[0,1,2]) + cm = ConfusionMatrix(model, ax=ax, classes=[0, 1, 2]) cm.fit(self.digits.X.train, self.digits.y.train) cm.score(self.digits.X.test, self.digits.y.test) self.assert_images_similar(cm, tol=10) # Ensure correct confusion matrix under the hood - npt.assert_array_equal(cm.confusion_matrix_, np.array([ - [38, 0, 0], - [ 0, 35, 0], - [ 0, 0, 39]])) + npt.assert_array_equal( + cm.confusion_matrix_, np.array([[38, 0, 0], [0, 35, 0], [0, 0, 39]]) + ) def test_extra_classes(self): """ - Assert that any extra classes are simply ignored - """ - # TODO: raise exception instead - _, ax = plt.subplots() - - model = LogisticRegression(random_state=93) - cm = ConfusionMatrix(model, ax=ax, classes=[0,1,2,11]) - cm.fit(self.digits.X.train, self.digits.y.train) - cm.score(self.digits.X.test, self.digits.y.test) - - npt.assert_array_equal(cm.class_counts_, [38, 37, 39, 0]) - - # Ensure correct confusion matrix under the hood - npt.assert_array_equal(cm.confusion_matrix_, np.array([ - [38, 0, 0, 0], - [ 0, 35, 0, 0], - [ 0, 0, 39, 0], - [ 0, 0, 0, 0]])) - - self.assert_images_similar(cm, tol=10) - - def test_one_class(self): - """ - Test single class confusion matrix with LogisticRegression + Assert that any extra classes raise an exception """ - _, ax = plt.subplots() - model = LogisticRegression(random_state=93) - cm = ConfusionMatrix(model, ax=ax, classes=[0]) - cm.fit(self.digits.X.train, self.digits.y.train) - cm.score(self.digits.X.test, self.digits.y.test) + cm = ConfusionMatrix(model, classes=[0, 1, 2, 11]) - self.assert_images_similar(cm, tol=10) + with pytest.raises(ModelError, match="could not decode"): + cm.fit(self.digits.X.train, self.digits.y.train) def test_defined_mapping(self): """ @@ -235,16 +226,43 @@ def test_defined_mapping(self): _, ax = plt.subplots() model = LogisticRegression(random_state=93) - classes = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] - mapping = {0: 'zero', 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', - 6: 'six', 7: 'seven', 8: 'eight', 9: 'nine'} - cm = ConfusionMatrix(model, ax=ax, classes=classes, label_encoder=mapping) + classes = np.array( + [ + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + ] + ) + mapping = { + 0: "zero", + 1: "one", + 2: "two", + 3: "three", + 4: "four", + 5: "five", + 6: "six", + 7: "seven", + 8: "eight", + 9: "nine", + } + cm = ConfusionMatrix(model, ax=ax, encoder=mapping) cm.fit(self.digits.X.train, self.digits.y.train) cm.score(self.digits.X.test, self.digits.y.test) - assert [l.get_text() for l in ax.get_xticklabels()] == classes + xlabels = np.array([l.get_text() for l in ax.get_xticklabels()]) + npt.assert_array_equal(xlabels, classes) + ylabels = [l.get_text() for l in ax.get_yticklabels()] ylabels.reverse() + ylabels = np.asarray(ylabels) + npt.assert_array_equal(ylabels, classes) def test_inverse_mapping(self): """ @@ -254,20 +272,36 @@ def test_inverse_mapping(self): model = LogisticRegression(random_state=93) le = LabelEncoder() - classes = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] - le.fit(['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']) + le.fit( + [ + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + ] + ) - cm = ConfusionMatrix(model, ax=ax, classes=classes, label_encoder=le) + cm = ConfusionMatrix(model, ax=ax, encoder=le) cm.fit(self.digits.X.train, self.digits.y.train) cm.score(self.digits.X.test, self.digits.y.test) - assert [l.get_text() for l in ax.get_xticklabels()] == classes + xlabels = np.array([l.get_text() for l in ax.get_xticklabels()]) + npt.assert_array_equal(xlabels, le.classes_) + ylabels = [l.get_text() for l in ax.get_yticklabels()] ylabels.reverse() - assert ylabels == classes + ylabels = np.asarray(ylabels) + npt.assert_array_equal(ylabels, le.classes_) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) @pytest.mark.skipif(pd is None, reason="test requires pandas") def test_pandas_integration(self): @@ -277,15 +311,7 @@ def test_pandas_integration(self): _, ax = plt.subplots() # Load the occupancy dataset from fixtures - data = self.load_data('occupancy') - target = 'occupancy' - features = [ - "temperature", "relative_humidity", "light", "C02", "humidity" - ] - - # Create instances and target - X = pd.DataFrame(data[features]) - y = pd.Series(data[target].astype(int)) + X, y = load_occupancy(return_dataset=True).to_pandas() # Create train/test splits splits = tts(X, y, test_size=0.2, random_state=8873) @@ -297,29 +323,34 @@ def test_pandas_integration(self): cm.fit(X_train, y_train) cm.score(X_test, y_test) - tol = 0.1 if six.PY3 else 40 - self.assert_images_similar(cm, tol=tol) + self.assert_images_similar(cm, tol=0.1) # Ensure correct confusion matrix under the hood - npt.assert_array_equal(cm.confusion_matrix_, np.array([ - [3012, 114], - [ 1, 985] - ])) + npt.assert_array_equal(cm.confusion_matrix_, np.array([[3012, 114], [1, 985]])) - @pytest.mark.skip(reason="requires random state in quick method") + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) def test_quick_method(self): """ Test the quick method with a random dataset """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=27 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=27, ) _, ax = plt.subplots() - confusion_matrix(DecisionTreeClassifier(), X, y, ax=ax) + model = DecisionTreeClassifier(random_state=25) + confusion_matrix(model, X, y, ax=ax, random_state=23) - self.assert_images_similar(ax=ax) + self.assert_images_similar(ax=ax, tol=0.1) def test_isclassifier(self): """ @@ -327,31 +358,21 @@ def test_isclassifier(self): """ model = PassiveAggressiveRegressor() message = ( - 'This estimator is not a classifier; ' - 'try a regression or clustering score visualizer instead!' + "This estimator is not a classifier; " + "try a regression or clustering score visualizer instead!" ) with pytest.raises(yb.exceptions.YellowbrickError, match=message): ConfusionMatrix(model) - @pytest.mark.xfail( - sys.platform == 'win32', reason="Changing the dtype to a subarray type is only supported if the total itemsize is unchanged" - ) def test_score_returns_score(self): """ Test that ConfusionMatrix score() returns a score between 0 and 1 """ - data = self.load_data("occupancy") - X = data[[ - "temperature", "relative_humidity", "light", "C02", "humidity" - ]] - - y = data['occupancy'] - - # Convert X to an ndarray - X = X.copy().view((float, len(X.dtype.names))) - + # Load the occupancy dataset from fixtures + X, y = load_occupancy(return_dataset=True).to_numpy() X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=42) + # Create and fit the visualizer visualizer = ConfusionMatrix(LogisticRegression()) visualizer.fit(X_train, y_train) @@ -360,3 +381,27 @@ def test_score_returns_score(self): s = visualizer.score(X_test, y_test) assert 0 <= s <= 1 + + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + model = LogisticRegression().fit(X, y) + classes = ["unoccupied", "occupied"] + + with patch.object(model, "fit") as mockfit: + oz = ConfusionMatrix(model, classes=classes) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ConfusionMatrix(model, classes=classes, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ConfusionMatrix(model, classes=classes, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) diff --git a/tests/test_classifier/test_prcurve.py b/tests/test_classifier/test_prcurve.py index d4ebbc2da..502578e24 100644 --- a/tests/test_classifier/test_prcurve.py +++ b/tests/test_classifier/test_prcurve.py @@ -1,10 +1,13 @@ # tests.test_classifier.test_prcurve # Tests for the Precision-Recall curves visualizer # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Sep 04 16:48:09 2018 -0400 # -# ID: test_prcurve.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_prcurve.py [48889c4] benjamin@bengfort.com $ """ Tests for the Precision-Recall curves visualizer @@ -16,29 +19,33 @@ import sys import pytest +import matplotlib from yellowbrick.exceptions import * from yellowbrick.classifier.prcurve import * +from yellowbrick.datasets import load_occupancy -from tests.base import VisualTestCase +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase from .test_rocauc import FakeClassifier from sklearn.svm import LinearSVC -from sklearn.datasets import load_iris from sklearn.naive_bayes import GaussianNB from sklearn.datasets import make_regression from sklearn.tree import DecisionTreeClassifier from sklearn.linear_model import RidgeClassifier +from sklearn.model_selection import train_test_split as tts from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier +try: + import pandas as pd +except ImportError: + pd = None ########################################################################## ## Assertion Helpers ########################################################################## -LEARNED_FIELDS = ( - 'target_type_', 'score_', 'precision_', 'recall_' -) +LEARNED_FIELDS = ("target_type_", "score_", "precision_", "recall_") def assert_not_fitted(oz): @@ -51,11 +58,11 @@ def assert_fitted(oz): assert hasattr(oz, field) - ########################################################################## ## PrecisionRecallCurve Tests ########################################################################## + @pytest.mark.usefixtures("binary", "multiclass") class TestPrecisionRecallCurve(VisualTestCase): """ @@ -75,7 +82,9 @@ def test_ensure_fit(self): """ Requires visualizer to be fit """ - with pytest.raises(NotFitted, match="cannot wrap an already fitted estimator"): + with pytest.raises( + NotFitted, match="this PrecisionRecallCurve instance is not fitted yet" + ): oz = PrecisionRecallCurve(RidgeClassifier()) oz.score(self.binary.X.test, self.binary.y.test) @@ -106,9 +115,15 @@ def test_binary_probability(self): # Compare the images oz.finalize() - tol = 1.5 if sys.platform == 'win32' else 1.0 # fails with RMSE 1.409 on AppVeyor + tol = ( + 1.5 if sys.platform == "win32" else 1.0 + ) # fails with RMSE 1.409 on AppVeyor self.assert_images_similar(oz, tol=tol) + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) def test_binary_probability_decision(self): """ Visual similarity of binary classifier with both predict_proba & decision @@ -136,8 +151,7 @@ def test_binary_probability_decision(self): # Compare the images oz.finalize() - tol = 4.6 if sys.platform == 'win32' else 1.0 # fails with RMSE 4.522 on AppVeyor - self.assert_images_similar(oz, tol=tol) + self.assert_images_similar(oz) def test_binary_decision(self): """ @@ -199,17 +213,27 @@ def test_multiclass_decision(self): # Compare the images oz.finalize() - tol = 1.25 if sys.platform == 'win32' else 1.0 # fails with RMSE 1.118 on AppVeyor + tol = ( + 1.25 if sys.platform == "win32" else 1.0 + ) # fails with RMSE 1.118 on AppVeyor self.assert_images_similar(oz, tol=tol) + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) def test_multiclass_probability(self): """ Visual similarity of multiclass classifier with predict_proba function """ # Create and fit the visualizer oz = PrecisionRecallCurve( - GaussianNB(), per_class=True, micro=False, fill_area=False, - iso_f1_curves=True, ap_score=False + GaussianNB(), + per_class=True, + micro=False, + fill_area=False, + iso_f1_curves=True, + ap_score=False, ) assert_not_fitted(oz) @@ -235,30 +259,207 @@ def test_multiclass_probability(self): # Compare the images oz.finalize() - tol = 6.6 if sys.platform == 'win32' else 1.0 # fails with RMSE 6.583 on AppVeyor + self.assert_images_similar(oz) + + def test_multiclass_probability_with_class_labels(self): + """Visual similarity of multiclass classifier with class labels.""" + # Create and fit the visualizer + oz = PrecisionRecallCurve( + GaussianNB(), + per_class=True, + micro=False, + fill_area=False, + iso_f1_curves=True, + ap_score=False, + classes=["a", "b", "c", "d", "e", "f"], + ) + assert_not_fitted(oz) + + # Fit returns self + assert oz.fit(self.multiclass.X.train, self.multiclass.y.train) is oz + + # Score the visualizer + s = oz.score(self.multiclass.X.test, self.multiclass.y.test) + assert_fitted(oz) + + # Score should be between 0 and 1 + assert 0.0 <= s <= 1.0 + + # Check the multiclass classification properties + assert oz.target_type_ == MULTICLASS + assert isinstance(oz.score_, dict) + assert oz.score_[MICRO] == s + assert isinstance(oz.precision_, dict) + assert isinstance(oz.recall_, dict) + assert len(oz.score_) == len(oz.classes_) + 1 + assert len(oz.precision_) == len(oz.classes_) + 1 + assert len(oz.recall_) == len(oz.classes_) + 1 + + # Finalize image + oz.finalize() + + # Compare the label text of the images. + assert oz.ax.get_xlabel() == "Recall" + oz.ax.set_xlabel("") + assert oz.ax.get_ylabel() == "Precision" + oz.ax.set_ylabel("") + assert oz.ax.get_title() == "Precision-Recall Curve for GaussianNB" + oz.ax.set_title("") + + # Compare the Legend text + expected_legend_txt = [ + "PR for class a (area=0.42)", + "PR for class b (area=0.36)", + "PR for class c (area=0.44)", + "PR for class d (area=0.52)", + "PR for class e (area=0.37)", + "PR for class f (area=0.49)", + ] + assert [x.get_text() for x in oz.ax.legend().get_texts()] == expected_legend_txt + oz.ax.get_legend().remove() + + # Text in iso_f1_curves. + # Will not check for these as they appears okay in other test images. + for child in oz.ax.get_children(): + if isinstance(child, matplotlib.text.Annotation): + oz.ax.texts.remove(child) + + # Compare the images + tol = ( + 6.6 if sys.platform == "win32" else 1.0 + ) # fails with RMSE 6.583 on AppVeyor self.assert_images_similar(oz, tol=tol) @pytest.mark.filterwarnings("ignore:From version 0.21") + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) def test_quick_method(self): """ - Test the precision_recall_curve quick method. + Test the precision_recall_curve quick method with numpy arrays. """ - data = load_iris() + X, y = load_occupancy(return_dataset=True).to_numpy() + model = DecisionTreeClassifier(random_state=14) oz = precision_recall_curve( - model, data.data, data.target, per_class=True, micro=True, - fill_area=False, iso_f1_curves=True, ap_score=False, - random_state=2) + model, + X, + y, + per_class=True, + micro=True, + fill_area=False, + iso_f1_curves=True, + ap_score=False, + random_state=2, + ) assert isinstance(oz, PrecisionRecallCurve) - tol = 5.8 if sys.platform == 'win32' else 1.0 # fails with RMSE 5.740 on AppVeyor - self.assert_images_similar(oz, tol=tol) + self.assert_images_similar(oz) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_pandas_integration(self): + """ + Test the precision_recall_curve with Pandas dataframes + """ + X, y = load_occupancy(return_dataset=True).to_pandas() + + model = DecisionTreeClassifier(random_state=14) + + X_train, X_test, y_train, y_test = tts( + X, y, test_size=0.2, shuffle=True, random_state=555 + ) + + oz = PrecisionRecallCurve( + model, + per_class=True, + micro=False, + fill_area=False, + iso_f1_curves=True, + ap_score=False, + classes=["unoccupied", "occupied"], + ) + oz.fit(X_train, y_train) + oz.score(X_test, y_test) + + oz.finalize() + + self.assert_images_similar(oz, tol=5.0) def test_no_scoring_function(self): """ Test get y scores with classifiers that have no scoring method """ oz = PrecisionRecallCurve(FakeClassifier()) - with pytest.raises(ModelError, match="requires .* predict_proba or decision_function"): + with pytest.raises( + ModelError, match="requires .* predict_proba or decision_function" + ): oz._get_y_scores(self.binary.X.train) + + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_custom_iso_f1_scores(self): + """ + Test using custom ISO F1 Values + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + vals = (0.1, 0.6, 0.3, 0.9, 0.9) + viz = PrecisionRecallCurve( + RandomForestClassifier(random_state=27), + iso_f1_curves=True, + iso_f1_values=vals, + ) + + X_train, X_test, y_train, y_test = tts( + X, y, test_size=0.2, shuffle=True, random_state=555 + ) + + assert viz.fit(X_train, y_train) is viz + viz.score(X_test, y_test) + viz.finalize() + + self.assert_images_similar(viz) + + def test_quick_method_with_test_set(self): + """ + Test quick method when both train and test data is supplied + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + X_train, X_test, y_train, y_test = tts( + X, y, test_size=0.2, shuffle=True, random_state=555 + ) + + viz = precision_recall_curve( + RandomForestClassifier(random_state=72), + X_train, + y_train, + X_test, + y_test, + random_state=7, + ) + self.assert_images_similar(viz) + + def test_missing_test_data_in_quick_method(self): + """ + Test quick method when test data is missing. + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + X_train, X_test, y_train, y_test = tts( + X, y, test_size=0.2, shuffle=True, random_state=55555 + ) + + emsg = "both X_test and y_test are required if one is specified" + + with pytest.raises(YellowbrickValueError, match=emsg): + precision_recall_curve( + RandomForestClassifier(), X_train, y_train, y_test=y_test + ) + + with pytest.raises(YellowbrickValueError, match=emsg): + precision_recall_curve(RandomForestClassifier(), X_train, y_train, X_test) diff --git a/tests/test_classifier/test_rocauc.py b/tests/test_classifier/test_rocauc.py index dccf2666a..24822080f 100644 --- a/tests/test_classifier/test_rocauc.py +++ b/tests/test_classifier/test_rocauc.py @@ -1,11 +1,11 @@ # tests.test_classifier.test_rocauc # Testing for the ROCAUC visualizer # -# Author: Benjamin Bengfort -# Author: Rebecca Bilbro +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Tue May 23 13:41:55 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_rocauc.py [] benjamin@bengfort.com $ @@ -18,46 +18,70 @@ ## Imports ########################################################################## -import os import pytest import numpy as np import numpy.testing as npt +from unittest.mock import patch from tests.base import VisualTestCase -from tests.dataset import DatasetMixin + from yellowbrick.classifier.rocauc import * +from yellowbrick.datasets import load_occupancy from yellowbrick.exceptions import ModelError, YellowbrickValueError from sklearn.svm import LinearSVC from sklearn.naive_bayes import GaussianNB from sklearn.tree import DecisionTreeClassifier -from sklearn.datasets import load_breast_cancer from sklearn.linear_model import LogisticRegression from sklearn.base import BaseEstimator, ClassifierMixin +from sklearn.model_selection import train_test_split as tts from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier +try: + import pandas as pd +except ImportError: + pd = None ########################################################################## ## Fixtures ########################################################################## -# Increased tolerance for AppVeyor tests -TOL = 10 if os.name == 'nt' else 0.1 - class FakeClassifier(BaseEstimator, ClassifierMixin): """ A fake classifier for testing noops on the visualizer. """ + pass +def assert_valid_rocauc_scores(visualizer, nscores=4): + """ + Assertion helper to ensure scores are correctly computed + """ + __tracebackhide__ = True + assert len(visualizer.fpr.keys()) == nscores + assert len(visualizer.tpr.keys()) == nscores + assert len(visualizer.roc_auc.keys()) == nscores + + for k in (0, 1, "micro", "macro"): + assert k in visualizer.fpr + assert k in visualizer.tpr + assert k in visualizer.roc_auc + assert len(visualizer.fpr[k]) == len(visualizer.tpr[k]) + assert 0.0 < visualizer.roc_auc[k] < 1.0 + + ########################################################################## ## Tests ########################################################################## + @pytest.mark.usefixtures("binary", "multiclass") -class ROCAUCTests(VisualTestCase, DatasetMixin): +class TestROCAUC(VisualTestCase): + """ + Test ROCAUC visualizer + """ def test_binary_probability(self): """ @@ -74,21 +98,11 @@ def test_binary_probability(self): assert 0 <= s <= 1 # Check the scores - self.assertEqual(len(visualizer.fpr.keys()), 4) - self.assertEqual(len(visualizer.tpr.keys()), 4) - self.assertEqual(len(visualizer.roc_auc.keys()), 4) - - for k in (0, 1, "micro", "macro"): - self.assertIn(k, visualizer.fpr) - self.assertIn(k, visualizer.tpr) - self.assertIn(k, visualizer.roc_auc) - self.assertEqual(len(visualizer.fpr[k]), len(visualizer.tpr[k])) - self.assertGreater(visualizer.roc_auc[k], 0.0) - self.assertLess(visualizer.roc_auc[k], 1.0) + assert_valid_rocauc_scores(visualizer) # Compare the images - visualizer.poof() - self.assert_images_similar(visualizer, tol=TOL) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.1, windows_tol=10) def test_binary_probability_decision(self): """ @@ -105,28 +119,20 @@ def test_binary_probability_decision(self): assert 0 <= s <= 1 # Check the scores - self.assertEqual(len(visualizer.fpr.keys()), 4) - self.assertEqual(len(visualizer.tpr.keys()), 4) - self.assertEqual(len(visualizer.roc_auc.keys()), 4) - - for k in (0, 1, "micro", "macro"): - self.assertIn(k, visualizer.fpr) - self.assertIn(k, visualizer.tpr) - self.assertIn(k, visualizer.roc_auc) - self.assertEqual(len(visualizer.fpr[k]), len(visualizer.tpr[k])) - self.assertGreater(visualizer.roc_auc[k], 0.0) - self.assertLess(visualizer.roc_auc[k], 1.0) + assert_valid_rocauc_scores(visualizer) # Compare the images - visualizer.poof() - self.assert_images_similar(visualizer, tol=TOL) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.1, windows_tol=10) def test_binary_decision(self): """ Test ROCAUC with a binary classifier with a decision_function """ # Create and fit the visualizer - visualizer = ROCAUC(LinearSVC(random_state=42), micro=False, macro=False, per_class=False) + visualizer = ROCAUC( + LinearSVC(random_state=42), micro=False, macro=False, per_class=False + ) visualizer.fit(self.binary.X.train, self.binary.y.train) # Score the visualizer @@ -136,13 +142,13 @@ def test_binary_decision(self): assert 0 <= s <= 1 # Check the scores - self.assertEqual(len(visualizer.fpr.keys()), 1) - self.assertEqual(len(visualizer.tpr.keys()), 1) - self.assertEqual(len(visualizer.roc_auc.keys()), 1) + assert len(visualizer.fpr.keys()) == 1 + assert len(visualizer.tpr.keys()) == 1 + assert len(visualizer.roc_auc.keys()) == 1 # Compare the images # NOTE: increased tolerance for both AppVeyor and Travis CI tests - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=10) def test_binary_micro_error(self): @@ -154,7 +160,7 @@ def test_binary_micro_error(self): visualizer.fit(self.binary.X.train, self.binary.y.train) # Ensure score raises error (micro curves aren't defined for binary decisions) - with self.assertRaises(ModelError): + with pytest.raises(ModelError): visualizer.score(self.binary.X.test, self.binary.y.test) def test_binary_macro_error(self): @@ -166,7 +172,7 @@ def test_binary_macro_error(self): visualizer.fit(self.binary.X.train, self.binary.y.train) # Ensure score raises error (macro curves aren't defined for binary decisions) - with self.assertRaises(ModelError): + with pytest.raises(ModelError): visualizer.score(self.binary.X.test, self.binary.y.test) def test_binary_per_class_error(self): @@ -178,7 +184,7 @@ def test_binary_per_class_error(self): visualizer.fit(self.binary.X.train, self.binary.y.train) # Ensure score raises error (per_class curves not defined for binary decisions) - with self.assertRaises(ModelError): + with pytest.raises(ModelError): visualizer.score(self.binary.X.test, self.binary.y.test) def test_multiclass_rocauc(self): @@ -196,31 +202,40 @@ def test_multiclass_rocauc(self): assert 0 <= s <= 1 # Check the scores - self.assertEqual(len(visualizer.fpr.keys()), 8) - self.assertEqual(len(visualizer.tpr.keys()), 8) - self.assertEqual(len(visualizer.roc_auc.keys()), 8) - - for k in (0, 1, "micro", "macro"): - self.assertIn(k, visualizer.fpr) - self.assertIn(k, visualizer.tpr) - self.assertIn(k, visualizer.roc_auc) - self.assertEqual(len(visualizer.fpr[k]), len(visualizer.tpr[k])) - self.assertGreater(visualizer.roc_auc[k], 0.0) - self.assertLess(visualizer.roc_auc[k], 1.0) + assert_valid_rocauc_scores(visualizer, nscores=8) # Compare the images - visualizer.poof() - self.assert_images_similar(visualizer, tol=TOL) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.1, windows_tol=10) def test_rocauc_quickmethod(self): """ Test the ROCAUC quick method """ - data = load_breast_cancer() + X, y = load_occupancy(return_dataset=True).to_numpy() model = DecisionTreeClassifier() # TODO: image comparison of the quick method - roc_auc(model, data.data, data.target) + roc_auc(model, X, y) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_pandas_integration(self): + """ + Test the ROCAUC with Pandas dataframe + """ + X, y = load_occupancy(return_dataset=True).to_pandas() + + # Create train/test splits + splits = tts(X, y, test_size=0.2, random_state=4512) + X_train, X_test, y_train, y_test = splits + + visualizer = ROCAUC(GaussianNB()) + visualizer.fit(X_train, y_train) + visualizer.score(X_test, y_test) + + # Compare the images + visualizer.finalize() + self.assert_images_similar(visualizer) def test_rocauc_no_micro(self): """ @@ -232,16 +247,16 @@ def test_rocauc_no_micro(self): # Score the visualizer (should be the macro average) s = visualizer.score(self.binary.X.test, self.binary.y.test) - self.assertAlmostEqual(s, 0.8) + assert s == pytest.approx(0.8661, abs=1e-4) # Assert that there is no micro score - self.assertNotIn("micro", visualizer.fpr) - self.assertNotIn("micro", visualizer.tpr) - self.assertNotIn("micro", visualizer.roc_auc) + assert "micro" not in visualizer.fpr + assert "micro" not in visualizer.tpr + assert "micro" not in visualizer.roc_auc # Compare the images - visualizer.poof() - self.assert_images_similar(visualizer, tol=TOL) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.1, windows_tol=10) def test_rocauc_no_macro(self): """ @@ -253,16 +268,16 @@ def test_rocauc_no_macro(self): # Score the visualizer (should be the micro average) s = visualizer.score(self.binary.X.test, self.binary.y.test) - self.assertAlmostEqual(s, 0.8) + assert s == pytest.approx(0.8573, abs=1e-4) # Assert that there is no macro score - self.assertNotIn("macro", visualizer.fpr) - self.assertNotIn("macro", visualizer.tpr) - self.assertNotIn("macro", visualizer.roc_auc) + assert "macro" not in visualizer.fpr + assert "macro" not in visualizer.tpr + assert "macro" not in visualizer.roc_auc # Compare the images - visualizer.poof() - self.assert_images_similar(visualizer, tol=TOL) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.1, windows_tol=10) def test_rocauc_no_macro_no_micro(self): """ @@ -274,21 +289,21 @@ def test_rocauc_no_macro_no_micro(self): # Score the visualizer (should be the F1 score) s = visualizer.score(self.binary.X.test, self.binary.y.test) - self.assertAlmostEqual(s, 0.8) + assert s == pytest.approx(0.8) # Assert that there is no macro score - self.assertNotIn("macro", visualizer.fpr) - self.assertNotIn("macro", visualizer.tpr) - self.assertNotIn("macro", visualizer.roc_auc) + assert "macro" not in visualizer.fpr + assert "macro" not in visualizer.tpr + assert "macro" not in visualizer.roc_auc # Assert that there is no micro score - self.assertNotIn("micro", visualizer.fpr) - self.assertNotIn("micro", visualizer.tpr) - self.assertNotIn("micro", visualizer.roc_auc) + assert "micro" not in visualizer.fpr + assert "micro" not in visualizer.tpr + assert "micro" not in visualizer.roc_auc # Compare the images - visualizer.poof() - self.assert_images_similar(visualizer, tol=TOL) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.1, windows_tol=10) def test_rocauc_no_classes(self): """ @@ -300,24 +315,26 @@ def test_rocauc_no_classes(self): # Score the visualizer (should be the micro average) s = visualizer.score(self.binary.X.test, self.binary.y.test) - self.assertAlmostEqual(s, 0.8) + assert s == pytest.approx(0.8661, abs=1e-4) # Assert that there still are per-class scores for c in (0, 1): - self.assertIn(c, visualizer.fpr) - self.assertIn(c, visualizer.tpr) - self.assertIn(c, visualizer.roc_auc) + assert c in visualizer.fpr + assert c in visualizer.tpr + assert c in visualizer.roc_auc # Compare the images - visualizer.poof() - self.assert_images_similar(visualizer, tol=TOL) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.1, windows_tol=10) def test_rocauc_no_curves(self): """ Test ROCAUC with no curves specified at all """ # Create and fit the visualizer - visualizer = ROCAUC(LogisticRegression(), per_class=False, macro=False, micro=False) + visualizer = ROCAUC( + LogisticRegression(), per_class=False, macro=False, micro=False + ) visualizer.fit(self.binary.X.train, self.binary.y.train) # Attempt to score the visualizer @@ -328,7 +345,7 @@ def test_rocauc_label_encoded(self): """ Test ROCAUC with a target specifying a list of classes as strings """ - class_labels = ['a', 'b', 'c', 'd', 'e', 'f'] + class_labels = ["a", "b", "c", "d", "e", "f"] # Create and fit the visualizer visualizer = ROCAUC(LogisticRegression(), classes=class_labels) @@ -336,14 +353,14 @@ def test_rocauc_label_encoded(self): # Score the visualizer visualizer.score(self.multiclass.X.test, self.multiclass.y.test) - self.assertEqual(list(visualizer.classes_), class_labels) + assert list(visualizer.classes_) == class_labels def test_rocauc_not_label_encoded(self): """ Test ROCAUC with a target whose classes are unencoded strings before scoring """ # Map numeric targets to strings - classes = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f'} + classes = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f"} y_train = np.array([classes[yi] for yi in self.multiclass.y.train]) y_test = np.array([classes[yi] for yi in self.multiclass.y.test]) @@ -352,7 +369,7 @@ def test_rocauc_not_label_encoded(self): visualizer.fit(self.multiclass.X.train, y_train) # Confirm that y_train and y_test have the same targets before calling score - self.assertEqual(set(y_train), set(y_test)) + assert set(y_train) == set(y_test) def test_binary_decision_function_rocauc(self): """ @@ -360,7 +377,7 @@ def test_binary_decision_function_rocauc(self): """ # Load the model and assert there is no predict_proba method. model = LinearSVC() - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): model.predict_proba # Fit model and visualizer @@ -368,9 +385,9 @@ def test_binary_decision_function_rocauc(self): visualizer.fit(self.binary.X.train, self.binary.y.train) # First 10 expected values in the y_scores - first_ten_expected = np.asarray([ - -0.092, 0.019, -0.751, -0.838, 0.183, -0.344, -1.019, 2.203, 1.415, -0.529 - ]) + first_ten_expected = np.asarray( + [-0.092, 0.019, -0.751, -0.838, 0.183, -0.344, -1.019, 2.203, 1.415, -0.529] + ) # Get the predict_proba scores and evaluate y_scores = visualizer._get_y_scores(self.binary.X.train) @@ -384,7 +401,7 @@ def test_multi_decision_function_rocauc(self): """ # Load the model and assert there is no predict_proba method. model = LinearSVC() - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): model.predict_proba # Fit model and visualizer @@ -393,11 +410,11 @@ def test_multi_decision_function_rocauc(self): # First 5 expected arrays in the y_scores first_five_expected = [ - [-0.370, -0.543, -1.059, -0.466, -0.743, -1.156], - [-0.445, -0.693, -0.362, -1.002, -0.815, -0.878], - [-1.058, -0.808, -0.291, -0.767, -0.651, -0.586], - [-0.446, -1.255, -0.489, -0.961, -0.807, -0.126], - [-1.066, -0.493, -0.639, -0.442, -0.639, -1.017] + [-0.370, -0.543, -1.059, -0.466, -0.743, -1.156], + [-0.445, -0.693, -0.362, -1.002, -0.815, -0.878], + [-1.058, -0.808, -0.291, -0.767, -0.651, -0.586], + [-0.446, -1.255, -0.489, -0.961, -0.807, -0.126], + [-1.066, -0.493, -0.639, -0.442, -0.639, -1.017], ] # Get the predict_proba scores and evaluate @@ -412,7 +429,7 @@ def test_predict_proba_rocauc(self): """ # Load the model and assert there is no decision_function method. model = GaussianNB() - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): model.decision_function # Fit model and visualizer @@ -420,18 +437,20 @@ def test_predict_proba_rocauc(self): visualizer.fit(self.binary.X.train, self.binary.y.train) # First 10 expected arrays in the y_scores - first_ten_expected = np.asarray([ - [0.595, 0.405], - [0.161, 0.839], - [0.990, 0.010], - [0.833, 0.167], - [0.766, 0.234], - [0.996, 0.004], - [0.592, 0.408], - [0.007, 0.993], - [0.035, 0.965], - [0.764, 0.236] - ]) + first_ten_expected = np.asarray( + [ + [0.595, 0.405], + [0.161, 0.839], + [0.990, 0.010], + [0.833, 0.167], + [0.766, 0.234], + [0.996, 0.004], + [0.592, 0.408], + [0.007, 0.993], + [0.035, 0.965], + [0.764, 0.236], + ] + ) # Get the predict_proba scores and evaluate y_scores = visualizer._get_y_scores(self.binary.X.train) @@ -444,5 +463,29 @@ def test_no_scoring_function(self): Test ROCAUC with classifiers that have no scoring method """ visualizer = ROCAUC(FakeClassifier()) - with self.assertRaises(ModelError): + with pytest.raises(ModelError): visualizer._get_y_scores(self.binary.X.train) + + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_occupancy(return_dataset=True).to_numpy() + + model = GaussianNB().fit(X, y) + classes = ["unoccupied", "occupied"] + + with patch.object(model, "fit") as mockfit: + oz = ROCAUC(model, classes=classes) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ROCAUC(model, classes=classes, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with patch.object(model, "fit") as mockfit: + oz = ROCAUC(model, classes=classes, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) diff --git a/tests/test_classifier/test_threshold.py b/tests/test_classifier/test_threshold.py index f39d8b44e..810d3ea07 100644 --- a/tests/test_classifier/test_threshold.py +++ b/tests/test_classifier/test_threshold.py @@ -1,11 +1,11 @@ # tests.test_classifier.test_threshold # Ensure that the discrimination threshold visualizations work. # -# Author: Nathan Danielsen -# Author: Benjamin Bengfort +# Author: Nathan Danielsen +# Author: Benjamin Bengfort # Created: Wed April 26 20:17:29 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_threshold.py [] nathan.danielsen@gmail.com $ @@ -19,16 +19,16 @@ ########################################################################## import sys -import six import pytest import yellowbrick as yb import matplotlib.pyplot as plt from yellowbrick.classifier.threshold import * +from yellowbrick.datasets import load_occupancy from yellowbrick.utils import is_probabilistic, is_classifier +from unittest.mock import patch from tests.base import VisualTestCase -from tests.dataset import DatasetMixin from numpy.testing.utils import assert_array_equal from sklearn.svm import LinearSVC, NuSVC @@ -44,31 +44,30 @@ except ImportError: pd = None -try: - from unittest.mock import patch -except ImportError: - from mock import patch - ########################################################################## ## DiscriminationThreshold Test Cases ########################################################################## -class TestDiscriminationThreshold(VisualTestCase, DatasetMixin): + +class TestDiscriminationThreshold(VisualTestCase): """ DiscriminationThreshold visualizer tests """ - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_binary_discrimination_threshold(self): """ Correctly generates viz for binary classification with BernoulliNB """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=854 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=854, ) _, ax = plt.subplots() @@ -77,7 +76,7 @@ def test_binary_discrimination_threshold(self): visualizer = DiscriminationThreshold(model, ax=ax, random_state=23) visualizer.fit(X, y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) @@ -86,8 +85,13 @@ def test_multiclass_discrimination_threshold(self): Assert exception is raised in multiclass case. """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=3, n_clusters_per_class=4, random_state=854 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=3, + n_clusters_per_class=4, + random_state=854, ) visualizer = DiscriminationThreshold(GaussianNB(), random_state=23) @@ -96,9 +100,7 @@ def test_multiclass_discrimination_threshold(self): with pytest.raises(ValueError, match=msg): visualizer.fit(X, y) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") @pytest.mark.skipif(pd is None, reason="test requires pandas") def test_pandas_integration(self): """ @@ -107,24 +109,39 @@ def test_pandas_integration(self): _, ax = plt.subplots() # Load the occupancy dataset from fixtures - data = self.load_data('occupancy') - target = 'occupancy' - features = [ - "temperature", "relative_humidity", "light", "C02", "humidity" - ] + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + + classes = ["unoccupied", "occupied"] + + # Create the visualizer + viz = DiscriminationThreshold( + LogisticRegression(), ax=ax, classes=classes, random_state=193 + ) + viz.fit(X, y) + viz.finalize() - # Create instances and target - X = pd.DataFrame(data[features]) - y = pd.Series(data[target].astype(int)) + self.assert_images_similar(viz, tol=0.1) + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_numpy_integration(self): + """ + Test with NumPy arrays + """ + _, ax = plt.subplots() + + # Load the occupancy dataset from fixtures + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() - classes = ['unoccupied', 'occupied'] + classes = ["unoccupied", "occupied"] # Create the visualizer viz = DiscriminationThreshold( LogisticRegression(), ax=ax, classes=classes, random_state=193 ) viz.fit(X, y) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=0.1) @@ -134,8 +151,13 @@ def test_quick_method(self): """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=2721 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=2721, ) _, ax = plt.subplots() @@ -143,14 +165,19 @@ def test_quick_method(self): discrimination_threshold(BernoulliNB(3), X, y, ax=ax, random_state=5) self.assert_images_similar(ax=ax, tol=10) - @patch.object(DiscriminationThreshold, 'draw', autospec=True) + @patch.object(DiscriminationThreshold, "draw", autospec=True) def test_fit(self, mock_draw): """ Test the fit method generates scores, calls draw, and returns self """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=1221 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=1221, ) visualizer = DiscriminationThreshold(BernoulliNB()) @@ -160,8 +187,7 @@ def test_fit(self, mock_draw): out = visualizer.fit(X, y) assert out is visualizer - if six.PY3: - mock_draw.assert_called_once() + mock_draw.assert_called_once() assert hasattr(visualizer, "thresholds_") assert hasattr(visualizer, "cv_scores_") @@ -170,17 +196,21 @@ def test_fit(self, mock_draw): assert "{}_lower".format(metric) in visualizer.cv_scores_ assert "{}_upper".format(metric) in visualizer.cv_scores_ - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_binary_discrimination_threshold_alt_args(self): """ Correctly generates visualization with alternate arguments """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=10, n_redundant=3, - n_classes=2, n_clusters_per_class=4, random_state=1231, - flip_y=0.1, weights=[0.35, 0.65], + n_samples=400, + n_features=20, + n_informative=10, + n_redundant=3, + n_classes=2, + n_clusters_per_class=4, + random_state=1231, + flip_y=0.1, + weights=[0.35, 0.65], ) exclude = ["queue_rate", "fscore"] @@ -190,7 +220,7 @@ def test_binary_discrimination_threshold_alt_args(self): ) visualizer.fit(X, y) - visualizer.poof() + visualizer.finalize() for metric in exclude: assert metric not in visualizer.cv_scores_ diff --git a/tests/test_cluster/__init__.py b/tests/test_cluster/__init__.py index 2ff63587b..f2cf1adb7 100644 --- a/tests/test_cluster/__init__.py +++ b/tests/test_cluster/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Thu Mar 23 17:37:57 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [241edca] benjamin@bengfort.com $ diff --git a/tests/test_cluster/test_base.py b/tests/test_cluster/test_base.py index 66d91ada8..41db7d652 100644 --- a/tests/test_cluster/test_base.py +++ b/tests/test_cluster/test_base.py @@ -1,10 +1,10 @@ # tests.test_cluster.test_base # Test the cluster base visualizers. # -# Author: Rebecca Bilbro +# Author: Rebecca Bilbro # Created: Thu Mar 23 17:38:42 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_base.py [241edca] benjamin@bengfort.com $ @@ -17,41 +17,55 @@ ## Imports ########################################################################## -import unittest +import pytest from yellowbrick.exceptions import YellowbrickTypeError from yellowbrick.cluster.base import ClusteringScoreVisualizer from sklearn.svm import SVC, SVR -from sklearn.linear_model import Ridge, RidgeCV, LinearRegression from sklearn.ensemble import RandomForestClassifier -from sklearn.cluster import KMeans, MiniBatchKMeans, AffinityPropagation from sklearn.cluster import MeanShift, DBSCAN, Birch +from sklearn.linear_model import Ridge, RidgeCV, LinearRegression +from sklearn.cluster import KMeans, MiniBatchKMeans, AffinityPropagation + ########################################################################## ## Clustering Base Test Cases ########################################################################## -class ClusterBaseTests(unittest.TestCase): - def test_clusterer_enforcement(self): +class TestClusterBase(object): + @pytest.mark.parametrize( + "model", [SVC, SVR, Ridge, RidgeCV, LinearRegression, RandomForestClassifier] + ) + def test_clusterer_enforcement_raises(self, model): + """ + Assert that non-cluster models raise a TypeError for cluster visualizers + """ + with pytest.raises(YellowbrickTypeError): + ClusteringScoreVisualizer(model()) + + @pytest.mark.parametrize( + "model", + [KMeans, MiniBatchKMeans, AffinityPropagation, MeanShift, DBSCAN, Birch], + ) + def test_clusterer_enforcement(self, model): """ Assert that only clustering estimators can be passed to cluster viz """ - nomodels = [ - SVC, SVR, Ridge, RidgeCV, LinearRegression, RandomForestClassifier - ] - - for nomodel in nomodels: - with self.assertRaises(YellowbrickTypeError): - ClusteringScoreVisualizer(nomodel()) - - models = [ - KMeans, MiniBatchKMeans, AffinityPropagation, MeanShift, DBSCAN, Birch - ] - - for model in models: - try: - ClusteringScoreVisualizer(model()) - except YellowbrickTypeError: - self.fail("could not pass clustering estimator to visualizer") + try: + ClusteringScoreVisualizer(model()) + except YellowbrickTypeError: + self.fail("could not pass clustering estimator to visualizer") + + def test_force_estimator(self): + """ + Test that an estimator can be forced through + """ + with pytest.raises(YellowbrickTypeError): + ClusteringScoreVisualizer(LinearRegression()) + + try: + ClusteringScoreVisualizer(LinearRegression(), force_model=True) + except YellowbrickTypeError as e: + pytest.fail("type error was raised incorrectly: {}".format(e)) diff --git a/tests/test_cluster/test_elbow.py b/tests/test_cluster/test_elbow.py index 3bd16793e..81f3579ca 100644 --- a/tests/test_cluster/test_elbow.py +++ b/tests/test_cluster/test_elbow.py @@ -1,10 +1,10 @@ # tests.test_cluster.test_elbow # Tests for the KElbowVisualizer # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu Mar 23 22:30:19 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_elbow.py [5a370c8] benjamin@bengfort.com $ @@ -22,9 +22,6 @@ import numpy as np import matplotlib.pyplot as plt -from ..base import VisualTestCase -from ..dataset import DatasetMixin - from scipy.sparse import csc_matrix, csr_matrix from numpy.testing.utils import assert_array_almost_equal @@ -32,44 +29,62 @@ from sklearn.cluster import KMeans, MiniBatchKMeans from sklearn.feature_extraction.text import TfidfVectorizer +from tests.fixtures import Dataset +from tests.base import VisualTestCase +from yellowbrick.datasets import load_hobbies from yellowbrick.cluster.elbow import distortion_score -from yellowbrick.cluster.elbow import KElbowVisualizer -from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.cluster.elbow import KElbowVisualizer, kelbow_visualizer +from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning + +from tests.base import IS_WINDOWS_OR_CONDA try: import pandas as pd except ImportError: pd = None +########################################################################## +## Data +########################################################################## + + +@pytest.fixture(scope="class") +def clusters(request): + # TODO: replace with make_blobs + X = np.array( + [ + [-0.40020753, -4.67055317, -0.27191127, -1.49156318], + [0.37143349, -4.89391622, -1.23893945, 0.48318165], + [8.625142, -1.2372284, 1.39301471, 4.3394457], + [7.65803596, -2.21017215, 1.99175714, 3.71004654], + [0.89319875, -5.37152317, 1.50313598, 1.95284886], + [2.68362166, -5.78810913, -0.41233406, 1.94638989], + [7.63541182, -1.99606076, 0.9241231, 4.53478238], + [9.04699415, -0.74540679, 0.98042851, 5.99569071], + [1.02552122, -5.73874278, -1.74804915, -0.07831216], + [7.18135665, -3.49473178, 1.14300963, 4.46065816], + [0.58812902, -4.66559815, -0.72831685, 1.40171779], + [1.48620862, -5.9963108, 0.19145963, -1.11369256], + [7.6625556, -1.21328083, 2.06361094, 6.2643551], + [9.45050727, -1.36536078, 1.31154384, 3.89103468], + [6.88203724, -1.62040255, 3.89961049, 2.12865388], + [5.60842705, -2.10693356, 1.93328514, 3.90825432], + [2.35150936, -6.62836131, -1.84278374, 0.51540886], + [1.17446451, -5.62506058, -2.18420699, 1.21385128], + ] + ) + + y = np.array([0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0]) + + request.cls.clusters = Dataset(X, y) + ########################################################################## ## K-Elbow Helpers Test Cases ########################################################################## -X = np.array( - [[-0.40020753, -4.67055317, -0.27191127, -1.49156318], - [ 0.37143349, -4.89391622, -1.23893945, 0.48318165], - [ 8.625142 , -1.2372284 , 1.39301471, 4.3394457 ], - [ 7.65803596, -2.21017215, 1.99175714, 3.71004654], - [ 0.89319875, -5.37152317, 1.50313598, 1.95284886], - [ 2.68362166, -5.78810913, -0.41233406, 1.94638989], - [ 7.63541182, -1.99606076, 0.9241231 , 4.53478238], - [ 9.04699415, -0.74540679, 0.98042851, 5.99569071], - [ 1.02552122, -5.73874278, -1.74804915, -0.07831216], - [ 7.18135665, -3.49473178, 1.14300963, 4.46065816], - [ 0.58812902, -4.66559815, -0.72831685, 1.40171779], - [ 1.48620862, -5.9963108 , 0.19145963, -1.11369256], - [ 7.6625556 , -1.21328083, 2.06361094, 6.2643551 ], - [ 9.45050727, -1.36536078, 1.31154384, 3.89103468], - [ 6.88203724, -1.62040255, 3.89961049, 2.12865388], - [ 5.60842705, -2.10693356, 1.93328514, 3.90825432], - [ 2.35150936, -6.62836131, -1.84278374, 0.51540886], - [ 1.17446451, -5.62506058, -2.18420699, 1.21385128]] -) - -y = np.array([0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0]) - +@pytest.mark.usefixtures("clusters") class TestKElbowHelper(object): """ Helper functions for K-Elbow Visualizer @@ -79,36 +94,36 @@ def test_distortion_score(self): """ Test the distortion score metric function """ - score = distortion_score(X, y) - assert score == 7.6777850157143783 + score = distortion_score(self.clusters.X, self.clusters.y) + assert score == pytest.approx(69.10006514142941) - @pytest.mark.parametrize("Xs", [ - csc_matrix(X), csr_matrix(X), - ], ids=["csc", "csr"]) - def test_distortion_score_sparse_matrix_input(self, Xs): + @pytest.mark.parametrize("func", [csc_matrix, csr_matrix], ids=["csc", "csr"]) + def test_distortion_score_sparse_matrix_input(self, func): """ Test the distortion score metric on a sparse array """ - score = distortion_score(Xs, y) - assert score == pytest.approx(7.6777850157143783) + score = distortion_score(func(self.clusters.X), self.clusters.y) + assert score == pytest.approx(69.10006514142938) @pytest.mark.skipif(pd is None, reason="pandas is required") def test_distortion_score_pandas_input(self): """ Test the distortion score metric on pandas DataFrame and Series """ - df = pd.DataFrame(X) - s = pd.Series(y) + df = pd.DataFrame(self.clusters.X) + s = pd.Series(self.clusters.y) score = distortion_score(df, s) - assert score == pytest.approx(7.6777850157143783) + assert score == pytest.approx(69.10006514142941) ########################################################################## ## KElbowVisualizer Test Cases ########################################################################## -class TestKElbowVisualizer(VisualTestCase, DatasetMixin): + +@pytest.mark.usefixtures("clusters") +class TestKElbowVisualizer(VisualTestCase): """ K-Elbow Visualizer Tests """ @@ -121,9 +136,8 @@ def test_integrated_kmeans_elbow(self): # NOTE #182: cannot use occupancy dataset because of memory usage # Generate a blobs data set - X,y = make_blobs( - n_samples=1000, n_features=12, centers=6, - shuffle=True, random_state=42 + X, y = make_blobs( + n_samples=1000, n_features=12, centers=6, shuffle=True, random_state=42 ) try: @@ -131,7 +145,7 @@ def test_integrated_kmeans_elbow(self): visualizer = KElbowVisualizer(KMeans(random_state=42), k=4, ax=ax) visualizer.fit(X) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) except Exception as e: @@ -145,18 +159,16 @@ def test_integrated_mini_batch_kmeans_elbow(self): # NOTE #182: cannot use occupancy dataset because of memory usage # Generate a blobs data set - X,y = make_blobs( + X, y = make_blobs( n_samples=1000, n_features=12, centers=6, shuffle=True, random_state=42 ) try: _, ax = plt.subplots() - visualizer = KElbowVisualizer( - MiniBatchKMeans(random_state=42), k=4, ax=ax - ) + visualizer = KElbowVisualizer(MiniBatchKMeans(random_state=42), k=4, ax=ax) visualizer.fit(X) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) except Exception as e: @@ -167,14 +179,14 @@ def test_topic_modeling_k_means(self): """ Test topic modeling k-means on the hobbies corpus """ - corpus = self.load_corpus("hobbies") + corpus = load_hobbies() - tfidf = TfidfVectorizer() - docs = tfidf.fit_transform(corpus.data) + tfidf = TfidfVectorizer() + docs = tfidf.fit_transform(corpus.data) visualizer = KElbowVisualizer(KMeans(), k=(4, 8)) visualizer.fit(docs) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) @@ -184,20 +196,21 @@ def test_invalid_k(self): """ with pytest.raises(YellowbrickValueError): - KElbowVisualizer(KMeans(), k=(1, 2, 3, 'foo', 5)) + KElbowVisualizer(KMeans(), k=(1, 2, 3, "foo", 5)) with pytest.raises(YellowbrickValueError): KElbowVisualizer(KMeans(), k="foo") def test_valid_k(self): """ - Assert that valid values of K generate correct k_values_: - if k is an int, k_values_ = range(2, k+1) - if k is a tuple of 2 ints, k_values = range(k[0], k[1]) - if k is an iterable, k_values_ = list(k) + Assert that valid values of K generate correct k_values_ """ + # if k is an int, k_values_ = range(2, k+1) + # if k is a tuple of 2 ints, k_values = range(k[0], k[1]) + # if k is an iterable, k_values_ = list(k) + visualizer = KElbowVisualizer(KMeans(), k=8) - assert visualizer.k_values_ == list(np.arange(2, 8+1)) + assert visualizer.k_values_ == list(np.arange(2, 8 + 1)) visualizer = KElbowVisualizer(KMeans(), k=(4, 12)) assert visualizer.k_values_ == list(np.arange(4, 12)) @@ -205,72 +218,117 @@ def test_valid_k(self): visualizer = KElbowVisualizer(KMeans(), k=np.arange(10, 100, 10)) assert visualizer.k_values_ == list(np.arange(10, 100, 10)) - visualizer = KElbowVisualizer(KMeans(), - k=[10, 20, 30, 40, 50, 60, 70, 80, 90]) + visualizer = KElbowVisualizer(KMeans(), k=[10, 20, 30, 40, 50, 60, 70, 80, 90]) assert visualizer.k_values_ == list(np.arange(10, 100, 10)) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_distortion_metric(self): """ Test the distortion metric of the k-elbow visualizer """ visualizer = KElbowVisualizer( - KMeans(random_state=0), k=5, metric="distortion", timings=False + KMeans(random_state=0), + k=5, + metric="distortion", + timings=False, + locate_elbow=False, ) - visualizer.fit(X) + visualizer.fit(self.clusters.X) - expected = np.array([ 7.677785, 8.364319, 8.893634, 8.013021]) + expected = np.array([69.100065, 54.081571, 43.146921, 34.978487]) assert len(visualizer.k_scores_) == 4 - visualizer.poof() - self.assert_images_similar(visualizer) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.03) assert_array_almost_equal(visualizer.k_scores_, expected) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_silhouette_metric(self): """ Test the silhouette metric of the k-elbow visualizer """ visualizer = KElbowVisualizer( - KMeans(random_state=0), k=5, metric="silhouette", timings=False + KMeans(random_state=0), + k=5, + metric="silhouette", + timings=False, + locate_elbow=False, ) - visualizer.fit(X) + visualizer.fit(self.clusters.X) - expected = np.array([ 0.691636, 0.456646, 0.255174, 0.239842]) + expected = np.array([0.691636, 0.456646, 0.255174, 0.239842]) assert len(visualizer.k_scores_) == 4 - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) assert_array_almost_equal(visualizer.k_scores_, expected) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) - def test_calinski_harabaz_metric(self): + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_calinski_harabasz_metric(self): """ - Test the calinski-harabaz metric of the k-elbow visualizer + Test the calinski-harabasz metric of the k-elbow visualizer """ visualizer = KElbowVisualizer( - KMeans(random_state=0), k=5, - metric="calinski_harabaz", timings=False + KMeans(random_state=0), + k=5, + metric="calinski_harabasz", + timings=False, + locate_elbow=False, ) - visualizer.fit(X) + visualizer.fit(self.clusters.X) assert len(visualizer.k_scores_) == 4 + assert visualizer.elbow_value_ is None - expected = np.array([ - 81.662726256035683, 50.992378259195554, - 40.952179227847012, 35.939494 - ]) - + expected = np.array( + [81.662726256035683, 50.992378259195554, 40.952179227847012, 35.939494] + ) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) assert_array_almost_equal(visualizer.k_scores_, expected) + def test_locate_elbow(self): + """ + Test the addition of locate_elbow to an image + """ + X, y = make_blobs( + n_samples=1000, n_features=5, centers=3, shuffle=True, random_state=42 + ) + + visualizer = KElbowVisualizer( + KMeans(random_state=0), + k=6, + metric="calinski_harabasz", + timings=False, + locate_elbow=True, + ) + visualizer.fit(X) + assert len(visualizer.k_scores_) == 5 + assert visualizer.elbow_value_ == 3 + expected = np.array( + [4286.479848, 12463.383743, 8766.999551, 6950.08391, 5865.79722] + ) + + visualizer.finalize() + self.assert_images_similar(visualizer, windows_tol=2.2) + assert_array_almost_equal(visualizer.k_scores_, expected) + + def test_no_knee(self): + """ + Assert that a warning is issued if there is no knee detected + """ + X, y = make_blobs(n_samples=1000, centers=3, n_features=12, random_state=12) + message = ( + "No 'knee' or 'elbow point' detected " + "This could be due to bad clustering, no " + "actual clusters being formed etc." + ) + with pytest.warns(YellowbrickWarning, match=message): + visualizer = KElbowVisualizer( + KMeans(random_state=12), k=(4, 12), locate_elbow=True + ) + visualizer.fit(X) + def test_bad_metric(self): """ Assert KElbow raises an exception when a bad metric is supplied @@ -279,16 +337,17 @@ def test_bad_metric(self): KElbowVisualizer(KMeans(), k=5, metric="foo") @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) def test_timings(self): """ Test the twinx double axes with k-elbow timings """ visualizer = KElbowVisualizer( - KMeans(random_state=0), k=5, timings=True + KMeans(random_state=0), k=5, timings=True, locate_elbow=False ) - visualizer.fit(X) + visualizer.fit(self.clusters.X) # Check that we kept track of time assert len(visualizer.k_timers_) == 4 @@ -302,13 +361,30 @@ def test_timings(self): # overwrite k_timers_, k_values_ for image similarity Tests visualizer.axes[1].remove() visualizer.k_timers_ = [ - 0.01084589958190918, 0.011144161224365234, - 0.017028093338012695, 0.010634183883666992 + 0.01084589958190918, + 0.011144161224365234, + 0.017028093338012695, + 0.010634183883666992, ] visualizer.k_values_ = [2, 3, 4, 5] # call draw again which is normally called in fit visualizer.draw() - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) + + @pytest.mark.xfail(reason="images not close due to timing lines") + def test_quick_method(self): + """ + Test the quick method producing a valid visualization + """ + X, y = make_blobs( + n_samples=1000, n_features=12, centers=8, shuffle=False, random_state=2 + ) + + model = MiniBatchKMeans(3, random_state=43) + oz = kelbow_visualizer(model, X, random_state=13, legend=False) + assert isinstance(oz, KElbowVisualizer) + + self.assert_images_similar(oz) diff --git a/tests/test_cluster/test_icdm.py b/tests/test_cluster/test_icdm.py index 9228b66f5..c6ac439da 100644 --- a/tests/test_cluster/test_icdm.py +++ b/tests/test_cluster/test_icdm.py @@ -1,10 +1,13 @@ # tests.test_cluster.test_icdm # Tests for the intercluster distance map visualizer. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Aug 21 11:57:44 2018 -0400 # -# ID: test_icdm.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_icdm.py [2f23976] benjamin@bengfort.com $ """ Tests for the intercluster distance map visualizer. @@ -14,36 +17,36 @@ ## Imports ########################################################################## -import sys import pytest import matplotlib as mpl from yellowbrick.cluster.icdm import * +from yellowbrick.datasets import load_nfl from yellowbrick.exceptions import YellowbrickValueError -from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset +from unittest import mock +from tests.fixtures import Dataset +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase from sklearn.datasets import make_blobs from sklearn.cluster import Birch, AgglomerativeClustering -from sklearn.cluster import KMeans, AffinityPropagation, MiniBatchKMeans from sklearn.decomposition import LatentDirichletAllocation as LDA +from sklearn.cluster import KMeans, AffinityPropagation, MiniBatchKMeans try: import pandas as pd except ImportError: pd = None - # Determine version of matplotlib MPL_VERS_MAJ = int(mpl.__version__.split(".")[0]) - ########################################################################## ## Fixtures ########################################################################## -@pytest.fixture(scope='class') + +@pytest.fixture(scope="class") def blobs12(request): """ Creates a fixture of 1000 instances in 12 clusters with 16 features. @@ -54,7 +57,7 @@ def blobs12(request): request.cls.blobs12 = Dataset(X, y) -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def blobs4(request): """ Creates a fixture of 400 instances in 4 clusters with 16 features. @@ -66,22 +69,22 @@ def blobs4(request): def assert_fitted(oz): - for param in ('cluster_centers_', 'embedded_centers_', 'scores_', 'fit_time_'): + for param in ("cluster_centers_", "embedded_centers_", "scores_", "fit_time_"): assert hasattr(oz, param) def assert_not_fitted(oz): - for param in ('embedded_centers_', 'scores_', 'fit_time_'): + for param in ("embedded_centers_", "scores_", "fit_time_"): assert not hasattr(oz, param) - ########################################################################## ## InterclusterDistance Test Cases ########################################################################## + @pytest.mark.usefixtures("blobs12", "blobs4") -class TestInterclusterDistance(VisualTestCase, DatasetMixin): +class TestInterclusterDistance(VisualTestCase): """ Test the InterclusterDistance visualizer """ @@ -92,11 +95,11 @@ def test_only_valid_embeddings(self): """ # On init with pytest.raises(YellowbrickValueError, match="unknown embedding 'foo'"): - InterclusterDistance(KMeans(), embedding='foo') + InterclusterDistance(KMeans(), embedding="foo") # After init icdm = InterclusterDistance(KMeans()) - icdm.embedding = 'foo' + icdm.embedding = "foo" with pytest.raises(YellowbrickValueError, match="unknown embedding 'foo'"): icdm.transformer @@ -106,25 +109,29 @@ def test_only_valid_scoring(self): """ # On init with pytest.raises(YellowbrickValueError, match="unknown scoring 'foo'"): - InterclusterDistance(KMeans(), scoring='foo') + InterclusterDistance(KMeans(), scoring="foo") # After init icdm = InterclusterDistance(KMeans()) - icdm.scoring = 'foo' + icdm.scoring = "foo" with pytest.raises(YellowbrickValueError, match="unknown scoring method 'foo'"): icdm._score_clusters(None) + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) def test_kmeans_mds(self): """ Visual similarity with KMeans and MDS scaling """ model = KMeans(9, random_state=38) - oz = InterclusterDistance(model, random_state=83, embedding='mds') + oz = InterclusterDistance(model, random_state=83, embedding="mds") # Prefit assertions assert_not_fitted(oz) - assert oz.fit(self.blobs12.X) is oz # Fit returns self + assert oz.fit(self.blobs12.X) is oz # Fit returns self # Postfit assertions assert_fitted(oz) @@ -135,23 +142,26 @@ def test_kmeans_mds(self): # Image similarity oz.finalize() - tol = 4.9 if sys.platform == 'win32' else 1.0 # fails with RMSE 4.740 on AppVeyor - self.assert_images_similar(oz, tol=tol) + self.assert_images_similar(oz) @pytest.mark.filterwarnings("ignore:the matrix subclass is not the recommended way") + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) def test_affinity_tsne_no_legend(self): """ Visual similarity with AffinityPropagation, TSNE scaling, and no legend """ model = AffinityPropagation() oz = InterclusterDistance( - model, random_state=763, embedding='tsne', legend=False + model, random_state=763, embedding="tsne", legend=False ) # Prefit assertions assert_not_fitted(oz) - assert oz.fit(self.blobs4.X) is oz # Fit returns self + assert oz.fit(self.blobs4.X) is oz # Fit returns self # Postfit assertions assert_fitted(oz) @@ -160,9 +170,7 @@ def test_affinity_tsne_no_legend(self): # Image similarity oz.finalize() - tol = 2.75 if sys.platform == 'win32' else 1.0 # fails with RMSE 2.687 on AppVeyor - self.assert_images_similar(oz, tol=tol) - + self.assert_images_similar(oz) @pytest.mark.skip(reason="LDA not implemented yet") def test_lda_mds(self): @@ -170,12 +178,12 @@ def test_lda_mds(self): Visual similarity with LDA and MDS scaling """ model = LDA(9, random_state=6667) - oz = InterclusterDistance(model, random_state=2332, embedding='mds') + oz = InterclusterDistance(model, random_state=2332, embedding="mds") # Prefit assertions assert_not_fitted(oz) - assert oz.fit(self.blobs12.X) is oz # Fit returns self + assert oz.fit(self.blobs12.X) is oz # Fit returns self # Postfit assertions assert_fitted(oz) @@ -195,12 +203,12 @@ def test_birch_tsne(self): """ Visual similarity with Birch and MDS scaling """ - oz = InterclusterDistance(Birch(n_clusters=9), random_state=83, embedding='mds') + oz = InterclusterDistance(Birch(n_clusters=9), random_state=83, embedding="mds") # Prefit assertions assert_not_fitted(oz) - assert oz.fit(self.blobs12.X) is oz # Fit returns self + assert oz.fit(self.blobs12.X) is oz # Fit returns self # Postfit assertions assert_fitted(oz) @@ -220,13 +228,13 @@ def test_ward_mds_no_legend(self): """ model = AgglomerativeClustering(n_clusters=9) oz = InterclusterDistance( - model, random_state=83, embedding='tsne', legend=False + model, random_state=83, embedding="tsne", legend=False ) # Prefit assertions assert_not_fitted(oz) - assert oz.fit(self.blobs12.X) is oz # Fit returns self + assert oz.fit(self.blobs12.X) is oz # Fit returns self # Postfit assertions assert_fitted(oz) @@ -239,6 +247,10 @@ def test_ward_mds_no_legend(self): oz.finalize() self.assert_images_similar(oz, tol=1.0) + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) def test_quick_method(self): """ Test the quick method producing a valid visualization @@ -247,31 +259,63 @@ def test_quick_method(self): oz = intercluster_distance(model, self.blobs4.X, random_state=93, legend=False) assert isinstance(oz, InterclusterDistance) - tol = 2.75 if sys.platform == 'win32' else 1.0 # fails with RMSE 2.631 on AppVeyor - self.assert_images_similar(oz, tol=tol) + self.assert_images_similar(oz) - @pytest.mark.skipif(MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2") + @pytest.mark.skipif( + MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2" + ) def test_legend_matplotlib_version(self, mock_toolkit): """ ValueError is raised when matplotlib version is incorrect and legend=True """ with pytst.raises(ImportError): from mpl_toolkits.axes_grid1 import inset_locator + assert not inset_locator with pytest.raises(YellowbrickValueError, match="requires matplotlib 2.0.2"): InterclusterDistance(KMeans(), legend=True) - @pytest.mark.skipif(MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2") + @pytest.mark.skipif( + MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2" + ) def test_no_legend_matplotlib_version(self, mock_toolkit): """ No error is raised when matplotlib version is incorrect and legend=False """ with pytst.raises(ImportError): from mpl_toolkits.axes_grid1 import inset_locator + assert not inset_locator try: InterclusterDistance(KMeans(), legend=False) except YellowbrickValueError as e: self.fail(e) + + @pytest.mark.xfail( + reason="""third test fails with AssertionError: Expected fit + to be called once. Called 0 times.""" + ) + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_nfl(return_dataset=True).to_numpy() + + model = KMeans().fit(X, y) + + with mock.patch.object(model, "fit") as mockfit: + oz = ICDM(model) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = ICDM(model, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = ICDM(model, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) diff --git a/tests/test_cluster/test_silhouette.py b/tests/test_cluster/test_silhouette.py index 6daa64a29..4df85b7d2 100644 --- a/tests/test_cluster/test_silhouette.py +++ b/tests/test_cluster/test_silhouette.py @@ -1,10 +1,10 @@ # tests.test_cluster.test_silhouette # Tests for the SilhouetteVisualizer # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Mon Mar 27 10:01:37 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_silhouette.py [57b563b] benjamin@bengfort.com $ @@ -19,29 +19,29 @@ import sys import pytest - import matplotlib.pyplot as plt -from ..base import VisualTestCase - from sklearn.datasets import make_blobs from sklearn.cluster import KMeans, MiniBatchKMeans -from yellowbrick.cluster.silhouette import SilhouetteVisualizer +from unittest import mock +from tests.base import VisualTestCase + +from yellowbrick.datasets import load_nfl +from yellowbrick.cluster.silhouette import SilhouetteVisualizer, silhouette_visualizer ########################################################################## ## SilhouetteVisualizer Test Cases ########################################################################## -class SilhouetteVisualizerTests(VisualTestCase): + +class TestSilhouetteVisualizer(VisualTestCase): """ - Silhouette Visualizer + Silhouette Visualizer Tests """ - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_integrated_kmeans_silhouette(self): """ Test no exceptions for kmeans silhouette visualizer on blobs dataset @@ -59,15 +59,13 @@ def test_integrated_kmeans_silhouette(self): visualizer = SilhouetteVisualizer(KMeans(random_state=0), ax=ax) visualizer.fit(X) - visualizer.poof() + visualizer.finalize() - self.assert_images_similar(visualizer) + self.assert_images_similar(visualizer, remove_legend=True) except Exception as e: self.fail("error during silhouette: {}".format(e)) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_integrated_mini_batch_kmeans_silhouette(self): """ Test no exceptions for mini-batch kmeans silhouette visualizer @@ -85,17 +83,134 @@ def test_integrated_mini_batch_kmeans_silhouette(self): visualizer = SilhouetteVisualizer(MiniBatchKMeans(random_state=0), ax=ax) visualizer.fit(X) - visualizer.poof() + visualizer.finalize() - self.assert_images_similar(visualizer) + self.assert_images_similar(visualizer, remove_legend=True) except Exception as e: self.fail("error during silhouette: {}".format(e)) - @pytest.mark.skip( - reason="no negative silhouette example available yet" - ) + @pytest.mark.skip(reason="no negative silhouette example available yet") def test_negative_silhouette_score(self): """ Ensure negative silhouette scores are correctly displayed by the visualizer. """ - raise NotImplementedError("no negative silhouette example available") \ No newline at end of file + raise NotImplementedError("no negative silhouette example available") + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_colormap_silhouette(self): + """ + Test no exceptions for modifying the colormap in a silhouette visualizer + """ + # Generate a blobs data set + X, y = make_blobs( + n_samples=1000, n_features=12, centers=8, shuffle=False, random_state=0 + ) + + try: + fig = plt.figure() + ax = fig.add_subplot() + + visualizer = SilhouetteVisualizer( + MiniBatchKMeans(random_state=0), ax=ax, colormap="gnuplot" + ) + visualizer.fit(X) + visualizer.finalize() + + self.assert_images_similar(visualizer, remove_legend=True) + except Exception as e: + self.fail("error during silhouette: {}".format(e)) + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_colors_silhouette(self): + """ + Test no exceptions for modifying the colors in a silhouette visualizer + with a list of color names + """ + # Generate a blobs data set + X, y = make_blobs( + n_samples=1000, n_features=12, centers=8, shuffle=False, random_state=0 + ) + + try: + fig = plt.figure() + ax = fig.add_subplot() + + visualizer = SilhouetteVisualizer( + MiniBatchKMeans(random_state=0), + ax=ax, + colors=["red", "green", "blue", "indigo", "cyan", "lavender"], + ) + visualizer.fit(X) + visualizer.finalize() + + self.assert_images_similar(visualizer, remove_legend=True) + except Exception as e: + self.fail("error during silhouette: {}".format(e)) + + def test_colormap_as_colors_silhouette(self): + """ + Test no exceptions for modifying the colors in a silhouette visualizer + by using a matplotlib colormap as colors + """ + # Generate a blobs data set + X, y = make_blobs( + n_samples=1000, n_features=12, centers=8, shuffle=False, random_state=0 + ) + + try: + fig = plt.figure() + ax = fig.add_subplot() + + visualizer = SilhouetteVisualizer( + MiniBatchKMeans(random_state=0), ax=ax, colors="cool" + ) + visualizer.fit(X) + visualizer.finalize() + + tol = ( + 3.2 if sys.platform == "win32" else 0.01 + ) # Fails on AppVeyor with RMS 3.143 + self.assert_images_similar(visualizer, remove_legend=True, tol=tol) + except Exception as e: + self.fail("error during silhouette: {}".format(e)) + + def test_quick_method(self): + """ + Test the quick method producing a valid visualization + """ + X, y = make_blobs( + n_samples=1000, n_features=12, centers=8, shuffle=False, random_state=0 + ) + + model = MiniBatchKMeans(3, random_state=343) + oz = silhouette_visualizer(model, X, random_state=93, legend=False) + assert isinstance(oz, SilhouetteVisualizer) + + self.assert_images_similar(oz) + + @pytest.mark.xfail( + reason="""third test fails with AssertionError: Expected fit + to be called once. Called 0 times.""" + ) + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_nfl(return_dataset=True).to_numpy() + + model = MiniBatchKMeans().fit(X, y) + + with mock.patch.object(model, "fit") as mockfit: + oz = SilhouetteVisualizer(model) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = SilhouetteVisualizer(model, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = SilhouetteVisualizer(model, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) diff --git a/tests/test_contrib/__init__.py b/tests/test_contrib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_contrib/test_classifier/__init__.py b/tests/test_contrib/test_classifier/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_contrib/test_classifier/test_boundaries.py b/tests/test_contrib/test_classifier/test_boundaries.py index 02ae5e894..f901a1491 100644 --- a/tests/test_contrib/test_classifier/test_boundaries.py +++ b/tests/test_contrib/test_classifier/test_boundaries.py @@ -4,10 +4,10 @@ # Author: Author: Nathan Danielsen # Created: Sun Mar 19 13:01:29 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_boundaries.py [] nathan.danielsen@gmail.com $ +# ID: test_boundaries.py [a60bc41] nathan.danielsen@gmail.com $ """ Ensure that the Decision Boundary visualizations work. """ @@ -17,10 +17,10 @@ ########################################################################## import sys -import six import pytest import numpy as np +from unittest import mock from tests.base import VisualTestCase from yellowbrick.contrib.classifier import * @@ -31,11 +31,6 @@ from sklearn import neighbors from sklearn import naive_bayes -try: - from unittest import mock -except ImportError: - import mock - try: import pandas as pd except ImportError: @@ -46,20 +41,22 @@ # Data ########################################################################## -X = np.array([ - [2.318, 2.727, 4.260, 7.212, 4.792, ], - [2.315, 2.726, 4.295, 7.140, 4.783, ], - [2.315, 2.724, 4.260, 7.135, 4.779, ], - [2.110, 3.609, 4.330, 7.985, 5.595, ], - [2.110, 3.626, 4.330, 8.203, 5.621, ], - [2.110, 3.620, 4.470, 8.210, 5.612, ], - [2.318, 2.727, 4.260, 7.212, 4.792, ], - [2.315, 2.726, 4.295, 7.140, 4.783, ], - [2.315, 2.724, 4.260, 7.135, 4.779, ], - [2.110, 3.609, 4.330, 7.985, 5.595, ], - [2.110, 3.626, 4.330, 8.203, 5.621, ], - [2.110, 3.620, 4.470, 8.210, 5.612, ] - ]) +X = np.array( + [ + [2.318, 2.727, 4.260, 7.212, 4.792], + [2.315, 2.726, 4.295, 7.140, 4.783], + [2.315, 2.724, 4.260, 7.135, 4.779], + [2.110, 3.609, 4.330, 7.985, 5.595], + [2.110, 3.626, 4.330, 8.203, 5.621], + [2.110, 3.620, 4.470, 8.210, 5.612], + [2.318, 2.727, 4.260, 7.212, 4.792], + [2.315, 2.726, 4.295, 7.140, 4.783], + [2.315, 2.724, 4.260, 7.135, 4.779], + [2.110, 3.609, 4.330, 7.985, 5.595], + [2.110, 3.626, 4.330, 8.203, 5.621], + [2.110, 3.620, 4.470, 8.210, 5.612], + ] +) y = np.array([1, 2, 1, 2, 1, 0, 0, 1, 3, 1, 3, 2]) @@ -70,13 +67,13 @@ ########################################################################## -@pytest.mark.filterwarnings('ignore') -class DecisionBoundariesVisualizerTest(VisualTestCase): +@pytest.mark.filterwarnings("ignore") +class TestDecisionBoundariesVisualizer(VisualTestCase): """ - DecisionBoundariesVisualizer + Test DecisionBoundariesVisualizer """ - def test_decision_bounardies(self): + def test_decision_boundaries(self): """ Assert no errors during kNN DecisionBoundariesVisualizer integration """ @@ -85,13 +82,20 @@ def test_decision_bounardies(self): viz.fit_draw_poof(X_two_cols, y=y) def test_deprecated(self): + """ + Assert the DecisionViz class issues deprecation warning + """ with pytest.deprecated_call(): model = neighbors.KNeighborsClassifier(3) DecisionViz(model) - @pytest.mark.skipif(six.PY2, reason="deprecation warnings filtered in PY2") def test_deprecated_message(self): - with pytest.warns(DeprecationWarning, match='Will be moved to yellowbrick.contrib in v0.8'): + """ + Test the deprecation warning message + """ + with pytest.warns( + DeprecationWarning, match="Will be moved to yellowbrick.contrib in v0.8" + ): model = neighbors.KNeighborsClassifier(3) DecisionViz(model) @@ -102,24 +106,23 @@ def test_init(self): model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer(model) - self.assertEquals(viz.step_size, 0.0025) - self.assertEqual(viz.name, 'KNeighborsClassifier') - self.assertEqual(viz.estimator, model) - - self.assertIsNone(viz.classes_) - self.assertIsNone(viz.features_) - self.assertIsNotNone(viz.markers) - self.assertIsNotNone(viz.scatter_alpha) - self.assertTrue(viz.show_scatter) + assert viz.step_size == 0.0025 + assert viz.name == "KNeighborsClassifier" + assert viz.estimator is model - self.assertIsNone(viz.Z) - self.assertIsNone(viz.xx) - self.assertIsNone(viz.yy) - self.assertIsNone(viz.class_labels) - self.assertIsNone(viz.title) - self.assertIsNone(viz.x) - self.assertIsNone(viz.y) + assert not hasattr(viz, "classes_") + assert viz.features_ is None + assert viz.markers is not None + assert viz.scatter_alpha is not None + assert viz.show_scatter is True + assert viz.Z is None + assert viz.xx is None + assert viz.yy is None + assert viz.class_labels is None + assert viz.title is None + assert viz.x is None + assert viz.y is None def test_scatter_xy_and_features_raise_error(self): """ @@ -128,19 +131,16 @@ def test_scatter_xy_and_features_raise_error(self): model = neighbors.KNeighborsClassifier(3) features = ["temperature", "relative_humidity", "light"] - with self.assertRaises(YellowbrickValueError): - DecisionBoundariesVisualizer( - model, features=features, x='one', y='two' - ) + with pytest.raises(YellowbrickValueError): + DecisionBoundariesVisualizer(model, features=features, x="one", y="two") def test_scatter_xy_changes_to_features(self): """ Assert that x,y and features will raise error """ model = neighbors.KNeighborsClassifier(3) - visualizer = DecisionBoundariesVisualizer(model, x='one', y='two') - self.assertEquals(visualizer.features_, ['one', 'two']) - + visualizer = DecisionBoundariesVisualizer(model, x="one", y="two") + assert visualizer.features_ == ["one", "two"] def test_fit(self): """ @@ -154,17 +154,17 @@ def test_fit(self): fitted_viz = viz.fit(X_two_cols, y=y) # assert that classes and labels are established - self.assertEqual(fitted_viz.classes_, {0: '0', 1: '1', 2: '2', 3: '3'}) - self.assertEqual(fitted_viz.features_, ['Feature One', 'Feature Two']) + assert fitted_viz.classes_ == {0: "0", 1: "1", 2: "2", 3: "3"} + assert fitted_viz.features_ == ["Feature One", "Feature Two"] # assert that the fit method is called model.fit.assert_called_once_with(X_two_cols, y) # mock object is called twice in predict and reshape - self.assertEqual(len(model.predict.mock_calls), 2) + assert len(model.predict.mock_calls) == 2 # test that attrs are set - self.assertIsNotNone(fitted_viz.ax) - self.assertIsNotNone(fitted_viz.Z_shape) + assert fitted_viz.ax is not None + assert fitted_viz.Z_shape is not None def test_fit_class_labels(self): """ @@ -172,13 +172,15 @@ def test_fit_class_labels(self): """ model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer( - model, classes=['one', 'two', 'three', 'four']) + model, classes=["one", "two", "three", "four"] + ) fitted_viz = viz.fit(X_two_cols, y=y) - self.assertEquals(fitted_viz.classes_, - {'three': '2', - 'four': '3', - 'two': '1', - 'one': '0'}) + assert fitted_viz.classes_ == { + "three": "2", + "four": "3", + "two": "1", + "one": "0", + } def test_fit_class_labels_class_names_edge_case(self): """ @@ -186,8 +188,11 @@ def test_fit_class_labels_class_names_edge_case(self): """ model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer( - model, classes=['one', 'two', 'three', 'four', 'five']) - self.assertRaises(YellowbrickTypeError, viz.fit, X_two_cols, y=y) + model, classes=["one", "two", "three", "four", "five"] + ) + + with pytest.raises(YellowbrickTypeError): + viz.fit(X_two_cols, y=y) def test_fit_features_assignment_None(self): """ @@ -195,18 +200,18 @@ def test_fit_features_assignment_None(self): """ model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer(model) - self.assertIsNone(viz.features_) + assert viz.features_ is None fitted_viz = viz.fit(X_two_cols, y=y) - self.assertEquals(fitted_viz.features_, ['Feature One', 'Feature Two']) + assert fitted_viz.features_ == ["Feature One", "Feature Two"] def test_fit_features_assignment(self): """ Test fit when features are specified """ model = neighbors.KNeighborsClassifier(3) - viz = DecisionBoundariesVisualizer(model, features=['one', 'two']) + viz = DecisionBoundariesVisualizer(model, features=["one", "two"]) fitted_viz = viz.fit(X_two_cols, y=y) - self.assertEquals(fitted_viz.features_, ['one', 'two']) + assert fitted_viz.features_ == ["one", "two"] @mock.patch("yellowbrick.contrib.classifier.boundaries.OrderedDict") def test_draw_ordereddict_calls(self, mock_odict): @@ -215,9 +220,12 @@ def test_draw_ordereddict_calls(self, mock_odict): """ mock_odict.return_value = {} model = neighbors.KNeighborsClassifier(3) - viz = DecisionBoundariesVisualizer(model, features=['one', 'two']) - self.assertRaises(KeyError, viz.fit_draw, X_two_cols, y=y) - self.assertEquals(len(mock_odict.mock_calls), 2) + viz = DecisionBoundariesVisualizer(model, features=["one", "two"]) + + with pytest.raises(KeyError): + viz.fit_draw(X_two_cols, y=y) + + assert len(mock_odict.mock_calls) == 2 @mock.patch("yellowbrick.contrib.classifier.boundaries.resolve_colors") def test_draw_ordereddict_calls_one(self, mock_resolve_colors): @@ -226,16 +234,19 @@ def test_draw_ordereddict_calls_one(self, mock_resolve_colors): """ mock_resolve_colors.return_value = [] model = neighbors.KNeighborsClassifier(3) - viz = DecisionBoundariesVisualizer(model, features=['one', 'two']) - self.assertRaises(StopIteration, viz.fit_draw, X_two_cols, y=y) - self.assertEquals(len(mock_resolve_colors.mock_calls), 1) + viz = DecisionBoundariesVisualizer(model, features=["one", "two"]) + + with pytest.raises(StopIteration): + viz.fit_draw(X_two_cols, y=y) + + assert len(mock_resolve_colors.mock_calls) == 1 def test_draw_ax_show_scatter_true(self): """ Test that the matplotlib functions are being called """ model = neighbors.KNeighborsClassifier(3) - viz = DecisionBoundariesVisualizer(model, features=['one', 'two']) + viz = DecisionBoundariesVisualizer(model, features=["one", "two"]) fitted_viz = viz.fit(X_two_cols, y=y) fitted_viz.ax = mock.Mock() fitted_viz.ax.pcolormesh = mock.MagicMock() @@ -243,9 +254,9 @@ def test_draw_ax_show_scatter_true(self): fitted_viz.ax.legend = mock.MagicMock() fitted_viz.draw(X_two_cols, y=y) - self.assertEquals(len(fitted_viz.ax.pcolormesh.mock_calls), 1) - self.assertEquals(len(fitted_viz.ax.scatter.mock_calls), 4) - self.assertEquals(len(fitted_viz.ax.legend.mock_calls), 0) + assert len(fitted_viz.ax.pcolormesh.mock_calls) == 1 + assert len(fitted_viz.ax.scatter.mock_calls) == 4 + assert len(fitted_viz.ax.legend.mock_calls) == 0 def test_draw_ax_show_scatter_False(self): """ @@ -253,7 +264,8 @@ def test_draw_ax_show_scatter_False(self): """ model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer( - model, features=['one', 'two'], show_scatter=False) + model, features=["one", "two"], show_scatter=False + ) fitted_viz = viz.fit(X_two_cols, y=y) fitted_viz.ax = mock.Mock() fitted_viz.ax.pcolormesh = mock.MagicMock() @@ -261,9 +273,9 @@ def test_draw_ax_show_scatter_False(self): fitted_viz.ax.legend = mock.MagicMock() fitted_viz.draw(X_two_cols, y=y) - self.assertEquals(len(fitted_viz.ax.pcolormesh.mock_calls), 1) - self.assertEquals(len(fitted_viz.ax.scatter.mock_calls), 0) - self.assertEquals(len(fitted_viz.ax.legend.mock_calls), 1) + assert len(fitted_viz.ax.pcolormesh.mock_calls) == 1 + assert len(fitted_viz.ax.scatter.mock_calls) == 0 + assert len(fitted_viz.ax.legend.mock_calls) == 1 def test_finalize(self): """ @@ -271,7 +283,8 @@ def test_finalize(self): """ model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer( - model, features=['one', 'two'], show_scatter=False) + model, features=["one", "two"], show_scatter=False + ) fitted_viz = viz.fit(X_two_cols, y=y) fitted_viz.draw(X_two_cols, y=y) @@ -280,11 +293,11 @@ def test_finalize(self): fitted_viz.ax.set_xlabel = mock.MagicMock() fitted_viz.ax.set_ylabel = mock.MagicMock() - fitted_viz.poof() + fitted_viz.finalize() - fitted_viz.ax.legend.assert_called_once_with(loc='best', frameon=True) - fitted_viz.ax.set_xlabel.assert_called_once_with('one') - fitted_viz.ax.set_ylabel.assert_called_once_with('two') + fitted_viz.ax.legend.assert_called_once_with(loc="best", frameon=True) + fitted_viz.ax.set_xlabel.assert_called_once_with("one") + fitted_viz.ax.set_ylabel.assert_called_once_with("two") def test_fit_draw(self): """ @@ -292,7 +305,8 @@ def test_fit_draw(self): """ model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer( - model, features=['one', 'two'], show_scatter=False) + model, features=["one", "two"], show_scatter=False + ) viz.fit = mock.Mock() viz.draw = mock.Mock() @@ -308,7 +322,8 @@ def test_fit_draw_poof(self): """ model = neighbors.KNeighborsClassifier(3) viz = DecisionBoundariesVisualizer( - model, features=['one', 'two'], show_scatter=False) + model, features=["one", "two"], show_scatter=False + ) viz.fit = mock.Mock() viz.draw = mock.Mock() @@ -320,34 +335,39 @@ def test_fit_draw_poof(self): viz.draw.assert_called_once_with(X_two_cols, y) viz.poof.assert_called_once_with() - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(reason="numpy structured arrays have changed since v1.14") def test_integrated_plot_numpy_named_arrays(self): """ Test integration of visualizer with numpy named arrays """ model = naive_bayes.MultinomialNB() - X = np.array([ - (1.1, 9.52, 1.23, 0.86, 7.89, 0.13), - (3.4, 2.84, 8.65, 0.45, 7.43, 0.16), - (1.2, 3.22, 6.56, 0.24, 3.45, 0.17), - (3.8, 6.18, 2.45, 0.28, 2.53, 0.13), - (5.1, 9.12, 1.06, 0.19, 1.43, 0.13), - (4.4, 8.84, 4.97, 0.98, 1.35, 0.13), - (3.2, 3.22, 5.03, 0.68, 3.53, 0.32), - (7.8, 2.18, 6.87, 0.35, 3.25, 0.38), - ], dtype=[('a',' # Created: Thu Mar 29 12:13:04 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_bar.py [7d3f5e6] nathan.danielsen@gmail.com $ +# ID: test_bar.py [1443e16] ndanielsen@users.noreply.github.com $ """ Tests for the MissingValuesBar visualizations. @@ -18,6 +18,9 @@ ########################################################################## import os +import pytest +import numpy as np + from tests.base import VisualTestCase from sklearn.datasets import make_classification from yellowbrick.contrib.missing.bar import * @@ -27,28 +30,35 @@ except ImportError: pd = None + +@pytest.fixture(scope="class") +def missing_bar_tolerance(request): + request.cls.tol = 0.5 if os.name == "nt" else 0.01 + + ########################################################################## ## Feature Importances Tests ########################################################################## + +@pytest.mark.usefixtures("missing_bar_tolerance") class TestMissingBarVisualizer(VisualTestCase): """ FeatureImportances visualizer """ - def setUp(self): - super(TestMissingBarVisualizer, self).setUp() - self.tol = 0.01 - if os.name == 'nt': # Windows - self.tol = 0.5 - def test_missingvaluesbar_pandas(self): """ Integration test of visualizer with pandas """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=854 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=854, ) # add nan values to a range of values in the matrix @@ -58,18 +68,22 @@ def test_missingvaluesbar_pandas(self): features = [str(n) for n in range(20)] viz = MissingValuesBar(features=features) viz.fit(X_) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) - def test_missingvaluesbar_numpy(self): """ Integration test of visualizer with numpy without target y passed in """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=856 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=856, ) # add nan values to a range of values in the matrix @@ -78,7 +92,7 @@ def test_missingvaluesbar_numpy(self): features = [str(n) for n in range(20)] viz = MissingValuesBar(features=features) viz.fit(X) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) @@ -88,8 +102,13 @@ def test_missingvaluesbar_numpy_with_y_target(self): but no class labels """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=856 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=856, ) # add nan values to a range of values in the matrix @@ -98,7 +117,7 @@ def test_missingvaluesbar_numpy_with_y_target(self): features = [str(n) for n in range(20)] viz = MissingValuesBar(features=features) viz.fit(X, y) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) @@ -108,16 +127,21 @@ def test_missingvaluesbar_numpy_with_y_target_with_labels(self): but no class labels """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=856 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=856, ) # add nan values to a range of values in the matrix X[X > 1.5] = np.nan features = [str(n) for n in range(20)] - viz = MissingValuesBar(features=features, classes=['class A', 'class B']) + viz = MissingValuesBar(features=features, classes=["class A", "class B"]) viz.fit(X, y) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) diff --git a/tests/test_contrib/test_missing/test_dispersion.py b/tests/test_contrib/test_missing/test_dispersion.py index 0636f7a70..7db9a525a 100644 --- a/tests/test_contrib/test_missing/test_dispersion.py +++ b/tests/test_contrib/test_missing/test_dispersion.py @@ -4,10 +4,10 @@ # Author: Nathan Danielsen # Created: Thu Mar 29 12:13:04 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_dispersion.py [7d3f5e6] nathan.danielsen@gmail.com $ +# ID: test_dispersion.py [1443e16] ndanielsen@users.noreply.github.com $ """ Tests for the MissingValuesDispersion visualizations. @@ -16,7 +16,10 @@ ########################################################################## ## Imports ########################################################################## + import os +import pytest + from sklearn.datasets import make_classification from tests.base import VisualTestCase @@ -27,28 +30,35 @@ except ImportError: pd = None + +@pytest.fixture(scope="class") +def missing_dispersion_tolerance(request): + request.cls.tol = 0.5 if os.name == "nt" else 0.01 + + ########################################################################## ## Feature Importances Tests ########################################################################## -class MissingValuesDispersionTestCase(VisualTestCase): + +@pytest.mark.usefixtures("missing_dispersion_tolerance") +class TestMissingValuesDispersion(VisualTestCase): """ MissingValuesDispersion visualizer """ - def setUp(self): - super(MissingValuesDispersionTestCase, self).setUp() - self.tol = 0.01 - if os.name == 'nt': # Windows - self.tol = 5.0 - def test_missingvaluesdispersion_with_pandas(self): """ Integration test of visualizer with pandas """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=854 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=854, ) # add nan values to a range of values in the matrix @@ -58,7 +68,7 @@ def test_missingvaluesdispersion_with_pandas(self): features = [str(n) for n in range(20)] viz = MissingValuesDispersion(features=features) viz.fit(X_) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) @@ -67,8 +77,13 @@ def test_missingvaluesdispersion_with_pandas_with_y_targets(self): Integration test of visualizer with pandas with y targets """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=854 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=854, ) # add nan values to a range of values in the matrix @@ -76,21 +91,25 @@ def test_missingvaluesdispersion_with_pandas_with_y_targets(self): X_ = pd.DataFrame(X) features = [str(n) for n in range(20)] - classes = ['Class A', 'Class B'] + classes = ["Class A", "Class B"] viz = MissingValuesDispersion(features=features, classes=classes) viz.fit(X_, y=y) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) - def test_missingvaluesdispersion_with_numpy(self): """ Integration test of visualizer with numpy """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=852 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=852, ) # add nan values to a range of values in the matrix @@ -99,7 +118,7 @@ def test_missingvaluesdispersion_with_numpy(self): features = [str(n) for n in range(20)] viz = MissingValuesDispersion(features=features) viz.fit(X) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) @@ -108,17 +127,22 @@ def test_missingvaluesdispersion_with_numpy_with_y_targets(self): Integration test of visualizer with numpy with y targets """ X, y = make_classification( - n_samples=400, n_features=20, n_informative=8, n_redundant=8, - n_classes=2, n_clusters_per_class=4, random_state=852 + n_samples=400, + n_features=20, + n_informative=8, + n_redundant=8, + n_classes=2, + n_clusters_per_class=4, + random_state=852, ) # add nan values to a range of values in the matrix X[X > 1.5] = np.nan features = [str(n) for n in range(20)] - classes = ['Class A', 'Class B'] + classes = ["Class A", "Class B"] viz = MissingValuesDispersion(features=features, classes=classes) viz.fit(X, y=y) - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=self.tol) diff --git a/tests/test_contrib/test_scatter.py b/tests/test_contrib/test_scatter.py index 082d2e992..28c15b8eb 100644 --- a/tests/test_contrib/test_scatter.py +++ b/tests/test_contrib/test_scatter.py @@ -1,13 +1,13 @@ # tests.test_contrib.test_scatter # Test the ScatterViz feature analysis visualizers # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Fri Feb 26 19:40:00 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_scatter.py [fc94ec4] ndanielsen@users.noreply.github.com $ +# ID: test_scatter.py [a89633e] benjamin@bengfort.com $ """ Test the ScatterViz feature analysis visualizers """ @@ -16,36 +16,32 @@ # Imports ########################################################################## -import sys import pytest import numpy as np -import matplotlib as mptl +from unittest import mock +from tests.base import VisualTestCase from yellowbrick.contrib.scatter import * +from yellowbrick.datasets import load_occupancy from yellowbrick.exceptions import YellowbrickValueError from yellowbrick.style import palettes -from tests.dataset import DatasetMixin -from tests.base import VisualTestCase -from yellowbrick.exceptions import ImageComparisonFailure - try: import pandas as pd except ImportError: pd = None -try: - from unittest import mock -except ImportError: - import mock - ########################################################################## # ScatterViz Base Tests ########################################################################## -@pytest.mark.filterwarnings('ignore') -class ScatterVizTests(VisualTestCase, DatasetMixin): + +@pytest.mark.filterwarnings("ignore") +class TestScatterViz(VisualTestCase): + """ + Test ScatterViz + """ # yapf: disable X = np.array([ @@ -55,32 +51,24 @@ class ScatterVizTests(VisualTestCase, DatasetMixin): [2.110, 3.609, 4.330, 7.985, 5.595, ], [2.110, 3.626, 4.330, 8.203, 5.621, ], [2.110, 3.620, 4.470, 8.210, 5.612, ] - ]) + ]) # yapf: enable y = np.array([1, 0, 1, 0, 1, 0]) - def setUp(self): - self.occupancy = self.load_data('occupancy') - super(ScatterVizTests, self).setUp() - - def tearDown(self): - self.occupancy = None - super(ScatterVizTests, self).tearDown() - def test_init_alias(self): """ Test alias for ScatterViz """ - features = ["temperature", "relative_humidity"] - visualizer = ScatterVisualizer(features=features, markers=['*']) - self.assertIsNotNone(visualizer.markers) + features = ["temperature", "relative humidity"] + visualizer = ScatterVisualizer(features=features, markers=["*"]) + assert visualizer.markers is not None def test_scatter(self): """ Assert no errors occur during scatter visualizer integration """ X_two_cols = self.X[:, :2] - features = ["temperature", "relative_humidity"] + features = ["temperature", "relative humidity"] visualizer = ScatterViz(features=features) visualizer.fit_transform(X_two_cols, self.y) @@ -88,9 +76,9 @@ def test_color_builds(self): """ Assert no errors occur during scatter visualizer integration """ - colors = palettes.PALETTES['pastel'] + colors = palettes.PALETTES["pastel"] X_two_cols = self.X[:, :2] - features = ["temperature", "relative_humidity"] + features = ["temperature", "relative humidity"] visualizer = ScatterViz(features=features, color=colors) visualizer.fit_transform(X_two_cols, self.y) @@ -101,61 +89,50 @@ def test_scatter_no_features(self): X_two_cols = self.X[:, :2] visualizer = ScatterViz() visualizer.fit_transform_poof(X_two_cols, self.y) - self.assertEquals(visualizer.features_, ['Feature One', 'Feature Two']) + assert visualizer.features_ == ["Feature One", "Feature Two"] def test_scatter_only_two_features_allowed_init(self): """ Assert that only two features are allowed for scatter visualizer init """ - features = ["temperature", "relative_humidity", "light"] + features = ["temperature", "relative humidity", "light"] - with self.assertRaises(YellowbrickValueError): + with pytest.raises(YellowbrickValueError): ScatterViz(features=features) def test_scatter_xy_and_features_raise_error(self): """ Assert that x,y and features will raise scatterviz error """ - features = ["temperature", "relative_humidity", "light"] + features = ["temperature", "relative humidity", "light"] - with self.assertRaises(YellowbrickValueError): - ScatterViz(features=features, x='one', y='two') + with pytest.raises(YellowbrickValueError): + ScatterViz(features=features, x="one", y="two") def test_scatter_xy_changes_to_features(self): """ Assert that x,y with no features will not raise scatterviz error """ - visualizer = ScatterViz(x='one', y='two') - self.assertEquals(visualizer.features_, ['one', 'two']) + visualizer = ScatterViz(x="one", y="two") + assert visualizer.features == ["one", "two"] def test_scatter_requires_two_features_in_numpy_matrix(self): """ Assert only two features allowed for scatter visualizer if not in init """ visualizer = ScatterViz() - with self.assertRaises(YellowbrickValueError) as context: + with pytest.raises(YellowbrickValueError, match="only accepts two features"): visualizer.fit_transform(self.X, self.y) - self.assertTrue( - 'only accepts two features' in str(context.exception)) - @pytest.mark.xfail( - sys.platform == 'win32', reason="Changing the dtype to a subarray type is only supported if the total itemsize is unchanged" - ) def test_integrated_scatter(self): """ Test scatter on the real, occupancy data set """ # Load the data from the fixture - X = self.occupancy[[ - "temperature", "relative_humidity", "light", "C02", "humidity" - ]] - - # Convert to numpy arrays - X = X.copy().view((float, len(X.dtype.names))) - y = self.occupancy['occupancy'].astype(int) + X, y = load_occupancy(return_dataset=True).to_numpy() # Test the visualizer - features = ["temperature", "relative_humidity"] + features = ["temperature", "relative humidity"] visualizer = ScatterViz(features=features) visualizer.fit_transform_poof(X[:, :2], y) @@ -178,28 +155,19 @@ def test_alpha_param(self): assert "alpha" in scatter_kwargs assert scatter_kwargs["alpha"] == 0.7 - @pytest.mark.xfail( - sys.platform == 'win32', reason="Changing the dtype to a subarray type is only supported if the total itemsize is unchanged" - ) def test_scatter_quick_method(self): """ Test scatter quick method on the real, occupancy data set """ # Load the data from the fixture - X = self.occupancy[[ - "temperature", "relative_humidity", "light", "C02", "humidity" - ]] - - # Convert to numpy arrays - X = X.copy().view((float, len(X.dtype.names))) - y = self.occupancy['occupancy'].astype(int) + X, y = load_occupancy(return_dataset=True).to_numpy() # Test the visualizer - features = ["temperature", "relative_humidity"] - ax = scatterviz(X[:, :2], y=y, ax=None, features=features) + features = ["temperature", "relative humidity"] + viz = scatterviz(X[:, :2], y=y, ax=None, features=features) # test that is returns a matplotlib obj with axes - self.assertIsInstance(ax, mptl.axes.Axes) + assert isinstance(viz, ScatterVisualizer) @pytest.mark.skipif(pd is None, reason="pandas is required for this test") def test_integrated_scatter_with_pandas(self): @@ -207,44 +175,30 @@ def test_integrated_scatter_with_pandas(self): Test scatterviz on the real, occupancy data set with pandas """ # Load the data from the fixture - X = self.occupancy[[ - "temperature", "relative_humidity", "light", "C02", "humidity" - ]] - y = self.occupancy['occupancy'].astype(int) - - # Convert X to a pandas dataframe - X = pd.DataFrame(X) - X.columns = [ - "temperature", "relative_humidity", "light", "C02", "humidity" - ] + # Load the data from the fixture + X, y = load_occupancy(return_dataset=True).to_pandas() # Test the visualizer - features = ["temperature", "relative_humidity"] + features = ["temperature", "relative humidity"] visualizer = ScatterViz(features=features) visualizer.fit_transform_poof(X, y) - @pytest.mark.xfail( - sys.platform == 'win32', reason="Changing the dtype to a subarray type is only supported if the total itemsize is unchanged" - ) + @pytest.mark.xfail(reason="numpy structured arrays have changed since v1.14") def test_integrated_scatter_numpy_named_arrays(self): """ Test scatterviz on numpy named arrays """ - dt = np.dtype({ - 'names': ['one', 'two', 'three', 'four', "five"], - 'formats': [ - np.float64, - np.float64, - np.float64, - np.float64, - np.float64, - ] - }) - - X_named = self.X.astype(dt, casting='unsafe') - visualizer = ScatterViz(features=['one', 'two']) + dt = np.dtype( + { + "names": ["one", "two", "three", "four", "five"], + "formats": [np.float64, np.float64, np.float64, np.float64, np.float64], + } + ) + + X_named = self.X.astype(dt, casting="unsafe") + visualizer = ScatterViz(features=["one", "two"]) visualizer.fit_transform_poof(X_named, self.y) - self.assertEquals(visualizer.features_, ['one', 'two']) + assert visualizer.features_ == ["one", "two"] def test_integrated_scatter_numpy_arrays_no_names(self): """ @@ -252,7 +206,7 @@ def test_integrated_scatter_numpy_arrays_no_names(self): """ visualizer = ScatterViz(features=[1, 2]) visualizer.fit_transform_poof(self.X, self.y) - self.assertEquals(visualizer.features_, [1, 2]) + assert visualizer.features_ == [1, 2] def test_scatter_image(self): """ @@ -261,24 +215,9 @@ def test_scatter_image(self): # self.setUp_ImageTest() X_two_cols = self.X[:, :2] - features = ["temperature", "relative_humidity"] + features = ["temperature", "relative humidity"] visualizer = ScatterViz(features=features) visualizer.fit(X_two_cols, self.y) visualizer.draw(X_two_cols, self.y) self.assert_images_similar(visualizer) - - - def test_scatter_image_fail(self): - """ - Assert bad image similarity on scatterviz errors - """ - - X_two_cols = self.X[:, :2] - features = ["temperature", "relative_humidity"] - visualizer = ScatterViz(features=features) - visualizer.fit(X_two_cols, self.y) - visualizer.draw(X_two_cols, self.y) - - with self.assertRaises(ImageComparisonFailure): - self.assert_images_similar(visualizer) diff --git a/tests/test_contrib/test_statsmodels/__init__.py b/tests/test_contrib/test_statsmodels/__init__.py index a4651ffae..9ecc36cb9 100644 --- a/tests/test_contrib/test_statsmodels/__init__.py +++ b/tests/test_contrib/test_statsmodels/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Wed Apr 04 13:28:13 2018 -0400 # -# ID: __init__.py [] benjamin@bengfort.com $ +# ID: __init__.py [d6ebc39] benjamin@bengfort.com $ """ Tests for the statsmodels contrib package diff --git a/tests/test_contrib/test_statsmodels/test_base.py b/tests/test_contrib/test_statsmodels/test_base.py index a3c7acbec..777098718 100644 --- a/tests/test_contrib/test_statsmodels/test_base.py +++ b/tests/test_contrib/test_statsmodels/test_base.py @@ -4,7 +4,7 @@ # Author: Ian Ozsvald # Created: Wed Jan 10 12:47:00 2018 -0500 # -# ID: test_base.py [] benjamin@bengfort.com $ +# ID: test_base.py [d6ebc39] benjamin@bengfort.com $ """ Tests for the statsmodels estimator wrapper. @@ -30,6 +30,7 @@ ## Test Cases ########################################################################## + @pytest.mark.skipif(sm is None, reason="test requires statsmodels") def test_stats_models_wrapper(): """ diff --git a/tests/test_datasets/__init__.py b/tests/test_datasets/__init__.py new file mode 100644 index 000000000..13a92dae8 --- /dev/null +++ b/tests/test_datasets/__init__.py @@ -0,0 +1,15 @@ +# tests.test_datasets +# Tests for the datasets module +# +# Author: Benjamin Bengfort +# Created: Thu Jul 26 14:28:14 2018 -0400 +# +# ID: __init__.py [7082742] benjamin@bengfort.com $ + +""" +Tests for the datasets module +""" + +########################################################################## +## Imports +########################################################################## diff --git a/tests/test_datasets/test_download.py b/tests/test_datasets/test_download.py index a2fc2978a..517b3ce0a 100644 --- a/tests/test_datasets/test_download.py +++ b/tests/test_datasets/test_download.py @@ -1,44 +1,59 @@ -import unittest - -import numpy as np -from sklearn.utils import Bunch - -from yellowbrick.datasets import * - - -class TestDataDownloaders(unittest.TestCase): +# tests.test_datasets.test_download +# Tests the download from S3 to ensure data is accessible. +# +# Author: Benjamin Bengfort +# Created: Tue Jan 01 15:06:05 2019 -0500 +# +# For license information, see LICENSE.txt +# +# ID: test_download.py [57aab02] ndanielsen@users.noreply.github.com $ + +""" +Tests the download from S3 to ensure data is accessible. +""" + +########################################################################## +## Imports +########################################################################## + +import pytest + +from yellowbrick.datasets.loaders import * +from yellowbrick.datasets.loaders import DATASETS +from yellowbrick.datasets.path import dataset_exists, dataset_archive + + +@pytest.mark.parametrize( + "loader", + [ + load_bikeshare, + load_concrete, + load_credit, + load_energy, + load_game, + load_mushroom, + load_occupancy, + load_spam, + load_walking, + load_hobbies, + load_nfl, + ], + ids=lambda l: l.__name__, +) +def test_loader_download(tmpdir, loader): """ - Test the dataset loading functions + Test download of dataset when it does not exist (requires Internet connection!) """ + name = loader.__name__[len("load_") :] + data_home = str(tmpdir.mkdir("datasets")) - def test_load_concrete(self): - data = load_concrete() - self.assertIsInstance(data, np.ndarray) - - def test_load_energy(self): - data = load_energy() - self.assertIsInstance(data, np.ndarray) - - def test_load_occupancy(self): - data = load_occupancy() - self.assertIsInstance(data, np.ndarray) - - def test_load_mushroom(self): - data = load_mushroom() - self.assertIsInstance(data, np.ndarray) - - def test_load_hobbies(self): - data = load_hobbies() - self.assertIsInstance(data, Bunch) - - def test_load_game(self): - data = load_game() - self.assertIsInstance(data, np.ndarray) + # The dataset should not exist + assert not dataset_exists(name, data_home=data_home) + assert not dataset_archive(name, DATASETS[name]["signature"], data_home=data_home) - def test_load_bikeshare(self): - data = load_bikeshare() - self.assertIsInstance(data, np.ndarray) + # Load the dataset + loader(data_home=data_home) - def test_load_spam(self): - data = load_spam() - self.assertIsInstance(data, np.ndarray) + # The dataset should have been downloaded + assert dataset_exists(name, data_home=data_home) + assert dataset_archive(name, DATASETS[name]["signature"], data_home=data_home) diff --git a/tests/test_datasets/test_loaders.py b/tests/test_datasets/test_loaders.py new file mode 100644 index 000000000..774d3ac16 --- /dev/null +++ b/tests/test_datasets/test_loaders.py @@ -0,0 +1,308 @@ +# tests.test_datasets.test_loaders +# Test the dataset loading utilities +# +# Author: Benjamin Bengfort +# Created: Tue Jul 31 15:34:56 2018 -0400 +# +# ID: test_loaders.py [7082742] benjamin@bengfort.com $ + +""" +Test the dataset loading utilities +""" + +########################################################################## +## Imports +########################################################################## + +import pytest +import numpy as np + +from unittest.mock import patch + +from yellowbrick.datasets.loaders import * +from yellowbrick.datasets.loaders import DATASETS +from yellowbrick.datasets.base import Dataset, Corpus +from yellowbrick.datasets.path import dataset_exists, dataset_archive +from yellowbrick.datasets.path import find_dataset_path +from yellowbrick.exceptions import DatasetsError + +try: + import pandas as pd +except ImportError: + pd = None + + +########################################################################## +## Assertion Helpers +########################################################################## + + +def assert_valid_dataset(data, name): + __tracebackhide__ = True + assert isinstance(data, Dataset), "not a Dataset object" + assert name in DATASETS, "dataset not in manifest" + + assert dataset_exists(name), "dataset directory does not exist" + assert dataset_archive( + name, DATASETS[name]["signature"] + ), "dataset archive does not match signature" + assert ( + find_dataset_path(name, ext=".csv.gz", raises=False) is not None + ), "no .csv.tgz in dataset" + assert ( + find_dataset_path(name, ext=".npz", raises=False) is not None + ), "no .npz in dataset" + + n_files = len(data.contents()) + assert n_files == 4 or n_files == 5, "not enough files in dataset" + assert len(data.README) > 0, "readme contains no data" + assert len(data.meta) > 0, "metadata is empty" + + if n_files == 5: + assert len(data.citation) > 0, "citation.bib is empty" + + assert "features" in data.meta, "no features in metadata" + assert "target" in data.meta, "no target in metadata" + + +def assert_valid_corpus(corpus, name): + __tracebackhide__ = True + assert isinstance(corpus, Corpus), "not a Corpus object" + assert name in DATASETS, "corpus not in manifest" + + assert dataset_exists(name), "corpus directory does not exist" + assert dataset_archive( + name, DATASETS[name]["signature"] + ), "corpus archive does not match signature" + + n_contents = len(corpus.contents()) + assert n_contents > 2, "not enough files/directories in corpus" + assert len(corpus.README) > 0, "readme contains no data" + assert corpus.citation is None or len(corpus.citation) > 0, "citation.bib is empty" + + +def assert_valid_pandas(data): + __tracebackhide__ = True + # Get raw data frame + df = data.to_dataframe() + assert isinstance(df, pd.DataFrame), "raw dataframe is wrong type" + + # Get pandas data + X, y = data.to_pandas() + assert isinstance(X, pd.DataFrame), "X is not a DataFrame" + assert isinstance(y, pd.Series), "y is not a Series" + + # Assert pandas is returned from to_data() + X, y = data.to_data() + assert isinstance(X, pd.DataFrame), "to_data does not return pandas" + assert isinstance(y, pd.Series), "to_data does not return pandas" + + +def assert_valid_numpy(data): + __tracebackhide__ = True + X, y = data.to_numpy() + assert isinstance(X, np.ndarray), "X is not a numpy array" + assert isinstance(y, np.ndarray), "y is not a numpy array" + assert X.ndim == 2 and y.ndim == 1, "X and y dimensions are incorrect" + + # Patch pandas and make defaults assertions + X, y = data.to_data() + assert isinstance(X, np.ndarray), "to_data does not return numpy" + assert isinstance(y, np.ndarray), "to_data does not return numpy" + + with pytest.raises(DatasetsError): + data.to_pandas(), "exception not raised when pandas unavailable" + + +########################################################################## +## Test Cases +########################################################################## + + +class TestDatasetLoaders(object): + """ + Test the dataset loading functions + + Broadly: test each of the dataset loaders to ensure that they are valid + for their particular type of dataset and that they return X and y by + default. Then test their shape to ensure that the dataset hasn't changed + unexpectedly between versions. See ``test_load_concrete`` for a sketch. + + Final tests with parametrize test all loaders against Base classes. + Make sure you scroll to the bottom and implement your loader in the + correct test batch! + """ + + def test_load_concrete(self): + """ + Test loading the concrete regression dataset + """ + # Load the type-specific dataset wrapper and validate it + data = load_concrete(return_dataset=True) + assert_valid_dataset(data, "concrete") + + # Ensure that the default returns X, y to match documentation + # Check shape to ensure no unexpected dataset changes have occured + # before we push something to PyPI! + X, y = load_concrete() + assert X.shape == (1030, 8) + assert y.shape == (1030,) + + def test_load_energy(self): + """ + Test loading the energy multi regression dataset + """ + data = load_energy(return_dataset=True) + assert_valid_dataset(data, "energy") + + X, y = load_energy() + assert X.shape == (768, 8) + assert y.shape == (768,) + + def test_load_credit(self): + """ + Test loading the credit binary classification dataset + """ + data = load_credit(return_dataset=True) + assert_valid_dataset(data, "credit") + + X, y = load_credit() + assert X.shape == (30000, 23) + assert y.shape == (30000,) + + def test_load_occupancy(self): + """ + Test loading the occupancy binary classification dataset + """ + data = load_occupancy(return_dataset=True) + assert_valid_dataset(data, "occupancy") + + X, y = load_occupancy() + assert X.shape == (20560, 5) + assert y.shape == (20560,) + + def test_load_mushroom(self): + """ + Test loading the mushroom binary classification dataset + """ + data = load_mushroom(return_dataset=True) + assert_valid_dataset(data, "mushroom") + + X, y = load_mushroom() + assert X.shape == (8123, 3) + assert y.shape == (8123,) + + def test_load_hobbies(self): + """ + Test loading the hobbies text corpus dataset + """ + corpus = load_hobbies() + assert_valid_corpus(corpus, "hobbies") + + assert len(corpus.labels) == 5 + assert len(corpus.files) == 448 + assert len(corpus.data) == 448 + assert len(corpus.target) == 448 + + def test_load_game(self): + """ + Test loading the game multiclass classification dataset + """ + data = load_game(return_dataset=True) + assert_valid_dataset(data, "game") + + X, y = load_game() + assert X.shape == (67557, 42) + assert y.shape == (67557,) + + def test_load_bikeshare(self): + """ + Test loading the bikeshare regression dataset + """ + data = load_bikeshare(return_dataset=True) + assert_valid_dataset(data, "bikeshare") + + X, y = load_bikeshare() + assert X.shape == (17379, 12) + assert y.shape == (17379,) + + def test_load_spam(self): + """ + Test loading the spam binary classification dataset + """ + data = load_spam(return_dataset=True) + assert_valid_dataset(data, "spam") + + X, y = load_spam() + assert X.shape == (4600, 57) + assert y.shape == (4600,) + + def test_load_walking(self): + """ + Test loading the walking activity clustering dataset + """ + data = load_walking(return_dataset=True) + assert_valid_dataset(data, "walking") + + X, y = load_walking() + assert X.shape == (149332, 4) + assert y.shape == (149332,) + + def test_load_nfl(self): + """ + Test loading the nfl clustering dataset + """ + data = load_nfl(return_dataset=True) + assert_valid_dataset(data, "nfl") + + X, y = load_nfl() + assert X.shape == (494, 23) + assert y.shape == (494,) + + @pytest.mark.skipif(pd is None, reason="pandas is required for this test") + @pytest.mark.parametrize( + "loader", + [ + load_bikeshare, + load_concrete, + load_credit, + load_energy, + load_game, + load_mushroom, + load_occupancy, + load_spam, + load_walking, + load_nfl, + ], + ids=lambda l: l.__name__, + ) + def test_load_pandas(self, loader): + """ + Test loading datasets as pandas objects + """ + data = loader(return_dataset=True) + assert_valid_pandas(data) + + @patch("yellowbrick.datasets.base.pd", None) + @pytest.mark.parametrize( + "loader", + [ + load_bikeshare, + load_concrete, + load_credit, + load_energy, + load_game, + load_mushroom, + load_occupancy, + load_spam, + load_walking, + load_nfl, + ], + ids=lambda l: l.__name__, + ) + def test_load_numpy(self, loader): + """ + Test loading datasets as numpy defaults + """ + data = loader(return_dataset=True) + assert_valid_numpy(data) diff --git a/tests/test_datasets/test_path.py b/tests/test_datasets/test_path.py new file mode 100644 index 000000000..0ed3c0a39 --- /dev/null +++ b/tests/test_datasets/test_path.py @@ -0,0 +1,220 @@ +# tests.test_datasets.test_paths +# Tests for the dataset path utilities +# +# Author: Benjamin Bengfort +# Created: Thu Jul 26 14:28:14 2018 -0400 +# +# ID: test_path.py [7082742] benjamin@bengfort.com $ + +""" +Tests for the dataset path utilities +""" + +########################################################################## +## Imports +########################################################################## + +import os +import pytest +import contextlib + +from yellowbrick.datasets.path import * +from yellowbrick.exceptions import DatasetsError + + +########################################################################## +## Utilities +########################################################################## + + +@contextlib.contextmanager +def environ(**env): + """ + Temporarily set the environment variables for a test, restoring them when + the test is complete (e.g. when the context manager exits). + + Parameters + ---------- + env : dict + The environment variables that should exist in context + """ + old_env = dict(os.environ) + os.environ.clear() + os.environ.update(env) + + try: + yield + finally: + os.environ.clear() + os.environ.update(old_env) + + +########################################################################## +## Test Cases +########################################################################## + + +def test_get_data_home_fixtures(): + """ + get_data_home should return fixtures by default + """ + assert get_data_home() == FIXTURES + assert os.path.exists(FIXTURES) + + +def test_get_data_home_env(tmpdir): + """ + get_data_home should return the environment variable if set + """ + path = str(tmpdir.mkdir("fixtures").join("foo")) + assert not os.path.exists(path) + + with environ(YELLOWBRICK_DATA=path): + assert get_data_home() == path + assert os.path.exists(path) + + +def test_get_data_home_specified(tmpdir): + """ + get_data_home should return a passed in path + """ + path = str(tmpdir.mkdir("fixtures").join("foo")) + assert not os.path.exists(path) + + assert get_data_home(path) == path + assert os.path.exists(path) + + +def test_find_dataset_path(tmpdir): + """ + Test find_dataset_path with a specified data_home + """ + + # Create the dataset + data_home = tmpdir.mkdir("fixtures") + foo = data_home.mkdir("foo") + + # Test the default lookup of foo/foo.csv.gz + fpath = foo.join("foo.csv.gz") + fpath.write("1,2,3") + assert find_dataset_path("foo", data_home=data_home) == fpath + + # Test the extension based lookup of foo/foo.npz + fpath = foo.join("foo.npz") + fpath.write("1234") + assert find_dataset_path("foo", data_home=data_home, ext=".npz") == fpath + + # Test the fname based lookup of foo/data.txt + fpath = foo.join("data.txt") + fpath.write("there is data in this file") + assert find_dataset_path("foo", data_home=data_home, fname="data.txt") == fpath + + +def test_missing_find_dataset_path(tmpdir): + """ + Test find_dataset_path when the dataset does not exist + """ + data_home = tmpdir.mkdir("fixtures") + + # When the data directory doesn't exist + with pytest.raises(DatasetsError): + find_dataset_path("foo", data_home=str(data_home)) + + # When the data directory exists but no file is in the directory + foo = data_home.mkdir("foo") + with pytest.raises(DatasetsError): + find_dataset_path("foo", data_home=str(data_home)) + + # When the specified file doesn't exist + fpath = foo.join("foo.csv") + fpath.write("1,2,3") + with pytest.raises(DatasetsError): + find_dataset_path("foo", data_home=str(data_home), ext=".npz") + + +def test_suppress_exception_find_dataset_path(tmpdir): + """ + Assert that find_dataset_path can suppress exceptions + """ + data_home = str(tmpdir.mkdir("fixtures")) + assert find_dataset_path("foo", data_home=data_home, raises=False) is None + + +def test_dataset_exists(tmpdir): + """ + Test the dataset_exists helper function + """ + data_home = tmpdir.mkdir("fixtures") + assert not os.path.exists(str(data_home.join("foo"))) + + # Test when directory doesn't exist + assert not dataset_exists("foo", str(data_home)) + + # Test when path exists but is file + fpath = data_home.join("foo.txt") + fpath.write("foo") + + assert not dataset_exists("foo.txt", str(data_home)) + + # Test correct case + data_home.mkdir("foo") + assert dataset_exists("foo", str(data_home)) + + +def test_dataset_archive(tmpdir): + """ + Test the dataset_archive determines if an archive is up to date + """ + sig = "49b3fc3143d727d7819fabd4365d7e7b29794089dc9fa1e5e452aeb0b33d5eda" + data_home = tmpdir.mkdir("fixtures") + + # When archive does not exist + assert not dataset_archive("foo", sig, data_home=str(data_home)) + + # Create archive + fpath = data_home.join("foo.zip") + fpath.write("this is a data archive") + + # When archive exists + assert dataset_archive("foo", sig, data_home=data_home) + + # When archive does not match signature + assert not dataset_archive("foo", "abcd", data_home=data_home) + + +def test_cleanup_dataset(tmpdir): + """ + Test cleanup_dataset removes both data dir and archive + """ + data_home = tmpdir.mkdir("fixtures") + + # Make dataset and archive + foo = data_home.mkdir("foo") + fdata = foo.join("foo.csv") + fdata.write("testing 1,2,3") + + fzip = data_home.join("foo.zip") + fzip.write("this is the archive file") + + # Make sure the files exist + assert os.path.exists(fzip) + assert os.path.exists(fdata) + + # Cleanup the dataset + cleanup_dataset("foo", data_home=data_home) + + # Files should be gone + assert not os.path.exists(fzip) + assert not os.path.exists(fdata) + + +def test_cleanup_dataset_no_data(tmpdir): + """ + Assert cleanup_dataset fails gracefully if data and archive don't exist. + """ + data_home = tmpdir.mkdir("fixtures") + cleanup_dataset("foo", data_home=str(data_home)) + + # Files should be gone + assert not os.path.exists(str(data_home.join("foo.zip"))) + assert not os.path.exists(str(data_home.join("foo"))) diff --git a/tests/test_datasets/test_signature.py b/tests/test_datasets/test_signature.py new file mode 100644 index 000000000..36fffa746 --- /dev/null +++ b/tests/test_datasets/test_signature.py @@ -0,0 +1,45 @@ +# tests.test_datasets.test_signature +# Test the sha256sum file signature library +# +# Author: Benjamin Bengfort +# Created: Tue Jul 31 14:20:10 2018 -0400 +# +# ID: test_signature.py [7082742] benjamin@bengfort.com $ + +""" +Test the sha256sum file signature library +""" + +########################################################################## +## Imports +########################################################################## + +import json +from yellowbrick.datasets.signature import sha256sum + + +########################################################################## +## Test Case +########################################################################## + +FIXTURE = { + "name": "The Cat in the Hat", + "color": "red and black", + "weather": "rainy", + "chaos_level": "HIGH", + "things": ["1", "2"], + "extra": "angry fish in bowl", +} + + +def test_signature(tmpdir): + """ + Test the SHA 256 signature of a temporary file + """ + + fpath = tmpdir.join("test.json") + json.dump(FIXTURE, fpath, indent=2) + assert ( + sha256sum(str(fpath)) + == "d10b36aa74a59bcf4a88185837f658afaf3646eff2bb16c3928d0e9335e945d2" + ) diff --git a/tests/test_draw.py b/tests/test_draw.py index 675371846..3dcca499c 100644 --- a/tests/test_draw.py +++ b/tests/test_draw.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Sun Aug 19 11:21:04 2018 -0400 # -# ID: test_draw.py [] benjamin@bengfort.com $ +# ID: test_draw.py [dd915ad] benjamin@bengfort.com $ """ Tests for the high-level drawing utility functions @@ -26,18 +26,36 @@ ## Simple tests for high-level drawing utilities ########################################################################## + def test_manual_legend_uneven_colors(): """ Raise exception when colors and labels are mismatched in manual_legend """ with pytest.raises(YellowbrickValueError, match="same number of colors as labels"): - manual_legend(None, ('a', 'b', 'c'), ('r', 'g')) + manual_legend(None, ("a", "b", "c"), ("r", "g")) + + +@pytest.fixture(scope="class") +def data(request): + + data = np.array( + [ + [4, 8, 7, 6, 5, 2, 1], + [6, 7, 9, 6, 9, 3, 6], + [5, 1, 6, 8, 4, 7, 8], + [6, 8, 1, 5, 6, 7, 4], + ] + ) + + request.cls.data = data ########################################################################## ## Visual test cases for high-level drawing utilities ########################################################################## + +@pytest.mark.usefixtures("data") class TestDraw(VisualTestCase): """ Visual tests for the high-level drawing utilities @@ -54,16 +72,92 @@ def test_manual_legend(self): Bx, By = random.normal(42, 3, 100), random.normal(44, 1, 100) Cx, Cy = random.normal(20, 10, 100), random.normal(30, 1, 100) - _, ax = plt.subplots() - ax.scatter(Ax, Ay, c='r', alpha=0.35, label='a') - ax.scatter(Bx, By, c='g', alpha=0.35, label='b') - ax.scatter(Cx, Cy, c='b', alpha=0.35, label='c') + ax.scatter(Ax, Ay, c="r", alpha=0.35, label="a") + ax.scatter(Bx, By, c="g", alpha=0.35, label="b") + ax.scatter(Cx, Cy, c="b", alpha=0.35, label="c") # Add the manual legend manual_legend( - ax, ('a', 'b', 'c'), ('r', 'g', 'b'), frameon=True, loc='upper left' + ax, ("a", "b", "c"), ("r", "g", "b"), frameon=True, loc="upper left" ) # Assert image similarity self.assert_images_similar(ax=ax) + + def test_vertical_bar_stack(self): + """ + Test bar_stack for vertical orientation + """ + _, ax = plt.subplots() + + # Plots stacked bar charts + bar_stack(self.data, ax=ax, orientation="v") + + # Assert image similarity + self.assert_images_similar(ax=ax, tol=0.1) + + def test_horizontal_bar_stack(self): + """ + Test bar_stack for horizontal orientation + """ + _, ax = plt.subplots() + # Plots stacked bar charts + bar_stack(self.data, ax=ax, orientation="h") + + # Assert image similarity + self.assert_images_similar(ax=ax, tol=0.1) + + def test_single_row_bar_stack(self): + """ + Test bar_stack for single row + """ + data = np.array([[4, 8, 7, 6, 5, 2, 1]]) + + _, ax = plt.subplots() + + # Plots stacked bar charts + bar_stack(data, ax=ax) + + # Assert image similarity + self.assert_images_similar(ax=ax, tol=0.1) + + def test_labels_vertical(self): + """ + Test labels and ticks for vertical barcharts + """ + labels = ["books", "cinema", "cooking", "gaming"] + ticks = ["noun", "verb", "adverb", "pronoun", "preposition", "digit", "other"] + _, ax = plt.subplots() + + # Plots stacked bar charts + bar_stack(self.data, labels=labels, ticks=ticks, colors=["r", "b", "g", "y"]) + + # Extract tick labels from the plot + ticks_ax = [tick.get_text() for tick in ax.xaxis.get_ticklabels()] + # Assert that ticks are set properly + assert ticks_ax == ticks + + # Assert image similarity + self.assert_images_similar(ax=ax, tol=0.05) + + def test_labels_horizontal(self): + """ + Test labels and ticks with horizontal barcharts + """ + labels = ["books", "cinema", "cooking", "gaming"] + ticks = ["noun", "verb", "adverb", "pronoun", "preposition", "digit", "other"] + _, ax = plt.subplots() + + # Plots stacked bar charts + bar_stack( + self.data, labels=labels, ticks=ticks, orientation="h", colormap="cool" + ) + + # Extract tick labels from the plot + ticks_ax = [tick.get_text() for tick in ax.yaxis.get_ticklabels()] + # Assert that ticks are set properly + assert ticks_ax == ticks + + # Assert image similarity + self.assert_images_similar(ax=ax, tol=0.05) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 000000000..ca5aeabdb --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,27 @@ +########################################################################## +## Imports +########################################################################## + +import pytest + +from yellowbrick.exceptions import NotFitted + + +########################################################################## +## NotFitted Exception Tests +########################################################################## + + +class TestExceptions(object): + """ + Test exception specific code and utilities + """ + + @pytest.mark.parametrize("method", ["transform", None]) + def test_not_fitted_from_estimator(self, method): + """ + Ensure not fitted can be raised directly from an estimator + """ + msg = "instance is not fitted yet, please call fit" + with pytest.raises(NotFitted, match=msg): + raise NotFitted.from_estimator(self, method) diff --git a/tests/test_features/__init__.py b/tests/test_features/__init__.py index d8573ef26..19d116c6a 100644 --- a/tests/test_features/__init__.py +++ b/tests/test_features/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Thu Oct 06 11:19:55 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [1d407ab] benjamin@bengfort.com $ diff --git a/tests/test_features/conftest.py b/tests/test_features/conftest.py new file mode 100644 index 000000000..9820ffb7e --- /dev/null +++ b/tests/test_features/conftest.py @@ -0,0 +1,73 @@ +# tests.test_features.conftest +# Provides fixtures for the feature tests module. +# +# Author: Naresh Bachwani +# Created: Thu Aug 15 07:35:53 2019 -0400 +# +# Copyright (C) 2016 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: conftest.py [2c5f0e9] 43993586+naresh-bachwani@users.noreply.github.com $ + +""" +Provides fixtures for the feature tests module. +""" + +########################################################################## +## Imports +########################################################################## + +import pytest + +from tests.fixtures import Dataset +from sklearn.datasets import make_classification, make_regression, make_s_curve + + +########################################################################## +## Fixtures +########################################################################## + + +@pytest.fixture(scope="class") +def discrete(request): + """ + Creates a fixture of train and test splits for the sklearn digits dataset + For ease of use returns a Dataset named tuple composed of two Split tuples. + """ + X, y = make_classification( + n_samples=400, + n_features=12, + n_informative=8, + n_redundant=0, + n_classes=5, + n_clusters_per_class=1, + class_sep=1.8, + random_state=854, + scale=[14.2, 2.1, 0.32, 0.001, 32.3, 44.1, 102.3, 2.3, 2.4, 38.2, 0.05, 1.0], + ) + + # Set a class attribute for discrete data. + request.cls.discrete = Dataset(X, y) + + +@pytest.fixture(scope="class") +def continuous(request): + """ + Creates a random regressor fixture. + """ + X, y = make_regression( + n_samples=500, n_features=22, n_informative=8, random_state=2019 + ) + + # Set a class attribute for continuous data + request.cls.continuous = Dataset(X, y) + + +@pytest.fixture(scope="class") +def s_curves(request): + """ + Creates a random regressor fixture. + """ + X, y = make_s_curve(1000, random_state=888) + # Set a class attribute for continuous data + request.cls.s_curves = Dataset(X, y) diff --git a/tests/test_features/test_base.py b/tests/test_features/test_base.py index 75f3023f4..f25abcebb 100644 --- a/tests/test_features/test_base.py +++ b/tests/test_features/test_base.py @@ -1,10 +1,10 @@ # tests.test_features.test_base # Tests for the feature selection and analysis base classes # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Oct 07 13:43:55 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_base.py [2e898a6] benjamin@bengfort.com $ @@ -17,33 +17,451 @@ ## Imports ########################################################################## -import unittest +import pytest +import numpy as np +import numpy.testing as npt -from yellowbrick.base import * +from tests.fixtures import Dataset +from yellowbrick.base import Visualizer from yellowbrick.features.base import * + from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.datasets import make_classification, make_regression + +from unittest.mock import Mock + +try: + import pandas as pd +except ImportError: + pd = None + + +########################################################################## +## Fixtures +########################################################################## + + +@pytest.fixture(scope="class") +def discrete(request): + """ + Create a random classification dataset fixture. + """ + X, y = make_classification( + n_classes=5, + n_samples=400, + n_features=12, + n_informative=10, + n_redundant=0, + random_state=2019, + ) + + # Dataset is accessible on the class so it is only generated once + request.cls.discrete = Dataset(X, y) + + +@pytest.fixture(scope="class") +def continuous(request): + """ + Creates a random regression dataset fixture. + """ + X, y = make_regression( + n_samples=500, n_features=22, n_informative=8, random_state=2019 + ) + + # Dataset is accessible on the class so it is only generated once + request.cls.continuous = Dataset(X, y) ########################################################################## ## FeatureVisualizer Base Tests ########################################################################## -class FeatureVisualizerBaseTests(unittest.TestCase): + +@pytest.mark.usefixtures("discrete") +class TestFeatureVisualizer(object): + """ + Test FeatureVisualizer base class + """ def test_subclass(self): """ - Assert the feature visualizer is in its rightful place + Check the visualizer/transformer class hierarchy """ visualizer = FeatureVisualizer() - self.assertIsInstance(visualizer, TransformerMixin) - self.assertIsInstance(visualizer, BaseEstimator) - self.assertIsInstance(visualizer, Visualizer) - - # def test_interface(self): - # """ - # Test the feature visualizer interface - # """ - # - # visualizer = FeatureVisualizer() - # with self.assertRaises(NotImplementedError): - # visualizer.poof() + assert isinstance(visualizer, TransformerMixin) + assert isinstance(visualizer, BaseEstimator) + assert isinstance(visualizer, Visualizer) + + def test_transform_returns_unmodified(self): + """ + Ensure transformer is just a passthrough + """ + X, y = self.discrete + visualizer = FeatureVisualizer().fit(X, y) + assert visualizer.transform(X, y) is X + + def test_fit_transform_poof(self): + """ + Test the fit/transform/poof quick method + """ + + class MockFeatureVisaulizer(FeatureVisualizer): + pass + + viz = MockFeatureVisaulizer() + viz.fit = Mock(return_value=viz) + viz.transform = Mock(return_value="a") + viz.poof = Mock() + + X, y = self.discrete + assert viz.fit_transform_poof(X, y, outpath="a.png", clear_figure=True) == "a" + assert viz.fit.called_once_with(X, y) + assert viz.transform.called_once_with(X, y) + assert viz.poof.called_once_with(outpath="a.png", clear_figure=True) + + +########################################################################## +## MultiFeatureVisualizer Tests +########################################################################## + + +@pytest.mark.usefixtures("discrete") +class TestMultiFeatureVisualizer(object): + """ + Test the MultiFeatureVisualizer base class + """ + + def test_subclass(self): + """ + Check the visualizer/transformer class hierarchy + """ + visualizer = MultiFeatureVisualizer() + assert isinstance(visualizer, FeatureVisualizer) + assert isinstance(visualizer, TransformerMixin) + assert isinstance(visualizer, BaseEstimator) + assert isinstance(visualizer, Visualizer) + + def test_user_supplied_features(self): + """ + Test that a user can supply feature names directly + """ + X, y = self.discrete + features = ["f{}".format(i + 1) for i in range(X.shape[1])] + oz = MultiFeatureVisualizer(features=features) + + assert not hasattr(oz, "features_") + assert oz.fit(X, y) is oz + assert hasattr(oz, "features_") + npt.assert_array_equal(oz.features_, np.asarray(features)) + + def test_numeric_features(self): + """ + Test that the features are column indices for numpy arrays + """ + X, y = self.discrete + oz = MultiFeatureVisualizer() + + assert not hasattr(oz, "features_") + assert oz.fit(X, y) is oz + assert hasattr(oz, "features_") + assert len(oz.features_) == X.shape[1] + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_pandas_string_columns(self): + """ + Ensure that DataFrame column names are uses as features + """ + X, y = self.discrete + features = ["f{}".format(i + 1) for i in range(X.shape[1])] + + X = pd.DataFrame(X, columns=features) + y = pd.Series(y, name="target") + + oz = MultiFeatureVisualizer() + assert not hasattr(oz, "features_") + assert oz.fit(X, y) is oz + assert hasattr(oz, "features_") + npt.assert_array_equal(oz.features_, np.asarray(features)) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_pandas_no_columns(self): + """ + Test when a DataFrame has no column names + """ + """ + Ensure that Pandas column names are uses as features + """ + X, y = self.discrete + + X = pd.DataFrame(X) + y = pd.Series(y, name="target") + + oz = MultiFeatureVisualizer() + assert not hasattr(oz, "features_") + assert oz.fit(X, y) is oz + assert hasattr(oz, "features_") + assert len(oz.features_) == X.shape[1] + + +########################################################################## +## DataVisualizer Tests +########################################################################## + + +@pytest.mark.usefixtures("discrete", "continuous") +class TestDataVisualizer(object): + """ + Test DataVisualizer base class + """ + + FIELDS = ( + "features_", + "classes_", + "range_", + "_colors", + "_target_color_type", + "_label_encoder", + ) + + def assert_not_fitted(self, obj): + __tracebackhide__ = True + for field in self.FIELDS: + if field.endswith("_"): + assert not hasattr(obj, field), "has {} before fit".format(field) + else: + msg = "missing internal var {}".format(field) + assert getattr(obj, field) is None, msg + + def assert_fitted(self, obj, fields=FIELDS): + __tracebackhide__ = True + # Mutually exclusive fields + if obj._target_color_type == TargetType.SINGLE: + assert (not hasattr(obj, "classes_")) and (not hasattr(obj, "range_")) + + elif obj._target_color_type == TargetType.DISCRETE: + assert hasattr(obj, "_label_encoder") + assert hasattr(obj, "classes_") and (not hasattr(obj, "range_")) + + elif obj._target_color_type == TargetType.CONTINUOUS: + assert (not hasattr(obj, "classes_")) and hasattr(obj, "range_") + + else: + raise ValueError( + "cannot test target type {}".format(obj._target_color_type) + ) + + for field in fields: + if field in {"classes_", "range_", "_label_encoder"}: + continue # handled by mutually exclusive + assert hasattr(obj, field) + + def test_single_when_none(self): + """ + Ensure that the target type is "single" when y is None + """ + X, _ = self.discrete + oz = DataVisualizer() + assert oz.target_type == TargetType.AUTO + + # Assert single when y is None + self.assert_not_fitted(oz) + assert oz.fit(X, y=None) is oz + self.assert_fitted(oz) + assert oz._target_color_type == TargetType.SINGLE + + @pytest.mark.parametrize("target_type", ("discrete", "continuous")) + def test_none_overrides_user_specified_target(self, target_type): + """ + Even if a user supplies a target type it should be overriden by y=None + """ + X, _ = getattr(self, target_type) + oz = DataVisualizer(target_type=target_type).fit(X, y=None) + + assert oz._colors == "C0" + assert oz._target_color_type == TargetType.SINGLE + + @pytest.mark.parametrize("dataset", ("discrete", "continuous")) + def test_user_overrides_auto_target(self, dataset): + """ + Ensure user specified target type overrides auto discovery + """ + X, y = getattr(self, dataset) + target_type = ( + TargetType.CONTINUOUS if dataset == "discrete" else TargetType.DISCRETE + ) + + oz = DataVisualizer(target_type=target_type) + assert oz.target_type != TargetType.AUTO + oz.fit(X, y) + assert oz.target_type == target_type + + def test_continuous(self): + """ + Test data visualizer on continuous data + """ + # Check when y is continuous + X, y = self.continuous + oz = DataVisualizer() + assert oz.target_type == TargetType.AUTO + + self.assert_not_fitted(oz) + assert oz.fit(X, y) is oz + self.assert_fitted(oz) + assert oz._target_color_type == TargetType.CONTINUOUS + assert oz.range_ == (y.min(), y.max()) + + def test_discrete(self): + """ + Test data visualizer on discrete data + """ + # Check when y is discrete + X, y = self.discrete + oz = DataVisualizer() + assert oz.target_type == TargetType.AUTO + + self.assert_not_fitted(oz) + assert oz.fit(X, y) is oz + self.assert_fitted(oz) + assert oz._target_color_type == TargetType.DISCRETE + assert len(oz.classes_) == np.unique(y).shape[0] + + def test_bad_target_type(self): + """ + Assert target type is validated on init + """ + msg = "unknown target color type 'foo'" + with pytest.raises(YellowbrickValueError, match=msg): + DataVisualizer(target_type="foo") + + def test_classes_discrete(self): + """ + Ensure classes are assigned correctly for label encoding + """ + X, y = self.discrete + classes = ["a", "b", "c", "d", "e"] + oz = DataVisualizer(classes=classes, target_type="discrete").fit(X, y) + + npt.assert_array_equal(oz.classes_, classes) + assert list(oz._colors.keys()) == classes + + def test_classes_continuous(self): + """ + Ensure classes are ignored in continuous case + """ + X, y = self.continuous + classes = ["a", "b", "c", "d", "e"] + oz = DataVisualizer(classes=classes, target_type="continuous").fit(X, y) + + assert not hasattr(oz, "classes_") + + def test_get_target_color_type(self): + """ + Test the get_target_color_type helper method + """ + oz = DataVisualizer() + + with pytest.raises(NotFitted, match="unknown target color type"): + oz.get_target_color_type() + + oz.fit(*self.continuous) + assert oz.get_target_color_type() == TargetType.CONTINUOUS + + def test_get_colors_not_fitted(self): + """ + Assert get_colors requires an fitted visualizer + """ + oz = DataVisualizer() + with pytest.raises(NotFitted, match="cannot determine colors"): + oz.get_colors(["a", "b", "c"]) + + @pytest.mark.parametrize("color, expected", [(None, "C0"), ("#F3B8AB", "#F3B8AB")]) + def test_get_colors_single(self, color, expected): + """ + Test color assignment for single target type + """ + X, y = self.discrete + oz = DataVisualizer(colors=color).fit(X) + assert oz.get_target_color_type() == TargetType.SINGLE + + # Test default colors + colors = oz.get_colors(y) + assert len(colors) == len(y) + assert np.unique(colors) == expected + + def test_get_colors_discrete(self): + """ + Test discrete colors with no label encoding + """ + X, y = self.discrete + oz = DataVisualizer().fit(X, y) + assert oz.get_target_color_type() == TargetType.DISCRETE + + colors = oz.get_colors(y) + assert len(colors) == len(y) + assert set(colors) == set(oz._colors.values()) + + def test_get_colors_discrete_classes(self): + """ + Test discrete colors with label encoding and colors + """ + X, y = self.discrete + oz = DataVisualizer( + classes=["a", "b", "c", "d", "e"], colors=["g", "r", "b", "m", "y"] + ).fit(X, y) + assert oz.get_target_color_type() == TargetType.DISCRETE + + colors = oz.get_colors(y) + assert len(colors) == len(y) + assert set(colors) == set(["g", "r", "b", "m", "y"]) + + def test_get_colors_not_label_encoded(self): + """ + Assert exception is raised on unknown class label for get_colors + """ + X, y = self.discrete + oz = DataVisualizer(classes="abcde").fit(X, y) + + with pytest.raises(YellowbrickKeyError, match="could not determine color"): + oz.get_colors(["foo"]) + + @pytest.mark.parametrize( + "colors, colormap", + [ + (["#3f78de", "#f38b33"], None), + (["b", "g", "r", "m", "y"], None), + (None, "Blues"), + ], + ) + def test_user_get_colors_discrete(self, colors, colormap): + """ + Test the ways that users can specify colors + """ + X, y = self.discrete + oz = DataVisualizer( + colors=colors, colormap=colormap, target_type="discrete" + ).fit(X, y) + + colors = oz.get_colors(y) + assert len(colors) == len(y) + + def test_get_colors_continous(self): + """ + Test continuous colors with no default colormap + """ + X, y = self.continuous + oz = DataVisualizer().fit(X, y) + assert oz.get_target_color_type() == TargetType.CONTINUOUS + + colors = oz.get_colors(y) + assert len(colors) == len(y) + + def test_get_colors_continous_cmap(self): + """ + Test continuous colors with user specified cmap + """ + X, y = self.continuous + oz = DataVisualizer(colormap="jet").fit(X, y) + assert oz.get_target_color_type() == TargetType.CONTINUOUS + + colors = oz.get_colors(y) + assert len(colors) == len(y) diff --git a/tests/test_features/test_jointplot.py b/tests/test_features/test_jointplot.py index 4182a33af..aba077396 100644 --- a/tests/test_features/test_jointplot.py +++ b/tests/test_features/test_jointplot.py @@ -1,20 +1,21 @@ # tests.test_features.test_jointplot -# Test the JointPlotVisualizer +# Test the JointPlot Visualizer # # Author: Prema Damodaran Roman # Created: Mon Apr 10 21:00:54 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers. # For license information, see LICENSE.txt # # ID: test_jointplot.py [9e008b0] pdamodaran@users.noreply.github.com $ """ -Test the JointPlotVisualizer. +Test joint plot visualization methods. These tests work differently depending on what version of matplotlib is -installed. If version 2.0.0 or greater is installed, then most tests will -execute, otherwise most will skip and only the warning will be tested. +installed. If version 2.0.2 or greater is installed, then most tests will +execute, otherwise the histogram tests will skip and only the warning will +be tested. """ ########################################################################## @@ -23,117 +24,471 @@ import sys import pytest -import warnings import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from tests.dataset import DatasetMixin -from tests.base import VisualTestCase +from functools import partial +from unittest.mock import patch, MagicMock + +from sklearn.datasets import make_classification, make_regression + +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase +from yellowbrick.exceptions import YellowbrickValueError from yellowbrick.features.jointplot import * +from ..fixtures import Dataset + +try: + # Only available in Matplotlib >= 2.0.2 + from mpl_toolkits.axes_grid1 import make_axes_locatable +except ImportError: + make_axes_locatable = None + +try: + import pandas as pd +except ImportError: + pd = None + + +########################################################################## +## Fixtures +########################################################################## + +# Random numpy array generators +rand1d = partial(np.random.rand, 120) +rand2col = partial(np.random.rand, 120, 2) +rand3col = partial(np.random.rand, 120, 3) + + +@pytest.fixture(scope="class") +def discrete(request): + """ + Creates a simple 2-column dataset with a discrete target. + """ + X, y = make_classification( + n_samples=120, + n_features=2, + n_informative=2, + n_redundant=0, + n_classes=3, + n_clusters_per_class=1, + random_state=2221, + ) + + request.cls.discrete = Dataset(X, y) + + +@pytest.fixture(scope="class") +def continuous(request): + """ + Creates a simple 2-column dataset with a continuous target. + """ + X, y = make_regression(n_samples=120, n_features=2, random_state=1112) + + request.cls.continuous = Dataset(X, y) + ########################################################################## -## JointPlotVisualizer Tests +## JointPlot Tests ########################################################################## -# Determine version of matplotlib -MPL_VERS_MAJ = int(mpl.__version__.split(".")[0]) +@pytest.mark.usefixtures("discrete", "continuous") +class TestJointPlotNoHistogram(VisualTestCase): + """ + Test the JointPlot visualizer without histograms + """ -class JointPlotTests(VisualTestCase, DatasetMixin): + def test_invalid_columns_values(self): + """ + Assert invalid columns arguments raise exception + """ + with pytest.raises(YellowbrickValueError, match="invalid for joint plot"): + JointPlot(columns=["a", "b", "c"], hist=False) + + def test_invalid_correlation_values(self): + """ + Assert invalid correlation arguments raise an exception + """ + with pytest.raises(YellowbrickValueError, match="invalid correlation method"): + JointPlot(correlation="foo", hist=False) - X = np.array([1, 2, 3, 5, 8, 10]) + def test_invalid_kind_values(self): + """ + Assert invalid kind arguments raise exception + """ + for bad_kind in ("foo", None, 123): + with pytest.raises(YellowbrickValueError, match="invalid joint plot kind"): + JointPlot(kind=bad_kind, hist=False) - y = np.array([1, 3, 6, 2, 9, 2]) + def test_invalid_hist_values(self): + """ + Assert invalid hist arguments raise exception + """ + for bad_hist in ("foo", 123): + with pytest.raises( + YellowbrickValueError, match="invalid argument for hist" + ): + JointPlot(hist=bad_hist) - def setUp(self): - self.concrete = self.load_data('concrete') + def test_no_haxes(self): + """ + Test that xhax and yhax are not available + """ + oz = JointPlot(hist=False) + with pytest.raises(AttributeError, match="histogram for the X axis"): + oz.xhax - def tearDown(self): - self.concrete = None + with pytest.raises(AttributeError, match="histogram for the Y axis"): + oz.yhax - @pytest.mark.skipif(MPL_VERS_MAJ > 1, reason="requires matplotlib 1.5.3 or less") - def test_warning(self): + @patch("yellowbrick.features.jointplot.plt") + def test_correlation(self, mplt): """ - Ensure that the jointplot warns if mpl version is < 2.0.0 + Test correlation is correctly computed """ - # Note Python 3.2+ has a self.assertWarns ... but we need to be - # Python 2.7 compatible, so we're going to do this. - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") + x = self.discrete.X[:, 0] + y = self.discrete.X[:, 1] - # Trigger a warning. - JointPlotVisualizer() + cases = ( + ("pearson", -0.3847799883805261), + ("spearman", -0.37301201472324463), + ("covariance", -0.5535440619953924), + ("kendalltau", -0.2504201680672269), + ) + + for alg, expected in cases: + oz = JointPlot(hist=False, correlation=alg, columns=None) + oz.ax = MagicMock() + oz.fit(x, y) + + assert hasattr(oz, "corr_") + assert oz.corr_ == pytest.approx( + expected + ), "{} not computed correctly".format(alg) + + def test_columns_none_invalid_x(self): + """ + When self.columns=None validate X and y + """ + bad_kws = ( + {"X": rand1d(), "y": None}, + {"X": rand3col(), "y": None}, + {"X": rand2col(), "y": rand1d()}, + {"X": rand3col(), "y": rand1d()}, + {"X": rand1d(), "y": rand2col()}, + ) + + for kws in bad_kws: + oz = JointPlot(columns=None, hist=False) + with pytest.raises( + YellowbrickValueError, match="when self.columns is None" + ): + oz.fit(**kws) + + def test_columns_none_x_y(self): + """ + When self.columns=None image similarity with valid X and y + """ + oz = JointPlot(hist=False, columns=None) + assert oz.fit(self.discrete.X[:, 0], self.discrete.y) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + # Appveyor and Linux conda fail due to non-text-based differences + self.assert_images_similar(oz, tol=2.5) + + def test_columns_none_x(self): + """ + When self.columns=None image similarity with valid X, no y + """ + oz = JointPlot(hist=False, columns=None) + assert oz.fit(self.discrete.X) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + tol = ( + 4.0 if sys.platform == "win32" else 0.01 + ) # Fails on AppVeyor with RMS 3.941 + self.assert_images_similar(oz, tol=tol) + + def test_columns_single_index_no_y(self): + """ + When self.columns=int or str y must not be None + """ + oz = JointPlot(columns="foo", hist=False) + with pytest.raises(YellowbrickValueError, match="y must be specified"): + oz.fit(rand2col(), y=None) + + def test_columns_single_invalid_index_numpy(self): + """ + When self.columns=int validate the index in X + """ + oz = JointPlot(columns=2, hist=False) + with pytest.raises(IndexError, match="could not index column '2' into type"): + oz.fit(self.continuous.X, self.continuous.y) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_columns_single_invalid_index_pandas(self): + """ + When self.columns=str validate the index in X + """ + oz = JointPlot(columns="foo", hist=False) + X = pd.DataFrame(self.continuous.X, columns=["a", "b"]) + y = pd.Series(self.continuous.y) + + with pytest.raises(IndexError, match="could not index column 'foo' into type"): + oz.fit(X, y) + + def test_columns_single_int_index_numpy(self): + """ + When self.columns=int image similarity on numpy dataset + """ + oz = JointPlot(columns=1, hist=False) + assert oz.fit(self.continuous.X, self.continuous.y) is oz + assert hasattr(oz, "corr_") - # Ensure that a warning occurred - self.assertEqual(len(w), 1) - self.assertEqual( - str(w[-1].message), - "JointPlotVisualizer requires matplotlib major version 2 " - "or greater. Please upgrade." - ) + oz.finalize() + # Appveyor and Linux conda failed based on non-text-based differences + self.assert_images_similar(oz, tol=5) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_columns_single_str_index_pandas(self): + """ + When self.columns=str image similarity on pandas dataset + """ + oz = JointPlot(columns="a", hist=False) + X = pd.DataFrame(self.continuous.X, columns=["a", "b"]) + y = pd.Series(self.continuous.y) + assert oz.fit(X, y) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + # Appveyor and Linux conda failed based on non-text-based differences + self.assert_images_similar(oz, tol=5.5) + + def test_columns_double_int_index_numpy_no_y(self): + """ + When self.columns=[int, int] image similarity on numpy dataset no y + """ + oz = JointPlot(columns=[0, 1], hist=False) + assert oz.fit(self.discrete.X, y=None) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + tol = ( + 4.0 if sys.platform == "win32" else 0.01 + ) # Fails on AppVeyor with RMS 3.941 + self.assert_images_similar(oz, tol=tol) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_columns_double_str_index_pandas_no_y(self): + """ + When self.columns=[str, str] image similarity on pandas dataset no y + """ + oz = JointPlot(columns=["a", "b"], hist=False) + X = pd.DataFrame(self.continuous.X, columns=["a", "b"]) + assert oz.fit(X, y=None) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + tol = ( + 4.0 if sys.platform == "win32" else 0.01 + ) # Fails on AppVeyor with RMS 3.911 + self.assert_images_similar(oz, tol=tol) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_columns_double_index_discrete_y(self): + """ + When self.columns=[str, str] on DataFrame with discrete y + """ + oz = JointPlot(columns=["a", "b"], hist=False) + X = pd.DataFrame(self.discrete.X, columns=["a", "b"]) + y = pd.Series(self.discrete.y) + assert oz.fit(X, y) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + tol = ( + 4.0 if sys.platform == "win32" else 0.01 + ) # Fails on AppVeyor with RMS 3.940 + self.assert_images_similar(oz, tol=tol) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_columns_double_index_continuous_y(self): + """ + When self.columns=[str, str] on DataFrame with continuous y + """ + oz = JointPlot(columns=["a", "b"], hist=False) + X = pd.DataFrame(self.continuous.X, columns=["a", "b"]) + y = pd.Series(self.continuous.y) + assert oz.fit(X, y) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + tol = ( + 4.0 if sys.platform == "win32" else 0.01 + ) # Fails on AppVeyor with RMS 3.911 + self.assert_images_similar(oz, tol=tol) + + +@pytest.mark.skipif( + make_axes_locatable is not None, reason="requires matplotlib <= 2.0.1" +) +def test_matplotlib_version_error(): + """ + Assert an exception is raised with incompatible matplotlib versions + """ + with pytest.raises(YellowbrickValueError): + JointPlot(hist=True) + + +@patch("yellowbrick.features.jointplot.make_axes_locatable", None) +def test_matplotlib_incompatibility(): + """ + Assert an exception is raised if make_axes_locatable is None + """ + with pytest.raises(YellowbrickValueError): + JointPlot(hist=True) + + +@pytest.mark.usefixtures("discrete", "continuous") +@pytest.mark.skipif(make_axes_locatable is None, reason="requires matplotlib >= 2.0.2") +class TestJointPlotHistogram(VisualTestCase): + """ + Test the JointPlot visualizer with histograms + """ + + def test_haxes_available(self): + """ + Test that xhax and yhax are available + """ + oz = JointPlot(hist=True) + assert oz.xhax is not None + assert oz.yhax is not None @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) - @pytest.mark.skipif(MPL_VERS_MAJ < 2, reason="requires matplotlib 2.0.0 or greater") - @pytest.mark.filterwarnings("ignore:internal gelsd driver") - def test_jointplot_has_no_errors(self): + def test_columns_none_x_y_hist(self): """ - Assert no errors occur during jointplot visualizer integration + When self.columns=None image similarity with valid X and y """ - fig = plt.figure() - ax = fig.add_subplot() + oz = JointPlot(hist=True, columns=None) + assert oz.fit(self.discrete.X[:, 0], self.discrete.y) is oz + assert hasattr(oz, "corr_") - visualizer = JointPlotVisualizer(ax=ax) - visualizer.fit(self.X, self.y) + oz.finalize() + self.assert_images_similar(oz) + + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_columns_none_x_hist(self): + """ + When self.columns=None image similarity with valid X, no y + """ + oz = JointPlot(hist=True, columns=None) + assert oz.fit(self.discrete.X) is oz + assert hasattr(oz, "corr_") - self.assert_images_similar(visualizer, tol=10) + oz.finalize() + self.assert_images_similar(oz) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) - @pytest.mark.skipif(MPL_VERS_MAJ < 2, reason="requires matplotlib 2.0.0 or greater") - def test_jointplot_integrated_has_no_errors(self): + def test_columns_single_int_index_numpy_hist(self): """ - Test jointplot on the concrete data set + When self.columns=int image similarity on numpy dataset """ + oz = JointPlot(columns=1, hist=True) + assert oz.fit(self.continuous.X, self.continuous.y) is oz + assert hasattr(oz, "corr_") - fig = plt.figure() - ax = fig.add_subplot() + oz.finalize() + self.assert_images_similar(oz) - # Load the data from the fixture - X = self.concrete['cement'] - y = self.concrete['strength'] - feature = 'cement' - target = 'strength' + @pytest.mark.skipif(pd is None, reason="test requires pandas") + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_columns_single_str_index_pandas_hist(self): + """ + When self.columns=str image similarity on pandas dataset + """ + oz = JointPlot(columns="a", hist=True) + X = pd.DataFrame(self.continuous.X, columns=["a", "b"]) + y = pd.Series(self.continuous.y) + assert oz.fit(X, y) is oz + assert hasattr(oz, "corr_") - # Test the visualizer - visualizer = JointPlotVisualizer( - feature=feature, target=target, joint_plot="hex", ax=ax) - visualizer.fit(X, y) + oz.finalize() + self.assert_images_similar(oz) - self.assert_images_similar(visualizer, tol=15) + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_columns_double_int_index_numpy_no_y_hist(self): + """ + When self.columns=[int, int] image similarity on numpy dataset no y + """ + oz = JointPlot(columns=[0, 1], hist=True) + assert oz.fit(self.discrete.X, y=None) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + self.assert_images_similar(oz) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_columns_double_str_index_pandas_no_y_hist(self): + """ + When self.columns=[str, str] image similarity on pandas dataset no y + """ + oz = JointPlot(columns=["a", "b"], hist=True) + X = pd.DataFrame(self.continuous.X, columns=["a", "b"]) + assert oz.fit(X, y=None) is oz + assert hasattr(oz, "corr_") + + oz.finalize() + self.assert_images_similar(oz) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_columns_double_index_discrete_y_hist(self): + """ + When self.columns=[str, str] on DataFrame with discrete y + """ + oz = JointPlot(columns=["a", "b"], hist=True) + X = pd.DataFrame(self.discrete.X, columns=["a", "b"]) + y = pd.Series(self.discrete.y) + assert oz.fit(X, y) is oz + assert hasattr(oz, "corr_") + oz.finalize() + self.assert_images_similar(oz) - @pytest.mark.skipif(MPL_VERS_MAJ < 2, reason="requires matplotlib 2.0.0 or greater") - def test_jointplot_no_matplotlib2_warning(self): + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_columns_double_index_continuous_y_hist(self): """ - Assert no UserWarning occurs if matplotlib major version >= 2 + When self.columns=[str, str] on DataFrame with continuous y """ - with warnings.catch_warnings(record=True) as ws: - # Filter on UserWarnings - warnings.filterwarnings("always", category=UserWarning) - visualizer = JointPlotVisualizer() - visualizer.fit(self.X, self.y) - visualizer.finalize() + oz = JointPlot(columns=["a", "b"], hist=True) + X = pd.DataFrame(self.continuous.X, columns=["a", "b"]) + y = pd.Series(self.continuous.y) + assert oz.fit(X, y) is oz + assert hasattr(oz, "corr_") - # Filter out user warnings not related to matplotlib version - ver_warn_msg = "requires matplotlib major version 2 or greater" - mpl_ver_cnt = 0 - for w in ws: - if w and w.message and ver_warn_msg in str(w.message): - mpl_ver_cnt += 1 - self.assertEqual(0, mpl_ver_cnt, ws[-1].message \ - if ws else "No error") + oz.finalize() + # Appveyor and Linux conda failed due to non-text-based differences + self.assert_images_similar(oz, tol=4.0) diff --git a/tests/test_features/test_manifold.py b/tests/test_features/test_manifold.py index f9850c39e..499f44fff 100644 --- a/tests/test_features/test_manifold.py +++ b/tests/test_features/test_manifold.py @@ -4,7 +4,10 @@ # Author: Benjamin Bengfort # Created: Sat May 12 11:24:41 2018 -0400 # -# ID: test_manifold.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_manifold.py [02f8c27] benjamin@bengfort.com $ """ Tests for the Manifold High Dimensional Visualizations @@ -18,21 +21,18 @@ from yellowbrick.features.manifold import * from yellowbrick.utils.types import is_estimator -from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.features.base import TargetType +from yellowbrick.exceptions import YellowbrickValueError, ModelError, NotFitted from sklearn.pipeline import Pipeline from sklearn.decomposition import PCA +from sklearn.datasets import make_blobs +from sklearn.datasets import make_s_curve from sklearn.manifold import LocallyLinearEmbedding -from sklearn.datasets.samples_generator import make_s_curve -from sklearn.datasets import make_classification, make_regression, make_blobs +from unittest.mock import patch from tests.base import VisualTestCase -try: - from unittest.mock import patch -except ImportError: - from mock import patch - try: import pandas as pd except ImportError: @@ -43,35 +43,55 @@ ## Manifold Visualizer Tests ########################################################################## + +@pytest.mark.usefixtures("s_curves", "discrete", "continuous") class TestManifold(VisualTestCase): """ Test Manifold visualizer """ - def test_manifold_construction(self): + @pytest.mark.parametrize( + "algorithm", + ["lle", "ltsa", "hessian", "modified", "isomap", "mds", "spectral", "tsne"], + ) + def test_manifold_construction(self, algorithm): """ Should be able to construct a manifold estimator from a string """ - # TODO: parametrize this once unittest.TestCase dependency removed. - algorithms = [ - "lle", "ltsa", "hessian", "modified", - "isomap", "mds", "spectral", "tsne", - ] - - for algorithm in algorithms: - message = "case failed for {}".format(algorithm) - params = { - "n_neighbors": 18, - "random_state": 53, - } - oz = Manifold(manifold=algorithm, **params) - assert is_estimator(oz.manifold), message - assert oz.manifold.get_params()["n_components"] == 2, message - - manifold_params = oz.manifold.get_params() - for param, value in params.items(): - if param in manifold_params: - assert value == manifold_params[param], message + message = "case failed for {}".format(algorithm) + params = {"n_neighbors": 18, "random_state": 53} + oz = Manifold(manifold=algorithm, **params) + assert is_estimator(oz.manifold), message + assert oz.manifold.get_params()["n_components"] == 2, message + + manifold_params = oz.manifold.get_params() + for param, value in params.items(): + if param in manifold_params: + assert value == manifold_params[param], message + + @pytest.mark.parametrize( + "algorithm", ["lle", "ltsa", "hessian", "modified", "isomap", "spectral"] + ) + def test_manifold_warning(self, algorithm): + """ + Should raise a warning if n_neighbors not specified + """ + message = "case failed for {}".format(algorithm) + n_neighbors = 6 if algorithm == "hessian" else 5 + + with pytest.warns(YellowbrickWarning): + oz = Manifold(manifold=algorithm) + assert oz.n_neighbors == n_neighbors, message + + @pytest.mark.parametrize("algorithm", ["mds", "tsne"]) + def test_manifold_no_warning(self, algorithm): + """ + Should not raise a warning if n_neighbors not specified + """ + message = "case failed for {}".format(algorithm) + + with pytest.warns(None) as record: + assert not record.list, message def test_bad_manifold_exception(self): """ @@ -84,186 +104,213 @@ def test_manifold_instance_construction(self): """ Should allow a sklearn.Estimator object to be set as manifold """ - manifold = Pipeline([ - ('pca', PCA(n_components=50)), - ('lle', LocallyLinearEmbedding(n_components=2)), - ]) + manifold = Pipeline( + [ + ("pca", PCA(n_components=50)), + ("lle", LocallyLinearEmbedding(n_components=2)), + ] + ) oz = Manifold(manifold=manifold) assert oz.manifold is manifold - @patch('yellowbrick.features.manifold.Manifold.fit_transform', spec=True) - def test_manifold_fit(self, mock_fit_transform): + @pytest.mark.filterwarnings("ignore:Conversion of the second argument") + @pytest.mark.parametrize( + "algorithm", ["lle", "ltsa", "hessian", "modified", "isomap"] + ) + def test_manifold_algorithm_transform_fit(self, algorithm): """ - Test manifold fit method + Test manifold fit with algorithms having transform implemented """ - X, y = make_s_curve(1000, random_state=888) - manifold = Manifold(target="auto") + X, y = make_s_curve(1000, random_state=94) + with pytest.warns(YellowbrickWarning): + manifold = Manifold(manifold=algorithm, target="auto") assert manifold.fit(X, y) is manifold, "fit did not return self" - mock_fit_transform.assert_called_once() - @patch('yellowbrick.features.manifold.Manifold.draw', spec=True) - def test_manifold_fit_transform(self, mock_draw): + @pytest.mark.filterwarnings("ignore:Conversion of the second argument") + @pytest.mark.parametrize("algorithm", ["mds", "spectral", "tsne"]) + def test_manifold_algorithm_no_transform_fit(self, algorithm): + """ + Test manifold fit with algorithms not having transform implemented + """ + X, y = self.s_curves + msg = "requires data to be simultaneously fit and transformed" + oz = Manifold(manifold=algorithm, n_neighbors=10, random_state=223) + with pytest.raises(ModelError, match=msg): + oz.fit(X) + + @patch("yellowbrick.features.manifold.Manifold.draw", spec=True) + @pytest.mark.parametrize("projection", [2, 3]) + def test_manifold_fit_transform(self, mock_draw, projection): """ Test manifold fit_transform method """ - X, y = make_s_curve(1000, random_state=888) - manifold = Manifold(target="auto") + X, y = self.s_curves + manifold = Manifold(target="auto", projection=projection) - assert not hasattr(manifold, 'fit_time_') + assert not hasattr(manifold, "fit_time_") Xp = manifold.fit_transform(X, y) - assert Xp.shape == (X.shape[0], 2) + assert Xp.shape == (X.shape[0], projection) mock_draw.assert_called_once() - assert hasattr(manifold, 'fit_time_') - assert manifold._target_color_type == CONTINUOUS + assert hasattr(manifold, "fit_time_") + assert manifold._target_color_type == TargetType.CONTINUOUS - @pytest.mark.filterwarnings("ignore:Conversion of the second argument") - def test_manifold_classification(self): + @patch("yellowbrick.features.manifold.Manifold.fit_transform", spec=True) + @patch("yellowbrick.features.manifold.Manifold.draw", spec=True) + @pytest.mark.parametrize("projection", [2, 3]) + def test_manifold_transform(self, mock_draw, mock_fit_transform, projection): """ - Image similarity test for classification dataset (discrete y) + Test manifold transform method """ - X, y = make_classification( - n_samples=300, n_features=7, n_informative=4, n_redundant=2, - n_classes=4, n_clusters_per_class=2, random_state=78 + X, y = self.s_curves + manifold = Manifold( + manifold="lle", target="auto", n_neighbors=5, projection=projection ) - oz = Manifold(manifold="spectral", target="discrete", random_state=108) - assert not hasattr(oz, 'classes_') + manifold.fit(X, y) + Xp = manifold.transform(X, y) + assert Xp.shape == (X.shape[0], projection) - oz.fit(X, y) - - assert hasattr(oz, 'classes_') - assert not hasattr(oz, 'range_') - self.assert_images_similar(oz, tol=0.5) + mock_draw.assert_called_once() - def test_manifold_regression(self): + def test_manifold_no_transform(self): """ - Image similarity test for regression dataset (continuous y) + Test the exception when manifold doesn't implement transform. """ - X, y = make_regression( - n_samples=300, n_features=7, n_informative=4, random_state=87 - ) - - oz = Manifold(manifold="tsne", target="continuous", random_state=1) - assert not hasattr(oz, 'range_') + X, _ = self.s_curves + manifold = Manifold(manifold="lle", n_neighbors=5, target="auto") - oz.fit(X, y) + msg = "instance is not fitted yet, please call fit" + with pytest.raises(NotFitted, match=msg): + manifold.transform(X) - assert not hasattr(oz, 'classes_') - assert hasattr(oz, 'range_') - self.assert_images_similar(oz, tol=1.5) + @patch("yellowbrick.features.manifold.Manifold.fit", spec=True) + @pytest.mark.parametrize("manifolds", ["mds", "spectral", "tsne"]) + def test_manifold_assert_no_transform(self, mock_fit, manifolds): + """ + Assert that transform raises error when MDS, TSNE or Spectral Embedding algorithms are used. + """ + X, _ = self.s_curves + manifold = Manifold(manifold=manifolds, target="auto", n_neighbors=10) + mock_fit(X) + msg = "requires data to be simultaneously fit and transformed" + with pytest.raises(ModelError, match=msg): + manifold.transform(X) - def test_manifold_single(self): + @pytest.mark.filterwarnings("ignore:Conversion of the second argument") + def test_manifold_classification(self): """ - Image similarity test for simple dataset (no y) + Image similarity test for classification dataset (discrete y) """ - X, _ = make_blobs( - n_samples=300, n_features=7, centers=3, random_state=1112, + X, y = self.discrete + + oz = Manifold( + manifold="spectral", target="discrete", n_neighbors=5, random_state=108 ) + assert not hasattr(oz, "classes_") - oz = Manifold(manifold="mds", random_state=139973) - oz.fit(X) + oz.fit_transform(X, y) - self.assert_images_similar(oz, tol=5.0) + assert hasattr(oz, "classes_") + assert not hasattr(oz, "range_") + self.assert_images_similar(oz, tol=0.5) - @pytest.mark.skipif(pd is None, reason="requires pandas") - def test_manifold_pandas(self): + def test_manifold_classification_3d(self): """ - Test manifold on a dataset made up of a pandas DataFrame and Series + Image similarity test for classification dataset (discrete y) """ - X, y = make_s_curve(200, random_state=888) - - X = pd.DataFrame(X) - y = pd.Series(y) + X, y = self.discrete oz = Manifold( - manifold='ltsa', colors='nipy_spectral', - target='continuous', random_state=223 - ).fit(X, y) + manifold="spectral", + target="discrete", + n_neighbors=5, + random_state=108, + projection=3, + ) - # TODO: find a way to decrease this tolerance - self.assert_images_similar(oz, tol=35) + assert not hasattr(oz, "classes_") - @pytest.mark.filterwarnings("ignore:Conversion of the second argument") - def test_manifold_algorithm_fit(self): + oz.fit_transform(X, y) + + assert hasattr(oz, "classes_") + assert not hasattr(oz, "range_") + self.assert_images_similar(oz) + + def test_manifold_regression(self): """ - Test that all algorithms can be fitted correctly + Image similarity test for regression dataset (continuous y) """ - # TODO: parametrize this once unittest.TestCase dependency removed. - algorithms = [ - "lle", "ltsa", "hessian", "modified", - "isomap", "mds", "spectral", "tsne", - ] + X, y = self.continuous - X, y = make_s_curve(200, random_state=888) + oz = Manifold(manifold="tsne", target="continuous", random_state=1) + assert not hasattr(oz, "range_") - for algorithm in algorithms: - oz = Manifold(manifold=algorithm, random_state=223) - oz.fit(X, y) + oz.fit_transform(X, y) + oz.finalize() + assert not hasattr(oz, "classes_") + assert hasattr(oz, "range_") + self.assert_images_similar(oz, tol=1.5) - def test_determine_target_color_type(self): + def test_manifold_regression_3d(self): """ - Check that the target type is determined by a value y + Image similarity test for regression dataset (continuous y) """ - manifold = Manifold() - - # Check default is auto - assert manifold.target == AUTO + X, y = self.continuous - # Assert single when y is None - manifold._determine_target_color_type(None) - assert manifold._target_color_type == SINGLE + oz = Manifold( + manifold="tsne", target="continuous", random_state=1, projection=3 + ) + assert not hasattr(oz, "range_") - # Check when y is continuous - y = np.random.rand(100) - manifold._determine_target_color_type(y) - assert manifold._target_color_type == CONTINUOUS + oz.fit_transform(X, y) + oz.finalize() + oz.cbar.set_ticks([]) + assert not hasattr(oz, "classes_") + assert hasattr(oz, "range_") + self.assert_images_similar(oz, tol=15) - # Check when y is discrete - y = np.random.choice(['a', 'b', 'c', 'd'], 100) - manifold._determine_target_color_type(y) - assert manifold._target_color_type == DISCRETE + def test_manifold_single(self): + """ + Image similarity test for simple dataset (no y) + """ + X, _ = make_blobs(n_samples=300, n_features=7, centers=3, random_state=1112) - # Check when default is set to continuous and discrete data passed in - manifold = Manifold(target=CONTINUOUS) - y = np.random.choice(['a', 'b', 'c', 'd'], 100) - manifold._determine_target_color_type(y) - assert manifold._target_color_type == CONTINUOUS + oz = Manifold(manifold="mds", random_state=139973) + oz.fit_transform(X) - # Check when default is set to discrete and continuous data passed in - manifold = Manifold(target=DISCRETE) - y = np.random.rand(100) - manifold._determine_target_color_type(y) - assert manifold._target_color_type == DISCRETE + self.assert_images_similar(oz) - # None overrides specified target - manifold = Manifold(target=CONTINUOUS) - manifold._determine_target_color_type(None) - assert manifold._target_color_type == SINGLE + def test_manifold_single_3d(self): + """ + Image similarity test for simple dataset (no y) + """ + X, _ = make_blobs(n_samples=300, n_features=7, centers=3, random_state=1112) - # None overrides specified target - manifold = Manifold(target=DISCRETE) - manifold._determine_target_color_type(None) - assert manifold._target_color_type == SINGLE + oz = Manifold(manifold="mds", random_state=139973, projection=3) + oz.fit_transform(X) - # Bad target raises exception - # None overrides specified target - manifold = Manifold(target="foo") - msg = "could not determine target color type" - with pytest.raises(YellowbrickValueError, match=msg): - manifold._determine_target_color_type([]) + self.assert_images_similar(oz) - def test_manifold_no_transform(self): + @pytest.mark.skipif(pd is None, reason="requires pandas") + def test_manifold_pandas(self): """ - Test the exception when manifold doesn't implement transform. + Test manifold on a dataset made up of a pandas DataFrame and Series """ - X, _ = make_s_curve(1000, random_state=888) - manifold = Manifold(manifold='mds', target="auto") + X, y = self.s_curves - assert not hasattr(manifold._manifold, 'transform') - - with pytest.raises(AttributeError, match="try using fit_transform instead"): - manifold.transform(X) + oz = Manifold( + manifold="ltsa", + colormap="nipy_spectral", + n_neighbors=10, + target="continuous", + random_state=223, + ) + oz.fit_transform(X, y) + oz.finalize() + oz.cbar.set_ticks([]) + # TODO: find a way to decrease this tolerance + self.assert_images_similar(oz, tol=40) diff --git a/tests/test_features/test_pca.py b/tests/test_features/test_pca.py index ed56753f4..ee8242230 100644 --- a/tests/test_features/test_pca.py +++ b/tests/test_features/test_pca.py @@ -2,11 +2,14 @@ # tests.test_features.test_pca # Tests for the PCA based feature visualizer. # -# Author: Carlo Morales <@cjmorale> -# Author: Raúl Peralta Lozada <@RaulPL> -# Author: Benjamin Bengfort <@bbengfort> +# Author: Carlo Morales +# Author: Raúl Peralta Lozada +# Author: Benjamin Bengfort # Created: Tue May 23 18:34:27 2017 -0400 # +# Copyright (C) 2017 The scikit-yb developers. +# For license information, see LICENSE.txt +# # ID: test_pca.py [] cmorales@pacificmetrics.com $ """ @@ -20,171 +23,342 @@ import sys import pytest import numpy as np +import numpy.testing as npt + +from unittest import mock +from tests.base import VisualTestCase, IS_WINDOWS_OR_CONDA -from tests.dataset import Dataset -from tests.base import VisualTestCase from yellowbrick.features.pca import * -from yellowbrick.exceptions import YellowbrickError -from sklearn.datasets import make_classification +from yellowbrick.exceptions import YellowbrickError, NotFitted + +# Note: this can be removed when we deprecate mpl in #826 +try: + # Only available in Matplotlib >= 2.0.2 + from mpl_toolkits.axes_grid1 import make_axes_locatable +except ImportError: + make_axes_locatable = None ########################################################################## -## Fixtures +# PCA Tests ########################################################################## -@pytest.fixture(scope='class') -def binary(request): + +@pytest.mark.usefixtures("discrete", "continuous") +class TestPCA(VisualTestCase): """ - Creates a fixture of train and test splits for the sklearn digits dataset - For ease of use returns a Dataset named tuple composed of two Split tuples. + Test the PCA visualizer """ - X, y = make_classification( - n_samples=400, n_features=12, n_informative=8, n_redundant=0, - n_classes=2, n_clusters_per_class=1, class_sep=1.8, random_state=854, - scale=[14.2, 2.1, 0.32, 0.001, 32.3, 44.1, 102.3, 2.3, 2.4, 38.2, 0.05, 1.0], - ) - # Set a class attribute for digits - request.cls.dataset = Dataset(X, y) + def test_single(self): + """ + Test single target. + """ + visualizer = PCA(random_state=1998) + visualizer.fit(self.continuous.X) + visualizer.transform(self.continuous.X) + assert not hasattr(visualizer, "classes_") + assert not hasattr(visualizer, "range_") + self.assert_images_similar(visualizer) + @pytest.mark.xfail(IS_WINDOWS_OR_CONDA, reason="RMS of 10.205 on miniconda") + def test_continuous(self): + """ + Test continuous target + """ + visualizer = PCA(colormap="YlOrRd", random_state=2019) + assert not hasattr(visualizer, "range_") + visualizer.fit(*self.continuous) + visualizer.transform(*self.continuous) + assert hasattr(visualizer, "range_") + assert not hasattr(visualizer, "classes_") + visualizer.finalize() -########################################################################## -##PCA Tests -########################################################################## + visualizer.cax.set_yticklabels([]) -@pytest.mark.usefixtures("binary") -class PCADecompositionTests(VisualTestCase): - """ - Test the PCADecomposition visualizer - """ + # AppVeyor tests fail with RMS 10.085 + self.assert_images_similar(visualizer, windows_tol=10.5) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=4)" - ) + def test_discrete(self): + """ + Test discrete target. + """ + classes = ["a", "b", "c", "d", "e"] + colors = ["r", "b", "g", "m", "c"] + + visualizer = PCA(colors=colors, classes=classes, random_state=83) + assert not hasattr(visualizer, "classes_") + visualizer.fit(*self.discrete) + assert hasattr(visualizer, "classes_") + assert not hasattr(visualizer, "range_") + visualizer.transform(*self.discrete) + + # Make sure that classes are set correctly. + npt.assert_array_equal(visualizer.classes_, classes) + + self.assert_images_similar(visualizer) + + def test_fit(self): + """ + Test that fit returns self. + """ + pca = PCA() + assert pca.fit(*self.discrete) is pca + + @pytest.mark.parametrize("n_components", [2, 3]) + def test_transform(self, n_components): + Xprime = PCA(projection=n_components).fit_transform(*self.continuous) + assert Xprime.shape == (500, n_components) + + def test_transform_without_fit(self): + """ + Test that appropriate error is raised when transform called without fit. + """ + oz = PCA(projection=3) + msg = "instance is not fitted yet, please call fit" + with pytest.raises(NotFitted, match=msg): + oz.transform(*self.continuous) + + @pytest.mark.xfail(IS_WINDOWS_OR_CONDA, reason="RMS of 12.115 on miniconda") def test_pca_decomposition_quick_method(self): """ - Test the quick method PCADecomposition visualizer 2 dimensions scaled. + Test the quick method PCA visualizer 2 dimensions scaled. """ - ax = pca_decomposition( - X=self.dataset.X, proj_dim=2, scale=True, random_state=28 + visualizer = pca_decomposition( + *self.discrete, projection=2, scale=True, random_state=28 ) - self.assert_images_similar(ax=ax) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=?)" - ) + # AppVeyor tests fail with RMS 12.115 + self.assert_images_similar(visualizer, windows_tol=12.5) + def test_scale_true_2d(self): """ - Test the PCADecomposition visualizer 2 dimensions scaled. + Test the PCA visualizer 2 dimensions scaled. """ - params = {'scale': True, 'proj_dim': 2, 'random_state': 9932} - visualizer = PCADecomposition(**params).fit(self.dataset.X) - pca_array = visualizer.transform(self.dataset.X) + params = {"scale": True, "projection": 2, "random_state": 9932} + visualizer = PCA(**params).fit(*self.discrete) + pca_array = visualizer.transform(*self.discrete) # Image comparison tests self.assert_images_similar(visualizer) # Assert PCA transformation occurred successfully - assert pca_array.shape == (self.dataset.X.shape[0], 2) + assert pca_array.shape == (self.discrete.X.shape[0], 2) + @pytest.mark.xfail(IS_WINDOWS_OR_CONDA, reason="RMS of 8.828 on miniconda") def test_scale_false_2d(self): """ - Test the PCADecomposition visualizer 2 dimensions non-scaled. + Test the PCA visualizer 2 dimensions non-scaled. """ - params = {'scale': False, 'proj_dim': 2, 'random_state': 1229} - visualizer = PCADecomposition(**params).fit(self.dataset.X) - pca_array = visualizer.transform(self.dataset.X) - + params = {"scale": False, "projection": 2, "random_state": 1229} + visualizer = PCA(**params).fit(*self.continuous) + pca_array = visualizer.transform(*self.continuous) + visualizer.finalize() + visualizer.cax.set_yticklabels([]) # Image comparison tests - self.assert_images_similar(visualizer) + # AppVeyor tests fail with RMS 8.180 + self.assert_images_similar(visualizer, tol=0.03, windows_tol=8.5) # Assert PCA transformation occurred successfully - assert pca_array.shape == (self.dataset.X.shape[0], 2) + assert pca_array.shape == (self.continuous.X.shape[0], 2) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=3)" - ) def test_biplot_2d(self): """ - Test the PCADecomposition 2D biplot (proj_features). + Test the PCA 2D biplot (proj_features). """ params = { - 'features': 'ABCDEFGHIKLM', 'random_state': 67, - 'proj_features': True, 'proj_dim': 2, + "features": list("ABCDEFGHIKLM"), + "random_state": 67, + "proj_features": True, + "projection": 2, } - visualizer = PCADecomposition(**params).fit(self.dataset.X) - pca_array = visualizer.transform(self.dataset.X) + visualizer = PCA(**params).fit(self.discrete.X) + pca_array = visualizer.transform(self.discrete.X) # Image comparison tests - self.assert_images_similar(visualizer) + self.assert_images_similar(visualizer, tol=5) # Assert PCA transformation occurred successfully - assert pca_array.shape == (self.dataset.X.shape[0], 2) + assert pca_array.shape == (self.discrete.X.shape[0], 2) def test_scale_true_3d(self): """ - Test the PCADecomposition visualizer 3 dimensions scaled. + Test the PCA visualizer 3 dimensions scaled. """ - params = {'scale': True, 'proj_dim': 3, 'random_state': 7382} - visualizer = PCADecomposition(**params).fit(self.dataset.X) - pca_array = visualizer.transform(self.dataset.X) + params = {"scale": True, "projection": 3, "random_state": 7382} + visualizer = PCA(**params).fit(self.discrete.X) + pca_array = visualizer.transform(self.discrete.X) # Image comparison tests self.assert_images_similar(visualizer) # Assert PCA transformation occurred successfully - assert pca_array.shape == (self.dataset.X.shape[0], 3) + assert pca_array.shape == (self.discrete.X.shape[0], 3) def test_scale_false_3d(self): """ - Test the PCADecomposition visualizer 3 dimensions non-scaled. + Test the PCA visualizer 3 dimensions non-scaled. """ - params = {'scale': False, 'proj_dim': 3, 'random_state': 98} - visualizer = PCADecomposition(**params).fit(self.dataset.X) - pca_array = visualizer.transform(self.dataset.X) + params = {"scale": False, "projection": 3, "random_state": 98} + visualizer = PCA(**params).fit(self.discrete.X) + pca_array = visualizer.transform(self.discrete.X) # Image comparison tests self.assert_images_similar(visualizer) # Assert PCA transformation occurred successfully - assert pca_array.shape == (self.dataset.X.shape[0], 3) + assert pca_array.shape == (self.discrete.X.shape[0], 3) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=3)" + sys.platform == "win32", reason="images not close on windows (RMSE=3)" ) def test_biplot_3d(self): """ - Test the PCADecomposition 3D biplot (proj_features). + Test the PCA 3D biplot (proj_features). """ params = { - 'features': 'ABCDEFGHIKLM', 'random_state': 800, - 'proj_features': True, 'proj_dim': 3, + "features": list("ABCDEFGHIKLM"), + "random_state": 800, + "proj_features": True, + "projection": 3, } - visualizer = PCADecomposition(**params).fit(self.dataset.X) - pca_array = visualizer.transform(self.dataset.X) + visualizer = PCA(**params).fit(*self.discrete) + pca_array = visualizer.transform(*self.discrete) # Image comparison tests - self.assert_images_similar(visualizer) + self.assert_images_similar(visualizer, tol=5) # Assert PCA transformation occurred successfully - assert pca_array.shape == (self.dataset.X.shape[0], 3) + assert pca_array.shape == (self.discrete.X.shape[0], 3) - def test_scale_true_4d_execption(self): + def test_scale_true_4d_exception(self): """ - Test the PCADecomposition visualizer 4 dimensions scaled (catch YellowbrickError). + Test PCA visualizer 4 dimensions scaled (catch YellowbrickError). """ - params = {'scale': True, 'proj_dim': 4} - with pytest.raises(YellowbrickError, match="proj_dim object is not 2 or 3"): - PCADecomposition(**params) + params = {"scale": True, "projection": 4} + msg = "Projection dimensions must be either 2 or 3" + with pytest.raises(YellowbrickError, match=msg): + PCA(**params) - def test_scale_true_3d_execption(self): + def test_scale_true_3d_exception(self): """ - Test the PCADecomposition visualizer 3 dims scaled on 2 dim data set (catch ValueError). + Test PCA visualizer 3 dims scaled on 2 dim data set (catch ValueError). """ X = np.random.normal(loc=2, size=(100, 2)) - params = {'scale': True, 'proj_dim': 3} + params = {"scale": True, "projection": 3} - e = r'n_components=3 must be between 0 and min\(n_samples, n_features\)=2' + e = r"n_components=3 must be between 0 and min\(n_samples, n_features\)=2" with pytest.raises(ValueError, match=e): - pca = PCADecomposition(**params) + pca = PCA(**params) pca.fit(X) + + @mock.patch("yellowbrick.features.pca.plt.sca", autospec=True) + def test_alpha_param(self, mock_sca): + """ + Test that the user can supply an alpha param on instantiation + """ + # Instantiate a prediction error plot, provide custom alpha + params = {"alpha": 0.3, "projection": 2, "random_state": 9932} + visualizer = PCA(**params).fit(self.discrete.X) + pca_array = visualizer.transform(self.discrete.X) + assert visualizer.alpha == 0.3 + + visualizer.ax = mock.MagicMock() + visualizer.fit(self.discrete.X) + visualizer.transform(self.discrete.X) + + # Test that alpha was passed to internal matplotlib scatterplot + _, scatter_kwargs = visualizer.ax.scatter.call_args + assert "alpha" in scatter_kwargs + assert scatter_kwargs["alpha"] == 0.3 + assert pca_array.shape == (self.discrete.X.shape[0], 2) + + @pytest.mark.xfail(IS_WINDOWS_OR_CONDA, reason="RMS of 7.332 on miniconda") + def test_colorbar(self): + """ + Test the PCA visualizer's colorbar features. + """ + params = { + "scale": True, + "projection": 2, + "random_state": 7382, + "color": self.discrete.y, + "colorbar": True, + } + visualizer = PCA(**params).fit(*self.continuous) + visualizer.transform(self.continuous.X, self.continuous.y) + visualizer.finalize() + visualizer.cax.set_yticklabels([]) + + # Image comparison tests + # AppVeyor tests fail with RMS of 7.280 + self.assert_images_similar(visualizer, windows_tol=7.5) + + @pytest.mark.xfail(IS_WINDOWS_OR_CONDA, reason="RMS of 14.515 on miniconda") + def test_heatmap(self): + """ + Test the PCA visualizer's heatmap features. + """ + params = { + "scale": True, + "projection": 2, + "random_state": 7382, + "color": self.discrete.y, + "heatmap": True, + } + visualizer = PCA(**params).fit(self.discrete.X, self.discrete.y) + visualizer.transform(self.discrete.X, self.discrete.y) + visualizer.finalize() + # TODO: manually modifying ticks should be removed after #916 is fixed + visualizer.lax.set_xticks([]) + visualizer.lax.set_yticks([]) + visualizer.lax.set_xticks([], minor=True) + visualizer.uax.set_xticklabels([]) + + # Image comparison tests + # AppVeyor tests fail with RMS 14.492 + self.assert_images_similar(visualizer, windows_tol=14.5) + + @pytest.mark.xfail(IS_WINDOWS_OR_CONDA, reason="RMS of 10.987 on miniconda") + def test_colorbar_heatmap(self): + """ + Test the PCA visualizer with both colorbar and heatmap. + """ + params = { + "scale": True, + "projection": 2, + "random_state": 7382, + "color": self.discrete.y, + "colorbar": True, + "heatmap": True, + } + visualizer = PCA(**params).fit(self.continuous.X, self.continuous.y) + visualizer.transform(self.continuous.X, self.continuous.y) + visualizer.finalize() + # TODO: manually modifying ticks should be removed after #916 is fixed + visualizer.lax.set_xticks([]) + visualizer.lax.set_yticks([]) + visualizer.lax.set_xticks([], minor=True) + visualizer.uax.set_xticklabels([]) + visualizer.cax.set_yticklabels([]) + + # Image comparison tests + # AppVeyor tests fail with RMS 10.331 + self.assert_images_similar(visualizer, windows_tol=10.5) + + def test_3d_heatmap_enabled_error(self): + """ + Assert an exception if colorbar and heatmap is enabled with 3-dimensions + """ + with pytest.raises(YellowbrickValueError): + PCA(projection=3, heatmap=True) + + @pytest.mark.skipif( + make_axes_locatable is not None, reason="requires matplotlib <= 2.0.1" + ) + def test_matplotlib_version_error(): + """ + Assert an exception is raised with incompatible matplotlib versions + """ + with pytest.raises(YellowbrickValueError): + PCA(colorbar=True, heatmap=True) diff --git a/tests/test_features/test_pcoords.py b/tests/test_features/test_pcoords.py index e66bf18de..f26824aa4 100644 --- a/tests/test_features/test_pcoords.py +++ b/tests/test_features/test_pcoords.py @@ -1,11 +1,11 @@ # tests.test_features.test_pcoords # Testing for the parallel coordinates feature visualizers # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Author: @thekylesaurus # Created: Thu Oct 06 11:21:27 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2017 The scikit-yb developers. # For license information, see LICENSE.txt # # ID: test_pcoords.py [1d407ab] benjamin@bengfort.com $ @@ -21,10 +21,11 @@ import pytest import numpy as np +from yellowbrick.datasets import load_occupancy from yellowbrick.features.pcoords import * from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset +from ..fixtures import Dataset from sklearn.datasets import make_classification @@ -38,28 +39,36 @@ ## Fixtures ########################################################################## -@pytest.fixture(scope='class') + +@pytest.fixture(scope="class") def dataset(request): """ Creates a random multiclass classification dataset fixture """ X, y = make_classification( - n_samples=200, n_features=5, n_informative=4, n_redundant=0, - n_classes=3, n_clusters_per_class=1, random_state=451, flip_y=0, - class_sep=3, scale=np.array([1.0, 2.0, 100.0, 20.0, 1.0]) + n_samples=200, + n_features=5, + n_informative=4, + n_redundant=0, + n_classes=3, + n_clusters_per_class=1, + random_state=451, + flip_y=0, + class_sep=3, + scale=np.array([1.0, 2.0, 100.0, 20.0, 1.0]), ) dataset = Dataset(X, y) request.cls.dataset = dataset - ########################################################################## ## Parallel Coordinates Tests ########################################################################## -@pytest.mark.usefixtures('dataset') -class TestParallelCoordinates(VisualTestCase, DatasetMixin): + +@pytest.mark.usefixtures("dataset") +class TestParallelCoordinates(VisualTestCase): """ Test the ParallelCoordinates visualizer """ @@ -70,7 +79,7 @@ def test_parallel_coords(self): """ visualizer = ParallelCoordinates() visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_parallel_coords_fast(self): @@ -79,7 +88,7 @@ def test_parallel_coords_fast(self): """ visualizer = ParallelCoordinates(fast=True) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_alpha(self): @@ -88,7 +97,7 @@ def test_alpha(self): """ visualizer = ParallelCoordinates(alpha=1.0) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_alpha_fast(self): @@ -97,7 +106,7 @@ def test_alpha_fast(self): """ visualizer = ParallelCoordinates(alpha=1.0, fast=True) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_labels(self): @@ -105,10 +114,10 @@ def test_labels(self): Test image closeness when class and feature labels are supplied """ visualizer = ParallelCoordinates( - classes=['a', 'b', 'c'], features=['f1', 'f2', 'f3', 'f4', 'f5'] + classes=["a", "b", "c"], features=["f1", "f2", "f3", "f4", "f5"] ) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) def test_labels_fast(self): @@ -116,46 +125,46 @@ def test_labels_fast(self): Test image closeness when class and feature labels are supplied in fast mode """ visualizer = ParallelCoordinates( - classes=['a', 'b', 'c'], features=['f1', 'f2', 'f3', 'f4', 'f5'], fast=True + classes=["a", "b", "c"], features=["f1", "f2", "f3", "f4", "f5"], fast=True ) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) def test_normalized_l2(self): """ Test image closeness on l2 normalized 3 class dataset """ - visualizer = ParallelCoordinates(normalize='l2') + visualizer = ParallelCoordinates(normalize="l2") visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_normalized_l2_fast(self): """ Test image closeness on l2 normalized 3 class dataset in fast mode """ - visualizer = ParallelCoordinates(normalize='l2', fast=True) + visualizer = ParallelCoordinates(normalize="l2", fast=True) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_normalized_minmax(self): """ Test image closeness on minmax normalized 3 class dataset """ - visualizer = ParallelCoordinates(normalize='minmax') + visualizer = ParallelCoordinates(normalize="minmax") visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_normalized_minmax_fast(self): """ Test image closeness on minmax normalized 3 class dataset in fast mode """ - visualizer = ParallelCoordinates(normalize='minmax', fast=True) + visualizer = ParallelCoordinates(normalize="minmax", fast=True) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) @pytest.mark.skipif(pd is None, reason="test requires pandas") @@ -163,24 +172,41 @@ def test_pandas_integration_sampled(self): """ Test on a real dataset with pandas DataFrame and Series sampled for speed """ - df = self.load_pandas("occupancy") - - target = "occupancy" - features = [ - 'temperature', 'relative humidity', 'light', 'C02', 'humidity' + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + classes = [ + k for k, _ in sorted(data.meta["labels"].items(), key=lambda i: i[1]) ] - X = df[features] - y = pd.Series([ - 'occupied' if yi == 1 else 'unoccupied' for yi in df[target] - ]) - assert isinstance(X, pd.DataFrame) assert isinstance(y, pd.Series) - oz = ParallelCoordinates(sample=0.05, shuffle=True, random_state=4291) + oz = ParallelCoordinates( + sample=0.05, shuffle=True, random_state=4291, classes=classes + ) + oz.fit_transform(X, y) + oz.finalize() + + self.assert_images_similar(oz, tol=0.1) + + def test_numpy_integration_sampled(self): + """ + Ensure visualizer works in default case with numpy arrays and sampling + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + classes = [ + k for k, _ in sorted(data.meta["labels"].items(), key=lambda i: i[1]) + ] + + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) + + oz = ParallelCoordinates( + sample=0.05, shuffle=True, random_state=4291, classes=classes + ) oz.fit_transform(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz, tol=0.1) @@ -189,24 +215,37 @@ def test_pandas_integration_fast(self): """ Test on a real dataset with pandas DataFrame and Series in fast mode """ - df = self.load_pandas("occupancy") - - target = "occupancy" - features = [ - 'temperature', 'relative humidity', 'light', 'C02', 'humidity' + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + classes = [ + k for k, _ in sorted(data.meta["labels"].items(), key=lambda i: i[1]) ] - X = df[features] - y = pd.Series([ - 'occupied' if yi == 1 else 'unoccupied' for yi in df[target] - ]) - assert isinstance(X, pd.DataFrame) assert isinstance(y, pd.Series) - oz = ParallelCoordinates(fast=True) + oz = ParallelCoordinates(fast=True, classes=classes) + oz.fit_transform(X, y) + oz.finalize() + + self.assert_images_similar(oz, tol=0.1) + + def test_numpy_integration_fast(self): + """ + Ensure visualizer works in default case with numpy arrays and fast mode + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + classes = [ + k for k, _ in sorted(data.meta["labels"].items(), key=lambda i: i[1]) + ] + + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) + + oz = ParallelCoordinates(fast=True, classes=classes) oz.fit_transform(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz, tol=0.1) @@ -214,8 +253,8 @@ def test_normalized_invalid_arg(self): """ Invalid argument to 'normalize' should raise """ - with self.assertRaises(YellowbrickValueError): - ParallelCoordinates(normalize='foo') + with pytest.raises(YellowbrickValueError): + ParallelCoordinates(normalize="foo") def test_sample_int(self): """ @@ -234,7 +273,9 @@ def test_sample_int_shuffle(self): visualizer = ParallelCoordinates(sample=3, shuffle=True, random_state=444) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer = ParallelCoordinates(sample=3, shuffle=True, random_state=np.random.RandomState()) + visualizer = ParallelCoordinates( + sample=3, shuffle=True, random_state=np.random.RandomState() + ) visualizer.fit_transform(self.dataset.X, self.dataset.y) def test_sample_int_shuffle_false(self): @@ -247,14 +288,16 @@ def test_sample_int_shuffle_false(self): visualizer = ParallelCoordinates(sample=3, shuffle=False, random_state=444) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer = ParallelCoordinates(sample=3, shuffle=False, random_state=np.random.RandomState()) + visualizer = ParallelCoordinates( + sample=3, shuffle=False, random_state=np.random.RandomState() + ) visualizer.fit_transform(self.dataset.X, self.dataset.y) def test_sample_int_invalid(self): """ Negative int values should raise exception """ - with self.assertRaises(YellowbrickValueError): + with pytest.raises(YellowbrickValueError): ParallelCoordinates(sample=-1) def test_sample_float(self): @@ -274,7 +317,9 @@ def test_sample_float_shuffle(self): visualizer = ParallelCoordinates(sample=0.5, shuffle=True, random_state=444) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer = ParallelCoordinates(sample=0.5, shuffle=True, random_state=np.random.RandomState()) + visualizer = ParallelCoordinates( + sample=0.5, shuffle=True, random_state=np.random.RandomState() + ) visualizer.fit_transform(self.dataset.X, self.dataset.y) def test_sample_float_shuffle_false(self): @@ -287,24 +332,27 @@ def test_sample_float_shuffle_false(self): visualizer = ParallelCoordinates(sample=0.5, shuffle=False, random_state=444) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer = ParallelCoordinates(sample=0.5, shuffle=False, random_state=np.random.RandomState()) + visualizer = ParallelCoordinates( + sample=0.5, shuffle=False, random_state=np.random.RandomState() + ) visualizer.fit_transform(self.dataset.X, self.dataset.y) def test_sample_float_invalid(self): """ Float values for 'sample' argument outside [0,1] should raise. """ - with self.assertRaises(YellowbrickValueError): + with pytest.raises(YellowbrickValueError): ParallelCoordinates(sample=-0.2) - with self.assertRaises(YellowbrickValueError): + + with pytest.raises(YellowbrickValueError): ParallelCoordinates(sample=1.1) def test_sample_invalid_type(self): """ Non-numeric values for 'sample' argument should raise. """ - with self.assertRaises(YellowbrickTypeError): - ParallelCoordinates(sample='foo') + with pytest.raises(YellowbrickTypeError): + ParallelCoordinates(sample="foo") @staticmethod def test_static_subsample(): @@ -329,7 +377,9 @@ def test_static_subsample(): assert np.array_equal(yprime, y) sample = 50 - visualizer = ParallelCoordinates(sample=sample, random_state=None, shuffle=False) + visualizer = ParallelCoordinates( + sample=sample, random_state=None, shuffle=False + ) Xprime, yprime = visualizer._subsample(X, y) assert np.array_equal(Xprime, X[:sample, :]) assert np.array_equal(yprime, y[:sample]) @@ -343,8 +393,8 @@ def test_static_subsample(): visualizer = ParallelCoordinates(sample=0.5, random_state=None, shuffle=False) Xprime, yprime = visualizer._subsample(X, y) - assert np.array_equal(Xprime, X[:int(ntotal/2), :]) - assert np.array_equal(yprime, y[:int(ntotal/2)]) + assert np.array_equal(Xprime, X[: int(ntotal / 2), :]) + assert np.array_equal(yprime, y[: int(ntotal / 2)]) sample = 0.5 visualizer = ParallelCoordinates(sample=sample, random_state=None, shuffle=True) @@ -361,7 +411,9 @@ def test_static_subsample(): assert len(yprime) == ntotal * sample sample = 0.99 - visualizer = ParallelCoordinates(sample=sample, random_state=np.random.RandomState(), shuffle=True) + visualizer = ParallelCoordinates( + sample=sample, random_state=np.random.RandomState(), shuffle=True + ) Xprime, yprime = visualizer._subsample(X, y) assert np.array_equal(Xprime, X[yprime.flatten(), :]) assert len(Xprime) == ntotal * sample diff --git a/tests/test_features/test_projection.py b/tests/test_features/test_projection.py new file mode 100644 index 000000000..6cd00c0be --- /dev/null +++ b/tests/test_features/test_projection.py @@ -0,0 +1,241 @@ +# tests.test_features.test_projection +# Test the base ProjectionVisualizer drawing functionality +# +# Author: Naresh Bachwani +# Created: Wed Jul 17 09:53:07 2019 -0400 +# +# Copyright (C) 2019 the scikit-yb developers. +# For license information, see LICENSE.txt +# +# ID: test_projection.py [21eb9d2] 43993586+naresh-bachwani@users.noreply.github.com $ + +""" +Test the base ProjectionVisualizer drawing functionality +""" + +########################################################################## +## Imports +########################################################################## + +import pytest +import numpy.testing as npt +import matplotlib.pyplot as plt + +from yellowbrick.features.projection import * +from yellowbrick.exceptions import YellowbrickValueError + +from tests.base import VisualTestCase +from unittest import mock + +from sklearn.decomposition import PCA +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler + +########################################################################## +## MockVisualizer +########################################################################## + + +class MockVisualizer(ProjectionVisualizer): + """ + The MockVisualizer implements the ProjectionVisualizer interface using + PCA as an internal transformer. This visualizer is used to directly test + how subclasses interact with the ProjectionVisualizer base class. + """ + + def __init__( + self, + ax=None, + features=None, + classes=None, + colors=None, + colormap=None, + target_type="auto", + projection=2, + alpha=0.75, + colorbar=True, + **kwargs + ): + + super(MockVisualizer, self).__init__( + ax=ax, + features=features, + classes=classes, + colors=colors, + colormap=colormap, + target_type=target_type, + projection=projection, + alpha=alpha, + colorbar=colorbar, + **kwargs + ) + + self.pca_transformer = Pipeline( + [ + ("scale", StandardScaler()), + ("pca", PCA(self.projection, random_state=2019)), + ] + ) + + def fit(self, X, y=None): + super(MockVisualizer, self).fit(X, y) + self.pca_transformer.fit(X) + return self + + def transform(self, X, y=None): + try: + Xp = self.pca_transformer.transform(X) + except AttributeError as e: + raise AttributeError(str(e) + " try using fit_transform instead.") + self.draw(Xp, y) + return Xp + + +########################################################################## +## ProjectionVisualizer Tests +########################################################################## + + +@pytest.mark.usefixtures("discrete", "continuous") +class TestProjectionVisualizer(VisualTestCase): + """ + Test the ProjectionVisualizer base class + """ + + def test_discrete_plot(self): + """ + Test the visualizer with discrete target. + """ + X, y = self.discrete + classes = ["a", "b", "c", "d", "e"] + visualizer = MockVisualizer(projection=2, colormap="plasma", classes=classes) + X_prime = visualizer.fit_transform(X, y) + npt.assert_array_equal(visualizer.classes_, classes) + visualizer.finalize() + self.assert_images_similar(visualizer) + assert X_prime.shape == (self.discrete.X.shape[0], 2) + + def test_continuous_plot(self): + """ + Test the visualizer with continuous target. + """ + X, y = self.continuous + visualizer = MockVisualizer(projection="2d") + visualizer.fit_transform(X, y) + visualizer.finalize() + visualizer.cax.set_yticklabels([]) + self.assert_images_similar(visualizer) + + def test_continuous_when_target_discrete(self): + """ + Ensure user can override discrete target_type by specifying continuous + """ + _, ax = plt.subplots() + X, y = self.discrete + visualizer = MockVisualizer( + ax=ax, projection="2D", target_type="continuous", colormap="cool" + ) + visualizer.fit(X, y) + visualizer.transform(X, y) + visualizer.finalize() + visualizer.cax.set_yticklabels([]) + self.assert_images_similar(visualizer) + + def test_single_plot(self): + """ + Assert single color plot when y is not specified + """ + X, y = self.discrete + visualizer = MockVisualizer(projection=2, colormap="plasma") + visualizer.fit_transform(X) + visualizer.finalize() + self.assert_images_similar(visualizer) + + def test_discrete_3d(self): + """ + Test visualizer for 3 dimensional discrete plots + """ + X, y = self.discrete + + classes = ["a", "b", "c", "d", "e"] + colors = ["r", "b", "g", "m", "c"] + visualizer = MockVisualizer(projection=3, colors=colors, classes=classes) + visualizer.fit_transform(X, y) + npt.assert_array_equal(visualizer.classes_, classes) + visualizer.finalize() + self.assert_images_similar(visualizer) + + def test_3d_continuous_plot(self): + """ + Tests visualizer for 3 dimensional continuous plots + """ + X, y = self.continuous + visualizer = MockVisualizer(projection="3D") + visualizer.fit_transform(X, y) + visualizer.finalize() + visualizer.cbar.set_ticks([]) + self.assert_images_similar(visualizer) + + def test_alpha_param(self): + """ + Ensure that the alpha parameter modifies opacity + """ + # Instantiate a prediction error plot, provide custom alpha + X, y = self.discrete + params = {"alpha": 0.3, "projection": 2} + visualizer = MockVisualizer(**params) + visualizer.ax = mock.MagicMock() + visualizer.fit(X, y) + visualizer.transform(X, y) + + assert visualizer.alpha == 0.3 + + # Test that alpha was passed to internal matplotlib scatterplot + _, scatter_kwargs = visualizer.ax.scatter.call_args + assert "alpha" in scatter_kwargs + assert scatter_kwargs["alpha"] == 0.3 + + # Check Errors + @pytest.mark.parametrize("projection", ["4D", 1, "100d", 0]) + def test_wrong_projection_dimensions(self, projection): + """ + Validate projection hyperparameter + """ + msg = "Projection dimensions must be either 2 or 3" + with pytest.raises(YellowbrickValueError, match=msg): + MockVisualizer(projection=projection) + + def test_target_not_label_encoded(self): + """ + Assert label encoding mismatch with y raises exception + """ + X, y = self.discrete + # Multiply every element by 10 to make non-label encoded + y = y * 10 + visualizer = MockVisualizer() + msg = "Target needs to be label encoded." + with pytest.raises(YellowbrickValueError, match=msg): + visualizer.fit_transform(X, y) + + @pytest.mark.parametrize("dataset", ("discrete", "continuous")) + def test_y_required_for_discrete_and_continuous(self, dataset): + """ + Assert error is raised when y is not passed to transform + """ + X, y = getattr(self, dataset) + visualizer = MockVisualizer() + visualizer.fit(X, y) + + msg = "y is required for {} target".format(dataset) + with pytest.raises(YellowbrickValueError, match=msg): + visualizer.transform(X) + + def test_colorbar_false(self): + """ + Test that colorbar equals false works correctly + """ + visualizer = MockVisualizer(colorbar=False, colormap="YlOrRd") + visualizer.fit_transform(*self.continuous) + visualizer.finalize() + + self.assert_images_similar(visualizer) diff --git a/tests/test_features/test_radviz.py b/tests/test_features/test_radviz.py index a198f7062..0513ca254 100644 --- a/tests/test_features/test_radviz.py +++ b/tests/test_features/test_radviz.py @@ -1,10 +1,10 @@ # tests.test_features.test_radviz # Test the RadViz feature analysis visualizers # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Oct 07 12:19:19 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_radviz.py [01d5996] benjamin@bengfort.com $ @@ -21,31 +21,39 @@ import pytest import numpy.testing as npt -from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase +from ..fixtures import Dataset from sklearn.datasets import make_classification +from yellowbrick.datasets import load_occupancy from yellowbrick.features.radviz import * try: - import pandas + import pandas as pd except ImportError: - pandas = None - + pd = None ########################################################################## ## Fixtures ########################################################################## -@pytest.fixture(scope='class') + +@pytest.fixture(scope="class") def dataset(request): """ Creates a random multiclass classification dataset fixture """ X, y = make_classification( - n_samples=200, n_features=5, n_informative=4, n_redundant=0, - n_classes=3, n_clusters_per_class=1, random_state=451, flip_y=0, - class_sep=3, scale=np.array([1.0, 2.0, 100.0, 20.0, 1.0]) + n_samples=200, + n_features=5, + n_informative=4, + n_redundant=0, + n_classes=3, + n_clusters_per_class=1, + random_state=451, + flip_y=0, + class_sep=3, + scale=np.array([1.0, 2.0, 100.0, 20.0, 1.0]), ) dataset = Dataset(X, y) @@ -56,8 +64,9 @@ def dataset(request): ## RadViz Tests ########################################################################## -@pytest.mark.usefixtures('dataset') -class TestRadViz(VisualTestCase, DatasetMixin): + +@pytest.mark.usefixtures("dataset") +class TestRadViz(VisualTestCase): """ Test the RadViz visualizer """ @@ -68,22 +77,26 @@ def test_normalize_x(self): """ # Original data X = np.array( - [[ 2.318, 2.727, 4.260, 7.212, 4.792], - [ 2.315, 2.726, 4.295, 7.140, 4.783,], - [ 2.315, 2.724, 4.260, 7.135, 4.779,], - [ 2.110, 3.609, 4.330, 7.985, 5.595,], - [ 2.110, 3.626, 4.330, 8.203, 5.621,], - [ 2.110, 3.620, 4.470, 8.210, 5.612,]] + [ + [2.318, 2.727, 4.260, 7.212, 4.792], + [2.315, 2.726, 4.295, 7.140, 4.783], + [2.315, 2.724, 4.260, 7.135, 4.779], + [2.110, 3.609, 4.330, 7.985, 5.595], + [2.110, 3.626, 4.330, 8.203, 5.621], + [2.110, 3.620, 4.470, 8.210, 5.612], + ] ) # Expected result Xe = np.array( - [[ 1. , 0.00332594, 0. , 0.07162791, 0.01543943], - [ 0.98557692, 0.00221729, 0.16666667, 0.00465116, 0.00475059], - [ 0.98557692, 0. , 0. , 0. , 0. ], - [ 0. , 0.98115299, 0.33333333, 0.79069767, 0.96912114], - [ 0. , 1. , 0.33333333, 0.99348837, 1. ], - [ 0. , 0.99334812, 1. , 1. , 0.98931116]] + [ + [1.0, 0.00332594, 0.0, 0.07162791, 0.01543943], + [0.98557692, 0.00221729, 0.16666667, 0.00465116, 0.00475059], + [0.98557692, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.98115299, 0.33333333, 0.79069767, 0.96912114], + [0.0, 1.0, 0.33333333, 0.99348837, 1.0], + [0.0, 0.99334812, 1.0, 1.0, 0.98931116], + ] ) # Xprime (transformed X) @@ -96,7 +109,7 @@ def test_radviz(self): """ visualizer = RadViz() visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) def test_radviz_alpha(self): @@ -105,47 +118,94 @@ def test_radviz_alpha(self): """ visualizer = RadViz(alpha=0.5) visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer, tol=0.25) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) - @pytest.mark.skipif(pandas is None, reason="test requires Pandas") - def test_integrated_radiz_with_pandas(self): + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_integrated_radviz_with_pandas(self): """ Test RadViz with Pandas on the occupancy dataset """ - occupancy = self.load_pandas("occupancy") + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() - # Load the data from the fixture - X = occupancy[[ - "temperature", "relative humidity", "light", "C02", "humidity" - ]] - y = occupancy['occupancy'].astype(int) + assert isinstance(X, pd.DataFrame) + assert isinstance(y, pd.Series) # Test the visualizer visualizer = RadViz() visualizer.fit_transform_poof(X, y) - self.assert_images_similar(visualizer) + self.assert_images_similar(visualizer, tol=0.1) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) - @pytest.mark.skipif(pandas is None, reason="test requires Pandas") - def test_integrated_radiz_pandas_classes_features(self): + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_integrated_radviz_with_numpy(self): + """ + Test RadViz with numpy on the occupancy dataset + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) + + # Test the visualizer + visualizer = RadViz() + visualizer.fit_transform_poof(X, y) + self.assert_images_similar(visualizer, tol=0.1) + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_integrated_radviz_pandas_classes_features(self): """ - Test RadViz with classes and features specified + Test RadViz with classes and features specified using Pandas """ # Load the data from the fixture - occupancy = self.load_pandas("occupancy") + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + features = ["temperature", "relative humidity", "light"] - classes = ['unoccupied', 'occupied'] + classes = [ + k for k, _ in sorted(data.meta["labels"].items(), key=lambda i: i[1]) + ] + + assert isinstance(X, pd.DataFrame) + assert isinstance(y, pd.Series) + + # Filter the dataset to make sure it's not just class names + X = X[features] + y = y.astype(int) + + # Test the visualizer + visualizer = RadViz(features=features, classes=classes) + visualizer.fit_transform_poof(X, y) + self.assert_images_similar(visualizer, tol=0.1) + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_integrated_radviz_numpy_classes_features(self): + """ + Test RadViz with classes and features specified using numpy + """ + # Load the data from the fixture + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + + features = data.meta["features"][0:3] + classes = [ + k for k, _ in sorted(data.meta["labels"].items(), key=lambda i: i[1]) + ] + + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) - X = occupancy[features] - y = occupancy['occupancy'].astype(int) + # Filter the dataset to make sure it's not just class names + X = X[:, :3] + y = y.astype(int) # Test the visualizer visualizer = RadViz(features=features, classes=classes) visualizer.fit_transform_poof(X, y) - self.assert_images_similar(visualizer) + self.assert_images_similar(visualizer, tol=0.1) diff --git a/tests/test_features/test_rankd.py b/tests/test_features/test_rankd.py index 1d880f2af..f3fd4b78e 100644 --- a/tests/test_features/test_rankd.py +++ b/tests/test_features/test_rankd.py @@ -1,13 +1,13 @@ # tests.test_features.test_rankd # Test the rankd feature analysis visualizers # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Oct 07 12:19:19 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_rankd.py [01d5996] benjamin@bengfort.com $ +# ID: test_rankd.py [7b4350a] nathan.danielsen@gmail.com $ """ Test the Rankd feature analysis visualizers @@ -17,15 +17,19 @@ ## Imports ########################################################################## -import sys -import six import pytest +import numpy as np +import numpy.testing as npt -from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase +from yellowbrick.datasets import load_occupancy from yellowbrick.features.rankd import * -from sklearn.datasets import make_classification +from yellowbrick.features.rankd import kendalltau +from yellowbrick.features.rankd import RankDBase +from sklearn.datasets import make_regression + +from yellowbrick.exceptions import YellowbrickValueError try: import pandas as pd @@ -33,106 +37,742 @@ pd = None -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def dataset(request): """ - Creates a binary classification dataset for use in RankD tests + Creates a dataset with 6 gaussian features and 2 categorical features + for testing the RankD ranking algorithms. The gaussian features have + different correlations with respect to each other, including strong + positive and negative correlation and no correlation at all. """ - X, y = make_classification( - n_samples=700, n_features=10, n_informative=8, n_redundant=2, - n_classes=2, n_clusters_per_class=2, random_state=6483 + X, _ = make_regression( + n_samples=100, + n_features=6, + effective_rank=2, + tail_strength=0, + n_informative=2, + noise=0.45, + random_state=27, ) - request.cls.dataset = Dataset(X, y) + rand = np.random.RandomState(seed=27) + request.cls.dataset = np.concatenate((X, rand.binomial(1, 0.6, (100, 2))), axis=1) + + +########################################################################## +## Kendall-Tau Tests +########################################################################## + + +@pytest.mark.usefixtures("dataset") +class TestKendallTau(object): + """ + Test the Kendall-Tau correlation metric + """ + + def test_kendalltau(self): + """ + Test results returned match expectations + """ + expected = np.array( + [ + [ + 1.0, + -0.68, + -0.57454545, + 0.49858586, + 0.07555556, + -0.05858586, + 0.02387848, + 0.11357219, + ], + [ + -0.68, + 1.0, + 0.58666667, + -0.69090909, + -0.22262626, + -0.17171717, + -0.05059964, + -0.12397575, + ], + [ + -0.57454545, + 0.58666667, + 1.0, + -0.61050505, + 0.18909091, + 0.07515152, + 0.00341121, + -0.0638663, + ], + [ + 0.49858586, + -0.69090909, + -0.61050505, + 1.0, + 0.11070707, + 0.3030303, + 0.03013237, + 0.07542581, + ], + [ + 0.07555556, + -0.22262626, + 0.18909091, + 0.11070707, + 1.0, + 0.4610101, + 0.01648752, + 0.05982047, + ], + [ + -0.05858586, + -0.17171717, + 0.07515152, + 0.3030303, + 0.4610101, + 1.0, + 0.03695479, + -0.02398599, + ], + [ + 0.02387848, + -0.05059964, + 0.00341121, + 0.03013237, + 0.01648752, + 0.03695479, + 1.0, + 0.18298883, + ], + [ + 0.11357219, + -0.12397575, + -0.0638663, + 0.07542581, + 0.05982047, + -0.02398599, + 0.18298883, + 1.0, + ], + ] + ) + npt.assert_almost_equal(expected, kendalltau(self.dataset)) + + def test_kendalltau_shape(self): + """ + Assert that a square correlation matrix is returned + """ + corr = kendalltau(self.dataset) + assert corr.shape[0] == corr.shape[1] + + for (i, j), val in np.ndenumerate(corr): + assert corr[j][i] == pytest.approx(val) + + def test_kendalltau_1D(self): + """ + Assert that a 2D matrix is required as input + """ + with pytest.raises(IndexError, match="tuple index out of range"): + X = 0.1 * np.arange(10) + kendalltau(X) + + +########################################################################## +## RankDBase Tests +########################################################################## + + +@pytest.mark.usefixtures("dataset") +class TestRankDBase(VisualTestCase): + """ + Test the RankDBase Visualizer + """ + + def test_rankdbase_unknown_algorithm(self): + """ + Assert that unknown algorithms raise an exception + """ + with pytest.raises( + YellowbrickValueError, match=".* is unrecognized ranking method" + ) as e: + oz = RankDBase(algorithm="unknown") + oz.fit_transform(self.dataset) + assert str(e.value) == "'unknown' is unrecognized ranking method" ########################################################################## ## Rank1D Base Tests ########################################################################## + @pytest.mark.usefixtures("dataset") -class TestRank1D(VisualTestCase, DatasetMixin): +class TestRank1D(VisualTestCase): """ Test the Rank1D visualizer """ - def test_rank1d_random(self): + def test_rank1d_shapiro(self): + """ + Test Rank1D using shapiro metric + """ + oz = Rank1D(algorithm="shapiro") + npt.assert_array_equal(oz.fit_transform(self.dataset), self.dataset) + + # Check Ranking + expected = np.array( + [ + 0.985617, + 0.992236, + 0.982354, + 0.984898, + 0.978514, + 0.990372, + 0.636401, + 0.624511, + ] + ) + + assert hasattr(oz, "ranks_") + assert oz.ranks_.shape == (self.dataset.shape[1],) + npt.assert_array_almost_equal(oz.ranks_, expected) + + # Image similarity comparison + oz.finalize() + self.assert_images_similar(oz) + + def test_rank1d_orientation(self): """ - Test Rank1D on a random binary classification dataset + Test Rank1D using vertical orientation """ - visualizer = Rank1D() - visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + oz = Rank1D(orient="v") + npt.assert_array_equal(oz.fit_transform(self.dataset), self.dataset) - self.assert_images_similar(visualizer) + # Image similarity comparison + oz.finalize() + self.assert_images_similar(oz) - @pytest.mark.skipif(pd is None, reason="requires pandas") @pytest.mark.filterwarnings("ignore:p-value") - def test_rank1d_integrated(self): + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_rank1d_integrated_pandas(self): """ Test Rank1D on occupancy dataset with pandas DataFrame and Series """ - df = self.load_pandas("occupancy") + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + features = data.meta["features"] + + assert isinstance(X, pd.DataFrame) + assert isinstance(y, pd.Series) + + # Test the visualizer + oz = Rank1D(features=features, show_feature_names=True) + assert oz.fit(X, y) is oz + assert oz.transform(X) is X + + # Image similarity testing + oz.finalize() + self.assert_images_similar(oz) + + @pytest.mark.filterwarnings("ignore:p-value") + def test_rank1d_integrated_numpy(self): + """ + Test Rank1D on occupancy dataset with default numpy data structures + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + features = data.meta["features"] - # Load the data from the fixture - X = df[[ - "temperature", "relative humidity", "light", "C02", "humidity" - ]] - y = df['occupancy'] + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) # Test the visualizer - visualizer = Rank1D() - visualizer.fit_transform(X, y) - visualizer.poof() + oz = Rank1D(features=features, show_feature_names=True) + assert oz.fit(X, y) is oz + assert oz.transform(X) is X - self.assert_images_similar(visualizer) + # Image similarity testing + oz.finalize() + self.assert_images_similar(oz) ########################################################################## ## Rank2D Test Cases ########################################################################## + @pytest.mark.usefixtures("dataset") -class TestRank2D(VisualTestCase, DatasetMixin): +class TestRank2D(VisualTestCase): """ Test the Rank2D visualizer """ @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_rank2d_pearson(self): + """ + Test Rank2D using pearson metric + """ + oz = Rank2D(algorithm="pearson") + npt.assert_array_equal(oz.fit_transform(self.dataset), self.dataset) + + # Check Ranking + expected = np.array( + [ + [ + 1.0, + -0.86937243, + -0.77884764, + 0.71424708, + 0.10836854, + -0.11550965, + 0.04494811, + 0.1725682, + ], + [ + -0.86937243, + 1.0, + 0.80436327, + -0.9086706, + -0.31117192, + -0.26313947, + -0.0711807, + -0.16924862, + ], + [ + -0.77884764, + 0.80436327, + 1.0, + -0.85520468, + 0.30940711, + 0.10634903, + -0.02485686, + -0.10230028, + ], + [ + 0.71424708, + -0.9086706, + -0.85520468, + 1.0, + 0.12537213, + 0.41306822, + 0.04704408, + 0.1031842, + ], + [ + 0.10836854, + -0.31117192, + 0.30940711, + 0.12537213, + 1.0, + 0.671111, + 0.06777278, + 0.09513859, + ], + [ + -0.11550965, + -0.26313947, + 0.10634903, + 0.41306822, + 0.671111, + 1.0, + 0.04684117, + -0.01072631, + ], + [ + 0.04494811, + -0.0711807, + -0.02485686, + 0.04704408, + 0.06777278, + 0.04684117, + 1.0, + 0.18298883, + ], + [ + 0.1725682, + -0.16924862, + -0.10230028, + 0.1031842, + 0.09513859, + -0.01072631, + 0.18298883, + 1.0, + ], + ] + ) + + assert hasattr(oz, "ranks_") + assert oz.ranks_.shape == (self.dataset.shape[1], self.dataset.shape[1]) + npt.assert_array_almost_equal(oz.ranks_, expected) + + # Image similarity comparision + oz.finalize() + self.assert_images_similar(oz, tol=0.1) + + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_rank2d_covariance(self): + """ + Test Rank2D using covariance metric + """ + oz = Rank2D(algorithm="covariance") + npt.assert_array_equal(oz.fit_transform(self.dataset), self.dataset) + + # Check Ranking + expected = np.array( + [ + [ + 4.09266931e-03, + -1.41062431e-03, + -2.26778429e-03, + 3.13507202e-03, + 2.21273274e-04, + -5.05566875e-04, + 1.44499782e-03, + 5.45713163e-03, + ], + [ + -1.41062431e-03, + 6.43286363e-04, + 9.28539346e-04, + -1.58126396e-03, + -2.51898163e-04, + -4.56609749e-04, + -9.07228811e-04, + -2.12191333e-03, + ], + [ + -2.26778429e-03, + 9.28539346e-04, + 2.07153281e-03, + -2.67061756e-03, + 4.49467833e-04, + 3.31158917e-04, + -5.68518509e-04, + -2.30156415e-03, + ], + [ + 3.13507202e-03, + -1.58126396e-03, + -2.67061756e-03, + 4.70751209e-03, + 2.74548546e-04, + 1.93898526e-03, + 1.62200836e-03, + 3.49952628e-03, + ], + [ + 2.21273274e-04, + -2.51898163e-04, + 4.49467833e-04, + 2.74548546e-04, + 1.01869657e-03, + 1.46545939e-03, + 1.08700151e-03, + 1.50099581e-03, + ], + [ + -5.05566875e-04, + -4.56609749e-04, + 3.31158917e-04, + 1.93898526e-03, + 1.46545939e-03, + 4.68073451e-03, + 1.61041253e-03, + -3.62750059e-04, + ], + [ + 1.44499782e-03, + -9.07228811e-04, + -5.68518509e-04, + 1.62200836e-03, + 1.08700151e-03, + 1.61041253e-03, + 2.52525253e-01, + 4.54545455e-02, + ], + [ + 5.45713163e-03, + -2.12191333e-03, + -2.30156415e-03, + 3.49952628e-03, + 1.50099581e-03, + -3.62750059e-04, + 4.54545455e-02, + 2.44343434e-01, + ], + ] + ) + + assert hasattr(oz, "ranks_") + assert oz.ranks_.shape == (self.dataset.shape[1], self.dataset.shape[1]) + npt.assert_array_almost_equal(oz.ranks_, expected) + + # Image similarity comparision + oz.finalize() + self.assert_images_similar(oz, tol=0.1) + + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) - def test_rank2d_random(self): + def test_rank2d_spearman(self): """ - Test Rank2D on a random binary classification dataset + Test Rank2D using spearman metric """ - visualizer = Rank2D() - visualizer.fit_transform(self.dataset.X, self.dataset.y) - visualizer.poof() + oz = Rank2D(algorithm="spearman") + npt.assert_array_equal(oz.fit_transform(self.dataset), self.dataset) + + # Check Ranking + expected = np.array( + [ + [ + 1.0, + -0.86889889, + -0.77551755, + 0.68520852, + 0.11369937, + -0.09489349, + 0.02909991, + 0.13840665, + ], + [ + -0.86889889, + 1.0, + 0.78232223, + -0.87065107, + -0.33450945, + -0.25244524, + -0.06166409, + -0.15108512, + ], + [ + -0.77551755, + 0.78232223, + 1.0, + -0.81636964, + 0.26846685, + 0.10348635, + 0.00415713, + -0.07783173, + ], + [ + 0.68520852, + -0.87065107, + -0.81636964, + 1.0, + 0.16316832, + 0.45167717, + 0.03672131, + 0.09191892, + ], + [ + 0.11369937, + -0.33450945, + 0.26846685, + 0.16316832, + 1.0, + 0.63986799, + 0.02009279, + 0.07290121, + ], + [ + -0.09489349, + -0.25244524, + 0.10348635, + 0.45167717, + 0.63986799, + 1.0, + 0.04503557, + -0.02923092, + ], + [ + 0.02909991, + -0.06166409, + 0.00415713, + 0.03672131, + 0.02009279, + 0.04503557, + 1.0, + 0.18298883, + ], + [ + 0.13840665, + -0.15108512, + -0.07783173, + 0.09191892, + 0.07290121, + -0.02923092, + 0.18298883, + 1.0, + ], + ] + ) + + assert hasattr(oz, "ranks_") + assert oz.ranks_.shape == (self.dataset.shape[1], self.dataset.shape[1]) + npt.assert_array_almost_equal(oz.ranks_, expected) + + # Image similarity comparision + oz.finalize() + self.assert_images_similar(oz, tol=0.1) + + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_rank2d_kendalltau(self): + """ + Test Rank2D using kendalltau metric + """ + oz = Rank2D(algorithm="kendalltau") + npt.assert_array_equal(oz.fit_transform(self.dataset), self.dataset) + + # Check Ranking + expected = np.array( + [ + [ + 1.0, + -0.68, + -0.57454545, + 0.49858586, + 0.07555556, + -0.05858586, + 0.02387848, + 0.11357219, + ], + [ + -0.68, + 1.0, + 0.58666667, + -0.69090909, + -0.22262626, + -0.17171717, + -0.05059964, + -0.12397575, + ], + [ + -0.57454545, + 0.58666667, + 1.0, + -0.61050505, + 0.18909091, + 0.07515152, + 0.00341121, + -0.0638663, + ], + [ + 0.49858586, + -0.69090909, + -0.61050505, + 1.0, + 0.11070707, + 0.3030303, + 0.03013237, + 0.07542581, + ], + [ + 0.07555556, + -0.22262626, + 0.18909091, + 0.11070707, + 1.0, + 0.4610101, + 0.01648752, + 0.05982047, + ], + [ + -0.05858586, + -0.17171717, + 0.07515152, + 0.3030303, + 0.4610101, + 1.0, + 0.03695479, + -0.02398599, + ], + [ + 0.02387848, + -0.05059964, + 0.00341121, + 0.03013237, + 0.01648752, + 0.03695479, + 1.0, + 0.18298883, + ], + [ + 0.11357219, + -0.12397575, + -0.0638663, + 0.07542581, + 0.05982047, + -0.02398599, + 0.18298883, + 1.0, + ], + ] + ) + + assert hasattr(oz, "ranks_") + assert oz.ranks_.shape == (self.dataset.shape[1], self.dataset.shape[1]) + npt.assert_array_almost_equal(oz.ranks_, expected) - tol = 10 if six.PY2 else 0.1 - self.assert_images_similar(visualizer, tol=tol) + # Image similarity comparision + oz.finalize() + self.assert_images_similar(oz, tol=0.1) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) - @pytest.mark.skipif(pd is None, reason="requires pandas") - def test_rank2d_integrated(self): + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_rank2d_integrated_pandas(self): """ Test Rank2D on occupancy dataset with pandas DataFrame and Series """ - df = self.load_pandas("occupancy") + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + features = data.meta["features"] - # Load the data from the fixture - X = df[[ - "temperature", "relative humidity", "light", "C02", "humidity" - ]] - y = df['occupancy'] + assert isinstance(X, pd.DataFrame) + assert isinstance(y, pd.Series) # Test the visualizer - visualizer = Rank2D() - visualizer.fit_transform(X, y) - visualizer.poof() + oz = Rank2D(features=features, show_feature_names=True) + assert oz.fit(X, y) is oz + assert oz.transform(X) is X + oz.finalize() - tol = 10 if six.PY2 else 0.1 - self.assert_images_similar(visualizer, tol=tol) -# + # Image similarity testing + self.assert_images_similar(oz, tol=0.1) + + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", + ) + def test_rank2d_integrated_numpy(self): + """ + Test Rank2D on occupancy dataset with numpy ndarray + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + features = data.meta["features"] + + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) + + # Test the visualizer + oz = Rank2D(features=features, show_feature_names=True) + assert oz.fit(X, y) is oz + assert oz.transform(X) is X + oz.finalize() + + # Image similarity testing + self.assert_images_similar(oz, tol=0.1) diff --git a/tests/test_meta.py b/tests/test_meta.py index 8318fc945..5f66c3d6c 100644 --- a/tests/test_meta.py +++ b/tests/test_meta.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Sat Apr 07 13:16:53 2018 -0400 # -# ID: test_meta.py [] benjamin@bengfort.com $ +# ID: test_meta.py [0a2d2b4] benjamin@bengfort.com $ """ Meta testing for testing helper functions! @@ -18,17 +18,15 @@ import pytest import inspect +import matplotlib as mpl + from tests.rand import RandomVisualizer +from unittest.mock import MagicMock, patch from tests.base import ACTUAL_IMAGES, BASELINE_IMAGES from tests.base import VisualTestCase, ImageComparison from yellowbrick.exceptions import ImageComparisonFailure -try: - from unittest.mock import MagicMock, patch -except ImportError: - from mock import MagicMock, patch - def assert_path_exists(*parts): # Hide this method from the pytest traceback on test failure. @@ -50,6 +48,7 @@ def assert_path_not_exists(*parts): ## Test Cases ########################################################################## + class TestMetaImageComparison(VisualTestCase): """ Meta Test: ImageComparison test cases @@ -59,6 +58,7 @@ def test_image_comparison(self): """ Test the image comparison initialization and properties """ + def inner_assertion_function(ax): stack = inspect.stack() return ImageComparison(stack, ax=ax) @@ -70,12 +70,16 @@ def inner_assertion_function(ax): assert compare.test_module_path == "test_meta" # Must use os.path.join for Windows/POSIX compatibility - assert compare.actual_image_path.endswith(os.path.join( - "tests", "actual_images", "test_meta", "test_image_comparison.png" - )) - assert compare.baseline_image_path.endswith(os.path.join( - "tests", "baseline_images", "test_meta", "test_image_comparison.png" - )) + assert compare.actual_image_path.endswith( + os.path.join( + "tests", "actual_images", "test_meta", "test_image_comparison.png" + ) + ) + assert compare.baseline_image_path.endswith( + os.path.join( + "tests", "baseline_images", "test_meta", "test_image_comparison.png" + ) + ) @patch.object(ImageComparison, "cleanup") @patch.object(ImageComparison, "save") @@ -84,6 +88,7 @@ def test_image_comparison_call(self, mock_cleanup, mock_save, mock_compare): """ Test that image comparison cleans up, saves, and compares """ + def inner_assertion_function(): stack = inspect.stack() return ImageComparison(stack, ax=MagicMock()) @@ -115,7 +120,7 @@ def test_missing_baseline_image(self): Test that a missing basline image raises an exception """ viz = RandomVisualizer(random_state=14).fit() - viz.poof() + viz.finalize() # Assert the baseline image does not exist assert_path_not_exists( @@ -135,7 +140,9 @@ def test_random_visualizer(self): Test that a random visualization is correctly compared to a baseline """ viz = RandomVisualizer(random_state=111).fit() - viz.poof() + viz.finalize() + + assert mpl.get_backend() == "agg" compare = self.assert_images_similar(viz, tol=1.0) assert_path_exists(compare.actual_image_path) @@ -145,15 +152,18 @@ def test_random_visualizer_not_close(self): """ Test that not close visualizers raise an assertion error. """ + # Baseline image random_state=225 viz = RandomVisualizer(random_state=224).fit() - viz.poof() + viz.finalize() with pytest.raises(ImageComparisonFailure, match="images not close"): self.assert_images_similar(viz) # Assert there is a diff assert_path_exists( - ACTUAL_IMAGES, "test_meta", "test_random_visualizer_not_close-failed-diff.png" + ACTUAL_IMAGES, + "test_meta", + "test_random_visualizer_not_close-failed-diff.png", ) def test_random_visualizer_increased_tolerance(self): @@ -161,6 +171,6 @@ def test_random_visualizer_increased_tolerance(self): Test that not close visualizers pass with increased tolerance """ viz = RandomVisualizer(random_state=224).fit() - viz.poof() + viz.finalize() self.assert_images_similar(viz, tol=30) diff --git a/tests/test_model_selection/__init__.py b/tests/test_model_selection/__init__.py index 9ec2e913e..c29f98a2d 100644 --- a/tests/test_model_selection/__init__.py +++ b/tests/test_model_selection/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Fri Mar 30 10:37:18 2018 -0400 # -# ID: __init__.py [] benjamin@bengfort.com $ +# ID: __init__.py [c5355ee] benjamin@bengfort.com $ """ Tests for the model selection visualizer library. diff --git a/tests/test_model_selection/conftest.py b/tests/test_model_selection/conftest.py index d4dae0d27..f9a4fc438 100644 --- a/tests/test_model_selection/conftest.py +++ b/tests/test_model_selection/conftest.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Fri Mar 30 14:35:39 2018 -0400 # -# ID: conftest.py [] benjamin@bengfort.com $ +# ID: conftest.py [c5355ee] benjamin@bengfort.com $ """ Provides fixtures for the classification tests module. @@ -16,7 +16,7 @@ import pytest -from tests.dataset import Dataset +from tests.fixtures import Dataset from sklearn.datasets import make_classification, make_regression, make_blobs @@ -24,42 +24,50 @@ ## Fixtures ########################################################################## -@pytest.fixture(scope='class') + +@pytest.fixture(scope="class") def classification(request): """ Creates a random multiclass classification dataset fixture """ X, y = make_classification( - n_samples=500, n_features=20, n_informative=8, n_redundant=2, - n_classes=3, n_clusters_per_class=3, random_state=3902 + n_samples=500, + n_features=20, + n_informative=8, + n_redundant=2, + n_classes=3, + n_clusters_per_class=3, + random_state=3902, ) dataset = Dataset(X, y) request.cls.classification = dataset -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def regression(request): """ Creates a random regression dataset fixture """ X, y = make_regression( - n_samples=500, n_features=20, n_informative=8, - noise=0.01, bias=1.4, random_state=953, + n_samples=500, + n_features=20, + n_informative=8, + noise=0.01, + bias=1.4, + random_state=953, ) dataset = Dataset(X, y) request.cls.regression = dataset -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def clusters(request): """ Creates a random regression dataset fixture """ - X, y = make_blobs( - n_samples=500, n_features=20, centers=3, random_state=743, - ) + X, y = make_blobs(n_samples=500, n_features=20, centers=3, random_state=743) dataset = Dataset(X, y) request.cls.clusters = dataset diff --git a/tests/test_model_selection/test_cross_validation.py b/tests/test_model_selection/test_cross_validation.py index 516215178..bc5df7a13 100644 --- a/tests/test_model_selection/test_cross_validation.py +++ b/tests/test_model_selection/test_cross_validation.py @@ -1,32 +1,37 @@ # tests.test_model_selection.test_cross_validation # Tests for the CVScores visualizer # -# Author: Rebecca Bilbro +# Author: Rebecca Bilbro # Created: Fri Aug 10 13:45:11 2018 -0400 # -# ID: test_cross_validation.py [] bilbro@gmail.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_cross_validation.py [962c8bb] rebeccabilbro@users.noreply.github.com $ """ Tests for the CVScores visualizer """ ########################################################################## -## Imports +# Imports ########################################################################## import pytest import numpy.testing as npt +from unittest.mock import patch from tests.base import VisualTestCase -from tests.dataset import DatasetMixin from sklearn.svm import SVC from sklearn.naive_bayes import BernoulliNB from sklearn.tree import DecisionTreeRegressor +from sklearn.preprocessing import OneHotEncoder from sklearn.neighbors import KNeighborsClassifier -from sklearn.model_selection import ShuffleSplit, StratifiedKFold from sklearn.linear_model import RidgeCV, LogisticRegressionCV +from sklearn.model_selection import ShuffleSplit, StratifiedKFold +from yellowbrick.datasets import load_mushroom from yellowbrick.model_selection.cross_validation import * @@ -35,30 +40,26 @@ except ImportError: pd = None -try: - from unittest.mock import patch -except ImportError: - from mock import patch - ########################################################################## -## Test Cases +# Test Cases ########################################################################## + @pytest.mark.usefixtures("classification", "regression") -class TestCrossValidation(VisualTestCase, DatasetMixin): +class TestCrossValidation(VisualTestCase): """ Test the CVScores visualizer """ - @patch.object(CVScores, 'draw') + @patch.object(CVScores, "draw") def test_fit(self, mock_draw): """ Assert that fit returns self and creates expected properties """ X, y = self.classification - params = ("cv_scores_", "cv_scores_mean_") + params = ("cv_scores_", "cv_scores_mean_") oz = CVScores(SVC()) @@ -79,12 +80,10 @@ def test_classifier(self): cv = ShuffleSplit(3, random_state=288) - oz = CVScores( - KNeighborsClassifier(), cv=cv, scoring='f1_weighted', - ) + oz = CVScores(KNeighborsClassifier(), cv=cv, scoring="f1_weighted") oz.fit(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz, tol=2.0) @@ -96,16 +95,12 @@ def test_classifier_with_cv(self): cv = ShuffleSplit(3, random_state=288) - oz_external_cv = CVScores( - LogisticRegressionCV(), cv=cv - ) + oz_external_cv = CVScores(LogisticRegressionCV(), cv=cv) - oz_internal_cv = CVScores( - LogisticRegressionCV(cv=cv) - ) + oz_internal_cv = CVScores(LogisticRegressionCV(cv=cv)) - oz_external_cv.fit(X,y) - oz_internal_cv.fit(X,y) + oz_external_cv.fit(X, y) + oz_internal_cv.fit(X, y) npt.assert_array_almost_equal( oz_external_cv.cv_scores_, oz_internal_cv.cv_scores_, decimal=1 @@ -119,12 +114,10 @@ def test_regression(self): cv = ShuffleSplit(3, random_state=938) - oz = CVScores( - DecisionTreeRegressor(random_state=23), cv=cv, scoring='r2', - ) + oz = CVScores(DecisionTreeRegressor(random_state=23), cv=cv, scoring="r2") oz.fit(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz, tol=36.0) @@ -136,16 +129,12 @@ def test_regressor_with_cv(self): cv = ShuffleSplit(3, random_state=288) - oz_external_cv = CVScores( - RidgeCV(), cv=cv - ) + oz_external_cv = CVScores(RidgeCV(), cv=cv) - oz_internal_cv = CVScores( - RidgeCV(cv=cv) - ) + oz_internal_cv = CVScores(RidgeCV(cv=cv)) - oz_external_cv.fit(X,y) - oz_internal_cv.fit(X,y) + oz_external_cv.fit(X, y) + oz_internal_cv.fit(X, y) npt.assert_array_almost_equal( oz_external_cv.cv_scores_, oz_internal_cv.cv_scores_ @@ -158,22 +147,19 @@ def test_quick_method(self): X, y = self.classification cv = ShuffleSplit(n_splits=5, test_size=0.2, random_state=321) - ax = cv_scores(SVC(), X, y, cv=cv) + viz = cv_scores(SVC(), X, y, cv=cv) - self.assert_images_similar(ax=ax, tol=2.0) + self.assert_images_similar(viz, tol=2.0) @pytest.mark.skipif(pd is None, reason="test requires pandas") def test_pandas_integration(self): """ Test on mushroom dataset with pandas DataFrame and Series and NB """ - df = self.load_pandas("mushroom") + data = load_mushroom(return_dataset=True) + X, y = data.to_pandas() - target = "target" - features = [col for col in df.columns if col != target] - - X = pd.get_dummies(df[features]) - y = df[target] + X = pd.get_dummies(X) assert isinstance(X, pd.DataFrame) assert isinstance(y, pd.Series) @@ -182,6 +168,23 @@ def test_pandas_integration(self): oz = CVScores(BernoulliNB(), cv=cv) oz.fit(X, y) - oz.poof() + oz.finalize() + + self.assert_images_similar(oz, tol=2.0) + + def test_numpy_integration(self): + """ + Test on mushroom dataset with NumPy arrays + """ + data = load_mushroom(return_dataset=True) + X, y = data.to_numpy() + + X = OneHotEncoder().fit_transform(X).toarray() + + cv = StratifiedKFold(n_splits=2, random_state=11) + oz = CVScores(BernoulliNB(), cv=cv) + + oz.fit(X, y) + oz.finalize() self.assert_images_similar(oz, tol=2.0) diff --git a/tests/test_features/test_importances.py b/tests/test_model_selection/test_importances.py similarity index 68% rename from tests/test_features/test_importances.py rename to tests/test_model_selection/test_importances.py index f01905d35..4463e0152 100644 --- a/tests/test_features/test_importances.py +++ b/tests/test_model_selection/test_importances.py @@ -1,12 +1,11 @@ -# tests.test_features.test_importances +# tests.test_model_selection.test_importances # Test the feature importance visualizers # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Fri Mar 02 15:23:22 2018 -0500 -# Author: Rebecca Bilbro -# Updated: Sun Jun 24 12:10:43 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_importances.py [] benjamin@bengfort.com $ @@ -19,28 +18,23 @@ ## Imports ########################################################################## -import sys import pytest import numpy as np import numpy.testing as npt import matplotlib.pyplot as plt from yellowbrick.exceptions import NotFitted -from yellowbrick.features.importances import * +from yellowbrick.model_selection.importances import * +from yellowbrick.datasets import load_occupancy, load_concrete from sklearn.datasets import load_iris -from sklearn.base import BaseEstimator, ClassifierMixin -from sklearn.linear_model import LogisticRegression, Lasso from sklearn.ensemble import RandomForestClassifier from sklearn.ensemble import GradientBoostingClassifier +from sklearn.base import BaseEstimator, ClassifierMixin +from sklearn.linear_model import LogisticRegression, Lasso +from unittest import mock from tests.base import VisualTestCase -from tests.dataset import DatasetMixin - -try: - from unittest import mock -except ImportError: - import mock try: import pandas as pd @@ -52,28 +46,19 @@ ## Feature Importances Tests ########################################################################## -class TestFeatureImportancesVisualizer(VisualTestCase, DatasetMixin): + +class TestFeatureImportancesVisualizer(VisualTestCase): """ - FeatureImportances visualizer + Test FeatureImportances visualizer """ - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) def test_integration_feature_importances(self): """ Integration test of visualizer with feature importances param """ - occupancy = self.load_data('occupancy') - features = [ - "temperature", "relative_humidity", "light", "C02", "humidity" - ] - - # Extract X and y as numpy arrays - X = occupancy[features].copy() - X = X.view((float, len(X.dtype.names))) - y = occupancy['occupancy'].astype(int) + # Load the test dataset + X, y = load_occupancy(return_dataset=True).to_numpy() fig = plt.figure() ax = fig.add_subplot() @@ -81,62 +66,49 @@ def test_integration_feature_importances(self): clf = GradientBoostingClassifier(random_state=42) viz = FeatureImportances(clf, ax=ax) viz.fit(X, y) - viz.poof() + viz.finalize() - self.assert_images_similar(viz) + # Appveyor and Linux conda non-text-based differences + self.assert_images_similar(viz, tol=13.0) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) def test_integration_coef(self): """ Integration test of visualizer with coef param """ - concrete = self.load_data('concrete') - feats = ['cement','slag','ash','water','splast','coarse','fine','age'] - - # Create X and y datasets as numpy arrays - X = concrete[feats].copy() - X = X.view((float, len(X.dtype.names))) - y = concrete['strength'] + # Load the test dataset + dataset = load_concrete(return_dataset=True) + X, y = dataset.to_numpy() + features = dataset.meta["features"] fig = plt.figure() ax = fig.add_subplot() reg = Lasso(random_state=42) - feats = list(map(lambda s: s.title(), feats)) - viz = FeatureImportances(reg, ax=ax, labels=feats, relative=False) + features = list(map(lambda s: s.title(), features)) + viz = FeatureImportances(reg, ax=ax, labels=features, relative=False) viz.fit(X, y) - viz.poof() + viz.finalize() - self.assert_images_similar(viz) + # Appveyor and Linux conda non-text-based differences + self.assert_images_similar(viz, tol=16.2) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) def test_integration_quick_method(self): """ Integration test of quick method """ - occupancy = self.load_data('occupancy') - features = [ - "temperature", "relative_humidity", "light", "C02", "humidity" - ] - - # Create X and y datasets as numpy arrays - X = occupancy[features].copy() - X = X.view((float, len(X.dtype.names))) - y = occupancy['occupancy'].astype(int) + # Load the test dataset + X, y = load_occupancy(return_dataset=True).to_numpy() fig = plt.figure() ax = fig.add_subplot() clf = RandomForestClassifier(random_state=42) - g = feature_importances(clf, X, y, ax) + g = feature_importances(clf, X, y, ax=ax) - self.assert_images_similar(ax=g) + # Appveyor and Linux conda non-text-based differences + self.assert_images_similar(g, tol=15.0) def test_fit_no_importances_model(self): """ @@ -155,8 +127,8 @@ def test_fit_sorted_params(self): """ On fit, sorted features_ and feature_importances_ params are created """ - coefs = np.array([0.4, 0.2, 0.08, 0.07, 0.16, .23, 0.38, 0.1, 0.05]) - names = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']) + coefs = np.array([0.4, 0.2, 0.08, 0.07, 0.16, 0.23, 0.38, 0.1, 0.05]) + names = np.array(["a", "b", "c", "d", "e", "f", "g", "h", "i"]) model = MockEstimator() model.make_importance_param(value=coefs) @@ -164,8 +136,8 @@ def test_fit_sorted_params(self): visualizer = FeatureImportances(model, labels=names) visualizer.fit(np.random.rand(100, len(names)), np.random.rand(100)) - assert hasattr(visualizer, 'features_') - assert hasattr(visualizer, 'feature_importances_') + assert hasattr(visualizer, "features_") + assert hasattr(visualizer, "feature_importances_") # get the expected sort index sort_idx = np.argsort(coefs) @@ -178,7 +150,7 @@ def test_fit_relative(self): """ Test fit computes relative importances """ - coefs = np.array([0.4, 0.2, 0.08, 0.07, 0.16, .23, 0.38, 0.1, 0.05]) + coefs = np.array([0.4, 0.2, 0.08, 0.07, 0.16, 0.23, 0.38, 0.1, 0.05]) model = MockEstimator() model.make_importance_param(value=coefs) @@ -194,7 +166,7 @@ def test_fit_not_relative(self): """ Test fit stores unmodified importances """ - coefs = np.array([0.4, 0.2, 0.08, 0.07, 0.16, .23, 0.38, 0.1, 0.05]) + coefs = np.array([0.4, 0.2, 0.08, 0.07, 0.16, 0.23, 0.38, 0.1, 0.05]) model = MockEstimator() model.make_importance_param(value=coefs) @@ -209,7 +181,7 @@ def test_fit_absolute(self): """ Test fit with absolute values """ - coefs = np.array([0.4, 0.2, -0.08, 0.07, 0.16, .23, -0.38, 0.1, -0.05]) + coefs = np.array([0.4, 0.2, -0.08, 0.07, 0.16, 0.23, -0.38, 0.1, -0.05]) model = MockEstimator() model.make_importance_param(value=coefs) @@ -218,63 +190,62 @@ def test_fit_absolute(self): visualizer = FeatureImportances(model, absolute=True, relative=False) visualizer.fit(np.random.rand(100, len(coefs)), np.random.rand(100)) - expected = np.array([0.05, 0.07, 0.08, 0.1, 0.16, 0.2, .23, 0.38, 0.4]) + expected = np.array([0.05, 0.07, 0.08, 0.1, 0.16, 0.2, 0.23, 0.38, 0.4]) npt.assert_array_equal(visualizer.feature_importances_, expected) # Test no absolute value visualizer = FeatureImportances(model, absolute=False, relative=False) visualizer.fit(np.random.rand(100, len(coefs)), np.random.rand(100)) - expected = np.array([-0.38, -0.08, -0.05, 0.07, 0.1, 0.16, 0.2, .23, 0.4]) + expected = np.array([-0.38, -0.08, -0.05, 0.07, 0.1, 0.16, 0.2, 0.23, 0.4]) npt.assert_array_equal(visualizer.feature_importances_, expected) def test_multi_coefs(self): """ - Test fit with multidimensional coefficients + Test fit with multidimensional coefficients and stack warning """ - coefs = np.array([ - [0.4, 0.2, -0.08, 0.07, 0.16, 0.23, -0.38, 0.1, -0.05], - [0.41, 0.12, -0.1, 0.1, 0.14, 0.21, 0.01, 0.31, -0.15], - [0.31, 0.2, -0.01, 0.1, 0.22, 0.23, 0.01, 0.12, -0.15] + coefs = np.array( + [ + [0.4, 0.2, -0.08, 0.07, 0.16, 0.23, -0.38, 0.1, -0.05], + [0.41, 0.12, -0.1, 0.1, 0.14, 0.21, 0.01, 0.31, -0.15], + [0.31, 0.2, -0.01, 0.1, 0.22, 0.23, 0.01, 0.12, -0.15], ] ) model = MockEstimator() model.make_importance_param(value=coefs) - visualizer = FeatureImportances(model) - visualizer.fit( - np.random.rand(100, len(np.mean(coefs, axis=0))), np.random.rand(100) - ) + visualizer = FeatureImportances(model, stack=False) + + with pytest.warns(YellowbrickWarning): + visualizer.fit( + np.random.rand(100, len(np.mean(coefs, axis=0))), np.random.rand(100) + ) npt.assert_equal(visualizer.feature_importances_.ndim, 1) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) def test_multi_coefs_stacked(self): """ Test stack plot with multidimensional coefficients """ - X_iris, y_iris = load_iris(True) - X_iris_pd = pd.DataFrame(X_iris, columns=['f1', 'f2', 'f3', 'f4']) + X, y = load_iris(True) - viz = FeatureImportances(LogisticRegression(), stack=True) - viz.fit(X_iris_pd, y_iris) - viz.poof() + viz = FeatureImportances(LogisticRegression(random_state=222), stack=True) + viz.fit(X, y) + viz.finalize() npt.assert_equal(viz.feature_importances_.shape, (3, 4)) - self.assert_images_similar(viz) - + # Appveyor and Linux conda non-text-based differences + self.assert_images_similar(viz, tol=17.5) @pytest.mark.skipif(pd is None, reason="pandas is required for this test") def test_fit_dataframe(self): """ Ensure feature names are extracted from DataFrame columns """ - labels = ['a', 'b', 'c', 'd', 'e', 'f'] + labels = ["a", "b", "c", "d", "e", "f"] df = pd.DataFrame(np.random.rand(100, 6), columns=labels) - s = pd.Series(np.random.rand(100), name='target') + s = pd.Series(np.random.rand(100), name="target") assert df.shape == (100, 6) @@ -284,7 +255,7 @@ def test_fit_dataframe(self): visualizer = FeatureImportances(model) visualizer.fit(df, s) - assert hasattr(visualizer, 'features_') + assert hasattr(visualizer, "features_") npt.assert_array_equal(visualizer.features_, np.array(df.columns)) def test_fit_makes_labels(self): @@ -298,7 +269,7 @@ def test_fit_makes_labels(self): visualizer.fit(np.random.rand(100, 10), np.random.rand(100)) # Don't have to worry about label space since importances are linspace - assert hasattr(visualizer, 'features_') + assert hasattr(visualizer, "features_") npt.assert_array_equal(np.arange(10), visualizer.features_) def test_fit_calls_draw(self): @@ -306,12 +277,12 @@ def test_fit_calls_draw(self): Assert that fit calls draw """ model = MockEstimator() - model.make_importance_param('coef_') + model.make_importance_param("coef_") visualizer = FeatureImportances(model) - with mock.patch.object(visualizer, 'draw') as mdraw: - visualizer.fit(np.random.rand(100,42), np.random.rand(100)) + with mock.patch.object(visualizer, "draw") as mdraw: + visualizer.fit(np.random.rand(100, 42), np.random.rand(100)) mdraw.assert_called_once() def test_draw_raises_unfitted(self): @@ -326,35 +297,36 @@ def test_find_importances_param(self): """ Test the expected parameters can be found """ - params = ('feature_importances_', 'coef_') + params = ("feature_importances_", "coef_") for param in params: model = MockEstimator() - model.make_importance_param(param, 'foo') + model.make_importance_param(param, "foo") visualizer = FeatureImportances(model) assert hasattr(model, param), "expected '{}' missing".format(param) for oparam in params: - if oparam == param: continue + if oparam == param: + continue assert not hasattr(model, oparam), "unexpected '{}'".format(oparam) importances = visualizer._find_importances_param() - assert importances == 'foo' + assert importances == "foo" def test_find_importances_param_priority(self): """ With both feature_importances_ and coef_, one has priority """ model = MockEstimator() - model.make_importance_param('feature_importances_', 'foo') - model.make_importance_param('coef_', 'bar') + model.make_importance_param("feature_importances_", "foo") + model.make_importance_param("coef_", "bar") visualizer = FeatureImportances(model) - assert hasattr(model, 'feature_importances_') - assert hasattr(model, 'coef_') + assert hasattr(model, "feature_importances_") + assert hasattr(model, "coef_") importances = visualizer._find_importances_param() - assert importances == 'foo' + assert importances == "foo" def test_find_importances_param_not_found(self): """ @@ -363,8 +335,8 @@ def test_find_importances_param_not_found(self): model = MockEstimator() visualizer = FeatureImportances(model) - assert not hasattr(model, 'feature_importances_') - assert not hasattr(model, 'coef_') + assert not hasattr(model, "feature_importances_") + assert not hasattr(model, "coef_") with pytest.raises(YellowbrickTypeError): visualizer._find_importances_param() @@ -376,9 +348,9 @@ def test_find_classes_param_not_found(self): model = MockClassifier() visualizer = FeatureImportances(model) - assert not hasattr(model, 'classes_') + assert not hasattr(model, "classes_") - e = 'could not find classes_ param on {}'.format( + e = "could not find classes_ param on {}".format( visualizer.estimator.__class__.__name__ ) with pytest.raises(YellowbrickTypeError, match=e): @@ -389,7 +361,7 @@ def test_xlabel(self): Check the various xlabels are sensical """ model = MockEstimator() - model.make_importance_param('feature_importances_') + model.make_importance_param("feature_importances_") visualizer = FeatureImportances(model, xlabel="foo", relative=True) # Assert the visualizer uses the user supplied xlabel @@ -405,7 +377,7 @@ def test_xlabel(self): # Check coeficients model = MockEstimator() - model.make_importance_param('coef_') + model.make_importance_param("coef_") visualizer = FeatureImportances(model, xlabel="baz", relative=True) # Assert the visualizer uses the user supplied xlabel @@ -421,7 +393,6 @@ def test_xlabel(self): assert "coefficient" in visualizer._get_xlabel() assert "relative" not in visualizer._get_xlabel() - def test_is_fitted(self): """ Test identification if is fitted @@ -438,17 +409,41 @@ def test_is_fitted(self): del visualizer.features_ assert not visualizer._is_fitted() + def test_with_fitted(self): + """ + Test that visualizer properly handles an already-fitted model + """ + X, y = load_concrete(return_dataset=True).to_numpy() + + model = Lasso().fit(X, y) + + with mock.patch.object(model, "fit") as mockfit: + oz = FeatureImportances(model) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = FeatureImportances(model, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = FeatureImportances(model, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) + ########################################################################## ## Mock Estimator ########################################################################## + class MockEstimator(BaseEstimator): """ Creates params when fit is called on demand. """ - def make_importance_param(self, name='feature_importances_', value=None): + def make_importance_param(self, name="feature_importances_", value=None): if value is None: value = np.random.rand(42) setattr(self, name, value) @@ -461,4 +456,5 @@ class MockClassifier(BaseEstimator, ClassifierMixin): """ Creates empty classifier. """ + pass diff --git a/tests/test_model_selection/test_learning_curve.py b/tests/test_model_selection/test_learning_curve.py index 0d887dc67..422db91dd 100644 --- a/tests/test_model_selection/test_learning_curve.py +++ b/tests/test_model_selection/test_learning_curve.py @@ -1,9 +1,12 @@ # tests.test_model_selection.test_learning_curve # Tests for the LearningCurve visualizer # -# Author: Jason Keung +# Author: Jason Keung # Created: Tues May 23 11:45:00 2017 -0400 # +# Copyright (C) 2017 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: test_learning_curve.py jason.s.keung@gmail.com $ """ @@ -11,24 +14,26 @@ """ ########################################################################## -## Imports +# Imports ########################################################################## import sys import pytest import numpy as np +from unittest.mock import patch from tests.base import VisualTestCase -from tests.dataset import DatasetMixin from sklearn.svm import LinearSVC from sklearn.linear_model import Ridge from sklearn.naive_bayes import GaussianNB from sklearn.cluster import MiniBatchKMeans +from sklearn.preprocessing import OneHotEncoder from sklearn.model_selection import ShuffleSplit from sklearn.model_selection import StratifiedKFold from sklearn.ensemble import RandomForestClassifier +from yellowbrick.datasets import load_mushroom from yellowbrick.exceptions import YellowbrickValueError from yellowbrick.model_selection.learning_curve import * @@ -37,23 +42,19 @@ except ImportError: pd = None -try: - from unittest.mock import patch -except ImportError: - from mock import patch - ########################################################################## -## LearningCurve Test Cases +# LearningCurve Test Cases ########################################################################## + @pytest.mark.usefixtures("classification", "regression", "clusters") -class TestLearningCurve(VisualTestCase, DatasetMixin): +class TestLearningCurve(VisualTestCase): """ Test the LearningCurve visualizer """ - @patch.object(LearningCurve, 'draw') + @patch.object(LearningCurve, "draw") def test_fit(self, mock_draw): """ Assert that fit returns self and creates expected properties @@ -61,8 +62,12 @@ def test_fit(self, mock_draw): X, y = self.classification params = ( "train_sizes_", - "train_scores_", "train_scores_mean_", "train_scores_std_", - "test_scores_", "test_scores_mean_", "test_scores_std_" + "train_scores_", + "train_scores_mean_", + "train_scores_std_", + "test_scores_", + "test_scores_mean_", + "test_scores_std_", ) oz = LearningCurve(GaussianNB(), random_state=12) @@ -75,9 +80,7 @@ def test_fit(self, mock_draw): for param in params: assert hasattr(oz, param) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_classifier(self): """ Test image closeness on a classification dataset @@ -87,13 +90,11 @@ def test_classifier(self): oz = LearningCurve( RandomForestClassifier(random_state=21), random_state=12 ).fit(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_regressor(self): """ Test image closeness on a regression dataset @@ -102,7 +103,7 @@ def test_regressor(self): oz = LearningCurve(Ridge(), random_state=18) oz.fit(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz) @@ -112,44 +113,40 @@ def test_clusters(self): """ X, y = self.clusters - oz = LearningCurve( - MiniBatchKMeans(random_state=281), random_state=182 - ).fit(X) - oz.poof() + oz = LearningCurve(MiniBatchKMeans(random_state=281), random_state=182).fit(X) + oz.finalize() self.assert_images_similar(oz, tol=10) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_quick_method(self): """ Test the learning curve quick method acts as expected """ X, y = self.classification - train_sizes = np.linspace(.1, 1.0, 8) - ax = learning_curve(GaussianNB(), X, y, train_sizes=train_sizes, + train_sizes = np.linspace(0.1, 1.0, 8) + viz = learning_curve( + GaussianNB(), + X, + y, + train_sizes=train_sizes, cv=ShuffleSplit(n_splits=10, test_size=0.2, random_state=34), - scoring='f1_macro', random_state=43, + scoring="f1_macro", + random_state=43, ) - self.assert_images_similar(ax=ax) + self.assert_images_similar(viz) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") @pytest.mark.skipif(pd is None, reason="test requires pandas") def test_pandas_integration(self): """ Test on a real dataset with pandas DataFrame and Series """ - df = self.load_pandas("mushroom") - - target = "target" - features = [col for col in df.columns if col != target] + data = load_mushroom(return_dataset=True) + X, y = data.to_pandas() - X = pd.get_dummies(df[features]) - y = df[target] + X = pd.get_dummies(X) assert isinstance(X, pd.DataFrame) assert isinstance(y, pd.Series) @@ -157,11 +154,27 @@ def test_pandas_integration(self): cv = StratifiedKFold(n_splits=4, random_state=32) oz = LearningCurve(GaussianNB(), cv=cv, random_state=23) oz.fit(X, y) - oz.poof() + oz.finalize() + + self.assert_images_similar(oz) + + def test_numpy_integration(self): + """ + Test on a real dataset with NumPy arrays + """ + data = load_mushroom(return_dataset=True) + X, y = data.to_numpy() + + X = OneHotEncoder().fit_transform(X).toarray() + + cv = StratifiedKFold(n_splits=4, random_state=32) + oz = LearningCurve(GaussianNB(), cv=cv, random_state=23) + oz.fit(X, y) + oz.finalize() self.assert_images_similar(oz) - @patch.object(LearningCurve, 'draw') + @patch.object(LearningCurve, "draw") def test_reshape_scores(self, mock_draw): """ Test supplying an alternate CV methodology and train_sizes diff --git a/tests/test_features/test_rfecv.py b/tests/test_model_selection/test_rfecv.py similarity index 50% rename from tests/test_features/test_rfecv.py rename to tests/test_model_selection/test_rfecv.py index c1ceae966..8405011c6 100644 --- a/tests/test_features/test_rfecv.py +++ b/tests/test_model_selection/test_rfecv.py @@ -1,10 +1,13 @@ -# tests.test_feautures.test_rfecv +# tests.test_model_selection.test_rfecv # Tests for the RFECV visualizer # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Apr 03 17:35:16 2018 -0400 # -# ID: test_rfecv.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_rfecv.py [a4599db] rebeccabilbro@users.noreply.github.com $ """ Tests for the RFECV visualizer @@ -16,17 +19,21 @@ import sys import pytest +import numpy as np +import numpy.testing as npt -from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset +from unittest.mock import patch -from yellowbrick.features.rfecv import * +from tests.fixtures import Dataset +from tests.base import VisualTestCase +from yellowbrick.model_selection.rfecv import * +from yellowbrick.datasets import load_occupancy from yellowbrick.exceptions import YellowbrickValueError from sklearn.svm import SVC +from sklearn.datasets import make_classification from sklearn.model_selection import ShuffleSplit from sklearn.model_selection import StratifiedKFold -from sklearn.datasets import make_classification from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier @@ -35,24 +42,26 @@ except ImportError: pd = None -try: - from unittest.mock import patch -except ImportError: - from mock import patch - ########################################################################## ## Fixtures ########################################################################## + @pytest.fixture(scope="class") def dataset(request): """ Creates a multiclass classification dataset fixture for RFECV """ X, y = make_classification( - n_samples=600, n_features=15, n_informative=7, n_redundant=4, - n_repeated=0, n_classes=8, n_clusters_per_class=1, random_state=0 + n_samples=600, + n_features=15, + n_informative=7, + n_redundant=4, + n_repeated=0, + n_classes=8, + n_clusters_per_class=1, + random_state=0, ) dataset = Dataset(X, y) @@ -63,21 +72,26 @@ def dataset(request): ## Test Cases ########################################################################## + @pytest.mark.usefixtures("dataset") -class TestRFECV(VisualTestCase, DatasetMixin): +class TestRFECV(VisualTestCase): """ Test the RFECV visualizer """ - @patch.object(RFECV, 'draw') + @patch.object(RFECV, "draw") def test_fit(self, mock_draw): """ Assert that fit returns self and creates expected properties with NB """ X, y = self.dataset params = ( - "n_features_", "support_", "ranking_", - "cv_scores_", "rfe_estimator_", + "n_features_", + "support_", + "ranking_", + "cv_scores_", + "rfe_estimator_", + "n_feature_subsets_", ) rf = RandomForestClassifier() @@ -98,9 +112,7 @@ def test_fit(self, mock_draw): assert oz._wrapped is not rf assert oz._wrapped is oz.rfe_estimator_ - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_rfecv_classification(self): """ Test image closeness on a classification dataset with an SVM @@ -108,14 +120,12 @@ def test_rfecv_classification(self): cv = ShuffleSplit(3, random_state=21) oz = RFECV(SVC(kernel="linear", C=1), cv=cv) oz.fit(self.dataset.X, self.dataset.y) - oz.poof() + oz.finalize() - self.assert_images_similar(oz) + self.assert_images_similar(oz, remove_legend=True) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) - @pytest.mark.filterwarnings('ignore:F-score is ill-defined') + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + @pytest.mark.filterwarnings("ignore:F-score is ill-defined") def test_quick_method(self): """ Test the recv quick method works with LogisticRegression @@ -124,27 +134,18 @@ def test_quick_method(self): model = LogisticRegression() X, y = self.dataset - ax = rfecv(model, X, y, step=3, cv=cv, scoring='f1_weighted') + viz = rfecv(model, X, y, step=2, cv=cv, scoring="f1_weighted") - self.assert_images_similar(ax=ax) + self.assert_images_similar(viz, remove_legend=True) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") @pytest.mark.skipif(pd is None, reason="test requires pandas") def test_pandas_integration(self): """ Test on a real dataset with pandas DataFrame and Series """ - df = self.load_pandas("occupancy") - - target = "occupancy" - features = [ - 'temperature', 'relative humidity', 'light', 'C02', 'humidity' - ] - - X = df[features] - y = df[target] + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() assert isinstance(X, pd.DataFrame) assert isinstance(y, pd.Series) @@ -152,15 +153,56 @@ def test_pandas_integration(self): cv = StratifiedKFold(n_splits=4, random_state=32) oz = RFECV(RandomForestClassifier(random_state=83), cv=cv) oz.fit(X, y) - oz.poof() + oz.finalize() - self.assert_images_similar(oz) + self.assert_images_similar(oz, remove_legend=True) - def test_valid_step(self): + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_numpy_integration(self): + """ + Test on a real dataset with numpy ndarray + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) + + cv = StratifiedKFold(n_splits=4, random_state=32) + oz = RFECV(RandomForestClassifier(random_state=83), cv=cv) + oz.fit(X, y) + oz.finalize() + + self.assert_images_similar(oz, remove_legend=True) + + @pytest.mark.parametrize("step", [0, -1, -5]) + def test_invalid_step(self, step): """ Test step hyperparam validation """ - # TODO: parametrize when unittest is removed - with pytest.raises(YellowbrickValueError): - oz = RFECV(SVC(kernel="lnear"), step=-1) + with pytest.raises(YellowbrickValueError, match="step must be >0"): + oz = RFECV(SVC(kernel="linear"), step=step) oz.fit(self.dataset.X, self.dataset.y) + + def test_rfecv_step(self): + """ + Test RFECV step=5 with LogisticRegression + """ + X, y = make_classification( + n_samples=200, + n_features=30, + n_informative=18, + n_redundant=6, + n_repeated=0, + n_classes=8, + n_clusters_per_class=1, + random_state=0, + ) + + oz = RFECV(LogisticRegression(random_state=32), step=5).fit(X, y) + assert hasattr(oz, "n_feature_subsets_") + npt.assert_array_equal(oz.n_feature_subsets_, np.arange(1, 35, 5)) + + oz.finalize() + tol = 1.75 if sys.platform == "win32" else 0.25 + self.assert_images_similar(oz, tol=tol, remove_legend=True) diff --git a/tests/test_model_selection/test_validation_curve.py b/tests/test_model_selection/test_validation_curve.py index 0ec254331..664cc3ea0 100644 --- a/tests/test_model_selection/test_validation_curve.py +++ b/tests/test_model_selection/test_validation_curve.py @@ -1,32 +1,37 @@ # tests.test_model_selection.test_validation_curve # Tests for the ValidationCurve visualizer # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Sat Mar 31 06:25:05 2018 -0400 # -# ID: test_validation_curve.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_validation_curve.py [c5355ee] benjamin@bengfort.com $ """ Tests for the ValidationCurve visualizer """ ########################################################################## -## Imports +# Imports ########################################################################## import sys import pytest import numpy as np +from unittest.mock import patch from tests.base import VisualTestCase -from tests.dataset import DatasetMixin from sklearn.svm import SVC from sklearn.naive_bayes import BernoulliNB from sklearn.tree import DecisionTreeRegressor +from sklearn.preprocessing import OneHotEncoder from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import ShuffleSplit, StratifiedKFold +from yellowbrick.datasets import load_mushroom from yellowbrick.exceptions import YellowbrickValueError from yellowbrick.model_selection.validation_curve import * @@ -36,31 +41,31 @@ except ImportError: pd = None -try: - from unittest.mock import patch -except ImportError: - from mock import patch - ########################################################################## -## Test Cases +# Test Cases ########################################################################## + @pytest.mark.usefixtures("classification", "regression", "clusters") -class TestValidationCurve(VisualTestCase, DatasetMixin): +class TestValidationCurve(VisualTestCase): """ Test the ValidationCurve visualizer """ - @patch.object(ValidationCurve, 'draw') + @patch.object(ValidationCurve, "draw") def test_fit(self, mock_draw): """ Assert that fit returns self and creates expected properties """ X, y = self.classification params = ( - "train_scores_", "train_scores_mean_", "train_scores_std_", - "test_scores_", "test_scores_mean_", "test_scores_std_" + "train_scores_", + "train_scores_mean_", + "train_scores_std_", + "test_scores_", + "test_scores_mean_", + "test_scores_std_", ) oz = ValidationCurve( @@ -76,9 +81,7 @@ def test_fit(self, mock_draw): for param in params: assert hasattr(oz, param) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_classifier(self): """ Test image closeness on a classification dataset with kNN @@ -89,12 +92,15 @@ def test_classifier(self): param_range = np.arange(3, 10) oz = ValidationCurve( - KNeighborsClassifier(), param_name="n_neighbors", - param_range=param_range, cv=cv, scoring='f1_weighted', + KNeighborsClassifier(), + param_name="n_neighbors", + param_range=param_range, + cv=cv, + scoring="f1_weighted", ) oz.fit(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz) @@ -108,18 +114,19 @@ def test_regression(self): param_range = np.arange(3, 10) oz = ValidationCurve( - DecisionTreeRegressor(random_state=23), param_name="max_depth", - param_range=param_range, cv=cv, scoring='r2', + DecisionTreeRegressor(random_state=23), + param_name="max_depth", + param_range=param_range, + cv=cv, + scoring="r2", ) oz.fit(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz, tol=12.0) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_quick_method(self): """ Test validation curve quick method with image closeness on SVC @@ -128,42 +135,53 @@ def test_quick_method(self): pr = np.logspace(-6, -1, 3) cv = ShuffleSplit(n_splits=5, test_size=0.2, random_state=321) - ax = validation_curve( - SVC(), X, y, logx=True, param_name='gamma', param_range=pr, cv=cv + viz = validation_curve( + SVC(), X, y, logx=True, param_name="gamma", param_range=pr, cv=cv ) - self.assert_images_similar(ax=ax) + self.assert_images_similar(viz) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") @pytest.mark.skipif(pd is None, reason="test requires pandas") def test_pandas_integration(self): """ Test on mushroom dataset with pandas DataFrame and Series and NB """ - df = self.load_pandas("mushroom") + data = load_mushroom(return_dataset=True) + X, y = data.to_pandas() - target = "target" - features = [col for col in df.columns if col != target] - - X = pd.get_dummies(df[features]) - y = df[target] + X = pd.get_dummies(X) assert isinstance(X, pd.DataFrame) assert isinstance(y, pd.Series) cv = StratifiedKFold(n_splits=2, random_state=11) pr = np.linspace(0.1, 3.0, 6) - oz = ValidationCurve( - BernoulliNB(), cv=cv, param_range=pr, param_name='alpha' - ) + oz = ValidationCurve(BernoulliNB(), cv=cv, param_range=pr, param_name="alpha") + oz.fit(X, y) + oz.finalize() + + self.assert_images_similar(oz) + + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") + def test_numpy_integration(self): + """ + Test on mushroom dataset with NumPy arrays + """ + data = load_mushroom(return_dataset=True) + X, y = data.to_numpy() + + X = OneHotEncoder().fit_transform(X).toarray() + + cv = StratifiedKFold(n_splits=2, random_state=11) + pr = np.linspace(0.1, 3.0, 6) + oz = ValidationCurve(BernoulliNB(), cv=cv, param_range=pr, param_name="alpha") oz.fit(X, y) - oz.poof() + oz.finalize() self.assert_images_similar(oz) - @patch.object(ValidationCurve, 'draw') + @patch.object(ValidationCurve, "draw") def test_reshape_scores(self, mock_draw): """ Test supplying an alternate CV methodology and train_sizes @@ -172,7 +190,7 @@ def test_reshape_scores(self, mock_draw): pr = np.logspace(-6, -1, 3) cv = ShuffleSplit(n_splits=5, test_size=0.2, random_state=14) - oz = ValidationCurve(SVC(), param_name='gamma', param_range=pr, cv=cv) + oz = ValidationCurve(SVC(), param_name="gamma", param_range=pr, cv=cv) oz.fit(X, y) assert oz.train_scores_.shape == (3, 5) @@ -183,4 +201,4 @@ def test_bad_train_sizes(self): Test learning curve with bad input for training size. """ with pytest.raises(YellowbrickValueError): - ValidationCurve(SVC(), param_name='gamma', param_range=100) + ValidationCurve(SVC(), param_name="gamma", param_range=100) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 940dc7e83..b642f795a 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Fri Oct 07 22:10:50 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_pipeline.py [1efae1f] benjamin@bengfort.com $ @@ -18,34 +18,30 @@ ########################################################################## import os -import unittest +import pytest +from unittest import mock from yellowbrick.base import Visualizer from yellowbrick.pipeline import VisualPipeline from sklearn.pipeline import Pipeline from sklearn.base import BaseEstimator, TransformerMixin -try: - from unittest import mock -except ImportError: - import mock - ########################################################################## ## Mock Objects ########################################################################## + class Thing(object): pass class MockEstimator(BaseEstimator): - def fit(self, X, y=None, **kwargs): return self -class MockVisualEstimator(Visualizer): +class MockVisualEstimator(Visualizer): def fit(self, X, y=None, **kwargs): self.draw(**kwargs) return self @@ -55,7 +51,6 @@ def draw(self, **kwargs): class MockTransformer(BaseEstimator, TransformerMixin): - def fit(self, X, y=None, **kwargs): return self @@ -64,7 +59,6 @@ def transform(self, X, **kwargs): class MockVisualTransformer(Visualizer, TransformerMixin): - def fit(self, X, y=None, **kwargs): self.draw(**kwargs) return self @@ -80,8 +74,8 @@ def draw(self, **kwargs): ## VisualPipeline Tests ########################################################################## -class VisualPipelineTests(unittest.TestCase): +class TestVisualPipeline(object): def test_validate_steps(self): """ Assert that visual transformers can be added to pipelines @@ -91,52 +85,54 @@ def test_validate_steps(self): # TypeError if the steps don't match transforms --> estimator. # validate a bad intermediate transformer on the Pipeline - with self.assertRaises(TypeError): - Pipeline([ - ('real', MockTransformer()), - ('bad', Thing()), - ('model', MockEstimator()), - ]) + with pytest.raises(TypeError): + Pipeline( + [ + ("real", MockTransformer()), + ("bad", Thing()), + ("model", MockEstimator()), + ] + ) # validate a bad intermediate transformer on the VisualPipeline - with self.assertRaises(TypeError): - VisualPipeline([ - ('real', MockTransformer()), - ('bad', Thing()), - ('model', MockEstimator()), - ]) + with pytest.raises(TypeError): + VisualPipeline( + [ + ("real", MockTransformer()), + ("bad", Thing()), + ("model", MockEstimator()), + ] + ) # validate a bad final estimator on the Pipeline - with self.assertRaises(TypeError): - Pipeline([ - ('real', MockTransformer()), - ('bad', Thing()), - ]) + with pytest.raises(TypeError): + Pipeline([("real", MockTransformer()), ("bad", Thing())]) # validate a bad final estimator on the VisualPipeline - with self.assertRaises(TypeError): - VisualPipeline([ - ('real', MockTransformer()), - ('bad', Thing()), - ]) + with pytest.raises(TypeError): + VisualPipeline([("real", MockTransformer()), ("bad", Thing())]) # validate visual transformers on a Pipeline try: - Pipeline([ - ('real', MockTransformer()), - ('visual', MockVisualTransformer()), - ('model', MockEstimator()), - ]) + Pipeline( + [ + ("real", MockTransformer()), + ("visual", MockVisualTransformer()), + ("model", MockEstimator()), + ] + ) except TypeError: self.fail("could not add a visual transformer to a Pipeline!") # validate visual transformers on a VisualPipeline try: - VisualPipeline([ - ('real', MockTransformer()), - ('visual', MockVisualTransformer()), - ('model', MockEstimator()), - ]) + VisualPipeline( + [ + ("real", MockTransformer()), + ("visual", MockVisualTransformer()), + ("model", MockEstimator()), + ] + ) except TypeError: self.fail("could not add a visual transformer to a VisualPipeline!") @@ -145,32 +141,36 @@ def test_visual_steps_property(self): Test the visual steps property to filter visualizers """ - pipeline = VisualPipeline([ - ('a', MockTransformer()), - ('b', MockVisualTransformer()), - ('c', MockTransformer()), - ('d', MockVisualTransformer()), - ('e', MockEstimator()), - ]) - - self.assertNotIn('a', pipeline.visual_steps) - self.assertIn('b', pipeline.visual_steps) - self.assertNotIn('c', pipeline.visual_steps) - self.assertIn('d', pipeline.visual_steps) - self.assertNotIn('e', pipeline.visual_steps) + pipeline = VisualPipeline( + [ + ("a", MockTransformer()), + ("b", MockVisualTransformer()), + ("c", MockTransformer()), + ("d", MockVisualTransformer()), + ("e", MockEstimator()), + ] + ) + + assert "a" not in pipeline.visual_steps + assert "b" in pipeline.visual_steps + assert "c" not in pipeline.visual_steps + assert "d" in pipeline.visual_steps + assert "e" not in pipeline.visual_steps def test_pipeline_poof(self): """ Test the poof call against the VisualPipeline """ - pipeline = VisualPipeline([ - ('a', mock.MagicMock(MockTransformer())), - ('b', mock.MagicMock(MockVisualTransformer())), - ('c', mock.MagicMock(MockTransformer())), - ('d', mock.MagicMock(MockVisualTransformer())), - ('e', mock.MagicMock(MockEstimator()),) - ]) + pipeline = VisualPipeline( + [ + ("a", mock.MagicMock(MockTransformer())), + ("b", mock.MagicMock(MockVisualTransformer())), + ("c", mock.MagicMock(MockTransformer())), + ("d", mock.MagicMock(MockVisualTransformer())), + ("e", mock.MagicMock(MockEstimator())), + ] + ) pipeline.poof() pipeline.steps[1][1].poof.assert_called_once_with(outpath=None) @@ -180,41 +180,49 @@ def test_pipeline_savefig_poof(self): """ Test the poof call with an outdir to save all the figures """ - pipeline = VisualPipeline([ - ('a', mock.MagicMock(MockTransformer())), - ('b', mock.MagicMock(MockVisualTransformer())), - ('c', mock.MagicMock(MockTransformer())), - ('d', mock.MagicMock(MockVisualTransformer())), - ('e', mock.MagicMock(MockVisualEstimator()),) - ]) + pipeline = VisualPipeline( + [ + ("a", mock.MagicMock(MockTransformer())), + ("b", mock.MagicMock(MockVisualTransformer())), + ("c", mock.MagicMock(MockTransformer())), + ("d", mock.MagicMock(MockVisualTransformer())), + ("e", mock.MagicMock(MockVisualEstimator())), + ] + ) # Must use path joining for Windows compatibility tmpdir = os.path.join("tmp", "figures") pipeline.poof(outdir=tmpdir) - pipeline.steps[1][1].poof.assert_called_once_with(outpath=os.path.join(tmpdir, "b.pdf")) - pipeline.steps[3][1].poof.assert_called_once_with(outpath=os.path.join(tmpdir, "d.pdf")) - pipeline.steps[4][1].poof.assert_called_once_with(outpath=os.path.join(tmpdir, "e.pdf")) - - @unittest.skip("need to find a way for fit to return self in mocks") + pipeline.steps[1][1].poof.assert_called_once_with( + outpath=os.path.join(tmpdir, "b.pdf") + ) + pipeline.steps[3][1].poof.assert_called_once_with( + outpath=os.path.join(tmpdir, "d.pdf") + ) + pipeline.steps[4][1].poof.assert_called_once_with( + outpath=os.path.join(tmpdir, "e.pdf") + ) + + @pytest.mark.skip(reason="need to find a way for fit to return self in mocks") def test_fit_transform_poof_and_draw_calls(self): """ Test calling fit, transform, and poof on the pipeline """ - pipeline = VisualPipeline([ - ('a', mock.MagicMock(MockTransformer())), - ('b', mock.MagicMock(MockVisualTransformer())), - ('c', mock.MagicMock(MockTransformer())), - ('d', mock.MagicMock(MockVisualTransformer())), - ('e', mock.MagicMock(MockEstimator()),) - ]) + pipeline = VisualPipeline( + [ + ("a", mock.MagicMock(MockTransformer())), + ("b", mock.MagicMock(MockVisualTransformer())), + ("c", mock.MagicMock(MockTransformer())), + ("d", mock.MagicMock(MockVisualTransformer())), + ("e", mock.MagicMock(MockEstimator())), + ] + ) - X = [[1, 1, 1, 1, 1], - [2, 2, 2, 2, 2], - [3, 3, 3, 3, 3]] + X = [[1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3]] - y = [1, 2, 3, 4, 5] + y = [1, 2, 3, 4, 5] pipeline.fit(X, y) for name, step in pipeline.named_steps.items(): @@ -222,10 +230,12 @@ def test_fit_transform_poof_and_draw_calls(self): pipeline.transform(X) for name, step in pipeline.named_steps.items(): - if name == 'e': continue + if name == "e": + continue step.transform.assert_called_once_with(X) pipeline.poof() for name, step in pipeline.named_steps.items(): - if name in {'a', 'c', 'e'}: continue + if name in {"a", "c", "e"}: + continue step.poof.assert_called_once_with(outpath=None) diff --git a/tests/test_regressor/__init__.py b/tests/test_regressor/__init__.py index 891dc2116..39eaa1d77 100644 --- a/tests/test_regressor/__init__.py +++ b/tests/test_regressor/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Mon Mar 06 22:01:34 2017 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [7d3f5e6] benjamin@bengfort.com $ diff --git a/tests/test_regressor/test_alphas.py b/tests/test_regressor/test_alphas.py index e7bf34608..6f43feba5 100644 --- a/tests/test_regressor/test_alphas.py +++ b/tests/test_regressor/test_alphas.py @@ -1,10 +1,10 @@ # tests.test_regressor.test_alphas # Tests for the alpha selection visualizations. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Mar 07 12:13:04 2017 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_alphas.py [7d3f5e6] benjamin@bengfort.com $ @@ -29,6 +29,8 @@ from yellowbrick.exceptions import YellowbrickValueError from sklearn.svm import SVR, SVC +from sklearn.cluster import KMeans +from sklearn.decomposition import PCA from sklearn.datasets import make_regression from sklearn.linear_model import Ridge, RidgeCV from sklearn.linear_model import Lasso, LassoCV @@ -40,14 +42,13 @@ ## Alpha Selection Tests ########################################################################## + class TestAlphaSelection(VisualTestCase): """ Test the AlphaSelection visualizer """ - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_similar_image(self): """ Integration test with image simiarlity comparison @@ -57,33 +58,35 @@ def test_similar_image(self): X, y = make_regression(random_state=0) visualizer.fit(X, y) - visualizer.poof() + visualizer.finalize() self.assert_images_similar(visualizer) - def test_regressor_cv(self): + @pytest.mark.parametrize("model", [SVR, Ridge, Lasso, LassoLars, ElasticNet]) + def test_regressor_nocv(self, model): """ Ensure only "CV" regressors are allowed """ - # TODO: parametrize with models when unittest dependency removed - for model in (SVR, Ridge, Lasso, LassoLars, ElasticNet): - with pytest.raises(YellowbrickTypeError): - AlphaSelection(model()) + with pytest.raises(YellowbrickTypeError): + AlphaSelection(model()) - # TODO: parametrize with models when unittest dependency removed (new test case) - for model in (RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV): - try: - AlphaSelection(model()) - except YellowbrickTypeError: - pytest.fail("could not instantiate RegressorCV on alpha selection") + @pytest.mark.parametrize("model", [RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV]) + def test_regressor_cv(self, model): + """ + Ensure "CV" regressors are allowed + """ + try: + AlphaSelection(model()) + except YellowbrickTypeError: + pytest.fail("could not instantiate RegressorCV on alpha selection") - def test_only_regressors(self): + @pytest.mark.parametrize("model", [SVC, KMeans, PCA]) + def test_only_regressors(self, model): """ Assert AlphaSelection only works with regressors """ - # TODO: parameterize with classifier, clusterer, decomposition with pytest.raises(YellowbrickTypeError): - AlphaSelection(SVC()) + AlphaSelection(model()) def test_store_cv_values(self): """ @@ -99,21 +102,19 @@ def test_store_cv_values(self): model = AlphaSelection(RidgeCV(store_cv_values=False)) assert model.estimator.store_cv_values - def test_get_alphas_param(self): + @pytest.mark.parametrize("model", [RidgeCV, LassoCV, ElasticNetCV]) + def test_get_alphas_param(self, model): """ - Assert that we can get the alphas from ridge, lasso, and elasticnet + Assert that we can get the alphas from original CV models """ alphas = np.logspace(-10, -2, 100) - # Test original CV models - # TODO: parametrize this test with different models - for model in (RidgeCV, LassoCV, ElasticNetCV): - try: - model = AlphaSelection(model(alphas=alphas)) - malphas = model._find_alphas_param() - assert_array_equal(alphas, malphas) - except YellowbrickValueError: - pytest.fail("could not find alphas on {}".format(model.name)) + try: + model = AlphaSelection(model(alphas=alphas)) + malphas = model._find_alphas_param() + assert_array_equal(alphas, malphas) + except YellowbrickValueError: + pytest.fail("could not find alphas on {}".format(model.name)) def test_get_alphas_param_lassolars(self): """ @@ -128,24 +129,21 @@ def test_get_alphas_param_lassolars(self): except YellowbrickValueError: pytest.fail("could not find alphas on {}".format(model.name)) - def test_get_errors_param(self): + @pytest.mark.parametrize("model", [RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV]) + def test_get_errors_param(self, model): """ Test known models we can get the cv errors for alpha selection """ + try: + model = AlphaSelection(model()) - # Test original CV models - # TODO: parametrize this test with different models - for model in (RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV): - try: - model = AlphaSelection(model()) - - X, y = make_regression() - model.fit(X, y) + X, y = make_regression() + model.fit(X, y) - errors = model._find_errors_param() - assert len(errors) > 0 - except YellowbrickValueError: - pytest.fail("could not find errors on {}".format(model.name)) + errors = model._find_errors_param() + assert len(errors) > 0 + except YellowbrickValueError: + pytest.fail("could not find errors on {}".format(model.name)) def test_score(self): """ diff --git a/tests/test_regressor/test_influence.py b/tests/test_regressor/test_influence.py new file mode 100644 index 000000000..f07f81054 --- /dev/null +++ b/tests/test_regressor/test_influence.py @@ -0,0 +1,173 @@ +# tests.test_regressor.test_influence +# Test the regressor influence visualizers. +# +# Author: Benjamin Bengfort +# Created: Sun Jun 09 16:03:31 2019 -0400 +# +# Copyright (C) 2019 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_influence.py [fe14cfd] benjamin@bengfort.com $ + +""" +Test the regressor influence visualizers. +""" + +########################################################################## +## Imports +########################################################################## + +import pytest +import numpy as np +import matplotlib.pyplot as plt + +from tests.base import VisualTestCase +from tests.fixtures import Dataset +from sklearn.datasets import make_regression + +from yellowbrick.regressor.influence import * +from yellowbrick.datasets import load_concrete + +try: + import pandas as pd +except ImportError: + pd = None + + +########################################################################## +## Fixtures +########################################################################## + + +@pytest.fixture(scope="class") +def data(request): + """ + Creates a random regression fixture that has a R2 score below 0.85 and several + outliers that best demonstrate the effectiveness of influence visualizers. + """ + X, y = make_regression( + n_samples=100, + n_features=14, + n_informative=6, + bias=1.2, + noise=49.8, + tail_strength=0.6, + random_state=637, + ) + + request.cls.data = Dataset(X, y) + + +########################################################################## +## Assertion Helpers +########################################################################## + +LEARNED_FIELDS = ( + "distance_", + "p_values_", + "influence_threshold_", + "outlier_percentage_", +) + + +def assert_not_fitted(oz): + for field in LEARNED_FIELDS: + assert not hasattr(oz, field) + + +def assert_fitted(oz): + for field in LEARNED_FIELDS: + assert hasattr(oz, field) + + +########################################################################## +## Test CooksDistance Visualizer +########################################################################## + + +@pytest.mark.usefixtures("data") +class TestCooksDistance(VisualTestCase): + """ + CooksDistance visual test cases + """ + + def test_cooks_distance(self): + """ + Test image similarity of Cook's Distance on a random dataset + """ + _, ax = plt.subplots() + viz = CooksDistance(ax=ax) + + assert_not_fitted(viz) + assert viz.fit(self.data.X, self.data.y) is viz + assert_fitted(viz) + + # Test fitted values + assert viz.distance_.shape == (self.data.X.shape[0],) + assert viz.p_values_.shape == viz.distance_.shape + assert 0.0 <= viz.influence_threshold_ <= 4.0 + assert 0.0 <= viz.outlier_percentage_ <= 100.0 + + self.assert_images_similar(viz) + + def test_cooks_distance_quickmethod(self): + """ + Test the cooks_distance quick method on a random dataset + """ + _, ax = plt.subplots() + viz = cooks_distance( + self.data.X, + self.data.y, + ax=ax, + draw_threshold=False, + linefmt="r-", + markerfmt="ro", + ) + + assert_fitted(viz) + self.assert_images_similar(viz) + + @pytest.mark.skipif(pd is None, reason="test requires pandas") + def test_pandas_integration(self): + """ + Test on the concrete dataset with pandas DataFrame and Series + """ + data = load_concrete(return_dataset=True) + X, y = data.to_pandas() + + assert isinstance(X, pd.DataFrame) + assert isinstance(y, pd.Series) + + _, ax = plt.subplots() + viz = CooksDistance(ax=ax).fit(X, y) + assert_fitted(viz) + + assert viz.distance_.sum() == pytest.approx(1.2911900571300652) + assert viz.p_values_.sum() == pytest.approx(1029.9999525376425) + assert viz.influence_threshold_ == pytest.approx(0.003883495145631068) + assert viz.outlier_percentage_ == pytest.approx(7.3786407766990285) + + viz.finalize() + self.assert_images_similar(viz) + + def test_numpy_integration(self): + """ + Test on concrete dataset with numpy arrays + """ + data = load_concrete(return_dataset=True) + X, y = data.to_numpy() + + assert isinstance(X, np.ndarray) + assert isinstance(y, np.ndarray) + + _, ax = plt.subplots() + viz = CooksDistance(ax=ax).fit(X, y) + assert_fitted(viz) + + assert viz.distance_.sum() == pytest.approx(1.2911900571300652) + assert viz.p_values_.sum() == pytest.approx(1029.9999525376425) + assert viz.influence_threshold_ == pytest.approx(0.003883495145631068) + assert viz.outlier_percentage_ == pytest.approx(7.3786407766990285) + + viz.finalize() + self.assert_images_similar(viz) diff --git a/tests/test_regressor/test_residuals.py b/tests/test_regressor/test_residuals.py index 4ab36e8c0..5550bd193 100644 --- a/tests/test_regressor/test_residuals.py +++ b/tests/test_regressor/test_residuals.py @@ -1,11 +1,11 @@ # tests.test_regressor.test_residuals # Ensure that the regressor residuals visualizations work. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Sat Oct 8 16:30:39 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_residuals.py [7d3f5e6] benjamin@bengfort.com $ @@ -23,17 +23,18 @@ import matplotlib as mpl import matplotlib.pyplot as plt +from yellowbrick.datasets import load_energy from yellowbrick.regressor.residuals import * from yellowbrick.exceptions import YellowbrickValueError -from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset, Split +from unittest import mock +from tests.fixtures import Dataset, Split +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase +from sklearn.datasets import make_regression from sklearn.linear_model import Ridge, Lasso -from sklearn.linear_model import LinearRegression from sklearn.neural_network import MLPRegressor - -from sklearn.datasets import make_regression +from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split as tts @@ -42,11 +43,6 @@ except ImportError: pd = None -try: - from unittest import mock -except ImportError: - import mock - # Determine version of matplotlib MPL_VERS_MAJ = int(mpl.__version__.split(".")[0]) @@ -55,25 +51,26 @@ ## Data ########################################################################## -@pytest.fixture(scope='class') + +@pytest.fixture(scope="class") def data(request): """ Creates a fixture of train and test splits for the sklearn digits dataset For ease of use returns a Dataset named tuple composed of two Split tuples. """ X, y = make_regression( - n_samples=500, n_features=22, n_informative=8, random_state=42, - noise=0.2, bias=0.2, + n_samples=500, + n_features=22, + n_informative=8, + random_state=42, + noise=0.2, + bias=0.2, ) - X_train, X_test, y_train, y_test = tts( - X, y, test_size=0.2, random_state=11 - ) + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=11) # Set a class attribute for digits - request.cls.data = Dataset( - Split(X_train, X_test), Split(y_train, y_test) - ) + request.cls.data = Dataset(Split(X_train, X_test), Split(y_train, y_test)) ########################################################################## @@ -82,7 +79,7 @@ def data(request): @pytest.mark.usefixtures("data") -class TestPredictionError(VisualTestCase, DatasetMixin): +class TestPredictionError(VisualTestCase): """ Test the PredictionError visualizer """ @@ -112,17 +109,29 @@ def test_prediction_error_pandas(self): _, ax = plt.subplots() # Load the occupancy dataset from fixtures - data = self.load_data('energy') - target = 'cooling_load' - features = [ - "relative_compactness", "surface_area", "wall_area", "roof_area", - "overall_height", "orientation", "glazing_area", - "glazing_area_distribution" - ] - - # Create instances and target - X = pd.DataFrame(data[features]) - y = pd.Series(data[target].astype(float)) + data = load_energy(return_dataset=True) + X, y = data.to_pandas() + + # Create train/test splits + splits = tts(X, y, test_size=0.2, random_state=8873) + X_train, X_test, y_train, y_test = splits + + visualizer = PredictionError(Ridge(random_state=22), ax=ax) + visualizer.fit(X_train, y_train) + visualizer.score(X_test, y_test) + visualizer.finalize() + + self.assert_images_similar(visualizer, tol=1, remove_legend=True) + + def test_prediction_error_numpy(self): + """ + Test NumPy real world dataset with image similarity on Ridge + """ + _, ax = plt.subplots() + + # Load the occupancy dataset from fixtures + data = load_energy(return_dataset=True) + X, y = data.to_numpy() # Create train/test splits splits = tts(X, y, test_size=0.2, random_state=8873) @@ -199,9 +208,7 @@ def test_alpha_param(self): # Instantiate a sklearn regressor model = Lasso(random_state=23, alpha=10) # Instantiate a prediction error plot, provide custom alpha - visualizer = PredictionError( - model, bestfit=False, identity=False, alpha=0.7 - ) + visualizer = PredictionError(model, bestfit=False, identity=False, alpha=0.7) # Test param gets set correctly assert visualizer.alpha == 0.7 @@ -216,19 +223,48 @@ def test_alpha_param(self): assert "alpha" in scatter_kwargs assert scatter_kwargs["alpha"] == 0.7 + @pytest.mark.xfail( + reason="""third test fails with AssertionError: Expected fit + to be called once. Called 0 times.""" + ) + def test_peplot_with_fitted(self): + """ + Test that PredictionError properly handles an already-fitted model + """ + X, y = load_energy(return_dataset=True).to_numpy() + + model = Ridge().fit(X, y) + + with mock.patch.object(model, "fit") as mockfit: + oz = PredictionError(model) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = PredictionError(model, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = PredictionError(model, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) + ########################################################################## ## Residuals Plot Test Cases ########################################################################## + @pytest.mark.usefixtures("data") -class TestResidualsPlot(VisualTestCase, DatasetMixin): +class TestResidualsPlot(VisualTestCase): """ Test ResidualPlot visualizer """ @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=32)" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) def test_residuals_plot(self): """ @@ -242,10 +278,10 @@ def test_residuals_plot(self): visualizer.score(self.data.X.test, self.data.y.test) visualizer.finalize() - self.assert_images_similar(visualizer, tol=1, remove_legend=True) + self.assert_images_similar(visualizer) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=32)" + sys.platform == "win32", reason="images not close on windows (RMSE=32)" ) @pytest.mark.filterwarnings("ignore:Stochastic Optimizer") def test_residuals_plot_no_histogram(self): @@ -263,25 +299,31 @@ def test_residuals_plot_no_histogram(self): self.assert_images_similar(visualizer, tol=1, remove_legend=True) - @pytest.mark.skipif(MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2") + @pytest.mark.skipif( + MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2" + ) def test_hist_matplotlib_version(self, mock_toolkit): """ ValueError is raised when matplotlib version is incorrect and hist=True """ with pytst.raises(ImportError): from mpl_toolkits.axes_grid1 import make_axes_locatable + assert not make_axes_locatable with pytest.raises(YellowbrickValueError, match="requires matplotlib 2.0.2"): ResidualsPlot(LinearRegression(), hist=True) - @pytest.mark.skipif(MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2") + @pytest.mark.skipif( + MPL_VERS_MAJ >= 2, reason="test requires mpl earlier than 2.0.2" + ) def test_no_hist_matplotlib_version(self, mock_toolkit): """ No error is raised when matplotlib version is incorrect and hist=False """ - with pytst.raises(ImportError): + with pytest.raises(ImportError): from mpl_toolkits.axes_grid1 import make_axes_locatable + assert not make_axes_locatable try: @@ -290,7 +332,8 @@ def test_no_hist_matplotlib_version(self, mock_toolkit): self.fail(e) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=32)" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) def test_residuals_quick_method(self): """ @@ -299,14 +342,15 @@ def test_residuals_quick_method(self): _, ax = plt.subplots() model = Lasso(random_state=19) - ax = residuals_plot( + oz = residuals_plot( model, self.data.X.train, self.data.y.train, ax=ax, random_state=23 ) - self.assert_images_similar(ax=ax, tol=1, remove_legend=True) + self.assert_images_similar(oz) @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows (RMSE=32)" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) @pytest.mark.skipif(pd is None, reason="pandas is required") def test_residuals_plot_pandas(self): @@ -316,17 +360,8 @@ def test_residuals_plot_pandas(self): _, ax = plt.subplots() # Load the occupancy dataset from fixtures - data = self.load_data('energy') - target = 'heating_load' - features = [ - "relative_compactness", "surface_area", "wall_area", "roof_area", - "overall_height", "orientation", "glazing_area", - "glazing_area_distribution" - ] - - # Create instances and target - X = pd.DataFrame(data[features]) - y = pd.Series(data[target].astype(float)) + data = load_energy(return_dataset=True) + X, y = data.to_pandas() # Create train/test splits splits = tts(X, y, test_size=0.2, random_state=231) @@ -337,7 +372,28 @@ def test_residuals_plot_pandas(self): visualizer.score(X_test, y_test) visualizer.finalize() - self.assert_images_similar(visualizer, tol=1, remove_legend=True) + self.assert_images_similar(visualizer) + + def test_residuals_plot_numpy(self): + """ + Test NumPy real world dataset with image similarity on Lasso + """ + _, ax = plt.subplots() + + # Load the occupancy dataset from fixtures + data = load_energy(return_dataset=True) + X, y = data.to_numpy() + + # Create train/test splits + splits = tts(X, y, test_size=0.2, random_state=231) + X_train, X_test, y_train, y_test = splits + + visualizer = ResidualsPlot(Lasso(random_state=44), ax=ax) + visualizer.fit(X_train, y_train) + visualizer.score(X_test, y_test) + visualizer.finalize() + + self.assert_images_similar(visualizer, tol=1.5) def test_score(self): """ @@ -352,18 +408,18 @@ def test_score(self): assert visualizer.train_score_ == pytest.approx(0.9999906, rel=1e-4) assert visualizer.test_score_ == score - @mock.patch('yellowbrick.regressor.residuals.plt.sca', autospec=True) + @mock.patch("yellowbrick.regressor.residuals.plt.sca", autospec=True) def test_alpha_param(self, mock_sca): """ Test that the user can supply an alpha param on instantiation """ # Instantiate a prediction error plot, provide custom alpha visualizer = ResidualsPlot( - Ridge(random_state=8893), alpha=0.3, hist=False + Ridge(random_state=8893), train_alpha=0.3, test_alpha=0.75, hist=False ) - + alphas = {"train_point": 0.3, "test_point": 0.75} # Test param gets set correctly - assert visualizer.alpha == 0.3 + assert visualizer.alphas == alphas visualizer.ax = mock.MagicMock() visualizer.fit(self.data.X.train, self.data.y.train) @@ -372,4 +428,31 @@ def test_alpha_param(self, mock_sca): # Test that alpha was passed to internal matplotlib scatterplot _, scatter_kwargs = visualizer.ax.scatter.call_args assert "alpha" in scatter_kwargs - assert scatter_kwargs["alpha"] == 0.3 + assert scatter_kwargs["alpha"] == 0.75 + + @pytest.mark.xfail( + reason="""third test fails with AssertionError: Expected fit + to be called once. Called 0 times.""" + ) + def test_residuals_with_fitted(self): + """ + Test that ResidualsPlot properly handles an already-fitted model + """ + X, y = load_energy(return_dataset=True).to_numpy() + + model = Ridge().fit(X, y) + + with mock.patch.object(model, "fit") as mockfit: + oz = ResidualsPlot(model) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = ResidualsPlot(model, is_fitted=True) + oz.fit(X, y) + mockfit.assert_not_called() + + with mock.patch.object(model, "fit") as mockfit: + oz = ResidualsPlot(model, is_fitted=False) + oz.fit(X, y) + mockfit.assert_called_once_with(X, y) diff --git a/tests/test_style/__init__.py b/tests/test_style/__init__.py index cdf542fd3..8f4e31e4a 100644 --- a/tests/test_style/__init__.py +++ b/tests/test_style/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Tue Oct 04 16:21:21 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [c6aff34] benjamin@bengfort.com $ diff --git a/tests/test_style/test_colors.py b/tests/test_style/test_colors.py index 36894571e..ea0a3e5c3 100644 --- a/tests/test_style/test_colors.py +++ b/tests/test_style/test_colors.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Thu Oct 06 09:30:49 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_colors.py [c6aff34] benjamin@bengfort.com $ @@ -17,13 +17,18 @@ ## Imports ########################################################################## +import sys import pytest from matplotlib import cm from cycler import Cycler +from sklearn.cluster import KMeans +from sklearn.datasets import make_blobs + from yellowbrick.style.colors import * from yellowbrick.style.palettes import ColorPalette, PALETTES +from yellowbrick.cluster.silhouette import SilhouetteVisualizer from tests.base import VisualTestCase @@ -32,6 +37,7 @@ ## Color Tests ########################################################################## + class TestGetColorCycle(VisualTestCase): """ Test get_color_cycle helper function @@ -44,7 +50,7 @@ def test_cycle_depends_on_palette(self): c = get_color_cycle() assert len(c) == 6 - with ColorPalette('paired'): + with ColorPalette("paired"): c = get_color_cycle() assert len(c) == 12 @@ -58,7 +64,7 @@ def test_mpl_ge_150(self): Test get color cycle with matplotlib 1.5 or later """ colors = get_color_cycle() - cycle = mpl.rcParams['axes.prop_cycle'] + cycle = mpl.rcParams["axes.prop_cycle"] # Ensure the cycle is in fact a cycle assert isinstance(cycle, Cycler) @@ -72,8 +78,7 @@ def test_mpl_ge_150(self): # Ensure the colors and cycle match for color, cycle_color in zip(colors, cycle): - assert color == cycle_color['color'] - + assert color == cycle_color["color"] @pytest.mark.filterwarnings() @pytest.mark.skipif(mpl_ge_150, reason="requires matplotlib ealier than 1.5") @@ -81,7 +86,7 @@ def test_mpl_lt_150(self): """ Test get color cycle with matplotlib earlier than 1.5 """ - assert get_color_cycle() == mpl.rcParams['axes.color_cycle'] + assert get_color_cycle() == mpl.rcParams["axes.color_cycle"] class TestResolveColors(VisualTestCase): @@ -115,15 +120,15 @@ def test_warning_on_colormap_and_colors_args(self): Warns when both colormap and colors is used, colors is default """ with pytest.warns(Warning, match="both colormap and colors specified"): - colors = resolve_colors(colormap='RdBu', colors=['r', 'g', 'b']) - assert colors == ['r', 'g', 'b'] + colors = resolve_colors(colormap="RdBu", colors=["r", "g", "b"]) + assert colors == ["r", "g", "b"] def test_colormap_invalid(self): """ Exception raised when invalid colormap is supplied """ with pytest.raises(YellowbrickValueError): - resolve_colors(12, colormap='foo') + resolve_colors(12, colormap="foo") def test_colormap_string(self): """ @@ -131,18 +136,18 @@ def test_colormap_string(self): """ cases = ( ( - {'n_colors': 6, 'colormap': 'RdBu'}, + {"n_colors": 6, "colormap": "RdBu"}, [ (0.403921568627451, 0.0, 0.12156862745098039, 1.0), (0.8392156862745098, 0.3764705882352941, 0.30196078431372547, 1.0), (0.9921568627450981, 0.8588235294117647, 0.7803921568627451, 1.0), (0.8196078431372551, 0.8980392156862746, 0.9411764705882353, 1.0), (0.2627450980392157, 0.5764705882352941, 0.7647058823529411, 1.0), - (0.0196078431372549, 0.18823529411764706, 0.3803921568627451, 1.0) + (0.0196078431372549, 0.18823529411764706, 0.3803921568627451, 1.0), ], ), ( - {'n_colors': 18, 'colormap': 'viridis'}, + {"n_colors": 18, "colormap": "viridis"}, [ (0.267004, 0.004874, 0.329415, 1.0), (0.281924, 0.089666, 0.412415, 1.0), @@ -161,11 +166,11 @@ def test_colormap_string(self): (0.535621, 0.835785, 0.281908, 1.0), (0.688944, 0.865448, 0.182725, 1.0), (0.845561, 0.887322, 0.099702, 1.0), - (0.993248, 0.906157, 0.143936, 1.0) + (0.993248, 0.906157, 0.143936, 1.0), ], ), ( - {'n_colors': 9, 'colormap': 'Set1'}, + {"n_colors": 9, "colormap": "Set1"}, [ (0.8941176470588236, 0.10196078431372549, 0.10980392156862745, 1.0), (0.21568627450980393, 0.49411764705882355, 0.7215686274509804, 1.0), @@ -175,7 +180,7 @@ def test_colormap_string(self): (1.0, 1.0, 0.2, 1.0), (0.6509803921568628, 0.33725490196078434, 0.1568627450980392, 1.0), (0.9686274509803922, 0.5058823529411764, 0.7490196078431373, 1.0), - (0.6, 0.6, 0.6, 1.0) + (0.6, 0.6, 0.6, 1.0), ], ), ) @@ -190,33 +195,121 @@ def test_colormap_string_default_length(self): Check colormap when n_colors is not specified """ n_colors = len(get_color_cycle()) - assert len(resolve_colors(colormap='autumn')) == n_colors + assert len(resolve_colors(colormap="autumn")) == n_colors def test_colormap_cmap(self): """ Assert that supplying a maptlotlib.cm as colormap works """ - cmap = cm.get_cmap('nipy_spectral') + cmap = cm.get_cmap("nipy_spectral") colors = resolve_colors(4, colormap=cmap) assert colors == [ (0.0, 0.0, 0.0, 1.0), (0.0, 0.6444666666666666, 0.7333666666666667, 1.0), (0.7999666666666666, 0.9777666666666667, 0.0, 1.0), - (0.8, 0.8, 0.8, 1.0) + (0.8, 0.8, 0.8, 1.0), + ] + + def test_colormap_palette_mpl(self): + """ + Assert that supplying a maptlotlib palette as colormap works + """ + colormap = cm.get_cmap("nipy_spectral") + colors = resolve_colors(colormap=colormap) + assert colors == [ + (0.0, 0.0, 0.0, 1.0), + (0.0, 0.0, 0.8667, 1.0), + (0.0, 0.6667, 0.5333, 1.0), + (0.0, 1.0, 0.0, 1.0), + (1.0, 0.6, 0.0, 1.0), + (0.8, 0.8, 0.8, 1.0), + ] + + def test_integrated_yb_colormap(self): + """ + Assert silhouette plot colormap can be set with a yellowbrick palette + """ + # Generate a blobs data set + X, y = make_blobs( + n_samples=1000, n_features=12, centers=8, shuffle=False, random_state=0 + ) + visualizer = SilhouetteVisualizer( + KMeans(random_state=0), colormap="neural_paint" + ) + visualizer.fit(X) + visualizer.finalize() + + tol = ( + 3.2 if sys.platform == "win32" else 0.01 + ) # Fails on AppVeyor with RMS 3.143 + self.assert_images_similar(visualizer, remove_legend=True, tol=tol) + + def test_colormap_palette_yb(self): + """ + Assert that supplying a yellowbrick palette as colormap works + """ + colormap = ColorPalette("neural_paint") + assert resolve_colors(colormap=colormap) == [ + (0.08627450980392157, 0.44313725490196076, 0.5725490196078431), + (0.43137254901960786, 0.4588235294117647, 0.2823529411764706), + (0.7725490196078432, 0.6352941176470588, 0.6705882352941176), + (0.0, 0.8, 1.0), + (0.8705882352941177, 0.47058823529411764, 0.6823529411764706), + (1.0, 0.8, 0.6), + (0.23921568627450981, 0.24705882352941178, 0.25882352941176473), + (1.0, 1.0, 0.8), ] + def test_colormap_cmap_with_colors(self): + """ + Assert that colors overrides a mpl colormap if both are provided + """ + colormap = cm.get_cmap("nipy_spectral") + overriding_colors = [ + (0.0, 0.0, 0.0, 1.0), + (0.0, 0.6444666666666666, 0.7333666666666667, 1.0), + (0.7999666666666666, 0.9777666666666667, 0.0, 1.0), + (0.8, 0.8, 0.8, 1.0), + ] + with pytest.warns(Warning, match="both colormap and colors specified"): + colors = resolve_colors(colormap=colormap, colors=overriding_colors) + assert colors == overriding_colors + + def test_colormap_palette_yb_colors(self): + """ + Assert that colors overrides a yellowbrick colormap if both are provided + """ + colormap = ColorPalette("neural_paint") + overriding_colors = [ + (0.0, 0.0, 0.0, 1.0), + (0.0, 0.6444666666666666, 0.7333666666666667, 1.0), + (0.7999666666666666, 0.9777666666666667, 0.0, 1.0), + (0.8, 0.8, 0.8, 1.0), + ] + with pytest.warns(Warning, match="both colormap and colors specified"): + colors = resolve_colors(colormap=colormap, colors=overriding_colors) + assert colors == overriding_colors + + def test_colormap_invalid_type(self): + """ + Exception raised when invalid colormap type is supplied + """ + with pytest.raises(YellowbrickValueError): + a = lambda x: x + 1 + resolve_colors(colormap=a) + def test_colors(self): """ Test passing in a list of colors """ - c = PALETTES['flatui'] + c = PALETTES["flatui"] assert resolve_colors(colors=c) == c def test_colors_truncate(self): """ Test passing in a list of colors with n_colors truncate """ - c = PALETTES['flatui'] + c = PALETTES["flatui"] assert len(c) > 3 assert len(resolve_colors(n_colors=3, colors=c)) == 3 @@ -225,7 +318,7 @@ def test_colors_multiply(self): """ Test passing in a list of colors with n_colors multiply """ - c = PALETTES['flatui'] + c = PALETTES["flatui"] assert len(c) < 12 assert len(resolve_colors(n_colors=12, colors=c)) == 12 diff --git a/tests/test_style/test_palettes.py b/tests/test_style/test_palettes.py index 6ff78e71f..ece83cd5b 100644 --- a/tests/test_style/test_palettes.py +++ b/tests/test_style/test_palettes.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Tue Oct 04 16:21:58 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_palettes.py [c6aff34] benjamin@bengfort.com $ @@ -17,7 +17,7 @@ ## Imports ########################################################################## -import unittest +import pytest import numpy as np import matplotlib as mpl @@ -35,7 +35,8 @@ ## Color Palette Tests ########################################################################## -class ColorPaletteObjectTests(VisualTestCase): + +class TestColorPaletteObject(VisualTestCase): """ Tests the ColorPalette object """ @@ -50,16 +51,16 @@ def test_init_palette_by_name(self): try: palette = ColorPalette(name) except YellowbrickValueError: - self.fail( - "Could not instantiate {} color palette by name".format(name) - ) + self.fail("Could not instantiate {} color palette by name".format(name)) - self.assertEqual(value, palette) + assert value == palette # Try a name not in PALETTES - with self.assertRaises(YellowbrickValueError): - self.assertNotIn('foo', PALETTES, "Cannot test bad name 'foo' it is in PALETTES!") - palette = ColorPalette('foo') + with pytest.raises(YellowbrickValueError): + assert ( + "foo" not in PALETTES + ), "Cannot test bad name 'foo' it is in PALETTES!" + palette = ColorPalette("foo") def test_init_palette_by_list(self): """ @@ -69,54 +70,52 @@ def test_init_palette_by_list(self): # Try all the values in the palettes (HEX) for value in PALETTES.values(): palette = ColorPalette(value) - self.assertEqual(len(value), len(palette)) + assert len(value) == len(palette) # Try all the values converted to RGB for value in PALETTES.values(): palette = ColorPalette(map(mpl.colors.colorConverter.to_rgb, value)) - self.assertEqual(len(value), len(palette)) + assert len(value) == len(palette) def test_color_palette_context(self): """ Test ColorPalette context management """ default = color_palette() - context = color_palette('dark') + context = color_palette("dark") - with ColorPalette('dark') as palette: - self.assertIsInstance(palette, ColorPalette) - self.assertEqual(get_color_cycle(), context) + with ColorPalette("dark") as palette: + assert isinstance(palette, ColorPalette) + assert get_color_cycle() == context - self.assertEqual(get_color_cycle(), default) + assert get_color_cycle() == default def test_as_hex_as_rgb(self): """ Test the conversion of a ColorPalette to hex values and back to rgb """ - palette = color_palette('flatui') - expected = PALETTES['flatui'] + palette = color_palette("flatui") + expected = PALETTES["flatui"] morgified = palette.as_hex() - self.assertIsNot(morgified, palette) - self.assertIsInstance(morgified, ColorPalette) - self.assertEqual(morgified, expected) + assert morgified is not palette + assert isinstance(morgified, ColorPalette) + assert morgified == expected remorgified = morgified.as_rgb() - self.assertIsNot(remorgified, morgified) - self.assertIsNot(remorgified, palette) - self.assertEqual(remorgified, palette) + assert remorgified is not morgified + assert remorgified is not palette + assert remorgified == palette - @unittest.skip("not implemented yet") + @pytest.mark.skip(reason="not implemented yet") def test_plot_color_palette(self): """ Test the plotting of a color palette for color visualization """ - raise NotImplementedError( - "Not quite sure how to implement this yet" - ) + raise NotImplementedError("Not quite sure how to implement this yet") -class ColorPaletteFunctionTests(VisualTestCase): +class TestColorPaletteFunction(VisualTestCase): """ Tests the color_palette function. """ @@ -127,7 +126,7 @@ def test_current_palette(self): """ pal = color_palette(["red", "blue", "green"], 3) set_palette(pal, 3) - self.assertEqual(pal, get_color_cycle()) + assert pal == get_color_cycle() # Reset the palette set_aesthetic() @@ -141,9 +140,9 @@ def test_palette_context(self): context_pal = color_palette("muted") with color_palette(context_pal): - self.assertEqual(get_color_cycle(), context_pal) + assert get_color_cycle() == context_pal - self.assertEqual(get_color_cycle(), default_pal) + assert get_color_cycle() == default_pal def test_big_palette_context(self): """ @@ -155,9 +154,9 @@ def test_big_palette_context(self): set_palette(original_pal) with color_palette(context_pal, 10): - self.assertEqual(get_color_cycle(), context_pal) + assert get_color_cycle() == context_pal - self.assertEqual(get_color_cycle(), original_pal) + assert get_color_cycle() == original_pal # Reset default set_aesthetic() @@ -169,17 +168,23 @@ def test_yellowbrick_palettes(self): pals = ["accent", "dark", "pastel", "bold", "muted"] for name in pals: pal_out = color_palette(name) - self.assertEqual(len(pal_out), 6, "{} is not of len 6".format(name)) + assert len(pal_out) == 6, "{} is not of len 6".format(name) def test_seaborn_palettes(self): """ Test the seaborn palettes have length 6 (bgrmyck) """ - pals = ["sns_deep", "sns_muted", "sns_pastel", - "sns_bright", "sns_dark", "sns_colorblind"] + pals = [ + "sns_deep", + "sns_muted", + "sns_pastel", + "sns_bright", + "sns_dark", + "sns_colorblind", + ] for name in pals: pal_out = color_palette(name) - self.assertEqual(len(pal_out), 6) + assert len(pal_out) == 6 def test_other_palettes(self): """ @@ -188,18 +193,18 @@ def test_other_palettes(self): pals = ["flatui", "paired", "neural_paint", "set1"] for name in pals: pal_out = color_palette(name) - self.assertTrue(pal_out) - + assert pal_out is not None + assert len(pal_out) > 0 def test_bad_palette_name(self): """ Test that a bad palette name raises an exception """ - with self.assertRaises(ValueError): + with pytest.raises(ValueError): color_palette("IAmNotAPalette") - with self.assertRaises(YellowbrickValueError): + with pytest.raises(YellowbrickValueError): color_palette("IAmNotAPalette") def test_bad_palette_colors(self): @@ -208,10 +213,10 @@ def test_bad_palette_colors(self): """ pal = ["red", "blue", "iamnotacolor"] - with self.assertRaises(ValueError): + with pytest.raises(ValueError): color_palette(pal) - with self.assertRaises(YellowbrickValueError): + with pytest.raises(YellowbrickValueError): color_palette(pal) def test_palette_is_list_of_tuples(self): @@ -222,10 +227,10 @@ def test_palette_is_list_of_tuples(self): pal_in = np.array(["red", "blue", "green"]) pal_out = color_palette(pal_in, 3) - self.assertIsInstance(pal_out, list) - self.assertIsInstance(pal_out[0], tuple) - self.assertIsInstance(pal_out[0][0], float) - self.assertEqual(len(pal_out[0]), 3) + assert isinstance(pal_out, list) + assert isinstance(pal_out[0], tuple) + assert isinstance(pal_out[0][0], float) + assert len(pal_out[0]) == 3 def test_palette_cycles(self): """ @@ -233,20 +238,20 @@ def test_palette_cycles(self): """ accent = color_palette("accent") double_accent = color_palette("accent", 12) - self.assertEqual(double_accent, accent + accent) + assert double_accent == accent + accent - @unittest.skip("Discovered this commented out, don't know why") + @pytest.mark.skip(reason="discovered this commented out, don't know why") def test_cbrewer_qual(self): """ Test colorbrewer qualitative palettes """ pal_short = mpl_palette("Set1", 4) pal_long = mpl_palette("Set1", 6) - self.assertEqual(pal_short, pal_long[:4]) + assert pal_short == pal_long[:4] pal_full = palettes.mpl_palette("Set2", 8) pal_long = palettes.mpl_palette("Set2", 10) - self.assertEqual(pal_full, pal_long[:8]) + assert pal_full == pal_long[:8] def test_color_codes(self): """ @@ -257,7 +262,7 @@ def test_color_codes(self): for code, color in zip("bgrmyck", colors): rgb_want = mpl.colors.colorConverter.to_rgb(color) rgb_got = mpl.colors.colorConverter.to_rgb(code) - self.assertEqual(rgb_want, rgb_got) + assert rgb_want == rgb_got set_color_codes("reset") def test_as_hex(self): @@ -266,10 +271,10 @@ def test_as_hex(self): """ pal = color_palette("accent") for rgb, hex in zip(pal, pal.as_hex()): - self.assertEqual(mpl.colors.rgb2hex(rgb), hex) + assert mpl.colors.rgb2hex(rgb) == hex for rgb_e, rgb_v in zip(pal, pal.as_hex().as_rgb()): - self.assertEqual(rgb_e, rgb_v) + assert rgb_e == rgb_v def test_preserved_palette_length(self): """ @@ -277,7 +282,7 @@ def test_preserved_palette_length(self): """ pal_in = color_palette("Set1", 10) pal_out = color_palette(pal_in) - self.assertEqual(pal_in, pal_out) + assert pal_in == pal_out def test_color_sequence(self): """ @@ -286,33 +291,30 @@ def test_color_sequence(self): for name, ncols in SEQUENCES.items(): for n in ncols.keys(): cmap = color_sequence(name, n) - self.assertEqual(name, cmap.name) - self.assertEqual(n, cmap.N) + assert name == cmap.name + assert n == cmap.N def test_color_sequence_default(self): """ Assert the default color sequence is RdBu """ cmap = color_sequence() - self.assertEqual(cmap.name, "RdBu") - self.assertEqual(cmap.N, 11) + assert cmap.name == "RdBu" + assert cmap.N == 11 def test_color_sequence_unrecocognized(self): """ Test value errors for unrecognized sequences """ - with self.assertRaises(YellowbrickValueError): - color_sequence('PepperBucks', 3) + with pytest.raises(YellowbrickValueError): + color_sequence("PepperBucks", 3) def test_color_sequence_bounds(self): """ Test color sequence out of bounds value error """ - with self.assertRaises(YellowbrickValueError): - color_sequence('RdBu', 18) - - with self.assertRaises(YellowbrickValueError): - color_sequence('RdBu', 2) + with pytest.raises(YellowbrickValueError): + color_sequence("RdBu", 18) -if __name__ == "__main__": - unittest.main() + with pytest.raises(YellowbrickValueError): + color_sequence("RdBu", 2) diff --git a/tests/test_style/test_rcmod.py b/tests/test_style/test_rcmod.py index d62ca2820..e15d38b98 100644 --- a/tests/test_style/test_rcmod.py +++ b/tests/test_style/test_rcmod.py @@ -5,7 +5,7 @@ # Author: Benjamin Bengfort # Created: Thu Oct 06 08:20:33 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_rcmod.py [c6aff34] benjamin@bengfort.com $ @@ -18,13 +18,12 @@ ## Imports ########################################################################## -import unittest +import pytest import numpy as np import matplotlib as mpl import numpy.testing as npt import yellowbrick.style.rcmod as yb_rcmod -from distutils.version import LooseVersion from tests.base import VisualTestCase @@ -32,6 +31,7 @@ ## Parameter Tests ########################################################################## + class RCParamTester(VisualTestCase): """ Base class for asserting parameters have been correctly changed. @@ -39,8 +39,8 @@ class RCParamTester(VisualTestCase): excluded_params = { "backend", # This cannot be changed by manipulating rc - "svg.embed_char_paths", # This param causes test issues and is deprecated anyway - "font.family", # breaks the visualtest case + "svg.embed_char_paths", # This param causes test issues and is deprecated + "font.family", # breaks the visualtest case } def flatten_list(self, orig_list): @@ -57,15 +57,15 @@ def assert_rc_params(self, params): elif isinstance(v, np.ndarray): npt.assert_array_equal(mpl.rcParams[k], v) else: - self.assertEqual((k, mpl.rcParams[k]), (k, v)) + assert (k, mpl.rcParams[k]) == (k, v) ########################################################################## ## Parameter Tests ########################################################################## -class TestAxesStyle(RCParamTester): +class TestAxesStyle(RCParamTester): def test_default_return(self): """ Test that the axes style returns the default params @@ -80,8 +80,8 @@ def test_rc_override(self): rc = {"axes.facecolor": "blue", "foo.notaparam": "bar"} out = yb_rcmod._axes_style("darkgrid", rc) - self.assertEqual(out["axes.facecolor"], "blue") - self.assertNotIn("foo.notaparam", out) + assert out["axes.facecolor"] == "blue" + assert "foo.notaparam" not in out def test_set_style(self): """ @@ -91,7 +91,7 @@ def test_set_style(self): yb_rcmod.set_style() self.assert_rc_params(style_dict) - @unittest.skip("This test doesn't make sense without multiple styles") + @pytest.mark.skip(reason="this test doesn't make sense without multiple styles") def test_style_context_manager(self): yb_rcmod.set_style("darkgrid") @@ -105,6 +105,7 @@ def test_style_context_manager(self): @yb_rcmod._axes_style("whitegrid") def func(): self.assert_rc_params(context_params) + func() self.assert_rc_params(orig_params) @@ -112,25 +113,20 @@ def test_style_context_independence(self): """ Assert context and style independence """ - self.assertTrue(set(yb_rcmod._style_keys) ^ set(yb_rcmod._context_keys)) + assert len(set(yb_rcmod._style_keys) ^ set(yb_rcmod._context_keys)) > 0 def test_set_rc(self): """ Test the ability to set the mpl configuration rc dict """ yb_rcmod.set_aesthetic(rc={"lines.linewidth": 4}) - self.assertEqual(mpl.rcParams["lines.linewidth"], 4) + assert mpl.rcParams["lines.linewidth"] == 4 yb_rcmod.set_aesthetic() def test_reset_defaults(self): """ Test the ability to reset to the mpl defaults """ - # Changes to the rc parameters make this test hard to manage - # on older versions of matplotlib, so we'll skip it - if LooseVersion(mpl.__version__) < LooseVersion("1.3"): - raise self.SkipTest - yb_rcmod.reset_defaults() self.assert_rc_params(mpl.rcParamsDefault) yb_rcmod.set_aesthetic() @@ -139,19 +135,12 @@ def test_reset_orig(self): """ Test the ability to reset to the original (respecting custom styles) """ - - # Changes to the rc parameters make this test hard to manage - # on older versions of matplotlib, so we'll skip it - if LooseVersion(mpl.__version__) < LooseVersion("1.3"): - raise self.SkipTest - yb_rcmod.reset_orig() self.assert_rc_params(mpl.rcParamsOrig) yb_rcmod.set_aesthetic() class TestPlottingContext(RCParamTester): - def test_default_return(self): """ Test the context returns the default @@ -167,11 +156,17 @@ def test_font_scale(self): notebook_ref = yb_rcmod._plotting_context("notebook") notebook_big = yb_rcmod._plotting_context("notebook", 2) - font_keys = ["axes.labelsize", "axes.titlesize", "legend.fontsize", - "xtick.labelsize", "ytick.labelsize", "font.size"] + font_keys = [ + "axes.labelsize", + "axes.titlesize", + "legend.fontsize", + "xtick.labelsize", + "ytick.labelsize", + "font.size", + ] for k in font_keys: - self.assertEqual(notebook_ref[k] * 2, notebook_big[k]) + assert notebook_ref[k] * 2 == notebook_big[k] def test_rc_override(self): """ @@ -180,8 +175,8 @@ def test_rc_override(self): key, val = "grid.linewidth", 5 rc = {key: val, "foo": "bar"} out = yb_rcmod._plotting_context("talk", rc=rc) - self.assertEqual(out[key], val) - self.assertNotIn("foo", out) + assert out[key] == val + assert "foo" not in out def test__set_context(self): """ @@ -191,7 +186,7 @@ def test__set_context(self): yb_rcmod._set_context() self.assert_rc_params(context_dict) - @unittest.skip("This test doesn't make sense without multiple contexts") + @pytest.mark.skip(reason="this test doesn't make sense without multiple contexts") def test_context_context_manager(self): yb_rcmod._set_context("notebook") @@ -205,9 +200,6 @@ def test_context_context_manager(self): @yb_rcmod._plotting_context("paper") def func(): self.assert_rc_params(context_params) + func() self.assert_rc_params(orig_params) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_target/__init__.py b/tests/test_target/__init__.py index 6a1059b01..7b433d5ba 100644 --- a/tests/test_target/__init__.py +++ b/tests/test_target/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Thu Jul 19 09:09:07 2018 -0400 # -# ID: __init__.py [] benjamin@bengfort.com $ +# ID: __init__.py [d742c57] benjamin@bengfort.com $ """ Tests for the target module. diff --git a/tests/test_target/test_binning.py b/tests/test_target/test_binning.py index 94d33c5b1..54ebbb517 100644 --- a/tests/test_target/test_binning.py +++ b/tests/test_target/test_binning.py @@ -1,39 +1,63 @@ # tests.test_target.test_binning # Tests for the BalancedBinningReference visualizer # -# Author: Juan L. Kehoe (juanluo2008@gmail.com) -# Author: Prema Damodaran Roman (pdamo24@gmail.com) +# Author: Juan L. Kehoe +# Author: Prema Damodaran Roman # Created: Thu Jul 20 10:21:49 2018 -0400 # +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: test_binning.py -from tests.base import VisualTestCase -from tests.dataset import DatasetMixin +########################################################################## +# Imports +########################################################################## +import pytest + from yellowbrick.target.binning import * +from yellowbrick.datasets import load_occupancy + +from tests.base import VisualTestCase + +try: + import pandas as pd +except ImportError: + pd = None ########################################################################## -## BalancedBinningReference Tests +# BalancedBinningReference Tests ########################################################################## -class TestBalancedBinningReference(VisualTestCase, DatasetMixin): - """ - Test the BalancedBinningReference visualizer - """ - - def test_balancedbinningreference(self): - """ - Test Histogram on a real dataset - """ - # Load the data from the fixture - dataset = self.load_data('occupancy') - - # Get the data - y = dataset["temperature"] - - - visualizer = BalancedBinningReference() - visualizer.fit(y) - visualizer.poof() - self.assert_images_similar(visualizer, tol=0.5) - - \ No newline at end of file + +class TestBalancedBinningReference(VisualTestCase): + """ + Test the BalancedBinningReference visualizer + """ + + def test_numpy_bins(self): + """ + Test Histogram on a NumPy array + """ + # Load the data from the fixture + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() + + visualizer = BalancedBinningReference() + visualizer.fit(y) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.5) + + @pytest.mark.skipif(pd is None, reason="pandas is required") + def test_pandas_bins(self): + """ + Test Histogram on a Pandas Dataframe + """ + # Load the data from the fixture + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + + visualizer = BalancedBinningReference() + visualizer.fit(y) + visualizer.finalize() + self.assert_images_similar(visualizer, tol=0.5) diff --git a/tests/test_target/test_class_balance.py b/tests/test_target/test_class_balance.py index 43a2e76d2..216cf3fe1 100644 --- a/tests/test_target/test_class_balance.py +++ b/tests/test_target/test_class_balance.py @@ -1,10 +1,13 @@ # tests.test_target.test_class_balance # Tests for the ClassBalance visualizer # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu Jul 19 10:21:49 2018 -0400 # -# ID: test_class_balance.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_class_balance.py [d742c57] benjamin@bengfort.com $ """ Tests for the ClassBalance visualizer @@ -15,13 +18,13 @@ ########################################################################## import pytest -import numpy as np from yellowbrick.target.class_balance import * +from yellowbrick.datasets import load_occupancy from yellowbrick.exceptions import YellowbrickValueError from tests.base import VisualTestCase -from tests.dataset import DatasetMixin, Dataset, Split +from tests.fixtures import Dataset, Split from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split as tts @@ -33,41 +36,45 @@ ########################################################################## -## Data Fixtures +# Data Fixtures ########################################################################## +# TODO: convert to Pytest fixture def make_fixture(binary=False, balanced=False, split=False): """ Make a dataset for testing ClassBalance based on the specified params. """ kwargs = { - "n_samples":100, "n_features":20, "n_informative":8, "n_redundant":2, - "n_clusters_per_class":1, "random_state":89092, + "n_samples": 100, + "n_features": 20, + "n_informative": 8, + "n_redundant": 2, + "n_clusters_per_class": 1, + "random_state": 89092, } if binary: - kwargs['n_classes'] = 2 - kwargs['weights'] = None if balanced else [0.3, 0.7] + kwargs["n_classes"] = 2 + kwargs["weights"] = None if balanced else [0.3, 0.7] else: - kwargs['n_classes'] = 5 - kwargs['weights'] = None if balanced else [0.1, 0.2, 0.4, 0.2, .01] + kwargs["n_classes"] = 5 + kwargs["weights"] = None if balanced else [0.1, 0.2, 0.4, 0.2, 0.01] X, y = make_classification(**kwargs) if split: - X_train, X_test, y_train, y_test = tts( - X, y, test_size=0.2, random_state=101 - ) + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=101) return Dataset(Split(X_train, X_test), Split(y_train, y_test)) return Dataset(X, y) ########################################################################## -## Tests +# Tests ########################################################################## -class ClassBalanceTests(VisualTestCase, DatasetMixin): + +class TestClassBalance(VisualTestCase): """ Test ClassBalance visualizer """ @@ -118,7 +125,7 @@ def test_binary_balance(self): assert oz.fit(dataset.y) is oz assert oz._mode == BALANCE - #oz.finalize() + # oz.finalize() self.assert_images_similar(oz) def test_binary_compare(self): @@ -131,7 +138,7 @@ def test_binary_compare(self): assert oz.fit(dataset.y.train, dataset.y.test) is oz assert oz._mode == COMPARE - #oz.finalize() + # oz.finalize() self.assert_images_similar(oz) def test_multiclass_balance(self): @@ -144,7 +151,7 @@ def test_multiclass_balance(self): assert oz.fit(dataset.y) is oz assert oz._mode == BALANCE - #oz.finalize() + # oz.finalize() self.assert_images_similar(oz) def test_multiclass_compare(self): @@ -157,7 +164,7 @@ def test_multiclass_compare(self): assert oz.fit(dataset.y.train, dataset.y.test) is oz assert oz._mode == COMPARE - #oz.finalize() + # oz.finalize() self.assert_images_similar(oz) @pytest.mark.skipif(pd is None, reason="test requires pandas") @@ -165,16 +172,28 @@ def test_pandas_occupancy_balance(self): """ Test pandas data frame with string target in balance mode """ - data = self.load_data("occupancy") - y = pd.Series([ - "occupied" if yi else "unoccupied" for yi in data['occupancy'] - ]) + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() + + # Create and fit the visualizer + oz = ClassBalance() + assert oz.fit(y) is oz + + # oz.finalize() + self.assert_images_similar(oz) + + def test_numpy_occupancy_balance(self): + """ + Test NumPy arrays with string target in balance mode + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() # Create and fit the visualizer oz = ClassBalance() assert oz.fit(y) is oz - #oz.finalize() + # oz.finalize() self.assert_images_similar(oz) @pytest.mark.skipif(pd is None, reason="test requires pandas") @@ -182,15 +201,24 @@ def test_pandas_occupancy_compare(self): """ Test pandas data frame with string target in compare mode """ - data = self.load_data("occupancy") - features = [ - "temperature", "relative_humidity", "light", "C02", "humidity" - ] + data = load_occupancy(return_dataset=True) + X, y = data.to_pandas() - X = pd.DataFrame(data[features]) - y = pd.Series([ - "occupied" if yi else "unoccupied" for yi in data['occupancy'] - ]) + _, _, y_train, y_test = tts(X, y, test_size=0.4, random_state=2242) + + # Create and fit the visualizer + oz = ClassBalance() + assert oz.fit(y_train, y_test) is oz + + # oz.finalize() + self.assert_images_similar(oz) + + def test_numpy_occupancy_compare(self): + """ + Test NumPy arrays with string target in compare mode + """ + data = load_occupancy(return_dataset=True) + X, y = data.to_numpy() _, _, y_train, y_test = tts(X, y, test_size=0.4, random_state=2242) @@ -198,7 +226,7 @@ def test_pandas_occupancy_compare(self): oz = ClassBalance() assert oz.fit(y_train, y_test) is oz - #oz.finalize() + # oz.finalize() self.assert_images_similar(oz) def test_quick_method(self): @@ -207,5 +235,5 @@ def test_quick_method(self): """ dataset = make_fixture(binary=False, split=False) - ax = class_balance(dataset.y) - self.assert_images_similar(ax=ax, tol=0.5) + viz = class_balance(dataset.y) + self.assert_images_similar(viz, tol=0.5) diff --git a/tests/test_target/test_feature_correlation.py b/tests/test_target/test_feature_correlation.py index b9ba6af7a..5c03cad5f 100644 --- a/tests/test_target/test_feature_correlation.py +++ b/tests/test_target/test_feature_correlation.py @@ -1,13 +1,13 @@ # tests.test_features.test_feature_correlation # Test the feature correlation visualizers # -# Author: Zijie (ZJ) Poh +# Author: Zijie (ZJ) Poh # Created: Tue Jul 31 20:21:32 2018 -0700 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_feature_correlation.py [] poh.zijie@gmail.com $ +# ID: test_feature_correlation.py [33aec16] 8103276+zjpoh@users.noreply.github.com $ """ Test the feature correlation to dependent variable visualizer. @@ -20,10 +20,6 @@ import sys import pytest import numpy as np -try: - import pandas as pd -except ImportError: - pd = None import numpy.testing as npt import matplotlib.pyplot as plt @@ -31,26 +27,29 @@ from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning from sklearn import datasets - from tests.base import VisualTestCase +try: + import pandas as pd +except ImportError: + pd = None + ########################################################################## ## Feature Correlation Tests ########################################################################## + class TestFeatureCorrelationVisualizer(VisualTestCase): """ FeatureCorrelation visualizer """ data = datasets.load_diabetes() - X, y = data['data'], data['target'] - labels = data['feature_names'] + X, y = data["data"], data["target"] + labels = data["feature_names"] - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_feature_correlation_integrated_pearson(self): """ Test FeatureCorrelation visualizer with pearson correlation @@ -58,37 +57,33 @@ def test_feature_correlation_integrated_pearson(self): """ viz = FeatureCorrelation() viz.fit(self.X, self.y) - viz.poof() + viz.finalize() self.assert_images_similar(viz) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_feature_correlation_integrated_mutual_info_regression(self): """ Test FeatureCorrelation visualizer with mutual information regression """ - viz = FeatureCorrelation(method='mutual_info-regression') + viz = FeatureCorrelation(method="mutual_info-regression") viz.fit(self.X, self.y, random_state=23456) - viz.poof() + viz.finalize() self.assert_images_similar(viz) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_feature_correlation_integrated_mutual_info_classification(self): """ Test FeatureCorrelation visualizer with mutual information on wine dataset (classification) """ data = datasets.load_wine() - X, y = data['data'], data['target'] + X, y = data["data"], data["target"] - viz = FeatureCorrelation(method='mutual_info-classification') + viz = FeatureCorrelation(method="mutual_info-classification") viz.fit(X, y, random_state=12345) - viz.poof() + viz.finalize() self.assert_images_similar(viz) @@ -96,8 +91,8 @@ def test_feature_correlation_method_not_implemented(self): """ Test FeatureCorrelation visualizer with unknown method """ - method = 'foo' - e = ('Method foo not implement; choose from *') + method = "foo" + e = "Method foo not implement; choose from *" with pytest.raises(YellowbrickValueError, match=e): FeatureCorrelation(method=method) @@ -135,7 +130,7 @@ def test_feature_correlation_select_feature_by_index_out_of_range(self): """ Test selecting feature by feature index but index is out of range """ - e = 'Feature index is out of range' + e = "Feature index is out of range" with pytest.raises(YellowbrickValueError, match=e): viz = FeatureCorrelation(feature_index=[0, 2, 10]) viz.fit(self.X, self.y) @@ -154,13 +149,16 @@ def test_feature_correlation_select_feature_by_index_and_name(self): Test selecting feature warning when both index and names are provided """ feature_index = [0, 2, 3] - feature_names = ['age'] + feature_names = ["age"] - e = ('Both feature_index and feature_names are specified. ' - 'feature_names is ignored') + e = ( + "Both feature_index and feature_names are specified. " + "feature_names is ignored" + ) with pytest.raises(YellowbrickWarning, match=e): - viz = FeatureCorrelation(feature_index=feature_index, - feature_names=feature_names) + viz = FeatureCorrelation( + feature_index=feature_index, feature_names=feature_names + ) viz.fit(self.X, self.y) assert viz.scores_.shape[0] == 3 @@ -168,9 +166,9 @@ def test_feature_correlation_select_feature_by_name_no_labels(self): """ Test selecting feature by feature names with labels is not supplied """ - feature_names = ['age'] + feature_names = ["age"] - e = 'age not in labels' + e = "age not in labels" with pytest.raises(YellowbrickValueError, match=e): viz = FeatureCorrelation(feature_names=feature_names) viz.fit(self.X, self.y) @@ -179,10 +177,9 @@ def test_feature_correlation_select_feature_by_name(self): """ Test selecting feature by feature names """ - feature_names = ['age', 'sex', 'bp', 's5'] + feature_names = ["age", "sex", "bp", "s5"] - viz = FeatureCorrelation(labels=self.labels, - feature_names=feature_names) + viz = FeatureCorrelation(labels=self.labels, feature_names=feature_names) viz.fit(self.X, self.y) npt.assert_array_equal(viz.features_, feature_names) @@ -196,9 +193,7 @@ def test_feature_correlation_sort(self): assert np.all(viz.scores_[:-1] <= viz.scores_[1:]) - @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" - ) + @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows") def test_feature_correlation_quick_method(self): """ Test sorting of correlation @@ -207,4 +202,4 @@ def test_feature_correlation_quick_method(self): ax = fig.add_subplot() g = feature_correlation.feature_correlation(self.X, self.y, ax) - self.assert_images_similar(ax=g) + self.assert_images_similar(g) diff --git a/tests/test_text/test_base.py b/tests/test_text/test_base.py index aa496cbf7..17e568470 100644 --- a/tests/test_text/test_base.py +++ b/tests/test_text/test_base.py @@ -1,10 +1,10 @@ # tests.test_text.test_base # Tests for the text visualization base classes # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Mon Feb 20 06:34:50 2017 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_base.py [6aa9198] benjamin@bengfort.com $ @@ -17,8 +17,6 @@ ## Imports ########################################################################## -import unittest - from yellowbrick.base import * from yellowbrick.text.base import * from sklearn.base import BaseEstimator, TransformerMixin @@ -28,22 +26,13 @@ ## TextVisualizer Base Tests ########################################################################## -class TextVisualizerBaseTests(unittest.TestCase): +class TestTextVisualizerBase(object): def test_subclass(self): """ - Assert the text visualizer is subclassed correctly + Assert the text visualizer is subclassed correctly """ visualizer = TextVisualizer() - self.assertIsInstance(visualizer, TransformerMixin) - self.assertIsInstance(visualizer, BaseEstimator) - self.assertIsInstance(visualizer, Visualizer) - - # def test_interface(self): - # """ - # Test the feature visualizer interface - # """ - # - # visualizer = TextVisualizer() - # with self.assertRaises(NotImplementedError): - # visualizer.poof() + assert isinstance(visualizer, TransformerMixin) + assert isinstance(visualizer, BaseEstimator) + assert isinstance(visualizer, Visualizer) diff --git a/tests/test_text/test_dispersion.py b/tests/test_text/test_dispersion.py index ec94eceb7..09962b6e7 100644 --- a/tests/test_text/test_dispersion.py +++ b/tests/test_text/test_dispersion.py @@ -2,13 +2,12 @@ # Tests for the dispersion plot visualization # # Author: Larry Gray -# Github: @lwgray # Created: 2018-06-22 15:27 # -# Copyright (C) 2018 +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_dispersion.py [] lwgray@gmail.com $ +# ID: test_dispersion.py [25f1b9a] lwgray@gmail.com $ """ Tests for the dispersion plot text visualization @@ -19,26 +18,46 @@ ########################################################################## import pytest +import matplotlib.pyplot as plt from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.datasets import load_hobbies from yellowbrick.text.dispersion import * -from tests.dataset import DatasetMixin from tests.base import VisualTestCase + +########################################################################## +## Data +########################################################################## + +corpus = load_hobbies() + ########################################################################## ## DispersionPlot Tests ########################################################################## -class DispersionPlotTests(VisualTestCase, DatasetMixin): - def test_integrated_dispersionplot(self): +class TestDispersionPlot(VisualTestCase): + def test_quick_method(self): """ - Assert no errors occur during DispersionPlot integration + Assert no errors occur when using the qucik method """ - corpus = self.load_data('hobbies') + _, ax = plt.subplots() text = [doc.split() for doc in corpus.data] - target_words = ['Game', 'player', 'score', 'oil', 'Man'] + target_words = ["Game", "player", "score", "oil", "Man"] + + viz = dispersion(words=target_words, corpus=text, ax=ax) + viz.ax.grid(False) + + self.assert_images_similar(viz, tol=25) + + def test_integrated_dispersion_plot(self): + """ + Assert no errors occur during DispersionPlot integration + """ + text = [doc.split() for doc in corpus.data] + target_words = ["Game", "player", "score", "oil", "Man"] visualizer = DispersionPlot(target_words) visualizer.fit(text) @@ -46,15 +65,13 @@ def test_integrated_dispersionplot(self): self.assert_images_similar(visualizer, tol=25) - def test_dispersionplot_ignore_case(self): + def test_dispersion_plot_ignore_case(self): """ Assert no errors occur during DispersionPlot integration with ignore_case parameter turned on """ - corpus = self.load_data('hobbies') - text = [doc.split() for doc in corpus.data] - target_words = ['Game', 'player', 'score', 'oil', 'Man'] + target_words = ["Game", "player", "score", "oil", "Man"] visualizer = DispersionPlot(target_words, ignore_case=True) visualizer.fit(text) @@ -62,15 +79,13 @@ def test_dispersionplot_ignore_case(self): self.assert_images_similar(visualizer, tol=25) - def test_dispersionplot_generator_input(self): + def test_dispersion_plot_generator_input(self): """ Assert no errors occur during dispersionPlot integration when the corpus' text type is a generator """ - corpus = self.load_data('hobbies') - - text = (doc.split() for doc in corpus.data) - target_words = ['Game', 'player', 'score', 'oil', 'Man'] + text = [doc.split() for doc in corpus.data] + target_words = ["Game", "player", "score", "oil", "Man"] visualizer = DispersionPlot(target_words, ignore_case=True) visualizer.fit(text) @@ -78,58 +93,49 @@ def test_dispersionplot_generator_input(self): self.assert_images_similar(visualizer, tol=25) - def test_dispersionplot_annotate_docs(self): + def test_dispersion_plot_annotate_docs(self): """ Assert no errors occur during DispersionPlot integration with annotate_docs parameter turned on """ - corpus = self.load_data('hobbies') - text = [doc.split() for doc in corpus.data] - target_words = ['girl', 'she', 'boy', 'he', 'man'] + target_words = ["girl", "she", "boy", "he", "man"] visualizer = DispersionPlot(target_words, annotate_docs=True) visualizer.fit(text) visualizer.ax.grid(False) - self.assert_images_similar(visualizer, tol=25) + self.assert_images_similar(visualizer, tol=25.5) - def test_dispersionplot_color_words_by_class(self): + def test_dispersion_plot_color_by_class(self): """ Assert no errors occur during DispersionPlot integration when target values are specified """ - corpus = self.load_data('hobbies') - - text = (doc.split() for doc in corpus.data) - target_words = ['girl', 'she', 'boy', 'he', 'man'] - - target_values = corpus.target + target = corpus.target + text = [doc.split() for doc in corpus.data] + target_words = ["girl", "she", "boy", "he", "man"] visualizer = DispersionPlot(target_words) - visualizer.fit(text, target_values) + visualizer.fit(text, target) visualizer.ax.grid(False) self.assert_images_similar(visualizer, tol=25) - def test_dispersionplot_mismatched_labels(self): + def test_dispersion_plot_mismatched_labels(self): """ Assert exception is raised when number of labels doesn't match """ - corpus = self.load_data('hobbies') - - text = (doc.split() for doc in corpus.data) - target_words = ['girl', 'she', 'boy', 'he', 'man'] - - target_values = corpus.target + target = corpus.target + text = [doc.split() for doc in corpus.data] + target_words = ["girl", "she", "boy", "he", "man"] - visualizer = DispersionPlot(target_words, annotate_docs=True, - labels=['a', 'b']) + visualizer = DispersionPlot(target_words, annotate_docs=True, labels=["a", "b"]) msg = ( - r'number of supplied labels \(\d\) ' - r'does not match the number of classes \(\d\)' + r"number of supplied labels \(\d\) " + r"does not match the number of classes \(\d\)" ) with pytest.raises(YellowbrickValueError, match=msg): - visualizer.fit(text, target_values) + visualizer.fit(text, target) diff --git a/tests/test_text/test_freqdist.py b/tests/test_text/test_freqdist.py index e687d8b28..2679703fe 100644 --- a/tests/test_text/test_freqdist.py +++ b/tests/test_text/test_freqdist.py @@ -2,13 +2,12 @@ # Tests for the frequency distribution visualization # # Author: Rebecca Bilbro -# Github: @rebeccabilbro # Created: 2017-03-22 15:27 # -# Copyright (C) 2018 +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: test_freqdist.py [bd9cbb9] rbilbro@districtdatalabs.com $ +# ID: test_freqdist.py [bd9cbb9] rebecca.bilbro@bytecubed.com $ """ Tests for the frequency distribution text visualization @@ -18,36 +17,41 @@ ## Imports ########################################################################## -import sys import pytest +from yellowbrick.datasets import load_hobbies from yellowbrick.text.freqdist import * -from tests.dataset import DatasetMixin -from tests.base import VisualTestCase +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase + from sklearn.feature_extraction.text import CountVectorizer +########################################################################## +## Data +########################################################################## + +corpus = load_hobbies() ########################################################################## ## FreqDist Tests ########################################################################## -class FreqDistTests(VisualTestCase, DatasetMixin): +class TestFreqDist(VisualTestCase): @pytest.mark.xfail( - sys.platform == 'win32', reason="images not close on windows" + IS_WINDOWS_OR_CONDA, + reason="font rendering different in OS and/or Python; see #892", ) def test_integrated_freqdist(self): """ Assert no errors occur during freqdist integration """ - corpus = self.load_data('hobbies') vectorizer = CountVectorizer() - docs = vectorizer.fit_transform(corpus.data) - features = vectorizer.get_feature_names() + docs = vectorizer.fit_transform(corpus.data) + features = vectorizer.get_feature_names() visualizer = FreqDistVisualizer(features) visualizer.fit(docs) - visualizer.poof() - self.assert_images_similar(visualizer, tol=1) + visualizer.finalize() + self.assert_images_similar(visualizer) diff --git a/tests/test_text/test_postag.py b/tests/test_text/test_postag.py index d465c1b12..16ffd6b90 100644 --- a/tests/test_text/test_postag.py +++ b/tests/test_text/test_postag.py @@ -1,11 +1,10 @@ -# -*- coding: utf8 -*- # tests.test_text.test_postag # Tests for the part-of-speech tagging visualization # -# Author: Rebecca Bilbro -# Created: 2017-03-22 15:46 +# Author: Rebecca Bilbro +# Created: 2019-02-19 21:29 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2019 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_postag.py [bd9cbb9] rebecca.bilbro@bytecubed.com $ @@ -20,36 +19,83 @@ import pytest +from yellowbrick.exceptions import YellowbrickValueError from yellowbrick.text.postag import * - +from tests.base import VisualTestCase +import matplotlib.pyplot as plt try: import nltk - from nltk import pos_tag, word_tokenize + from nltk import pos_tag, sent_tokenize + from nltk import word_tokenize, wordpunct_tokenize except ImportError: nltk = None +try: + import spacy +except ImportError: + spacy = None + ########################################################################## -## Fixtures +## Data ########################################################################## -pie = """ -In a small saucepan, combine sugar and eggs -until well blended. Cook over low heat, stirring -constantly, until mixture reaches 160° and coats -the back of a metal spoon. Remove from the heat. -Stir in chocolate and vanilla until smooth. Cool -to lukewarm (90°), stirring occasionally. In a small -bowl, cream butter until light and fluffy. Add cooled -chocolate mixture; beat on high speed for 5 minutes -or until light and fluffy. In another large bowl, -beat cream until it begins to thicken. Add -confectioners' sugar; beat until stiff peaks form. -Fold into chocolate mixture. Pour into crust. Chill -for at least 6 hours before serving. Garnish with -whipped cream and chocolate curls if desired. -""" +sonnets = [ + """ + FROM fairest creatures we desire increase, + That thereby beauty's rose might never die, + But as the riper should by time decease, + His tender heir might bear his memory: + But thou, contracted to thine own bright eyes, + Feed'st thy light'st flame with self-substantial fuel, + Making a famine where abundance lies, + Thyself thy foe, to thy sweet self too cruel. + Thou that art now the world's fresh ornament + And only herald to the gaudy spring, + Within thine own bud buriest thy content + And, tender churl, makest waste in niggarding. + Pity the world, or else this glutton be, + To eat the world's due, by the grave and thee. + """, + """ + When forty winters shall beseige thy brow, + And dig deep trenches in thy beauty's field, + Thy youth's proud livery, so gazed on now, + Will be a tatter'd weed, of small worth held: + Then being ask'd where all thy beauty lies, + Where all the treasure of thy lusty days, + To say, within thine own deep-sunken eyes, + Were an all-eating shame and thriftless praise. + How much more praise deserved thy beauty's use, + If thou couldst answer 'This fair child of mine + Shall sum my count and make my old excuse,' + Proving his beauty by succession thine! + This were to be new made when thou art old, + And see thy blood warm when thou feel'st it cold. + """, + """ + Look in thy glass, and tell the face thou viewest + Now is the time that face should form another; + Whose fresh repair if now thou not renewest, + Thou dost beguile the world, unbless some mother. + For where is she so fair whose unear'd womb + Disdains the tillage of thy husbandry? + Or who is he so fond will be the tomb + Of his self-love, to stop posterity? + Thou art thy mother's glass, and she in thee + Calls back the lovely April of her prime: + So thou through windows of thine age shall see + Despite of wrinkles this thy golden time. + But if thou live, remember'd not to be, + Die single, and thine image dies with thee. + """, +] + + +########################################################################## +## PosTag Utils +########################################################################## def check_nltk_data(): @@ -57,32 +103,247 @@ def check_nltk_data(): Returns True if NLTK data has been downloaded, False otherwise """ try: - nltk.data.find('corpora/treebank') + nltk.data.find("corpora/treebank") return True except LookupError: pytest.xfail("error occured because nltk postag data is not available") +def check_spacy_data(): + """ + Returns True if SpaCy data has been downloaded, False otherwise + """ + try: + spacy.load("en_core_web_sm") + return True + except OSError: + pytest.xfail("error occured because spacy data model is not available") + + +def get_tagged_docs(X, model="nltk", tagger="word"): + """ + X is a list of strings; each string is a single document. + For each document, perform part-of-speech tagging, and + yield a list of sentences, where each sentence is a list + of (token, tag) tuples + + If model=="nltk", `NLTK` will be used to sentence and word + tokenize the incoming documents. User may select the `NLTK` + tagger to be used; (for now) either the word tokenizer or the + workpunct tokenizer. + + If model=="spacy", `SpaCy` will be used to sentence and word + tokenize the incoming documents. + """ + if model == "spacy": + nlp = spacy.load("en_core_web_sm") + for doc in X: + tagged = nlp(doc) + yield [ + list((token.text, token.pos_) for token in sent) + for sent in tagged.sents + ] + + elif model == "nltk": + if tagger == "wordpunct": + for doc in X: + yield [pos_tag(wordpunct_tokenize(sent)) for sent in sent_tokenize(doc)] + else: + for doc in X: + yield [pos_tag(word_tokenize(sent)) for sent in sent_tokenize(doc)] + + ########################################################################## ## PosTag Tests ########################################################################## -class TestPosTag(object): + +class TestPosTag(VisualTestCase): """ PosTag (Part of Speech Tagging Visualizer) Tests """ + def test_quick_method(self): + """ + Assert no errors occur when using the quick method + """ + # Fail if data hasn't been downloaded + check_nltk_data() + + _, ax = plt.subplots() + tagged_docs = list(get_tagged_docs(sonnets)) + + viz = postag(tagged_docs, ax=ax) + viz.ax.grid(False) + + self.assert_images_similar(viz) + + def test_unknown_tagset(self): + """ + Ensure an exception is raised if the specified tagset is unknown + """ + with pytest.raises(YellowbrickValueError): + PosTagVisualizer(tagset="brill") + + def test_frequency_mode(self): + """ + Assert no errors occur when the visualizer is run on frequency mode + """ + check_nltk_data() + + _, ax = plt.subplots() + tagged_docs = list(get_tagged_docs(sonnets)) + + viz = PosTagVisualizer(ax=ax, frequency=True) + viz.fit(tagged_docs) + viz.finalize() + ax.grid(False) + + # Sorted tags i.e predetermined order + sorted_tags = [ + "noun", + "adjective", + "punctuation", + "verb", + "preposition", + "determiner", + "adverb", + "conjunction", + "pronoun", + "wh- word", + "modal", + "infinitive", + "possessive", + "other", + "symbol", + "existential", + "digit", + "non-English", + "interjection", + "list", + ] + + # Extract tick labels from the plot + ticks_ax = [tick.get_text() for tick in ax.xaxis.get_ticklabels()] + + # Assert that ticks are set properly + assert ticks_ax == sorted_tags + + self.assert_images_similar(ax=ax, tol=0.5) + @pytest.mark.skipif(nltk is None, reason="test requires nltk") - def test_integrated_postag(self): + def test_word_tagged(self): """ - Assert no errors occur during postag integration + Assert no errors occur during PosTagVisualizer integration + with word tokenized corpus """ + # Fail if data hasn't been downloaded + check_nltk_data() + + tagged_docs = list(get_tagged_docs(sonnets, model="nltk", tagger="word")) + + visualizer = PosTagVisualizer(tagset="penn_treebank") + + visualizer.fit(tagged_docs) + visualizer.ax.grid(False) + + self.assert_images_similar(visualizer) + @pytest.mark.skipif(nltk is None, reason="test requires nltk") + def test_wordpunct_tagged(self): + """ + Assert no errors occur during PosTagVisualizer integration + with wordpunct tokenized corpus + """ # Fail if data hasn't been downloaded check_nltk_data() - tokens = word_tokenize(pie) - tagged = pos_tag(tokens) + wordpunct_tagged_docs = list( + get_tagged_docs(sonnets, model="nltk", tagger="wordpunct") + ) + + visualizer = PosTagVisualizer(tagset="penn_treebank") + + visualizer.fit(wordpunct_tagged_docs) + visualizer.ax.grid(False) + + self.assert_images_similar(visualizer) + + @pytest.mark.skipif(spacy is None, reason="test requires spacy") + def test_spacy_tagged(self): + """ + Assert no errors occur during PosTagVisualizer integration + with spacy tokenized corpus + """ + # Fail if data hasn't been downloaded + check_spacy_data() + + spacy_tagged_docs = list(get_tagged_docs(sonnets, model="spacy")) + + visualizer = PosTagVisualizer(tagset="universal") + + visualizer.fit(spacy_tagged_docs) + visualizer.ax.grid(False) + + self.assert_images_similar(visualizer) + + def test_stack_mode(self): + """ + Assert no errors occur when the visualizer is run on stack mode + """ + check_nltk_data() + + _, ax = plt.subplots() + tagged_docs = list(get_tagged_docs(sonnets)) + + visualizer = PosTagVisualizer(stack=True, ax=ax) + visualizer.fit(tagged_docs, y=["a", "b", "c"]) + visualizer.ax.grid(False) + + self.assert_images_similar(ax=ax) + + def test_stack_frequency_mode(self): + """ + Assert no errors occur when the visualizer is run on both stack and + frequency mode + """ + check_nltk_data() + + _, ax = plt.subplots() + tagged_docs = list(get_tagged_docs(sonnets)) + + visualizer = PosTagVisualizer(stack=True, frequency=True, ax=ax) + visualizer.fit(tagged_docs, y=["a", "b", "c"]) + visualizer.ax.grid(False) + + # Sorted tags i.e predetermined order + sorted_tags = [ + "noun", + "adjective", + "punctuation", + "verb", + "preposition", + "determiner", + "adverb", + "conjunction", + "pronoun", + "wh- word", + "modal", + "infinitive", + "possessive", + "other", + "symbol", + "existential", + "digit", + "non-English", + "interjection", + "list", + ] + + # Extract tick labels from the plot + ticks_ax = [tick.get_text() for tick in ax.xaxis.get_ticklabels()] + + # Assert that ticks are set properly + assert ticks_ax == sorted_tags - visualizer = PosTagVisualizer() - visualizer.transform(tagged) + self.assert_images_similar(ax=ax) diff --git a/tests/test_text/test_tsne.py b/tests/test_text/test_tsne.py index 9cde779e5..2e6f7a864 100644 --- a/tests/test_text/test_tsne.py +++ b/tests/test_text/test_tsne.py @@ -1,10 +1,10 @@ # tests.test_text.test_tsne # Tests for the TSNE visual corpus embedding mechanism. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Mon Feb 20 07:23:53 2017 -0500 # -# Copyright (C) 2016 Bengfort.com +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_tsne.py [6aa9198] benjamin@bengfort.com $ @@ -17,12 +17,12 @@ ## Imports ########################################################################## -import six import pytest +from unittest import mock from yellowbrick.text.tsne import * from tests.base import VisualTestCase -from tests.dataset import DatasetMixin +from yellowbrick.datasets import load_hobbies from yellowbrick.exceptions import YellowbrickValueError from sklearn.manifold import TSNE @@ -34,16 +34,20 @@ except ImportError: pandas = None -try: - from unittest import mock -except ImportError: - import mock + +########################################################################## +## Data +########################################################################## + +corpus = load_hobbies() + ########################################################################## ## TSNE Tests ########################################################################## -class TestTSNE(VisualTestCase, DatasetMixin): + +class TestTSNE(VisualTestCase): """ TSNEVisualizer tests """ @@ -53,20 +57,20 @@ def test_bad_decomposition(self): Ensure an error is raised when a bad decompose argument is specified """ with pytest.raises(YellowbrickValueError): - TSNEVisualizer(decompose='bob') + TSNEVisualizer(decompose="bob") def test_make_pipeline(self): """ Verify the pipeline creation step for TSNE """ - tsne = TSNEVisualizer() # Should not cause an exception. + tsne = TSNEVisualizer() # Should not cause an exception. assert tsne.transformer_ is not None - svdp = tsne.make_transformer('svd', 90) + svdp = tsne.make_transformer("svd", 90) assert len(svdp.steps) == 2 - pcap = tsne.make_transformer('pca') + pcap = tsne.make_transformer("pca") assert len(pcap.steps) == 2 none = tsne.make_transformer(None) @@ -76,17 +80,15 @@ def test_integrated_tsne(self): """ Check tSNE integrated visualization on the hobbies corpus """ - corpus = self.load_data('hobbies') - tfidf = TfidfVectorizer() + tfidf = TfidfVectorizer() - docs = tfidf.fit_transform(corpus.data) + docs = tfidf.fit_transform(corpus.data) labels = corpus.target - tsne = TSNEVisualizer(random_state=8392, colormap='Set1', alpha=1.0) + tsne = TSNEVisualizer(random_state=8392, colormap="Set1", alpha=1.0) tsne.fit_transform(docs, labels) - tol = 50 if six.PY3 else 55 - self.assert_images_similar(tsne, tol=tol) + self.assert_images_similar(tsne, tol=50) def test_sklearn_tsne_size(self): """ @@ -97,7 +99,7 @@ def test_sklearn_tsne_size(self): # like size, are passed through to YB's finalize method. This test should # notify us if TSNE's params change on the sklearn side. with pytest.raises(TypeError): - TSNE(size=(100,100)) + TSNE(size=(100, 100)) def test_sklearn_tsne_title(self): """ @@ -126,22 +128,66 @@ def test_custom_size_tsne(self): assert tsne._size == (100, 50) + def test_custom_colors_tsne(self): + """ + Check tSNE accepts and properly handles custom colors from user + """ + ## produce random data + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=5, + random_state=42, + ) + + ## specify a list of custom colors >= n_classes + purple_blues = ["indigo", "orchid", "plum", "navy", "purple", "blue"] + + ## instantiate the visualizer and check that self.colors is correct + purple_tsne = TSNEVisualizer(colors=purple_blues, random_state=87) + assert purple_tsne.colors == purple_blues + + ## fit the visualizer and check that self.color_values is as long as + ## n_classes and is the first n_classes items in self.colors + purple_tsne.fit(X, y) + assert len(purple_tsne.color_values_) == len(purple_tsne.classes_) + assert purple_tsne.color_values_ == purple_blues[: len(purple_tsne.classes_)] + + ## specify a list of custom colors < n_classes + greens = ["green", "lime", "teal"] + + ## instantiate the visualizer and check that self.colors is correct + green_tsne = TSNEVisualizer(colors=greens, random_state=87) + assert green_tsne.colors == greens + + ## fit the visualizer and check that self.color_values is as long as + ## n_classes and the user-supplied color list gets recycled as expected + green_tsne.fit(X, y) + assert len(green_tsne.color_values_) == len(green_tsne.classes_) + assert green_tsne.color_values_ == ["green", "lime", "teal", "green", "lime"] + def test_make_classification_tsne(self): """ Test tSNE integrated visualization on a sklearn classifier dataset """ ## produce random data - X, y = make_classification(n_samples=200, n_features=100, - n_informative=20, n_redundant=10, - n_classes=3, random_state=42) + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) ## visualize data with t-SNE tsne = TSNEVisualizer(random_state=87) tsne.fit(X, y) - tol = 0.1 if six.PY3 else 40 - self.assert_images_similar(tsne, tol=tol) + self.assert_images_similar(tsne, tol=0.1) def test_make_classification_tsne_class_labels(self): """ @@ -149,44 +195,58 @@ def test_make_classification_tsne_class_labels(self): """ ## produce random data - X, y = make_classification(n_samples=200, n_features=100, - n_informative=20, n_redundant=10, - n_classes=3, random_state=42) + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) ## visualize data with t-SNE - tsne = TSNEVisualizer(random_state=87, labels=['a', 'b', 'c']) + tsne = TSNEVisualizer(random_state=87, labels=["a", "b", "c"]) tsne.fit(X, y) - tol = 0.1 if six.PY3 else 40 - self.assert_images_similar(tsne, tol=tol) + self.assert_images_similar(tsne, tol=0.1) def test_tsne_mismtached_labels(self): """ Assert exception is raised when number of labels doesn't match """ ## produce random data - X, y = make_classification(n_samples=200, n_features=100, - n_informative=20, n_redundant=10, - n_classes=3, random_state=42) + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) ## fewer labels than classes - tsne = TSNEVisualizer(random_state=87, labels=['a', 'b']) + tsne = TSNEVisualizer(random_state=87, labels=["a", "b"]) with pytest.raises(YellowbrickValueError): - tsne.fit(X,y) + tsne.fit(X, y) ## more labels than classes - tsne = TSNEVisualizer(random_state=87, labels=['a', 'b', 'c', 'd']) + tsne = TSNEVisualizer(random_state=87, labels=["a", "b", "c", "d"]) with pytest.raises(YellowbrickValueError): - tsne.fit(X,y) + tsne.fit(X, y) def test_no_target_tsne(self): """ Test tSNE when no target or classes are specified """ ## produce random data - X, y = make_classification(n_samples=200, n_features=100, - n_informative=20, n_redundant=10, - n_classes=3, random_state=6897) + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=6897, + ) ## visualize data with t-SNE tsne = TSNEVisualizer(random_state=64) @@ -200,8 +260,12 @@ def test_visualizer_with_pandas(self): Test tSNE when passed a pandas DataFrame and series """ X, y = make_classification( - n_samples=200, n_features=100, n_informative=20, n_redundant=10, - n_classes=3, random_state=3020 + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=3020, ) X = pandas.DataFrame(X) @@ -210,17 +274,21 @@ def test_visualizer_with_pandas(self): tsne = TSNEVisualizer(random_state=64) tsne.fit(X, y) - tol = 0.1 if six.PY3 else 40 - self.assert_images_similar(tsne, tol=tol) + self.assert_images_similar(tsne, tol=0.1) def test_alpha_param(self): """ Test that the user can supply an alpha param on instantiation """ ## produce random data - X, y = make_classification(n_samples=200, n_features=100, - n_informative=20, n_redundant=10, - n_classes=3, random_state=42) + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) ## Instantiate a TSNEVisualizer, provide custom alpha tsne = TSNEVisualizer(random_state=64, alpha=0.5) diff --git a/tests/test_text/test_umap.py b/tests/test_text/test_umap.py new file mode 100644 index 000000000..ef24d1e75 --- /dev/null +++ b/tests/test_text/test_umap.py @@ -0,0 +1,323 @@ +# tests.test_text.test_umap +# Tests for the UMAP visual corpus embedding mechanism. +# +# Author: John Healy +# Created: Mon Dec 03, 14:00:00 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: test_umap.py [] jchealy@gmail.com> $ + +""" +Tests for the UMAP visual corpus embedding mechanism. +""" + +########################################################################## +## Imports +########################################################################## + +import sys +import pytest +import warnings + +from unittest import mock +from tests.base import VisualTestCase +from yellowbrick.text.umap_vis import * +from yellowbrick.datasets import load_hobbies +from yellowbrick.exceptions import YellowbrickValueError + +from sklearn.datasets import make_classification +from sklearn.feature_extraction.text import TfidfVectorizer + +try: + import pandas +except ImportError: + pandas = None + +try: + from umap import UMAP +except ImportError: + UMAP = None +except (RuntimeError, AttributeError): + UMAP = None + warnings.warn( + "Error Importing UMAP. UMAP does not support python 2.7 on Windows 32 bit." + ) + + +########################################################################## +## Data +########################################################################## + +corpus = load_hobbies() + + +########################################################################## +## UMAP Tests +########################################################################## + + +@mock.patch("yellowbrick.text.umap_vis.UMAP", None) +def test_umap_unavailable(): + """ + Assert an appropriate exception is raised when UMAP is not installed + """ + from yellowbrick.text.umap_vis import UMAP + + assert UMAP is None + + with pytest.raises( + YellowbrickValueError, match="umap package doesn't seem to be installed" + ): + UMAPVisualizer() + + +@pytest.mark.skipif(UMAP is None, reason="tests require the umap library") +@pytest.mark.xfail( + sys.platform == "win32", reason="not supported on windows 32bit with Python 2.7" +) +class TestUMAP(VisualTestCase): + """ + UMAPVisualizer tests + """ + + def test_make_pipeline(self): + """ + Verify the pipeline creation step for UMAP + """ + + umap = UMAPVisualizer() # Should not cause an exception. + assert umap.transformer_ is not None + + assert len(umap.transformer_.steps) == 1 + + def test_integrated_umap(self): + """ + Check UMAP integrated visualization on the hobbies corpus + """ + tfidf = TfidfVectorizer() + + docs = tfidf.fit_transform(corpus.data) + labels = corpus.target + + umap = UMAPVisualizer(random_state=8392, colormap="Set1", alpha=1.0) + umap.fit_transform(docs, labels) + + tol = 55 + self.assert_images_similar(umap, tol=tol) + + def test_sklearn_umap_size(self): + """ + Check to make sure sklearn's UMAP doesn't use the size param + """ + # In UMAPVisualizer, the internal sklearn UMAP transform consumes + # some but not all kwargs passed in by user. Those not in get_params(), + # like size, are passed through to YB's finalize method. This test should + # notify us if UMAP's params change on the sklearn side. + with pytest.raises(TypeError): + UMAP(size=(100, 100)) + + def test_sklearn_umap_title(self): + """ + Check to make sure sklearn's UMAP doesn't use the title param + """ + # In TSNEVisualizer, the internal sklearn UMAP transform consumes + # some but not all kwargs passed in by user. Those not in get_params(), + # like title, are passed through to YB's finalize method. This test should + # notify us if UMAP's params change on the sklearn side. + with pytest.raises(TypeError): + UMAP(title="custom_title") + + def test_custom_title_umap(self): + """ + Check UMAP can accept a custom title (string) from the user + """ + umap = UMAPVisualizer(title="custom_title") + + assert umap.title == "custom_title" + + def test_custom_size_umap(self): + """ + Check UMAP can accept a custom size (tuple of pixels) from the user + """ + umap = UMAPVisualizer(size=(100, 50)) + + assert umap._size == (100, 50) + + def test_custom_colors_umap(self): + """ + Check UMAP accepts and properly handles custom colors from user + """ + ## produce random data + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=5, + random_state=42, + ) + + ## specify a list of custom colors >= n_classes + purple_blues = ["indigo", "orchid", "plum", "navy", "purple", "blue"] + + ## instantiate the visualizer and check that self.colors is correct + purple_umap = UMAPVisualizer(colors=purple_blues, random_state=87) + assert purple_umap.colors == purple_blues + + ## fit the visualizer and check that self.color_values is as long as + ## n_classes and is the first n_classes items in self.colors + purple_umap.fit(X, y) + assert len(purple_umap.color_values_) == len(purple_umap.classes_) + assert purple_umap.color_values_ == purple_blues[: len(purple_umap.classes_)] + + ## specify a list of custom colors < n_classes + greens = ["green", "lime", "teal"] + + ## instantiate the visualizer and check that self.colors is correct + green_umap = UMAPVisualizer(colors=greens, random_state=87) + assert green_umap.colors == greens + + ## fit the visualizer and check that self.color_values is as long as + ## n_classes and the user-supplied color list gets recycled as expected + green_umap.fit(X, y) + assert len(green_umap.color_values_) == len(green_umap.classes_) + assert green_umap.color_values_ == ["green", "lime", "teal", "green", "lime"] + + def test_make_classification_umap(self): + """ + Test UMAP integrated visualization on a sklearn classifier dataset + """ + + ## produce random data + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) + + ## visualize data with UMAP + umap = UMAPVisualizer(random_state=87) + umap.fit(X, y) + + self.assert_images_similar(umap, tol=40) + + def test_make_classification_umap_class_labels(self): + """ + Test UMAP integrated visualization with class labels specified + """ + + ## produce random data + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) + + ## visualize data with UMAP + umap = UMAPVisualizer(random_state=87, labels=["a", "b", "c"]) + umap.fit(X, y) + + self.assert_images_similar(umap, tol=40) + + def test_umap_mismtached_labels(self): + """ + Assert exception is raised when number of labels doesn't match + """ + ## produce random data + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) + + ## fewer labels than classes + umap = UMAPVisualizer(random_state=87, labels=["a", "b"]) + with pytest.raises(YellowbrickValueError): + umap.fit(X, y) + + ## more labels than classes + umap = UMAPVisualizer(random_state=87, labels=["a", "b", "c", "d"]) + with pytest.raises(YellowbrickValueError): + umap.fit(X, y) + + def test_no_target_umap(self): + """ + Test UMAP when no target or classes are specified + """ + ## produce random data + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=6897, + ) + + ## visualize data with UMAP + umap = UMAPVisualizer(random_state=64) + umap.fit(X) + + self.assert_images_similar(umap, tol=40) + + @pytest.mark.skipif(pandas is None, reason="test requires pandas") + def test_visualizer_with_pandas(self): + """ + Test UMAP when passed a pandas DataFrame and series + """ + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=3020, + ) + + X = pandas.DataFrame(X) + y = pandas.Series(y) + + umap = UMAPVisualizer(random_state=64) + umap.fit(X, y) + + self.assert_images_similar(umap, tol=40) + + def test_alpha_param(self): + """ + Test that the user can supply an alpha param on instantiation + """ + ## produce random data + X, y = make_classification( + n_samples=200, + n_features=100, + n_informative=20, + n_redundant=10, + n_classes=3, + random_state=42, + ) + + ## Instantiate a UMAPVisualizer, provide custom alpha + umap = UMAPVisualizer(random_state=64, alpha=0.5) + + # Test param gets set correctly + assert umap.alpha == 0.5 + + # Mock ax and fit the visualizer + umap.ax = mock.MagicMock(autospec=True) + umap.fit(X, y) + + # Test that alpha was passed to internal matplotlib scatterplot + _, scatter_kwargs = umap.ax.scatter.call_args + assert "alpha" in scatter_kwargs + assert scatter_kwargs["alpha"] == 0.5 diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py index bd1ef00e9..db507a95d 100644 --- a/tests/test_utils/__init__.py +++ b/tests/test_utils/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Thu May 18 15:15:05 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [79cd8cf] benjamin@bengfort.com $ diff --git a/tests/test_utils/test_decorators.py b/tests/test_utils/test_decorators.py index 99e6a477f..322de8713 100644 --- a/tests/test_utils/test_decorators.py +++ b/tests/test_utils/test_decorators.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Thu May 18 15:14:34 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_decorators.py [79cd8cf] benjamin@bengfort.com $ @@ -17,8 +17,6 @@ ## Imports ########################################################################## -import unittest - from yellowbrick.utils.decorators import * @@ -26,7 +24,8 @@ ## Decorator Tests ########################################################################## -class DecoratorTests(unittest.TestCase): + +class TestDecorators(object): """ Tests for the decorator utilities. """ @@ -37,16 +36,14 @@ def test_memoization(self): """ class Visualizer(object): - @memoized def foo(self): return "bar" viz = Visualizer() - self.assertFalse(hasattr(viz, "_foo")) - self.assertEqual(viz.foo, "bar") - self.assertEqual(viz._foo, "bar") - + assert not hasattr(viz, "_foo") + assert viz.foo == "bar" + assert viz._foo == "bar" def test_docutil(self): """ @@ -54,14 +51,12 @@ def test_docutil(self): """ class Visualizer(object): - def __init__(self): """ This is the correct docstring. """ pass - def undecorated(*args, **kwargs): """ This is an undecorated function string. @@ -69,34 +64,18 @@ def undecorated(*args, **kwargs): pass # Test the undecorated string to protect from magic - self.assertEqual( - undecorated.__doc__.strip(), "This is an undecorated function string." - ) + assert undecorated.__doc__.strip() == "This is an undecorated function string." # Decorate manually and test the newly decorated return function. decorated = docutil(Visualizer.__init__)(undecorated) - self.assertEqual( - decorated.__doc__.strip(), "This is the correct docstring." - ) + assert decorated.__doc__.strip() == "This is the correct docstring." # Assert that decoration modifies the original function. - self.assertEqual( - undecorated.__doc__.strip(), "This is the correct docstring." - ) + assert undecorated.__doc__.strip() == "This is the correct docstring." @docutil(Visualizer.__init__) def sugar(*args, **kwargs): pass # Assert that syntactic sugar works as expected. - self.assertEqual( - sugar.__doc__.strip(), "This is the correct docstring." - ) - - -########################################################################## -## Execute Tests -########################################################################## - -if __name__ == "__main__": - unittest.main() + assert sugar.__doc__.strip() == "This is the correct docstring." diff --git a/tests/test_utils/test_helpers.py b/tests/test_utils/test_helpers.py index 6c4fe9f62..91d761ebe 100644 --- a/tests/test_utils/test_helpers.py +++ b/tests/test_utils/test_helpers.py @@ -2,10 +2,11 @@ # tests.test_utils.test_helpers # Tests for the stand alone helper functions in Yellowbrick utils. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Fri May 19 10:43:43 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_helpers.py [79cd8cf] benjamin@bengfort.com $ @@ -22,32 +23,41 @@ import numpy as np import numpy.testing as npt -from yellowbrick.utils.helpers import * - +from sklearn.svm import SVR, SVC from sklearn.pipeline import Pipeline from sklearn.decomposition import PCA +from sklearn.naive_bayes import GaussianNB +from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier -from sklearn.linear_model import LassoCV -from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestClassifier -from sklearn.cluster import KMeans +from sklearn.cluster import Birch, AgglomerativeClustering +from sklearn.decomposition import LatentDirichletAllocation as LDA +from sklearn.cluster import KMeans, AffinityPropagation, MiniBatchKMeans +from sklearn.datasets import make_classification, make_regression, make_blobs +from sklearn.linear_model import Ridge, RidgeCV, Lasso, LassoCV, LinearRegression +from yellowbrick.utils.helpers import * ########################################################################## ## Helper Function Tests ########################################################################## + class TestHelpers(object): """ Helper functions and utilities """ - @pytest.mark.parametrize("model, name", [ - (LassoCV, 'LassoCV'), - (KNeighborsClassifier, 'KNeighborsClassifier'), - (KMeans, 'KMeans'), - (RandomForestClassifier, 'RandomForestClassifier'), - ], ids=["LassoCV", "KNeighborsClassifier", "KMeans", "RandomForestClassifier"]) + @pytest.mark.parametrize( + "model, name", + [ + (LassoCV, "LassoCV"), + (KNeighborsClassifier, "KNeighborsClassifier"), + (KMeans, "KMeans"), + (RandomForestClassifier, "RandomForestClassifier"), + ], + ids=["LassoCV", "KNeighborsClassifier", "KMeans", "RandomForestClassifier"], + ) def test_real_model(self, model, name): """ Test getting model name for sklearn estimators @@ -58,9 +68,8 @@ def test_pipeline(self): """ Test getting model name for sklearn pipelines """ - pipeline = Pipeline([('reduce_dim', PCA()), - ('linreg', LinearRegression())]) - assert get_model_name(pipeline) == 'LinearRegression' + pipeline = Pipeline([("reduce_dim", PCA()), ("linreg", LinearRegression())]) + assert get_model_name(pipeline) == "LinearRegression" def test_int_input(self): """ @@ -74,7 +83,95 @@ def test_str_input(self): Assert a type error is raised when a str is passed to model name """ with pytest.raises(TypeError): - get_model_name('helloworld') + get_model_name("helloworld") + + @pytest.mark.parametrize( + "regressor", + [SVR, Ridge, Lasso, RidgeCV, LassoCV], + ids=["SVR", "Ridge", "Lasso", "RidgeCV", "LassoCV"], + ) + def test_is_fitted_regressors(self, regressor): + """ + Assert sklearn regressors return False for fitted before fit and True after + """ + assert is_fitted(regressor()) is False + + X, y = make_regression(n_samples=20, n_features=3) + fitted = regressor().fit(X, y) + assert is_fitted(fitted) is True + + @pytest.mark.parametrize( + "classifier", + [ + SVC, + GaussianNB, + KNeighborsClassifier, + RandomForestClassifier, + DecisionTreeClassifier, + ], + ids=[ + "SVC", + "GaussianNB", + "KNeighborsClassifier", + "RandomForestClassifier", + "DecisionTreeClassifier", + ], + ) + def test_is_fitted_classifiers(self, classifier): + """ + Assert sklearn classifiers return False for fitted before fit and True after + """ + assert is_fitted(classifier()) is False + + X, y = make_classification(n_samples=20, n_features=3, n_redundant=0) + fitted = classifier().fit(X, y) + assert is_fitted(fitted) is True + + @pytest.mark.parametrize( + "clusterer", + [ + PCA, # AttributeError: has no attribute 'predict' + LDA, # AttributeError: has no attribute 'predict' + Birch, + KMeans, + MiniBatchKMeans, + AffinityPropagation, + AgglomerativeClustering, # AttributeError: has no attribute 'predict' + ], + ids=[ + "PCA", + "LDA", + "Birch", + "KMeans", + "MiniBatchKMeans", + "AffinityPropagation", + "AgglomerativeClustering", + ], + ) + def test_is_fitted_clusterers(self, clusterer): + """ + Assert sklearn clusterers return False for fitted before fit and True after + """ + assert is_fitted(clusterer()) is False + + X, _ = make_blobs(n_samples=20, center_box=(10.0, 20.0)) + fitted = clusterer().fit(X) + assert is_fitted(fitted) is True + + def test_check_fitted(self): + """ + Verify the user can specify whether or not to check if the model is fitted + """ + model = SVC() + assert check_fitted(model, is_fitted_by="auto") is False + assert check_fitted(model, is_fitted_by=True) is True + assert check_fitted(model, is_fitted_by=False) is False + + X, y = make_classification(n_samples=20, n_features=3, n_redundant=0) + model.fit(X, y) + assert check_fitted(model, is_fitted_by="auto") is True + assert check_fitted(model, is_fitted_by=True) is True + assert check_fitted(model, is_fitted_by=False) is False ########################################################################## @@ -91,33 +188,32 @@ def test_div_1d_by_scalar(self): """ Test divide 1D vector by scalar """ - result = div_safe( [-1, 0, 1], 0 ) + result = div_safe([-1, 0, 1], 0) assert result.all() == 0 def test_div_1d_by_1d(self): """ Test divide 1D vector by another 1D vector with same length """ - result = div_safe( [-1, 0 , 1], [0,0,0]) + result = div_safe([-1, 0, 1], [0, 0, 0]) assert result.all() == 0 def test_div_2d_by_1d(self): """ Test divide 2D vector by 1D vector with similar shape component """ - numerator = np.array([[-1,0,1,2],[1,-1,0,3]]) - denominator = [0,0,0,0] + numerator = np.array([[-1, 0, 1, 2], [1, -1, 0, 3]]) + denominator = [0, 0, 0, 0] npt.assert_array_equal( - div_safe(numerator, denominator), - np.array([[0,0,0,0], [0,0,0,0]]) + div_safe(numerator, denominator), np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) ) def test_invalid_dimensions(self): """ Assert an error is raised on division with invalid dimensions """ - numerator = np.array([[-1,0,1,2],[1,-1,0,3]]) - denominator = [0,0] + numerator = np.array([[-1, 0, 1, 2], [1, -1, 0, 3]]) + denominator = [0, 0] with pytest.raises(ValueError): div_safe(numerator, denominator) @@ -133,29 +229,98 @@ def test_prop_to_size_list(self): Test prop to size correctly returns scaled values for a list """ # Hieghts (in cm) of U.S. Presidents in order of term until Lincoln - heights = [188, 170, 189, 163, 183, 171, 185, 168, 173, 183, 173, 173, 175, 178, 183, 193] + heights = [ + 188, + 170, + 189, + 163, + 183, + 171, + 185, + 168, + 173, + 183, + 173, + 173, + 175, + 178, + 183, + 193, + ] sizes = prop_to_size(heights, mi=1, ma=10, log=False, power=0.33) - npt.assert_array_almost_equal(sizes, np.array([ - 9.47447296, 6.56768746, 9.58486955, 1. , 8.87285756, - 6.81851544, 9.12441277, 5.98256068, 7.26314542, 8.87285756, - 7.26314542, 7.26314542, 7.65154152, 8.15982835, 8.87285756, - 10. - ])) + npt.assert_array_almost_equal( + sizes, + np.array( + [ + 9.47447296, + 6.56768746, + 9.58486955, + 1.0, + 8.87285756, + 6.81851544, + 9.12441277, + 5.98256068, + 7.26314542, + 8.87285756, + 7.26314542, + 7.26314542, + 7.65154152, + 8.15982835, + 8.87285756, + 10.0, + ] + ), + ) def test_prop_to_size_log(self): """ Test prop to size returns natural log scaled values """ - # Hieghts (in cm) of U.S. Presidents in order of term until Lincoln - heights = [188, 170, 189, 163, 183, 171, 185, 168, 173, 183, 173, 173, 175, 178, 183, 193] + # Heights (in cm) of U.S. Presidents in order of term until Lincoln + heights = [ + 188, + 170, + 189, + 163, + 183, + 171, + 185, + 168, + 173, + 183, + 173, + 173, + 175, + 178, + 183, + 193, + ] sizes = prop_to_size(heights, mi=1, ma=10, log=True, power=0.5) - npt.assert_array_almost_equal(sizes, np.array([ - 9.271337, 5.49004 , 9.423692, 1. , 8.449214, 5.792968, - 8.791172, 4.806088, 6.343007, 8.449214, 6.343007, 6.343007, - 6.835994, 7.496806, 8.449214, 10. - ])) + npt.assert_array_almost_equal( + sizes, + np.array( + [ + 9.271337, + 5.49004, + 9.423692, + 1.0, + 8.449214, + 5.792968, + 8.791172, + 4.806088, + 6.343007, + 8.449214, + 6.343007, + 6.343007, + 6.835994, + 7.496806, + 8.449214, + 10.0, + ] + ), + ) def test_prop_to_size_default(self): """ @@ -173,15 +338,16 @@ def test_prop_to_size_zero_division(self): """ Ensure that prop to size does not cause division by zero errors """ - vals = [8]*8 + vals = [8] * 8 sizes = prop_to_size(vals) - npt.assert_array_equal(sizes, [0]*8) + npt.assert_array_equal(sizes, [0] * 8) ########################################################################## ## Features/Array Tests ########################################################################## + class TestNarrayIntColumns(object): """ Features and array helper tests @@ -191,7 +357,7 @@ def test_has_ndarray_int_columns_true_int_features(self): """ Ensure ndarray with int features has int columns """ - x = np.random.rand(3,5) + x = np.random.rand(3, 5) features = [0, 1] assert has_ndarray_int_columns(features, x) @@ -199,42 +365,50 @@ def test_has_ndarray_int_columns_true_int_strings(self): """ Ensure ndarray with str(int) features has int columns """ - x = np.random.rand(3,5) - features = ['0', '1'] + x = np.random.rand(3, 5) + features = ["0", "1"] assert has_ndarray_int_columns(features, x) def test_has_ndarray_int_columns_false_not_numeric(self): """ Ensure ndarray with str features does not have int columns """ - x = np.random.rand(3,5) - features = ['a', '1'] + x = np.random.rand(3, 5) + features = ["a", "1"] assert not has_ndarray_int_columns(features, x) def test_has_ndarray_int_columns_false_outside_column_range(self): """ Ensure ndarray with str(int) outside range does not have int columns """ - x = np.random.rand(3,5) - features = ['0', '10'] + x = np.random.rand(3, 5) + features = ["0", "10"] assert not has_ndarray_int_columns(features, x) - @pytest.mark.parametrize("a, increasing", [ - (np.array([0.8]), True), - (np.array([9]), False), - (np.array([0.2, 1.3, 1.4, 1.4, 1.4, 1.5, 8.3, 8.5]), True), - (np.array([8, 7, 6, 5, 5, 5, 5, 4, 3, -1, -5]), False), - ], ids=["increasing single", "decreasing single", "increasing", "decreasing"]) + @pytest.mark.parametrize( + "a, increasing", + [ + (np.array([0.8]), True), + (np.array([9]), False), + (np.array([0.2, 1.3, 1.4, 1.4, 1.4, 1.5, 8.3, 8.5]), True), + (np.array([8, 7, 6, 5, 5, 5, 5, 4, 3, -1, -5]), False), + ], + ids=["increasing single", "decreasing single", "increasing", "decreasing"], + ) def test_is_monotonic(self, a, increasing): """ Test if a vector is monotonic """ assert is_monotonic(a, increasing) - @pytest.mark.parametrize("a, increasing", [ - (np.array([0.2, 1.3, 1.3, 0.2, 1.8]), True), - (np.array([8, 7, 7, 8, 9, 6, 5]), False), - ], ids=["increasing", "decreasing"]) + @pytest.mark.parametrize( + "a, increasing", + [ + (np.array([0.2, 1.3, 1.3, 0.2, 1.8]), True), + (np.array([8, 7, 7, 8, 9, 6, 5]), False), + ], + ids=["increasing", "decreasing"], + ) def test_not_is_monotonic(self, a, increasing): """ Test if a vector is not monotonic @@ -246,13 +420,14 @@ def test_multi_dim_is_monotonic(self): Assert monotonicity is not decidable on multi-dimensional array """ with pytest.raises(ValueError): - is_monotonic(np.array([[1,2,3], [4,5,6], [7,8,9]])) + is_monotonic(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) ########################################################################## ## String Helpers Tests ########################################################################## + class TestStringHelpers(object): """ String helper functions @@ -265,7 +440,7 @@ def test_slugifiy(self): cases = ( ("This is a test ---", "this-is-a-test"), - ("This -- is a ## test ---" , "this-is-a-test"), + ("This -- is a ## test ---", "this-is-a-test"), ) for case, expected in cases: diff --git a/tests/test_utils/test_kneed.py b/tests/test_utils/test_kneed.py new file mode 100644 index 000000000..c15f0c23e --- /dev/null +++ b/tests/test_utils/test_kneed.py @@ -0,0 +1,134 @@ +# tests.test_utils.test_kneed +# A port of the tests for knee-point detection package, kneed. +# +# Author: Kevin Arvai +# Author: Pradeep Singh +# Created: Mon Apr 23 01:29:18 2019 -0400 +# +# Copyright (C) 2017 Kevin Arvai +# All rights reserved. +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list +# of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may +# be used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# ID: test_kneed.py [] pswaldia@no-reply.github.com $ + +""" +This package contains a port of the tests for knee-point detection package, kneed, by +Kevin Arvai and hosted at https://github.com/arvkevi/kneed. This port is maintained +with permission by the Yellowbrick contributors. +""" + +import numpy as np +from yellowbrick.utils.kneed import KneeLocator + +x = np.arange(0, 10) +y_convex_inc = np.array([1, 2, 3, 4, 5, 10, 15, 20, 40, 100]) +y_convex_dec = np.array(y_convex_inc[::-1]) +y_concave_dec = np.array(100 - y_convex_inc) +y_concave_inc = np.array(100 - y_convex_dec) + + +def test_concave_increasing(): + """Tests that a correct knee point is detected in + curve having concave and increasing nature.""" + kn = KneeLocator( + x, y_concave_inc, curve_nature="concave", curve_direction="increasing" + ) + assert kn.knee == 2 + + +def test_concave_decreasing(): + """Tests that a correct knee point is detected in + curve having concave and decreasing nature.""" + kn = KneeLocator( + x, y_concave_dec, curve_nature="concave", curve_direction="decreasing" + ) + assert kn.knee == 7 + + +def test_convex_increasing(): + """Tests that a correct knee point is detected in + curve having convex and increasing nature.""" + kn = KneeLocator( + x, y_convex_inc, curve_nature="convex", curve_direction="increasing" + ) + assert kn.knee == 7 + + +def test_convex_decreasing(): + """Tests that a correct knee point is detected in + curve having convex and decreasing nature.""" + kn = KneeLocator( + x, y_convex_dec, curve_nature="convex", curve_direction="decreasing" + ) + assert kn.knee == 2 + + +def test_concave_increasing_truncated(): + """Tests that a correct knee point is detected in + curve having truncated concave increasing nature""" + kn = KneeLocator( + x[:-3] / 10, + y_concave_inc[:-3] / 10, + curve_nature="concave", + curve_direction="increasing", + ) + assert kn.knee == 0.2 + + +def test_concave_decreasing_truncated(): + """Tests that a correct knee point is detected in + curve having truncated concave decreasing nature""" + kn = KneeLocator( + x[:-3] / 10, + y_concave_dec[:-3] / 10, + curve_nature="concave", + curve_direction="decreasing", + ) + assert kn.knee == 0.4 + + +def test_convex_increasing_truncated(): + """Tests that a correct knee point is detected in + curve having truncated convex increasing nature""" + kn = KneeLocator( + x[:-3] / 10, + y_convex_inc[:-3] / 10, + curve_nature="convex", + curve_direction="increasing", + ) + assert kn.knee == 0.4 + + +def test_convex_decreasing_truncated(): + """Tests that a correct knee point is detected in + curve having truncated convex decreasing nature""" + kn = KneeLocator( + x[:-3] / 10, + y_convex_dec[:-3] / 10, + curve_nature="convex", + curve_direction="decreasing", + ) + assert kn.knee == 0.2 diff --git a/tests/test_utils/test_nan_warnings.py b/tests/test_utils/test_nan_warnings.py index e47e89dd3..1b6714911 100644 --- a/tests/test_utils/test_nan_warnings.py +++ b/tests/test_utils/test_nan_warnings.py @@ -4,16 +4,17 @@ import pytest from yellowbrick.exceptions import DataWarning -from yellowbrick.utils.nan_warnings import count_nan_elements, \ - count_rows_with_nans, warn_if_nans_exist, filter_missing +from yellowbrick.utils.nan_warnings import ( + count_nan_elements, + count_rows_with_nans, + warn_if_nans_exist, + filter_missing, +) def test_raise_warning_if_nans_exist(): """Test that a warning is raised if any nans are in the data.""" - data = np.array([ - [1, 2, 3], - [1, 2, np.nan], - ]) + data = np.array([[1, 2, 3], [1, 2, np.nan]]) with pytest.warns(DataWarning): warn_if_nans_exist(data) @@ -21,23 +22,13 @@ def test_raise_warning_if_nans_exist(): def test_count_rows_in_2d_arrays_with_nans(): """Test that nan-containinr rows in 2d arrays are counted correctly.""" - data_1_row = np.array([ - [1, 2, 3], - ]) - - data_2_rows = np.array([ - [1, 2, 3], - [1, 2, 3], - [np.nan, 2, 3], - [1, np.nan, 3], - ]) - - data_3_rows = np.array([ - [1, 2, 3], - [np.nan, 2, 3], - [1, np.nan, 3], - [np.nan, np.nan, np.nan], - ]) + data_1_row = np.array([[1, 2, 3]]) + + data_2_rows = np.array([[1, 2, 3], [1, 2, 3], [np.nan, 2, 3], [1, np.nan, 3]]) + + data_3_rows = np.array( + [[1, 2, 3], [np.nan, 2, 3], [1, np.nan, 3], [np.nan, np.nan, np.nan]] + ) assert count_rows_with_nans(data_1_row) == 0 assert count_rows_with_nans(data_2_rows) == 2 @@ -57,11 +48,7 @@ def test_count_nan_elements(): def test_clean_data_X_only_no_nans(): """Test that an array with no nulls is returned intact.""" - X = np.array([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - ]) + X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) observed = filter_missing(X) np.testing.assert_array_equal(X, observed) @@ -69,15 +56,9 @@ def test_clean_data_X_only_no_nans(): def test_clean_data_X_only(): """Test that nan-containing X rows are removed without y.""" - X = np.array([ - [1, 2, np.nan], - [4, 5, 6], - [np.nan, np.nan, np.nan], - ]) - - expected = np.array([ - [4, 5, 6] - ]) + X = np.array([[1, 2, np.nan], [4, 5, 6], [np.nan, np.nan, np.nan]]) + + expected = np.array([[4, 5, 6]]) observed = filter_missing(X) np.testing.assert_array_equal(expected, observed) @@ -85,17 +66,10 @@ def test_clean_data_X_only(): def test_clean_data_dirty_X_dirty_y(): """Test that nan-containing X, y rows are removed when both contain nans.""" - X = np.array([ - [1, 2, 3], - [4, 5, 6], - [7, 8, np.nan], - [np.nan, np.nan, np.nan], - ]) + X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, np.nan], [np.nan, np.nan, np.nan]]) y = np.array([33, np.nan, 44, np.nan]) - expected_X = np.array([ - [1, 2, 3], - ]) + expected_X = np.array([[1, 2, 3]]) expected_y = np.array([33]) observed_X, observed_y = filter_missing(X, y) @@ -105,18 +79,10 @@ def test_clean_data_dirty_X_dirty_y(): def test_clean_data_dirty_X_clean_y(): """Test that nan-containing X, y rows are removed when X contains nans.""" - X = np.array([ - [1, 2, 3], - [4, 5, 6], - [7, 8, np.nan], - [np.nan, np.nan, np.nan], - ]) + X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, np.nan], [np.nan, np.nan, np.nan]]) y = np.array([33, 44, 55, 66]) - expected_X = np.array([ - [1, 2, 3], - [4, 5, 6], - ]) + expected_X = np.array([[1, 2, 3], [4, 5, 6]]) expected_y = np.array([33, 44]) observed_X, observed_y = filter_missing(X, y) @@ -126,18 +92,10 @@ def test_clean_data_dirty_X_clean_y(): def test_clean_data_clean_X_dirty_y(): """Test that nan-containing X, y rows are removed when y contains nans.""" - X = np.array([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10, 11, 12] - ]) + X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]) y = np.array([np.nan, 44, np.nan, 66]) - expected_X = np.array([ - [4, 5, 6], - [10, 11, 12] - ]) + expected_X = np.array([[4, 5, 6], [10, 11, 12]]) expected_y = np.array([44, 66]) observed_X, observed_y = filter_missing(X, y) diff --git a/tests/test_utils/test_target.py b/tests/test_utils/test_target.py new file mode 100644 index 000000000..cad27f0da --- /dev/null +++ b/tests/test_utils/test_target.py @@ -0,0 +1,135 @@ +# tests.test_utils.test_target +# Tests for the target helper functions module. +# +# Author: Benjamin Bengfort +# Created: Thu Dec 27 20:43:31 2018 -0500 +# +# For license information, see LICENSE.txt +# +# ID: test_target.py [899c88a] benjamin@bengfort.com $ + +""" +Tests for the target helper functions module. +""" + +########################################################################## +## Imports +########################################################################## + +import pytest +import numpy as np + +from yellowbrick.utils.target import * +from yellowbrick.exceptions import YellowbrickValueError +from sklearn.datasets import make_regression, make_classification + + +########################################################################## +## Target Color Type Tests +########################################################################## + + +@pytest.mark.parametrize( + "value,expected", + [ + (None, TargetType.SINGLE), + (np.ones(15), TargetType.SINGLE), + (["a", "b", "a", "b", "c"], TargetType.DISCRETE), + ([1, 2, 1, 2, 3], TargetType.DISCRETE), + ([0.23, 0.94, 1.3, -1.02, 0.11], TargetType.CONTINUOUS), + ([1, 2, 0.2, 0.5, 1], TargetType.CONTINUOUS), + (np.array([0.2, 2.2, 1.2, -3.1]), TargetType.CONTINUOUS), + (np.array([[1, 2], [0, 2], [2, 1]]), TargetType.DISCRETE), + (np.array([[[1, 2], [1, 2]], [[1, 2], [1, 2]]]), TargetType.UNKNOWN), + ], + ids=[ + "none", + "ones", + "list str", + "list int", + "list float", + "mixed list", + "float array", + "multioutput", + "cube", + ], +) +def test_target_color_type(value, expected): + """ + Test the target_color_type helper function with a variety of data types + """ + assert target_color_type(value) == expected + + +@pytest.mark.parametrize( + "n_classes,expected", + [ + (2, TargetType.DISCRETE), + (4, TargetType.DISCRETE), + (MAX_DISCRETE_CLASSES, TargetType.DISCRETE), + (MAX_DISCRETE_CLASSES + 3, TargetType.CONTINUOUS), + ], + ids=["binary", "multiclass", "max discrete", "too many discrete"], +) +def test_binary_target_color_type(n_classes, expected): + """ + Test classification target color type + """ + _, y = make_classification(n_classes=n_classes, n_informative=n_classes + 2) + assert target_color_type(y) == expected + + +def test_regression_target_color_type(): + """ + Test regression target color type + """ + _, y = make_regression() + assert target_color_type(y) == TargetType.CONTINUOUS + + +@pytest.mark.parametrize( + "val", + [ + "auto", + "single", + "discrete", + "continuous", + "unknown", + TargetType.AUTO, + TargetType.SINGLE, + TargetType.DISCRETE, + TargetType.CONTINUOUS, + TargetType.UNKNOWN, + ], +) +def test_target_type_validate_valid(val): + try: + TargetType.validate(val) + except YellowbrickValueError: + pyetst.fail("valid target type raised validation error") + + +@pytest.mark.parametrize( + "val", ["foo", 1, 3.14, "s", "DISCRETE", "CONTINUOUS", ["a", "b", "c"]] +) +def test_target_type_validate_invalid(val): + with pytest.raises(YellowbrickValueError, match="unknown target color type"): + TargetType.validate(val) + + +@pytest.mark.parametrize( + "val,expected", + [ + ("discrete", True), + (TargetType.DISCRETE, True), + ("DISCRETE", True), + (8, False), + ("FOO", False), + (3.14, False), + ("foo", False), + (["discrete"], False), + ({"discrete"}, False), + ], +) +def test_target_type_equals(val, expected): + assert (TargetType.DISCRETE == val) is expected diff --git a/tests/test_utils/test_timer.py b/tests/test_utils/test_timer.py index 31763b84c..eaa2f766c 100644 --- a/tests/test_utils/test_timer.py +++ b/tests/test_utils/test_timer.py @@ -4,7 +4,7 @@ # Author: ZJ Poh # Created: Tue Jul 17 21:11:11 2018 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt """ Tests for the stand alone timer functions in Yellowbrick utils. @@ -15,23 +15,22 @@ ########################################################################## import pytest -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock from yellowbrick.utils.timer import * + ########################################################################## ## Helper Function Tests ########################################################################## + class TestTimer(object): """ Timer functions and utilities """ - @mock.patch('time.time', mock.Mock(side_effect=[1234.2, 1242.8])) + @mock.patch("time.time", mock.Mock(side_effect=[1234.2, 1242.8])) def test_timer(self): with Timer() as timer: pass @@ -39,11 +38,14 @@ def test_timer(self): assert timer.interval == pytest.approx(8.6) -@pytest.mark.parametrize('s,expected', [ - (1.01, '00:00:01.0100'), - (61.01, '00:01:01.0100'), - (3661.01, '01:01:01.0100'), - (360061.01, '100:01:01.0100') -]) +@pytest.mark.parametrize( + "s,expected", + [ + (1.01, "00:00:01.0100"), + (61.01, "00:01:01.0100"), + (3661.01, "01:01:01.0100"), + (360061.01, "100:01:01.0100"), + ], +) def test_human_readable_time(s, expected): assert human_readable_time(s) == expected diff --git a/tests/test_utils/test_types.py b/tests/test_utils/test_types.py index 17d5ff033..8ef0e5c15 100644 --- a/tests/test_utils/test_types.py +++ b/tests/test_utils/test_types.py @@ -41,8 +41,15 @@ from sklearn.linear_model import Ridge, RidgeCV, Lasso, LassoCV REGRESSORS = [ - SVR, DecisionTreeRegressor, MLPRegressor, LinearRegression, - RandomForestRegressor, Ridge, RidgeCV, Lasso, LassoCV, + SVR, + DecisionTreeRegressor, + MLPRegressor, + LinearRegression, + RandomForestRegressor, + Ridge, + RidgeCV, + Lasso, + LassoCV, ] # Import Classifiers @@ -55,8 +62,13 @@ from sklearn.naive_bayes import MultinomialNB, GaussianNB CLASSIFIERS = [ - SVC, DecisionTreeClassifier, MLPClassifier, LogisticRegression, - RandomForestClassifier, GradientBoostingClassifier, MultinomialNB, + SVC, + DecisionTreeClassifier, + MLPClassifier, + LogisticRegression, + RandomForestClassifier, + GradientBoostingClassifier, + MultinomialNB, GaussianNB, ] @@ -64,17 +76,13 @@ from sklearn.cluster import KMeans, MiniBatchKMeans from sklearn.cluster import AffinityPropagation, Birch -CLUSTERERS = [ - KMeans, MiniBatchKMeans, AffinityPropagation, Birch, -] +CLUSTERERS = [KMeans, MiniBatchKMeans, AffinityPropagation, Birch] # Import Decompositions from sklearn.decomposition import PCA from sklearn.decomposition import TruncatedSVD -DECOMPOSITIONS = [ - PCA, TruncatedSVD -] +DECOMPOSITIONS = [PCA, TruncatedSVD] # Import Transformers from sklearn.feature_extraction.text import TfidfVectorizer @@ -84,7 +92,10 @@ from sklearn.impute import SimpleImputer TRANSFORMERS = [ - DictVectorizer, QuantileTransformer, StandardScaler, SimpleImputer, + DictVectorizer, + QuantileTransformer, + StandardScaler, + SimpleImputer, TfidfVectorizer, ] @@ -92,16 +103,12 @@ from sklearn.pipeline import Pipeline, FeatureUnion -PIPELINES = [ - Pipeline, FeatureUnion, -] +PIPELINES = [Pipeline, FeatureUnion] # Import GridSearch Utilities from sklearn.model_selection import GridSearchCV, RandomizedSearchCV -SEARCH = [ - GridSearchCV, RandomizedSearchCV, -] +SEARCH = [GridSearchCV, RandomizedSearchCV] # Other Groups @@ -120,6 +127,7 @@ def obj_name(obj): ## Model type checking test cases ########################################################################## + class TestModelTypeChecking(object): """ Test model type checking utilities @@ -146,9 +154,9 @@ def test_is_estimator(self, model): obj = model() assert is_estimator(obj) - @pytest.mark.parametrize("cls", [ - list, dict, tuple, set, str, bool, int, float - ], ids=obj_name) + @pytest.mark.parametrize( + "cls", [list, dict, tuple, set, str, bool, int, float], ids=obj_name + ) def test_not_is_estimator(self, cls): """ Assert Python objects are not estimators @@ -166,10 +174,7 @@ def test_is_estimator_pipeline(self): assert is_estimator(Pipeline) assert is_estimator(FeatureUnion) - model = Pipeline([ - ('reduce_dim', PCA()), - ('linreg', LinearRegression()) - ]) + model = Pipeline([("reduce_dim", PCA()), ("linreg", LinearRegression())]) assert is_estimator(model) @@ -180,14 +185,18 @@ def test_is_estimator_search(self): assert is_estimator(GridSearchCV) assert is_estimator(RandomizedSearchCV) - model = GridSearchCV(SVR(), {'kernel': ['linear', 'rbf']}) + model = GridSearchCV(SVR(), {"kernel": ["linear", "rbf"]}) assert is_estimator(model) - @pytest.mark.parametrize("viz,params", [ - (Visualizer, {}), - (ScoreVisualizer, {'model': LinearRegression()}), - (ModelVisualizer, {'model': LogisticRegression()}) - ], ids=["Visualizer", "ScoreVisualizer", "ModelVisualizer"]) + @pytest.mark.parametrize( + "viz,params", + [ + (Visualizer, {}), + (ScoreVisualizer, {"model": LinearRegression()}), + (ModelVisualizer, {"model": LogisticRegression()}), + ], + ids=["Visualizer", "ScoreVisualizer", "ModelVisualizer"], + ) def test_is_estimator_visualizer(self, viz, params): """ Test that is_estimator works for Visualizers @@ -219,9 +228,9 @@ def test_is_regressor(self, model): obj = model() assert is_regressor(obj) - @pytest.mark.parametrize("model", - CLASSIFIERS+CLUSTERERS+TRANSFORMERS+DECOMPOSITIONS, - ids=obj_name) + @pytest.mark.parametrize( + "model", CLASSIFIERS + CLUSTERERS + TRANSFORMERS + DECOMPOSITIONS, ids=obj_name + ) def test_not_is_regressor(self, model): """ Test that is_regressor does not match non-regressor estimators @@ -239,10 +248,7 @@ def test_is_regressor_pipeline(self): assert not is_regressor(Pipeline) assert not is_regressor(FeatureUnion) - model = Pipeline([ - ('reduce_dim', PCA()), - ('linreg', LinearRegression()) - ]) + model = Pipeline([("reduce_dim", PCA()), ("linreg", LinearRegression())]) assert is_regressor(model) @@ -254,13 +260,17 @@ def test_is_regressor_search(self): assert is_regressor(GridSearchCV) assert is_regressor(RandomizedSearchCV) - model = GridSearchCV(SVR(), {'kernel': ['linear', 'rbf']}) + model = GridSearchCV(SVR(), {"kernel": ["linear", "rbf"]}) assert is_regressor(model) - @pytest.mark.parametrize("viz,params", [ - (ScoreVisualizer, {'model': LinearRegression()}), - (ModelVisualizer, {'model': Ridge()}) - ], ids=["ScoreVisualizer", "ModelVisualizer"]) + @pytest.mark.parametrize( + "viz,params", + [ + (ScoreVisualizer, {"model": LinearRegression()}), + (ModelVisualizer, {"model": Ridge()}), + ], + ids=["ScoreVisualizer", "ModelVisualizer"], + ) def test_is_regressor_visualizer(self, viz, params): """ Test that is_regressor works on visualizers @@ -292,9 +302,9 @@ def test_is_classifier(self, model): obj = model() assert is_classifier(obj) - @pytest.mark.parametrize("model", - REGRESSORS+CLUSTERERS+TRANSFORMERS+DECOMPOSITIONS, - ids=obj_name) + @pytest.mark.parametrize( + "model", REGRESSORS + CLUSTERERS + TRANSFORMERS + DECOMPOSITIONS, ids=obj_name + ) def test_not_is_classifier(self, model): """ Test that is_classifier does not match non-classifier estimators @@ -312,10 +322,7 @@ def test_classifier_pipeline(self): assert not is_classifier(Pipeline) assert not is_classifier(FeatureUnion) - model = Pipeline([ - ('reduce_dim', PCA()), - ('linreg', LogisticRegression()) - ]) + model = Pipeline([("reduce_dim", PCA()), ("linreg", LogisticRegression())]) assert is_classifier(model) @@ -327,13 +334,17 @@ def test_is_classifier_search(self): assert is_classifier(GridSearchCV) assert is_classifier(RandomizedSearchCV) - model = GridSearchCV(SVC(), {'kernel': ['linear', 'rbf']}) + model = GridSearchCV(SVC(), {"kernel": ["linear", "rbf"]}) assert is_classifier(model) - @pytest.mark.parametrize("viz,params", [ - (ScoreVisualizer, {'model': MultinomialNB()}), - (ModelVisualizer, {'model': MLPClassifier()}) - ], ids=["ScoreVisualizer", "ModelVisualizer"]) + @pytest.mark.parametrize( + "viz,params", + [ + (ScoreVisualizer, {"model": MultinomialNB()}), + (ModelVisualizer, {"model": MLPClassifier()}), + ], + ids=["ScoreVisualizer", "ModelVisualizer"], + ) def test_is_classifier_visualizer(self, viz, params): """ Test that is_classifier works on visualizers @@ -365,9 +376,9 @@ def test_is_clusterer(self, model): obj = model() assert is_clusterer(obj) - @pytest.mark.parametrize("model", - REGRESSORS+CLASSIFIERS+TRANSFORMERS+DECOMPOSITIONS, - ids=obj_name) + @pytest.mark.parametrize( + "model", REGRESSORS + CLASSIFIERS + TRANSFORMERS + DECOMPOSITIONS, ids=obj_name + ) def test_not_is_clusterer(self, model): """ Test that is_clusterer does not match non-clusterer estimators @@ -385,16 +396,13 @@ def test_clusterer_pipeline(self): assert not is_clusterer(Pipeline) assert not is_clusterer(FeatureUnion) - model = Pipeline([ - ('reduce_dim', PCA()), - ('kmeans', KMeans()) - ]) + model = Pipeline([("reduce_dim", PCA()), ("kmeans", KMeans())]) assert is_clusterer(model) - @pytest.mark.parametrize("viz,params", [ - (ModelVisualizer, {'model': KMeans()}) - ], ids=["ModelVisualizer"]) + @pytest.mark.parametrize( + "viz,params", [(ModelVisualizer, {"model": KMeans()})], ids=["ModelVisualizer"] + ) def test_is_clusterer_visualizer(self, viz, params): """ Test that is_clusterer works on visualizers @@ -426,8 +434,9 @@ def test_is_gridsearch(self, model): obj = model(SVC, {"C": [0.5, 1, 10]}) assert is_gridsearch(obj) - @pytest.mark.parametrize("model", - [MLPRegressor, MLPClassifier, SimpleImputer], ids=obj_name) + @pytest.mark.parametrize( + "model", [MLPRegressor, MLPClassifier, SimpleImputer], ids=obj_name + ) def test_not_is_gridsearch(self, model): """ Test that is_gridsearch does not match non grid searches @@ -448,10 +457,19 @@ def test_probabilistic_alias(self): """ assert isprobabilistic is is_probabilistic - @pytest.mark.parametrize("model", [ - MultinomialNB, GaussianNB, LogisticRegression, SVC, - RandomForestClassifier, GradientBoostingClassifier, MLPClassifier, - ], ids=obj_name) + @pytest.mark.parametrize( + "model", + [ + MultinomialNB, + GaussianNB, + LogisticRegression, + SVC, + RandomForestClassifier, + GradientBoostingClassifier, + MLPClassifier, + ], + ids=obj_name, + ) def test_is_probabilistic(self, model): """ Test that is_probabilistic works correctly @@ -462,10 +480,11 @@ def test_is_probabilistic(self, model): obj = model() assert is_probabilistic(obj) - @pytest.mark.parametrize("model", [ - MLPRegressor, SimpleImputer, StandardScaler, KMeans, - RandomForestRegressor, - ], ids=obj_name) + @pytest.mark.parametrize( + "model", + [MLPRegressor, SimpleImputer, StandardScaler, KMeans, RandomForestRegressor], + ids=obj_name, + ) def test_not_is_probabilistic(self, model): """ Test that is_probabilistic does not match non probablistic estimators @@ -481,6 +500,7 @@ def test_not_is_probabilistic(self, model): ## Data type checking test cases ########################################################################## + class TestDataTypeChecking(object): """ Test data type checking utilities @@ -501,22 +521,24 @@ def test_is_dataframe(self): """ Test that is_dataframe works correctly """ - df = pd.DataFrame([ - {'a': 1, 'b': 2.3, 'c': 'Hello'}, - {'a': 2, 'b': 3.14, 'c': 'World'}, - ]) + df = pd.DataFrame( + [{"a": 1, "b": 2.3, "c": "Hello"}, {"a": 2, "b": 3.14, "c": "World"}] + ) assert is_dataframe(df) - @pytest.mark.parametrize("obj", [ - np.array([ - (1,2.,'Hello'), (2,3.,"World")], - dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'S10')] - ), - np.array([[1,2,3], [1,2,3]]), - [[1,2,3], [1,2,3]], - ], - ids=["structured array", "array", "list"]) + @pytest.mark.parametrize( + "obj", + [ + np.array( + [(1, 2.0, "Hello"), (2, 3.0, "World")], + dtype=[("foo", "i4"), ("bar", "f4"), ("baz", "S10")], + ), + np.array([[1, 2, 3], [1, 2, 3]]), + [[1, 2, 3], [1, 2, 3]], + ], + ids=["structured array", "array", "list"], + ) def test_not_is_dataframe(self, obj): """ Test that is_dataframe does not match non-dataframes @@ -542,15 +564,18 @@ def test_is_series(self): assert is_series(df) - @pytest.mark.parametrize("obj", [ - np.array([ - (1,2.,'Hello'), (2,3.,"World")], - dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'S10')] - ), - np.array([1,2,3]), - [1, 2, 3], - ], - ids=["structured array", "array", "list"]) + @pytest.mark.parametrize( + "obj", + [ + np.array( + [(1, 2.0, "Hello"), (2, 3.0, "World")], + dtype=[("foo", "i4"), ("bar", "f4"), ("baz", "S10")], + ), + np.array([1, 2, 3]), + [1, 2, 3], + ], + ids=["structured array", "array", "list"], + ) def test_not_is_series(self, obj): """ Test that is_series does not match non-dataframes @@ -571,18 +596,16 @@ def test_is_structured_array(self): """ Test that is_structured_array works correctly """ - x = np.array([ - (1,2.,'Hello'), (2,3.,"World")], - dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'S10')] + x = np.array( + [(1, 2.0, "Hello"), (2, 3.0, "World")], + dtype=[("foo", "i4"), ("bar", "f4"), ("baz", "S10")], ) assert is_structured_array(x) - @pytest.mark.parametrize("obj", [ - np.array([[1,2,3], [1,2,3]]), - [[1,2,3], [1,2,3]], - ], - ids=obj_name) + @pytest.mark.parametrize( + "obj", [np.array([[1, 2, 3], [1, 2, 3]]), [[1, 2, 3], [1, 2, 3]]], ids=obj_name + ) def test_not_is_structured_array(self, obj): """ Test that is_structured_array does not match non-structured-arrays diff --git a/tests/test_utils/test_wrapper.py b/tests/test_utils/test_wrapper.py index d2cab8848..83384fc7b 100644 --- a/tests/test_utils/test_wrapper.py +++ b/tests/test_utils/test_wrapper.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Mon May 22 09:25:52 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: test_wrapper.py [b2ecd50] benjamin@bengfort.com $ @@ -17,23 +17,20 @@ ## Imports ########################################################################## +from unittest import mock + from yellowbrick.base import Visualizer from yellowbrick.utils.wrapper import * from sklearn.naive_bayes import MultinomialNB from sklearn.naive_bayes import GaussianNB -try: - from unittest import mock -except ImportError: - import mock - ########################################################################## ## Fixture ########################################################################## -class MockVisualizer(Visualizer): +class MockVisualizer(Visualizer): def __init__(self, ax=None, **kwargs): self.ax = ax self.fit = mock.MagicMock() @@ -51,7 +48,6 @@ def ax(self, val): class WrappedEstimator(MockVisualizer, Wrapper): - def __init__(self, **kwargs): self.estimator = mock.MagicMock(spec=MultinomialNB()) @@ -62,13 +58,14 @@ def draw(self): return True def foo(self, a, b): - return a+b + return a + b ########################################################################## ## Wrapper Test Case ########################################################################## + class TestWrapper(object): """ Test the object Wrapper mixin utility @@ -82,7 +79,7 @@ def test_wrapper_methods(self): # Assert that all the wrapper methods are called assert obj.draw() - assert obj.foo(2,2) == 4 + assert obj.foo(2, 2) == 4 assert obj.estimator is not None def test_super_methods(self): diff --git a/yellowbrick/__init__.py b/yellowbrick/__init__.py index c2d491371..964afb995 100644 --- a/yellowbrick/__init__.py +++ b/yellowbrick/__init__.py @@ -1,10 +1,11 @@ # yellowbrick # A suite of visual analysis and diagnostic tools for machine learning. # -# Author: Rebecca Bilbro +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Wed May 18 10:46:33 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [0c5ba04] benjamin@bengfort.com $ @@ -20,6 +21,7 @@ # Capture the original matplotlib rcParams import matplotlib as mpl + _orig_rc_params = mpl.rcParams.copy() # Import the version number at the top level @@ -35,6 +37,7 @@ from .anscombe import anscombe from .datasaurus import datasaurus from .classifier import ROCAUC, ClassBalance, ClassificationScoreVisualizer + # from .classifier import crplot, rocplot # from .regressor import peplot, residuals_plot @@ -43,7 +46,7 @@ ## Set default aesthetics ########################################################################## -set_aesthetic() # NOTE: modifies mpl.rcParams +set_aesthetic() # NOTE: modifies mpl.rcParams ########################################################################## diff --git a/yellowbrick/anscombe.py b/yellowbrick/anscombe.py index 3406aeba7..1d72d5bf4 100644 --- a/yellowbrick/anscombe.py +++ b/yellowbrick/anscombe.py @@ -1,10 +1,10 @@ # yellowbrick.anscombe # Plots Anscombe's Quartet as an illustration of the importance of visualization. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Wed May 18 11:38:25 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The sckit-yb developers # For license information, see LICENSE.txt # # ID: anscombe.py [0bfa366] benjamin@bengfort.com $ @@ -29,22 +29,30 @@ ########################################################################## ANSCOMBE = [ - np.array([ - [10.0, 8.0, 13.0, 9.0, 11.0, 14.0, 6.0, 4.0, 12.0, 7.0, 5.0], - [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68] - ]), - np.array([ - [10.0, 8.0, 13.0, 9.0, 11.0, 14.0, 6.0, 4.0, 12.0, 7.0, 5.0], - [9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74] - ]), - np.array([ - [10.0, 8.0, 13.0, 9.0, 11.0, 14.0, 6.0, 4.0, 12.0, 7.0, 5.0], - [7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73] - ]), - np.array([ - [8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 19.0, 8.0, 8.0, 8.0], - [6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89] - ]) + np.array( + [ + [10.0, 8.0, 13.0, 9.0, 11.0, 14.0, 6.0, 4.0, 12.0, 7.0, 5.0], + [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68], + ] + ), + np.array( + [ + [10.0, 8.0, 13.0, 9.0, 11.0, 14.0, 6.0, 4.0, 12.0, 7.0, 5.0], + [9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74], + ] + ), + np.array( + [ + [10.0, 8.0, 13.0, 9.0, 11.0, 14.0, 6.0, 4.0, 12.0, 7.0, 5.0], + [7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73], + ] + ), + np.array( + [ + [8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 19.0, 8.0, 8.0, 8.0], + [6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89], + ] + ), ] @@ -52,7 +60,7 @@ def anscombe(): """ Creates 2x2 grid plot of the 4 anscombe datasets for illustration. """ - _, ((axa, axb), (axc, axd)) = plt.subplots(2, 2, sharex='col', sharey='row') + _, ((axa, axb), (axc, axd)) = plt.subplots(2, 2, sharex="col", sharey="row") colors = get_color_cycle() for arr, ax, color in zip(ANSCOMBE, (axa, axb, axc, axd), colors): @@ -72,6 +80,6 @@ def anscombe(): return (axa, axb, axc, axd) -if __name__ == '__main__': +if __name__ == "__main__": anscombe() plt.show() diff --git a/yellowbrick/base.py b/yellowbrick/base.py index 4652b1d47..c329b86db 100644 --- a/yellowbrick/base.py +++ b/yellowbrick/base.py @@ -1,11 +1,11 @@ # yellowbrick.base # Abstract base classes and interface for Yellowbrick. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Fri Jun 03 10:20:59 2016 -0700 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2019 The scikit-yb developers # For license information, see LICENSE.txt # # ID: base.py [4a59c49] benjamin@bengfort.com $ @@ -14,19 +14,24 @@ Abstract base classes and interface for Yellowbrick. """ -import matplotlib.pyplot as plt import math +import warnings +import matplotlib.pyplot as plt -from .utils.wrapper import Wrapper from sklearn.base import BaseEstimator -from .utils import get_model_name, isestimator -from sklearn.model_selection import cross_val_predict as cvp -from .exceptions import YellowbrickValueError, YellowbrickTypeError + +from yellowbrick.utils import get_model_name +from yellowbrick.utils.wrapper import Wrapper +from yellowbrick.utils.helpers import check_fitted +from yellowbrick.exceptions import YellowbrickWarning +from yellowbrick.exceptions import YellowbrickValueError, YellowbrickTypeError + ########################################################################## ## Base class hierarchy ########################################################################## + class Visualizer(BaseEstimator): """ The root of the visual object hierarchy that defines how yellowbrick @@ -43,6 +48,10 @@ class Visualizer(BaseEstimator): The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. Optional keyword @@ -60,26 +69,28 @@ class Visualizer(BaseEstimator): ----- Visualizers are objects that learn from data (e.g. estimators), therefore they must be ``fit()`` before they can be drawn or used. Visualizers also - maintain a reference to an ``ax`` object, a matplotlib Axes where the - figures are drawn and rendered. + maintain a reference to an ``ax`` object, a Matplotlib Axes where the + figures are drawn and rendered, as well as to a ``fig`` object, a Matplotlib + Figure on which the Visualizer will be plotted. """ - def __init__(self, ax=None, **kwargs): + def __init__(self, ax=None, fig=None, **kwargs): self.ax = ax - self.size = kwargs.pop('size', None) - self.color = kwargs.pop('color', None) - self.title = kwargs.pop('title', None) + self.fig = fig + self.size = kwargs.pop("size", None) + self.color = kwargs.pop("color", None) + self.title = kwargs.pop("title", None) - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// ## Primary Visualizer Properties - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// @property def ax(self): """ The matplotlib axes that the visualizer draws upon (can also be a grid - of multiple axes objects). The visualizer automatically creates an - axes for the user if one has not been specified. + of multiple axes objects). The visualizer uses :func:`matplotlib.pyplot.gca` + to create an axes for the user if one has not been specified. """ if not hasattr(self, "_ax") or self._ax is None: self._ax = plt.gca() @@ -89,6 +100,21 @@ def ax(self): def ax(self, ax): self._ax = ax + @property + def fig(self): + """ + The matplotlib fig that the visualizer draws upon. The visualizer uses + the matplotlib method :func:`matplotlib.pyplot.gcf` to create a figure for + the user if one has not been specified. + """ + if not hasattr(self, "_fig") or self._fig is None: + self._fig = plt.gcf() + return self._fig + + @fig.setter + def fig(self, fig): + self._fig = fig + @property def size(self): """ @@ -96,23 +122,21 @@ def size(self): the user provided size if available. """ if not hasattr(self, "_size") or self._size is None: - fig = plt.gcf() - self._size = fig.get_size_inches()*fig.dpi + self._size = self.fig.get_size_inches() * self.fig.dpi return self._size @size.setter def size(self, size): self._size = size if self._size is not None: - fig = plt.gcf() width, height = size - width_in_inches = width / fig.get_dpi() - height_in_inches = height / fig.get_dpi() - fig.set_size_inches(width_in_inches, height_in_inches) + width_in_inches = width / self.fig.get_dpi() + height_in_inches = height / self.fig.get_dpi() + self.fig.set_size_inches(width_in_inches, height_in_inches) - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// ## Estimator interface - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// def fit(self, X, y=None, **kwargs): """ @@ -142,9 +166,9 @@ def fit(self, X, y=None, **kwargs): """ return self - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// ## Visualizer interface - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// def draw(self, **kwargs): """ @@ -160,9 +184,7 @@ def draw(self, **kwargs): generic keyword arguments. """ - raise NotImplementedError( - "Visualizers must implement a drawing interface." - ) + raise NotImplementedError("Visualizers must implement a drawing interface.") def finalize(self, **kwargs): """ @@ -206,7 +228,14 @@ def poof(self, outpath=None, clear_figure=False, **kwargs): primarily called by the user to render the visualization. """ # Ensure that draw has been called - if self._ax is None: return + if self._ax is None: + warn_message = ( + "{} does not have a reference to a matplotlib.Axes " + "the figure may not render as expected!" + ) + warnings.warn( + warn_message.format(self.__class__.__name__), YellowbrickWarning + ) # Finalize the figure self.finalize() @@ -217,11 +246,14 @@ def poof(self, outpath=None, clear_figure=False, **kwargs): plt.show() if clear_figure: - plt.gcf().clear() + self.fig.clear() + + # Return ax to ensure display in notebooks + return self.ax - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// ## Helper Functions - ##//////////////////////////////////////////////////////////////////// + ## //////////////////////////////////////////////////////////////////// def set_title(self, title=None): """ @@ -241,6 +273,7 @@ def set_title(self, title=None): ## Model Visualizers ########################################################################## + class ModelVisualizer(Visualizer, Wrapper): """ The ModelVisualizer class wraps a Scikit-Learn estimator (usually a @@ -251,14 +284,26 @@ class ModelVisualizer(Visualizer, Wrapper): Parameters ---------- - model : Estimator + model : a Scikit-Learn estimator A Scikit-Learn estimator to wrap functionality for, usually regressor, - classifier, or clusterer predictive model. + classifier, or clusterer predictive model. If the estimator is not fitted, + it is fit when the visualizer is fitted, unless otherwise specified by + ``is_fitted``. ax : matplotlib Axes, default: None The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined by other Visualizers. @@ -268,21 +313,14 @@ class ModelVisualizer(Visualizer, Wrapper): Model visualizers can wrap either fitted or unfitted models. """ - def __init__(self, model, ax=None, **kwargs): - """ - Parameters - ---------- - ax: matplotlib axes - the axis to plot the figure on. - - kwargs: dict - keyword arguments for Scikit-Learn model - """ + def __init__(self, model, ax=None, fig=None, is_fitted="auto", **kwargs): self.estimator = model + self.is_fitted = is_fitted self.name = get_model_name(self.estimator) + # Initialize base classes independently Wrapper.__init__(self, self.estimator) - Visualizer.__init__(self, ax=ax, **kwargs) + Visualizer.__init__(self, ax=ax, fig=fig, **kwargs) def fit(self, X, y=None, **kwargs): """ @@ -300,7 +338,7 @@ def fit(self, X, y=None, **kwargs): kwargs: dict Keyword arguments passed to the drawing functionality or to the - Scikit-Learn API. See visualizer specific details for how to use + scikit-learn API. See visualizer specific details for how to use the kwargs to modify the visualization or fitting process. Returns @@ -308,7 +346,8 @@ def fit(self, X, y=None, **kwargs): self : visualizer The fit method must always return self to support pipelines. """ - self.estimator.fit(X, y) + if not check_fitted(self.estimator, is_fitted_by=self.is_fitted): + self.estimator.fit(X, y, **kwargs) return self @@ -316,6 +355,7 @@ def fit(self, X, y=None, **kwargs): ## Score Visualizers ########################################################################## + class ScoreVisualizer(ModelVisualizer): """ The ScoreVisualizer reports the performance of a Scikit-Learn estimator @@ -326,24 +366,30 @@ class ScoreVisualizer(ModelVisualizer): Parameters ---------- - model : Estimator + model : a Scikit-Learn estimator A Scikit-Learn estimator to wrap functionality for, usually regressor, - classifier, or clusterer predictive model. + classifier, or clusterer predictive model. If the estimator is not fitted, + it is fit when the visualizer is fitted, unless otherwise specified by + ``is_fitted``. ax : matplotlib Axes, default: None The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizersself. - Returns - ------- - score : float or array-like - Returns the score of the underlying model, which is model-speciifc, - e.g. accuracy for classifiers, R2 for regressors, etc. - Notes ----- Score visualizers can wrap either fitted or unfitted models. @@ -353,76 +399,20 @@ def score(self, X, y, **kwargs): """ The primary entry point for score visualizers is the score method, which makes predictions based on X and scores them relative to y. - """ - raise NotImplementedError( - "ScoreVisualizer subclasses should implement score" - ) + Returns + ------- + score : float or array-like + Returns the score of the underlying model, which is model-specific, + e.g. accuracy for classifiers, R2 for regressors, etc. + """ + raise NotImplementedError("ScoreVisualizer subclasses should implement score") ########################################################################## -## Multiple Models and Mixins +## Multiple Models ########################################################################## -class MultiModelMixin(object): - """ - Does predict for each of the models and generates subplots. - """ - - def __init__(self, models, ax=None, **kwargs): - # Ensure models is a collection, if it's a single estimator then we - # wrap it in a list so that the API doesn't break during render. - """ - These parameters can be influenced later on in the visualization - process, but can and should be set as early as possible. - - Parameters - ---------- - models: Scikit-Learn estimator - the Scikit-Learn models being compared with each other. - - kwargs: dict - keyword arguments. - """ - # TODO: How to handle the axes in this mixin? - self.ax = ax - - if all(isestimator, models): - models = [models] - - # Keep track of the models - self.models = models - self.names = kwargs.pop('names', list(map(get_model_name, models))) - - def generate_subplots(self): - """ - Generates the subplots for the number of given models. - """ - _, axes = plt.subplots(len(self.models), sharex=True, sharey=True) - return axes - - def predict(self, X, y): - """ - Returns a generator containing the predictions for each of the - internal models (using cross_val_predict and a CV=12). - - Parameters - ---------- - - X : ndarray or DataFrame of shape n x m - A matrix of n instances with m features - - y : ndarray or Series of length n - An array or series of target or class values - - kwargs: dict - keyword arguments passed to Scikit-Learn API. - - """ - for model in self.models: - yield cvp(model, X, y, cv=12) - - class VisualizerGrid(Visualizer): """ @@ -445,12 +435,13 @@ class VisualizerGrid(Visualizer): visualizers specified in the visualizers list. axarr: matplotlib.axarr, default: None. - If you want to put the plot onto an existing axarr, specify it here. Otherwise a new - one will be created. + If you want to put the plot onto an existing axarr, specify it here. Otherwise + a new one will be created. kwargs : additional keyword arguments, default: None - Any additional keyword arguments will be passed on to the fit() method and therefore - passed on to the fit() method of the wrapped estimators, if applicable. Otherwise ignored. + Any additional keyword arguments will be passed on to the fit() method and + therefore passed on to the fit() method of the wrapped estimators, if + applicable. Otherwise ignored. Examples -------- @@ -465,30 +456,34 @@ class VisualizerGrid(Visualizer): >>> mv.score(X_test, y_test) >>> mv.poof() """ - def __init__(self, visualizers = [], nrows = None, ncols = None, axarr = None, **kwargs): - #Class static params + + def __init__(self, visualizers=[], nrows=None, ncols=None, axarr=None, **kwargs): + # Class static params self.SUBPLOT_DEFAULT_PIXELS = 400 - #Allocate passed parameters + # Allocate passed parameters self._visualizers = visualizers plotcount = len(visualizers) - if nrows == None and ncols == None: - #TODO: enhancement would be to also allow a 2-d array of visualizers instead of just a 1-d left-to-right + top-to-bottom list + if nrows is None and ncols is None: + # TODO: enhancement would be to also allow a 2-d array of visualizers + # instead of just a 1-d left-to-right + top-to-bottom list self.ncols = 1 self.nrows = plotcount - elif ncols == None: + elif ncols is None: self.nrows = nrows self.ncols = int(math.ceil(plotcount / self.nrows)) - elif nrows == None: + elif nrows is None: self.ncols = ncols self.nrows = int(math.ceil(plotcount / self.ncols)) else: - raise YellowbrickValueError("You can only specify either nrows or ncols, \ - the other will be calculated based on the length of the list of visualizers.") - + raise YellowbrickValueError( + "You can only specify either nrows or ncols, \ + the other will be calculated based on the length of the list of \ + visualizers." + ) - if axarr == None: - fig, axarr = plt.subplots(self.nrows, self.ncols, squeeze = False) + if axarr is None: + fig, axarr = plt.subplots(self.nrows, self.ncols, squeeze=False) self.axarr = axarr @@ -497,10 +492,10 @@ def __init__(self, visualizers = [], nrows = None, ncols = None, axarr = None, * for col in range(self.ncols): try: self.visualizers[idx].ax = self.axarr[row, col] - #If len(visualizers) isn't evenly divisibly by rows/columns, - #we want to create the illusion of empty space by hiding the axis + # If len(visualizers) isn't evenly divisibly by rows/columns, + # we want to create the illusion of empty space by hiding the axis except IndexError: - self.axarr[row,col].axis('off') + self.axarr[row, col].axis("off") idx += 1 @@ -511,49 +506,53 @@ def visualizers(self): return self._visualizers @visualizers.setter - def visualizers(self,value): - raise AttributeError("Visualizers list can only be set during class instantiation.") + def visualizers(self, value): + raise AttributeError( + "Visualizers list can only be set during class instantiation." + ) @property def ax(self): - """ + """ Override Visualizer.ax to return the current axis """ - return plt.gca() + return plt.gca() @ax.setter def ax(self, ax): - raise YellowbrickTypeError("cannot set new axes objects on multiple visualizers") - + raise YellowbrickTypeError( + "cannot set new axes objects on multiple visualizers" + ) - def fit(self,X,y,**kwargs): + def fit(self, X, y, **kwargs): for vz in self.visualizers: - vz.fit(X,y,**kwargs) + vz.fit(X, y, **kwargs) return self - def score(self,X,y): + def score(self, X, y): for idx in range(len(self.visualizers)): - self.visualizers[idx].score(X,y) + self.visualizers[idx].score(X, y) return self def poof(self, outpath=None, clear_figure=False, **kwargs): - if self.axarr is None: return + if self.axarr is None: + return - #Finalize all visualizers + # Finalize all visualizers for idx in range(len(self.visualizers)): self.visualizers[idx].finalize() - #Choose a reasonable default size if the user has not manually specified one + # Choose a reasonable default size if the user has not manually specified one # self.size() uses pixels rather than matplotlib's default of inches if not hasattr(self, "_size") or self._size is None: self._width = self.SUBPLOT_DEFAULT_PIXELS * self.ncols self._height = self.SUBPLOT_DEFAULT_PIXELS * self.nrows - self.size = (self._width,self._height); + self.size = (self._width, self._height) if outpath is not None: plt.savefig(outpath, **kwargs) @@ -562,3 +561,6 @@ def poof(self, outpath=None, clear_figure=False, **kwargs): if clear_figure: plt.gcf().clear() + + # Return Axes array to ensure poof works in notebooks + return self.axarr diff --git a/yellowbrick/bestfit.py b/yellowbrick/bestfit.py index 32b401fa8..bac9e6fbe 100644 --- a/yellowbrick/bestfit.py +++ b/yellowbrick/bestfit.py @@ -1,10 +1,10 @@ # yellowbrick.bestfit # Uses Scikit-Learn to compute a best fit function, then draws it in the plot. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Sun Jun 26 17:27:08 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The sckit-yb developers # For license information, see LICENSE.txt # # ID: bestfit.py [56236f3] benjamin@bengfort.com $ @@ -35,18 +35,19 @@ ########################################################################## # Names of the various estimator functions -LINEAR = 'linear' -QUADRATIC = 'quadratic' -EXPONENTIAL = 'exponential' -LOG = 'log' -SELECT_BEST = 'select_best' +LINEAR = "linear" +QUADRATIC = "quadratic" +EXPONENTIAL = "exponential" +LOG = "log" +SELECT_BEST = "select_best" ########################################################################## ## Draw Line of Best Fit ########################################################################## -def draw_best_fit(X, y, ax, estimator='linear', **kwargs): + +def draw_best_fit(X, y, ax, estimator="linear", **kwargs): """ Uses Scikit-Learn to fit a model to X and y then uses the resulting model to predict the curve based on the X values. This curve is drawn to the ax @@ -95,11 +96,11 @@ def draw_best_fit(X, y, ax, estimator='linear', **kwargs): # Estimators are the types of best fit lines that can be drawn. estimators = { - LINEAR: fit_linear, # Uses OLS to fit the regression - QUADRATIC: fit_quadratic, # Uses OLS with Polynomial order 2 - EXPONENTIAL: fit_exponential, # Not implemented yet - LOG: fit_log, # Not implemented yet - SELECT_BEST: fit_select_best, # Selects the best fit via MSE + LINEAR: fit_linear, # Uses OLS to fit the regression + QUADRATIC: fit_quadratic, # Uses OLS with Polynomial order 2 + EXPONENTIAL: fit_exponential, # Not implemented yet + LOG: fit_log, # Not implemented yet + SELECT_BEST: fit_select_best, # Selects the best fit via MSE } # Check to make sure that a correct estimator value was passed in. @@ -115,10 +116,11 @@ def draw_best_fit(X, y, ax, estimator='linear', **kwargs): # Ensure that X and y are the same length if len(X) != len(y): - raise YellowbrickValueError(( - "X and y must have same length:" - " X len {} doesn't match y len {}!" - ).format(len(X), len(y))) + raise YellowbrickValueError( + ( + "X and y must have same length:" " X len {} doesn't match y len {}!" + ).format(len(X), len(y)) + ) # Ensure that X and y are np.arrays X = np.array(X) @@ -127,7 +129,7 @@ def draw_best_fit(X, y, ax, estimator='linear', **kwargs): # Verify that X is a two dimensional array for Scikit-Learn esitmators # and that its dimensions are (n, 1) where n is the number of rows. if X.ndim < 2: - X = X[:,np.newaxis] # Reshape X into the correct dimensions + X = X[:, np.newaxis] # Reshape X into the correct dimensions if X.ndim > 2: raise YellowbrickValueError( @@ -144,8 +146,8 @@ def draw_best_fit(X, y, ax, estimator='linear', **kwargs): model = estimator(X, y) # Set the color if not passed in. - if 'c' not in kwargs and 'color' not in kwargs: - kwargs['color'] = LINE_COLOR + if "c" not in kwargs and "color" not in kwargs: + kwargs["color"] = LINE_COLOR # Get the current working axes ax = ax or plt.gca() @@ -153,7 +155,7 @@ def draw_best_fit(X, y, ax, estimator='linear', **kwargs): # Plot line of best fit onto the axes that were passed in. # TODO: determine if xlim or X.min(), X.max() are better params xr = np.linspace(*ax.get_xlim(), num=100) - ax.plot(xr, model.predict(xr[:,np.newaxis]), **kwargs) + ax.plot(xr, model.predict(xr[:, np.newaxis]), **kwargs) return ax @@ -161,12 +163,13 @@ def draw_best_fit(X, y, ax, estimator='linear', **kwargs): ## Estimator Functions ########################################################################## + def fit_select_best(X, y): """ Selects the best fit of the estimators already implemented by choosing the model with the smallest mean square error metric for the trained values. """ - models = [fit(X,y) for fit in [fit_linear, fit_quadratic]] + models = [fit(X, y) for fit in [fit_linear, fit_quadratic]] errors = map(lambda model: mse(y, model.predict(X)), models) return min(zip(models, errors), key=itemgetter(1))[0] @@ -185,9 +188,7 @@ def fit_quadratic(X, y): """ Uses OLS with Polynomial order 2. """ - model = make_pipeline( - PolynomialFeatures(2), linear_model.LinearRegression() - ) + model = make_pipeline(PolynomialFeatures(2), linear_model.LinearRegression()) model.fit(X, y) return model @@ -210,6 +211,7 @@ def fit_log(X, y): ## Draw 45 Degree Line ########################################################################## + def draw_identity_line(ax=None, dynamic=True, **kwargs): """ Draws a 45 degree identity line such that y=x for all points within the @@ -249,15 +251,15 @@ def draw_identity_line(ax=None, dynamic=True, **kwargs): ax = ax or plt.gca() # Define the standard line color - if 'c' not in kwargs and 'color' not in kwargs: - kwargs['color'] = LINE_COLOR + if "c" not in kwargs and "color" not in kwargs: + kwargs["color"] = LINE_COLOR # Define the standard opacity - if 'alpha' not in kwargs: - kwargs['alpha'] = 0.5 + if "alpha" not in kwargs: + kwargs["alpha"] = 0.5 # Draw the identity line - identity, = ax.plot([],[], **kwargs) + identity, = ax.plot([], [], **kwargs) # Define the callback def callback(ax): @@ -266,35 +268,35 @@ def callback(ax): ylim = ax.get_ylim() # Set the bounding range of the line - data = ( - max(xlim[0], ylim[0]), min(xlim[1], ylim[1]) - ) + data = (max(xlim[0], ylim[0]), min(xlim[1], ylim[1])) identity.set_data(data, data) # Register the callback and return callback(ax) if dynamic: - ax.callbacks.connect('xlim_changed', callback) - ax.callbacks.connect('ylim_changed', callback) + ax.callbacks.connect("xlim_changed", callback) + ax.callbacks.connect("ylim_changed", callback) return ax -if __name__ == '__main__': +if __name__ == "__main__": import os import pandas as pd - path = os.path.join(os.path.dirname(__file__), "..", "examples", "data", "concrete.xls") + path = os.path.join( + os.path.dirname(__file__), "..", "examples", "data", "concrete.xls" + ) if not os.path.exists(path): raise Exception("Could not find path for testing") - xkey = 'Fine Aggregate (component 7)(kg in a m^3 mixture)' - ykey = 'Coarse Aggregate (component 6)(kg in a m^3 mixture)' + xkey = "Fine Aggregate (component 7)(kg in a m^3 mixture)" + ykey = "Coarse Aggregate (component 6)(kg in a m^3 mixture)" data = pd.read_excel(path) fig, axe = plt.subplots() axe.scatter(data[xkey], data[ykey]) - draw_best_fit(data[xkey], data[ykey], axe, 'select_best') + draw_best_fit(data[xkey], data[ykey], axe, "select_best") plt.show() diff --git a/yellowbrick/classifier/__init__.py b/yellowbrick/classifier/__init__.py index ad7e932ac..d0c5a151c 100644 --- a/yellowbrick/classifier/__init__.py +++ b/yellowbrick/classifier/__init__.py @@ -1,13 +1,13 @@ # yellowbrick.classifier # Visualizations related to evaluating Scikit-Learn classification models # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Author: Neal Humphrey -# Author: Jason Keung +# Author: Jason Keung # Created: Wed May 18 12:39:40 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [5eee25b] benjamin@bengfort.com $ diff --git a/yellowbrick/classifier/base.py b/yellowbrick/classifier/base.py index d581b2f48..508291933 100644 --- a/yellowbrick/classifier/base.py +++ b/yellowbrick/classifier/base.py @@ -1,12 +1,12 @@ # yellowbrick.classifier.base # API for classification visualizer hierarchy. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Author: Neal Humphrey # Created: Wed May 18 12:39:40 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: base.py [5388065] neal@nhumphrey.com $ @@ -19,120 +19,313 @@ ## Imports ########################################################################## +import warnings import numpy as np -from ..utils import isclassifier -from ..base import ScoreVisualizer -from ..style.palettes import color_palette -from ..exceptions import YellowbrickTypeError +from yellowbrick.utils import isclassifier +from yellowbrick.base import ScoreVisualizer +from yellowbrick.style.palettes import color_palette +from yellowbrick.exceptions import NotFitted, YellowbrickWarning +from yellowbrick.exceptions import YellowbrickTypeError, ModelError + +from sklearn.preprocessing import LabelEncoder ########################################################################## ## Base Classification Visualizer ########################################################################## + class ClassificationScoreVisualizer(ScoreVisualizer): + """Base class for classifier model selection. - def __init__(self, model, ax=None, classes=None, **kwargs): - """ - Check to see if model is an instance of a classifer. - Should return an error if it isn't. + The ClassificationScoreVisualizer wraps a classifier to produce a + visualization when the score method is called, usually to allow the user + to effectively compare the performance between models. - .. todo:: document this class. - .. tood:: accept as input classes as all visualizers need this. - """ + The base class provides helper functionality to ensure that classification + visualizers are able to correctly identify and encode classes with human + readable labels and to map colors to the classes if required. + + Parameters + ---------- + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). + + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. + + Attributes + ---------- + classes_ : ndarray of shape (n_classes,) + The class labels observed while fitting. + + class_count_ : ndarray of shape (n_classes,) + Number of samples encountered for each class during fitting. + + score_ : float + An evaluation metric of the classifier on test data produced when + ``score()`` is called. This metric is between 0 and 1 -- higher scores are + generally better. For classifiers, this score is usually accuracy, but + ensure you check the underlying model for more details about the metric. + """ + + def __init__( + self, + model, + ax=None, + fig=None, + classes=None, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs, + ): # A bit of type checking - if not isclassifier(model): + if not force_model and not isclassifier(model): raise YellowbrickTypeError( "This estimator is not a classifier; " "try a regression or clustering score visualizer instead!" - ) + ) # Initialize the super method. - super(ClassificationScoreVisualizer, self).__init__(model, ax=ax, **kwargs) - - # Convert to array if necessary to match estimator.classes_ - if classes is not None: - classes = np.array(classes) - - # Set up classifier score visualization properties - if classes is not None: - n_colors = len(classes) - else: - n_colors = None + super(ClassificationScoreVisualizer, self).__init__( + model, ax=ax, fig=fig, is_fitted=is_fitted, **kwargs + ) - self.colors = color_palette(kwargs.pop('colors', None), n_colors) - self.classes_ = classes + self.set_params(classes=classes, encoder=encoder, force_model=force_model) @property - def classes_(self): + def colors(self): """ - Proxy property to smartly access the classes from the estimator or - stored locally on the score visualizer for visualization. + Returns ``_colors`` if it exists, otherwise computes a categorical color + per class based on the matplotlib color cycle. If the visualizer is not + fitted, raises a NotFitted exception. + + If subclasses require users to choose colors or have specialized color + handling, they should set ``_colors`` on init or during fit. + + Notes + ----- + Because this is a property, this docstring is for developers only. """ - if self.__classes is None: - try: - return self.estimator.classes_ - except AttributeError: - return None - return self.__classes + if not hasattr(self, "_colors"): + if not hasattr(self, "classes_"): + raise NotFitted("cannot determine colors before fit") - @classes_.setter - def classes_(self, value): - self.__classes = value + # TODO: replace with resolve_colors + self._colors = color_palette(None, len(self.classes_)) + return self._colors def fit(self, X, y=None, **kwargs): """ + Fit the visualizer to the specified data. + Parameters ---------- - X : ndarray or DataFrame of shape n x m A matrix of n instances with m features y : ndarray or Series of length n An array or series of target or class values - kwargs: keyword arguments passed to Scikit-Learn API. - Returns ------- self : instance Returns the instance of the classification score visualizer - """ - # Fit the inner estimator - self.estimator.fit(X, y) + # Super fits the wrapped estimator + super(ClassificationScoreVisualizer, self).fit(X, y, **kwargs) + + # Extract the classes and the class counts from the target + self.classes_, self.class_counts_ = np.unique(y, return_counts=True) - # Extract the classes from the estimator - if self.classes_ is None: - self.classes_ = self.estimator.classes_ + # Ensure the classes are aligned with the estimator + # If they are not aligned, ignore class counts and issue a warning + if hasattr(self.estimator, "classes_"): + if not np.array_equal(self.classes_, self.estimator.classes_): + self.classes_ = self.estimator.classes_ + self.class_counts_ = None + + # Decode classes to human readable labels specified by the user + self.classes_ = self._decode_labels(self.classes_) # Always return self from fit return self - - def score(self, X, y, **kwargs): + def score(self, X, y): """ The score function is the hook for visual interaction. Pass in test data and the visualizer will create predictions on the data and evaluate them with respect to the test values. The evaluation will then be passed to draw() and the result of the estimator score will be returned. + Parameters ---------- X : array-like X (also X_test) are the dependent variables of test set to predict + y : array-like y (also y_test) is the independent actual variables to score against + Returns ------- score : float + Returns the score of the underlying model, usually accuracy for + classification models. Refer to the specific model for more details. """ - self.score_ = self.estimator.score(X, y, **kwargs) + # If the estimator has been passed in fitted but the visualizer was not fit + # then we can retrieve the classes from the estimator, unfortunately we cannot + # retrieve the class counts so we simply set them to None and warn the user. + # NOTE: cannot test if hasattr(self, "classes_") because it will be proxied. + if not hasattr(self, "class_counts_"): + if not hasattr(self.estimator, "classes_"): + raise NotFitted( + ( + "could not determine required property classes_; " + "the visualizer must either be fit or instantiated with a " + "fitted classifier before calling score()" + ) + ) + self.class_counts_ = None + self.classes_ = self._decode_labels(self.estimator.classes_) + warnings.warn( + "could not determine class_counts_ from previously fitted classifier", + YellowbrickWarning, + ) + + # This method implements ScoreVisualizer (do not call super). + self.score_ = self.estimator.score(X, y) return self.score_ - #TODO during refactoring this can be used to generalize ClassBalance - def class_counts(self, y): - unique, counts = np.unique(y, return_counts=True) - return dict(zip(unique, counts)) + def _decode_labels(self, y): + """ + An internal helper function that uses either the classes or encoder + properties to correctly decode y as user-readable string labels. + + If both classes and encoder are set, a warning is issued and encoder is + used instead of classes. If neither encoder nor classes is set then the + original array is returned unmodified. + """ + if self.classes is not None and self.encoder is not None: + warnings.warn( + "both classes and encoder specified, using encoder", YellowbrickWarning + ) + + if self.encoder is not None: + # Use the label encoder or other transformer + if hasattr(self.encoder, "inverse_transform"): + try: + return self.encoder.inverse_transform(y) + except ValueError: + y_labels = np.unique(y) + raise ModelError( + "could not decode {} y values to {} labels".format( + y_labels, self._labels() + ) + ) + + # Otherwise, treat as a dictionary + try: + return np.asarray([self.encoder[yi] for yi in y]) + except KeyError as e: + raise ModelError( + ( + "cannot decode class {} to label, " + "key not specified by encoder" + ).format(e) + ) + + if self.classes is not None: + # Determine indices to perform class mappings on + yp = np.asarray(y) + if yp.dtype.kind in {"i", "u"}: + idx = yp + else: + # Use label encoder to get indices by sorted class names + idx = LabelEncoder().fit_transform(yp) + + # Use index mapping for classes + try: + return np.asarray(self.classes)[idx] + except IndexError: + y_labels = np.unique(yp) + raise ModelError( + "could not decode {} y values to {} labels".format( + y_labels, self._labels() + ) + ) + + # could not decode y without encoder or classes, return it as it is, unmodified + return y + + def _labels(self): + """ + Returns the human specified labels in either the classes list or from the + encoder. Returns None if no human labels have been specified, but issues a + warning if a transformer has been passed that does not specify labels. + """ + if self.classes is not None and self.encoder is not None: + warnings.warn( + "both classes and encoder specified, using encoder", YellowbrickWarning + ) + + if self.encoder is not None: + # Use label encoder or other transformer + if hasattr(self.encoder, "transform"): + if hasattr(self.encoder, "classes_"): + return self.encoder.classes_ + + # This is not a label encoder + msg = "could not determine class labels from {}".format( + self.encoder.__class__.__name__ + ) + warnings.warn(msg, YellowbrickWarning) + return None + + # Otherwise, treat as dictionary and ensure sorted by key + keys = sorted(list(self.encoder.keys())) + return np.asarray([self.encoder[key] for key in keys]) + + if self.classes is not None: + return np.asarray(self.classes) + + return None diff --git a/yellowbrick/classifier/class_prediction_error.py b/yellowbrick/classifier/class_prediction_error.py index b9919becb..013e05f47 100644 --- a/yellowbrick/classifier/class_prediction_error.py +++ b/yellowbrick/classifier/class_prediction_error.py @@ -2,8 +2,11 @@ # Shows the balance of classes and their associated predictions. # # Author: Larry Gray -# Author: Benjamin Bengfort -# Created: Wed May 18 12:39:40 2016 -0400 +# Author: Benjamin Bengfort +# Created: Fri Jul 20 10:26:25 2018 -0400 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt # # ID: class_prediction_error.py [] lwgray@gmail.com $ @@ -18,61 +21,107 @@ import numpy as np import matplotlib.pyplot as plt -from .base import ClassificationScoreVisualizer - from sklearn.utils.multiclass import unique_labels from sklearn.metrics.classification import _check_targets from sklearn.model_selection import train_test_split as tts -from ..exceptions import ModelError, YellowbrickValueError -from ..style.colors import resolve_colors +from yellowbrick.draw import bar_stack +from yellowbrick.classifier.base import ClassificationScoreVisualizer +from yellowbrick.exceptions import ModelError, YellowbrickValueError, NotFitted ########################################################################## ## Class Prediction Error Chart ########################################################################## + class ClassPredictionError(ClassificationScoreVisualizer): """ Class Prediction Error chart that shows the support for each class in the - fitted classification model displayed as a stacked bar. Each bar is - segmented to show the distribution of predicted classes for each - class. It is initialized with a fitted model and generates a - class prediction error chart on draw. + fitted classification model displayed as a stacked bar. Each bar is segmented + to show the distribution of predicted classes for each class. It is initialized + with a fitted model and generates a class prediction error chart on draw. Parameters ---------- - ax: axes - the axis to plot the figure on. - - model: estimator - Scikit-Learn estimator object. Should be an instance of a classifier, - else ``__init__()`` will raise an exception. - - classes: list - A list of class names for the legend. If classes is None and a y value - is passed to fit then the classes are selected from the target vector. - - kwargs: dict - Keyword arguments passed to the super class. Here, used - to colorize the bars in the histogram. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). + + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. Attributes ---------- + classes_ : ndarray of shape (n_classes,) + The class labels observed while fitting. + + class_count_ : ndarray of shape (n_classes,) + Number of samples encountered for each class during fitting. + score_ : float - Global accuracy score + An evaluation metric of the classifier on test data produced when + ``score()`` is called. This metric is between 0 and 1 -- higher scores are + generally better. For classifiers, this score is usually accuracy, but + ensure you check the underlying model for more details about the score. predictions_ : ndarray An ndarray of predictions whose rows are the true classes and whose columns are the predicted classes - - Notes - ----- - These parameters can be influenced later on in the visualization - process, but can and should be set as early as possible. """ - def score(self, X, y, **kwargs): + def __init__( + self, + model, + ax=None, + classes=None, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs + ): + super(ClassPredictionError, self).__init__( + model, + ax=ax, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs + ) + + def score(self, X, y): """ Generates a 2D array where each row is the count of the predicted classes and each column is the true class @@ -90,58 +139,71 @@ def score(self, X, y, **kwargs): score_ : float Global accuracy score """ - + # Must be computed before calling super # We're relying on predict to raise NotFitted y_pred = self.predict(X) - y_type, y_true, y_pred = _check_targets(y, y_pred) - if y_type not in ("binary", "multiclass"): - raise YellowbrickValueError("%s is not supported" % y_type) + raise YellowbrickValueError("{} is not supported".format(y_type)) + # Get the indices of the unique labels indices = unique_labels(y_true, y_pred) - - if len(self.classes_) > len(indices): - raise ModelError("y and y_pred contain zero values " - "for one of the specified classes") - elif len(self.classes_) < len(indices): - raise NotImplementedError("filtering classes is " - "currently not supported") + labels = self._labels() + + # Call super to compute self.score_ and verify classes + try: + super(ClassPredictionError, self).score(X, y) + except ModelError as e: + # raise visualizer-specific errors + if labels is not None and len(labels) < len(indices): + raise NotImplementedError( + "filtering classes is currently not supported" + ) + else: + raise e + + # Ensure all labels are used + if labels is not None and len(labels) > len(indices): + raise ModelError( + "y and y_pred contain zero values for one of the specified classes" + ) # Create a table of predictions whose rows are the true classes # and whose columns are the predicted classes; each element # is the count of predictions for that class that match the true # value of that class. - self.predictions_ = np.array([ + self.predictions_ = np.array( [ - (y_pred[y == label_t] == label_p).sum() - for label_p in indices + [(y_pred[y == label_t] == label_p).sum() for label_p in indices] + for label_t in indices ] - for label_t in indices - ]) + ) self.draw() - self.score_ = self.estimator.score(X, y) - return self.score_ def draw(self): """ Renders the class prediction error across the axis. + + Returns + ------- + ax : Matplotlib Axes + The axes on which the figure is plotted """ - indices = np.arange(len(self.classes_)) - prev = np.zeros(len(self.classes_)) + if not hasattr(self, "predictions_") or not hasattr(self, "classes_"): + raise NotFitted.from_estimator(self, "draw") - colors = resolve_colors( + legend_kws = {"bbox_to_anchor": (1.04, 0.5), "loc": "center left"} + bar_stack( + self.predictions_, + self.ax, + labels=list(self.classes_), + ticks=self.classes_, colors=self.colors, - n_colors=len(self.classes_)) - - for idx, row in enumerate(self.predictions_): - self.ax.bar(indices, row, label=self.classes_[idx], - bottom=prev, color=colors[idx]) - prev += row - + legend_kws=legend_kws, + ) return self.ax def finalize(self, **kwargs): @@ -150,15 +212,9 @@ def finalize(self, **kwargs): The user calls poof and poof calls finalize. """ - indices = np.arange(len(self.classes_)) - # Set the title self.set_title("Class Prediction Error for {}".format(self.name)) - # Set the x ticks with the class names - self.ax.set_xticks(indices) - self.ax.set_xticklabels(self.classes_) - # Set the axes labels self.ax.set_xlabel("actual class") self.ax.set_ylabel("number of predicted class") @@ -167,37 +223,42 @@ def finalize(self, **kwargs): cmax = max([sum(predictions) for predictions in self.predictions_]) self.ax.set_ylim(0, cmax + cmax * 0.1) - # Put the legend outside of the graph - plt.legend(bbox_to_anchor=(1.04, 0.5), loc="center left") - plt.tight_layout(rect=[0, 0, 0.85, 1]) + # Ensure the legend fits on the figure + plt.tight_layout(rect=[0, 0, 0.90, 1]) # TODO: Could use self.fig now ########################################################################## ## Quick Method ########################################################################## + def class_prediction_error( model, X, - y=None, + y, ax=None, - classes=None, test_size=0.2, random_state=None, - **kwargs): - """Quick method: - Divides the dataset X and y into train and test splits, fits the model on - the train split, then scores the model on the test split. The visualizer - displays the support for each class in the fitted classification model - displayed as a stacked bar plot Each bar is segmented to show the - distribution of predicted classes for each class. - - This helper function is a quick wrapper to utilize the ClassPredictionError - ScoreVisualizer for one-off analysis. + classes=None, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs +): + """Class Prediction Error + + Divides the dataset X and y into train and test splits, fits the model on the train + split, then scores the model on the test split. The visualizer displays the support + for each class in the fitted classification model displayed as a stacked bar plot. + Each bar is segmented to show the distribution of predicted classes for each class. Parameters ---------- - model : the Scikit-Learn estimator (should be a classifier) + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. X : ndarray or DataFrame of shape n x m A matrix of n instances with m features. @@ -205,11 +266,9 @@ def class_prediction_error( y : ndarray or Series of length n An array or series of target or class values. - ax : matplotlib axes - The axes to plot the figure on. - - classes : list of strings - The names of the classes in the target + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). test_size : float, default=0.2 The percentage of the data to reserve as test data. @@ -217,13 +276,49 @@ def class_prediction_error( random_state : int or None, default=None The value to seed the random number generator for shuffling data. + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs: dict + Keyword arguments passed to the visualizer base classes. + Returns ------- - ax : matplotlib axes - Returns the axes that the class prediction error plot was drawn on. + viz : ClassPredictionError + Returns the fitted, finalized visualizer """ # Instantiate the visualizer - visualizer = ClassPredictionError(model, ax, classes, **kwargs) + visualizer = ClassPredictionError( + model=model, + ax=ax, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs + ) # Create the train and test splits X_train, X_test, y_train, y_test = tts( @@ -233,6 +328,7 @@ def class_prediction_error( # Fit and transform the visualizer (calls draw) visualizer.fit(X_train, y_train, **kwargs) visualizer.score(X_test, y_test) + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/classifier/classification_report.py b/yellowbrick/classifier/classification_report.py index 8ffe545e9..7eca50741 100644 --- a/yellowbrick/classifier/classification_report.py +++ b/yellowbrick/classifier/classification_report.py @@ -1,14 +1,14 @@ # yellowbrick.classifier.classification_report # Visual classification report for classifier scoring. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Author: Neal Humphrey # Author: Allyssa Riley # Author: Larry Gray -# Created: Wed May 18 12:39:40 2016 -0400 +# Created: Wed May 3 18:15:42 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: classification_report.py [5388065] neal@nhumphrey.com $ @@ -21,26 +21,25 @@ ## Imports ########################################################################## -from __future__ import division import numpy as np import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.metrics import precision_recall_fscore_support -from ..style import find_text_color -from ..style.palettes import color_sequence -from .base import ClassificationScoreVisualizer -from ..exceptions import YellowbrickValueError +from yellowbrick.style import find_text_color +from yellowbrick.style.palettes import color_sequence +from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.classifier.base import ClassificationScoreVisualizer ########################################################################## ## Classification Report ########################################################################## -CMAP_UNDERCOLOR = 'w' -CMAP_OVERCOLOR = '#2a7d4f' -SCORES_KEYS = ('precision', 'recall', 'f1', 'support') -PERCENT = 'percent' +PERCENT = "percent" +CMAP_UNDERCOLOR = "w" +CMAP_OVERCOLOR = "#2a7d4f" +SCORES_KEYS = ("precision", "recall", "f1", "support") class ClassificationReport(ClassificationScoreVisualizer): @@ -50,15 +49,23 @@ class ClassificationReport(ClassificationScoreVisualizer): Parameters ---------- - ax : The axis to plot the figure on. - - model : the Scikit-Learn estimator - Should be an instance of a classifier, else the __init__ will - return an error. - - classes : a list of class names for the legend - If classes is None and a y value is passed to fit then the classes - are selected from the target vector. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). + + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. cmap : string, default: ``'YlOrRd'`` Specify a colormap to define the heatmap of the predicted class @@ -68,7 +75,25 @@ class ClassificationReport(ClassificationScoreVisualizer): Specify if support will be displayed. It can be further defined by whether support should be reported as a raw count or percentage. - kwargs : keyword arguments passed to the super class. + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. Examples -------- @@ -81,35 +106,61 @@ class ClassificationReport(ClassificationScoreVisualizer): Attributes ---------- + classes_ : ndarray of shape (n_classes,) + The class labels observed while fitting. + + class_count_ : ndarray of shape (n_classes,) + Number of samples encountered for each class during fitting. + score_ : float - Global accuracy score + An evaluation metric of the classifier on test data produced when + ``score()`` is called. This metric is between 0 and 1 -- higher scores are + generally better. For classifiers, this score is usually accuracy, but + ensure you check the underlying model for more details about the score. scores_ : dict of dicts Outer dictionary composed of precision, recall, f1, and support scores with inner dictionaries specifiying the values for each class listed. """ - def __init__(self, model, ax=None, classes=None, cmap='YlOrRd', - support=None, **kwargs): + + def __init__( + self, + model, + ax=None, + classes=None, + cmap="YlOrRd", + support=None, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs + ): super(ClassificationReport, self).__init__( - model, ax=ax, classes=classes, **kwargs + model, + ax=ax, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs ) + self.support = support self.cmap = color_sequence(cmap) - self.cmap.set_under(color=CMAP_UNDERCOLOR) self.cmap.set_over(color=CMAP_OVERCOLOR) + self.cmap.set_under(color=CMAP_UNDERCOLOR) self._displayed_scores = [key for key in SCORES_KEYS] - self.support = support if support not in {None, True, False, "percent", "count"}: raise YellowbrickValueError( - "'{}' is an invalid argument for support, use None, True, " \ + "'{}' is an invalid argument for support, use None, True, " "False, 'percent', or 'count'".format(support) ) if not support: self._displayed_scores.remove("support") - def score(self, X, y=None, **kwargs): + def score(self, X, y): """ Generates the Scikit-Learn classification report. @@ -127,8 +178,10 @@ def score(self, X, y=None, **kwargs): score_ : float Global accuracy score """ - y_pred = self.predict(X) + # Call super to check if fitted and to compute self.score_ + super(ClassificationReport, self).score(X, y) + y_pred = self.predict(X) scores = precision_recall_fscore_support(y, y_pred) # Calculate the percentage for the support metric @@ -145,13 +198,9 @@ def score(self, X, y=None, **kwargs): # Remove support scores if not required if not self.support: - self.scores_.pop('support') + self.scores_.pop("support") self.draw() - - # Retrieve and store the score attribute from the sklearn classifier - self.score_ = self.estimator.score(X, y) - return self.score_ def draw(self): @@ -161,7 +210,6 @@ def draw(self): # Create display grid cr_display = np.zeros((len(self.classes_), len(self._displayed_scores))) - # For each class row, append columns for precision, recall, f1, and support for idx, cls in enumerate(self.classes_): for jdx, metric in enumerate(self._displayed_scores): @@ -169,7 +217,10 @@ def draw(self): # Set up the dimensions of the pcolormesh # NOTE: pcolormesh accepts grids that are (N+1,M+1) - X, Y = np.arange(len(self.classes_)+1), np.arange(len(self._displayed_scores)+1) + X, Y = ( + np.arange(len(self.classes_) + 1), + np.arange(len(self._displayed_scores) + 1), + ) self.ax.set_ylim(bottom=0, top=cr_display.shape[0]) self.ax.set_xlim(left=0, right=cr_display.shape[1]) @@ -194,21 +245,18 @@ def draw(self): text_color = find_text_color(base_color) # Add the label to the middle of the grid - cx, cy = x+0.5, y+0.5 - self.ax.text( - cy, cx, svalue, va='center', ha='center', color=text_color - ) - + cx, cy = x + 0.5, y + 0.5 + self.ax.text(cy, cx, svalue, va="center", ha="center", color=text_color) # Draw the heatmap with colors bounded by the min and max of the grid # NOTE: I do not understand why this is Y, X instead of X, Y it works # in this order but raises an exception with the other order. g = self.ax.pcolormesh( - Y, X, cr_display, vmin=0, vmax=1, cmap=self.cmap, edgecolor='w', + Y, X, cr_display, vmin=0, vmax=1, cmap=self.cmap, edgecolor="w" ) # Add the color bar - plt.colorbar(g, ax=self.ax) + plt.colorbar(g, ax=self.ax) # TODO: Could use self.fig now # Return the axes being drawn on return self.ax @@ -224,63 +272,125 @@ def finalize(self, **kwargs): """ # Set the title of the classifiation report - self.set_title('{} Classification Report'.format(self.name)) + self.set_title("{} Classification Report".format(self.name)) # Set the tick marks appropriately - self.ax.set_xticks(np.arange(len(self._displayed_scores))+0.5) - self.ax.set_yticks(np.arange(len(self.classes_))+0.5) + self.ax.set_xticks(np.arange(len(self._displayed_scores)) + 0.5) + self.ax.set_yticks(np.arange(len(self.classes_)) + 0.5) self.ax.set_xticklabels(self._displayed_scores, rotation=45) self.ax.set_yticklabels(self.classes_) - plt.tight_layout() - - -def classification_report(model, X, y=None, ax=None, classes=None, - random_state=None,**kwargs): - """Quick method: + plt.tight_layout() # TODO: Could use self.fig now + + +def classification_report( + model, + X, + y, + ax=None, + test_size=0.2, + random_state=None, + classes=None, + cmap="YlOrRd", + support=None, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs +): + """Classification Report Displays precision, recall, F1, and support scores for the model. Integrates numerical scores as well as color-coded heatmap. - This helper function is a quick wrapper to utilize the ClassificationReport - ScoreVisualizer for one-off analysis. - Parameters ---------- + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. + X : ndarray or DataFrame of shape n x m A matrix of n instances with m features. y : ndarray or Series of length n An array or series of target or class values. - ax : matplotlib axes - The axes to plot the figure on. + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). + + test_size : float, default=0.2 + The percentage of the data to reserve as test data. + + random_state : int or None, default=None + The value to seed the random number generator for shuffling data. + + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + cmap : string, default: ``'YlOrRd'`` + Specify a colormap to define the heatmap of the predicted class + against the actual class in the classification report. - model : the Scikit-Learn estimator (should be a classifier) + support: {True, False, None, 'percent', 'count'}, default: None + Specify if support will be displayed. It can be further defined by + whether support should be reported as a raw count or percentage. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. - classes : list of strings - The names of the classes in the target + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. - random_state: integer - The seed value for a random generator + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. Returns ------- - ax : matplotlib axes - Returns the axes that the classification report was drawn on. + viz : ClassificationReport + Returns the fitted, finalized visualizer """ # Instantiate the visualizer - visualizer = ClassificationReport(model, ax, classes, **kwargs) + visualizer = ClassificationReport( + model=model, + ax=ax, + classes=classes, + cmap=cmap, + support=support, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs + ) # Create the train and test splits X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.2, random_state=random_state + X, y, test_size=test_size, random_state=random_state ) # Fit and transform the visualizer (calls draw) visualizer.fit(X_train, y_train, **kwargs) visualizer.score(X_test, y_test) + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/classifier/confusion_matrix.py b/yellowbrick/classifier/confusion_matrix.py index 708e197eb..adb92cacc 100644 --- a/yellowbrick/classifier/confusion_matrix.py +++ b/yellowbrick/classifier/confusion_matrix.py @@ -4,7 +4,7 @@ # Author: Neal Humphrey # Created: Tue May 03 11:05:11 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: confusion_matrix.py [5388065] neal@nhumphrey.com $ @@ -19,22 +19,22 @@ import numpy as np -from ..utils import div_safe -from ..style import find_text_color -from ..style.palettes import color_sequence -from .base import ClassificationScoreVisualizer - from sklearn.model_selection import train_test_split from sklearn.metrics import confusion_matrix as confusion_matrix_metric +from yellowbrick.utils import div_safe +from yellowbrick.style import find_text_color +from yellowbrick.style.palettes import color_sequence +from yellowbrick.classifier.base import ClassificationScoreVisualizer + ########################################################################## ## ConfusionMatrix ########################################################################## -CMAP_UNDERCOLOR = 'w' -CMAP_OVERCOLOR = '#2a7d4f' -CMAP_MUTEDCOLOR = '0.75' +CMAP_UNDERCOLOR = "w" +CMAP_MUTEDCOLOR = "0.75" +CMAP_OVERCOLOR = "#2a7d4f" class ConfusionMatrix(ClassificationScoreVisualizer): @@ -53,11 +53,14 @@ class ConfusionMatrix(ClassificationScoreVisualizer): Parameters ---------- model : estimator - Must be a classifier, otherwise raises YellowbrickTypeError + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes - will be used (or generated if required). + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). sample_weight: array-like of shape = [n_samples], optional Passed to ``confusion_matrix`` to weight the samples. @@ -68,22 +71,19 @@ class ConfusionMatrix(ClassificationScoreVisualizer): classes, percent should be set to False or inaccurate figures will be displayed. - classes : list, default: None - a list of class names to use in the confusion_matrix. - This is passed to the ``labels`` parameter of - ``sklearn.metrics.confusion_matrix()``, and follows the behaviour - indicated by that function. It may be used to reorder or select a - subset of labels. If None, classes that appear at least once in - ``y_true`` or ``y_pred`` are used in sorted order. - - label_encoder : dict or LabelEncoder, default: None - When specifying the ``classes`` argument, the input to ``fit()`` - and ``score()`` must match the expected labels. If the ``X`` and ``y`` - datasets have been encoded prior to training and the labels must be - preserved for the visualization, use this argument to provide a - mapping from the encoded class to the correct label. Because typically - a Scikit-Learn ``LabelEncoder`` is used to perform this operation, you - may provide it directly to the class to utilize its fitted encoding. + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. cmap : string, default: ``'YlOrRd'`` Specify a colormap to define the heatmap of the predicted class @@ -93,16 +93,36 @@ class ConfusionMatrix(ClassificationScoreVisualizer): Specify the fontsize of the text in the grid and labels to make the matrix a bit easier to read. Uses rcParams font size by default. + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. + Attributes ---------- + classes_ : ndarray of shape (n_classes,) + The class labels observed while fitting. + + class_counts_ : ndarray of shape (n_classes,) + Number of samples encountered for each class supporting the confusion matrix. + score_ : float - Global accuracy score + An evaluation metric of the classifier on test data produced when + ``score()`` is called. This metric is between 0 and 1 -- higher scores are + generally better. For classifiers, this score is usually accuracy, but + ensure you check the underlying model for more details about the metric. confusion_matrix_ : array, shape = [n_classes, n_classes] - The numeric scores of the confusion matrix - - class_counts_ : array, shape = [n_classes,] - The total number of each class supporting the confusion matrix + The numeric scores of the confusion matrix. Examples -------- @@ -114,24 +134,39 @@ class ConfusionMatrix(ClassificationScoreVisualizer): >>> viz.poof() """ - - def __init__(self, model, ax=None, classes=None, sample_weight=None, - percent=False, label_encoder=None, cmap='YlOrRd', - fontsize=None, **kwargs): + def __init__( + self, + model, + ax=None, + sample_weight=None, + percent=False, + classes=None, + encoder=None, + cmap="YlOrRd", + fontsize=None, + is_fitted="auto", + force_model=False, + **kwargs + ): super(ConfusionMatrix, self).__init__( - model, ax=ax, classes=classes, **kwargs + model, + ax=ax, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs ) # Visual parameters + self.fontsize = fontsize self.cmap = color_sequence(cmap) self.cmap.set_under(color=CMAP_UNDERCOLOR) self.cmap.set_over(color=CMAP_OVERCOLOR) - self.fontsize = fontsize # Estimator parameters - self.label_encoder = label_encoder - self.sample_weight = sample_weight self.percent = percent + self.sample_weight = sample_weight # Used to draw diagonal line for predicted class = true class self._edgecolors = [] @@ -156,30 +191,30 @@ def score(self, X, y): score_ : float Global accuracy score """ + # Call super to check if fitted and to compute self.score_ + super(ConfusionMatrix, self).score(X, y) + # Create predictions from X (will raise not fitted error) y_pred = self.predict(X) - # Encode the target with the supplied label encoder - if self.label_encoder: - try : - y = self.label_encoder.inverse_transform(y) - y_pred = self.label_encoder.inverse_transform(y_pred) - except AttributeError: - # if a mapping is passed to class apply it here. - y = np.array([self.label_encoder[x] for x in y]) - y_pred = np.array([self.label_encoder[x] for x in y_pred]) + # Decode the target with the label encoder and get human readable labels + y = self._decode_labels(y) + y_pred = self._decode_labels(y_pred) + labels = self._labels() + if labels is None: + labels = self.classes_ # Compute the confusion matrix and class counts self.confusion_matrix_ = confusion_matrix_metric( - y, y_pred, labels=self.classes_, sample_weight=self.sample_weight + y, y_pred, labels=labels, sample_weight=self.sample_weight ) - self.class_counts_ = self.class_counts(y) + self.class_counts_ = dict(zip(*np.unique(y, return_counts=True))) # Make array of only the classes actually being used. # Needed because sklearn confusion_matrix only returns counts for # selected classes but percent should be calculated on all classes selected_class_counts = [] - for c in self.classes_: + for c in labels: try: selected_class_counts.append(self.class_counts_[c]) except KeyError: @@ -187,10 +222,6 @@ def score(self, X, y): self.class_counts_ = np.array(selected_class_counts) self.draw() - - # Retrieve and store the score attribute from the sklearn classifier - self.score_ = self.estimator.score(X, y) - return self.score_ def draw(self): @@ -203,27 +234,36 @@ def draw(self): # Convert confusion matrix to percent of each row, i.e. the # predicted as a percent of true in each class. - if self.percent == True: + if self.percent is True: # Note: div_safe function returns 0 instead of NAN. - cm_display = div_safe(self.confusion_matrix_, self.class_counts_.reshape(-1,1)) - cm_display = np.round(cm_display* 100, decimals=0) + cm_display = div_safe( + self.confusion_matrix_, self.class_counts_.reshape(-1, 1) + ) + cm_display = np.round(cm_display * 100, decimals=0) # Y axis should be sorted top to bottom in pcolormesh - cm_display = cm_display[::-1,::] + cm_display = cm_display[::-1, ::] + + # Get the human readable labels + labels = self._labels() + if labels is None: + labels = self.classes_ # Set up the dimensions of the pcolormesh - n_classes = len(self.classes_) - X, Y = np.arange(n_classes+1), np.arange(n_classes+1) + n_classes = len(labels) + X, Y = np.arange(n_classes + 1), np.arange(n_classes + 1) self.ax.set_ylim(bottom=0, top=cm_display.shape[0]) self.ax.set_xlim(left=0, right=cm_display.shape[1]) # Fetch the grid labels from the classes in correct order; set ticks. - xticklabels = self.classes_ - yticklabels = self.classes_[::-1] + xticklabels = labels + yticklabels = labels[::-1] ticks = np.arange(n_classes) + 0.5 self.ax.set(xticks=ticks, yticks=ticks) - self.ax.set_xticklabels(xticklabels, rotation="vertical", fontsize=self.fontsize) + self.ax.set_xticklabels( + xticklabels, rotation="vertical", fontsize=self.fontsize + ) self.ax.set_yticklabels(yticklabels, fontsize=self.fontsize) # Set data labels in the grid enumerating over all x,y class pairs. @@ -233,7 +273,7 @@ def draw(self): for y in Y[:-1]: # Extract the value and the text label - value = cm_display[x,y] + value = cm_display[x, y] svalue = "{:0.0f}".format(value) if self.percent: svalue += "%" @@ -243,37 +283,52 @@ def draw(self): text_color = find_text_color(base_color) # Make zero values more subtle - if cm_display[x,y] == 0: + if cm_display[x, y] == 0: text_color = CMAP_MUTEDCOLOR # Add the label to the middle of the grid - cx, cy = x+0.5, y+0.5 + cx, cy = x + 0.5, y + 0.5 self.ax.text( - cy, cx, svalue, va='center', ha='center', - color=text_color, fontsize=self.fontsize, + cy, + cx, + svalue, + va="center", + ha="center", + color=text_color, + fontsize=self.fontsize, ) # Add a dark line on the grid with the diagonal. Note that the # tick labels have already been reversed. - lc = 'k' if xticklabels[x] == yticklabels[y] else 'w' + lc = "k" if xticklabels[x] == yticklabels[y] else "w" self._edgecolors.append(lc) - # Draw the heatmap with colors bounded by vmin,vmax vmin = 0.00001 - vmax = 99.999 if self.percent == True else cm_display.max() + vmax = 99.999 if self.percent is True else cm_display.max() self.ax.pcolormesh( - X, Y, cm_display, vmin=vmin, vmax=vmax, - edgecolor=self._edgecolors, cmap=self.cmap, linewidth='0.01' + X, + Y, + cm_display, + vmin=vmin, + vmax=vmax, + edgecolor=self._edgecolors, + cmap=self.cmap, + linewidth="0.01", ) # Return the axes being drawn on return self.ax + def poof(self, outpath=None, **kwargs): + if outpath is not None: + kwargs["bbox_inches"] = kwargs.get("bbox_inches", "tight") + return super(ConfusionMatrix, self).poof(outpath, **kwargs) + def finalize(self, **kwargs): - self.set_title('{} Confusion Matrix'.format(self.name)) - self.ax.set_ylabel('True Class') - self.ax.set_xlabel('Predicted Class') + self.set_title("{} Confusion Matrix".format(self.name)) + self.ax.set_ylabel("True Class") + self.ax.set_xlabel("Predicted Class") ########################################################################## @@ -281,10 +336,24 @@ def finalize(self, **kwargs): ########################################################################## -def confusion_matrix(model, X, y, ax=None, classes=None, sample_weight=None, - percent=False, label_encoder=None, cmap='YlOrRd', - fontsize=None, **kwargs): - """Quick method: +def confusion_matrix( + model, + X, + y, + ax=None, + test_size=0.2, + random_state=None, + sample_weight=None, + percent=False, + classes=None, + encoder=None, + cmap="YlOrRd", + fontsize=None, + is_fitted="auto", + force_model=False, + **kwargs +): + """Confusion Matrix Creates a heatmap visualization of the sklearn.metrics.confusion_matrix(). A confusion matrix shows each combination of the true and predicted @@ -300,7 +369,10 @@ def confusion_matrix(model, X, y, ax=None, classes=None, sample_weight=None, Parameters ---------- model : estimator - Must be a classifier, otherwise raises YellowbrickTypeError + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. X : ndarray or DataFrame of shape n x m A matrix of n instances with m features. @@ -309,8 +381,14 @@ def confusion_matrix(model, X, y, ax=None, classes=None, sample_weight=None, An array or series of target or class values. ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes - will be used (or generated if required). + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). + + test_size : float, default=0.2 + The percentage of the data to reserve as test data. + + random_state : int or None, default=None + The value to seed the random number generator for shuffling data. sample_weight: array-like of shape = [n_samples], optional Passed to ``confusion_matrix`` to weight the samples. @@ -321,22 +399,19 @@ def confusion_matrix(model, X, y, ax=None, classes=None, sample_weight=None, classes, percent should be set to False or inaccurate figures will be displayed. - classes : list, default: None - a list of class names to use in the confusion_matrix. - This is passed to the ``labels`` parameter of - ``sklearn.metrics.confusion_matrix()``, and follows the behaviour - indicated by that function. It may be used to reorder or select a - subset of labels. If None, classes that appear at least once in - ``y_true`` or ``y_pred`` are used in sorted order. - - label_encoder : dict or LabelEncoder, default: None - When specifying the ``classes`` argument, the input to ``fit()`` - and ``score()`` must match the expected labels. If the ``X`` and ``y`` - datasets have been encoded prior to training and the labels must be - preserved for the visualization, use this argument to provide a - mapping from the encoded class to the correct label. Because typically - a Scikit-Learn ``LabelEncoder`` is used to perform this operation, you - may provide it directly to the class to utilize its fitted encoding. + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. cmap : string, default: ``'YlOrRd'`` Specify a colormap to define the heatmap of the predicted class @@ -346,23 +421,50 @@ def confusion_matrix(model, X, y, ax=None, classes=None, sample_weight=None, Specify the fontsize of the text in the grid and labels to make the matrix a bit easier to read. Uses rcParams font size by default. + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. + Returns ------- - ax : matplotlib axes - Returns the axes that the classification report was drawn on. + viz : ConfusionMatrix + Returns the fitted, finalized visualizer """ # Instantiate the visualizer visualizer = ConfusionMatrix( - model, ax, classes, sample_weight, percent, - label_encoder, cmap, fontsize, **kwargs + model=model, + ax=ax, + sample_weight=sample_weight, + percent=percent, + classes=classes, + encoder=encoder, + cmap=cmap, + fontsize=fontsize, + is_fitted=is_fitted, + force_model=force_model, + **kwargs ) # Create the train and test splits - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + # TODO: determine how to use quick methods that require train and test data. + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=test_size, random_state=random_state + ) # Fit and transform the visualizer (calls draw) visualizer.fit(X_train, y_train, **kwargs) visualizer.score(X_test, y_test) + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/classifier/prcurve.py b/yellowbrick/classifier/prcurve.py index 2199dbe6a..8069f834a 100644 --- a/yellowbrick/classifier/prcurve.py +++ b/yellowbrick/classifier/prcurve.py @@ -1,10 +1,13 @@ # yellowbrick.classifier.prcurve # Implements Precision-Recall curves for classification models. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Sep 04 16:47:19 2018 -0400 # -# ID: prcurve.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: prcurve.py [48889c4] benjamin@bengfort.com $ """ Implements Precision-Recall curves for classification models. @@ -16,10 +19,6 @@ import numpy as np -from ..exceptions import ModelError, NotFitted -from ..exceptions import YellowbrickValueError -from .base import ClassificationScoreVisualizer - from sklearn.preprocessing import label_binarize from sklearn.multiclass import OneVsRestClassifier from sklearn.utils.multiclass import type_of_target @@ -27,19 +26,28 @@ from sklearn.model_selection import train_test_split as tts from sklearn.metrics import precision_recall_curve as sk_precision_recall_curve +from yellowbrick.exceptions import ModelError, NotFitted +from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.classifier.base import ClassificationScoreVisualizer + # Target Type Constants +# TODO: These can now be imported from utils.target BINARY = "binary" MULTICLASS = "multiclass" # Average Metric Constants MICRO = "micro" +# Default Values +DEFAULT_ISO_F1_VALUES = (0.2, 0.4, 0.6, 0.8) + ########################################################################## ## PrecisionRecallCurve Visualizer ########################################################################## + class PrecisionRecallCurve(ClassificationScoreVisualizer): """ Precision-Recall curves are a metric used to evaluate a classifier's quality, @@ -54,54 +62,78 @@ class PrecisionRecallCurve(ClassificationScoreVisualizer): Parameters ---------- - model : the Scikit-Learn estimator - A classification model to score the precision-recall curve on. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes - will be used (or generated if required). - - classes : list - A list of class names for the legend. If classes is None and a y value - is passed to fit then the classes are selected from the target vector. - Note that the curves must be computed based on what is in the target - vector passed to the ``score()`` method. Class names are used for - labeling only and must be in the correct order to prevent confusion. - - fill_area : bool, default=True + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). + + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + fill_area : bool, default: True Fill the area under the curve (or curves) with the curve color. - ap_score : bool, default=True + ap_score : bool, default: True Annotate the graph with the average precision score, a summary of the plot that is computed as the weighted mean of precisions at each threshold, with the increase in recall from the previous threshold used as the weight. - micro : bool, default=True + micro : bool, default: True If multi-class classification, draw the precision-recall curve for the micro-average of all classes. In the multi-class case, either micro or per-class must be set to True. Ignored in the binary case. - iso_f1_curves : bool, default=False + iso_f1_curves : bool, default: False Draw ISO F1-Curves on the plot to show how close the precision-recall curves are to different F1 scores. - per_class : bool, default=False + iso_f1_values : tuple , default: (0.2, 0.4, 0.6, 0.8) + Values of f1 score for which to draw ISO F1-Curves + + per_class : bool, default: False If multi-class classification, draw the precision-recall curve for each class using a OneVsRestClassifier to compute the recall on a per-class basis. In the multi-class case, either micro or per-class must be set to True. Ignored in the binary case. - fill_opacity : float, default=0.2 + fill_opacity : float, default: 0.2 Specify the alpha or opacity of the fill area (0 being transparent, and 1.0 being completly opaque). - line_opacity : float, default=0.8 + line_opacity : float, default: 0.8 Specify the alpha or opacity of the lines (0 being transparent, and 1.0 being completly opaque). + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + kwargs : dict - Keyword arguments passed to the visualization base class. + Keyword arguments passed to the visualizer base classes. Attributes ---------- @@ -126,9 +158,16 @@ class PrecisionRecallCurve(ClassificationScoreVisualizer): predictions with score >= thresholds[i] and the last element is 0. In the multiclass case, a mapping of class/metric to recall array. + classes_ : ndarray of shape (n_classes,) + The class labels observed while fitting. + + class_count_ : ndarray of shape (n_classes,) + Number of samples encountered for each class during fitting. + + + Examples + -------- - Example - ------- >>> from yellowbrick.classifier import PrecisionRecallCurve >>> from sklearn.model_selection import train_test_split >>> from sklearn.svm import LinearSVC @@ -141,13 +180,36 @@ class PrecisionRecallCurve(ClassificationScoreVisualizer): Notes ----- - .. seealso:: http://scikit-learn.org/stable/auto_examples/model_selection/plot_precision_recall.html + .. seealso:: https://bit.ly/2kOIeCC """ - def __init__(self, model, ax=None, classes=None, fill_area=True, ap_score=True, - micro=True, iso_f1_curves=False, per_class=False, fill_opacity=0.2, - line_opacity=0.8, **kwargs): - super(PrecisionRecallCurve, self).__init__(model, ax=ax, classes=classes, **kwargs) + def __init__( + self, + model, + ax=None, + classes=None, + encoder=None, + fill_area=True, + ap_score=True, + micro=True, + iso_f1_curves=False, + iso_f1_values=DEFAULT_ISO_F1_VALUES, + per_class=False, + fill_opacity=0.2, + line_opacity=0.8, + is_fitted="auto", + force_model=False, + **kwargs + ): + super(PrecisionRecallCurve, self).__init__( + model, + ax=ax, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs + ) # Set visual params self.set_params( @@ -155,6 +217,7 @@ def __init__(self, model, ax=None, classes=None, fill_area=True, ap_score=True, ap_score=ap_score, micro=micro, iso_f1_curves=iso_f1_curves, + iso_f1_values=set(iso_f1_values), per_class=per_class, fill_opacity=fill_opacity, line_opacity=line_opacity, @@ -168,29 +231,29 @@ def fit(self, X, y=None): """ # The target determines what kind of estimator is fit ttype = type_of_target(y) + self._target_labels = np.unique(y) if ttype.startswith(MULTICLASS): self.target_type_ = MULTICLASS self.estimator = OneVsRestClassifier(self.estimator) - # Use label_binarize to create multi-label ouptut for OneVsRestClassifier - Y = label_binarize(y, classes=np.unique(y)) + # Use label_binarize to create multi-label output for OneVsRestClassifier + Y = label_binarize(y, classes=self._target_labels) elif ttype.startswith(BINARY): - self.target_type_ = BINARY - # Different variable is used here to prevent transformation Y = y + self.target_type_ = BINARY else: - raise YellowbrickValueError(( - "{} does not support target type '{}', " - "please provide a binary or multiclass single-output target" - ).format( - self.__class__.__name__, ttype - )) + raise YellowbrickValueError( + ( + "{} does not support target type '{}', " + "please provide a binary or multiclass single-output target" + ).format(self.__class__.__name__, ttype) + ) # Fit the model and return self return super(PrecisionRecallCurve, self).fit(X, Y) - def score(self, X, y=None): + def score(self, X, y): """ Generates the Precision-Recall curve on the specified test data. @@ -204,11 +267,16 @@ def score(self, X, y=None): # If we don't do this check, then it is possible that OneVsRestClassifier # has not correctly been fitted for multi-class targets. if not hasattr(self, "target_type_"): - raise NotFitted(( - "{} cannot wrap an already fitted estimator" - ).format( - self.__class__.__name__ - )) + raise NotFitted.from_estimator(self, "score") + + # Must perform label binarization before calling super + if self.target_type_ == MULTICLASS: + # Use label_binarize to create multi-label output for OneVsRestClassifier + y = label_binarize(y, classes=self._target_labels) + + # Call super to check if fitted and to compute classes_ + # Note that self.score_ computed in super will be overridden below + super(PrecisionRecallCurve, self).score(X, y) # Compute the prediction/threshold scores y_scores = self._get_y_scores(X) @@ -218,21 +286,20 @@ def score(self, X, y=None): self.precision_, self.recall_, _ = sk_precision_recall_curve(y, y_scores) self.score_ = average_precision_score(y, y_scores) else: - # Use label_binarize to create multi-label ouptut for OneVsRestClassifier - Y = label_binarize(y, classes=self.classes_) - self.precision_, self.recall_, self.score_ = {}, {}, {} # Compute PRCurve for all classes for i, class_i in enumerate(self.classes_): - self.precision_[class_i], self.recall_[class_i], _ = sk_precision_recall_curve(Y[:,i], y_scores[:,i]) - self.score_[class_i] = average_precision_score(Y[:,i], y_scores[:,i]) + self.precision_[class_i], self.recall_[ + class_i + ], _ = sk_precision_recall_curve(y[:, i], y_scores[:, i]) + self.score_[class_i] = average_precision_score(y[:, i], y_scores[:, i]) # Compute micro average PR curve self.precision_[MICRO], self.recall_[MICRO], _ = sk_precision_recall_curve( - Y.ravel(), y_scores.ravel() + y.ravel(), y_scores.ravel() ) - self.score_[MICRO] = average_precision_score(Y, y_scores, average=MICRO) + self.score_[MICRO] = average_precision_score(y, y_scores, average=MICRO) # Draw the figure self.draw() @@ -247,11 +314,11 @@ def draw(self): Draws the precision-recall curves computed in score on the axes. """ if self.iso_f1_curves: - for f1 in np.linspace(0.2, 0.8, num=4): + for f1 in self.iso_f1_values: x = np.linspace(0.01, 1) y = f1 * x / (2 * x - f1) - self.ax.plot(x[y>=0], y[y>=0], color='#333333', alpha=0.2) - self.ax.annotate('$f_1={:0.1f}$'.format(f1), xy=(0.9, y[45]+0.02)) + self.ax.plot(x[y >= 0], y[y >= 0], color="#333333", alpha=0.2) + self.ax.annotate("$f_1={:0.1f}$".format(f1), xy=(0.9, y[45] + 0.02)) if self.target_type_ == BINARY: return self._draw_binary() @@ -264,7 +331,6 @@ def _draw_binary(self): self._draw_pr_curve(self.recall_, self.precision_, label="binary PR curve") self._draw_ap_score(self.score_) - def _draw_multiclass(self): """ Draw the precision-recall curves in the multiclass case @@ -290,10 +356,12 @@ def _draw_pr_curve(self, recall, precision, label=None): Helper function to draw a precision-recall curve with specified settings """ self.ax.step( - recall, precision, alpha=self.line_opacity, where='post', label=label + recall, precision, alpha=self.line_opacity, where="post", label=label ) if self.fill_area: - self.ax.fill_between(recall, precision, step='post', alpha=self.fill_opacity) + self.ax.fill_between( + recall, precision, step="post", alpha=self.fill_opacity + ) def _draw_ap_score(self, score, label=None): """ @@ -301,16 +369,14 @@ def _draw_ap_score(self, score, label=None): """ label = label or "Avg Precision={:0.2f}".format(score) if self.ap_score: - self.ax.axhline( - y=score, color="r", ls="--", label=label - ) + self.ax.axhline(y=score, color="r", ls="--", label=label) def finalize(self): """ Finalize the figure by adding titles, labels, and limits. """ self.set_title("Precision-Recall Curve for {}".format(self.name)) - self.ax.legend(loc='lower left', frameon=True) + self.ax.legend(loc="lower left", frameon=True) self.ax.set_xlim([0.0, 1.0]) self.ax.set_ylim([0.0, 1.0]) @@ -328,10 +394,7 @@ def _get_y_scores(self, X): # TODO refactor shared method with ROCAUC # Resolution order of scoring functions - attrs = ( - 'decision_function', - 'predict_proba', - ) + attrs = ("decision_function", "predict_proba") # Return the first resolved function for attr in attrs: @@ -343,7 +406,7 @@ def _get_y_scores(self, X): # Return only the positive class for binary predict_proba if self.target_type_ == BINARY and y_scores.ndim == 2: - return y_scores[:,1] + return y_scores[:, 1] return y_scores except AttributeError: @@ -353,9 +416,11 @@ def _get_y_scores(self, X): continue # If we've gotten this far, we can't do anything - raise ModelError(( - "{} requires estimators with predict_proba or decision_function methods." - ).format(self.__class__.__name__)) + raise ModelError( + ( + "{} requires an estimator with predict_proba or decision_function." + ).format(self.__class__.__name__) + ) # Alias for PrecisionRecallCurve @@ -366,31 +431,74 @@ def _get_y_scores(self, X): ## Quick Method ########################################################################## -def precision_recall_curve(model, X, y, ax=None, train_size=0.8, - random_state=None, shuffle=True, **kwargs): - """Precision-Recall Curve quick method: + +def precision_recall_curve( + model, + X, + y, + X_test=None, + y_test=None, + train_size=0.8, + random_state=None, + shuffle=True, + ax=None, + classes=None, + encoder=None, + fill_area=True, + ap_score=True, + micro=True, + iso_f1_curves=False, + iso_f1_values=DEFAULT_ISO_F1_VALUES, + per_class=False, + fill_opacity=0.2, + line_opacity=0.8, + is_fitted="auto", + force_model=False, + **kwargs +): + """Precision-Recall Curve + + Precision-Recall curves are a metric used to evaluate a classifier's quality, + particularly when classes are very imbalanced. The precision-recall curve + shows the tradeoff between precision, a measure of result relevancy, and + recall, a measure of how many relevant results are returned. A large area + under the curve represents both high recall and precision, the best case + scenario for a classifier, showing a model that returns accurate results + for the majority of classes it selects. Parameters ---------- - model : the Scikit-Learn estimator - A classification model to score the precision-recall curve on. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. X : ndarray or DataFrame of shape n x m A feature array of n instances with m features the model is trained on. - This array will be split into train and test splits. + This array will be split into train and test splits if X_test is not specified. y : ndarray or Series of length n An array or series of target or class values. This vector will be split - into train and test splits. + into train and test splits if y_test is not specified. + + X_test : ndarray or DataFrame of shape n x m + An optional feature array of n instances with m features that the model + is tested on if specified, using X as the training data. Otherwise + X will be split into train and test splits. + + y_test : ndarray or Series of length n + An array or series of target or class values that serve as actual labels for + X_test. If not specified, y will be split into test and train along with X. ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes - will be used (or generated if required). + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). train_size : float or int, default=0.8 If float, should be between 0.0 and 1.0 and represent the proportion of the dataset to include in the train split. If int, represents the - absolute number of train samples. + absolute number of train samples. Used if X_test and y_test not specified. random_state : int, RandomState, or None, optional If int, random_state is the seed used by the random number generator; @@ -401,47 +509,68 @@ def precision_recall_curve(model, X, y, ax=None, train_size=0.8, shuffle : bool, default=True Whether or not to shuffle the data before splitting. - classes : list - A list of class names for the legend. If classes is None and a y value - is passed to fit then the classes are selected from the target vector. - Note that the curves must be computed based on what is in the target - vector passed to the ``score()`` method. Class names are used for - labeling only and must be in the correct order to prevent confusion. - - fill_area : bool, default=True + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + fill_area : bool, default: True Fill the area under the curve (or curves) with the curve color. - ap_score : bool, default=True + ap_score : bool, default: True Annotate the graph with the average precision score, a summary of the plot that is computed as the weighted mean of precisions at each threshold, with the increase in recall from the previous threshold used as the weight. - micro : bool, default=True + micro : bool, default: True If multi-class classification, draw the precision-recall curve for the micro-average of all classes. In the multi-class case, either micro or per-class must be set to True. Ignored in the binary case. - iso_f1_curves : bool, default=False + iso_f1_curves : bool, default: False Draw ISO F1-Curves on the plot to show how close the precision-recall curves are to different F1 scores. - per_class : bool, default=False + iso_f1_values : tuple , default: (0.2, 0.4, 0.6, 0.8) + Values of f1 score for which to draw ISO F1-Curves + + per_class : bool, default: False If multi-class classification, draw the precision-recall curve for each class using a OneVsRestClassifier to compute the recall on a per-class basis. In the multi-class case, either micro or per-class must be set to True. Ignored in the binary case. - fill_opacity : float, default=0.2 + fill_opacity : float, default: 0.2 Specify the alpha or opacity of the fill area (0 being transparent, and 1.0 being completly opaque). - line_opacity : float, default=0.8 + line_opacity : float, default: 0.8 Specify the alpha or opacity of the lines (0 being transparent, and 1.0 being completly opaque). + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + kwargs : dict - Keyword arguments passed to the visualization base class. + Keyword arguments passed to the visualizer base classes. Returns ------- @@ -455,12 +584,43 @@ def precision_recall_curve(model, X, y, ax=None, train_size=0.8, random_state, and shuffle are specified. Note that splits are not stratified, if required, it is recommended to use the base class. """ - # Instantiate the visualizer - viz = PRCurve(model, ax=ax, **kwargs) - # Create train and test splits to validate the model - X_train, X_test, y_train, y_test = tts( - X, y, train_size=train_size, random_state=random_state, shuffle=shuffle + if (X_test is None) and (y_test is None): + # Create train and test splits to validate the model + X_train, X_test, y_train, y_test = tts( + X, y, train_size=train_size, random_state=random_state, shuffle=shuffle + ) + elif any( + [ + ((X_test is not None) and (y_test is None)), + ((X_test is None) and (y_test is not None)), + ] + ): + # exception handling in case of missing X_test or y_test + raise YellowbrickValueError( + "both X_test and y_test are required if one is specified" + ) + + else: + X_train, y_train = X, y + + # Instantiate the visualizer + viz = PRCurve( + model, + ax=ax, + classes=classes, + encoder=encoder, + fill_area=fill_area, + ap_score=ap_score, + micro=micro, + iso_f1_curves=iso_f1_curves, + iso_f1_values=iso_f1_values, + per_class=per_class, + fill_opacity=fill_opacity, + line_opacity=line_opacity, + is_fitted=is_fitted, + force_model=force_model, + **kwargs ) # Fit and transform the visualizer diff --git a/yellowbrick/classifier/rocauc.py b/yellowbrick/classifier/rocauc.py index 45eced42b..5a61e9702 100644 --- a/yellowbrick/classifier/rocauc.py +++ b/yellowbrick/classifier/rocauc.py @@ -1,12 +1,12 @@ # yellowbrick.classifier.rocauc # Implements visual ROC/AUC curves for classification evaluation. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Author: Neal Humphrey -# Created: Wed May 18 12:39:40 2016 -0400 +# Created: Tue May 03 18:15:42 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: rocauc.py [5388065] neal@nhumphrey.com $ @@ -21,15 +21,15 @@ import numpy as np -from ..exceptions import ModelError -from ..exceptions import YellowbrickValueError -from ..style.palettes import LINE_COLOR -from .base import ClassificationScoreVisualizer - from scipy import interp +from sklearn.metrics import auc, roc_curve from sklearn.preprocessing import label_binarize from sklearn.model_selection import train_test_split -from sklearn.metrics import auc, roc_curve + +from yellowbrick.exceptions import ModelError +from yellowbrick.style.palettes import LINE_COLOR +from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.classifier.base import ClassificationScoreVisualizer # Dictionary keys for ROCAUC @@ -41,6 +41,7 @@ ## ROCAUC Visualizer ########################################################################## + class ROCAUC(ClassificationScoreVisualizer): """ Receiver Operating Characteristic (ROC) curves are a measure of a @@ -51,7 +52,7 @@ class ROCAUC(ClassificationScoreVisualizer): therefore the top-left corner of the plot: false positives are zero and true positives are one. - This leads to another metric, area under the curve (AUC), a computation + This leads to another metric, area under the curve (AUC), a computation of the relationship between false positives and true positives. The higher the AUC, the better the model generally is. However, it is also important to inspect the "steepness" of the curve, as this describes the @@ -62,46 +63,74 @@ class ROCAUC(ClassificationScoreVisualizer): Parameters ---------- model : estimator - Must be a classifier, otherwise raises YellowbrickTypeError + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes - will be used (or generated if required). + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). - classes : list - A list of class names for the legend. If classes is None and a y value - is passed to fit then the classes are selected from the target vector. - Note that the curves must be computed based on what is in the target - vector passed to the ``score()`` method. Class names are used for - labeling only and must be in the correct order to prevent confusion. - - micro : bool, default = True + micro : bool, default: True Plot the micro-averages ROC curve, computed from the sum of all true positives and false positives across all classes. Micro is not defined for binary classification problems with estimators with only a decision_function method. - macro : bool, default = True + macro : bool, default: True Plot the macro-averages ROC curve, which simply takes the average of curves across all classes. Macro is not defined for binary classification problems with estimators with only a decision_function method. - per_class : bool, default = True + per_class : bool, default: True Plot the ROC curves for each individual class. This should be set to false if only the macro or micro average curves are required. Per- class classification is not defined for binary classification problems with estimators with only a decision_function method. - kwargs : keyword arguments passed to the super class. - Currently passing in hard-coded colors for the Receiver Operating - Characteristic curve and the diagonal. - These will be refactored to a default Yellowbrick style. + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. Attributes ---------- + classes_ : ndarray of shape (n_classes,) + The class labels observed while fitting. + + class_count_ : ndarray of shape (n_classes,) + Number of samples encountered for each class during fitting. + score_ : float - Global accuracy score, unless micro or macro scores are requested + An evaluation metric of the classifier on test data produced when + ``score()`` is called. This metric is between 0 and 1 -- higher scores are + generally better. For classifiers, this score is usually accuracy, but + if micro or macro is specified this returns an F1 score. Notes ----- @@ -119,7 +148,9 @@ class classification is not defined for binary classification problems ensure the best quality visualization, do not use a LabelEncoder for this and do not pass in class labels. - .. seealso:: http://scikit-learn.org/stable/auto_examples/model_selection/plot_roc.html + .. seealso:: + http://scikit-learn.org/stable/auto_examples/model_selection/plot_roc.html + .. todo:: Allow the class list to filter the curves on the visualization. Examples @@ -136,16 +167,33 @@ class classification is not defined for binary classification problems >>> oz.poof() """ - def __init__(self, model, ax=None, classes=None, - micro=True, macro=True, per_class=True, **kwargs): - super(ROCAUC, self).__init__(model, ax=ax, classes=classes, **kwargs) + def __init__( + self, + model, + ax=None, + micro=True, + macro=True, + per_class=True, + classes=None, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs + ): + super(ROCAUC, self).__init__( + model, + ax=ax, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs + ) # Set the visual parameters for ROCAUC - self.micro = micro - self.macro = macro - self.per_class = per_class + self.set_params(micro=micro, macro=macro, per_class=per_class) - def score(self, X, y=None, **kwargs): + def score(self, X, y=None): """ Generates the predicted target values using the Scikit-Learn estimator. @@ -163,6 +211,9 @@ def score(self, X, y=None, **kwargs): score_ : float Global accuracy unless micro or macro scores are requested. """ + # Call super to check if fitted and to compute self.score_ + # NOTE: this sets score to the base score if neither macro nor micro + super(ROCAUC, self).score(X, y) # Compute the predictions for the test data y_pred = self._get_y_scores(X) @@ -205,13 +256,13 @@ def score(self, X, y=None, **kwargs): self.roc_auc = dict() # If the decision is binary, compute the ROC curve and ROC area - if self._binary_decision == True: + if self._binary_decision is True: self.fpr[0], self.tpr[0], _ = roc_curve(y, y_pred) self.roc_auc[0] = auc(self.fpr[0], self.tpr[0]) else: # Otherwise compute the ROC curve and ROC area for each class for i, c in enumerate(classes): - self.fpr[i], self.tpr[i], _ = roc_curve(y, y_pred[:,i], pos_label=c) + self.fpr[i], self.tpr[i], _ = roc_curve(y, y_pred[:, i], pos_label=c) self.roc_auc[i] = auc(self.fpr[i], self.tpr[i]) # Compute micro average @@ -233,9 +284,6 @@ def score(self, X, y=None, **kwargs): if self.macro: self.score_ = self.roc_auc[MACRO] - # Set score to the base score if neither macro nor micro - self.score_ = self.estimator.score(X, y) - return self.score_ def draw(self): @@ -247,50 +295,55 @@ def draw(self): ------- ax : the axis with the plotted figure """ - colors = self.colors[0:len(self.classes_)] + colors = self.colors[0 : len(self.classes_)] n_classes = len(colors) # If it's a binary decision, plot the single ROC curve - if self._binary_decision == True: + if self._binary_decision is True: self.ax.plot( - self.fpr[0], self.tpr[0], - label='ROC for binary decision, AUC = {:0.2f}'.format( - self.roc_auc[0] - ) + self.fpr[0], + self.tpr[0], + label="ROC for binary decision, AUC = {:0.2f}".format(self.roc_auc[0]), ) # If per-class plotting is requested, plot ROC curves for each class if self.per_class: for i, color in zip(range(n_classes), colors): self.ax.plot( - self.fpr[i], self.tpr[i], color=color, - label='ROC of class {}, AUC = {:0.2f}'.format( - self.classes_[i], self.roc_auc[i], - ) + self.fpr[i], + self.tpr[i], + color=color, + label="ROC of class {}, AUC = {:0.2f}".format( + self.classes_[i], self.roc_auc[i] + ), ) # If requested, plot the ROC curve for the micro average if self.micro: self.ax.plot( - self.fpr[MICRO], self.tpr[MICRO], linestyle="--", - color= self.colors[len(self.classes_)-1], - label='micro-average ROC curve, AUC = {:0.2f}'.format( - self.roc_auc["micro"], - ) + self.fpr[MICRO], + self.tpr[MICRO], + linestyle="--", + color=self.colors[len(self.classes_) - 1], + label="micro-average ROC curve, AUC = {:0.2f}".format( + self.roc_auc["micro"] + ), ) # If requested, plot the ROC curve for the macro average if self.macro: self.ax.plot( - self.fpr[MACRO], self.tpr[MACRO], linestyle="--", - color= self.colors[len(self.classes_)-1], - label='macro-average ROC curve, AUC = {:0.2f}'.format( - self.roc_auc["macro"], - ) + self.fpr[MACRO], + self.tpr[MACRO], + linestyle="--", + color=self.colors[len(self.classes_) - 1], + label="macro-average ROC curve, AUC = {:0.2f}".format( + self.roc_auc["macro"] + ), ) # Plot the line of no discrimination to compare the curve to. - self.ax.plot([0,1], [0,1], linestyle=':', c=LINE_COLOR) + self.ax.plot([0, 1], [0, 1], linestyle=":", c=LINE_COLOR) return self.ax def finalize(self, **kwargs): @@ -304,16 +357,16 @@ def finalize(self, **kwargs): """ # Set the title and add the legend - self.set_title('ROC Curves for {}'.format(self.name)) - self.ax.legend(loc='lower right', frameon=True) + self.set_title("ROC Curves for {}".format(self.name)) + self.ax.legend(loc="lower right", frameon=True) # Set the limits for the ROC/AUC (always between 0 and 1) self.ax.set_xlim([0.0, 1.0]) self.ax.set_ylim([0.0, 1.0]) # Set x and y axis labels - self.ax.set_ylabel('True Postive Rate') - self.ax.set_xlabel('False Positive Rate') + self.ax.set_ylabel("True Postive Rate") + self.ax.set_xlabel("False Positive Rate") def _get_y_scores(self, X): """ @@ -333,10 +386,7 @@ def _get_y_scores(self, X): that is associated with y_true values. """ # The resolution order of scoring functions - attrs = ( - 'predict_proba', - 'decision_function', - ) + attrs = ("predict_proba", "decision_function") # Return the first resolved function for attr in attrs: @@ -365,7 +415,7 @@ def _score_micro_average(self, y, y_pred, classes, n_classes): # Convert y to binarized array for micro and macro scores y = label_binarize(y, classes=classes) if n_classes == 2: - y = np.hstack((1-y, y)) + y = np.hstack((1 - y, y)) # Compute micro-average self.fpr[MICRO], self.tpr[MICRO], _ = roc_curve(y.ravel(), y_pred.ravel()) @@ -396,8 +446,24 @@ def _score_macro_average(self, n_classes): ## Quick method for ROCAUC ########################################################################## -def roc_auc(model, X, y=None, ax=None, **kwargs): - """ROCAUC Quick method: + +def roc_auc( + model, + X, + y, + ax=None, + test_size=0.2, + random_state=None, + micro=True, + macro=True, + per_class=True, + classes=None, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs +): + """ROCAUC Receiver Operating Characteristic (ROC) curves are a measure of a classifier's predictive quality that compares and visualizes the tradeoff @@ -417,9 +483,11 @@ def roc_auc(model, X, y=None, ax=None, **kwargs): Parameters ---------- - model : the Scikit-Learn estimator - Should be an instance of a classifier, else the __init__ will - return an error. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. X : ndarray or DataFrame of shape n x m A matrix of n instances with m features @@ -427,33 +495,62 @@ def roc_auc(model, X, y=None, ax=None, **kwargs): y : ndarray or Series of length n An array or series of target or class values - ax : the axis to plot the figure on. + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). - classes : list - A list of class names for the legend. If classes is None and a y value - is passed to fit then the classes are selected from the target vector. - Note that the curves must be computed based on what is in the target - vector passed to the ``score()`` method. Class names are used for - labeling only and must be in the correct order to prevent confusion. + test_size : float, default=0.2 + The percentage of the data to reserve as test data. - micro : bool, default = True + random_state : int or None, default=None + The value to seed the random number generator for shuffling data. + + micro : bool, default: True Plot the micro-averages ROC curve, computed from the sum of all true positives and false positives across all classes. Micro is not defined for binary classification problems with estimators with only a decision_function method. - macro : bool, default = True + macro : bool, default: True Plot the macro-averages ROC curve, which simply takes the average of curves across all classes. Macro is not defined for binary classification problems with estimators with only a decision_function method. - per_class : bool, default = True + per_class : bool, default: True Plot the ROC curves for each individual class. This should be set to false if only the macro or micro average curves are required. Per- class classification is not defined for binary classification problems with estimators with only a decision_function method. + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. + Notes ----- ROC curves are typically used in binary classification, and in fact the @@ -470,7 +567,7 @@ class classification is not defined for binary classification problems ensure the best quality visualization, do not use a LabelEncoder for this and do not pass in class labels. - .. seealso:: http://scikit-learn.org/stable/auto_examples/model_selection/plot_roc.html + .. seealso:: https://bit.ly/2IORWO2 .. todo:: Allow the class list to filter the curves on the visualization. Examples @@ -485,19 +582,32 @@ class classification is not defined for binary classification problems Returns ------- - ax : matplotlib axes - Returns the axes that the roc-auc curve was drawn on. + viz : ROCAUC + Returns the fitted, finalized visualizer object """ # Instantiate the visualizer - visualizer = ROCAUC(model, ax, **kwargs) + visualizer = ROCAUC( + model=model, + ax=ax, + micro=micro, + macro=macro, + per_class=per_class, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs + ) # Create the train and test splits - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=test_size, random_state=random_state + ) # Fit and transform the visualizer (calls draw) visualizer.fit(X_train, y_train, **kwargs) visualizer.score(X_test, y_test) visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/classifier/threshold.py b/yellowbrick/classifier/threshold.py index 0898f8edb..68c70bf69 100644 --- a/yellowbrick/classifier/threshold.py +++ b/yellowbrick/classifier/threshold.py @@ -1,10 +1,13 @@ # yellowbrick.classifier.threshold # DiscriminationThreshold visualizer for probabilistic classifiers. # -# Author: Nathan Danielsen -# Author: Benjamin Bengfort +# Author: Nathan Danielsen +# Author: Benjamin Bengfort # Created: Wed April 26 20:17:29 2017 -0700 # +# Copyright (C) 2017 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: threshold.py [] nathan.danielsen@gmail.com $ """ @@ -15,24 +18,23 @@ ## Imports ########################################################################## -import six import bisect import numpy as np from scipy.stats import mstats from collections import defaultdict -from yellowbrick.base import ModelVisualizer -from yellowbrick.style.colors import resolve_colors -from yellowbrick.utils import is_classifier, is_probabilistic, is_monotonic -from yellowbrick.exceptions import YellowbrickTypeError, YellowbrickValueError - from sklearn.base import clone from sklearn.model_selection import ShuffleSplit from sklearn.metrics import precision_recall_curve from sklearn.utils import indexable, safe_indexing from sklearn.utils.multiclass import type_of_target +from yellowbrick.base import ModelVisualizer +from yellowbrick.style.colors import resolve_colors +from yellowbrick.utils import is_classifier, is_probabilistic, is_monotonic +from yellowbrick.exceptions import YellowbrickTypeError, YellowbrickValueError + # Quantiles for lower bound, curve, and upper bound QUANTILES_MEDIAN_80 = np.array([0.1, 0.5, 0.9]) @@ -45,6 +47,7 @@ # Discrimination Thresholds Visualization ########################################################################## + class DiscriminationThreshold(ModelVisualizer): """ Visualizes how precision, recall, f1 score, and queue rate change as the @@ -70,14 +73,15 @@ class DiscriminationThreshold(ModelVisualizer): Parameters ---------- - model : Classification Estimator - A binary classification estimator that implements ``predict_proba`` or - ``decision_function`` methods. Will raise ``TypeError`` if the model - cannot be used with the visualizer. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. ax : matplotlib Axes, default: None - The axis to plot the figure on. If None is passed in the current axes - will be used (or generated if required). + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). n_trials : integer, default: 50 Number of times to shuffle and split the dataset to account for noise @@ -128,8 +132,19 @@ class DiscriminationThreshold(ModelVisualizer): Note that if a splitter is provided, it's random state will also be updated with this random state, even if it was previously set. + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + kwargs : dict - Keyword arguments that are passed to the base visualizer class. + Keyword arguments passed to the visualizer base classes. Attributes ---------- @@ -158,16 +173,31 @@ class DiscriminationThreshold(ModelVisualizer): by Insight Data. """ - def __init__(self, model, ax=None, n_trials=50, cv=0.1, fbeta=1.0, - argmax='fscore', exclude=None, quantiles=QUANTILES_MEDIAN_80, - random_state=None, **kwargs): + def __init__( + self, + model, + ax=None, + n_trials=50, + cv=0.1, + fbeta=1.0, + argmax="fscore", + exclude=None, + quantiles=QUANTILES_MEDIAN_80, + random_state=None, + is_fitted="auto", + force_model=False, + **kwargs + ): # Perform some quick type checking to help users avoid error. - if not is_classifier(model) or not is_probabilistic(model): + if not force_model and ( + not is_classifier(model) or not is_probabilistic(model) + ): raise YellowbrickTypeError( "{} requires a probabilistic binary classifier".format( - self.__class__.__name__ - )) + self.__class__.__name__ + ) + ) # Check the various inputs self._check_quantiles(quantiles) @@ -175,15 +205,21 @@ def __init__(self, model, ax=None, n_trials=50, cv=0.1, fbeta=1.0, self._check_exclude(exclude) # Initialize the ModelVisualizer - super(DiscriminationThreshold, self).__init__(model, ax=ax, **kwargs) + super(DiscriminationThreshold, self).__init__( + model, ax=ax, is_fitted=is_fitted, **kwargs + ) # Set params self.set_params( - n_trials=n_trials, cv=cv, fbeta=fbeta, argmax=argmax, - exclude=exclude, quantiles=quantiles, random_state=random_state, + n_trials=n_trials, + cv=cv, + fbeta=fbeta, + argmax=argmax, + exclude=exclude, + quantiles=quantiles, + random_state=random_state, ) - def fit(self, X, y, **kwargs): """ Fit is the entry point for the visualizer. Given instances described @@ -213,14 +249,15 @@ def fit(self, X, y, **kwargs): If the target y is not a binary classification target. """ # Check target before metrics raise crazy exceptions - if type_of_target(y) != 'binary': + # TODO: Switch to using target type from utils.target + if type_of_target(y) != "binary": raise YellowbrickValueError("multiclass format is not supported") # Make arrays indexable for cross validation X, y = indexable(X, y) # TODO: parallelize trials with joblib (using sklearn utility) - # NOTE: parallelization with matplotlib is tricy at best! + # NOTE: parallelization with matplotlib is tricky at best! trials = [ metric for idx in range(self.n_trials) @@ -228,7 +265,7 @@ def fit(self, X, y, **kwargs): ] # Compute maximum number of uniform thresholds across all trials - n_thresholds = np.array([len(t['thresholds']) for t in trials]).min() + n_thresholds = np.array([len(t["thresholds"]) for t in trials]).min() self.thresholds_ = np.linspace(0.0, 1.0, num=n_thresholds) # Filter metrics and collect values for uniform thresholds @@ -238,7 +275,7 @@ def fit(self, X, y, **kwargs): for trial in trials: rows = defaultdict(list) for t in self.thresholds_: - idx = bisect.bisect_left(trial['thresholds'], t) + idx = bisect.bisect_left(trial["thresholds"], t) for metric in metrics: rows[metric].append(trial[metric][idx]) @@ -247,8 +284,7 @@ def fit(self, X, y, **kwargs): # Convert metrics to metric arrays uniform_metrics = { - metric: np.array(values) - for metric, values in uniform_metrics.items() + metric: np.array(values) for metric, values in uniform_metrics.items() } # Perform aggregation and store cv_scores_ @@ -257,15 +293,17 @@ def fit(self, X, y, **kwargs): for metric, values in uniform_metrics.items(): # Compute the lower, median, and upper plots - lower, median, upper = mstats.mquantiles( - values, prob=quantiles, axis=0 - ) + lower, median, upper = mstats.mquantiles(values, prob=quantiles, axis=0) # Store the aggregates in cv scores self.cv_scores_[metric] = median self.cv_scores_["{}_lower".format(metric)] = lower self.cv_scores_["{}_upper".format(metric)] = upper + # TODO: fit the underlying estimator with the best decision threshold + # Call super to ensure the underlying estimator is correctly fitted + super(DiscriminationThreshold, self).fit(X, y) + # Draw and always return self self.draw() return self @@ -298,7 +336,7 @@ def _split_fit_score_trial(self, X, y, idx=0): if hasattr(model, "predict_proba"): # Get the probabilities for the positive class - y_scores = model.predict_proba(X_test)[:,1] + y_scores = model.predict_proba(X_test)[:, 1] else: # Use the decision function to get the scores y_scores = model.decision_function(X_test) @@ -309,26 +347,24 @@ def _split_fit_score_trial(self, X, y, idx=0): # Compute the F1 score from precision and recall # Don't need to warn for F, precision/recall would have warned - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): beta = self.fbeta ** 2 - f_score = ((1 + beta) * precision * recall / - (beta * precision + recall)) + f_score = (1 + beta) * precision * recall / (beta * precision + recall) # Ensure thresholds ends at 1 thresholds = np.append(thresholds, 1) # Compute the queue rate - queue_rate = np.array([ - (y_scores >= threshold).mean() - for threshold in thresholds - ]) + queue_rate = np.array( + [(y_scores >= threshold).mean() for threshold in thresholds] + ) yield { - 'thresholds': thresholds, - 'precision': precision, - 'recall': recall, - 'fscore': f_score, - 'queue_rate': queue_rate + "thresholds": thresholds, + "precision": precision, + "recall": recall, + "fscore": f_score, + "queue_rate": queue_rate, } def draw(self): @@ -357,8 +393,7 @@ def draw(self): # Draw the metric values self.ax.plot( - self.thresholds_, self.cv_scores_[metric], - color=color, label=label + self.thresholds_, self.cv_scores_[metric], color=color, label=label ) # Draw the upper and lower bounds @@ -366,8 +401,7 @@ def draw(self): upper = self.cv_scores_["{}_upper".format(metric)] self.ax.fill_between( - self.thresholds_, upper, lower, - alpha=0.35, linewidth=0, color=color + self.thresholds_, upper, lower, alpha=0.35, linewidth=0, color=color ) # Annotate the graph with the maximizing value @@ -375,8 +409,11 @@ def draw(self): argmax = self.cv_scores_[metric].argmax() threshold = self.thresholds_[argmax] self.ax.axvline( - threshold, ls='--', c='k', lw=1, - label="$t_{}={:0.2f}$".format(metric[0], threshold) + threshold, + ls="--", + c="k", + lw=1, + label="$t_{}={:0.2f}$".format(metric[0], threshold), ) return self.ax @@ -395,9 +432,9 @@ def finalize(self, **kwargs): # Set the title of the threshold visualiztion self.set_title("Threshold Plot for {}".format(self.name)) - self.ax.legend(frameon=True, loc='best') - self.ax.set_xlabel('discrimination threshold') - self.ax.set_ylabel('score') + self.ax.legend(frameon=True, loc="best") + self.ax.set_xlabel("discrimination threshold") + self.ax.set_ylabel("score") self.ax.set_xlim(0.0, 1.0) self.ax.set_ylim(0.0, 1.0) @@ -418,21 +455,18 @@ def _check_cv(self, val, random_state=None): validation exception is raised. """ # Use default splitter in this case - if val is None: val = 0.1 + if val is None: + val = 0.1 if isinstance(val, float) and val <= 1.0: - return ShuffleSplit( - n_splits=1, test_size=val, random_state=random_state - ) + return ShuffleSplit(n_splits=1, test_size=val, random_state=random_state) if hasattr(val, "split") and hasattr(val, "get_n_splits"): if random_state is not None and hasattr(val, "random_state"): val.random_state = random_state return val - raise YellowbrickValueError( - "'{}' is not a valid cv splitter".format(type(val)) - ) + raise YellowbrickValueError("'{}' is not a valid cv splitter".format(type(val))) def _check_exclude(self, val): """ @@ -440,7 +474,7 @@ def _check_exclude(self, val): """ if val is None: exclude = frozenset() - elif isinstance(val, six.string_types): + elif isinstance(val, str): exclude = frozenset([val.lower()]) else: exclude = frozenset(map(lambda s: s.lower(), val)) @@ -457,11 +491,24 @@ def _check_exclude(self, val): # Quick Methods ########################################################################## -def discrimination_threshold(model, X, y, ax=None, n_trials=50, cv=0.1, - fbeta=1.0, argmax='fscore', exclude=None, - quantiles=QUANTILES_MEDIAN_80, random_state=None, - **kwargs): - """Quick method for DiscriminationThreshold. + +def discrimination_threshold( + model, + X, + y, + ax=None, + n_trials=50, + cv=0.1, + fbeta=1.0, + argmax="fscore", + exclude=None, + quantiles=QUANTILES_MEDIAN_80, + random_state=None, + is_fitted="auto", + force_model=False, + **kwargs +): + """Discrimination Threshold: Visualizes how precision, recall, f1 score, and queue rate change as the discrimination threshold increases. For probabilistic, binary classifiers, @@ -475,10 +522,11 @@ def discrimination_threshold(model, X, y, ax=None, n_trials=50, cv=0.1, Parameters ---------- - model : Classification Estimator - A binary classification estimator that implements ``predict_proba`` or - ``decision_function`` methods. Will raise ``TypeError`` if the model - cannot be used with the visualizer. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. X : ndarray or DataFrame of shape n x m A matrix of n instances with m features @@ -488,8 +536,8 @@ def discrimination_threshold(model, X, y, ax=None, n_trials=50, cv=0.1, be a binary classification target. ax : matplotlib Axes, default: None - The axis to plot the figure on. If None is passed in the current axes - will be used (or generated if required). + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). n_trials : integer, default: 50 Number of times to shuffle and split the dataset to account for noise @@ -540,24 +588,44 @@ def discrimination_threshold(model, X, y, ax=None, n_trials=50, cv=0.1, Note that if a splitter is provided, it's random state will also be updated with this random state, even if it was previously set. + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + kwargs : dict - Keyword arguments that are passed to the base visualizer class. + Keyword arguments passed to the visualizer base classes. Returns ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. + viz : DiscriminationThreshold + Returns the fitted and finalized visualizer object. """ # Instantiate the visualizer visualizer = DiscriminationThreshold( - model, ax=ax, n_trials=n_trials, cv=cv, fbeta=fbeta, argmax=argmax, - exclude=exclude, quantiles=quantiles, random_state=random_state, + model, + ax=ax, + n_trials=n_trials, + cv=cv, + fbeta=fbeta, + argmax=argmax, + exclude=exclude, + quantiles=quantiles, + random_state=random_state, + is_fitted=is_fitted, + force_model=force_model, **kwargs ) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y) - visualizer.poof() + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/cluster/__init__.py b/yellowbrick/cluster/__init__.py index e783eb6ad..6672c1bc7 100644 --- a/yellowbrick/cluster/__init__.py +++ b/yellowbrick/cluster/__init__.py @@ -1,10 +1,10 @@ # yellowbrick.cluster # Visualizers for Cluster analysis and diagnostics # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu Mar 23 17:26:57 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [241edca] benjamin@bengfort.com $ @@ -22,4 +22,4 @@ from .base import * from .elbow import * from .silhouette import * -from .icdm import * +from .icdm import * diff --git a/yellowbrick/cluster/base.py b/yellowbrick/cluster/base.py index 1ae1dce6a..e183eef31 100644 --- a/yellowbrick/cluster/base.py +++ b/yellowbrick/cluster/base.py @@ -1,10 +1,10 @@ # yellowbrick.cluster.base # Base class for cluster visualizers. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu Mar 23 17:28:38 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: base.py [241edca] benjamin@bengfort.com $ @@ -17,21 +17,20 @@ ## Imports ########################################################################## -from ..utils import isclusterer -from ..base import ScoreVisualizer -from ..exceptions import YellowbrickTypeError +from yellowbrick.utils import isclusterer +from yellowbrick.base import ScoreVisualizer +from yellowbrick.exceptions import YellowbrickTypeError ## Packages for export -__all__ = [ - "ClusteringScoreVisualizer", -] +__all__ = ["ClusteringScoreVisualizer"] ########################################################################## ## Clustering Score Visualization Base Object ########################################################################## + class ClusteringScoreVisualizer(ScoreVisualizer): """ Base class for all ScoreVisualizers that evaluate a clustering estimator. @@ -41,11 +40,11 @@ class ClusteringScoreVisualizer(ScoreVisualizer): ``YellowbrickTypeError`` exception is raised. """ - def __init__(self, model, ax=None, **kwargs): - if not isclusterer(model): + def __init__(self, model, ax=None, fig=None, force_model=False, **kwargs): + if not force_model and not isclusterer(model): raise YellowbrickTypeError( "The supplied model is not a clustering estimator; try a " "classifier or regression score visualizer instead!" ) - - super(ClusteringScoreVisualizer, self).__init__(model, ax=ax, **kwargs) + self.force_model = force_model + super(ClusteringScoreVisualizer, self).__init__(model, ax=ax, fig=fig, **kwargs) diff --git a/yellowbrick/cluster/elbow.py b/yellowbrick/cluster/elbow.py index d08de7c29..bee510b00 100644 --- a/yellowbrick/cluster/elbow.py +++ b/yellowbrick/cluster/elbow.py @@ -1,10 +1,10 @@ # yellowbrick.cluster.elbow # Implements the elbow method for determining the optimal number of clusters. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu Mar 23 22:36:31 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: elbow.py [5a370c8] benjamin@bengfort.com $ @@ -18,31 +18,37 @@ ## Imports ########################################################################## -import collections import time +import warnings import numpy as np import scipy.sparse as sp - -from .base import ClusteringScoreVisualizer -from ..exceptions import YellowbrickValueError +from collections.abc import Iterable from sklearn.metrics import silhouette_score -from sklearn.metrics import calinski_harabaz_score -from sklearn.metrics.pairwise import pairwise_distances from sklearn.preprocessing import LabelEncoder +from sklearn.metrics.pairwise import pairwise_distances + +from yellowbrick.utils import KneeLocator +from yellowbrick.style.palettes import LINE_COLOR +from yellowbrick.cluster.base import ClusteringScoreVisualizer +from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning + +try: + from sklearn.metrics import calinski_harabasz_score as chs +except ImportError: + from sklearn.metrics import calinski_harabaz_score as chs ## Packages for export -__all__ = [ - "KElbowVisualizer", "distortion_score" -] +__all__ = ["KElbowVisualizer", "KElbow", "distortion_score", "kelbow_visualizer"] ########################################################################## ## Metrics ########################################################################## -def distortion_score(X, labels, metric='euclidean'): + +def distortion_score(X, labels, metric="euclidean"): """ Compute the mean distortion of all samples. @@ -65,7 +71,7 @@ def distortion_score(X, labels, metric='euclidean'): The metric to use when calculating distance between instances in a feature array. If metric is a string, it must be one of the options allowed by `sklearn.metrics.pairwise.pairwise_distances - `_ + `_ .. todo:: add sample_size and random_state kwds similar to silhouette_score """ @@ -98,8 +104,8 @@ def distortion_score(X, labels, metric='euclidean'): distances = pairwise_distances(instances, center, metric=metric) distances = distances ** 2 - # Add the mean square distance to the distortion - distortion += distances.mean() + # Add the sum of square distance to the distortion + distortion += distances.sum() return distortion @@ -111,7 +117,7 @@ def distortion_score(X, labels, metric='euclidean'): KELBOW_SCOREMAP = { "distortion": distortion_score, "silhouette": silhouette_score, - "calinski_harabaz": calinski_harabaz_score, + "calinski_harabasz": chs, } @@ -129,12 +135,12 @@ class KElbowVisualizer(ClusteringScoreVisualizer): average score for all clusters. By default, the ``distortion`` score is computed, the sum of square distances from each point to its assigned center. Other metrics can also be used such as the ``silhouette`` score, - the mean silhouette coefficient for all samples or the - ``calinski_harabaz`` score, which computes the ratio of dispersion between + the mean silhouette coefficient for all samples or the + ``calinski_harabasz`` score, which computes the ratio of dispersion between and within clusters. When these overall metrics for each model are plotted, it is possible to - visually determine the best value for K. If the line chart looks like an + visually determine the best value for k. If the line chart looks like an arm, then the "elbow" (the point of inflection on the curve) is the best value of k. The "arm" can be either up or down, but if there is a strong inflection point, it is a good indication that the underlying model fits @@ -143,8 +149,8 @@ class KElbowVisualizer(ClusteringScoreVisualizer): Parameters ---------- - model : a Scikit-Learn clusterer - Should be an instance of a clusterer, specifically ``KMeans`` or + model : a scikit-learn clusterer + Should be an instance of an unfitted clusterer, specifically ``KMeans`` or ``MiniBatchKMeans``. If it is not a clusterer, an exception is raised. ax : matplotlib Axes, default: None @@ -164,16 +170,37 @@ class KElbowVisualizer(ClusteringScoreVisualizer): - **distortion**: mean sum of squared distances to centers - **silhouette**: mean ratio of intra-cluster and nearest-cluster distance - - **calinski_harabaz**: ratio of within to between cluster dispersion + - **calinski_harabasz**: ratio of within to between cluster dispersion timings : bool, default: True Display the fitting time per k to evaluate the amount of time required to train the clustering model. + locate_elbow : bool, default: True + Automatically find the "elbow" or "knee" which likely corresponds to the optimal + value of k using the "knee point detection algorithm". The knee point detection + algorithm finds the point of maximum curvature, which in a well-behaved + clustering problem also represents the pivot of the elbow curve. The point is + labeled with a dashed line and annotated with the score and k values. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ---------- + k_scores_ : array of shape (n,) where n is no. of k values + The silhouette score corresponding to each k value. + + k_timers_ : array of shape (n,) where n is no. of k values + The time taken to fit n KMeans model corresponding to each k value. + + elbow_value_ : integer + The optimal value of k. + + elbow_score_ : float + The silhouette score corresponding to the optimal value of k. + Examples -------- @@ -193,60 +220,88 @@ class KElbowVisualizer(ClusteringScoreVisualizer): SSE, also can be used to explore if clustering is a correct choice. For a discussion on the Elbow method, read more at - `Robert Gove's Block `_. - + `Robert Gove's Block website `_. + For more on the knee point detection algorithm see the paper `"Finding a "kneedle" + in a Haystack" `_. + .. seealso:: The scikit-learn documentation for the `silhouette_score - `_ and `calinski_harabaz_score - `_. The default, `distortion_score`, is - implemented in`yellowbrick.cluster.elbow`. + `_ and `calinski_harabasz_score + `_. The default, ``distortion_score``, is + implemented in ``yellowbrick.cluster.elbow``. .. todo:: add parallelization option for performance .. todo:: add different metrics for scores and silhouette .. todo:: add timing information about how long it's taking """ - def __init__(self, model, ax=None, k=10, - metric="distortion", timings=True, **kwargs): + def __init__( + self, + model, + ax=None, + k=10, + metric="distortion", + timings=True, + locate_elbow=True, + **kwargs + ): super(KElbowVisualizer, self).__init__(model, ax=ax, **kwargs) # Get the scoring method if metric not in KELBOW_SCOREMAP: raise YellowbrickValueError( "'{}' is not a defined metric " - "use one of distortion, silhouette, or calinski_harabaz" + "use one of distortion, silhouette, or calinski_harabasz" ) # Store the arguments self.scoring_metric = KELBOW_SCOREMAP[metric] + self.metric = metric self.timings = timings + self.locate_elbow = locate_elbow # Convert K into a tuple argument if an integer if isinstance(k, int): - self.k_values_ = list(range(2, k+1)) - elif isinstance(k, tuple) and len(k) == 2 and \ - all(isinstance(x, (int, np.integer)) for x in k): + self.k_values_ = list(range(2, k + 1)) + elif ( + isinstance(k, tuple) + and len(k) == 2 + and all(isinstance(x, (int, np.integer)) for x in k) + ): self.k_values_ = list(range(*k)) - elif isinstance(k, collections.Iterable) and \ - all(isinstance(x, (int, np.integer)) for x in k): + elif isinstance(k, Iterable) and all( + isinstance(x, (int, np.integer)) for x in k + ): self.k_values_ = list(k) else: - raise YellowbrickValueError(( - "Specify an iterable of integers, a range, or maximal K value," - " the value '{}' is not a valid argument for K.".format(k) - )) + raise YellowbrickValueError( + ( + "Specify an iterable of integers, a range, or maximal K value," + " the value '{}' is not a valid argument for K.".format(k) + ) + ) # Holds the values of the silhoutte scores self.k_scores_ = None + # Set Default Elbow Value + self.elbow_value_ = None def fit(self, X, y=None, **kwargs): """ Fits n KMeans models where n is the length of ``self.k_values_``, - storing the silhoutte scores in the ``self.k_scores_`` attribute. + storing the silhouette scores in the ``self.k_scores_`` attribute. + The "elbow" and silhouette score corresponding to it are stored in + ``self.elbow_value`` and ``self.elbow_score`` respectively. This method finishes up by calling draw to create the plot. """ self.k_scores_ = [] self.k_timers_ = [] + self.kneedle = None + self.knee_value = None + + if self.locate_elbow: + self.elbow_value_ = None + self.elbow_score_ = None for k in self.k_values_: # Compute the start time for each model @@ -258,9 +313,39 @@ def fit(self, X, y=None, **kwargs): # Append the time and score to our plottable metrics self.k_timers_.append(time.time() - start) - self.k_scores_.append( - self.scoring_metric(X, self.estimator.labels_) + self.k_scores_.append(self.scoring_metric(X, self.estimator.labels_)) + + if self.locate_elbow: + locator_kwargs = { + "distortion": { + "curve_nature": "convex", + "curve_direction": "decreasing", + }, + "silhouette": { + "curve_nature": "concave", + "curve_direction": "increasing", + }, + "calinski_harabasz": { + "curve_nature": "concave", + "curve_direction": "increasing", + }, + }.get(self.metric, {}) + elbow_locator = KneeLocator( + self.k_values_, self.k_scores_, **locator_kwargs ) + if elbow_locator.knee is None: + self.elbow_value_ = None + self.elbow_score_ = 0 + warning_message = ( + "No 'knee' or 'elbow' point detected, " + "pass `locate_elbow=False` to remove the warning" + ) + warnings.warn(warning_message, YellowbrickWarning) + else: + self.elbow_value_ = elbow_locator.knee + self.elbow_score_ = self.k_scores_[ + self.k_values_.index(self.elbow_value_) + ] self.draw() @@ -271,14 +356,26 @@ def draw(self): Draw the elbow curve for the specified scores and values of K. """ # Plot the silhouette score against k - self.ax.plot(self.k_values_, self.k_scores_, marker="D", label="score") + self.ax.plot(self.k_values_, self.k_scores_, marker="D") + if self.locate_elbow is True and self.elbow_value_ is not None: + elbow_label = "$elbow at k={}, score={:0.3f}$".format( + self.elbow_value_, self.elbow_score_ + ) + self.ax.axvline( + self.elbow_value_, c=LINE_COLOR, linestyle="--", label=elbow_label + ) # If we're going to plot the timings, create a twinx axis if self.timings: self.axes = [self.ax, self.ax.twinx()] self.axes[1].plot( - self.k_values_, self.k_timers_, label="fit time", - c='g', marker="o", linestyle="--", alpha=0.75, + self.k_values_, + self.k_timers_, + label="fit time", + c="g", + marker="o", + linestyle="--", + alpha=0.75, ) return self.ax @@ -287,21 +384,105 @@ def finalize(self): """ Prepare the figure for rendering by setting the title as well as the X and Y axis labels and adding the legend. + """ # Get the metric name metric = self.scoring_metric.__name__.replace("_", " ").title() # Set the title - self.set_title( - '{} Elbow for {} Clustering'.format(metric, self.name) - ) + self.set_title("{} Elbow for {} Clustering".format(metric, self.name)) # Set the x and y labels - self.ax.set_xlabel('k') + self.ax.set_xlabel("k") self.ax.set_ylabel(metric.lower()) + # set the legend if locate_elbow=True + if self.locate_elbow is True and self.elbow_value_ is not None: + self.ax.legend(loc="best", fontsize="medium") + # Set the second y axis labels if self.timings: self.axes[1].grid(False) - self.axes[1].set_ylabel("fit time (seconds)", color='g') - self.axes[1].tick_params('y', colors='g') + self.axes[1].set_ylabel("fit time (seconds)", color="g") + self.axes[1].tick_params("y", colors="g") + + +# alias +KElbow = KElbowVisualizer + + +########################################################################## +## Quick Method +########################################################################## + + +def kelbow_visualizer( + model, + X, + y=None, + k=10, + ax=None, + timings=True, + locate_elbow=True, + metric="distortion", + **kwargs +): + """ + Quick Method: + + model : a Scikit-Learn clusterer + Should be an instance of an unfitted clusterer, specifically ``KMeans`` or + ``MiniBatchKMeans``. If it is not a clusterer, an exception is raised. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes + will be used (or generated if required). + + k : integer, tuple, or iterable + The k values to compute silhouette scores for. If a single integer + is specified, then will compute the range (2,k). If a tuple of 2 + integers is specified, then k will be in np.arange(k[0], k[1]). + Otherwise, specify an iterable of integers to use as values for k. + + metric : string, default: ``"distortion"`` + Select the scoring metric to evaluate the clusters. The default is the + mean distortion, defined by the sum of squared distances between each + observation and its closest centroid. Other metrics include: + + - **distortion**: mean sum of squared distances to centers + - **silhouette**: mean ratio of intra-cluster and nearest-cluster distance + - **calinski_harabasz**: ratio of within to between cluster dispersion + + timings : bool, default: True + Display the fitting time per k to evaluate the amount of time required + to train the clustering model. + + locate_elbow : bool, default: True + Automatically find the "elbow" or "knee" which likely corresponds to the optimal + value of k using the "knee point detection algorithm". The knee point detection + algorithm finds the point of maximum curvature, which in a well-behaved + clustering problem also represents the pivot of the elbow curve. The point is + labeled with a dashed line and annotated with the score and k values. + + kwargs : dict + Keyword arguments that are passed to the base class and may influence + the visualization as defined in other Visualizers. + + Returns + ------- + viz : KElbowVisualizer + The kelbow visualizer, fitted and finalized. + """ + oz = KElbow( + model, + ax=ax, + k=k, + metric=metric, + timings=timings, + locate_elbow=locate_elbow, + **kwargs + ) + + oz.fit(X, y) + oz.finalize() + return oz diff --git a/yellowbrick/cluster/icdm.py b/yellowbrick/cluster/icdm.py index f24d580ef..7b007845f 100644 --- a/yellowbrick/cluster/icdm.py +++ b/yellowbrick/cluster/icdm.py @@ -1,10 +1,13 @@ # yellowbrick.cluster.icdm # Implements Intercluster Distance Map visualizations. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Aug 21 11:56:53 2018 -0400 # -# ID: icdm.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: icdm.py [2f23976] benjamin@bengfort.com $ """ Implements Intercluster Distance Map visualizations. @@ -20,12 +23,11 @@ from matplotlib.patches import Circle from sklearn.manifold import MDS, TSNE -from .base import ClusteringScoreVisualizer - -from ..utils.timer import Timer -from ..utils.decorators import memoized -from ..utils.helpers import prop_to_size -from ..exceptions import YellowbrickValueError +from yellowbrick.utils.timer import Timer +from yellowbrick.utils.decorators import memoized +from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.cluster.base import ClusteringScoreVisualizer +from yellowbrick.utils.helpers import prop_to_size, check_fitted try: # Only available in Matplotlib >= 2.0.2 @@ -36,22 +38,26 @@ ## Packages for export __all__ = [ - "InterclusterDistance", "intercluster_distance", - "VALID_EMBEDDING", "VALID_SCORING", + "InterclusterDistance", + "intercluster_distance", + "VALID_EMBEDDING", + "VALID_SCORING", + "ICDM", ] # Valid strings to use for embedding names -VALID_EMBEDDING = {'mds', 'tsne'} +VALID_EMBEDDING = {"mds", "tsne"} # Valid strings to use for scoring names -VALID_SCORING = {'membership',} +VALID_SCORING = {"membership"} ########################################################################## ## InterclusterDistance Visualizer ########################################################################## + class InterclusterDistance(ClusteringScoreVisualizer): """ Intercluster distance maps display an embedding of the cluster centers in @@ -69,7 +75,9 @@ class InterclusterDistance(ClusteringScoreVisualizer): Should be an instance of a centroidal clustering algorithm (or a hierarchical algorithm with a specified number of clusters). Also accepts some other models like LDA for text clustering. - If it is not a clusterer, an exception is raised. + If it is not a clusterer, an exception is raised. If the estimator + is not fitted, it is fit when the visualizer is fitted, unless + otherwise specified by ``is_fitted``. ax : matplotlib Axes, default: None The axes to plot the figure on. If None is passed in the current axes @@ -116,6 +124,12 @@ class InterclusterDistance(ClusteringScoreVisualizer): random_state : int or RandomState, default: None Fixes the random state for stochastic embedding algorithms. + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments passed to the base class and may influence the feature visualization properties. @@ -136,7 +150,7 @@ class InterclusterDistance(ClusteringScoreVisualizer): Notes ----- - Currently the only two embeddings supportted are MDS and TSNE. Soon to + Currently the only two embeddings supported are MDS and TSNE. Soon to follow will be PCoA and a customized version of PCoA for LDA. The only supported scoring metric is membership, but in the future, silhouette scores and cluster diameter will be added. @@ -147,11 +161,21 @@ class InterclusterDistance(ClusteringScoreVisualizer): clusterers that have ``n_components`` and LDA. """ - - def __init__(self, model, ax=None, min_size=400, max_size=25000, - embedding='mds', scoring='membership', - legend=True, legend_loc="lower left", legend_size=1.5, - random_state=None, **kwargs): + def __init__( + self, + model, + ax=None, + min_size=400, + max_size=25000, + embedding="mds", + scoring="membership", + legend=True, + legend_loc="lower left", + legend_size=1.5, + random_state=None, + is_fitted="auto", + **kwargs + ): # Initialize the visualizer bases super(InterclusterDistance, self).__init__(model, ax=ax, **kwargs) @@ -160,14 +184,14 @@ def __init__(self, model, ax=None, min_size=400, max_size=25000, validate_scoring(scoring) # Set decomposition properties - self.embedding = embedding self.scoring = scoring + self.embedding = embedding self.random_state = random_state # Set visual properties + self.legend = legend self.min_size = min_size self.max_size = max_size - self.legend = legend self.legend_loc = legend_loc self.legend_size = legend_size @@ -178,7 +202,7 @@ def __init__(self, model, ax=None, min_size=400, max_size=25000, self.edgecolor = "#2e719399" if self.legend: - self.lax # If legend True, test the version availability + self.lax # If legend True, test the version availability @memoized def lax(self): @@ -188,20 +212,25 @@ def lax(self): is mostly invisible). The legend can then be drawn on this axes. """ if inset_locator is None: - raise YellowbrickValueError(( - "intercluster distance map legend requires matplotlib 2.0.2 or greater " - "please upgrade matplotlib or set legend=False on the visualizer" - )) + raise YellowbrickValueError( + ( + "intercluster distance map legend requires matplotlib 2.0.2 or " + "later please upgrade matplotlib or set legend=False " + ) + ) lax = inset_locator.inset_axes( - self.ax, width=self.legend_size, height=self.legend_size, loc=self.legend_loc + self.ax, + width=self.legend_size, + height=self.legend_size, + loc=self.legend_loc, ) lax.set_frame_on(False) lax.set_facecolor("none") lax.grid(False) - lax.set_xlim(-1.4,1.4) - lax.set_ylim(-1.4,1.4) + lax.set_xlim(-1.4, 1.4) + lax.set_ylim(-1.4, 1.4) lax.set_xticks([]) lax.set_yticks([]) @@ -216,12 +245,12 @@ def transformer(self): Creates the internal transformer that maps the cluster center's high dimensional space to its two dimensional space. """ - ttype = self.embedding.lower() # transformer method type + ttype = self.embedding.lower() # transformer method type - if ttype == 'mds': + if ttype == "mds": return MDS(n_components=2, random_state=self.random_state) - if ttype == 'tsne': + if ttype == "tsne": return TSNE(n_components=2, random_state=self.random_state) raise YellowbrickValueError("unknown embedding '{}'".format(ttype)) @@ -235,7 +264,7 @@ def cluster_centers_(self): maintained. """ # TODO: Handle agglomerative clustering and LDA - for attr in ('cluster_centers_',): + for attr in ("cluster_centers_",): try: return getattr(self.estimator, attr) except AttributeError: @@ -243,8 +272,9 @@ def cluster_centers_(self): raise AttributeError( "could not find or make cluster_centers_ for {}".format( - self.estimator.__class__.__name__ - )) + self.estimator.__class__.__name__ + ) + ) def fit(self, X, y=None): """ @@ -252,16 +282,17 @@ def fit(self, X, y=None): into 2D space using the embedding method specified. """ with Timer() as self.fit_time_: - # Fit the underlying estimator - self.estimator.fit(X, y) + if not check_fitted(self.estimator, is_fitted_by=self.is_fitted): + # Fit the underlying estimator + self.estimator.fit(X, y) - # Get the centers - # TODO: is this how sklearn stores all centers in the model? - C = self.cluster_centers_ + # Get the centers + # TODO: is this how sklearn stores all centers in the model? + C = self.cluster_centers_ - # Embed the centers in 2D space and get the cluster scores - self.embedded_centers_ = self.transformer.fit_transform(C) - self.scores_ = self._score_clusters(X, y) + # Embed the centers in 2D space and get the cluster scores + self.embedded_centers_ = self.transformer.fit_transform(C) + self.scores_ = self._score_clusters(X, y) # Draw the clusters and fit returns self self.draw() @@ -276,8 +307,12 @@ def draw(self): # Draw the scatter plots with associated sizes on the graph self.ax.scatter( - self.embedded_centers_[:,0], self.embedded_centers_[:,1], - s=sizes, c=self.facecolor, edgecolor=self.edgecolor, linewidth=1, + self.embedded_centers_[:, 0], + self.embedded_centers_[:, 1], + s=sizes, + c=self.facecolor, + edgecolor=self.edgecolor, + linewidth=1, ) # Annotate the clusters with their labels @@ -298,9 +333,11 @@ def finalize(self): sizes if required. """ # Set the default title if a user hasn't supplied one - self.set_title("{} Intercluster Distance Map (via {})".format( - self.estimator.__class__.__name__, self.embedding.upper() - )) + self.set_title( + "{} Intercluster Distance Map (via {})".format( + self.estimator.__class__.__name__, self.embedding.upper() + ) + ) # Create the origin grid and minimalist display self.ax.set_xticks([0]) @@ -315,14 +352,12 @@ def finalize(self): if self.legend: self._make_size_legend() - return self.ax - def _score_clusters(self, X, y=None): """ Determines the "scores" of the cluster, the metric that determines the size of the cluster visualized on the visualization. """ - stype = self.scoring.lower() # scoring method name + stype = self.scoring.lower() # scoring method name if stype == "membership": return np.bincount(self.estimator.labels_) @@ -349,48 +384,56 @@ def _make_size_legend(self): # radius of the markers. areas = self._get_cluster_sizes() radii = np.sqrt(areas / np.pi) - scaled = np.interp(radii, (radii.min(), radii.max()), (.1, 1)) + scaled = np.interp(radii, (radii.min(), radii.max()), (0.1, 1)) # Compute the locations of the 25th, 50th, and 75th percentile scores - indices = np.array([ - percentile_index(self.scores_, p) for p in (25, 50, 75) - ]) + indices = np.array([percentile_index(self.scores_, p) for p in (25, 50, 75)]) # Draw size circles annotated with the percentile score as the legend. for idx in indices: # TODO: should the size circle's center be hard coded like this? - center = (-0.30, 1-scaled[idx]) + center = (-0.30, 1 - scaled[idx]) c = Circle( - center, scaled[idx], facecolor="none", edgecolor="#2e7193", - linewidth=1.5, linestyle="--" + center, + scaled[idx], + facecolor="none", + edgecolor="#2e7193", + linewidth=1.5, + linestyle="--", ) self.lax.add_patch(c) # Add annotation to the size circle with the value of the score self.lax.annotate( - self.scores_[idx], (-0.30, 1-(2*scaled[idx])), xytext=(1, 1-(2*scaled[idx])), - arrowprops=dict(arrowstyle="wedge", color="#2e7193"), va='center', ha='center', + self.scores_[idx], + (-0.30, 1 - (2 * scaled[idx])), + xytext=(1, 1 - (2 * scaled[idx])), + arrowprops=dict(arrowstyle="wedge", color="#2e7193"), + va="center", + ha="center", ) # Draw size legend title - self.lax.text(s="membership", x=0, y=1.2, va='center', ha='center') + self.lax.text(s="membership", x=0, y=1.2, va="center", ha="center") # Ensure the current axes is always the main axes after modifying the # inset axes and while drawing. plt.sca(self.ax) +# alias +ICDM = InterclusterDistance + ########################################################################## ## Helper Methods ########################################################################## + def percentile_index(a, q): """ Returns the index of the value at the Qth percentile in array a. """ - return np.where( - a==np.percentile(a, q, interpolation='nearest') - )[0][0] + return np.where(a == np.percentile(a, q, interpolation="nearest"))[0][0] def validate_string_param(s, valid, param_name="param"): @@ -400,9 +443,7 @@ def validate_string_param(s, valid, param_name="param"): """ if s.lower() not in valid: raise YellowbrickValueError( - "unknown {} '{}', chose from '{}'".format( - param_name, s, ", ".join(valid) - ) + "unknown {} '{}', chose from '{}'".format(param_name, s, ", ".join(valid)) ) @@ -424,13 +465,24 @@ def validate_scoring(param): ## Quick Method ########################################################################## -def intercluster_distance(model, X, y=None, ax=None, - min_size=400, max_size=25000, - embedding='mds', scoring='membership', - legend=True, legend_loc="lower left", legend_size=1.5, - random_state=None, **kwargs): - """Quick Method: +def intercluster_distance( + model, + X, + y=None, + ax=None, + min_size=400, + max_size=25000, + embedding="mds", + scoring="membership", + legend=True, + legend_loc="lower left", + legend_size=1.5, + random_state=None, + is_fitted="auto", + **kwargs +): + """Quick Method: Intercluster distance maps display an embedding of the cluster centers in 2 dimensions with the distance to other centers preserved. E.g. the closer to centers are in the visualization, the closer they are in the original @@ -446,7 +498,9 @@ def intercluster_distance(model, X, y=None, ax=None, Should be an instance of a centroidal clustering algorithm (or a hierarchical algorithm with a specified number of clusters). Also accepts some other models like LDA for text clustering. - If it is not a clusterer, an exception is raised. + If it is not a clusterer, an exception is raised. If the estimator + is not fitted, it is fit when the visualizer is fitted, unless + otherwise specified by ``is_fitted``. X : array-like of shape (n, m) A matrix or data frame with n instances and m features @@ -499,6 +553,12 @@ def intercluster_distance(model, X, y=None, ax=None, random_state : int or RandomState, default: None Fixes the random state for stochastic embedding algorithms. + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments passed to the base class and may influence the feature visualization properties. @@ -509,11 +569,20 @@ def intercluster_distance(model, X, y=None, ax=None, The intercluster distance visualizer, fitted and finalized. """ oz = InterclusterDistance( - model, ax=ax, min_size=min_size, max_size=max_size, embedding=embedding, - scoring=scoring, legend=legend, legend_loc=legend_loc, legend_size=legend_size, - random_state=random_state, **kwargs + model, + ax=ax, + min_size=min_size, + max_size=max_size, + embedding=embedding, + scoring=scoring, + legend=legend, + legend_loc=legend_loc, + legend_size=legend_size, + random_state=random_state, + is_fitted=is_fitted, + **kwargs ) oz.fit(X, y) - oz.poof() + oz.finalize() return oz diff --git a/yellowbrick/cluster/silhouette.py b/yellowbrick/cluster/silhouette.py index b8d7199e7..d37f82a69 100644 --- a/yellowbrick/cluster/silhouette.py +++ b/yellowbrick/cluster/silhouette.py @@ -1,10 +1,11 @@ # yellowbrick.cluster.silhouette # Implements visualizers using the silhouette metric for cluster evaluation. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Mon Mar 27 10:09:24 2017 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: silhouette.py [57b563b] benjamin@bengfort.com $ @@ -20,22 +21,21 @@ import numpy as np import matplotlib.ticker as ticker -from ..style import color_palette -from .base import ClusteringScoreVisualizer - from sklearn.metrics import silhouette_score, silhouette_samples +from yellowbrick.utils import check_fitted +from yellowbrick.style import resolve_colors +from yellowbrick.cluster.base import ClusteringScoreVisualizer ## Packages for export -__all__ = [ - "SilhouetteVisualizer" -] +__all__ = ["SilhouetteVisualizer", "silhouette_visualizer"] ########################################################################## ## Silhouette Method for K Selection ########################################################################## + class SilhouetteVisualizer(ClusteringScoreVisualizer): """ The Silhouette Visualizer displays the silhouette coefficient for each @@ -60,12 +60,24 @@ class SilhouetteVisualizer(ClusteringScoreVisualizer): ---------- model : a Scikit-Learn clusterer Should be an instance of a centroidal clustering algorithm (``KMeans`` - or ``MiniBatchKMeans``). + or ``MiniBatchKMeans``). If the estimator is not fitted, it is fit when + the visualizer is fitted, unless otherwise specified by ``is_fitted``. ax : matplotlib Axes, default: None The axes to plot the figure on. If None is passed in the current axes will be used (or generated if required). + colors : iterable or string, default: None + A collection of colors to use for each cluster group. If there are + fewer colors than cluster groups, colors will repeat. May also be a + Yellowbrick or matplotlib colormap string. + + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. @@ -87,6 +99,9 @@ class SilhouetteVisualizer(ClusteringScoreVisualizer): Number of clusters (e.g. n_clusters or k value) passed to internal scikit-learn model. + y_tick_pos_ : array of shape (n_clusters,) + The computed center positions of each cluster on the y-axis + Examples -------- @@ -97,13 +112,18 @@ class SilhouetteVisualizer(ClusteringScoreVisualizer): >>> model.poof() """ - def __init__(self, model, ax=None, **kwargs): + def __init__(self, model, ax=None, colors=None, is_fitted="auto", **kwargs): + + # Initialize the visualizer bases super(SilhouetteVisualizer, self).__init__(model, ax=ax, **kwargs) # Visual Properties - # TODO: Fix the color handling - self.colormap = kwargs.get('colormap', 'set1') - self.color = kwargs.get('color', None) + # Use colors if it is given, otherwise attempt to use colormap which + # which will override colors. If neither is found, default to None. + # The colormap may yet still be found in resolve_colors + self.colors = colors + if "colormap" in kwargs: + self.colors = kwargs["colormap"] def fit(self, X, y=None, **kwargs): """ @@ -113,8 +133,9 @@ def fit(self, X, y=None, **kwargs): # NOTE: Probably this would be better in score, but the standard score # is a little different and I'm not sure how it's used. - # Fit the wrapped estimator - self.estimator.fit(X, y, **kwargs) + if not check_fitted(self.estimator, is_fitted_by=self.is_fitted): + # Fit the wrapped estimator + self.estimator.fit(X, y, **kwargs) # Get the properties of the dataset self.n_samples_ = X.shape[0] @@ -145,13 +166,22 @@ def draw(self, labels): """ # Track the positions of the lines being drawn - y_lower = 10 # The bottom of the silhouette + y_lower = 10 # The bottom of the silhouette # Get the colors from the various properties - # TODO: Use resolve_colors instead of this - colors = color_palette(self.colormap, self.n_clusters_) + color_kwargs = {"n_colors": self.n_clusters_} + + if self.colors is None: + color_kwargs["colormap"] = "Set1" + elif isinstance(self.colors, str): + color_kwargs["colormap"] = self.colors + else: + color_kwargs["colors"] = self.colors + + colors = resolve_colors(**color_kwargs) # For each cluster, plot the silhouette scores + self.y_tick_pos_ = [] for idx in range(self.n_clusters_): # Collect silhouette scores for samples in the current cluster . @@ -164,19 +194,26 @@ def draw(self, labels): color = colors[idx] self.ax.fill_betweenx( - np.arange(y_lower, y_upper), 0, values, - facecolor=color, edgecolor=color, alpha=0.5 + np.arange(y_lower, y_upper), + 0, + values, + facecolor=color, + edgecolor=color, + alpha=0.5, ) - # Label the silhouette plots with their cluster numbers - self.ax.text(-0.05, y_lower + 0.5 * size, str(idx)) + # Collect the tick position for each cluster + self.y_tick_pos_.append(y_lower + 0.5 * size) # Compute the new y_lower for next plot y_lower = y_upper + 10 # The vertical line for average silhouette score of all the values self.ax.axvline( - x=self.silhouette_score_, color="red", linestyle="--" + x=self.silhouette_score_, + color="red", + linestyle="--", + label="Average Silhouette Score", ) return self.ax @@ -188,18 +225,18 @@ def finalize(self): """ # Set the title - self.set_title(( - "Silhouette Plot of {} Clustering for {} Samples in {} Centers" - ).format( - self.name, self.n_samples_, self.n_clusters_ - )) + self.set_title( + ("Silhouette Plot of {} Clustering for {} Samples in {} Centers").format( + self.name, self.n_samples_, self.n_clusters_ + ) + ) # Set the X and Y limits # The silhouette coefficient can range from -1, 1; # but here we scale the plot according to our visualizations # l_xlim and u_xlim are lower and upper limits of the x-axis, - # set according to our calculated maximum and minimum silhouette score along with necessary padding + # set according to our calculated max and min score with necessary padding l_xlim = max(-1, min(-0.1, round(min(self.silhouette_samples_) - 0.1, 1))) u_xlim = min(1, round(max(self.silhouette_samples_) + 0.1, 1)) self.ax.set_xlim([l_xlim, u_xlim]) @@ -213,5 +250,75 @@ def finalize(self): self.ax.set_ylabel("cluster label") # Set the ticks on the axis object. - self.ax.set_yticks([]) # Clear the yaxis labels / ticks - self.ax.xaxis.set_major_locator(ticker.MultipleLocator(0.1)) # Set the ticks at multiples of 0.1 + self.ax.set_yticks(self.y_tick_pos_) + self.ax.set_yticklabels(str(idx) for idx in range(self.n_clusters_)) + # Set the ticks at multiples of 0.1 + self.ax.xaxis.set_major_locator(ticker.MultipleLocator(0.1)) + + # Show legend (Average Silhouette Score axis) + self.ax.legend(loc="best") + + +########################################################################## +## Quick Method +########################################################################## + + +def silhouette_visualizer( + model, X, y=None, ax=None, colors=None, is_fitted="auto", **kwargs +): + """Quick Method: + The Silhouette Visualizer displays the silhouette coefficient for each + sample on a per-cluster basis, visually evaluating the density and + separation between clusters. The score is calculated by averaging the + silhouette coefficient for each sample, computed as the difference + between the average intra-cluster distance and the mean nearest-cluster + distance for each sample, normalized by the maximum value. This produces a + score between -1 and +1, where scores near +1 indicate high separation + and scores near -1 indicate that the samples may have been assigned to + the wrong cluster. + + Parameters + ---------- + model : a Scikit-Learn clusterer + Should be an instance of a centroidal clustering algorithm (``KMeans`` + or ``MiniBatchKMeans``). If the estimator is not fitted, it is fit when + the visualizer is fitted, unless otherwise specified by ``is_fitted``. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes + will be used (or generated if required). + + X : array-like of shape (n, m) + A matrix or data frame with n instances and m features + + y : array-like of shape (n,), optional + A vector or series representing the target for each instance + + colors : iterable or string, default: None + A collection of colors to use for each cluster group. If there are + fewer colors than cluster groups, colors will repeat. May also be a + Yellowbrick or matplotlib colormap string. + + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + + kwargs : dict + Keyword arguments that are passed to the base class and may influence + the visualization as defined in other Visualizers. + + Returns + ------- + viz : SilhouetteVisualizer + The silhouette visualizer, fitted and finalized. + """ + oz = SilhouetteVisualizer( + model, ax=ax, colors=colors, is_fitted=is_fitted, **kwargs + ) + + oz.fit(X, y) + oz.finalize() + return oz diff --git a/yellowbrick/contrib/__init__.py b/yellowbrick/contrib/__init__.py index 363954ba8..e9e66dfe4 100644 --- a/yellowbrick/contrib/__init__.py +++ b/yellowbrick/contrib/__init__.py @@ -4,8 +4,7 @@ # core support or still in development. # # -# ID: __init__.py [] bilbro@gmail.com $ - +# ID: __init__.py [a60bc41] nathan.danielsen@gmail.com $ from .scatter import ScatterViz, ScatterVisualizer, scatterviz diff --git a/yellowbrick/contrib/classifier/__init__.py b/yellowbrick/contrib/classifier/__init__.py index 3963f69b8..a91152496 100644 --- a/yellowbrick/contrib/classifier/__init__.py +++ b/yellowbrick/contrib/classifier/__init__.py @@ -1,12 +1,12 @@ # yellowbrick.contrib.classifier # Visualizations related to evaluating Scikit-Learn classification models # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Wed Mar 29 12:39:40 2018 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: __init__.py [5eee25b] nathan.danielsen@gmail.com $ +# ID: __init__.py [a60bc41] nathan.danielsen@gmail.com $ from .boundaries import decisionviz, DecisionBoundariesVisualizer, DecisionViz diff --git a/yellowbrick/contrib/classifier/boundaries.py b/yellowbrick/contrib/classifier/boundaries.py index ece628f52..45047f712 100644 --- a/yellowbrick/contrib/classifier/boundaries.py +++ b/yellowbrick/contrib/classifier/boundaries.py @@ -1,11 +1,13 @@ # yellowbrick.contrib.classifier.boundaries # Decision boundaries classifier visualizer for Yellowbrick. # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Sat Mar 12 14:17:29 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt +# +# ID: boundaries.py [a60bc41] nathan.danielsen@gmail.com $ import itertools import numpy as np @@ -28,20 +30,28 @@ ########################################################################## # Quick Methods ########################################################################## + + @deprecated("Will be moved to yellowbrick.contrib in v0.8") -def decisionviz(model, - X, - y, - colors=None, - classes=None, - features=None, - show_scatter=True, - step_size=0.0025, - markers=None, - pcolormesh_alpha=0.8, - scatter_alpha=1.0, - title=None, - **kwargs): +def decisionviz( + model, + X, + y, + ax=None, + x_name=None, + y_name=None, + features=None, + classes=None, + show_scatter=True, + step_size=0.0025, + markers=None, + pcolormesh_alpha=0.8, + scatter_alpha=1.0, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs +): """DecisionBoundariesVisualizer is a bivariate data visualization algorithm that plots the decision boundaries of each class. @@ -50,25 +60,41 @@ def decisionviz(model, Parameters ---------- - model : the Scikit-Learn estimator, required - Should be an instance of a classifier, else the __init__ will - return an error. + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. + + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features + + y : ndarray or Series of length n + An array or series of target or class values + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). - x : matrix, required + x_name : string, default: None The feature name that corresponds to a column name or index postion in the matrix that will be plotted against the x-axis - y : array, required + y_name : string, default: None The feature name that corresponds to a column name or index postion in the matrix that will be plotted against the y-axis - classes : a list of class names for the legend, default: None - If classes is None and a y value is passed to fit then the classes - are selected from the target vector. - features : list of strings, default: None The names of the features or columns + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + show_scatter : boolean, default: True If boolean is True, then a scatter plot with points will be drawn on top of the decision boundary graph @@ -89,40 +115,63 @@ def decisionviz(model, scatter_alpha : float, default: 1.0 Sets the alpha transparency for the scatter plot points - title : string, default: stringified feature_one and feature_two - Sets the title of the visualization + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. - kwargs : keyword arguments passed to the super class. + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs : dict + Keyword arguments passed to the visualizer base classes. Returns ------- - ax : matplotlib axes - Returns the axes that the decision boundaries graph were drawn on. + viz : DecisionBoundariesVisualizer + Returns the fitted and finalized visualizer """ # Instantiate the visualizer - visualizer = DecisionBoundariesVisualizer(model, - X, - y, - colors=colors, - classes=classes, - features=features, - show_scatter=show_scatter, - step_size=step_size, - markers=markers, - pcolormesh_alpha=pcolormesh_alpha, - scatter_alpha=scatter_alpha, - title=title, - **kwargs) + visualizer = DecisionBoundariesVisualizer( + model, + ax=ax, + x=x_name, + y=y_name, + features=features, + classes=classes, + show_scatter=show_scatter, + step_size=step_size, + markers=markers, + pcolormesh_alpha=pcolormesh_alpha, + scatter_alpha=scatter_alpha, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + **kwargs + ) # Fit, draw and poof the visualizer - visualizer.fit_draw_poof(X, y, **kwargs) + visualizer.fit(X, y) + visualizer.finalize() # Return the axes object on the visualizer - return visualizer.ax + return visualizer + ########################################################################## # Static ScatterVisualizer Visualizer ########################################################################## + + @deprecated("Will be moved to yellowbrick.contrib in v0.8") class DecisionBoundariesVisualizer(ClassificationScoreVisualizer): """ @@ -131,10 +180,15 @@ class DecisionBoundariesVisualizer(ClassificationScoreVisualizer): Parameters ---------- + model : estimator + A scikit-learn estimator that should be a classifier. If the model is + not a classifier, an exception is raised. If the internal model is not + fitted, it is fit when the visualizer is fitted, unless otherwise specified + by ``is_fitted``. - model : the Scikit-Learn estimator - Should be an instance of a classifier, else the __init__ will - return an error. + ax : matplotlib Axes, default: None + The axes to plot the figure on. If not specified the current axes will be + used (or generated if required). x : string, default: None The feature name that corresponds to a column name or index postion @@ -144,13 +198,17 @@ class DecisionBoundariesVisualizer(ClassificationScoreVisualizer): The feature name that corresponds to a column name or index postion in the matrix that will be plotted against the y-axis - classes : a list of class names for the legend, default: None - If classes is None and a y value is passed to fit then the classes - are selected from the target vector. - features : list of strings, default: None The names of the features or columns + classes : list of str, defult: None + The class labels to use for the legend ordered by the index of the sorted + classes discovered in the ``fit()`` method. Specifying classes in this + manner is used to change the class names to a more specific format or + to label encoded integer classes. Some visualizers may also use this + field to filter the visualization for specific classes. For more advanced + usage specify an encoder rather than class labels. + show_scatter : boolean, default: True If boolean is True, then a scatter plot with points will be drawn on top of the decision boundary graph @@ -171,34 +229,54 @@ class DecisionBoundariesVisualizer(ClassificationScoreVisualizer): scatter_alpha : float, default: 1.0 Sets the alpha transparency for the scatter plot points - title : string, default: stringified feature_one and feature_two - Sets the title of the visualization + encoder : dict or LabelEncoder, default: None + A mapping of classes to human readable labels. Often there is a mismatch + between desired class labels and those contained in the target variable + passed to ``fit()`` or ``score()``. The encoder disambiguates this mismatch + ensuring that classes are labeled correctly in the visualization. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. - kwargs : keyword arguments passed to the super class. + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. - These parameters can be influenced later on in the visualization - process, but can and should be set as early as possible. + kwargs : dict + Keyword arguments passed to the visualizer base classes. """ - def __init__(self, - model, - x=None, - y=None, - features=None, - show_scatter=True, - step_size=0.0025, - markers=None, - pcolormesh_alpha=0.8, - scatter_alpha=1.0, - # title=None, - *args, - **kwargs): - """ - Pass in a unfitted model to generate a decision boundaries - visualization. - """ - super(DecisionBoundariesVisualizer, self).__init__(model, *args, **kwargs) + def __init__( + self, + model, + ax=None, + x=None, + y=None, + features=None, + classes=None, + show_scatter=True, + step_size=0.0025, + markers=None, + pcolormesh_alpha=0.8, + scatter_alpha=1.0, + encoder=None, + is_fitted="auto", + force_model=False, + **kwargs + ): + super(DecisionBoundariesVisualizer, self).__init__( + model, + ax=ax, + classes=classes, + encoder=encoder, + is_fitted=is_fitted, + force_model=force_model, + ) self.x = x self.y = y @@ -207,7 +285,8 @@ def __init__(self, self.show_scatter = show_scatter self.step_size = step_size self.markers = itertools.cycle( - kwargs.pop('markers', (',', 'o', 'd', '*', 'v', 'h', '+'))) + kwargs.pop("markers", (",", "o", "d", "*", "v", "h", "+")) + ) self.pcolormesh_alpha = pcolormesh_alpha self.scatter_alpha = scatter_alpha @@ -219,8 +298,7 @@ def __init__(self, self.class_labels = None if self.x is not None and self.y is not None and self.features_ is not None: - raise YellowbrickValueError( - 'Please specify x,y or features, not both.') + raise YellowbrickValueError("Please specify x,y or features, not both.") if self.x is not None and self.y is not None and self.features_ is None: self.features_ = [self.x, self.y] @@ -229,7 +307,8 @@ def __init__(self, if features is not None: if len(features) != 2: raise YellowbrickValueError( - 'DecisionBoundariesVisualizer only accepts two features.') + "DecisionBoundariesVisualizer only accepts two features." + ) def _select_feature_columns(self, X): """ """ @@ -261,10 +340,12 @@ def _select_feature_columns(self, X): X_two_cols = X[:, [int(f_one), int(f_two)]] else: - raise YellowbrickValueError(""" + raise YellowbrickValueError( + """ ScatterVisualizer only accepts two features, please explicitly set these two features in the init kwargs or - pass a matrix/ dataframe in with only two columns.""") + pass a matrix/ dataframe in with only two columns.""" + ) return X_two_cols @@ -291,18 +372,17 @@ def fit(self, X, y=None, **kwargs): Returns the instance of the visualizer """ X = self._select_feature_columns(X) + self.classes_ = self._labels() # Assign each class a unique number for drawing if self.classes_ is None: self.classes_ = { - label: str(kls_num) - for kls_num, label in enumerate(np.unique(y)) + label: str(kls_num) for kls_num, label in enumerate(np.unique(y)) } self.class_labels = None elif len(set(y)) == len(self.classes_): self.classes_ = { - label: str(kls_num) - for kls_num, label in enumerate(self.classes_) + label: str(kls_num) for kls_num, label in enumerate(self.classes_) } self.class_labels = dict(zip(set(y), self.classes_)) else: @@ -316,8 +396,14 @@ def fit(self, X, y=None, **kwargs): # Plot the decision boundary. For that, we will assign a color to each # point in the mesh [x_min, x_max]x[y_min, y_max]. - x_min, x_max = X[:, 0].min() - (X[:, 0].min() * .01), X[:, 0].max() + (X[:, 0].max() * .01) - y_min, y_max = X[:, 1].min() - (X[:, 1].min() * .01), X[:, 1].max() + (X[:, 1].max() * .01) + x_min, x_max = ( + X[:, 0].min() - (X[:, 0].min() * 0.01), + X[:, 0].max() + (X[:, 0].max() * 0.01), + ) + y_min, y_max = ( + X[:, 1].min() - (X[:, 1].min() * 0.01), + X[:, 1].max() + (X[:, 1].max() * 0.01), + ) self.ax.set_xlim([x_min, x_max]) self.ax.set_ylim([y_min, y_max]) @@ -326,7 +412,8 @@ def fit(self, X, y=None, **kwargs): y_step = (y_max - y_min) * self.step_size self.xx, self.yy = np.meshgrid( - np.arange(x_min, x_max, x_step), np.arange(y_min, y_max, y_step)) + np.arange(x_min, x_max, x_step), np.arange(y_min, y_max, y_step) + ) # raise Exception(self.yy.ravel().shape) Z = self.estimator.predict(np.c_[self.xx.ravel(), self.yy.ravel()]) @@ -345,16 +432,17 @@ def draw(self, X, y=None, **kwargs): X = self._select_feature_columns(X) color_cycle = iter( - resolve_colors(colors=self.colors, n_colors=len(self.classes_))) - colors = OrderedDict([(c, next(color_cycle)) - for c in self.classes_.keys()]) + resolve_colors(colors=self.colors, n_colors=len(self.classes_)) + ) + colors = OrderedDict([(c, next(color_cycle)) for c in self.classes_.keys()]) self.ax.pcolormesh( self.xx, self.yy, self.Z_shape, alpha=self.pcolormesh_alpha, - cmap=ListedColormap(colors.values())) + cmap=ListedColormap(colors.values()), + ) # Create a data structure to hold the scatter plot representations to_plot = OrderedDict() @@ -387,13 +475,13 @@ def draw(self, X, y=None, **kwargs): color=colors[kls], alpha=self.scatter_alpha, s=30, - edgecolors='black', + edgecolors="black", label=str(kls), - **kwargs) + **kwargs + ) else: labels = [ - Patch(color=colors[kls], label=kls) - for kls in self.classes_.keys() + Patch(color=colors[kls], label=kls) for kls in self.classes_.keys() ] self.ax.legend(handles=labels) @@ -412,7 +500,7 @@ def finalize(self, **kwargs): self.set_title(self.title) # Add the legend - self.ax.legend(loc='best', frameon=True) + self.ax.legend(loc="best", frameon=True) self.ax.set_xlabel(feature_one) self.ax.set_ylabel(feature_two) diff --git a/yellowbrick/contrib/missing/__init__.py b/yellowbrick/contrib/missing/__init__.py index 96ab3aecc..e6a64cd03 100644 --- a/yellowbrick/contrib/missing/__init__.py +++ b/yellowbrick/contrib/missing/__init__.py @@ -1,13 +1,13 @@ # yellowbrick.contrib.missing # Visualizations related to missing values # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Fri Mar 29 5:17:36 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: __init__.py [5eee25b] nathan.danielsen@gmail.com $ +# ID: __init__.py [1443e16] ndanielsen@users.noreply.github.com $ from .bar import MissingValuesBar, missing_bar from .dispersion import MissingValuesDispersion, missing_dispersion diff --git a/yellowbrick/contrib/missing/bar.py b/yellowbrick/contrib/missing/bar.py index 7fd1a7d1e..99e0b5c72 100644 --- a/yellowbrick/contrib/missing/bar.py +++ b/yellowbrick/contrib/missing/bar.py @@ -1,13 +1,13 @@ # yellowbrick.contrib.missing.bar # Missing Values Bar Visualizer # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Fri Mar 29 5:17:36 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: bar.py [] nathan.danielsen@gmail.com.com $ +# ID: bar.py [1443e16] ndanielsen@users.noreply.github.com $ """ Bar visualizer of missing values by column. @@ -48,7 +48,7 @@ class MissingValuesBar(MissingDataVisualizer): fit. colors : list, default: None - The color pallette for drawing a stack bar chart when the y targets + The color palette for drawing a stack bar chart when the y targets are passed to fit. classes : list, default: None @@ -77,7 +77,10 @@ class MissingValuesBar(MissingDataVisualizer): >>> visualizer.poof() """ - def __init__(self, width=0.5, color='black', colors=None, classes=None, **kwargs): + def __init__(self, width=0.5, color=None, colors=None, classes=None, **kwargs): + + if "target_type" not in kwargs: + kwargs["target_type"] = "single" super(MissingValuesBar, self).__init__(**kwargs) self.width = width # the width of the bars self.classes_ = classes @@ -88,17 +91,20 @@ def __init__(self, width=0.5, color='black', colors=None, classes=None, **kwargs self.classes_ = np.array(classes) # Set up classifier score visualization properties + self.color = color if self.classes_ is not None: n_colors = len(self.classes_) else: n_colors = None - self.colors = color_palette(kwargs.pop('colors', None), n_colors) + self.colors = color_palette(kwargs.pop("colors", None), n_colors) def get_nan_col_counts(self, **kwargs): # where matrix contains strings, handle them - if np.issubdtype(self.X.dtype, np.string_) or np.issubdtype(self.X.dtype, np.unicode_): - mask = np.where( self.X == '' ) + if np.issubdtype(self.X.dtype, np.string_) or np.issubdtype( + self.X.dtype, np.unicode_ + ): + mask = np.where(self.X == "") nan_matrix = np.zeros(self.X.shape) nan_matrix[mask] = np.nan @@ -116,7 +122,9 @@ def get_nan_col_counts(self, **kwargs): indices = np.argwhere(self.y == target_value) target_matrix = nan_matrix[indices.flatten()] - nan_col_counts = np.array([np.count_nonzero(np.isnan(col)) for col in target_matrix.T]) + nan_col_counts = np.array( + [np.count_nonzero(np.isnan(col)) for col in target_matrix.T] + ) nan_counts.append((target_value, nan_col_counts)) return nan_counts @@ -134,8 +142,13 @@ def draw(self, X, y, **kwargs): self.ind = np.arange(len(self.features_)) if y is None: - self.ax.barh(self.ind - self.width / 2, nan_col_counts, self.width, - color=self.color, label=None) + self.ax.barh( + self.ind - self.width / 2, + nan_col_counts, + self.width, + color=self.color, + label=None, + ) else: self.draw_stacked_bar(nan_col_counts) @@ -156,8 +169,14 @@ def draw_stacked_bar(self, nan_col_counts): color = self.colors[index] - self.ax.barh(self.ind - self.width / 2, nan_col_counts, self.width, - color=color, label=label, left=bottom_chart) + self.ax.barh( + self.ind - self.width / 2, + nan_col_counts, + self.width, + color=color, + label=label, + left=bottom_chart, + ) # keep track of counts to build on stacked bottom_chart = nan_col_counts @@ -173,22 +192,24 @@ def finalize(self, **kwargs): """ # Set the title - self.set_title( - 'Count of Missing Values by Column' - ) - tick_locations = np.arange(len(self.features_)) # the x locations for the groups + self.set_title("Count of Missing Values by Column") + tick_locations = np.arange( + len(self.features_) + ) # the x locations for the groups self.ax.set_yticks(tick_locations) self.ax.set_yticklabels(self.get_feature_names()) # Remove the ticks from the graph - self.ax.set_xlabel('Count') + self.ax.set_xlabel("Count") + + self.ax.legend(loc="best") - self.ax.legend(loc='best') ########################################################################## ## Quick Method ########################################################################## -def missing_bar(X, y=None, ax=None, classes=None, width=0.5, color='black', **kwargs): + +def missing_bar(X, y=None, ax=None, classes=None, width=0.5, color="black", **kwargs): """The MissingValues Bar visualizer creates a bar graph that lists the total count of missing values for each selected feature column. diff --git a/yellowbrick/contrib/missing/base.py b/yellowbrick/contrib/missing/base.py index 1c7310b8a..5f6512538 100644 --- a/yellowbrick/contrib/missing/base.py +++ b/yellowbrick/contrib/missing/base.py @@ -1,13 +1,13 @@ # yellowbrick.contrib.missing.base # Base Visualizer for missing values # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Fri Mar 29 5:17:36 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: base.py [] nathan.danielsen@gmail.com.com $ +# ID: base.py [1443e16] ndanielsen@users.noreply.github.com $ """ Base classes for missing values visualizers. @@ -16,15 +16,18 @@ ########################################################################## ## Imports ########################################################################## + import numpy as np from yellowbrick.features.base import DataVisualizer from yellowbrick.utils import is_dataframe + ########################################################################## ## Feature Visualizers ########################################################################## + class MissingDataVisualizer(DataVisualizer): """Base class for MissingDataVisualizers. """ @@ -51,6 +54,12 @@ def fit(self, X, y=None, **kwargs): self : instance Returns the instance of the transformer/visualizer """ + # Do not call super here - the data visualizer has been refactored + # to provide increased functionality that is not yet compatible with + # the current implementation. This mimicks the previous functionality. + # TODO: Refactor MissingDataVisualizer to make use of new features. + self.features_ = self.features + if is_dataframe(X): self.X = X.values if self.features_ is None: @@ -60,8 +69,8 @@ def fit(self, X, y=None, **kwargs): self.y = y - super(MissingDataVisualizer, self).fit(X, y, **kwargs) - + self.draw(X, y, **kwargs) + return self def get_feature_names(self): if self.features_ is None: diff --git a/yellowbrick/contrib/missing/dispersion.py b/yellowbrick/contrib/missing/dispersion.py index e51760762..e6f089057 100644 --- a/yellowbrick/contrib/missing/dispersion.py +++ b/yellowbrick/contrib/missing/dispersion.py @@ -1,13 +1,13 @@ # yellowbrick.contrib.missing.dispersion # Missing Values Dispersion Visualizer # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Fri Mar 29 5:17:36 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: dispersion.py [] nathan.danielsen@gmail.com.com $ +# ID: dispersion.py [1443e16] ndanielsen@users.noreply.github.com $ """ Dispersion visualizer for locations of missing values by column against index position. @@ -27,6 +27,7 @@ ## MissingValues Visualizer ########################################################################## + class MissingValuesDispersion(MissingDataVisualizer): """ The Missing Values Dispersion visualizer shows the locations of missing (nan) @@ -71,6 +72,8 @@ class MissingValuesDispersion(MissingDataVisualizer): def __init__(self, alpha=0.5, marker="|", classes=None, **kwargs): + if "target_type" not in kwargs: + kwargs["target_type"] = "single" super(MissingValuesDispersion, self).__init__(**kwargs) self.alpha = alpha self.marker = marker @@ -87,15 +90,16 @@ def __init__(self, alpha=0.5, marker="|", classes=None, **kwargs): else: n_colors = None - self.colors = color_palette(kwargs.pop('colors', None), n_colors) - + self.colors = color_palette(kwargs.pop("colors", None), n_colors) def get_nan_locs(self, **kwargs): """Gets the locations of nans in feature data and returns the coordinates in the matrix """ - if np.issubdtype(self.X.dtype, np.string_) or np.issubdtype(self.X.dtype, np.unicode_): - mask = np.where( self.X == '' ) + if np.issubdtype(self.X.dtype, np.string_) or np.issubdtype( + self.X.dtype, np.unicode_ + ): + mask = np.where(self.X == "") nan_matrix = np.zeros(self.X.shape) nan_matrix[mask] = np.nan @@ -143,7 +147,9 @@ def draw_multi_dispersion_chart(self, nan_locs): color = self.colors[index] x_, y_ = list(zip(*nan_locations)) - self.ax.scatter(x_, y_, alpha=self.alpha, marker=self.marker, color=color, label=label) + self.ax.scatter( + x_, y_, alpha=self.alpha, marker=self.marker, color=color, label=label + ) def finalize(self, **kwargs): """ @@ -156,24 +162,24 @@ def finalize(self, **kwargs): """ # Set the title - self.set_title( - 'Dispersion of Missing Values by Feature' - ) + self.set_title("Dispersion of Missing Values by Feature") # the x locations for the groups tick_locations = np.arange(len(self.features_)) - self.ax.set_xlabel('Position by index') + self.ax.set_xlabel("Position by index") self.ax.set_yticks(tick_locations) self.ax.set_yticklabels(self.get_feature_names()) - self.ax.legend(loc='upper left', prop={'size':5}, bbox_to_anchor=(1,1)) - + self.ax.legend(loc="upper left", prop={"size": 5}, bbox_to_anchor=(1, 1)) ########################################################################## ## Quick Method ########################################################################## -def missing_dispersion(X, y=None, ax=None, classes=None, alpha=0.5, marker="|", **kwargs): + +def missing_dispersion( + X, y=None, ax=None, classes=None, alpha=0.5, marker="|", **kwargs +): """ The Missing Values Dispersion visualizer shows the locations of missing (nan) values in the feature dataset by the order of the index. diff --git a/yellowbrick/contrib/scatter.py b/yellowbrick/contrib/scatter.py index 5206df57b..0d062184c 100644 --- a/yellowbrick/contrib/scatter.py +++ b/yellowbrick/contrib/scatter.py @@ -1,12 +1,13 @@ # yellowbrick.contrib.scatter # Implements a 2d scatter plot for feature analysis. # -# Author: Nathan Danielsen +# Author: Nathan Danielsen # Created: Fri Feb 26 19:40:00 2017 -0400 # +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: scatter.py [fc94ec4] ndanielsen@users.noreply.github.com $ +# ID: scatter.py [a89633e] benjamin@bengfort.com $ """ Implements a 2D scatter plot for feature analysis. """ @@ -29,16 +30,19 @@ # Quick Methods ########################################################################## -def scatterviz(X, - y=None, - ax=None, - features=None, - classes=None, - color=None, - colormap=None, - markers=None, - alpha=1.0, - **kwargs): + +def scatterviz( + X, + y=None, + ax=None, + features=None, + classes=None, + color=None, + colormap=None, + markers=None, + alpha=1.0, + **kwargs +): """Displays a bivariate scatter plot. This helper function is a quick wrapper to utilize the ScatterVisualizer @@ -78,26 +82,34 @@ def scatterviz(X, Returns ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. + viz : ScatterVisualizer + Returns the fitted, finalized visualizer """ # Instantiate the visualizer - visualizer = ScatterVisualizer(ax=ax, features=features, classes=classes, - color=color, colormap=colormap, - markers=markers, alpha=alpha, **kwargs) + visualizer = ScatterVisualizer( + ax=ax, + features=features, + classes=classes, + color=color, + colormap=colormap, + markers=markers, + alpha=alpha, + **kwargs + ) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y, **kwargs) visualizer.transform(X) - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer object + return visualizer ########################################################################## # Static ScatterVisualizer Visualizer ########################################################################## + class ScatterVisualizer(DataVisualizer): """ ScatterVisualizer is a bivariate feature data visualization algorithm that @@ -149,23 +161,31 @@ class ScatterVisualizer(DataVisualizer): process, but can and should be set as early as possible. """ - def __init__(self, - ax=None, - x=None, - y=None, - features=None, - classes=None, - color=None, - colormap=None, - markers=None, - alpha=1.0, - **kwargs): + def __init__( + self, + ax=None, + x=None, + y=None, + features=None, + classes=None, + color=None, + colormap=None, + markers=None, + alpha=1.0, + **kwargs + ): """ Initialize the base scatter with many of the options required in order to make the visualization work. """ - super(ScatterVisualizer, self).__init__(ax, features, classes, color, - colormap, **kwargs) + super(ScatterVisualizer, self).__init__( + ax=ax, + features=features, + classes=classes, + color=color, + colormap=colormap, + **kwargs + ) self.x = x self.y = y @@ -173,23 +193,24 @@ def __init__(self, self.alpha = alpha self.markers = itertools.cycle( - kwargs.pop('markers', (',', '+', 'o', '*', 'v', 'h', 'd'))) + kwargs.pop("markers", (",", "+", "o", "*", "v", "h", "d")) + ) self.color = color self.colormap = colormap - if self.x is not None and self.y is not None and self.features_ is not None: - raise YellowbrickValueError( - 'Please specify x,y or features, not both.') + if self.x is not None and self.y is not None and self.features is not None: + raise YellowbrickValueError("Please specify x,y or features, not both.") - if self.x is not None and self.y is not None and self.features_ is None: - self.features_ = [self.x, self.y] + if self.x is not None and self.y is not None and self.features is None: + self.features = [self.x, self.y] # Ensure with init that features doesn't have more than two features if features is not None: if len(features) != 2: raise YellowbrickValueError( - 'ScatterVisualizer only accepts two features.') + "ScatterVisualizer only accepts two features." + ) def fit(self, X, y=None, **kwargs): """ @@ -215,6 +236,12 @@ def fit(self, X, y=None, **kwargs): """ _, ncols = X.shape + # NOTE: Do not call super for this class, it conflicts with the fit. + # Setting these variables is similar to the old behavior of DataVisualizer. + # TODO: refactor to make use of the new DataVisualizer functionality + self.features_ = self.features + self.classes_ = self.classes + if ncols == 2: X_two_cols = X if self.features_ is None: @@ -227,7 +254,9 @@ def fit(self, X, y=None, **kwargs): # handle numpy named/ structured array elif self.features_ is not None and is_structured_array(X): X_selected = X[self.features_] - X_two_cols = X_selected.copy().view((np.float64, len(X_selected.dtype.names))) + X_two_cols = X_selected.copy().view( + (np.float64, len(X_selected.dtype.names)) + ) # handle features that are numeric columns in ndarray matrix elif self.features_ is not None and has_ndarray_int_columns(self.features_, X): @@ -235,10 +264,12 @@ def fit(self, X, y=None, **kwargs): X_two_cols = X[:, [int(f_one), int(f_two)]] else: - raise YellowbrickValueError(""" + raise YellowbrickValueError( + """ ScatterVisualizer only accepts two features, please explicitly set these two features in the init kwargs or - pass a matrix/ dataframe in with only two columns.""") + pass a matrix/ dataframe in with only two columns.""" + ) # Store the classes for the legend if they're None. if self.classes_ is None: @@ -257,14 +288,12 @@ def draw(self, X, y, **kwargs): is determined by the feature data set. """ # Set the axes limits - self.ax.set_xlim([-1,1]) - self.ax.set_ylim([-1,1]) + self.ax.set_xlim([-1, 1]) + self.ax.set_ylim([-1, 1]) # set the colors color_values = resolve_colors( - n_colors=len(self.classes_), - colormap=self.colormap, - colors=self.color + n_colors=len(self.classes_), colormap=self.colormap, colors=self.color ) colors = dict(zip(self.classes_, color_values)) @@ -295,9 +324,10 @@ def draw(self, X, y, **kwargs): color=colors[kls], label=str(kls), alpha=self.alpha, - **kwargs) + **kwargs + ) - self.ax.axis('equal') + self.ax.axis("equal") def finalize(self, **kwargs): """ @@ -313,10 +343,11 @@ def finalize(self, **kwargs): feature_one, feature_two = self.features_ # Set the title - self.set_title('Scatter Plot: {0} vs {1}'.format( - str(feature_one), str(feature_two))) + self.set_title( + "Scatter Plot: {0} vs {1}".format(str(feature_one), str(feature_two)) + ) # Add the legend - self.ax.legend(loc='best') + self.ax.legend(loc="best") self.ax.set_xlabel(str(feature_one)) self.ax.set_ylabel(str(feature_two)) diff --git a/yellowbrick/contrib/statsmodels/__init__.py b/yellowbrick/contrib/statsmodels/__init__.py index 632ec3754..556bad9a9 100644 --- a/yellowbrick/contrib/statsmodels/__init__.py +++ b/yellowbrick/contrib/statsmodels/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Wed Apr 04 13:13:24 2018 -0400 # -# ID: __init__.py [] benjamin@bengfort.com $ +# ID: __init__.py [d6ebc39] benjamin@bengfort.com $ """ Implements wrappers around hte statsmodels library to use Yellowbrick with. diff --git a/yellowbrick/contrib/statsmodels/base.py b/yellowbrick/contrib/statsmodels/base.py index 7860c9a17..3e68edf50 100644 --- a/yellowbrick/contrib/statsmodels/base.py +++ b/yellowbrick/contrib/statsmodels/base.py @@ -4,7 +4,7 @@ # Author: Ian Ozsvald # Created: Wed Jan 10 12:47:00 2018 -0500 # -# ID: base.py [] benjamin@bengfort.com $ +# ID: base.py [d6ebc39] benjamin@bengfort.com $ """ A basic wrapper for statsmodels that emulates a scikit-learn estimator. @@ -22,6 +22,7 @@ ## statsmodels Estimator ########################################################################## + class StatsModelsWrapper(BaseEstimator): """ Wrap a statsmodels GLM as a sklearn (fake) BaseEstimator for YellowBrick. @@ -48,13 +49,13 @@ class StatsModelsWrapper(BaseEstimator): >>> gaussian_model = glm_gaussian_partial(y_train, X_train) - Note - ---- + Notes + ----- .. note:: This wrapper is trivial, options and extra things like weights are not currently handled. """ - def __init__(self, glm_partial, stated_estimator_type="regressor", - scorer=r2_score): + + def __init__(self, glm_partial, stated_estimator_type="regressor", scorer=r2_score): # YellowBrick checks the attribute to see if it is a # regressor/clusterer/classifier diff --git a/yellowbrick/datasaurus.py b/yellowbrick/datasaurus.py index b65d31ccf..68c45a433 100644 --- a/yellowbrick/datasaurus.py +++ b/yellowbrick/datasaurus.py @@ -4,10 +4,10 @@ # Author: Larry Gray # Created: Wed Jun 20 15:17:35 2018 -0400 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The sckit-yb developers # For license information, see LICENSE.txt # -# ID: datasaurus.py [] lwgray@gmail.com $ +# ID: datasaurus.py [e49d780] lwgray@gmail.com $ """ Plots a Datasaurus Quartet as an illustration of the importance of visualization. @@ -29,222 +29,1175 @@ ########################################################################## DATASAURUS = [ - np.array([[55.3846, 51.5385, 46.1538, 42.8205, 40.7692, 38.7179, 35.641 , - 33.0769, 28.9744, 26.1538, 23.0769, 22.3077, 22.3077, 23.3333, - 25.8974, 29.4872, 32.8205, 35.3846, 40.2564, 44.1026, 46.6667, - 50. , 53.0769, 56.6667, 59.2308, 61.2821, 61.5385, 61.7949, - 57.4359, 54.8718, 52.5641, 48.2051, 49.4872, 51.0256, 45.3846, - 42.8205, 38.7179, 35.1282, 32.5641, 30. , 33.5897, 36.6667, - 38.2051, 29.7436, 29.7436, 30. , 32.0513, 35.8974, 41.0256, - 44.1026, 47.1795, 49.4872, 51.5385, 53.5897, 55.1282, 56.6667, - 59.2308, 62.3077, 64.8718, 67.9487, 70.5128, 71.5385, 71.5385, - 69.4872, 46.9231, 48.2051, 50. , 53.0769, 55.3846, 56.6667, - 56.1538, 53.8462, 51.2821, 50. , 47.9487, 29.7436, 29.7436, - 31.2821, 57.9487, 61.7949, 64.8718, 68.4615, 70.7692, 72.0513, - 73.8462, 75.1282, 76.6667, 77.6923, 79.7436, 81.7949, 83.3333, - 85.1282, 86.4103, 87.9487, 89.4872, 93.3333, 95.3846, 98.2051, - 56.6667, 59.2308, 60.7692, 63.0769, 64.1026, 64.359 , 74.359 , - 71.2821, 67.9487, 65.8974, 63.0769, 61.2821, 58.7179, 55.1282, - 52.3077, 49.7436, 47.4359, 44.8718, 48.7179, 51.2821, 54.1026, - 56.1538, 52.0513, 48.7179, 47.1795, 46.1538, 50.5128, 53.8462, - 57.4359, 60. , 64.1026, 66.9231, 71.2821, 74.359 , 78.2051, - 67.9487, 68.4615, 68.2051, 37.6923, 39.4872, 91.2821, 50. , - 47.9487, 44.1026], - [97.1795, 96.0256, 94.4872, 91.4103, 88.3333, 84.8718, 79.8718, - 77.5641, 74.4872, 71.4103, 66.4103, 61.7949, 57.1795, 52.9487, - 51.0256, 51.0256, 51.0256, 51.4103, 51.4103, 52.9487, 54.1026, - 55.2564, 55.641 , 56.0256, 57.9487, 62.1795, 66.4103, 69.1026, - 55.2564, 49.8718, 46.0256, 38.3333, 42.1795, 44.1026, 36.4103, - 32.5641, 31.4103, 30.2564, 32.1795, 36.7949, 41.4103, 45.641 , - 49.1026, 36.0256, 32.1795, 29.1026, 26.7949, 25.2564, 25.2564, - 25.641 , 28.718 , 31.4103, 34.8718, 37.5641, 40.641 , 42.1795, - 44.4872, 46.0256, 46.7949, 47.9487, 53.718 , 60.641 , 64.4872, - 69.4872, 79.8718, 84.1026, 85.2564, 85.2564, 86.0256, 86.0256, - 82.9487, 80.641 , 78.718 , 78.718 , 77.5641, 59.8718, 62.1795, - 62.5641, 99.4872, 99.1026, 97.5641, 94.1026, 91.0256, 86.4103, - 83.3333, 79.1026, 75.2564, 71.4103, 66.7949, 60.2564, 55.2564, - 51.4103, 47.5641, 46.0256, 42.5641, 39.8718, 36.7949, 33.718 , - 40.641 , 38.3333, 33.718 , 29.1026, 25.2564, 24.1026, 22.9487, - 22.9487, 22.1795, 20.2564, 19.1026, 19.1026, 18.3333, 18.3333, - 18.3333, 17.5641, 16.0256, 13.718 , 14.8718, 14.8718, 14.8718, - 14.1026, 12.5641, 11.0256, 9.8718, 6.0256, 9.4872, 10.2564, - 10.2564, 10.641 , 10.641 , 10.641 , 10.641 , 10.641 , 10.641 , - 8.718 , 5.2564, 2.9487, 25.7692, 25.3846, 41.5385, 95.7692, - 95. , 92.6923]]), - np.array([[51.20389114, 58.9744699 , 51.87207267, 48.17993079, 41.6832004 , - 37.8904155 , 39.54897369, 39.64957388, 34.75059705, 27.56083529, - 24.63553998, 20.95946481, 20.68914905, 19.28820474, 20.02450057, - 35.469523 , 36.89432765, 39.05554978, 46.95708015, 37.31045274, - 40.009672 , 48.01438668, 53.70377593, 63.06749989, 62.04803251, - 59.83996671, 55.16094182, 61.27978658, 60.83491753, 61.52059065, - 36.91654386, 38.50219967, 48.66437073, 50.2852524 , 42.27633267, - 54.03177562, 37.32935526, 41.38952255, 40.07466666, 35.34968062, - 34.76370042, 37.02662945, 36.45556953, 35.53766421, 20.40894789, - 23.49571047, 29.55754336, 33.00823391, 53.98039918, 52.2343086 , - 59.50307661, 41.16378107, 48.99304012, 59.26928032, 45.469177 , - 62.69126654, 73.42867087, 70.84642611, 71.53901985, 67.62086589, - 72.47095256, 64.81223756, 60.85367987, 67.78949616, 41.60955727, - 53.00302532, 54.71417106, 44.29166872, 49.19172196, 53.10138178, - 51.59984815, 54.37972195, 46.4807681 , 53.17465627, 45.27200294, - 36.03340215, 28.27119417, 25.05480608, 64.758887 , 63.14452748, - 50.42467869, 70.64499626, 63.14904908, 62.82402452, 70.23686951, - 70.04273524, 72.57062345, 75.13071604, 83.29390573, 79.66426228, - 88.43210253, 89.11555901, 89.09219763, 91.72600577, 91.73553876, - 91.50788817, 88.2390019 , 88.5305192 , 55.36516034, 62.56025887, - 58.00666912, 55.06711799, 61.61477596, 68.54314354, 77.70610965, - 68.453046 , 68.25720644, 70.25547467, 65.04432528, 60.09224661, - 52.99202897, 50.14462898, 46.50861419, 43.80703196, 57.81785469, - 50.94049266, 63.49732308, 50.01648295, 58.63676508, 54.73028909, - 65.8755478 , 57.06098271, 46.81990795, 38.35939487, 47.31541578, - 55.05191654, 50.51596026, 49.67741465, 67.28065952, 66.17301826, - 61.08854414, 66.05308577, 72.66998927, 61.5034725 , 68.99502863, - 78.24991617, 36.48198057, 50.96774838, 91.19105361, 55.86376849, - 49.2805948 , 43.36850154], - [83.33977661, 85.49981761, 85.82973763, 85.04511674, 84.0179406 , - 82.567493 , 80.81260177, 82.66453387, 80.01109099, 72.84782559, - 71.61071483, 66.04149838, 62.72130521, 62.06305936, 61.34262387, - 43.11588495, 47.70655597, 55.54697371, 65.24040739, 45.2587509 , - 60.98658251, 65.71281959, 66.38948204, 64.03500046, 63.84586325, - 64.47676444, 65.23730817, 65.7664025 , 64.60376971, 64.79185504, - 41.09524744, 41.56715562, 30.68066685, 30.33792211, 34.52763612, - 29.67234831, 39.60204231, 37.29605623, 34.6236852 , 47.14107313, - 47.62479992, 44.46229305, 40.79184303, 48.72938687, 32.20303042, - 25.32246815, 21.36477746, 15.98507146, 29.35098671, 29.71167299, - 30.66967394, 34.31575825, 32.03035884, 29.64070177, 33.83119273, - 30.29037383, 48.57785513, 52.28225333, 45.52180616, 38.00655847, - 51.12213482, 62.81091559, 65.49914703, 61.36370155, 83.84868656, - 84.6747986 , 84.04312807, 82.90944121, 85.87622912, 84.54765869, - 84.81982149, 84.24035555, 83.51821167, 84.26056799, 85.23707942, - 53.37168776, 72.84023126, 71.54859792, 82.31522364, 85.23669633, - 85.17474759, 82.43091876, 83.94685535, 84.96618595, 82.17115106, - 80.38502135, 80.97121843, 79.98409314, 70.77843179, 73.93230972, - 64.624247 , 64.00150664, 57.76819305, 52.62335326, 48.97021089, - 53.31265209, 31.47743488, 30.47603101, 30.44585028, 30.44713567, - 30.2537213 , 29.0115352 , 29.99439119, 35.65783217, 20.30426019, - 13.03552859, 12.38463915, 13.25038497, 11.00084148, 11.87211171, - 9.90666848, 12.21154309, 11.20713449, 11.31894489, 10.94514243, - 9.69154713, 11.91406917, 11.93385209, 11.97472107, 11.41288267, - 11.73243636, 9.92056085, 10.49465268, 13.43132262, 12.85345178, - 11.94998862, 9.76559162, 10.38313251, 14.12865153, 12.03791702, - 10.08453441, 13.38022601, 15.23422594, 10.82841448, 13.99431053, - 17.88324091, 15.16276009, 29.67977429, 46.67434284, 85.33648676, - 84.04882283, 84.3321772 ]]), - np.array([[58.21360826, 58.19605369, 58.71823072, 57.27837287, 58.08202049, - 57.48944777, 28.08874132, 28.08546821, 28.08727305, 27.57802522, - 27.77991911, 28.58899981, 28.7391415 , 27.02460324, 28.8013367 , - 27.18646384, 29.2851466 , 39.4029453 , 28.81132844, 34.30395791, - 29.60276098, 49.11615686, 39.61754583, 43.23308466, 64.89278794, - 62.49014932, 68.98808443, 62.10561863, 32.46184674, 41.32720065, - 44.00714993, 44.07406069, 44.00131524, 45.00630045, 44.44384061, - 42.1787134 , 44.04456562, 41.64045402, 41.93833001, 44.05392751, - 39.20671933, 28.70444923, 31.7086629 , 42.81171147, 43.30061489, - 40.39863291, 40.43569158, 40.93654667, 39.66157367, 40.89925917, - 41.96861683, 40.38340582, 56.53812645, 52.97069128, 54.62095259, - 65.09904439, 63.05599091, 70.96013623, 69.89581924, 70.59589286, - 69.64702143, 77.39298249, 64.40078719, 63.86895983, 56.59442132, - 56.53133729, 59.65215837, 56.6365087 , 58.672288 , 58.22161273, - 57.91466448, 55.31550906, 54.57572859, 54.41309365, 55.0745059 , - 29.43296052, 29.42268607, 29.00561416, 58.46183859, 57.99780474, - 57.54947408, 59.52992846, 58.24939106, 58.02451401, 58.38212449, - 62.56675904, 72.17582431, 79.47276157, 80.35770088, 78.75723614, - 82.54023959, 86.43589719, 79.48868442, 81.53042032, 79.18678857, - 77.89905795, 75.13071421, 76.05801375, 57.61467439, 56.17139753, - 66.2878906 , 67.88171962, 64.0280813 , 77.49665175, 77.63465176, - 77.86372643, 77.33815817, 76.18041653, 77.25265109, 77.41337528, - 76.7318494 , 49.47110541, 42.47653994, 43.59511586, 50.33996967, - 40.74898026, 38.38652558, 38.40401521, 38.76427889, 41.47014233, - 47.15540481, 39.58256675, 41.74024382, 39.31187189, 41.67984769, - 39.08746445, 41.48150286, 77.60608655, 75.98266152, 76.94575724, - 77.54372007, 77.58473984, 76.82230426, 77.34857166, 77.57315269, - 77.97261068, 41.52891976, 43.7225508 , 79.32607818, 56.66397408, - 57.82178923, 58.2431719 ], - [91.88189151, 92.21498865, 90.31053209, 89.90760672, 92.00814501, - 88.08528556, 63.51079443, 63.59019695, 63.12328281, 62.82103866, - 63.51814752, 63.02408057, 62.72086389, 62.90185886, 63.38904039, - 63.55872965, 63.38360583, 51.1508572 , 61.35785406, 56.54212591, - 60.15734672, 63.66000062, 62.92518796, 63.16521872, 65.81417676, - 74.58428961, 63.2321473 , 75.99087076, 62.88190292, 49.07025127, - 46.44967378, 34.55320389, 33.90420735, 38.29901955, 36.0190833 , - 26.49211948, 35.66223828, 27.09309542, 24.99152298, 33.55639249, - 51.5337157 , 61.7775254 , 58.83775437, 30.02044842, 31.5264262 , - 16.34700838, 20.23267068, 16.91300484, 15.60935558, 20.79852895, - 26.4970726 , 21.39122552, 32.44424547, 29.04019669, 30.34452445, - 27.24155756, 29.70909567, 41.25950129, 43.45375927, 41.96474387, - 44.04444502, 63.37145906, 67.44871845, 70.21373883, 86.92700622, - 87.49981107, 87.80946159, 85.63749556, 90.07716031, 90.41101877, - 89.95380277, 80.25186069, 77.53628847, 78.22908659, 79.81754642, - 60.80177654, 63.06846482, 63.39075133, 90.26532639, 92.15990861, - 90.74890656, 88.32727415, 92.12968148, 91.69442117, 90.55347607, - 77.74393476, 63.12892942, 63.40868612, 63.29543754, 53.33262001, - 56.54105229, 59.79276181, 53.65167426, 56.02536457, 53.23479185, - 51.82245833, 23.37244197, 16.38374969, 33.82244765, 32.11798877, - 26.11710975, 24.23601841, 27.67268551, 14.94852356, 14.46185393, - 14.61067765, 15.89005466, 15.91257375, 15.15151702, 15.22192798, - 16.21684614, 25.06301931, 18.33847356, 19.99420098, 26.47139661, - 16.18214166, 14.58021515, 14.45194845, 14.36559047, 17.27803344, - 22.37793253, 17.64845284, 17.82932431, 15.64071697, 17.74591901, - 15.12230394, 18.04743744, 15.16287254, 16.30692238, 15.85847833, - 15.25394915, 15.83003939, 15.59516532, 15.77452924, 14.78064583, - 14.95569875, 24.91642519, 19.0773278 , 52.90039129, 87.94012501, - 90.69316655, 92.10432787]]), - np.array([[51.14791671, 50.51712581, 50.2074802 , 50.06948192, 50.56284634, - 50.2885278 , 25.58347508, 25.48358339, 25.4435257 , 25.56511342, - 25.92884427, 27.55147826, 27.53046637, 27.09557036, 27.43924961, - 27.87826426, 27.33886892, 27.67840297, 52.63565768, 52.02521411, - 52.88116479, 52.95260731, 52.52055249, 52.34282206, 51.92759021, - 52.71377449, 50.44380279, 50.21669503, 52.18418011, 52.79209735, - 52.58971986, 52.02884867, 52.72924658, 52.88431329, 52.50930089, - 50.86268433, 50.89149225, 25.8551276 , 26.02564455, 27.89317272, - 27.63996794, 27.8926589 , 52.79773294, 27.58063881, 26.49139853, - 25.98531782, 26.20141928, 25.85756947, 50.70468436, 50.81197535, - 50.56484556, 50.93930391, 50.45885484, 52.90136407, 52.68495344, - 52.50008894, 51.83563726, 76.9954121 , 77.31060048, 77.92604434, - 77.25438834, 76.2431578 , 77.08448437, 75.2280532 , 50.65835477, - 50.20336581, 50.9295477 , 50.17867185, 50.42269806, 50.46422483, - 50.44927033, 49.92838028, 50.48801364, 49.96490538, 50.75210826, - 27.42242921, 27.6740834 , 27.53739532, 52.26334738, 51.73728166, - 75.87096369, 75.24432621, 75.19829529, 75.70104153, 75.47933966, - 75.19456687, 74.82025396, 75.16434049, 75.26335555, 77.75641893, - 77.95443505, 77.08333777, 76.06355025, 77.68201632, 76.87808198, - 76.94850272, 77.86405471, 75.77145009, 52.33156913, 52.59281837, - 50.47704772, 75.29647509, 75.57395413, 75.40052716, 75.87099084, - 75.60588476, 75.89557705, 75.7465632 , 75.14234148, 50.66177956, - 50.69985064, 50.91894087, 50.72525854, 51.26387123, 51.25091965, - 50.78515721, 50.50139658, 50.73367454, 50.71137854, 50.8127449 , - 51.01423295, 50.35352141, 50.43552957, 50.63098196, 51.0668072 , - 50.79235473, 50.55127806, 50.55975806, 75.32597855, 75.04472578, - 75.28708772, 75.23996998, 75.1524592 , 75.96184009, 75.44806251, - 75.75938382, 50.3782623 , 50.53363501, 77.50090732, 50.69112419, - 49.99039495, 50.12718203], - [90.86741233, 89.10239459, 85.4600474 , 83.05766953, 82.93782178, - 82.97525357, 82.91489113, 82.92908498, 82.8742005 , 82.92409777, - 82.82118411, 51.48738653, 51.41484656, 52.07679944, 51.71207905, - 50.70890793, 51.65304675, 51.18198917, 51.41855226, 52.12301105, - 50.62155476, 50.07473901, 51.5024421 , 51.86195209, 52.25779061, - 51.19794432, 82.94182882, 83.75234297, 51.97525067, 51.07339565, - 51.3380902 , 52.1768375 , 51.20176505, 50.44143545, 51.41620515, - 17.14563109, 17.14132373, 17.08190869, 16.92501353, 50.66196341, - 51.39909748, 50.79528152, 50.68603709, 51.52476126, 17.40539097, - 17.20372213, 17.09382391, 17.11384266, 17.02374454, 17.11492526, - 17.07777732, 16.98102188, 17.03857897, 50.69056272, 51.29446922, - 51.59435617, 52.33576553, 52.04552865, 51.74673004, 50.31866042, - 51.46182482, 52.12368985, 51.9671367 , 82.98566202, 83.11447934, - 82.98265686, 82.84604113, 83.18462233, 82.90990147, 82.93532841, - 83.96992038, 82.99366549, 83.09951912, 83.7083177 , 82.9019501 , - 51.43887623, 51.30411215, 51.59365408, 94.24932783, 92.97911753, - 88.38644174, 83.90349738, 83.46230334, 82.91945886, 82.88405139, - 82.93211578, 82.96238879, 83.03499717, 82.9452793 , 51.15177033, - 50.47557897, 52.15779927, 52.10465206, 51.16563781, 51.8675623 , - 51.90751654, 49.66254553, 17.11125121, 51.87886035, 51.39159152, - 17.04828941, 17.01565319, 17.06219214, 17.04110689, 17.13489391, - 17.06772306, 17.16994971, 17.10571651, 16.75492389, 17.07814052, - 17.08518438, 17.14760476, 16.90746981, 17.16234971, 17.24045586, - 17.18019648, 17.10577072, 16.99296341, 17.08831585, 16.57271805, - 17.22109553, 17.06474308, 17.0651685 , 17.07652235, 17.20885971, - 17.20421434, 17.08465518, 17.09388377, 15.77189199, 17.00426226, - 16.17493491, 17.03184749, 17.0049424 , 16.69484223, 17.04514941, - 16.94292965, 16.94627981, 17.01958137, 50.16698595, 87.51396042, - 83.99735692, 82.99075 ]])] + np.array( + [ + [ + 55.3846, + 51.5385, + 46.1538, + 42.8205, + 40.7692, + 38.7179, + 35.641, + 33.0769, + 28.9744, + 26.1538, + 23.0769, + 22.3077, + 22.3077, + 23.3333, + 25.8974, + 29.4872, + 32.8205, + 35.3846, + 40.2564, + 44.1026, + 46.6667, + 50.0, + 53.0769, + 56.6667, + 59.2308, + 61.2821, + 61.5385, + 61.7949, + 57.4359, + 54.8718, + 52.5641, + 48.2051, + 49.4872, + 51.0256, + 45.3846, + 42.8205, + 38.7179, + 35.1282, + 32.5641, + 30.0, + 33.5897, + 36.6667, + 38.2051, + 29.7436, + 29.7436, + 30.0, + 32.0513, + 35.8974, + 41.0256, + 44.1026, + 47.1795, + 49.4872, + 51.5385, + 53.5897, + 55.1282, + 56.6667, + 59.2308, + 62.3077, + 64.8718, + 67.9487, + 70.5128, + 71.5385, + 71.5385, + 69.4872, + 46.9231, + 48.2051, + 50.0, + 53.0769, + 55.3846, + 56.6667, + 56.1538, + 53.8462, + 51.2821, + 50.0, + 47.9487, + 29.7436, + 29.7436, + 31.2821, + 57.9487, + 61.7949, + 64.8718, + 68.4615, + 70.7692, + 72.0513, + 73.8462, + 75.1282, + 76.6667, + 77.6923, + 79.7436, + 81.7949, + 83.3333, + 85.1282, + 86.4103, + 87.9487, + 89.4872, + 93.3333, + 95.3846, + 98.2051, + 56.6667, + 59.2308, + 60.7692, + 63.0769, + 64.1026, + 64.359, + 74.359, + 71.2821, + 67.9487, + 65.8974, + 63.0769, + 61.2821, + 58.7179, + 55.1282, + 52.3077, + 49.7436, + 47.4359, + 44.8718, + 48.7179, + 51.2821, + 54.1026, + 56.1538, + 52.0513, + 48.7179, + 47.1795, + 46.1538, + 50.5128, + 53.8462, + 57.4359, + 60.0, + 64.1026, + 66.9231, + 71.2821, + 74.359, + 78.2051, + 67.9487, + 68.4615, + 68.2051, + 37.6923, + 39.4872, + 91.2821, + 50.0, + 47.9487, + 44.1026, + ], + [ + 97.1795, + 96.0256, + 94.4872, + 91.4103, + 88.3333, + 84.8718, + 79.8718, + 77.5641, + 74.4872, + 71.4103, + 66.4103, + 61.7949, + 57.1795, + 52.9487, + 51.0256, + 51.0256, + 51.0256, + 51.4103, + 51.4103, + 52.9487, + 54.1026, + 55.2564, + 55.641, + 56.0256, + 57.9487, + 62.1795, + 66.4103, + 69.1026, + 55.2564, + 49.8718, + 46.0256, + 38.3333, + 42.1795, + 44.1026, + 36.4103, + 32.5641, + 31.4103, + 30.2564, + 32.1795, + 36.7949, + 41.4103, + 45.641, + 49.1026, + 36.0256, + 32.1795, + 29.1026, + 26.7949, + 25.2564, + 25.2564, + 25.641, + 28.718, + 31.4103, + 34.8718, + 37.5641, + 40.641, + 42.1795, + 44.4872, + 46.0256, + 46.7949, + 47.9487, + 53.718, + 60.641, + 64.4872, + 69.4872, + 79.8718, + 84.1026, + 85.2564, + 85.2564, + 86.0256, + 86.0256, + 82.9487, + 80.641, + 78.718, + 78.718, + 77.5641, + 59.8718, + 62.1795, + 62.5641, + 99.4872, + 99.1026, + 97.5641, + 94.1026, + 91.0256, + 86.4103, + 83.3333, + 79.1026, + 75.2564, + 71.4103, + 66.7949, + 60.2564, + 55.2564, + 51.4103, + 47.5641, + 46.0256, + 42.5641, + 39.8718, + 36.7949, + 33.718, + 40.641, + 38.3333, + 33.718, + 29.1026, + 25.2564, + 24.1026, + 22.9487, + 22.9487, + 22.1795, + 20.2564, + 19.1026, + 19.1026, + 18.3333, + 18.3333, + 18.3333, + 17.5641, + 16.0256, + 13.718, + 14.8718, + 14.8718, + 14.8718, + 14.1026, + 12.5641, + 11.0256, + 9.8718, + 6.0256, + 9.4872, + 10.2564, + 10.2564, + 10.641, + 10.641, + 10.641, + 10.641, + 10.641, + 10.641, + 8.718, + 5.2564, + 2.9487, + 25.7692, + 25.3846, + 41.5385, + 95.7692, + 95.0, + 92.6923, + ], + ] + ), + np.array( + [ + [ + 51.20389114, + 58.9744699, + 51.87207267, + 48.17993079, + 41.6832004, + 37.8904155, + 39.54897369, + 39.64957388, + 34.75059705, + 27.56083529, + 24.63553998, + 20.95946481, + 20.68914905, + 19.28820474, + 20.02450057, + 35.469523, + 36.89432765, + 39.05554978, + 46.95708015, + 37.31045274, + 40.009672, + 48.01438668, + 53.70377593, + 63.06749989, + 62.04803251, + 59.83996671, + 55.16094182, + 61.27978658, + 60.83491753, + 61.52059065, + 36.91654386, + 38.50219967, + 48.66437073, + 50.2852524, + 42.27633267, + 54.03177562, + 37.32935526, + 41.38952255, + 40.07466666, + 35.34968062, + 34.76370042, + 37.02662945, + 36.45556953, + 35.53766421, + 20.40894789, + 23.49571047, + 29.55754336, + 33.00823391, + 53.98039918, + 52.2343086, + 59.50307661, + 41.16378107, + 48.99304012, + 59.26928032, + 45.469177, + 62.69126654, + 73.42867087, + 70.84642611, + 71.53901985, + 67.62086589, + 72.47095256, + 64.81223756, + 60.85367987, + 67.78949616, + 41.60955727, + 53.00302532, + 54.71417106, + 44.29166872, + 49.19172196, + 53.10138178, + 51.59984815, + 54.37972195, + 46.4807681, + 53.17465627, + 45.27200294, + 36.03340215, + 28.27119417, + 25.05480608, + 64.758887, + 63.14452748, + 50.42467869, + 70.64499626, + 63.14904908, + 62.82402452, + 70.23686951, + 70.04273524, + 72.57062345, + 75.13071604, + 83.29390573, + 79.66426228, + 88.43210253, + 89.11555901, + 89.09219763, + 91.72600577, + 91.73553876, + 91.50788817, + 88.2390019, + 88.5305192, + 55.36516034, + 62.56025887, + 58.00666912, + 55.06711799, + 61.61477596, + 68.54314354, + 77.70610965, + 68.453046, + 68.25720644, + 70.25547467, + 65.04432528, + 60.09224661, + 52.99202897, + 50.14462898, + 46.50861419, + 43.80703196, + 57.81785469, + 50.94049266, + 63.49732308, + 50.01648295, + 58.63676508, + 54.73028909, + 65.8755478, + 57.06098271, + 46.81990795, + 38.35939487, + 47.31541578, + 55.05191654, + 50.51596026, + 49.67741465, + 67.28065952, + 66.17301826, + 61.08854414, + 66.05308577, + 72.66998927, + 61.5034725, + 68.99502863, + 78.24991617, + 36.48198057, + 50.96774838, + 91.19105361, + 55.86376849, + 49.2805948, + 43.36850154, + ], + [ + 83.33977661, + 85.49981761, + 85.82973763, + 85.04511674, + 84.0179406, + 82.567493, + 80.81260177, + 82.66453387, + 80.01109099, + 72.84782559, + 71.61071483, + 66.04149838, + 62.72130521, + 62.06305936, + 61.34262387, + 43.11588495, + 47.70655597, + 55.54697371, + 65.24040739, + 45.2587509, + 60.98658251, + 65.71281959, + 66.38948204, + 64.03500046, + 63.84586325, + 64.47676444, + 65.23730817, + 65.7664025, + 64.60376971, + 64.79185504, + 41.09524744, + 41.56715562, + 30.68066685, + 30.33792211, + 34.52763612, + 29.67234831, + 39.60204231, + 37.29605623, + 34.6236852, + 47.14107313, + 47.62479992, + 44.46229305, + 40.79184303, + 48.72938687, + 32.20303042, + 25.32246815, + 21.36477746, + 15.98507146, + 29.35098671, + 29.71167299, + 30.66967394, + 34.31575825, + 32.03035884, + 29.64070177, + 33.83119273, + 30.29037383, + 48.57785513, + 52.28225333, + 45.52180616, + 38.00655847, + 51.12213482, + 62.81091559, + 65.49914703, + 61.36370155, + 83.84868656, + 84.6747986, + 84.04312807, + 82.90944121, + 85.87622912, + 84.54765869, + 84.81982149, + 84.24035555, + 83.51821167, + 84.26056799, + 85.23707942, + 53.37168776, + 72.84023126, + 71.54859792, + 82.31522364, + 85.23669633, + 85.17474759, + 82.43091876, + 83.94685535, + 84.96618595, + 82.17115106, + 80.38502135, + 80.97121843, + 79.98409314, + 70.77843179, + 73.93230972, + 64.624247, + 64.00150664, + 57.76819305, + 52.62335326, + 48.97021089, + 53.31265209, + 31.47743488, + 30.47603101, + 30.44585028, + 30.44713567, + 30.2537213, + 29.0115352, + 29.99439119, + 35.65783217, + 20.30426019, + 13.03552859, + 12.38463915, + 13.25038497, + 11.00084148, + 11.87211171, + 9.90666848, + 12.21154309, + 11.20713449, + 11.31894489, + 10.94514243, + 9.69154713, + 11.91406917, + 11.93385209, + 11.97472107, + 11.41288267, + 11.73243636, + 9.92056085, + 10.49465268, + 13.43132262, + 12.85345178, + 11.94998862, + 9.76559162, + 10.38313251, + 14.12865153, + 12.03791702, + 10.08453441, + 13.38022601, + 15.23422594, + 10.82841448, + 13.99431053, + 17.88324091, + 15.16276009, + 29.67977429, + 46.67434284, + 85.33648676, + 84.04882283, + 84.3321772, + ], + ] + ), + np.array( + [ + [ + 58.21360826, + 58.19605369, + 58.71823072, + 57.27837287, + 58.08202049, + 57.48944777, + 28.08874132, + 28.08546821, + 28.08727305, + 27.57802522, + 27.77991911, + 28.58899981, + 28.7391415, + 27.02460324, + 28.8013367, + 27.18646384, + 29.2851466, + 39.4029453, + 28.81132844, + 34.30395791, + 29.60276098, + 49.11615686, + 39.61754583, + 43.23308466, + 64.89278794, + 62.49014932, + 68.98808443, + 62.10561863, + 32.46184674, + 41.32720065, + 44.00714993, + 44.07406069, + 44.00131524, + 45.00630045, + 44.44384061, + 42.1787134, + 44.04456562, + 41.64045402, + 41.93833001, + 44.05392751, + 39.20671933, + 28.70444923, + 31.7086629, + 42.81171147, + 43.30061489, + 40.39863291, + 40.43569158, + 40.93654667, + 39.66157367, + 40.89925917, + 41.96861683, + 40.38340582, + 56.53812645, + 52.97069128, + 54.62095259, + 65.09904439, + 63.05599091, + 70.96013623, + 69.89581924, + 70.59589286, + 69.64702143, + 77.39298249, + 64.40078719, + 63.86895983, + 56.59442132, + 56.53133729, + 59.65215837, + 56.6365087, + 58.672288, + 58.22161273, + 57.91466448, + 55.31550906, + 54.57572859, + 54.41309365, + 55.0745059, + 29.43296052, + 29.42268607, + 29.00561416, + 58.46183859, + 57.99780474, + 57.54947408, + 59.52992846, + 58.24939106, + 58.02451401, + 58.38212449, + 62.56675904, + 72.17582431, + 79.47276157, + 80.35770088, + 78.75723614, + 82.54023959, + 86.43589719, + 79.48868442, + 81.53042032, + 79.18678857, + 77.89905795, + 75.13071421, + 76.05801375, + 57.61467439, + 56.17139753, + 66.2878906, + 67.88171962, + 64.0280813, + 77.49665175, + 77.63465176, + 77.86372643, + 77.33815817, + 76.18041653, + 77.25265109, + 77.41337528, + 76.7318494, + 49.47110541, + 42.47653994, + 43.59511586, + 50.33996967, + 40.74898026, + 38.38652558, + 38.40401521, + 38.76427889, + 41.47014233, + 47.15540481, + 39.58256675, + 41.74024382, + 39.31187189, + 41.67984769, + 39.08746445, + 41.48150286, + 77.60608655, + 75.98266152, + 76.94575724, + 77.54372007, + 77.58473984, + 76.82230426, + 77.34857166, + 77.57315269, + 77.97261068, + 41.52891976, + 43.7225508, + 79.32607818, + 56.66397408, + 57.82178923, + 58.2431719, + ], + [ + 91.88189151, + 92.21498865, + 90.31053209, + 89.90760672, + 92.00814501, + 88.08528556, + 63.51079443, + 63.59019695, + 63.12328281, + 62.82103866, + 63.51814752, + 63.02408057, + 62.72086389, + 62.90185886, + 63.38904039, + 63.55872965, + 63.38360583, + 51.1508572, + 61.35785406, + 56.54212591, + 60.15734672, + 63.66000062, + 62.92518796, + 63.16521872, + 65.81417676, + 74.58428961, + 63.2321473, + 75.99087076, + 62.88190292, + 49.07025127, + 46.44967378, + 34.55320389, + 33.90420735, + 38.29901955, + 36.0190833, + 26.49211948, + 35.66223828, + 27.09309542, + 24.99152298, + 33.55639249, + 51.5337157, + 61.7775254, + 58.83775437, + 30.02044842, + 31.5264262, + 16.34700838, + 20.23267068, + 16.91300484, + 15.60935558, + 20.79852895, + 26.4970726, + 21.39122552, + 32.44424547, + 29.04019669, + 30.34452445, + 27.24155756, + 29.70909567, + 41.25950129, + 43.45375927, + 41.96474387, + 44.04444502, + 63.37145906, + 67.44871845, + 70.21373883, + 86.92700622, + 87.49981107, + 87.80946159, + 85.63749556, + 90.07716031, + 90.41101877, + 89.95380277, + 80.25186069, + 77.53628847, + 78.22908659, + 79.81754642, + 60.80177654, + 63.06846482, + 63.39075133, + 90.26532639, + 92.15990861, + 90.74890656, + 88.32727415, + 92.12968148, + 91.69442117, + 90.55347607, + 77.74393476, + 63.12892942, + 63.40868612, + 63.29543754, + 53.33262001, + 56.54105229, + 59.79276181, + 53.65167426, + 56.02536457, + 53.23479185, + 51.82245833, + 23.37244197, + 16.38374969, + 33.82244765, + 32.11798877, + 26.11710975, + 24.23601841, + 27.67268551, + 14.94852356, + 14.46185393, + 14.61067765, + 15.89005466, + 15.91257375, + 15.15151702, + 15.22192798, + 16.21684614, + 25.06301931, + 18.33847356, + 19.99420098, + 26.47139661, + 16.18214166, + 14.58021515, + 14.45194845, + 14.36559047, + 17.27803344, + 22.37793253, + 17.64845284, + 17.82932431, + 15.64071697, + 17.74591901, + 15.12230394, + 18.04743744, + 15.16287254, + 16.30692238, + 15.85847833, + 15.25394915, + 15.83003939, + 15.59516532, + 15.77452924, + 14.78064583, + 14.95569875, + 24.91642519, + 19.0773278, + 52.90039129, + 87.94012501, + 90.69316655, + 92.10432787, + ], + ] + ), + np.array( + [ + [ + 51.14791671, + 50.51712581, + 50.2074802, + 50.06948192, + 50.56284634, + 50.2885278, + 25.58347508, + 25.48358339, + 25.4435257, + 25.56511342, + 25.92884427, + 27.55147826, + 27.53046637, + 27.09557036, + 27.43924961, + 27.87826426, + 27.33886892, + 27.67840297, + 52.63565768, + 52.02521411, + 52.88116479, + 52.95260731, + 52.52055249, + 52.34282206, + 51.92759021, + 52.71377449, + 50.44380279, + 50.21669503, + 52.18418011, + 52.79209735, + 52.58971986, + 52.02884867, + 52.72924658, + 52.88431329, + 52.50930089, + 50.86268433, + 50.89149225, + 25.8551276, + 26.02564455, + 27.89317272, + 27.63996794, + 27.8926589, + 52.79773294, + 27.58063881, + 26.49139853, + 25.98531782, + 26.20141928, + 25.85756947, + 50.70468436, + 50.81197535, + 50.56484556, + 50.93930391, + 50.45885484, + 52.90136407, + 52.68495344, + 52.50008894, + 51.83563726, + 76.9954121, + 77.31060048, + 77.92604434, + 77.25438834, + 76.2431578, + 77.08448437, + 75.2280532, + 50.65835477, + 50.20336581, + 50.9295477, + 50.17867185, + 50.42269806, + 50.46422483, + 50.44927033, + 49.92838028, + 50.48801364, + 49.96490538, + 50.75210826, + 27.42242921, + 27.6740834, + 27.53739532, + 52.26334738, + 51.73728166, + 75.87096369, + 75.24432621, + 75.19829529, + 75.70104153, + 75.47933966, + 75.19456687, + 74.82025396, + 75.16434049, + 75.26335555, + 77.75641893, + 77.95443505, + 77.08333777, + 76.06355025, + 77.68201632, + 76.87808198, + 76.94850272, + 77.86405471, + 75.77145009, + 52.33156913, + 52.59281837, + 50.47704772, + 75.29647509, + 75.57395413, + 75.40052716, + 75.87099084, + 75.60588476, + 75.89557705, + 75.7465632, + 75.14234148, + 50.66177956, + 50.69985064, + 50.91894087, + 50.72525854, + 51.26387123, + 51.25091965, + 50.78515721, + 50.50139658, + 50.73367454, + 50.71137854, + 50.8127449, + 51.01423295, + 50.35352141, + 50.43552957, + 50.63098196, + 51.0668072, + 50.79235473, + 50.55127806, + 50.55975806, + 75.32597855, + 75.04472578, + 75.28708772, + 75.23996998, + 75.1524592, + 75.96184009, + 75.44806251, + 75.75938382, + 50.3782623, + 50.53363501, + 77.50090732, + 50.69112419, + 49.99039495, + 50.12718203, + ], + [ + 90.86741233, + 89.10239459, + 85.4600474, + 83.05766953, + 82.93782178, + 82.97525357, + 82.91489113, + 82.92908498, + 82.8742005, + 82.92409777, + 82.82118411, + 51.48738653, + 51.41484656, + 52.07679944, + 51.71207905, + 50.70890793, + 51.65304675, + 51.18198917, + 51.41855226, + 52.12301105, + 50.62155476, + 50.07473901, + 51.5024421, + 51.86195209, + 52.25779061, + 51.19794432, + 82.94182882, + 83.75234297, + 51.97525067, + 51.07339565, + 51.3380902, + 52.1768375, + 51.20176505, + 50.44143545, + 51.41620515, + 17.14563109, + 17.14132373, + 17.08190869, + 16.92501353, + 50.66196341, + 51.39909748, + 50.79528152, + 50.68603709, + 51.52476126, + 17.40539097, + 17.20372213, + 17.09382391, + 17.11384266, + 17.02374454, + 17.11492526, + 17.07777732, + 16.98102188, + 17.03857897, + 50.69056272, + 51.29446922, + 51.59435617, + 52.33576553, + 52.04552865, + 51.74673004, + 50.31866042, + 51.46182482, + 52.12368985, + 51.9671367, + 82.98566202, + 83.11447934, + 82.98265686, + 82.84604113, + 83.18462233, + 82.90990147, + 82.93532841, + 83.96992038, + 82.99366549, + 83.09951912, + 83.7083177, + 82.9019501, + 51.43887623, + 51.30411215, + 51.59365408, + 94.24932783, + 92.97911753, + 88.38644174, + 83.90349738, + 83.46230334, + 82.91945886, + 82.88405139, + 82.93211578, + 82.96238879, + 83.03499717, + 82.9452793, + 51.15177033, + 50.47557897, + 52.15779927, + 52.10465206, + 51.16563781, + 51.8675623, + 51.90751654, + 49.66254553, + 17.11125121, + 51.87886035, + 51.39159152, + 17.04828941, + 17.01565319, + 17.06219214, + 17.04110689, + 17.13489391, + 17.06772306, + 17.16994971, + 17.10571651, + 16.75492389, + 17.07814052, + 17.08518438, + 17.14760476, + 16.90746981, + 17.16234971, + 17.24045586, + 17.18019648, + 17.10577072, + 16.99296341, + 17.08831585, + 16.57271805, + 17.22109553, + 17.06474308, + 17.0651685, + 17.07652235, + 17.20885971, + 17.20421434, + 17.08465518, + 17.09388377, + 15.77189199, + 17.00426226, + 16.17493491, + 17.03184749, + 17.0049424, + 16.69484223, + 17.04514941, + 16.94292965, + 16.94627981, + 17.01958137, + 50.16698595, + 87.51396042, + 83.99735692, + 82.99075, + ], + ] + ), +] def datasaurus(): @@ -258,7 +1211,7 @@ def datasaurus(): CHI 2017 Conference proceedings: ACM SIGCHI Conference on Human Factors in Computing Systems """ - _, ((axa, axb), (axc, axd)) = plt.subplots(2, 2, sharex='col', sharey='row') + _, ((axa, axb), (axc, axd)) = plt.subplots(2, 2, sharex="col", sharey="row") colors = get_color_cycle() for arr, ax, color in zip(DATASAURUS, (axa, axb, axc, axd), colors): x = arr[0] @@ -277,6 +1230,6 @@ def datasaurus(): return (axa, axb, axc, axd) -if __name__ == '__main__': +if __name__ == "__main__": datasaurus() plt.show() diff --git a/yellowbrick/datasets/__init__.py b/yellowbrick/datasets/__init__.py index 0535e081a..f5c53c6b1 100644 --- a/yellowbrick/datasets/__init__.py +++ b/yellowbrick/datasets/__init__.py @@ -1,9 +1,24 @@ -from .download import load_concrete -from .download import load_energy -from .download import load_credit -from .download import load_occupancy -from .download import load_mushroom -from .download import load_hobbies -from .download import load_game -from .download import load_bikeshare -from .download import load_spam +# yellowbrick.datasets +# Management utilities for Yellowbrick example datasets. +# +# Author: Raul Peralta +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort +# Author: Nathan Danielsen +# Created: Tue May 15 11:54:45 2018 -0400 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: __init__.py [] raulpl25@gmail.com $ + +""" +Management utilities for Yellowbrick example datasets. +""" + +########################################################################## +## Imports +########################################################################## + +from .loaders import * +from .path import get_data_home diff --git a/yellowbrick/datasets/base.py b/yellowbrick/datasets/base.py new file mode 100644 index 000000000..308c4803b --- /dev/null +++ b/yellowbrick/datasets/base.py @@ -0,0 +1,334 @@ +# yellowbrick.datasets.base +# Loading utilities for the yellowbrick datasets. +# +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort +# Author: Raul Peralta +# Created: Thu Jul 26 13:53:01 2018 -0400 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: base.py [] benjamin@bengfort.com $ + +""" +Loading utilities for the yellowbrick datasets. +""" + +########################################################################## +## Imports +########################################################################## + +import os +import json +import numpy as np + +from .download import download_data +from .path import find_dataset_path, dataset_exists + +from yellowbrick.exceptions import DatasetsError +from yellowbrick.utils.decorators import memoized + +try: + import pandas as pd +except ImportError: + pd = None + + +########################################################################## +## Dataset Object +########################################################################## + + +class BaseDataset(object): + """ + Base functionality for Dataset and Corpus objects. + """ + + def __init__(self, name, url=None, signature=None, data_home=None): + self.url = url + self.name = name + self.data_home = data_home + self.signature = signature + + # Check if the dataset exists, and if not - download it! + if not dataset_exists(self.name, data_home=data_home): + self.download() + + def download(self, replace=False): + """ + Download the dataset from the hosted Yellowbrick data store and save + it to the location specified by ``get_data_home``. The downloader + verifies the download completed successfully and safely by comparing + the expected signature with the SHA 256 signature of the downloaded + archive file. + + Parameters + ---------- + replace : bool, default: False + If the data archive already exists, replace the dataset. If this is + False and the dataset exists, an exception is raised. + """ + download_data( + self.url, + self.signature, + data_home=self.data_home, + replace=replace, + extract=True, + ) + + def contents(self): + """ + Contents returns a list of the files in the data directory. + """ + data = find_dataset_path(self.name, data_home=self.data_home, ext=None) + return os.listdir(data) + + @memoized + def README(self): + """ + Returns the contents of the README.md file that describes the dataset + in detail and contains attribution information. + """ + path = find_dataset_path(self.name, data_home=self.data_home, fname="README.md") + with open(path, "r") as f: + return f.read() + + @memoized + def meta(self): + """ + Returns the contents of the meta.json file that describes important + attributes about the dataset and modifies the behavior of the loader. + """ + path = find_dataset_path( + self.name, data_home=self.data_home, fname="meta.json", raises=False + ) + if path is None: + return None + + with open(path, "r") as f: + return json.load(f) + + @memoized + def citation(self): + """ + Returns the contents of the citation.bib file that describes the source + and provenance of the dataset or to cite for academic work. + """ + path = find_dataset_path( + self.name, data_home=self.data_home, fname="meta.json", raises=False + ) + if path is None: + return None + + with open(path, "r") as f: + return f.read() + + +class Dataset(BaseDataset): + """ + Datasets contain a reference to data on disk and provide utilities for + quickly loading files and objects into a variety of formats. The most + common use of the Dataset object is to load example datasets provided by + Yellowbrick to run the examples in the documentation. + + The dataset by default will return the data as a numpy array, however if + Pandas is installed, it is possible to access the data as a DataFrame and + Series object. In either case, the data is represented by a features table, + X and a target vector, y. + + Parameters + ---------- + name : str + The name of the dataset; should either be a folder in data home or + specified in the yellowbrick.datasets.DATASETS variable. This name is + used to perform all lookups and identify the dataset externally. + + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from YELLOWBRICK_DATA or the default returned by ``get_data_home``. + + url : str, optional + The web location where the archive file of the dataset can be + downloaded from. + + signature : str, optional + The signature of the data archive file, used to verify that the latest + version of the data has been downloaded and that the download hasn't + been corrupted or modified in anyway. + """ + + def to_data(self): + """ + Returns the data contained in the dataset as X and y where X is the + features matrix and y is the target vector. If pandas is installed, + the data will be returned as DataFrame and Series objects. Otherwise, + the data will be returned as two numpy arrays. + + Returns + ------- + X : array-like with shape (n_instances, n_features) + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) + A pandas Series or numpy array describing the target vector. + """ + if pd is not None: + return self.to_pandas() + return self.to_numpy() + + def to_numpy(self): + """ + Returns the dataset as two numpy arrays: X and y. + + Returns + ------- + X : array-like with shape (n_instances, n_features) + A numpy array describing the instance features. + + y : array-like with shape (n_instances,) + A numpy array describing the target vector. + """ + path = find_dataset_path(self.name, ext=".npz", data_home=self.data_home) + with np.load(path, allow_pickle=False) as npf: + if "X" not in npf or "y" not in npf: + raise DatasetsError( + ( + "the downloaded dataset was improperly packaged without numpy " + "arrays - please report this bug to the Yellowbrick maintainers!" + ) + ) + + # TODO: How to handle the case where y is None? + return npf["X"], npf["y"] + + def to_pandas(self): + """ + Returns the dataset as two pandas objects: X and y. + + Returns + ------- + X : DataFrame with shape (n_instances, n_features) + A pandas DataFrame containing feature data and named columns. + + y : Series with shape (n_instances,) + A pandas Series containing target data and an index that matches + the feature DataFrame index. + """ + # Ensure the metadata is valid before continuing + if self.meta is None: + raise DatasetsError( + ( + "the downloaded dataset was improperly packaged without meta.json " + "- please report this bug to the Yellowbrick maintainers!" + ) + ) + + if "features" not in self.meta or "target" not in self.meta: + raise DatasetsError( + ( + "the downloaded dataset was improperly packaged without features " + "or target - please report this bug to the Yellowbrick maintainers!" + ) + ) + + # Load data frame and return features and target + # TODO: Return y as None if there is no self.meta["target"] + df = self.to_dataframe() + return df[self.meta["features"]], df[self.meta["target"]] + + def to_dataframe(self): + """ + Returns the entire dataset as a single pandas DataFrame. + + Returns + ------- + df : DataFrame with shape (n_instances, n_columns) + A pandas DataFrame containing the complete original data table + including all targets (specified by the meta data) and all + features (including those that might have been filtered out). + """ + if pd is None: + raise DatasetsError( + "pandas is required to load DataFrame, it can be installed with pip" + ) + + path = find_dataset_path(self.name, ext=".csv.gz", data_home=self.data_home) + return pd.read_csv(path, compression="gzip") + + +class Corpus(BaseDataset): + """ + Corpus datasets contain a reference to documents on disk and provide + utilities for quickly loading text data for use in machine learning + workflows. The most common use of the corpus is to load the text analysis + examples from the Yellowbrick documentation. + + Parameters + ---------- + name : str + The name of the corpus; should either be a folder in data home or + specified in the yellowbrick.datasets.DATASETS variable. This name is + used to perform all lookups and identify the corpus externally. + + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from YELLOWBRICK_DATA or the default returned by ``get_data_home``. + + url : str, optional + The web location where the archive file of the corpus can be + downloaded from. + + signature : str, optional + The signature of the data archive file, used to verify that the latest + version of the data has been downloaded and that the download hasn't + been corrupted or modified in anyway. + """ + + @memoized + def root(self): + """ + Discovers and caches the root directory of the corpus. + """ + return find_dataset_path(self.name, data_home=self.data_home, ext=None) + + @memoized + def labels(self): + """ + Return the unique labels assigned to the documents. + """ + return [ + name + for name in os.listdir(self.root) + if os.path.isdir(os.path.join(self.root, name)) + ] + + @property + def files(self): + """ + Returns the list of file names for all documents. + """ + return [ + os.path.join(self.root, label, name) + for label in self.labels + for name in os.listdir(os.path.join(self.root, label)) + ] + + @property + def data(self): + """ + Read all of the documents from disk into an in-memory list. + """ + + def read(path): + with open(path, "r", encoding="UTF-8") as f: + return f.read() + + return [read(f) for f in self.files] + + @property + def target(self): + """ + Returns the label associated with each item in data. + """ + return [os.path.basename(os.path.dirname(f)) for f in self.files] diff --git a/yellowbrick/datasets/download.py b/yellowbrick/datasets/download.py index 831c7da60..25f398391 100644 --- a/yellowbrick/datasets/download.py +++ b/yellowbrick/datasets/download.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -# download +# yellowbrick.datasets.download # Downloads the example datasets for running the examples. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort -# Author: Raul Peralta +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort +# Author: Raul Peralta # Created: Wed May 18 11:54:45 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # # ID: download.py [1f73d2b] benjamin@bengfort.com $ @@ -21,123 +20,91 @@ ########################################################################## import os -import numpy as np +import zipfile -from .utils import load_numpy, load_corpus, download_data, DATASETS -from .utils import _lookup_path +from urllib.request import urlopen -########################################################################## -## Functions -########################################################################## +from .signature import sha256sum +from .path import get_data_home, cleanup_dataset -FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures") +from yellowbrick.exceptions import DatasetsError -def download_all(data_path=FIXTURES, verify=True): - """ - Downloads all the example datasets. If verify is True then compare the - download signature with the hardcoded signature. If extract is True then - extract the contents of the zipfile to the given path. - """ - for name, meta in DATASETS.items(): - download_data(name, data_dir=data_path) +# Downlod chunk size +CHUNK = 524288 -def load_concrete(data_path=FIXTURES): - """ - Downloads the 'concrete' dataset, saving it to the output - path specified and returns the data. - """ - # name of the dataset - name = 'concrete' - data = load_numpy(name, data_path=data_path) - return data +########################################################################## +## Download functions +########################################################################## -def load_energy(data_path=FIXTURES): - """ - Downloads the 'energy' dataset, saving it to the output - path specified and returns the data. +def download_data(url, signature, data_home=None, replace=False, extract=True): """ - # name of the dataset - name = 'energy' - data = load_numpy(name, data_path=data_path) - return data + Downloads the zipped data set specified at the given URL, saving it to + the data directory specified by ``get_data_home``. This function verifies + the download with the given signature and extracts the archive. + Parameters + ---------- + url : str + The URL of the dataset on the Internet to GET -def load_credit(data_path=FIXTURES): - """ - Downloads the 'credit' dataset, saving it to the output - path specified and returns the data. - """ - # name of the dataset - name = 'credit' - data = load_numpy(name, data_path=data_path) - return data - + signature : str + The SHA 256 hash of the dataset archive being downloaded to verify + that the dataset has been correctly downloaded -def load_occupancy(data_path=FIXTURES): - """ - Downloads the 'occupancy' dataset, saving it to the output - path specified and returns the data. - """ - # name of the dataset - name = 'occupancy' - data = load_numpy(name, data_path=data_path) - return data + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from YELLOWBRICK_DATA or the default returned by ``get_data_home``. + replace : bool, default: False + If the data archive already exists, replace the dataset. If this is + False and the dataset exists, an exception is raised. -def load_mushroom(data_path=FIXTURES): - """ - Downloads the 'mushroom' dataset, saving it to the output - path specified and returns the data. + extract : bool, default: True + Extract the archive file after downloading it """ - # name of the dataset - name = 'mushroom' - data = load_numpy(name, data_path=data_path) - return data + data_home = get_data_home(data_home) + # Get the name of the file from the URL + basename = os.path.basename(url) + name, _ = os.path.splitext(basename) -def load_hobbies(data_path=FIXTURES): - """ - Downloads the 'hobbies' dataset, saving it to the output - path specified and returns the data. - """ - # name of the dataset - name = 'hobbies' - data = load_corpus(name, data_path=data_path) - return data + # Get the archive and data directory paths + archive = os.path.join(data_home, basename) + datadir = os.path.join(data_home, name) + # If the archive exists cleanup or raise override exception + if os.path.exists(archive): + if not replace: + raise DatasetsError( + ("dataset already exists at {}, set replace=False to overwrite").format( + archive + ) + ) -def load_game(data_path=FIXTURES): - """ - Downloads the 'game' dataset, saving it to the output - path specified and returns the data. - """ - # name of the dataset - name = 'game' - path = _lookup_path(name, data_path=data_path) - dtype = np.array(['S1']*42+['|S4']) - return np.genfromtxt(path, dtype=dtype, delimiter=',', names=True) + cleanup_dataset(name, data_home=data_home) + # Create the output directory if it does not exist + if not os.path.exists(datadir): + os.mkdir(datadir) -def load_bikeshare(data_path=FIXTURES): - """ - Downloads the 'bikeshare' dataset, saving it to the output - path specified and returns the data. - """ - # name of the dataset - name = 'bikeshare' - data = load_numpy(name, data_path=data_path) - return data + # Fetch the response in a streaming fashion and write it to disk. + response = urlopen(url) + with open(archive, "wb") as f: + while True: + chunk = response.read(CHUNK) + if not chunk: + break + f.write(chunk) -def load_spam(data_path=FIXTURES): - """ - Downloads the 'spam' dataset, saving it to the output - path specified and returns the data. - """ - # name of the dataset - name = 'spam' - data = load_numpy(name, skip_header=True, data_path=data_path) - return data + # Compare the signature of the archive to the expected one + if sha256sum(archive) != signature: + raise ValueError("Download signature does not match hardcoded signature!") + + # If extract, extract the zipfile. + if extract: + zf = zipfile.ZipFile(archive) + zf.extractall(path=data_home) diff --git a/yellowbrick/datasets/loaders.py b/yellowbrick/datasets/loaders.py new file mode 100644 index 000000000..ba871f641 --- /dev/null +++ b/yellowbrick/datasets/loaders.py @@ -0,0 +1,517 @@ +# yellowbrick.datasets.loaders +# Dataset loading utilities and primary API to the datasets module. +# +# Author: Benjamin Bengfort +# Created: Tue Jul 31 13:31:23 2018 -0400 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: loaders.py [7082742] benjamin@bengfort.com $ + +""" +Dataset loading utilities and primary API to the datasets module. +""" + +########################################################################## +## Imports +########################################################################## + +import os +import json + +from .base import Dataset, Corpus + +__all__ = [ + "load_concrete", + "load_energy", + "load_credit", + "load_occupancy", + "load_mushroom", + "load_hobbies", + "load_game", + "load_bikeshare", + "load_spam", + "load_walking", + "load_nfl", +] + + +########################################################################## +## Links and SHA 256 signature of Yellowbrick hosted datasets +########################################################################## + +MANIFEST = os.path.join(os.path.dirname(__file__), "manifest.json") +with open(MANIFEST, "r") as f: + DATASETS = json.load(f) + + +########################################################################## +## Specific loading utilities +########################################################################## + + +def _load_dataset(name, data_home=None, return_dataset=False): + """ + Load a dataset by name and return specified format. + """ + info = DATASETS[name] + data = Dataset(name, data_home=data_home, **info) + if return_dataset: + return data + return data.to_data() + + +def _load_corpus(name, data_home=None): + """ + Load a corpus object by name. + """ + info = DATASETS[name] + return Corpus(name, data_home=data_home, **info) + + +def load_concrete(data_home=None, return_dataset=False): + """ + Loads the concrete multivariate dataset that is well suited to regression + tasks. The dataset contains 1030 instances and 8 real valued attributes + with a continuous target. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("concrete", data_home, return_dataset) + + +def load_energy(data_home=None, return_dataset=False): + """ + Loads the energy multivariate dataset that is well suited to multi-output + regression and classification tasks. The dataset contains 768 instances and + 8 real valued attributes with two continous targets. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("energy", data_home, return_dataset) + + +def load_credit(data_home=None, return_dataset=False): + """ + Loads the credit multivariate dataset that is well suited to binary + classification tasks. The dataset contains 30000 instances and 23 integer + and real value attributes with a discrete target. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("credit", data_home, return_dataset) + + +def load_occupancy(data_home=None, return_dataset=False): + """ + Loads the occupancy multivariate, time-series dataset that is well suited + to binary classification tasks. The dataset contains 20560 instances with + 5 real valued attributes and a discrete target. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("occupancy", data_home, return_dataset) + + +def load_mushroom(data_home=None, return_dataset=False): + """ + Loads the mushroom multivariate dataset that is well suited to binary + classification tasks. The dataset contains 8123 instances with 3 + categorical attributes and a discrete target. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("mushroom", data_home, return_dataset) + + +def load_hobbies(data_home=None): + """ + Loads the hobbies text corpus that is well suited to classification, + clustering, and text analysis tasks. The dataset contains 448 documents in + 5 categories with 7420 paragraphs, 14251 sentences, 288520 words, and a + vocabulary of 23738. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + Returns + ------- + dataset : Corpus + The Yellowbrick Corpus object provides an interface to accessing the + text documents and metadata associated with the corpus. + """ + return _load_corpus("hobbies", data_home) + + +def load_game(data_home=None, return_dataset=False): + """ + Load the Connect-4 game multivariate and spatial dataset that is well + suited to multiclass classification tasks. The dataset contains 67557 + instances with 42 categorical attributes and a discrete target. + + Note that the game data is stored with categorical features that need to + be numerically encoded before use with scikit-learn estimators. We + recommend the use of the ``sklearn.preprocessing.OneHotEncoder`` for this + task and to develop a ``Pipeline`` using this dataset. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("game", data_home, return_dataset) + + +def load_bikeshare(data_home=None, return_dataset=False): + """ + Loads the bike sharing univariate dataset that is well suited to regression + tasks. The dataset contains 17379 instances with 12 integer and real valued + attributes and a continuous target. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("bikeshare", data_home, return_dataset) + + +def load_spam(data_home=None, return_dataset=False): + """ + Loads the email spam dataset that is weill suited to binary classification + and threshold tasks. The dataset contains 4600 instances with 57 integer and + real valued attributes and a discrete target. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("spam", data_home, return_dataset) + + +def load_walking(data_home=None, return_dataset=False): + """ + Loads the walking activity dataset that is weill suited to clustering and + multi-label classification tasks. The dataset contains multi-variate time + series data with 149,332 real valued measurements across 22 unique walkers. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("walking", data_home, return_dataset) + + +def load_nfl(data_home=None, return_dataset=False): + """ + Loads the football receivers dataset that is well suited to clustering + tasks. The dataset contains 494 instances with 28 integer, real valued, and + categorical attributes and a discrete target. + + The Yellowbrick datasets are hosted online and when requested, the dataset + is downloaded to your local computer for use. Note that if the dataset + hasn't been downloaded before, an Internet connection is required. However, + if the data is cached locally, no data will be downloaded. Yellowbrick + checks the known signature of the dataset with the data downloaded to + ensure the download completes successfully. + + Datasets are stored alongside the code, but the location can be specified + with the ``data_home`` parameter or the ``$YELLOWBRICK_DATA`` envvar. + + Parameters + ---------- + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from ``$YELLOWBRICK_DATA`` or the default returned by ``get_data_home``. + + return_dataset : bool, default=False + Return the raw dataset object instead of X and y numpy arrays to + get access to alternative targets, extra features, content and meta. + + Returns + ------- + X : array-like with shape (n_instances, n_features) if return_dataset=False + A pandas DataFrame or numpy array describing the instance features. + + y : array-like with shape (n_instances,) if return_dataset=False + A pandas Series or numpy array describing the target vector. + + dataset : Dataset instance if return_dataset=True + The Yellowbrick Dataset object provides an interface to accessing the + data in a variety of formats as well as associated metadata and content. + """ + return _load_dataset("nfl", data_home, return_dataset) diff --git a/yellowbrick/datasets/manifest.json b/yellowbrick/datasets/manifest.json new file mode 100644 index 000000000..ea071ccf0 --- /dev/null +++ b/yellowbrick/datasets/manifest.json @@ -0,0 +1,46 @@ +{ + "bikeshare": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/bikeshare.zip", + "signature": "4ed07a929ccbe0171309129e6adda1c4390190385dd6001ba9eecc795a21eef2" + }, + "hobbies": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/hobbies.zip", + "signature": "6114e32f46baddf049a18fb05bad3efa98f4e6a0fe87066c94071541cb1e906f" + }, + "concrete": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/concrete.zip", + "signature": "5807af2f04e14e407f61e66a4f3daf910361a99bb5052809096b47d3cccdfc0a" + }, + "credit": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/credit.zip", + "signature": "2c6f5821c4039d70e901cc079d1404f6f49c3d6815871231c40348a69ae26573" + }, + "energy": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/energy.zip", + "signature": "174eca3cd81e888fc416c006de77dbe5f89d643b20319902a0362e2f1972a34e" + }, + "game": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/game.zip", + "signature": "ce799d1c55fcf1985a02def4d85672ac86c022f8f7afefbe42b20364fba47d7a" + }, + "mushroom": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/mushroom.zip", + "signature": "f79fdbc33b012dabd06a8f3cb3007d244b6aab22d41358b9aeda74417c91f300" + }, + "occupancy": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/occupancy.zip", + "signature": "0b390387584586a05f45c7da610fdaaf8922c5954834f323ae349137394e6253" + }, + "spam": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/spam.zip", + "signature": "000309ac2b61090a3001de3e262a5f5319708bb42791c62d15a08a2f9f7cb30a" + }, + "walking": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/walking.zip", + "signature": "7a36615978bc3bb74a2e9d5de216815621bd37f6a42c65d3fc28b242b4d6e040" + }, + "nfl": { + "url": "https://s3.amazonaws.com/ddl-data-lake/yellowbrick/v1.0/nfl.zip", + "signature": "4989c66818ea18217ee0fe3a59932b963bd65869928c14075a5c50366cb81e1f" + } +} diff --git a/yellowbrick/datasets/path.py b/yellowbrick/datasets/path.py new file mode 100644 index 000000000..6b40deda3 --- /dev/null +++ b/yellowbrick/datasets/path.py @@ -0,0 +1,240 @@ +# yellowbrick.datasets.path +# Helper functions for looking up dataset paths. +# +# Author: Benjamin Bengfort +# Created: Thu Jul 26 14:10:51 2018 -0400 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: path.py [7082742] benjamin@bengfort.com $ + +""" +Helper functions for looking up dataset paths. +""" + +########################################################################## +## Imports +########################################################################## + +import os +import shutil + +from .signature import sha256sum +from yellowbrick.exceptions import DatasetsError + + +########################################################################## +## Fixtures +########################################################################## + +FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures") + + +########################################################################## +## Dataset path utilities +########################################################################## + + +def get_data_home(path=None): + """ + Return the path of the Yellowbrick data directory. This folder is used by + dataset loaders to avoid downloading data several times. + + By default, this folder is colocated with the code in the install directory + so that data shipped with the package can be easily located. Alternatively + it can be set by the ``$YELLOWBRICK_DATA`` environment variable, or + programmatically by giving a folder path. Note that the ``'~'`` symbol is + expanded to the user home directory, and environment variables are also + expanded when resolving the path. + """ + if path is None: + path = os.environ.get("YELLOWBRICK_DATA", FIXTURES) + + path = os.path.expanduser(path) + path = os.path.expandvars(path) + + if not os.path.exists(path): + os.makedirs(path) + + return path + + +def find_dataset_path(dataset, data_home=None, fname=None, ext=".csv.gz", raises=True): + """ + Looks up the path to the dataset specified in the data home directory, + which is found using the ``get_data_home`` function. By default data home + is colocated with the code, but can be modified with the YELLOWBRICK_DATA + environment variable, or passing in a different directory. + + The file returned will be by default, the name of the dataset in compressed + CSV format. Other files and extensions can be passed in to locate other data + types or auxilliary files. + + If the dataset is not found a ``DatasetsError`` is raised by default. + + Parameters + ---------- + dataset : str + The name of the dataset; should either be a folder in data home or + specified in the yellowbrick.datasets.DATASETS variable. + + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from YELLOWBRICK_DATA or the default returned by ``get_data_home``. + + fname : str, optional + The filename to look up in the dataset path, by default it will be the + name of the dataset. The fname must include an extension. + + ext : str, default: ".csv.gz" + The extension of the data to look up in the dataset path, if the fname + is specified then the ext parameter is ignored. If ext is None then + the directory of the dataset will be returned. + + raises : bool, default: True + If the path does not exist, raises a DatasetsError unless this flag is set + to False, at which point None is returned (e.g. for checking if the + path exists or not). + + Returns + ------- + path : str or None + A path to the requested file, guaranteed to exist if an exception is + not raised during processing of the request (unless None is returned). + + raises : DatasetsError + If raise is True and the path does not exist, raises a DatasetsError. + """ + # Figure out the root directory of the datasets + data_home = get_data_home(data_home) + + # Figure out the relative path to the dataset + if fname is None: + if ext is None: + path = os.path.join(data_home, dataset) + else: + path = os.path.join(data_home, dataset, "{}{}".format(dataset, ext)) + else: + path = os.path.join(data_home, dataset, fname) + + # Determine if the path exists + if not os.path.exists(path): + + # Suppress exceptions if required + if not raises: + return None + + raise DatasetsError( + ("could not find dataset at {} - does it need to be downloaded?").format( + path + ) + ) + + return path + + +def dataset_exists(dataset, data_home=None): + """ + Checks to see if a directory with the name of the specified dataset exists + in the data home directory, found with ``get_data_home``. + + Parameters + ---------- + dataset : str + The name of the dataset; should either be a folder in data home or + specified in the yellowbrick.datasets.DATASETS variable. + + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from YELLOWBRICK_DATA or the default returned by ``get_data_home``. + + Returns + ------- + exists : bool + If a folder with the dataset name is in the data home directory. + """ + data_home = get_data_home(data_home) + path = os.path.join(data_home, dataset) + + return os.path.exists(path) and os.path.isdir(path) + + +def dataset_archive(dataset, signature, data_home=None, ext=".zip"): + """ + Checks to see if the dataset archive file exists in the data home directory, + found with ``get_data_home``. By specifying the signature, this function + also checks to see if the archive is the latest version by comparing the + sha256sum of the local archive with the specified signature. + + Parameters + ---------- + dataset : str + The name of the dataset; should either be a folder in data home or + specified in the yellowbrick.datasets.DATASETS variable. + + signature : str + The SHA 256 signature of the dataset, used to determine if the archive + is the latest version of the dataset or not. + + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from YELLOWBRICK_DATA or the default returned by ``get_data_home``. + + ext : str, default: ".zip" + The extension of the archive file. + + Returns + ------- + exists : bool + True if the dataset archive exists and is the latest version. + """ + data_home = get_data_home(data_home) + path = os.path.join(data_home, dataset + ext) + + if os.path.exists(path) and os.path.isfile(path): + return sha256sum(path) == signature + + return False + + +def cleanup_dataset(dataset, data_home=None, ext=".zip"): + """ + Removes the dataset directory and archive file from the data home directory. + + Parameters + ---------- + dataset : str + The name of the dataset; should either be a folder in data home or + specified in the yellowbrick.datasets.DATASETS variable. + + data_home : str, optional + The path on disk where data is stored. If not passed in, it is looked + up from YELLOWBRICK_DATA or the default returned by ``get_data_home``. + + ext : str, default: ".zip" + The extension of the archive file. + + Returns + ------- + removed : int + The number of objects removed from data_home. + """ + removed = 0 + data_home = get_data_home(data_home) + + # Paths to remove + datadir = os.path.join(data_home, dataset) + archive = os.path.join(data_home, dataset + ext) + + # Remove directory and contents + if os.path.exists(datadir): + shutil.rmtree(datadir) + removed += 1 + + # Remove the archive file + if os.path.exists(archive): + os.remove(archive) + removed += 1 + + return removed diff --git a/yellowbrick/datasets/signature.py b/yellowbrick/datasets/signature.py new file mode 100644 index 000000000..3b73a07bd --- /dev/null +++ b/yellowbrick/datasets/signature.py @@ -0,0 +1,38 @@ +# yellowbrick.datasets.signature +# Performs SHA 256 hashing of a file for dataset archive verification. +# +# Author: Benjamin Bengfort +# Created: Tue Jul 31 14:18:11 2018 -0400 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: signature.py [7082742] benjamin@bengfort.com $ + +""" +Performs SHA 256 hashing of a file for dataset archive verification. +""" + +########################################################################## +## Imports +########################################################################## + +import hashlib + +########################################################################## +## Signature checking utility +########################################################################## + + +def sha256sum(path, blocksize=65536): + """ + Computes the SHA256 signature of a file to verify that the file has not + been modified in transit and that it is the correct version of the data. + """ + sig = hashlib.sha256() + with open(path, "rb") as f: + buf = f.read(blocksize) + while len(buf) > 0: + sig.update(buf) + buf = f.read(blocksize) + return sig.hexdigest() diff --git a/yellowbrick/datasets/utils.py b/yellowbrick/datasets/utils.py deleted file mode 100644 index 5abd0002d..000000000 --- a/yellowbrick/datasets/utils.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python -""" -Utils for downloading datasets for running the examples. -""" - -########################################################################## -## Imports -########################################################################## - -import os -import six -import hashlib -import zipfile -import numpy as np - -from sklearn.datasets.base import Bunch - -if six.PY2: - # backport for encoding in open for python2 - from io import open - -try: - from urllib.request import urlopen -except ImportError: - # python 2 - from urllib2 import urlopen - -try: - import pandas as pd -except ImportError: - pd = None - -########################################################################## -## Links and MD5 hash of datasets -########################################################################## - -DATASETS = { - 'concrete': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/concrete.zip', - 'signature': 'b9ea5f26a7bb272a040e2f1a993b26babbf8dc4a04ab8198bb315ca66d71f10d', - 'type': 'numpy', - }, - 'energy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/energy.zip', - 'signature': '19fb86f3bcdde208eed46944172cb643ef6a7d58da103fb568fae43205ed89d3', - 'type': 'numpy', - }, - 'credit': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/credit.zip', - 'signature': '4a91339c69f55e18f3f48004328fbcb7868070b618208fed099920427b084e5e', - 'type': 'numpy', - }, - 'occupancy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/occupancy.zip', - 'signature': '429cfe376dc9929a1fa528da89f0e1626e34e19695f3f555d8954025bbc522b8', - 'type': 'numpy', - }, - 'mushroom': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/mushroom.zip', - 'signature': '884c43cb70db35d211c67b1cf6a3683b2b4569393d2789d5c07840da4dc85ba8', - 'type': 'numpy', - }, - 'game': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/game.zip', - 'signature': 'b1bd85789a014a898daa34cb5f89ceab6d2cd6488a2e572187e34aa4ec21a43b', - 'type': 'numpy', - }, - 'bikeshare': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/bikeshare.zip', - 'signature': 'a9b440f65549746dff680c92ff8bdca3c7265f09db1cf09e708e6e26fc8aba44', - 'type': 'numpy', - }, - 'spam': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/spam.zip', - 'signature': '65be21196ba3d8448847409b70a67d761f873f30719c807600eb516d7aef1de1', - 'type': 'numpy', - }, - 'hobbies': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/hobbies.zip', - 'signature': '415c8f68df1486d5d84a1d1757a5aa3035aef5ad63ede5013c261d622fbd29d8', - 'type': 'corpus', - }, -} - - -########################################################################## -## Download functions -########################################################################## - -def sha256sum(path, blocksize=65536): - """ - Computes the SHA256 signature of a file to verify that the file has not - been modified in transit and that it is the correct version of the data. - """ - sig = hashlib.sha256() - with open(path, 'rb') as f: - buf = f.read(blocksize) - while len(buf) > 0: - sig.update(buf) - buf = f.read(blocksize) - return sig.hexdigest() - - -def download_data(name, data_dir=None, signature=None, extract=True): - """ - Downloads the zipped data set specified at the given URL, saving it to - the output path specified. This function verifies the download with the - given signature (if supplied) and extracts the zip file if requested. - """ - - # Create the fixture directory - if not os.path.exists(data_dir): - os.mkdir(data_dir) - - dataset = DATASETS[name] - url = dataset['url'] - - # Get the name of the file from the URL - filename = os.path.basename(url) - dlpath = os.path.join(data_dir, filename) - dataset_path = os.path.join(data_dir, name) - - #Create the output directory if it does not exist - if not os.path.exists(dataset_path): - os.mkdir(dataset_path) - - # Fetch the response in a streaming fashion and write it to disk. - response = urlopen(url) - CHUNK = 16 * 1024 - with open(dlpath, 'wb') as f: - - while True: - chunk = response.read(CHUNK) - if not chunk: - break - f.write(chunk) - - # If verify, compare the signature - if signature is not None: - dlsignature = sha256sum(dlpath) - if signature != dlsignature: - raise ValueError( - "Download signature does not match hardcoded signature!" - ) - - # If extract, extract the zipfile. - if extract: - zf = zipfile.ZipFile(dlpath) - zf.extractall(path=data_dir) - -def load_numpy(name, data_path=None, **kwargs): - """ - Loads the numpy matrix from the specified data set, downloads it if - it hasn't already been downloaded. - """ - - path = _lookup_path(name, data_path=data_path) - return np.genfromtxt(path, dtype=float, delimiter=',', names=True, **kwargs) - - -def load_corpus(name, data_path=None): - """ - Loads a sklearn Bunch with the corpus and downloads it if it hasn't - already been downloaded. Used to test text visualizers. - """ - path = _lookup_path(name, data_path=data_path, ext=None) - - # Read the directories in the directory as the categories. - categories = [ - cat for cat in os.listdir(path) - if os.path.isdir(os.path.join(path, cat)) - ] - - files = [] # holds the file names relative to the root - data = [] # holds the text read from the file - target = [] # holds the string of the category - - # Load the data from the files in the corpus - for cat in categories: - for name in os.listdir(os.path.join(path, cat)): - files.append(os.path.join(path, cat, name)) - target.append(cat) - - with open(os.path.join(path, cat, name), 'r', encoding='UTF-8') as f: - data.append(f.read()) - - # Return the data bunch for use similar to the newsgroups example - return Bunch( - categories=categories, - files=files, - data=data, - target=target, - ) - -def _lookup_path(name, data_path=None, ext=".csv"): - """ - Looks up the path to the dataset, downloading it if necessary - """ - if ext is None: - path = os.path.join(data_path, name) - else: - path = os.path.join(data_path, name, "{}{}".format(name, ext)) - - if not os.path.exists(path): - download_data(name, signature=None, extract=True, data_dir=data_path) - - return path diff --git a/yellowbrick/download.py b/yellowbrick/download.py index bd73e4752..a45ba958c 100644 --- a/yellowbrick/download.py +++ b/yellowbrick/download.py @@ -1,12 +1,11 @@ -#!/usr/bin/env python # yellowbrick.download # Downloads the example datasets for running the examples. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Wed May 18 11:54:45 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The sckit-yb developers # For license information, see LICENSE.txt # # ID: download.py [1f73d2b] benjamin@bengfort.com $ @@ -20,130 +19,86 @@ ########################################################################## import os -import sys -import hashlib -import zipfile +import argparse -try: - import requests -except ImportError: - print(( - "The requests module is required to download data --\n" - "please install it with pip install requests." - )) - sys.exit(1) +from yellowbrick.datasets import get_data_home +from yellowbrick.datasets.loaders import DATASETS +from yellowbrick.datasets.download import download_data +from yellowbrick.datasets.path import cleanup_dataset ########################################################################## -## Links and MD5 hash of datasets +## Functions ########################################################################## -DATASETS = { - 'concrete': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/concrete.zip', - 'signature': 'b9ea5f26a7bb272a040e2f1a993b26babbf8dc4a04ab8198bb315ca66d71f10d', - }, - 'energy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/energy.zip', - 'signature': '19fb86f3bcdde208eed46944172cb643ef6a7d58da103fb568fae43205ed89d3', - }, - 'credit': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/credit.zip', - 'signature': '4a91339c69f55e18f3f48004328fbcb7868070b618208fed099920427b084e5e', - }, - 'occupancy': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/occupancy.zip', - 'signature': '429cfe376dc9929a1fa528da89f0e1626e34e19695f3f555d8954025bbc522b8', - }, - 'mushroom': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/mushroom.zip', - 'signature': '884c43cb70db35d211c67b1cf6a3683b2b4569393d2789d5c07840da4dc85ba8', - }, - 'hobbies': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/hobbies.zip', - 'signature': '415c8f68df1486d5d84a1d1757a5aa3035aef5ad63ede5013c261d622fbd29d8', - }, - 'game': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/game.zip', - 'signature': 'b1bd85789a014a898daa34cb5f89ceab6d2cd6488a2e572187e34aa4ec21a43b', - }, - 'bikeshare': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/bikeshare.zip', - 'signature': 'a9b440f65549746dff680c92ff8bdca3c7265f09db1cf09e708e6e26fc8aba44', - }, - 'spam': { - 'url': 'https://s3.amazonaws.com/ddl-data-lake/yellowbrick/spam.zip', - 'signature': '65be21196ba3d8448847409b70a67d761f873f30719c807600eb516d7aef1de1', - }, -} - -########################################################################## -## Download functions -########################################################################## - -def sha256sum(path, blocksize=65536): +def download_all(data_home=None, replace=False): """ - Computes the SHA256 signature of a file to verify that the file has not - been modified in transit and that it is the correct version of the data. + Downloads all the example datasets to the data directory specified by + ``get_data_home``. This function ensures that all datasets are available + for use with the examples. """ - sig = hashlib.sha256() - with open(path, 'rb') as f: - buf = f.read(blocksize) - while len(buf) > 0: - sig.update(buf) - buf = f.read(blocksize) - return sig.hexdigest() + for _, meta in DATASETS.items(): + download_data( + meta["url"], meta["signature"], data_home=data_home, replace=replace + ) + print( + "Downloaded {} datasets to {}".format(len(DATASETS), get_data_home(data_home)) + ) -def download_data(url, path='data', signature=None, extract=True): - """ - Downloads the zipped data set specified at the given URL, saving it to - the output path specified. This function verifies the download with the - given signature (if supplied) and extracts the zip file if requested. - """ - # Create the output directory if it does not exist - if not os.path.exists(path): - os.mkdir(path) - - # Get the name of the file from the URL - name = os.path.basename(url) - dlpath = os.path.join(path, name) - - # Fetch the response in a streaming fashion and write it to disk. - response = requests.get(url, stream=True) - with open(dlpath, 'wb') as f: - for chunk in response.iter_content(65536): - f.write(chunk) - - # If verify, compare the signature - if signature is not None: - dlsignature = sha256sum(dlpath) - if signature != dlsignature: - raise ValueError( - "Download signature does not match hardcoded signature!" - ) - - # If extract, extract the zipfile. - if extract: - zf = zipfile.ZipFile(dlpath) - zf.extractall(path) - - -def download_all(path='data', verify=True, extract=True): + +def cleanup_all(data_home=None): """ - Downloads all the example datasets. If verify is True then compare the - download signature with the hardcoded signature. If extract is True then - extract the contents of the zipfile to the given path. + Cleans up all the example datasets in the data directory specified by + ``get_data_home`` either to clear up disk space or start from fresh. """ + removed = 0 for name, meta in DATASETS.items(): - url = meta['url'] - signature = meta['signature'] if verify else None - - download_data(url, path=path, signature=signature, extract=extract) - - -if __name__ == '__main__': - path='data' - download_all(path) - print("Downloaded datasets to {}".format(os.path.abspath(path))) + _, ext = os.path.splitext(meta["url"]) + removed += cleanup_dataset(name, data_home=data_home, ext=ext) + + print( + "Removed {} fixture objects from {}".format(removed, get_data_home(data_home)) + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Yellowbrick data downloader", + epilog="for troubleshooting please visit our GitHub issues", + ) + parser.add_argument( + "-c", + "--cleanup", + action="store_true", + default=False, + help="cleanup any existing datasets before download", + ) + parser.add_argument( + "--no-download", + action="store_true", + default=False, + help="prevent new data from being downloaded", + ) + parser.add_argument( + "-f", + "--overwrite", + action="store_true", + default=False, + help="overwrite any existing data with new download", + ) + parser.add_argument( + "data_home", + default=None, + nargs="?", + help="specify the data download location or set $YELLOWBRICK_DATA", + ) + + args = parser.parse_args() + + if args.cleanup: + cleanup_all(data_home=args.data_home) + + if not args.no_download: + download_all(data_home=args.data_home, replace=args.overwrite) diff --git a/yellowbrick/draw.py b/yellowbrick/draw.py index e7b4f80c2..a3fcca4a7 100644 --- a/yellowbrick/draw.py +++ b/yellowbrick/draw.py @@ -1,10 +1,13 @@ # yellowbrick.draw # Utilities for common matplotlib drawing procedures. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Sun Aug 19 10:35:50 2018 -0400 # -# ID: draw.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The sckit-yb developers +# For license information, see LICENSE.txt +# +# ID: draw.py [dd915ad] benjamin@bengfort.com $ """ Utilities for common matplotlib drawing procedures. @@ -16,16 +19,18 @@ from .base import Visualizer from .exceptions import YellowbrickValueError +from .style.colors import resolve_colors from matplotlib import patches import matplotlib.pyplot as plt - +import numpy as np ########################################################################## ## Legend Drawing Utilities ########################################################################## + def manual_legend(g, labels, colors, **legend_kwargs): """ Adds a manual legend for a scatter plot to the visualizer where the labels @@ -82,9 +87,108 @@ def manual_legend(g, labels, colors, **legend_kwargs): # Create the legend handles with the associated colors and labels handles = [ - patches.Patch(color=color, label=label) - for color, label in zip(colors, labels) + patches.Patch(color=color, label=label) for color, label in zip(colors, labels) ] # Return the Legend artist return g.legend(handles=handles, **legend_kwargs) + + +def bar_stack( + data, + ax=None, + labels=None, + ticks=None, + colors=None, + colormap=None, + orientation="vertical", + legend=True, + legend_kws=None, + **kwargs +): + """ + An advanced bar chart plotting utility that can draw bar and stacked bar charts from + data, wrapping calls to the specified matplotlib.Axes object. + + Parameters + ---------- + data : 2D array-like + The data passed to the Visualizer. Rows represent each stack in the bar chart and columns + represent each bar. Therefore, a single bar chart is created by passing a 2D array + containing a single row, while the data to create a bar chart with 3 stacks would have a + shape of (3, b). + + ax : matplotlib.Axes, default: None + The axes object to draw the barplot on, uses plt.gca() if not specified. + + labels : list of str, default: None + The labels for each row in the bar stack, used to create a legend. + + ticks : list of str, default: None + The labels for each bar, added to the x-axis for a vertical plot, or the y-axis + for a horizontal plot. + + colors : array-like, default: None + Specify the colors of each bar, each row in the stack, or every segment. + + colormap : string or matplotlib cmap + Specify a colormap for each bar, each row in the stack, or every segment. + + orientation:‘vertical’ or ‘horizontal’ + Specifies a horizontal or vertical bar chart. + + legend : boolean, default: True + If True, the function add a legend with the plot + + legend_kws : dict, default: None + Additional keyword arguments for the legend components. + + kwargs : dict + Additional keyword arguments to pass to ``ax.bar``. + """ + if ax is None: + ax = plt.gca() + + colors = resolve_colors(n_colors=data.shape[0], colormap=colormap, colors=colors) + + idx = np.arange(data.shape[1]) + zeros = np.zeros(data.shape[1]) + # Stores stacks for both side of plotting axes + stack_arr = np.zeros((data.shape[1], 2)) + orientation = orientation.lower() + + if orientation.startswith("h"): + + for rdx in range(len(data)): + stack = [stack_arr[j, int(data[rdx][j] > 0)] for j in range(len(data[rdx]))] + ax.barh(idx, data[rdx], left=stack, color=colors[rdx]) + # Updates the stack for negative side of y-axis + stack_arr[:, 0] += np.minimum(data[rdx], zeros) + # Updates stack for positive side of y-axis + stack_arr[:, 1] += np.maximum(data[rdx], zeros) + ax.set_yticks(idx) + if ticks is not None: + ax.set_yticklabels(ticks) + + elif orientation.startswith("v"): + for rdx in range(len(data)): + stack = [stack_arr[j, int(data[rdx][j] > 0)] for j in range(len(data[rdx]))] + ax.bar(idx, data[rdx], bottom=stack, color=colors[rdx]) + # Updates the stack for negative side of x-axis + stack_arr[:, 0] += np.minimum(data[rdx], zeros) + # Updates the stack for negative side of x-axis + stack_arr[:, 1] += np.maximum(data[rdx], zeros) + ax.set_xticks(idx) + if ticks is not None: + ax.set_xticklabels(ticks, rotation=90) + + else: + raise YellowbrickValueError("unknown orientation '{}'".format(orientation)) + + # Generates default labels is labels are not specified. + labels = labels or np.arange(data.shape[0]) + + if legend: + legend_kws = legend_kws or {} + manual_legend(ax, labels=labels, colors=colors, **legend_kws) + return ax diff --git a/yellowbrick/exceptions.py b/yellowbrick/exceptions.py index 772b4741a..2bf03247e 100644 --- a/yellowbrick/exceptions.py +++ b/yellowbrick/exceptions.py @@ -1,10 +1,10 @@ # yellowbrick.exceptions # Exceptions hierarchy for the yellowbrick library # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Jun 03 10:39:41 2016 -0700 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The sckit-yb developers # For license information, see LICENSE.txt # # ID: exceptions.py [cb75e0e] benjamin@bengfort.com $ @@ -22,6 +22,7 @@ class YellowbrickError(Exception): """ The root exception for all yellowbrick related errors. """ + pass @@ -29,6 +30,7 @@ class VisualError(YellowbrickError): """ A problem when interacting with matplotlib or the display framework. """ + pass @@ -36,6 +38,7 @@ class ModelError(YellowbrickError): """ A problem when interacting with sklearn or the ML framework. """ + pass @@ -43,6 +46,22 @@ class NotFitted(ModelError): """ An action was called that requires a fitted model. """ + + @classmethod + def from_estimator(klass, estimator, method=None): + method = method or "this method" + message = ( + "this {} instance is not fitted yet, please call fit " + "with the appropriate arguments before using {}" + ).format(estimator.__class__.__name__, method) + return klass(message) + + +class DatasetsError(YellowbrickError): + """ + A problem occured when interacting with data sets. + """ + pass @@ -50,6 +69,7 @@ class YellowbrickTypeError(YellowbrickError, TypeError): """ There was an unexpected type or none for a property or input. """ + pass @@ -57,6 +77,7 @@ class YellowbrickValueError(YellowbrickError, ValueError): """ A bad value was passed into a function. """ + pass @@ -64,32 +85,47 @@ class YellowbrickKeyError(YellowbrickError, KeyError): """ An invalid key was used in a hash (dict or set). """ + pass -class YellowbrickWarning(UserWarning): +########################################################################## +## Assertions +########################################################################## + + +class YellowbrickAssertionError(YellowbrickError, AssertionError): """ - Warning class used to notify users of Yellowbrick-specific issues. + Used to indicate test failures. """ + pass -class DataWarning(YellowbrickWarning): +class ImageComparisonFailure(YellowbrickAssertionError): """ - The supplied data has an issue that may produce unexpected visualizations. + Provides a cleaner error when image comparison assertions fail. """ + pass -class YellowbrickAssertionError(YellowbrickError, AssertionError): +########################################################################## +## Warnings +########################################################################## + + +class YellowbrickWarning(UserWarning): """ - Used to indicate test failures. + Warning class used to notify users of Yellowbrick-specific issues. """ + pass -class ImageComparisonFailure(YellowbrickAssertionError): +class DataWarning(YellowbrickWarning): """ - Provides a cleaner error when image comparison assertions fail. + The supplied data has an issue that may produce unexpected visualizations. """ + pass diff --git a/yellowbrick/features/__init__.py b/yellowbrick/features/__init__.py index c42917e8e..7b7fc0fc2 100644 --- a/yellowbrick/features/__init__.py +++ b/yellowbrick/features/__init__.py @@ -1,10 +1,10 @@ # yellowbrick.features # Visualizers for feature analysis and diagnostics. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Mon Oct 03 21:30:18 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [0f4b236] benjamin@bengfort.com $ @@ -21,8 +21,14 @@ from .pcoords import ParallelCoordinates, parallel_coordinates from .radviz import RadialVisualizer, RadViz, radviz from .rankd import Rank1D, rank1d, Rank2D, rank2d -from .jointplot import JointPlotVisualizer -from .pca import PCADecomposition, pca_decomposition -from .importances import FeatureImportances, feature_importances -from .rfecv import RFECV, rfecv +from .jointplot import JointPlot, JointPlotVisualizer, joint_plot +from .pca import PCA, PCADecomposition, pca_decomposition from .manifold import Manifold, manifold_embedding + +# Alias the TargetType defined in yellowbrick.utils.target +from yellowbrick.utils.target import TargetType + +# RFECV and Feature Importances moved to model selection module as of YB v1.0 +from yellowbrick.model_selection.rfecv import RFECV, rfecv +from yellowbrick.model_selection.importances import FeatureImportances +from yellowbrick.model_selection.importances import feature_importances diff --git a/yellowbrick/features/base.py b/yellowbrick/features/base.py index 5623480eb..e65344277 100644 --- a/yellowbrick/features/base.py +++ b/yellowbrick/features/base.py @@ -1,11 +1,11 @@ # yellowbrick.features.base # Base classes for feature visualizers and feature selection tools. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Fri Oct 07 13:41:24 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: base.py [2e898a6] benjamin@bengfort.com $ @@ -19,9 +19,17 @@ ########################################################################## import numpy as np +import matplotlib as mpl from yellowbrick.base import Visualizer from yellowbrick.utils import is_dataframe +from yellowbrick.style import resolve_colors +from yellowbrick.exceptions import NotFitted +from yellowbrick.utils.target import target_color_type, TargetType +from yellowbrick.exceptions import YellowbrickKeyError, YellowbrickValueError +from yellowbrick.style import palettes + +from matplotlib.colors import Normalize from sklearn.base import TransformerMixin @@ -31,171 +39,311 @@ class FeatureVisualizer(Visualizer, TransformerMixin): - """ - Base class for feature visualization to investigate features - individually or together. + """Base class for feature visualization. + + Feature engineering is primarily conceptualized as a transformation or + extraction operation, e.g. some raw data is passed through a series of + transformers and mappings to result in some final dataset which can be + directly fitted to a model. Therefore feature visualizers are + transformers and support the sklearn transformer interface by implementing + a transform method. + + Subclasses of the FeatureVisualizer may call draw either from fit or from + transform but must implement both so that they can be supported in pipeline + objects. By default, the transform method of the visualizer is just a data + pass through that ensures the visualizer can be placed into a feature + extraction workflow. + + Parameters + ---------- + ax : matplotlib.Axes, default: None + The axis to plot the figure on. If None is passed in the current axes + will be used (or generated if required). - FeatureVisualizer is itself a transformer so that it can be used in - a Scikit-Learn Pipeline to perform automatic visual analysis during build. + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). - Accepts as input a DataFrame or Numpy array. + kwargs : dict + Any additional keyword arguments to pass to the base Visualizer. """ - def __init__(self, ax=None, **kwargs): - super(FeatureVisualizer, self).__init__(ax=ax, **kwargs) + def __init__(self, ax=None, fig=None, **kwargs): + super(FeatureVisualizer, self).__init__(ax=ax, fig=fig, **kwargs) - def transform(self, X): + def transform(self, X, y=None): """ - Primarily a pass-through to ensure that the feature visualizer will - work in a pipeline setting. This method can also call drawing methods - in order to ensure that the visualization is constructed. + A pass-through to ensure that feature visualizers work in Pipelines. + Subclasses may override this method to actually transform data or to + call drawing methods. The transformer may also take an optional y + argument if it is required for either transformation or drawing. + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + Feature dataset to be transformed. - This method must return a numpy array with the same shape as X. + y : array-like, shape (n_samples,) + Dependent target data associated with X, unused. + + Returns + ------- + X : array-like, shape (n_samples, n_features) + Returns the original dataset, unmodified. """ return X def fit_transform_poof(self, X, y=None, **kwargs): - """ - Fit to data, transform it, then visualize it. + """Fit, transform, then visualize data in one step. - Fits the visualizer to X and y with opetional parameters by passing in - all of kwargs, then calls poof with the same kwargs. This method must - return the result of the transform method. + A helper method similar to ``fit_transform`` that allows you to fit, + transform, and create a visualization in one simple step. Returns a + transformed dataset similar to ``fit_transform``. + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + Feature dataset for both training and transformation. + + y : array-like, shape (n_samples,) + Dependent target dataset optionally used for training. + + kwargs : dict, optional + Keyword arguments to pass to the ``poof()`` method. + + Returns + ------- + Xp : array-like, shape (m_samples, m_features) + The transformed dataset X prime. """ - Xp = self.fit_transform(X, y, **kwargs) + Xp = self.fit_transform(X, y) self.poof(**kwargs) return Xp class MultiFeatureVisualizer(FeatureVisualizer): - """ - MultiFeatureVisualiers are a subclass of FeatureVisualizer that visualize - several features at once. This class provides base functionality for - getting the names of features for use in plot annotation. + """Direct visualization of a feature set. + + MultiFeatureVisualiers visualize several features at once, usually in order + to compare the effectiveness of a subset of features to the superset. This + type of visualizer provides base functionality for identifying the names of + the features either directly from the data or from user supplied values. It + also provides other functionality for feature selection, e.g. ensuring that + a subset of features is used if specified by the user. Parameters ---------- - - ax: matplotlib Axes, default: None + ax : matplotlib.Axes, default: None The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). - features: list, default: None - a list of feature names to use - If a DataFrame is passed to fit and features is None, feature - names are selected as the columns of the DataFrame. + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). - kwargs : dict + features : list, default: None + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. + + kwargs : dict, optional Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ---------- + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. """ - def __init__(self, ax=None, features=None, **kwargs): - super(MultiFeatureVisualizer, self).__init__(ax=ax, **kwargs) + def __init__(self, ax=None, fig=None, features=None, **kwargs): + super(MultiFeatureVisualizer, self).__init__(ax=ax, fig=fig, **kwargs) # Data Parameters - self.features_ = features + self.features = features - def fit(self, X, y=None, **fit_params): + def fit(self, X, y=None): """ This method performs preliminary computations in order to set up the figure or perform other analyses. It can also call drawing methods in order to set up various non-instance related figure elements. - This method must return self. - """ + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + Feature dataset to be transformed. - # Handle the feature names if they're None. - if self.features_ is None: + y : array-like, shape (n_samples,) + Optional dependent target data associated with X. - # If X is a data frame, get the columns off it. + Returns + ------- + self : MultiFeatureVisualizer + Returns the visualizer/transformer for use in Pipelines and chaining. + """ + n_columns = X.shape[1] + + if self.features is not None: + # Use the user-specified features with some checking + # TODO: allow the user specified features to filter the dataset + if len(self.features) != n_columns: + raise YellowbrickValueError( + ( + "number of supplied feature names does not match the number " + "of columns in the training data." + ) + ) + + self.features_ = np.array(self.features) + + else: + # Attempt to determine the feature names from the input data if is_dataframe(X): self.features_ = np.array(X.columns) # Otherwise create numeric labels for each column. else: - _, ncols = X.shape - self.features_ = np.arange(0, ncols) + self.features_ = np.arange(0, n_columns) + # Ensure super is called and fit is returned + super(MultiFeatureVisualizer, self).fit(X, y) return self + ########################################################################## ## Data Visualizers ########################################################################## + class DataVisualizer(MultiFeatureVisualizer): - """ - Data Visualizers are a subclass of Feature Visualizers which plot the - instances in feature space (also called data space, hence the name of the - visualizer). Feature space is a multi-dimensional space defined by the - columns of the instance dependent vector input, X which is passed to - ``fit()`` and ``transform()``. Instances can also be labeled by the target - independent vector input, y which is only passed to ``fit()``. For that - reason most Data Visualizers perform their drawing in ``fit()``. + """Visualizations of instances in feature space. + + Data Visualizers plot instances in feature space (sometimes also referred + to as data space). Feature space is a multi-dimensional space defined by + the columns of the dataset ``X`` when passed to ``fit()`` and ``transform``. + These instances and their features are directly plotted in a representation + of the higher dimensional space. + + Instances can also be labeled by an target vectory, ``y``. The target is + visualized in data space by color. For example a discrete target for + classification problems will use categorical colors and a legend. A + continuous target for regression problems will use sequential colors with + a colormap. This class provides helper functionality related to target identification: whether or not the target is sequential or categorical, and mapping a - color sequence or color set to the targets as appropriate. It also uses - the fit method to call the drawing utilities. + color sequence or color set to the targets as appropriate. It also + determines the scope of the target, e.g. the unique classes or the range + of the dataset for use in specific visualizations. Parameters ---------- - - ax: matplotlib Axes, default: None + ax : matplotlib.Axes, default: None The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). - features: list, default: None - a list of feature names to use - If a DataFrame is passed to fit and features is None, feature - names are selected as the columns of the DataFrame. - - classes: list, default: None - a list of class names for the legend - If classes is None and a y value is passed to fit then the classes - are selected from the target vector. - - color: list or tuple, default: None - optional list or tuple of colors to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. - - colormap: string or cmap, default: None - optional string or matplotlib cmap to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + + features : list, default: None + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. + + classes : list, default: None + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. + + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. This parameter is only used in the + discrete target type case and is ignored otherwise. + + colors : list or tuple, default: None + A single color to plot all instances as or a list of colors to color each + instance according to its class in the discrete case or as an ordered + colormap in the sequential case. If not enough colors per class are + specified then the colors are treated as a cycle. + + colormap : string or cmap, default: None + The colormap used to create the individual colors. In the discrete case + it is used to compute the number of colors needed for each class and + in the continuous case it is used to create a sequential color map based + on the range of the target. + + target_type : str, default: "auto" + Specify the type of target as either "discrete" (classes) or "continuous" + (real numbers, usually for regression). If "auto", then it will + attempt to determine the type by counting the number of unique values. + + If the target is discrete, the colors are returned as a dict with classes + being the keys. If continuous the colors will be list having value of + color for each point. In either case, if no target is specified, then + color will be specified as the first color in the color cycle. kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. - Notes - ----- - These parameters can be influenced later on in the visualization - process, but can and should be set as early as possible. + Attributes + ---------- + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. + + classes_ : ndarray, shape (n_classes,) + The class labels that define the discrete values in the target. Only + available if the target type is discrete. This is guaranteed to be + strings even if the classes are a different type. + + range_ : (min y, max y) + A tuple that describes the minimum and maximum values in the target. + Only available if the target type is continuous. """ - def __init__(self, ax=None, features=None, classes=None, color=None, - colormap=None, **kwargs): - """ - Initialize the data visualization with many of the options required - in order to make most visualizations work. - """ - super(DataVisualizer, self).__init__(ax=ax, features=features, **kwargs) + def __init__( + self, + ax=None, + fig=None, + features=None, + classes=None, + colors=None, + colormap=None, + target_type="auto", + **kwargs + ): + super(DataVisualizer, self).__init__( + ax=ax, fig=fig, features=features, **kwargs + ) + + # Validate raises YellowbrickValueError if invalid + TargetType.validate(target_type) # Data Parameters - self.classes_ = classes + self.classes = classes + self.target_type = target_type # Visual Parameters - self.color = color + self.colors = colors self.colormap = colormap - def fit(self, X, y=None, **kwargs): + # Internal attributes + self._colors = None + self._target_color_type = None + self._label_encoder = None + + def fit(self, X, y=None): """ - The fit method is the primary drawing input for the - visualization since it has both the X and y data required for the - viz and the transform method does not. + Fits the visualizer to the training data set by determining the + target type, colors, classes, and range of the data to ensure that + the visualizer can accurately portray the instances in data space. Parameters ---------- @@ -205,23 +353,146 @@ def fit(self, X, y=None, **kwargs): y : ndarray or Series of length n An array or series of target or class values - kwargs : dict - Pass generic arguments to the drawing method - Returns ------- - self : instance + self : DataVisualizer Returns the instance of the transformer/visualizer """ - super(DataVisualizer, self).fit(X, y, **kwargs) + # Compute the features from the data + super(DataVisualizer, self).fit(X, y) + + # Determine the target color type + self._determine_target_color_type(y) + + # Handle the single target color type + if self._target_color_type == TargetType.SINGLE: + # use the user supplied color or the first color in the color cycle + self._colors = self.colors or "C0" + + # Compute classes and colors if target type is discrete + elif self._target_color_type == TargetType.DISCRETE: + # Unique labels are used both for validation and color mapping + labels = np.unique(y) + + # Handle user supplied classes + if self.classes is not None: + self.classes_ = np.asarray([str(c) for c in self.classes]) + + # Validate user supplied class labels + if len(self.classes_) != len(labels): + raise YellowbrickValueError( + ( + "number of specified classes does not match " + "number of unique values in target" + ) + ) + + # Get the string labels from the unique values in y + else: + self.classes_ = np.asarray([str(c) for c in labels]) + + # Create a map of class labels to colors + color_values = resolve_colors( + n_colors=len(self.classes_), colormap=self.colormap, colors=self.colors + ) + self._colors = dict(zip(self.classes_, color_values)) + self._label_encoder = dict(zip(labels, self.classes_)) + + # Compute target range if colors are continuous + elif self._target_color_type == TargetType.CONTINUOUS: + y = np.asarray(y) + self.range_ = (y.min(), y.max()) + if self.colormap is None: + self.colormap = palettes.DEFAULT_SEQUENCE + # TODO: allow for Yellowbrick palettes here as well + self._colors = mpl.cm.get_cmap(self.colormap) + + # If this exception is raised a developer error has occurred because + # unknown types should have errored when the type was determined. + else: + raise YellowbrickValueError( + "unknown target color type '{}'".format(self._target_color_type) + ) + + # NOTE: cannot call draw in fit to support data transformers + return self - # Store the classes for the legend if they're None. - if self.classes_ is None: - # TODO: Is this the most efficient method? - self.classes_ = [str(label) for label in np.unique(y)] + def _determine_target_color_type(self, y): + """ + Determines the target color type from the vector y as follows: - # Draw the instances - self.draw(X, y, **kwargs) + - if y is None: only a single color is used + - if target is auto: determine if y is continuous or discrete + - otherwise specify supplied target type - # Fit always returns self. - return self + This property will be used to compute the colors for each point. + """ + if y is None: + self._target_color_type = TargetType.SINGLE + elif self.target_type == TargetType.AUTO: + self._target_color_type = target_color_type(y) + else: + self._target_color_type = TargetType(self.target_type) + + # Ensures that target is either SINGLE, DISCRETE or CONTINUOUS before continuing + if ( + self._target_color_type == TargetType.AUTO + or self._target_color_type == TargetType.UNKNOWN + ): + raise YellowbrickValueError( + ( + "could not determine target color type " "from target='{}' to '{}'" + ).format(self.target_type, self._target_color_type) + ) + + def get_target_color_type(self): + """ + Returns the computed target color type if fitted or specified by the user. + """ + if self._target_color_type is None: + raise NotFitted("unknown target color type on unfitted visualizer") + return self._target_color_type + + def get_colors(self, y): + """ + Returns the color for the specified value(s) of y based on the learned + colors property for any specified target type. + + Parameters + ---------- + y : array-like + The values of y to get the associated colors for. + + Returns + ------- + colors : list + Returns a list of colors for each value in y. + """ + if self._colors is None: + raise NotFitted("cannot determine colors on unfitted visualizer") + + if self._target_color_type == TargetType.SINGLE: + return [self._colors] * len(y) + + if self._target_color_type == TargetType.DISCRETE: + try: + # Use the label encoder to get the class name (or use the value + # if the label is not mapped in the encoder) then use the class + # name to get the color from the color map. + return [self._colors[self._label_encoder.get(yi, yi)] for yi in y] + except KeyError: + unknown = set(y) - set(self._label_encoder.keys()) + unknown = ", ".join(["'{}'".format(uk) for uk in unknown]) + raise YellowbrickKeyError( + "could not determine color for classes {}".format(unknown) + ) + + if self._target_color_type == TargetType.CONTINUOUS: + # Normalize values into target range and compute colors from colormap + norm = Normalize(*self.range_) + return self._colors(norm(y)) + + # This is a developer error, we should never get here! + raise YellowbrickValueError( + "unknown target color type '{}'".format(self._target_color_type) + ) diff --git a/yellowbrick/features/decomposition.py b/yellowbrick/features/decomposition.py index c679f1d5f..c8cbb4448 100644 --- a/yellowbrick/features/decomposition.py +++ b/yellowbrick/features/decomposition.py @@ -1,9 +1,19 @@ +# yellowbrick.features.decomposition +# +# Author: George Richardson +# Created: Fri Mar 2 16:16:00 2018 +0000 +# +# Copyright (C) 2016 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: decomposition.py [0ed6e8a] g.raymond.richardson@gmail.com $ + ########################################################################## ## Imports ########################################################################## -from .base import FeatureVisualizer from yellowbrick.style import palettes +from yellowbrick.features.base import FeatureVisualizer from sklearn.pipeline import Pipeline from sklearn.decomposition import PCA @@ -13,11 +23,18 @@ ## Quick Methods ########################################################################## -def explained_variance_visualizer(X, y=None, ax=None, scale=True, - center=True, colormap=palettes.DEFAULT_SEQUENCE, - **kwargs): - """Produce a plot of the explained variance produced by a dimensionality - reduction algorithm using n=1 to n=n_components dimensions. This is a single + +def explained_variance_visualizer( + X, + y=None, + ax=None, + scale=True, + center=True, + colormap=palettes.DEFAULT_SEQUENCE, + **kwargs +): + """Produce a plot of the explained variance produced by a dimensionality + reduction algorithm using n=1 to n=n_components dimensions. This is a single plot to help identify the best trade off between number of dimensions and amount of information retained within the data. @@ -28,7 +45,7 @@ def explained_variance_visualizer(X, y=None, ax=None, scale=True, y : ndarray or Series of length n An array or Series of target or class values - + ax : matplotlib Axes, default: None The aces to plot the figure on @@ -43,38 +60,58 @@ def explained_variance_visualizer(X, y=None, ax=None, scale=True, kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. - + + Returns + ------- + viz : ExplainedVariance + Returns the fitted, finalized visualizer + Examples -------- >>> from sklearn import datasets >>> bc = datasets.load_breast_cancer() >>> X = bc = bc.data >>> explained_variance_visualizer(X, scale=True, center=True, colormap='RdBu_r') - + """ - # Instantiate the visualizer - visualizer = ExplainedVariance(X=X) + # Instantiate the visualizer + visualizer = ExplainedVariance(X=X) - # Fit and transform the visualizer (calls draw) - visualizer.fit(X, y, **kwargs) - visualizer.transform(X) + # Fit and transform the visualizer (calls draw) + visualizer.fit(X, y, **kwargs) + visualizer.transform(X) + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.poof() + # Return the visualizer object + return visualizer ########################################################################## ## Explained Variance Feature Visualizer ########################################################################## + class ExplainedVariance(FeatureVisualizer): """ Parameters ---------- - - + ax : matplotlib Axes, default: None + The aces to plot the figure on + + scale : bool, default: True + Boolean that indicates if the values of X should be scaled. + + colormap : string or cmap, default: None + optional string or matplotlib cmap to colorize lines + Use either color to colorize the lines on a per class basis or + colormap to color them on a continuous scale. + + kwargs : dict + Keyword arguments that are passed to the base class and may influence + the visualization as defined in other Visualizers. + Examples -------- @@ -84,13 +121,17 @@ class ExplainedVariance(FeatureVisualizer): >>> visualizer.transform(X) >>> visualizer.poof() - Notes - ----- - """ - def __init__(self, n_components=None, ax=None, scale=True, center=True, - colormap=palettes.DEFAULT_SEQUENCE, **kwargs): + def __init__( + self, + ax=None, + scale=True, + center=True, + n_components=None, + colormap=palettes.DEFAULT_SEQUENCE, + **kwargs + ): super(ExplainedVariance, self).__init__(ax=ax, **kwargs) @@ -98,9 +139,12 @@ def __init__(self, n_components=None, ax=None, scale=True, center=True, self.n_components = n_components self.center = center self.scale = scale - self.pipeline = Pipeline([('scale', StandardScaler(with_mean=self.center, - with_std=self.scale)), - ('pca', PCA(n_components=self.n_components))]) + self.pipeline = Pipeline( + [ + ("scale", StandardScaler(with_mean=self.center, with_std=self.scale)), + ("pca", PCA(n_components=self.n_components)), + ] + ) self.pca_features = None @property @@ -123,9 +167,8 @@ def draw(self): def finalize(self, **kwargs): # Set the title - self.set_title('Explained Variance Plot') + self.set_title("Explained Variance Plot") # Set the axes labels - self.ax.set_ylabel('Explained Variance') - self.ax.set_xlabel('Number of Components') - + self.ax.set_ylabel("Explained Variance") + self.ax.set_xlabel("Number of Components") diff --git a/yellowbrick/features/jointplot.py b/yellowbrick/features/jointplot.py index 4403745c2..ab4f4732c 100644 --- a/yellowbrick/features/jointplot.py +++ b/yellowbrick/features/jointplot.py @@ -1,11 +1,10 @@ - # yellowbrick.features.jointplot -# Implementations of joint plots for univariate and bivariate analysis. +# Implementation of joint plots for univariate and bivariate analysis. # # Author: Prema Damodaran Roman # Created: Mon Apr 10 21:00:54 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers. # For license information, see LICENSE.txt # # ID: jointplot.py [7f47800] pdamodaran@users.noreply.github.com $ @@ -14,306 +13,439 @@ ## Imports ########################################################################## -import warnings import numpy as np -import matplotlib as mpl import matplotlib.pyplot as plt -from yellowbrick.features.base import FeatureVisualizer -from yellowbrick.exceptions import YellowbrickValueError -from yellowbrick.bestfit import draw_best_fit -from yellowbrick.utils import is_dataframe +try: + # Only available in Matplotlib >= 2.0.2 + from mpl_toolkits.axes_grid1 import make_axes_locatable +except ImportError: + make_axes_locatable = None + +from .base import FeatureVisualizer + +# from ..bestfit import draw_best_fit # TODO: return in #728 +from ..utils.types import is_dataframe +from ..exceptions import YellowbrickValueError +from scipy.stats import pearsonr, spearmanr, kendalltau + + +# Default Colors +# TODO: should we reuse these colors? +FACECOLOR = "#FAFAFA" +HISTCOLOR = "#6897bb" + + +# Objects for export +__all__ = ["JointPlot", "JointPlotVisualizer", "joint_plot"] + ########################################################################## ## Joint Plot Visualizer ########################################################################## -class JointPlotVisualizer(FeatureVisualizer): +class JointPlot(FeatureVisualizer): """ - JointPlotVisualizer allows for a simultaneous visualization of the relationship - between two variables and the distrbution of each individual variable. The - relationship is plotted along the joint axis and univariate distributions - are plotted on top of the x axis and to the right of the y axis. + Joint plots are useful for machine learning on multi-dimensional data, allowing for + the visualization of complex interactions between different data dimensions, their + varying distributions, and even their relationships to the target variable for + prediction. + + The Yellowbrick ``JointPlot`` can be used both for pairwise feature analysis and + feature-to-target plots. For pairwise feature analysis, the ``columns`` argument can + be used to specify the index of the two desired columns in ``X``. If ``y`` is also + specified, the plot can be colored with a heatmap or by class. For feature-to-target + plots, the user can provide either ``X`` and ``y`` as 1D vectors, or a ``columns`` + argument with an index to a single feature in ``X`` to be plotted against ``y``. + + Histograms can be included by setting the ``hist`` argument to ``True`` for a + frequency distribution, or to ``"density"`` for a probability density function. Note + that histograms requires matplotlib 2.0.2 or greater. Parameters ---------- - ax: matplotlib Axes, default: None - This is inherited from FeatureVisualizer but is defined within - JointPlotVisualizer since there are three axes objects. - - feature: string, default: None - The name of the X variable - If a DataFrame is passed to fit and feature is None, feature - is selected as the column of the DataFrame. There must be only - one column in the DataFrame. - - target: string, default: None - The name of the Y variable - If target is None and a y value is passed to fit then the target - is selected from the target vector. - - joint_plot: one of {'scatter', 'hex'}, default: 'scatter' - The type of plot to render in the joint axis - Currently, the choices are scatter and hex. - Use scatter for small datasets and hex for large datasets - - joint_args: dict, default: None - Keyword arguments used for customizing the joint plot: - - ============= ================================================================== - Property Description - ------------- ------------------------------------------------------------------ - alpha transparency - facecolor background color of the joint axis - aspect aspect ratio - fit used if scatter is selected for joint_plot to draw a - best fit line - values can be True or False. - Uses ``Yellowbrick.bestfit`` - estimator used if scatter is selected for joint_plot to determine - the type of best fit line to use. Refer to - Yellowbrick.bestfit for types of estimators that can be used. - x_bins used if hex is selected to set the number of bins for the x value - y_bins used if hex is selected to set the number of bins for the y value - cmap string or matplotlib cmap to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. - ============= ================================================================== - - xy_plot: one of {'hist'}, default: 'hist' - The type of plot to render along the x and y axes - Currently, the choice is hist - - xy_args: dict, default: None - Keyword arguments used for customizing the x and y plots: - - ============== ===================================================== - Property Description - -------------- ----------------------------------------------------- - alpha transparency - facecolor_x background color of the x axis - facecolor_y background color of the y axis - bins used to set up the number of bins for the hist plot - histcolor_x used to set the color for the histogram on the x axis - histcolor_y used to set the color for the histogram on the y axis - ============== ===================================================== - - size: float, default: 600 - Size of each side of the figure in pixels - - ratio: float, default: 5 - Ratio of joint axis size to the x and y axes height - - space: float, default: 0.2 - Space between the joint axis and the x and y axes + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes will be + used (or generated if required). This is considered the base axes where the + the primary joint plot is drawn. It will be shifted and two additional axes + added above (xhax) and to the right (yhax) if hist=True. + + columns : int, str, [int, int], [str, str], default: None + Determines what data is plotted in the joint plot and acts as a selection index + into the data passed to ``fit(X, y)``. This data therefore must be indexable by + the column type (e.g. an int for a numpy array or a string for a DataFrame). + + If None is specified then either both X and y must be 1D vectors and they will + be plotted against each other or X must be a 2D array with only 2 columns. If a + single index is specified then the data is indexed as ``X[columns]`` and plotted + jointly with the target variable, y. If two indices are specified then they are + both selected from X, additionally in this case, if y is specified, then it is + used to plot the color of points. + + Note that these names are also used as the x and y axes labels if they aren't + specified in the joint_kws argument. + + correlation : str, default: 'pearson' + The algorithm used to compute the relationship between the variables in the + joint plot, one of: 'pearson', 'covariance', 'spearman', 'kendalltau'. + + kind : str in {'scatter', 'hex'}, default: 'scatter' + The type of plot to render in the joint axes. Note that when kind='hex' the + target cannot be plotted by color. + + hist : {True, False, None, 'density', 'frequency'}, default: True + Draw histograms showing the distribution of the variables plotted jointly. + If set to 'density', the probability density function will be plotted. + If set to True or 'frequency' then the frequency will be plotted. + Requires Matplotlib >= 2.0.2. + + alpha : float, default: 0.65 + Specify a transparency where 1 is completely opaque and 0 is completely + transparent. This property makes densely clustered points more visible. + + {joint, hist}_kws : dict, default: None + Additional keyword arguments for the plot components. kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ---------- + corr_ : float + The correlation or relationship of the data in the joint plot, specified by the + correlation algorithm. + Examples -------- - >>> visualizer = JointPlotVisualizer() - >>> visualizer.fit(X,y) - >>> visualizer.poof() - - Notes - ----- - These parameters can be influenced later on in the visualization - process, but can and should be set as early as possible. + >>> viz = JointPlot(columns=["temp", "humidity"]) + >>> viz.fit(X, y) + >>> viz.poof() """ - def __init__(self, ax=None, feature=None, target=None, - joint_plot='scatter', joint_args=None, - xy_plot='hist', xy_args=None, - size=600, ratio=5, space=.2, **kwargs): - - # Check matplotlib version - needs to be version 2.0.0 or greater. - mpl_vers_maj = int(mpl.__version__.split(".")[0]) - if mpl_vers_maj < 2: - warnings.warn(( - "{} requires matplotlib major version 2 or greater. " - "Please upgrade." - ).format(self.__class__.__name__)) - - super(JointPlotVisualizer, self).__init__(ax, **kwargs) - - self.feature = feature - self.target = target - self.joint_plot = joint_plot - self.joint_args = joint_args - self.xy_plot = xy_plot - self.xy_args = xy_args - self.size = (size, size) - self.ratio = ratio - self.space = space - - def fit(self, X, y, **kwargs): - """ - Sets up the X and y variables for the jointplot - and checks to ensure that X and y are of the - correct data type + # TODO: should we couple more closely with Rank2D? + correlation_methods = { + "pearson": lambda x, y: pearsonr(x, y)[0], + "spearman": lambda x, y: spearmanr(x, y)[0], + "covariance": lambda x, y: np.cov(x, y)[0, 1], + "kendalltau": lambda x, y: kendalltau(x, y)[0], + } + + def __init__( + self, + ax=None, + columns=None, + correlation="pearson", + kind="scatter", + hist=True, + alpha=0.65, + joint_kws=None, + hist_kws=None, + **kwargs + ): + # Initialize the visualizer + super(JointPlot, self).__init__(ax=ax, **kwargs) + self._xhax, self._yhax = None, None + + # Set and validate the columns + self.columns = columns + if self.columns is not None and not isinstance(self.columns, (int, str)): + self.columns = tuple(self.columns) + if len(self.columns) > 2: + raise YellowbrickValueError( + ( + "'{}' contains too many indices or is invalid for joint plot - " + "specify either a single int or str index or two columns as a list" + ).format(columns) + ) + + # Seet and validate the correlation + self.correlation = correlation + if self.correlation not in self.correlation_methods: + raise YellowbrickValueError( + "'{}' is an invalid correlation method, use one of {}".format( + self.correlation, ", ".join(self.correlation_methods.keys()) + ) + ) - Fit calls draw + # Set and validate the kind of plot + self.kind = kind + if self.kind not in {"scatter", "hex", "hexbin"}: + raise YellowbrickValueError( + ("'{}' is invalid joint plot kind, use 'scatter' or 'hex'").format( + self.kind + ) + ) - Parameters - ---------- + # Set and validate the histogram if specified + self.hist = hist + if self.hist not in {True, "density", "frequency", None, False}: + raise YellowbrickValueError( + ( + "'{}' is an invalid argument for hist, use None, True, " + "False, 'density', or 'frequency'" + ).format(hist) + ) - X : ndarray or DataFrame of shape n x 1 - A matrix of n instances with 1 feature + # If hist is True, test the version availability + if self.hist in {True, "density", "frequency"}: + self._layout() - y : ndarray or Series of length n - An array or series of the target value + # Set the additional visual parameters + self.alpha = alpha + self.joint_kws = joint_kws + self.hist_kws = hist_kws - kwargs: dict - keyword arguments passed to Scikit-Learn API. + @property + def xhax(self): """ + The axes of the histogram for the top of the JointPlot (X-axis) + """ + if self._xhax is None: + raise AttributeError( + "this visualizer does not have a histogram for the X axis" + ) + return self._xhax - #throw an error if X has more than 1 column - if is_dataframe(X): - nrows, ncols = X.shape - - if ncols > 1: - raise YellowbrickValueError(( - "X needs to be an ndarray or DataFrame with one feature, " - "please select one feature from the DataFrame" - )) + @property + def yhax(self): + """ + The axes of the histogram for the right of the JointPlot (Y-axis) + """ + if self._yhax is None: + raise AttributeError( + "this visualizer does not have a histogram for the Y axis" + ) + return self._yhax - #throw an error is y is None - if y is None: - raise YellowbrickValueError(( - "Joint plots are useful for classification and regression " - "problems, which require a target variable" - )) + def _layout(self): + """ + Creates the grid layout for the joint plot, adding new axes for the histograms + if necessary and modifying the aspect ratio. Does not modify the axes or the + layout if self.hist is False or None. + """ + # Ensure the axes are created if not hist, then return. + if not self.hist: + self.ax + return + + # Ensure matplotlib version compatibility + if make_axes_locatable is None: + raise YellowbrickValueError( + ( + "joint plot histograms requires matplotlib 2.0.2 or greater " + "please upgrade matplotlib or set hist=False on the visualizer" + ) + ) + # Create the new axes for the histograms + divider = make_axes_locatable(self.ax) + self._xhax = divider.append_axes("top", size=1, pad=0.1, sharex=self.ax) + self._yhax = divider.append_axes("right", size=1, pad=0.1, sharey=self.ax) - # Handle the feature name if it is None. - if self.feature is None: + # Modify the display of the axes + self._xhax.xaxis.tick_top() + self._yhax.yaxis.tick_right() + self._xhax.grid(False, axis="y") + self._yhax.grid(False, axis="x") - # If X is a data frame, get the columns off it. - if is_dataframe(X): - self.feature = X.columns + def fit(self, X, y=None): + """ + Fits the JointPlot, creating a correlative visualization between the columns + specified during initialization and the data and target passed into fit: - else: - self.feature = ['x'] + - If self.columns is None then X and y must both be specified as 1D arrays + or X must be a 2D array with only 2 columns. + - If self.columns is a single int or str, that column is selected to be + visualized against the target y. + - If self.columns is two ints or strs, those columns are visualized against + each other. If y is specified then it is used to color the points. - # Handle the target name if it is None. - if self.target is None: - self.target = ['y'] + This is the main entry point into the joint plot visualization. - self.draw(X, y, **kwargs) - return self + Parameters + ---------- + X : array-like + An array-like object of either 1 or 2 dimensions depending on self.columns. + Usually this is a 2D table with shape (n, m) - def draw(self, X, y, **kwargs): + y : array-like, default: None + An vector or 1D array that has the same length as X. May be used to either + directly plot data or to color data points. """ - Sets up the layout for the joint plot draw calls ``draw_joint`` and - ``draw_xy`` to render the visualizations. - """ - fig = plt.gcf() - gs = plt.GridSpec(self.ratio + 1, self.ratio + 1) + # Convert python objects to numpy arrays + if isinstance(X, (list, tuple)): + X = np.array(X) + + if y is not None and isinstance(y, (list, tuple)): + y = np.array(y) + + # Case where no columns are specified + if self.columns is None: + if (y is None and (X.ndim != 2 or X.shape[1] != 2)) or ( + y is not None and (X.ndim != 1 or y.ndim != 1) + ): + raise YellowbrickValueError( + ( + "when self.columns is None specify either X and y as 1D arrays " + "or X as a matrix with 2 columns" + ) + ) + + if y is None: + # Draw the first column as x and the second column as y + self.draw(X[:, 0], X[:, 1], xlabel="0", ylabel="1") + return self + + # Draw x against y + self.draw(X, y, xlabel="x", ylabel="y") + return self + + # Case where a single string or int index is specified + if isinstance(self.columns, (int, str)): + if y is None: + raise YellowbrickValueError( + "when self.columns is a single index, y must be specified" + ) + + # fetch the index from X -- raising index error if not possible + x = self._index_into(self.columns, X) + self.draw(x, y, xlabel=str(self.columns), ylabel="target") + return self + + # Case where there is a double index for both columns + columns = tuple(self.columns) + if len(columns) != 2: + raise YellowbrickValueError( + ("'{}' contains too many indices or is invalid for joint plot").format( + columns + ) + ) - #Set up the 3 axes objects - joint_ax = fig.add_subplot(gs[1:, :-1]) - x_ax = fig.add_subplot(gs[0, :-1], sharex=joint_ax) - y_ax = fig.add_subplot(gs[1:, -1], sharey=joint_ax) + # TODO: color the points based on the target if it is given + x = self._index_into(columns[0], X) + y = self._index_into(columns[1], X) + self.draw(x, y, xlabel=str(columns[0]), ylabel=str(columns[1])) + return self - fig.tight_layout() - fig.subplots_adjust(hspace=self.space, wspace=self.space) + def draw(self, x, y, xlabel=None, ylabel=None): + """ + Draw the joint plot for the data in x and y. - self.fig = fig - self.joint_ax = joint_ax - self.x_ax = x_ax - self.y_ax = y_ax + Parameters + ---------- + x, y : 1D array-like + The data to plot for the x axis and the y axis - self.draw_joint(X, y, **kwargs) - self.draw_xy(X, y, **kwargs) + xlabel, ylabel : str + The labels for the x and y axes. + """ + # This is a little weird to be here, but it is the best place to perform + # this computation given how fit calls draw and returns. + self.corr_ = self.correlation_methods[self.correlation](x, y) + + # First draw the joint plot + joint_kws = self.joint_kws or {} + joint_kws.setdefault("alpha", self.alpha) + joint_kws.setdefault("label", "{}={:0.3f}".format(self.correlation, self.corr_)) + + # Draw scatter joint plot + if self.kind == "scatter": + self.ax.scatter(x, y, **joint_kws) + + # TODO: Draw best fit line (or should this be kind='reg'?) + + # Draw hexbin joint plot + elif self.kind in ("hex", "hexbin"): + joint_kws.setdefault("mincnt", 1) + joint_kws.setdefault("gridsize", 50) + joint_kws.setdefault("cmap", "Blues") + self.ax.hexbin(x, y, **joint_kws) + + # Something bad happened + else: + raise ValueError("unknown joint plot kind '{}'".format(self.kind)) + + # Set the X and Y axis labels on the plot + self.ax.set_xlabel(xlabel) + self.ax.set_ylabel(ylabel) + + # If we're not going to draw histograms, stop here + if not self.hist: + # Ensure the current axes is always the main joint plot axes + plt.sca(self.ax) + return self.ax + + # Draw the histograms + hist_kws = self.hist_kws or {} + hist_kws.setdefault("bins", 50) + if self.hist == "density": + hist_kws.setdefault("density", True) + + self.xhax.hist(x, **hist_kws) + self.yhax.hist(y, orientation="horizontal", **hist_kws) + + # Ensure the current axes is always the main joint plot axes + plt.sca(self.ax) + return self.ax - def draw_joint(self, X, y, **kwargs): + def finalize(self, **kwargs): """ - Draws the visualization for the joint axis. + Finalize executes any remaining image modifications making it ready to show. """ - - if self.joint_args is None: - self.joint_args = {} - - self.joint_args.setdefault("alpha", 0.4) - facecolor = self.joint_args.pop("facecolor", "#dddddd") - self.joint_ax.set_facecolor(facecolor) - - if self.joint_plot == "scatter": - aspect = self.joint_args.pop("aspect", "auto") - self.joint_ax.set_aspect(aspect) - self.joint_ax.scatter(X, y, **self.joint_args) - - fit = self.joint_args.pop("fit", True) - if fit: - estimator = self.joint_args.pop("estimator", "linear") - draw_best_fit(X, y, self.joint_ax, estimator) - - elif self.joint_plot == "hex": - x_bins = self.joint_args.pop("x_bins", 50) - y_bins = self.joint_args.pop("y_bins", 50) - colormap = self.joint_args.pop("cmap", 'Blues') - gridsize = int(np.mean([x_bins, y_bins])) - - xmin = X.min() - xmax = X.max() - ymin = y.min() - ymax = y.max() - - self.joint_ax.hexbin(X, y, - gridsize=gridsize, cmap=colormap, mincnt=1, **self.joint_args - ) - self.joint_ax.axis([xmin, xmax, ymin, ymax]) - - def draw_xy(self, X, y, **kwargs): + # Set the aspect ratio to make the visualization square + # TODO: still unable to make plot square using make_axes_locatable + # x0,x1 = self.ax.get_xlim() + # y0,y1 = self.ax.get_ylim() + # self.ax.set_aspect(abs(x1-x0)/abs(y1-y0)) + + # Add the title to the plot if the user has set one. + self.set_title("") + + # TODO: use manual legend so legend works with both scatter and hexbin + # Set the legend with full opacity patches using manual legend. + # Or Add the colorbar if this is a continuous plot. + if self.kind == "scatter": + self.ax.legend(loc="best", frameon=True) + + # Finalize the histograms + if self.hist: + plt.setp(self.xhax.get_xticklabels(), visible=False) + plt.setp(self.yhax.get_yticklabels(), visible=False) + plt.sca(self.ax) + + # Call tight layout to maximize readability + plt.tight_layout() + + def _index_into(self, idx, data): """ - Draws the visualization for the x and y axes + Attempts to get the column from the data using the specified index, raises an + exception if this is not possible from this point in the stack. """ + try: + if is_dataframe(data): + # Assume column indexing + return data[idx] + # Otherwise assume numpy array-like indexing + return data[:, idx] + except Exception as e: + raise IndexError( + "could not index column '{}' into type {}: {}".format( + self.columns, data.__class__.__name__, e + ) + ) - if self.xy_args is None: - self.xy_args = {} - facecolor_x = self.xy_args.pop("facecolor_x", "#dddddd") - self.x_ax.set_facecolor(facecolor_x) - facecolor_y = self.xy_args.pop("facecolor_y", "#dddddd") - self.y_ax.set_facecolor(facecolor_y) +# Alias for JointPlot +JointPlotVisualizer = JointPlot - if self.xy_plot == "hist": - hist_bins = self.xy_args.pop("bins", 50) - self.xy_args.setdefault("alpha", 0.4) - histcolor_x = self.xy_args.pop("histcolor_x", "#6897bb") - self.x_ax.set_facecolor(facecolor_x) - histcolor_y = self.xy_args.pop("histcolor_y", "#6897bb") - self.y_ax.set_facecolor(facecolor_y) - self.x_ax.hist(X, bins=hist_bins, color=histcolor_x, **self.xy_args) - self.y_ax.hist(y, bins=hist_bins, color=histcolor_y, - orientation='horizontal', **self.xy_args) +########################################################################## +## Quick Method for JointPlot visualizations +########################################################################## - def finalize(self, **kwargs): - """ - Finalize executes any subclass-specific axes finalization steps. - The user calls poof and poof calls finalize. - Parameters - ---------- - kwargs: generic keyword arguments. - """ - self.joint_ax.set_xlabel(self.feature) - self.joint_ax.set_ylabel(self.target) - - plt.setp(self.x_ax.get_xticklabels(), visible=False) - plt.setp(self.y_ax.get_yticklabels(), visible=False) - - plt.setp(self.x_ax.yaxis.get_majorticklines(), visible=False) - plt.setp(self.x_ax.yaxis.get_minorticklines(), visible=False) - plt.setp(self.y_ax.xaxis.get_majorticklines(), visible=False) - plt.setp(self.y_ax.xaxis.get_minorticklines(), visible=False) - plt.setp(self.x_ax.get_yticklabels(), visible=False) - plt.setp(self.y_ax.get_xticklabels(), visible=False) - self.x_ax.yaxis.grid(False) - self.y_ax.xaxis.grid(False) - self.fig.suptitle("Joint Plot of {} vs {}" - .format(self.feature, self.target), y=1.05) +def joint_plot(): + raise NotImplementedError("quick method still needs to be implemented") diff --git a/yellowbrick/features/manifold.py b/yellowbrick/features/manifold.py index 8a6577040..80aadcab1 100644 --- a/yellowbrick/features/manifold.py +++ b/yellowbrick/features/manifold.py @@ -1,10 +1,13 @@ # yellowbrick.features.manifold # Use manifold algorithms for high dimensional visualization. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Sat May 12 11:25:24 2018 -0400 # -# ID: manifold.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: manifold.py [02f8c27] benjamin@bengfort.com $ """ Use manifold algorithms for high dimensional visualization. @@ -14,19 +17,16 @@ ## Imports ########################################################################## -import numpy as np -import matplotlib.pyplot as plt - -from six import string_types +import warnings from yellowbrick.utils.timer import Timer -from yellowbrick.draw import manual_legend from yellowbrick.utils.types import is_estimator -from yellowbrick.style import palettes, resolve_colors -from yellowbrick.features.base import FeatureVisualizer -from yellowbrick.exceptions import YellowbrickValueError, NotFitted +from yellowbrick.exceptions import ModelError, NotFitted +from yellowbrick.features.projection import ProjectionVisualizer +from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning from sklearn.base import clone +from sklearn.exceptions import NotFittedError from sklearn.manifold import LocallyLinearEmbedding from sklearn.manifold import Isomap, MDS, TSNE, SpectralEmbedding @@ -36,14 +36,14 @@ ########################################################################## MANIFOLD_ALGORITHMS = { - "lle": LocallyLinearEmbedding(method="standard", eigen_solver='auto'), - "ltsa":LocallyLinearEmbedding(method="ltsa", eigen_solver='auto'), - "hessian": LocallyLinearEmbedding(method="hessian", eigen_solver='auto'), - "modified": LocallyLinearEmbedding(method="modified", eigen_solver='auto'), + "lle": LocallyLinearEmbedding(method="standard", eigen_solver="auto"), + "ltsa": LocallyLinearEmbedding(method="ltsa", eigen_solver="auto"), + "hessian": LocallyLinearEmbedding(method="hessian", eigen_solver="auto"), + "modified": LocallyLinearEmbedding(method="modified", eigen_solver="auto"), "isomap": Isomap(), "mds": MDS(), "spectral": SpectralEmbedding(), - "tsne": TSNE(init='pca'), + "tsne": TSNE(init="pca"), } MANIFOLD_NAMES = { @@ -57,18 +57,13 @@ "tsne": "t-SNE", } -# Target type constants -AUTO = "auto" -SINGLE = "single" -DISCRETE = "discrete" -CONTINUOUS = "continuous" - ########################################################################## ## Manifold Embeddings ########################################################################## -class Manifold(FeatureVisualizer): + +class Manifold(ProjectionVisualizer): """ The Manifold visualizer provides high dimensional visualization for feature analysis by embedding data into 2 dimensions using the sklearn.manifold @@ -109,42 +104,72 @@ class Manifold(FeatureVisualizer): The axes to plot the figure on. If None, the current axes will be used or generated if required. - manifold : str or Transformer, default: "lle" + manifold : str or Transformer, default: "mds" Specify the manifold algorithm to perform the embedding. Either one of the strings listed in the table above, or an actual scikit-learn transformer. The constructed manifold is accessible with the manifold property, so as to modify hyperparameters before fit. - n_neighbors : int, default: 10 + n_neighbors : int, default: None Many manifold algorithms are nearest neighbors based, for those that are, this parameter specfies the number of neighbors to use in the - embedding. If the manifold algorithm doesn't use nearest neighbors, - then this parameter is ignored. - - colors : str or list of colors, default: None - Specify the colors used, though note that the specification depends - very much on whether the target is continuous or discrete. If - continuous, colors must be the name of a colormap. If discrete, then - colors can be the name of a palette or a list of colors to use for each - class in the target. - - target : str, default: "auto" + embedding. If n_neighbors is not specified for those embeddings, it is + set to 5 and a warning is issued. If the manifold algorithm doesn't use + nearest neighbors, then this parameter is ignored. + + features : list, default: None + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. + + classes : list, default: None + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. + + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. This parameter is only used in the + discrete target type case and is ignored otherwise. + + colors : list or tuple, default: None + A single color to plot all instances as or a list of colors to color each + instance according to its class in the discrete case or as an ordered + colormap in the sequential case. If not enough colors per class are + specified then the colors are treated as a cycle. + + colormap : string or cmap, default: None + The colormap used to create the individual colors. In the discrete case + it is used to compute the number of colors needed for each class and + in the continuous case it is used to create a sequential color map based + on the range of the target. + + target_type : str, default: "auto" Specify the type of target as either "discrete" (classes) or "continuous" - (real numbers, usually for regression). If "auto", the Manifold will + (real numbers, usually for regression). If "auto", then it will attempt to determine the type by counting the number of unique values. - If the target is discrete, points will be colored by the target class - and a legend will be displayed. If continuous, points will be displayed - with a colormap and a color bar will be displayed. In either case, if - no target is specified, only a single color will be drawn. + If the target is discrete, the colors are returned as a dict with classes + being the keys. If continuous the colors will be list having value of + color for each point. In either case, if no target is specified, then + color will be specified as the first color in the color cycle. + + projection : int or string, default: 2 + The number of axes to project into, either 2d or 3d. To plot 3d plots + with matplotlib, please ensure a 3d axes is passed to the visualizer, + otherwise one will be created using the current figure. - alpha : float, default: 0.7 + alpha : float, default: 0.75 Specify a transparency where 1 is completely opaque and 0 is completely transparent. This property makes densely clustered points more visible. random_state : int or RandomState, default: None Fixes the random state for stochastic manifold algorithms. + colorbar : bool, default: True + If the target_type is "continous" draw a colorbar to the right of the + scatter plot. The colobar axes is accessible using the cax property. + kwargs : dict Keyword arguments passed to the base class and may influence the feature visualization properties. @@ -154,11 +179,20 @@ class in the target. fit_time_ : yellowbrick.utils.timer.Timer The amount of time in seconds it took to fit the Manifold. - classes_ : np.ndarray, optional - If discrete, the classes identified in the target y. + classes_ : ndarray, shape (n_classes,) + The class labels that define the discrete values in the target. Only + available if the target type is discrete. This is guaranteed to be + strings even if the classes are a different type. - range_ : tuple of floats, optional - If continuous, the maximum and minimum values in the target y. + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. + + range_ : (min y, max y) + A tuple that describes the minimum and maximum values in the target. + Only available if the target type is continuous. Examples -------- @@ -201,25 +235,38 @@ class in the target. def __init__( self, ax=None, - manifold="lle", - n_neighbors=10, + manifold="mds", + n_neighbors=None, + features=None, + classes=None, colors=None, - target=AUTO, - alpha=0.7, + colormap=None, + target_type="auto", + projection=2, + alpha=0.75, random_state=None, + colorbar=True, **kwargs ): - super(Manifold, self).__init__(ax, **kwargs) + + super(Manifold, self).__init__( + ax, + features, + classes, + colors, + colormap, + target_type, + projection, + alpha, + colorbar, + **kwargs + ) self._name = None self._manifold = None - self._target_color_type = None self.n_neighbors = n_neighbors - self.colors = colors - self.target = target - self.alpha = alpha self.random_state = random_state - self.manifold = manifold # must be set last + self.manifold = manifold # must be set last @property def manifold(self): @@ -239,14 +286,40 @@ def manifold(self, transformer): if not is_estimator(transformer): if transformer not in self.ALGORITHMS: raise YellowbrickValueError( - "could not create manifold for '%s'".format(str(transformer)) + "could not create manifold for '{}'".format(str(transformer)) ) + # 2 components is required for 2D plots + n_components = self.projection + requires_default_neighbors = { + "lle", + "ltsa", + "isomap", + "hessian", + "spectral", + "modified", + } + + # Check if the n_neighbors attribute needs to be set. + if self.n_neighbors is None and transformer in requires_default_neighbors: + if transformer == "hessian": + self.n_neighbors = int( + 1 + (n_components * (1 + (n_components + 1) / 2)) + ) + else: + self.n_neighbors = 5 + + # Issue a warning that the n_neighbors was set to a default. + warnmsg = ( + "using n_neighbors={}; please explicitly specify for the '{}' manifold" + ).format(self.n_neighbors, str(transformer)) + warnings.warn(warnmsg, YellowbrickWarning) + # Create a new transformer with the specified params self._name = MANIFOLD_NAMES[transformer] transformer = clone(self.ALGORITHMS[transformer]) params = { - "n_components": 2, + "n_components": n_components, "n_neighbors": self.n_neighbors, "random_state": self.random_state, } @@ -261,15 +334,42 @@ def manifold(self, transformer): if self._name is None: self._name = self._manifold.__class__.__name__ - def fit(self, X, y=None): + def fit(self, X, y=None, **kwargs): """ Fits the manifold on X and transforms the data to plot it on the axes. See fit_transform() for more details. + + Parameters + ---------- + X : array-like of shape (n, m) + A matrix or data frame with n instances and m features + + y : array-like of shape (n,), optional + A vector or series with target values for each instance in X. This + vector is used to determine the color of the points in X. + + Returns + ------- + self : Manifold + Returns the visualizer object. + """ - self.fit_transform(X, y) + if not hasattr(self.manifold, "transform"): + name = self.manifold.__class__.__name__ + raise ModelError( + ( + "{} requires data to be simultaneously fit and transformed, " + "use fit_transform instead" + ).format(name) + ) + + # Call super to compute features, classes, colors, etc. + super(Manifold, self).fit(X, y) + with Timer() as self.fit_time_: + self.manifold.fit(X) return self - def fit_transform(self, X, y=None): + def fit_transform(self, X, y=None, **kwargs): """ Fits the manifold on X and transforms the data to plot it on the axes. The optional y specified can be used to declare discrete colors. If @@ -282,7 +382,7 @@ def fit_transform(self, X, y=None): Parameters ---------- X : array-like of shape (n, m) - A matrix or data frame with n instances and m features where m > 2. + A matrix or data frame with n instances and m features y : array-like of shape (n,), optional A vector or series with target values for each instance in X. This @@ -290,37 +390,21 @@ def fit_transform(self, X, y=None): Returns ------- - self : Manifold - Returns the visualizer object. - """ - # Determine target type - self._determine_target_color_type(y) - - # Compute classes and colors if target type is discrete - if self._target_color_type == DISCRETE: - self.classes_ = np.unique(y) - - color_kwargs = {'n_colors': len(self.classes_)} - - if isinstance(self.colors, string_types): - color_kwargs['colormap'] = self.colors - else: - color_kwargs['colors'] = self.colors - - self._colors = resolve_colors(**color_kwargs) + Xprime : array-like of shape (n, 2) + Returns the 2-dimensional embedding of the instances. - # Compute target range if colors are continuous - elif self._target_color_type == CONTINUOUS: - y = np.asarray(y) - self.range_ = (y.min(), y.max()) + """ + # Because some manifolds do not have transform, we cannot call individual + # fit and transform methods, but must do it manually here. + # Call super fit to compute features, classes, colors, etc. + super(Manifold, self).fit(X, y) with Timer() as self.fit_time_: Xp = self.manifold.fit_transform(X) - self.draw(Xp, y) return Xp - def transform(self, X): + def transform(self, X, y=None, **kwargs): """ Returns the transformed data points from the manifold embedding. @@ -329,63 +413,40 @@ def transform(self, X): X : array-like of shape (n, m) A matrix or data frame with n instances and m features - Returns - ------- - Xprime : array-like of shape (n, 2) - Returns the 2-dimensional embedding of the instances. - """ - try: - return self.manifold.transform(X) - except AttributeError as e: - raise AttributeError(str(e) + " try using fit_transform instead.") - - def draw(self, X, y=None): - """ - Draws the points described by X and colored by the points in y. Can be - called multiple times before finalize to add more scatter plots to the - axes, however ``fit()`` must be called before use. - - Parameters - ---------- - X : array-like of shape (n, 2) - The matrix produced by the ``transform()`` method. - y : array-like of shape (n,), optional The target, used to specify the colors of the points. Returns ------- - self.ax : matplotlib Axes object - Returns the axes that the scatter plot was drawn on. - """ - scatter_kwargs = {"alpha": self.alpha} - - # Determine the colors - if self._target_color_type == SINGLE: - scatter_kwargs["c"] = "b" - - elif self._target_color_type == DISCRETE: - if y is None: - raise YellowbrickValueError("y is required for discrete target") - - scatter_kwargs["c"] = [ - self._colors[np.searchsorted(self.classes_, (yi))] for yi in y - ] - - elif self._target_color_type == CONTINUOUS: - if y is None: - raise YellowbrickValueError("y is required for continuous target") + Xprime : array-like of shape (n, 2) + Returns the 2-dimensional embedding of the instances. - # TODO manually make colorbar so we can draw it in finalize - scatter_kwargs["c"] = y - scatter_kwargs["cmap"] = self.colors or palettes.DEFAULT_SEQUENCE + Note + ---- + This method does not work with MDS, TSNE and SpectralEmbedding because + it is yet to be implemented in sklearn. + """ + # Because some manifolds do not have transform we cannot call super + try: + Xp = self.manifold.transform(X) + self.draw(Xp, y) + return Xp + except NotFittedError: + raise NotFitted.from_estimator(self, "transform") + except AttributeError: + name = self.manifold.__class__.__name__ + raise ModelError( + ( + "{} requires data to be simultaneously fit and transformed, " + "use fit_transform instead" + ).format(name) + ) - else: - # Technically this should never be raised - raise NotFitted("could not determine target color type") + return Xp - # Draw the scatter plot with the associated colors and alpha - self._scatter = self.ax.scatter(X[:,0], X[:,1], **scatter_kwargs) + def draw(self, Xp, y=None): + # Calls draw method from super class which draws scatter plot. + super(Manifold, self).draw(Xp, y) return self.ax def finalize(self): @@ -393,64 +454,37 @@ def finalize(self): Add title and modify axes to make the image ready for display. """ self.set_title( - '{} Manifold (fit in {:0.2f} seconds)'.format( + "{} Manifold (fit in {:0.2f} seconds)".format( self._name, self.fit_time_.interval ) ) - self.ax.set_xticklabels([]) - self.ax.set_yticklabels([]) - - if self._target_color_type == DISCRETE: - # Add the legend - manual_legend(self, self.classes_, self._colors) - - elif self._target_color_type == CONTINUOUS: - # Add the color bar - plt.colorbar(self._scatter, ax=self.ax) - - def _determine_target_color_type(self, y): - """ - Determines the target color type from the vector y as follows: - - - if y is None: only a single color is used - - if target is auto: determine if y is continuous or discrete - - otherwise specify supplied target type - - This property will be used to compute the colors for each point. - """ - if y is None: - self._target_color_type = SINGLE - elif self.target == "auto": - # NOTE: See #73 for a generalization to use when implemented - if len(np.unique(y)) < 10: - self._target_color_type = DISCRETE - else: - self._target_color_type = CONTINUOUS - else: - self._target_color_type = self.target - - if self._target_color_type not in {SINGLE, DISCRETE, CONTINUOUS}: - raise YellowbrickValueError(( - "could not determine target color type " - "from target='{}' to '{}'" - ).format(self.target, self._target_color_type)) + self.ax.set_xlabel("Using {} features".format(len(self.features_))) + # Draws legend for discrete target and colorbar for continuous. + super(Manifold, self).finalize() ########################################################################## ## Quick Method ########################################################################## + def manifold_embedding( X, y=None, ax=None, - manifold="lle", - n_neighbors=10, + manifold="mds", + n_neighbors=None, + features=None, + classes=None, colors=None, - target=AUTO, - alpha=0.7, + colormap=None, + target_type="auto", + projection=2, + alpha=0.75, random_state=None, - **kwargs): + colorbar=True, + **kwargs +): """Quick method for Manifold visualizer. The Manifold visualizer provides high dimensional visualization for feature @@ -470,9 +504,9 @@ def manifold_embedding( A vector or series with target values for each instance in X. This vector is used to determine the color of the points in X. - ax : matplotlib Axes, default: None - The axes to plot the figure on. If None, the current axes will be used - or generated if required. + ax : matplotlib.Axes, default: None + The axis to plot the figure on. If None is passed in the current axes + will be used (or generated if required). manifold : str or Transformer, default: "lle" Specify the manifold algorithm to perform the embedding. Either one of @@ -493,54 +527,116 @@ def manifold_embedding( ``"tsne"`` `t-SNE `_ ============== ========================== - n_neighbors : int, default: 10 + n_neighbors : int, default: None Many manifold algorithms are nearest neighbors based, for those that are, this parameter specfies the number of neighbors to use in the - embedding. If the manifold algorithm doesn't use nearest neighbors, - then this parameter is ignored. - - colors : str or list of colors, default: None - Specify the colors used, though note that the specification depends - very much on whether the target is continuous or discrete. If - continuous, colors must be the name of a colormap. If discrete, then - colors can be the name of a palette or a list of colors to use for each - class in the target. - - target : str, default: "auto" + embedding. If n_neighbors is not specified for those embeddings, it is + set to 5 and a warning is issued. If the manifold algorithm doesn't use + nearest neighbors, then this parameter is ignored. + + features : list, default: None + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. + + classes : list, default: None + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. + + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. This parameter is only used in the + discrete target type case and is ignored otherwise. + + colors : list or tuple, default: None + A single color to plot all instances as or a list of colors to color each + instance according to its class in the discrete case or as an ordered + colormap in the sequential case. If not enough colors per class are + specified then the colors are treated as a cycle. + + colormap : string or cmap, default: None + The colormap used to create the individual colors. In the discrete case + it is used to compute the number of colors needed for each class and + in the continuous case it is used to create a sequential color map based + on the range of the target. + + target_type : str, default: "auto" Specify the type of target as either "discrete" (classes) or "continuous" - (real numbers, usually for regression). If "auto", the Manifold will + (real numbers, usually for regression). If "auto", then it will attempt to determine the type by counting the number of unique values. - If the target is discrete, points will be colored by the target class - and a legend will be displayed. If continuous, points will be displayed - with a colormap and a color bar will be displayed. In either case, if - no target is specified, only a single color will be drawn. + If the target is discrete, the colors are returned as a dict with classes + being the keys. If continuous the colors will be list having value of + color for each point. In either case, if no target is specified, then + color will be specified as the first color in the color cycle. + + projection : int or string, default: 2 + The number of axes to project into, either 2d or 3d. To plot 3d plots + with matplotlib, please ensure a 3d axes is passed to the visualizer, + otherwise one will be created using the current figure. - alpha : float, default: 0.7 + alpha : float, default: 0.75 Specify a transparency where 1 is completely opaque and 0 is completely transparent. This property makes densely clustered points more visible. random_state : int or RandomState, default: None Fixes the random state for stochastic manifold algorithms. + colorbar : bool, default: True + If the target_type is "continous" draw a colorbar to the right of the + scatter plot. The colobar axes is accessible using the cax property. + kwargs : dict Keyword arguments passed to the base class and may influence the feature visualization properties. + Attributes + ---------- + fit_time_ : yellowbrick.utils.timer.Timer + The amount of time in seconds it took to fit the Manifold. + + classes_ : ndarray, shape (n_classes,) + The class labels that define the discrete values in the target. Only + available if the target type is discrete. This is guaranteed to be + strings even if the classes are a different type. + + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. + + range_ : (min y, max y) + A tuple that describes the minimum and maximum values in the target. + Only available if the target type is continuous. + + Returns ------- - ax : matplotlib axes - Returns the axes that the embedded scatter plot was drawn on. + viz : Manifold + Returns the fitted, finalized visualizer """ # Instantiate the visualizer viz = Manifold( - ax=ax, manifold=manifold, n_neighbors=n_neighbors, colors=colors, - target=target, alpha = alpha, random_state=random_state, **kwargs + ax=ax, + manifold=manifold, + n_neighbors=n_neighbors, + features=features, + classes=classes, + colors=colors, + colormap=colormap, + target_type=target_type, + projection=projection, + alpha=alpha, + random_state=random_state, + colorbar=colorbar, + **kwargs ) - # Fit and poof (calls draw) - viz.fit(X, y) - viz.poof() + # Fit and finalize (calls draw) + viz.fit_transform(X, y) + viz.finalize() - # Return the axes object - return viz.ax + # Return the visualizer object + return viz diff --git a/yellowbrick/features/pca.py b/yellowbrick/features/pca.py index 981104039..59b6ee5b6 100644 --- a/yellowbrick/features/pca.py +++ b/yellowbrick/features/pca.py @@ -2,11 +2,14 @@ # yellowbrick.features.pca # Decomposition based feature visualization with PCA. # -# Author: Carlo Morales <@cjmorale> -# Author: Raúl Peralta Lozada <@RaulPL> -# Author: Benjamin Bengfort <@bbengfort> +# Author: Carlo Morales +# Author: Raúl Peralta Lozada +# Author: Benjamin Bengfort # Created: Tue May 23 18:34:27 2017 -0400 # +# Copyright (C) 2017 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: pca.py [] cmorales@pacificmetrics.com $ """ @@ -18,110 +21,245 @@ ########################################################################## # NOTE: must import mplot3d to load the 3D projection -import mpl_toolkits.mplot3d # noqa +import numpy as np import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable -from yellowbrick.features.base import MultiFeatureVisualizer from yellowbrick.style import palettes -from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.features.projection import ProjectionVisualizer +from yellowbrick.exceptions import YellowbrickValueError, NotFitted from sklearn.pipeline import Pipeline -from sklearn.decomposition import PCA +from sklearn.decomposition import PCA as PCATransformer from sklearn.preprocessing import StandardScaler +from sklearn.exceptions import NotFittedError ########################################################################## -##2D and #3D PCA Visualizer +# 2D and 3D PCA Visualizer ########################################################################## -class PCADecomposition(MultiFeatureVisualizer): + +class PCA(ProjectionVisualizer): """ Produce a two or three dimensional principal component plot of a data array - projected onto it's largest sequential principal components. It is common + projected onto its largest sequential principal components. It is common practice to scale the data array ``X`` before applying a PC decomposition. Variable scaling can be controlled using the ``scale`` argument. Parameters ---------- ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes. + The axes to plot the figure on. If None is passed in, the current axes will be used (or generated if required). - features: list, default: None - a list of feature names to use - If a DataFrame is passed to fit and features is None, feature - names are selected as the columns of the DataFrame. + features : list, default: None + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. + + classes : list, default: None + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. + + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. This parameter is only used in the + discrete target type case and is ignored otherwise. scale : bool, default: True Boolean that indicates if user wants to scale data. - proj_dim : int, default: 2 - Dimension of the PCA visualizer. + projection : int or string, default: 2 + The number of axes to project into, either 2d or 3d. To plot 3d plots + with matplotlib, please ensure a 3d axes is passed to the visualizer, + otherwise one will be created using the current figure. proj_features : bool, default: False Boolean that indicates if the user wants to project the features in the projected space. If True the plot will be similar to a biplot. - color : list or tuple of colors, default: None - Specify the colors for each individual class. + colors : list or tuple, default: None + A single color to plot all instances as or a list of colors to color each + instance according to its class in the discrete case or as an ordered + colormap in the sequential case. If not enough colors per class are + specified then the colors are treated as a cycle. colormap : string or cmap, default: None - Optional string or matplotlib cmap to colorize lines. - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. + The colormap used to create the individual colors. In the discrete case + it is used to compute the number of colors needed for each class and + in the continuous case it is used to create a sequential color map based + on the range of the target. + + alpha : float, default: 0.75 + Specify a transparency where 1 is completely opaque and 0 is completely + transparent. This property makes densely clustered points more visible. random_state : int, RandomState instance or None, optional (default None) - If input data is larger than 500x500 and the number of components to - extract is lower than 80% of the smallest dimension of the data, then - the more efficient `randomized` solver is enabled, this parameter sets - the random state on this solver. + This parameter sets the random state on this solver. If the input X is + larger than 500x500 and the number of components to extract is lower + than 80% of the smallest dimension of the data, then the more efficient + `randomized` solver is enabled. + + colorbar : bool, default: True + If the target_type is "continous" draw a colorbar to the right of the + scatter plot. The colobar axes is accessible using the cax property. + + heatmap : bool, default: False + Add a heatmap showing contribution of each feature in the principal components. + Also draws a colorbar for readability purpose. The heatmap is accessible + using lax property and colorbar using uax property. kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ---------- + pca_components_ : ndarray, shape (n_features, n_components) + This tells about the magnitude of each feature in the pricipal components. + This is primarily used to draw the biplots. + + classes_ : ndarray, shape (n_classes,) + The class labels that define the discrete values in the target. Only + available if the target type is discrete. This is guaranteed to be + strings even if the classes are a different type. + + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. + + range_ : (min y, max y) + A tuple that describes the minimum and maximum values in the target. + Only available if the target type is continuous. + Examples -------- >>> from sklearn import datasets >>> iris = datasets.load_iris() >>> X = iris.data >>> y = iris.target - >>> visualizer = PCADecomposition() - >>> visualizer.fit_transform(X) + >>> visualizer = PCA() + >>> visualizer.fit_transform(X, y) >>> visualizer.poof() """ - def __init__(self, - ax=None, - features=None, - scale=True, - proj_dim=2, - proj_features=False, - color=None, - colormap=palettes.DEFAULT_SEQUENCE, - random_state=None, - **kwargs): - super(PCADecomposition, self).__init__(ax=ax, - features=features, - **kwargs) - - if proj_dim not in (2, 3): - raise YellowbrickValueError("proj_dim object is not 2 or 3.") + + def __init__( + self, + ax=None, + features=None, + classes=None, + scale=True, + projection=2, + proj_features=False, + colors=None, + colormap=None, + alpha=0.75, + random_state=None, + colorbar=True, + heatmap=False, + **kwargs + ): + super(PCA, self).__init__( + ax=ax, + features=features, + classes=classes, + colors=colors, + colormap=colormap, + projection=projection, + alpha=alpha, + colorbar=colorbar, + **kwargs + ) # Data Parameters self.scale = scale - self.proj_dim = proj_dim self.proj_features = proj_features # Create the PCA transformer self.pca_transformer = Pipeline( - [('scale', StandardScaler(with_std=self.scale)), - ('pca', PCA(self.proj_dim, random_state=random_state))] + [ + ("scale", StandardScaler(with_std=self.scale)), + ("pca", PCATransformer(self.projection, random_state=random_state)), + ] ) + self.alpha = alpha # Visual Parameters - self.color = color - self.colormap = colormap + self.heatmap = heatmap + + self._uax, self._lax = None, None + + # No heatmap can be drawn with 3d plots as they do not have permit axes + # division. + if self.projection == 3 and self.heatmap: + raise YellowbrickValueError( + "heatmap and colorbar are not compatible with 3d projections" + ) + + @property + def uax(self): + """ + The axes of the colorbar, bottom of scatter plot. This is the colorbar + for heatmap and not for the scatter plot. + """ + if self._uax is None: + raise AttributeError("This visualizer does not have an axes for colorbar") + + return self._uax + + @property + def lax(self): + """ + The axes of the heatmap below scatter plot. + """ + if self._lax is None: + raise AttributeError("This visualizer does not have an axes for heatmap") + + return self._lax + + def layout(self, divider=None): + """ + Creates the layout for colorbar and heatmap, adding new axes for the heatmap + if necessary and modifying the aspect ratio. Does not modify the axes or the + layout if ``self.heatmap`` is ``False`` or ``None``. + + Parameters + ---------- + divider: AxesDivider + An AxesDivider to be passed among all layout calls. + """ + + # Ensure matplotlib version compatibility + if make_axes_locatable is None: + raise YellowbrickValueError( + ( + "heatmap requires matplotlib 2.0.2 or greater " + "please upgrade matplotlib or set heatmap=False on the visualizer" + ) + ) + + # Create the new axes for the colorbar and heatmap + if divider is None: + divider = make_axes_locatable(self.ax) + + # Call to super class ensures that a colorbar is drawn when target is + # continuous. + super(PCA, self).layout(divider) + + if self.heatmap: + + # Axes for colorbar(for heatmap). + if self._uax is None: + self._uax = divider.append_axes("bottom", size="10%", pad=0.7) + + # Axes for heatmap + if self._lax is None: + self._lax = divider.append_axes("bottom", size="15%", pad=0.5) def fit(self, X, y=None, **kwargs): """ @@ -139,88 +277,205 @@ def fit(self, X, y=None, **kwargs): Returns ------- self : visualizer - Returns self for use in Pipelines + Returns self for use in Pipelines. + + """ - super(PCADecomposition, self).fit(X=X, y=y, **kwargs) + # Call super fit to compute features, classes, colors, etc. + super(PCA, self).fit(X=X, y=y, **kwargs) self.pca_transformer.fit(X) - self.pca_components_ = self.pca_transformer.named_steps['pca'].components_ + self.pca_components_ = self.pca_transformer.named_steps["pca"].components_ return self def transform(self, X, y=None, **kwargs): - self.pca_features_ = self.pca_transformer.transform(X) - self.draw() - return self.pca_features_ - - def draw(self, **kwargs): - X = self.pca_features_ - if self.proj_dim == 2: - self.ax.scatter(X[:, 0], X[:, 1], c=self.color, cmap=self.colormap) - if self.proj_features: - x_vector = self.pca_components_[0] - y_vector = self.pca_components_[1] - max_x = max(X[:, 0]) - max_y = max(X[:, 1]) - for i in range(self.pca_components_.shape[1]): - self.ax.arrow( - x=0, y=0, - dx=x_vector[i] * max_x, - dy=y_vector[i] * max_y, - color='r', head_width=0.05, - width=0.005, - ) - self.ax.text( - x_vector[i] * max_x * 1.05, - y_vector[i] * max_y * 1.05, - self.features_[i], color='r' - ) - if self.proj_dim == 3: - self.fig = plt.figure() - self.ax = self.fig.add_subplot(111, projection='3d') - self.ax.scatter(X[:, 0], X[:, 1], X[:, 2], - c=self.color, cmap=self.colormap) - if self.proj_features: - x_vector = self.pca_components_[0] - y_vector = self.pca_components_[1] - z_vector = self.pca_components_[2] - max_x = max(X[:, 0]) - max_y = max(X[:, 1]) - max_z = max(X[:, 1]) - for i in range(self.pca_components_.shape[1]): - self.ax.plot( - [0, x_vector[i] * max_x], - [0, y_vector[i] * max_y], - [0, z_vector[i] * max_z], - color='r' - ) - self.ax.text( - x_vector[i] * max_x * 1.05, - y_vector[i] * max_y * 1.05, - z_vector[i] * max_z * 1.05, - self.features_[i], color='r' - ) + """ + Calls the internal `transform` method of the scikit-learn PCA transformer, which + performs a dimensionality reduction on the input features ``X``. Next calls the + ``draw`` method of the Yellowbrick visualizer, finally returning a new array of + transformed features of shape ``(len(X), proj_dim)``. + + Parameters + ---------- + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features. + + y : ndarray or Series of length n + An array or series of target or class values. + + Returns + ------- + Xp : ndarray or DataFrame of shape n x m + Returns a new array-like object of transformed features of shape + ``(len(X), proj_dim)``. + """ + try: + Xp = self.pca_transformer.transform(X) + self.draw(Xp, y) + return Xp + except NotFittedError: + raise NotFitted.from_estimator(self, "transform") + + def draw(self, Xp, y): + """ + Plots a scatterplot of points that represented the decomposition, + `pca_features_`, of the original features, `X`, projected into either 2 or + 3 dimensions. + + If 2 dimensions are selected, a colorbar and heatmap can also be optionally + included to show the magnitude of each feature value to the component. + + Parameters + ---------- + Xp : array-like of shape (n, 2) or (n, 3) + The matrix produced by the ``transform()`` method. + + y : array-like of shape (n,), optional + The target, used to specify the colors of the points. + + + Returns + ------- + self.ax : matplotlib Axes object + Returns the axes that the scatter plot was drawn on. + """ + # Call to super draw which draws the scatter plot. + super(PCA, self).draw(Xp, y) + if self.proj_features: + # Draws projection features in transformed space. + self._draw_projection_features(Xp, y) + if self.projection == 2: + if self.heatmap: + if not self.colormap: + self.colormap = palettes.DEFAULT_SEQUENCE + # TODO: change to pcolormesh instead of imshow per #615 spec + im = self.lax.imshow( + self.pca_components_, + interpolation="none", + cmap=self.colormap, + aspect="auto", + ) + plt.colorbar( + im, + cax=self.uax, + orientation="horizontal", + ticks=[self.pca_components_.min(), 0, self.pca_components_.max()], + ) + return self.ax + + def _draw_projection_features(self, Xp, y): + """ + Draw the projection of features in the transformed space. + Parameters + ---------- + Xp : array-like of shape (n, 2) or (n, 3) + The matrix produced by the ``transform()`` method. + + y : array-like of shape (n,), optional + The target, used to specify the colors of the points. + + Returns + ------- + self.ax : matplotlib Axes object + Returns the axes that the scatter plot was drawn on. + + """ + + x_vector = self.pca_components_[0] + y_vector = self.pca_components_[1] + max_x = max(Xp[:, 0]) + max_y = max(Xp[:, 1]) + if self.projection == 2: + for i in range(self.pca_components_.shape[1]): + self.ax.arrow( + x=0, + y=0, + dx=x_vector[i] * max_x, + dy=y_vector[i] * max_y, + color="r", + head_width=0.05, + width=0.005, + ) + self.ax.text( + x_vector[i] * max_x * 1.05, + y_vector[i] * max_y * 1.05, + self.features_[i], + color="r", + ) + elif self.projection == 3: + z_vector = self.pca_components_[2] + max_z = max(Xp[:, 1]) + for i in range(self.pca_components_.shape[1]): + self.ax.plot( + [0, x_vector[i] * max_x], + [0, y_vector[i] * max_y], + [0, z_vector[i] * max_z], + color="r", + ) + self.ax.text( + x_vector[i] * max_x * 1.05, + y_vector[i] * max_y * 1.05, + z_vector[i] * max_z * 1.05, + self.features_[i], + color="r", + ) + else: + raise YellowbrickValueError("Projection dimensions must be either 2 or 3") + return self.ax def finalize(self, **kwargs): - # Set the title - self.ax.set_title('Principal Component Plot') - self.ax.set_xlabel('Principal Component 1') - self.ax.set_ylabel('Principal Component 2') - if self.proj_dim == 3: - self.ax.set_zlabel('Principal Component 3') + """ + Draws the title, labels, legends, heatmap, and colorbar as specified by the + keyword arguments. + """ + super(PCA, self).finalize() + + self.ax.set_title("Principal Component Plot") + self.ax.set_xlabel("$PC_1$") + self.ax.set_ylabel("$PC_2$") + if self.projection == 3: + self.ax.set_zlabel("$PC_3$") + if self.heatmap == True: + self.lax.set_xticks(np.arange(-0.5, len(self.features_))) + self.lax.set_xticklabels([]) + # Makes the labels centered. + self.lax.set_xticks(np.arange(0, len(self.features_)), minor=True) + self.lax.set_xticklabels( + self.features_, rotation=90, fontsize=12, minor=True + ) + self.lax.set_yticks(np.arange(0.5, 2)) + self.lax.set_yticklabels(["$PC_1$", "$PC_2$"], va="bottom", fontsize=10) + self.fig.tight_layout() ########################################################################## ## Quick Method ########################################################################## -def pca_decomposition(X, y=None, ax=None, features=None, scale=True, - proj_dim=2, proj_features=False, color=None, - colormap=palettes.DEFAULT_SEQUENCE, - random_state=None, **kwargs): - """Produce a two or three dimensional principal component plot of the data array ``X`` - projected onto it's largest sequential principal components. It is common practice to scale the - data array ``X`` before applying a PC decomposition. Variable scaling can be controlled using - the ``scale`` argument. + +def pca_decomposition( + X, + y=None, + ax=None, + features=None, + classes=None, + scale=True, + projection=2, + proj_features=False, + colors=None, + colormap=None, + alpha=0.75, + random_state=None, + colorbar=True, + heatmap=False, + **kwargs +): + + """ + Produce a two or three dimensional principal component plot of the data array ``X`` + projected onto its largest sequential principal components. It is common practice + to scale the data array ``X`` before applying a PC decomposition. Variable scaling + can be controlled using the ``scale`` argument. Parameters ---------- @@ -231,62 +486,125 @@ def pca_decomposition(X, y=None, ax=None, features=None, scale=True, An array or series of target or class values. ax : matplotlib Axes, default: None - The axes to plot the figure on. If None is passed in the current axes. + The axes to plot the figure on. If None is passed in, the current axes will be used (or generated if required). - features: list, default: None - a list of feature names to use - If a DataFrame is passed to fit and features is None, feature - names are selected as the columns of the DataFrame. + features : list, default: None + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. + + classes : list, default: None + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. + + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. This parameter is only used in the + discrete target type case and is ignored otherwise. scale : bool, default: True Boolean that indicates if user wants to scale data. - proj_dim : int, default: 2 - Dimension of the PCA visualizer. + projection : int or string, default: 2 + The number of axes to project into, either 2d or 3d. To plot 3d plots + with matplotlib, please ensure a 3d axes is passed to the visualizer, + otherwise one will be created using the current figure. proj_features : bool, default: False Boolean that indicates if the user wants to project the features in the projected space. If True the plot will be similar to a biplot. - color : list or tuple of colors, default: None - Specify the colors for each individual class. + colors : list or tuple, default: None + A single color to plot all instances as or a list of colors to color each + instance according to its class in the discrete case or as an ordered + colormap in the sequential case. If not enough colors per class are + specified then the colors are treated as a cycle. colormap : string or cmap, default: None - Optional string or matplotlib cmap to colorize lines. - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. + The colormap used to create the individual colors. In the discrete case + it is used to compute the number of colors needed for each class and + in the continuous case it is used to create a sequential color map based + on the range of the target. + + alpha : float, default: 0.75 + Specify a transparency where 1 is completely opaque and 0 is completely + transparent. This property makes densely clustered points more visible. random_state : int, RandomState instance or None, optional (default None) - If input data is larger than 500x500 and the number of components to - extract is lower than 80% of the smallest dimension of the data, then - the more efficient `randomized` solver is enabled, this parameter sets - the random state on this solver. + This parameter sets the random state on this solver. If the input X is + larger than 500x500 and the number of components to extract is lower + than 80% of the smallest dimension of the data, then the more efficient + `randomized` solver is enabled. + + colorbar : bool, default: True + If the target_type is "continous" draw a colorbar to the right of the + scatter plot. The colobar axes is accessible using the cax property. + + heatmap : bool, default: False + Add a heatmap showing contribution of each feature in the principal components. + Also draws a colorbar for readability purpose. The heatmap is accessible + using lax property and colorbar using uax property. kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ---------- + pca_components_ : ndarray, shape (n_features, n_components) + This tells about the magnitude of each feature in the pricipal components. + This is primarily used to draw the biplots. + + classes_ : ndarray, shape (n_classes,) + The class labels that define the discrete values in the target. Only + available if the target type is discrete. This is guaranteed to be + strings even if the classes are a different type. + + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. + + range_ : (min y, max y) + A tuple that describes the minimum and maximum values in the target. + Only available if the target type is continuous. + Examples -------- >>> from sklearn import datasets >>> iris = datasets.load_iris() >>> X = iris.data >>> y = iris.target - >>> pca_decomposition(X, color=y, proj_dim=3, colormap='RdBu_r') + >>> pca_decomposition(X, y, colors=['r', 'g', 'b'], projection=3) """ # Instantiate the visualizer - visualizer = PCADecomposition( - ax=ax, features=features, scale=scale, proj_dim=proj_dim, - proj_features=proj_features, color=color, colormap=colormap, - random_state=random_state, **kwargs + visualizer = PCA( + ax=ax, + features=features, + scale=scale, + projection=projection, + proj_features=proj_features, + colors=colors, + colormap=colormap, + alpha=alpha, + random_state=random_state, + colorbar=colorbar, + heatmap=heatmap, + **kwargs ) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y) - visualizer.transform(X) - visualizer.poof() + visualizer.transform(X, y) + visualizer.finalize() + + # Returns the visualizer object. + return visualizer + - # Return the axes object on the visualizer - return visualizer.ax +# Alias for PCA +PCADecomposition = PCA diff --git a/yellowbrick/features/pcoords.py b/yellowbrick/features/pcoords.py index f5c742ed4..8876e00b3 100644 --- a/yellowbrick/features/pcoords.py +++ b/yellowbrick/features/pcoords.py @@ -1,11 +1,11 @@ # yellowbrick.features.pcoords # Implementations of parallel coordinates for feature analysis. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Author: @thekylesaurus # Created: Mon Oct 03 21:46:06 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: pcoords.py [0f4b236] benjamin@bengfort.com $ @@ -20,26 +20,37 @@ import numpy as np -from six import string_types from numpy.random import RandomState from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler from sklearn.preprocessing import Normalizer, StandardScaler from yellowbrick.draw import manual_legend -from yellowbrick.utils import is_dataframe, is_series from yellowbrick.features.base import DataVisualizer +from yellowbrick.utils import is_dataframe, is_series from yellowbrick.exceptions import YellowbrickTypeError, YellowbrickValueError -from yellowbrick.style.colors import resolve_colors ########################################################################## ## Quick Methods ########################################################################## -def parallel_coordinates(X, y, ax=None, features=None, classes=None, - normalize=None, sample=1.0, color=None, colormap=None, - alpha=None, fast=False, vlines=True, vlines_kwds=None, - **kwargs): + +def parallel_coordinates( + X, + y, + ax=None, + features=None, + classes=None, + normalize=None, + sample=1.0, + colors=None, + colormap=None, + alpha=None, + fast=False, + vlines=True, + vlines_kwds=None, + **kwargs +): """Displays each feature as a vertical axis and each instance as a line. This helper function is a quick wrapper to utilize the ParallelCoordinates @@ -78,7 +89,7 @@ def parallel_coordinates(X, y, ax=None, features=None, classes=None, If int, specifies the maximum number of samples to display. If float, specifies a fraction between 0 and 1 to display. - color : list or tuple, default: None + colors : list or tuple, default: None optional list or tuple of colors to colorize lines Use either color to colorize the lines on a per class basis or colormap to color them on a continuous scale. @@ -111,27 +122,38 @@ def parallel_coordinates(X, y, ax=None, features=None, classes=None, Returns ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. + viz : ParallelCoordinates + Returns the fitted, finalized visualizer """ # Instantiate the visualizer visualizer = ParallelCoordinates( - ax, features, classes, normalize, sample, color, colormap, alpha, - fast, vlines, vlines_kwds, **kwargs + ax=ax, + features=features, + classes=classes, + normalize=normalize, + sample=sample, + colors=colors, + colormap=colormap, + alpha=alpha, + fast=fast, + vlines=vlines, + vlines_kwds=vlines_kwds, + **kwargs ) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y, **kwargs) visualizer.transform(X) - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer object + return visualizer ########################################################################## ## Static Parallel Coordinates Visualizer ########################################################################## + class ParallelCoordinates(DataVisualizer): """ Parallel coordinates displays each feature as a vertical axis spaced @@ -141,7 +163,6 @@ class ParallelCoordinates(DataVisualizer): Parameters ---------- - ax : matplotlib Axes, default: None The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). @@ -153,8 +174,13 @@ class ParallelCoordinates(DataVisualizer): classes : list, default: None a list of class names for the legend - If classes is None and a y value is passed to fit then the classes - are selected from the target vector. + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. + + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. normalize : string or None, default: None specifies which normalization method to use, if any @@ -175,15 +201,14 @@ class ParallelCoordinates(DataVisualizer): shuffle : boolean, default: True specifies whether sample is drawn randomly - color : list or tuple, default: None - optional list or tuple of colors to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. + colors : list or tuple, default: None + A single color to plot all instances as or a list of colors to color each + instance according to its class. If not enough colors per class are + specified then the colors are treated as a cycle. colormap : string or cmap, default: None - optional string or matplotlib cmap to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. + The colormap used to create the individual colors. If classes are + specified the colormap is used to evenly space colors across each class. alpha : float, default: None Specify a transparency where 1 is completely opaque and 0 is completely @@ -211,6 +236,17 @@ class ParallelCoordinates(DataVisualizer): n_samples_ : int number of samples included in the visualization object + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. + + classes_ : ndarray, shape (n_classes,) + The class labels that define the discrete values in the target. Only + available if the target type is discrete. This is guaranteed to be + strings even if the classes are a different type. + Examples -------- @@ -227,31 +263,39 @@ class ParallelCoordinates(DataVisualizer): """ NORMALIZERS = { - 'minmax': MinMaxScaler(), - 'maxabs': MaxAbsScaler(), - 'standard': StandardScaler(), - 'l1': Normalizer('l1'), - 'l2': Normalizer('l2'), + "minmax": MinMaxScaler(), + "maxabs": MaxAbsScaler(), + "standard": StandardScaler(), + "l1": Normalizer("l1"), + "l2": Normalizer("l2"), } - def __init__(self, - ax=None, - features=None, - classes=None, - normalize=None, - sample=1.0, - random_state=None, - shuffle=False, - color=None, - colormap=None, - alpha=None, - fast=False, - vlines=True, - vlines_kwds=None, - **kwargs): - + def __init__( + self, + ax=None, + features=None, + classes=None, + normalize=None, + sample=1.0, + random_state=None, + shuffle=False, + colors=None, + colormap=None, + alpha=None, + fast=False, + vlines=True, + vlines_kwds=None, + **kwargs + ): + if "target_type" not in kwargs: + kwargs["target_type"] = "discrete" super(ParallelCoordinates, self).__init__( - ax, features, classes, color, colormap, **kwargs + ax=ax, + features=features, + classes=classes, + colors=colors, + colormap=colormap, + **kwargs ) # Validate 'normalize' argument @@ -259,8 +303,7 @@ def __init__(self, self.normalize = normalize else: raise YellowbrickValueError( - "'{}' is an unrecognized normalization method" - .format(normalize) + "'{}' is an unrecognized normalization method".format(normalize) ) # Validate 'sample' argument @@ -275,18 +318,14 @@ def __init__(self, "`sample` parameter of type `float` must be between 0 and 1" ) else: - raise YellowbrickTypeError( - "`sample` parameter must be int or float" - ) + raise YellowbrickTypeError("`sample` parameter must be int or float") self.sample = sample # Set sample parameters if isinstance(shuffle, bool): self.shuffle = shuffle else: - raise YellowbrickTypeError( - "`shuffle` parameter must be boolean" - ) + raise YellowbrickTypeError("`shuffle` parameter must be boolean") if self.shuffle: if (random_state is None) or isinstance(random_state, int): self._rng = RandomState(random_state) @@ -294,7 +333,7 @@ def __init__(self, self._rng = random_state else: raise YellowbrickTypeError( - "`random_state` parameter must be None, int, or np.random.RandomState" + "`random_state` must be None, int, or np.random.RandomState" ) else: self._rng = None @@ -303,9 +342,7 @@ def __init__(self, self.fast = fast self.alpha = alpha self.show_vlines = vlines - self.vlines_kwds = vlines_kwds or { - 'linewidth': 1, 'color': 'black' - } + self.vlines_kwds = vlines_kwds or {"linewidth": 1, "color": "black"} # Internal properties self._increments = None @@ -333,34 +370,15 @@ def fit(self, X, y=None, **kwargs): self : instance Returns the instance of the transformer/visualizer """ + # Determine the features, classes, and colors + super(ParallelCoordinates, self).fit(X, y) # Convert from pandas data types if is_dataframe(X): - # Get column names before reverting to an np.ndarray - if self.features_ is None: - self.features_ = np.array(X.columns) - X = X.values if is_series(y): y = y.values - # Assign integer labels to the feature columns from the input - if self.features_ is None: - self.features_ = np.arange(0, X.shape[1]) - - # Ensure that all classes are represented in the color mapping (before sample) - # NOTE: np.unique also specifies the ordering of the classes - if self.classes_ is None: - self.classes_ = [str(label) for label in np.unique(y)] - - # Create the color mapping for each class - # TODO: Allow both colormap, listed colors, and palette definition - # TODO: Make this an independent function or property for override! - color_values = resolve_colors( - n_colors=len(self.classes_), colormap=self.colormap, colors=self.color - ) - self._colors = dict(zip(self.classes_, color_values)) - # Ticks for each feature specified self._increments = np.arange(len(self.features_)) @@ -371,8 +389,8 @@ def fit(self, X, y=None, **kwargs): if self.normalize is not None: X = self.NORMALIZERS[self.normalize].fit_transform(X) - # the super method calls draw and returns self - return super(ParallelCoordinates, self).fit(X, y, **kwargs) + self.draw(X, y, **kwargs) + return self def draw(self, X, y, **kwargs): """ @@ -424,18 +442,9 @@ def draw_instances(self, X, y, **kwargs): for idx in range(len(X)): Xi = X[idx] yi = y[idx] + color = self.get_colors([yi])[0] - # TODO: generalize this duplicated code into a single function - if isinstance(yi, string_types): - label = yi - else: - # TODO: what happens if yi is not in classes?! - label = self.classes_[yi] - - self.ax.plot( - self._increments, Xi, - color=self._colors[label], alpha=alpha, **kwargs - ) + self.ax.plot(self._increments, Xi, color=color, alpha=alpha, **kwargs) return self.ax @@ -470,18 +479,18 @@ def draw_classes(self, X, y, **kwargs): # Plot each class as a single line plot for yi in y_values: - if isinstance(yi, string_types): - label = yi - else: - # TODO: what happens if yi is not in classes?! - label = self.classes_[yi] + color = self.get_colors([yi])[0] X_in_class = X_separated[y == yi, :] increments_in_class = increments_separated * len(X_in_class) if len(X_in_class) > 0: self.ax.plot( - increments_in_class, X_in_class.flatten(), linewidth=1, - color=self._colors[label], alpha=alpha, **kwargs + increments_in_class, + X_in_class.flatten(), + linewidth=1, + color=color, + alpha=alpha, + **kwargs ) return self.ax @@ -498,7 +507,7 @@ def finalize(self, **kwargs): """ # Set the title self.set_title( - 'Parallel Coordinates for {} Features'.format(len(self.features_)) + "Parallel Coordinates for {} Features".format(len(self.features_)) ) # Add the vertical lines @@ -515,7 +524,7 @@ def finalize(self, **kwargs): # Add the legend sorting classes by name labels = sorted(list(self._colors.keys())) colors = [self._colors[lbl] for lbl in labels] - manual_legend(self, labels, colors, loc='best', frameon=True) + manual_legend(self, labels, colors, loc="best", frameon=True) # Add the grid view self.ax.grid() diff --git a/yellowbrick/features/projection.py b/yellowbrick/features/projection.py new file mode 100644 index 000000000..6f3120679 --- /dev/null +++ b/yellowbrick/features/projection.py @@ -0,0 +1,335 @@ +# yellowbrick.features.projection +# Base class for all projection (decomposition) high dimensional data visualizers. +# +# Author: Naresh Bachwani +# Created: Wed Jul 17 08:59:33 2019 -0400 +# +# Copyright (C) 2019, the scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: projection.py [21eb9d2] 43993586+naresh-bachwani@users.noreply.github.com $ + +""" +Base class for all projection (decomposition) high dimensional data visualizers. +""" + +########################################################################## +## Imports +########################################################################## + +import warnings +import matplotlib as mpl +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable +import mpl_toolkits.mplot3d # noqa + +from yellowbrick.draw import manual_legend +from yellowbrick.features.base import DataVisualizer, TargetType +from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning, NotFitted + + +########################################################################## +## Projection Visualizers +########################################################################## + + +class ProjectionVisualizer(DataVisualizer): + """ + The ProjectionVisualizer provides functionality for projecting a multi-dimensional + dataset into either 2 or 3 components so they can be plotted as a scatter plot on + 2d or 3d axes. The visualizer acts as a transformer, and draws the transformed data + on behalf of the user. Because it is a DataVisualizer, the ProjectionVisualizer + can plot continuous scatter plots with a colormap or discrete scatter plots with + a legend. + + This visualizer is a base class and is not intended to be uses directly. + Subclasses should implement a ``transform()`` method that calls ``draw()`` using + the transformed data and the optional target as input. + + Parameters + ---------- + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes. + will be used (or generated if required). + + features : list, default: None + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. + + classes : list, default: None + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. + + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. This parameter is only used in the + discrete target type case and is ignored otherwise. + + colors : list or tuple, default: None + A single color to plot all instances as or a list of colors to color each + instance according to its class in the discrete case or as an ordered + colormap in the sequential case. If not enough colors per class are + specified then the colors are treated as a cycle. + + colormap : string or cmap, default: None + The colormap used to create the individual colors. In the discrete case + it is used to compute the number of colors needed for each class and + in the continuous case it is used to create a sequential color map based + on the range of the target. + + target_type : str, default: "auto" + Specify the type of target as either "discrete" (classes) or "continuous" + (real numbers, usually for regression). If "auto", then it will + attempt to determine the type by counting the number of unique values. + + If the target is discrete, the colors are returned as a dict with classes + being the keys. If continuous the colors will be list having value of + color for each point. In either case, if no target is specified, then + color will be specified as the first color in the color cycle. + + projection : int or string, default: 2 + The number of axes to project into, either 2d or 3d. To plot 3d plots + with matplotlib, please ensure a 3d axes is passed to the visualizer, + otherwise one will be created using the current figure. + + alpha : float, default: 0.75 + Specify a transparency where 1 is completely opaque and 0 is completely + transparent. This property makes densely clustered points more visible. + + colorbar : bool, default: True + If the target_type is "continous" draw a colorbar to the right of the + scatter plot. The colobar axes is accessible using the cax property. + + kwargs : dict + Keyword arguments that are passed to the base class and may influence + the visualization as defined in other Visualizers. + """ + + def __init__( + self, + ax=None, + features=None, + classes=None, + colors=None, + colormap=None, + target_type="auto", + projection=2, + alpha=0.75, + colorbar=True, + **kwargs + ): + + super(ProjectionVisualizer, self).__init__( + ax=ax, + features=features, + classes=classes, + colors=colors, + colormap=colormap, + target_type=target_type, + **kwargs + ) + + # Convert string to integer + if isinstance(projection, str): + if projection in {"2D", "2d"}: + projection = 2 + if projection in {"3D", "3d"}: + projection = 3 + if projection not in {2, 3}: + raise YellowbrickValueError("Projection dimensions must be either 2 or 3") + self.projection = projection + + if self.ax.name != "3d" and self.projection == 3: + warnings.warn( + "data projection to 3 dimensions requires a 3d axes to draw on.", + YellowbrickWarning, + ) + + self.alpha = alpha + self.colorbar = colorbar + self._cax = None + + @property + def cax(self): + """ + The axes of the colorbar, right of the scatterplot. + """ + if self._cax is None: + raise AttributeError("This visualizer does not have an axes for colorbar") + + return self._cax + + @property + def ax(self): + """ + Overloads the axes property from base class. If no axes is specified then + creates an axes for users. A 3d axes is created for 3 dimensional plots. + """ + if not hasattr(self, "_ax") or self._ax is None: + if self.projection == 3: + fig = plt.gcf() + self._ax = fig.add_subplot(111, projection="3d") + else: + self._ax = plt.gca() + return self._ax + + @ax.setter + def ax(self, ax): + self._ax = ax + + def layout(self, divider=None): + """ + Creates the layout for colorbar when target type is continuous. + The colorbar is added to the right of the scatterplot. + + Subclasses can override this method to add other axes or layouts. + + Parameters + ---------- + divider: AxesDivider + An AxesDivider to be passed among all layout calls. + """ + if ( + self._target_color_type == TargetType.CONTINUOUS + and self.projection == 2 + and self.colorbar + and self._cax is None + ): + # Ensure matplotlib version compatibility + if make_axes_locatable is None: + raise YellowbrickValueError( + ( + "Colorbar requires matplotlib 2.0.2 or greater " + "please upgrade matplotlib" + ) + ) + + # Create the new axes for the colorbar + if divider is None: + divider = make_axes_locatable(self.ax) + + self._cax = divider.append_axes("right", size="5%", pad=0.3) + self._cax.set_yticks([]) + self._cax.set_xticks([]) + + def fit_transform(self, X, y=None): + """ + Fits the visualizer on the input data, and returns transformed X. + + Parameters + ---------- + X : ndarray or DataFrame of shape n x m + A matrix or data frame of n instances with m features where m>2. + + y : array-like of shape (n,), optional + A vector or series with target values for each instance in X. This + vector is used to determine the color of the points in X. + + Returns + ------- + Xprime : array-like of shape (n, 2) + Returns the 2-dimensional embedding of the instances. + """ + return self.fit(X, y).transform(X, y) + + def draw(self, Xp, y=None): + """ + Draws the points described by Xp and colored by the points in y. Can be + called multiple times before finalize to add more scatter plots to the + axes, however ``fit()`` must be called before use. + + Parameters + ---------- + Xp : array-like of shape (n, 2) or (n, 3) + The matrix produced by the ``transform()`` method. + + y : array-like of shape (n,), optional + The target, used to specify the colors of the points. + + Returns + ------- + self.ax : matplotlib Axes object + Returns the axes that the scatter plot was drawn on. + """ + scatter_kwargs = self._determine_scatter_kwargs(y) + + # Draws the layout of the visualizer. It draws the axes for colorbars, + # heatmap, etc. + self.layout() + + if self.projection == 2: + # Adds colorbar axis for continuous target type. + self.ax.scatter(Xp[:, 0], Xp[:, 1], **scatter_kwargs) + + if self.projection == 3: + self.ax.scatter(Xp[:, 0], Xp[:, 1], Xp[:, 2], **scatter_kwargs) + + return self.ax + + def finalize(self): + """ + Draws legends and colorbar for scatter plots. + """ + self.ax.set_xticklabels([]) + self.ax.set_yticklabels([]) + if self.projection == 3: + self.ax.set_zticklabels([]) + + if self._target_color_type == TargetType.DISCRETE: + # Add the legend + manual_legend( + self, self.classes_, list(self._colors.values()), frameon=True + ) + elif self._target_color_type == TargetType.CONTINUOUS: + if self.colorbar: + if self.projection == 3: + sm = plt.cm.ScalarMappable(cmap=self._colors, norm=self._norm) + self.cbar = plt.colorbar(sm, ax=self.ax) + + else: + # Manually draw the colorbar. + self.cbar = mpl.colorbar.ColorbarBase( + self.cax, cmap=self._colors, norm=self._norm + ) + + def _determine_scatter_kwargs(self, y=None): + """ + Determines scatter argumnets to pass into ``plt.scatter()``. If y is + discrete or single then determine colors. If continuous then determine + colors and colormap.Also normalize to range + + Parameters + ---------- + y : array-like of shape (n,), optional + The target, used to specify the colors of the points for continuous + target. + """ + + scatter_kwargs = {"alpha": self.alpha} + # Determine the colors + if self._target_color_type == TargetType.SINGLE: + scatter_kwargs["c"] = self._colors + + elif self._target_color_type == TargetType.DISCRETE: + if y is None: + raise YellowbrickValueError("y is required for discrete target") + + try: + scatter_kwargs["c"] = [self._colors[self.classes_[yi]] for yi in y] + except IndexError: + raise YellowbrickValueError("Target needs to be label encoded.") + + elif self._target_color_type == TargetType.CONTINUOUS: + if y is None: + raise YellowbrickValueError("y is required for continuous target") + + scatter_kwargs["c"] = y + scatter_kwargs["cmap"] = self._colors + self._norm = mpl.colors.Normalize(vmin=self.range_[0], vmax=self.range_[1]) + + else: + # Technically this should never be raised + raise NotFitted("could not determine target color type") + return scatter_kwargs diff --git a/yellowbrick/features/radviz.py b/yellowbrick/features/radviz.py index 714beed63..ddb871a47 100644 --- a/yellowbrick/features/radviz.py +++ b/yellowbrick/features/radviz.py @@ -1,10 +1,10 @@ # yellowbrick.features.radviz # Implements radviz for feature analysis. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Oct 07 13:18:00 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: radviz.py [0f4b236] benjamin@bengfort.com $ @@ -24,15 +24,24 @@ from yellowbrick.utils import is_dataframe from yellowbrick.utils import nan_warnings from yellowbrick.features.base import DataVisualizer -from yellowbrick.style.colors import resolve_colors ########################################################################## ## Quick Methods ########################################################################## -def radviz(X, y=None, ax=None, features=None, classes=None, - color=None, colormap=None, alpha=1.0, **kwargs): + +def radviz( + X, + y=None, + ax=None, + features=None, + classes=None, + colors=None, + colormap=None, + alpha=1.0, + **kwargs +): """ Displays each feature as an axis around a circle surrounding a scatter plot whose points are each individual instance. @@ -58,7 +67,7 @@ def radviz(X, y=None, ax=None, features=None, classes=None, classes : list of strings, default: None The names of the classes in the target - color : list or tuple of colors, default: None + colors : list or tuple of colors, default: None Specify the colors for each individual class colormap : string or matplotlib cmap, default: None @@ -70,26 +79,33 @@ def radviz(X, y=None, ax=None, features=None, classes=None, Returns ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. + viz : RadViz + Returns the fitted, finalized visualizer """ # Instantiate the visualizer visualizer = RadialVisualizer( - ax, features, classes, color, colormap, alpha, **kwargs + ax=ax, + features=features, + classes=classes, + colors=colors, + colormap=colormap, + alpha=alpha, + **kwargs ) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y, **kwargs) visualizer.transform(X) - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer object + return visualizer ########################################################################## ## Static RadViz Visualizer ########################################################################## + class RadialVisualizer(DataVisualizer): """ RadViz is a multivariate data visualization algorithm that plots each @@ -99,30 +115,37 @@ class RadialVisualizer(DataVisualizer): Parameters ---------- - ax : matplotlib Axes, default: None The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). features : list, default: None a list of feature names to use - If a DataFrame is passed to fit and features is None, feature - names are selected as the columns of the DataFrame. + The names of the features specified by the columns of the input dataset. + This length of this list must match the number of columns in X, otherwise + an exception will be raised on ``fit()``. classes : list, default: None a list of class names for the legend - If classes is None and a y value is passed to fit then the classes - are selected from the target vector. + The class labels for each class in y, ordered by sorted class index. These + names act as a label encoder for the legend, identifying integer classes + or renaming string labels. If omitted, the class labels will be taken from + the unique values in y. - color : list or tuple, default: None + Note that the length of this list must match the number of unique values in + y, otherwise an exception is raised. This parameter is only used in the + discrete target type case and is ignored otherwise. + + colors : list or tuple, default: None optional list or tuple of colors to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. + A single color to plot all instances as or a list of colors to color each + instance according to its class. If not enough colors per class are + specified then the colors are treated as a cycle. colormap : string or cmap, default: None optional string or matplotlib cmap to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. + The colormap used to create the individual colors. If classes are + specified the colormap is used to evenly space colors across each class. alpha : float, default: 1.0 Specify a transparency where 1 is completely opaque and 0 is completely @@ -140,16 +163,39 @@ class RadialVisualizer(DataVisualizer): >>> visualizer.transform(X) >>> visualizer.poof() - Notes - ----- - These parameters can be influenced later on in the visualization - process, but can and should be set as early as possible. + Attributes + ---------- + features_ : ndarray, shape (n_features,) + The names of the features discovered or used in the visualizer that + can be used as an index to access or modify data in X. If a user passes + feature names in, those features are used. Otherwise the columns of a + DataFrame are used or just simply the indices of the data array. + + classes_ : ndarray, shape (n_classes,) + The class labels that define the discrete values in the target. Only + available if the target type is discrete. This is guaranteed to be + strings even if the classes are a different type. """ - def __init__(self, ax=None, features=None, classes=None, color=None, - colormap=None, alpha=1.0, **kwargs): + def __init__( + self, + ax=None, + features=None, + classes=None, + colors=None, + colormap=None, + alpha=1.0, + **kwargs + ): + if "target_type" not in kwargs: + kwargs["target_type"] = "discrete" super(RadialVisualizer, self).__init__( - ax, features, classes, color, colormap, **kwargs + ax=ax, + features=features, + classes=classes, + colors=colors, + colormap=colormap, + **kwargs ) self.alpha = alpha @@ -162,6 +208,32 @@ def normalize(X): b = X.max(axis=0) return (X - a[np.newaxis, :]) / ((b - a)[np.newaxis, :]) + def fit(self, X, y=None, **kwargs): + """ + The fit method is the primary drawing input for the + visualization since it has both the X and y data required for the + viz and the transform method does not. + + Parameters + ---------- + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features + + y : ndarray or Series of length n + An array or series of target or class values + + kwargs : dict + Pass generic arguments to the drawing method + + Returns + ------- + self : instance + Returns the instance of the transformer/visualizer + """ + super(RadialVisualizer, self).fit(X, y) + self.draw(X, y, **kwargs) + return self + def draw(self, X, y, **kwargs): """ Called from the fit method, this method creates the radviz canvas and @@ -180,73 +252,102 @@ def draw(self, X, y, **kwargs): nrows, ncols = X.shape # Set the axes limits - self.ax.set_xlim([-1,1]) - self.ax.set_ylim([-1,1]) - - # Create the colors - # TODO: Allow both colormap, listed colors, and palette definition - # TODO: Make this an independent function or property for override! - color_values = resolve_colors( - n_colors=len(self.classes_), colormap=self.colormap, colors=self.color - ) - self._colors = dict(zip(self.classes_, color_values)) + self.ax.set_xlim([-1, 1]) + self.ax.set_ylim([-1, 1]) # Create a data structure to hold scatter plot representations - to_plot = {} - for kls in self.classes_: - to_plot[kls] = [[], []] + to_plot = {label: [[], []] for label in self.classes_} # Compute the arcs around the circumference for each feature axis # TODO: make this an independent function for override - s = np.array([ + s = np.array( + [ (np.cos(t), np.sin(t)) - for t in [ - 2.0 * np.pi * (i / float(ncols)) - for i in range(ncols) - ] - ]) + for t in [2.0 * np.pi * (i / float(ncols)) for i in range(ncols)] + ] + ) # Compute the locations of the scatter plot for each class # Normalize the data first to plot along the 0, 1 axis for i, row in enumerate(self.normalize(X)): row_ = np.repeat(np.expand_dims(row, axis=1), 2, axis=1) - xy = (s * row_).sum(axis=0) / row.sum() - kls = self.classes_[y[i]] + xy = (s * row_).sum(axis=0) / row.sum() + label = self._label_encoder[y[i]] - to_plot[kls][0].append(xy[0]) - to_plot[kls][1].append(xy[1]) + to_plot[label][0].append(xy[0]) + to_plot[label][1].append(xy[1]) # Add the scatter plots from the to_plot function # TODO: store these plots to add more instances to later # TODO: make this a separate function - for i, kls in enumerate(self.classes_): + for label in self.classes_: + color = self.get_colors([label])[0] self.ax.scatter( - to_plot[kls][0], to_plot[kls][1], color=self._colors[kls], - label=str(kls), alpha=self.alpha, **kwargs + to_plot[label][0], + to_plot[label][1], + color=color, + label=label, + alpha=self.alpha, + **kwargs ) # Add the circular axis path # TODO: Make this a seperate function (along with labeling) - self.ax.add_patch(patches.Circle( - (0.0, 0.0), radius=1.0, facecolor='none', edgecolor='grey', linewidth=.5 - )) + self.ax.add_patch( + patches.Circle( + (0.0, 0.0), + radius=1.0, + facecolor="none", + edgecolor="grey", + linewidth=0.5, + ) + ) # Add the feature names for xy, name in zip(s, self.features_): # Add the patch indicating the location of the axis - self.ax.add_patch(patches.Circle(xy, radius=0.025, facecolor='#777777')) + self.ax.add_patch(patches.Circle(xy, radius=0.025, facecolor="#777777")) # Add the feature names offset around the axis marker if xy[0] < 0.0 and xy[1] < 0.0: - self.ax.text(xy[0] - 0.025, xy[1] - 0.025, name, ha='right', va='top', size='small') + self.ax.text( + xy[0] - 0.025, + xy[1] - 0.025, + name, + ha="right", + va="top", + size="small", + ) elif xy[0] < 0.0 and xy[1] >= 0.0: - self.ax.text(xy[0] - 0.025, xy[1] + 0.025, name, ha='right', va='bottom', size='small') + self.ax.text( + xy[0] - 0.025, + xy[1] + 0.025, + name, + ha="right", + va="bottom", + size="small", + ) elif xy[0] >= 0.0 and xy[1] < 0.0: - self.ax.text(xy[0] + 0.025, xy[1] - 0.025, name, ha='left', va='top', size='small') + self.ax.text( + xy[0] + 0.025, + xy[1] - 0.025, + name, + ha="left", + va="top", + size="small", + ) elif xy[0] >= 0.0 and xy[1] >= 0.0: - self.ax.text(xy[0] + 0.025, xy[1] + 0.025, name, ha='left', va='bottom', size='small') - - self.ax.axis('equal') + self.ax.text( + xy[0] + 0.025, + xy[1] + 0.025, + name, + ha="left", + va="bottom", + size="small", + ) + + self.ax.axis("equal") + return self.ax def finalize(self, **kwargs): """ @@ -259,17 +360,15 @@ def finalize(self, **kwargs): """ # Set the title - self.set_title( - 'RadViz for {} Features'.format(len(self.features_)) - ) + self.set_title("RadViz for {} Features".format(len(self.features_))) # Remove the ticks from the graph self.ax.set_yticks([]) self.ax.set_xticks([]) # Add the legend - colors = [self._colors[c] for c in self.classes_] - manual_legend(self, self.classes_, colors, loc='best') + colors = self.get_colors(self.classes_) + manual_legend(self, self.classes_, colors, loc="best") # Alias for RadViz diff --git a/yellowbrick/features/rankd.py b/yellowbrick/features/rankd.py index b6ef1b2f1..229135632 100644 --- a/yellowbrick/features/rankd.py +++ b/yellowbrick/features/rankd.py @@ -1,10 +1,10 @@ # yellowbrick.features.rankd # Implements 1D (histograms) and 2D (joint plot) feature rankings. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Oct 07 15:14:01 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: rankd.py [ee754dc] benjamin@bengfort.com $ @@ -17,130 +17,50 @@ ## Imports ########################################################################## +import warnings import numpy as np +import matplotlib as mpl + from scipy.stats import shapiro from scipy.stats import spearmanr +from scipy.stats import kendalltau as sp_kendalltau from yellowbrick.utils import is_dataframe from yellowbrick.features.base import MultiFeatureVisualizer -from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning __all__ = ["rank1d", "rank2d", "Rank1D", "Rank2D"] ########################################################################## -## Quick Methods +## Metrics ########################################################################## -def rank1d(X, y=None, ax=None, algorithm='shapiro', features=None, - orient='h', show_feature_names=True, **kwargs): - """Scores each feature with the algorithm and ranks them in a bar plot. - - This helper function is a quick wrapper to utilize the Rank1D Visualizer - (Transformer) for one-off analysis. - - Parameters - ---------- - X : ndarray or DataFrame of shape n x m - A matrix of n instances with m features - - y : ndarray or Series of length n - An array or series of target or class values - - ax : matplotlib axes - the axis to plot the figure on. - - algorithm : one of {'shapiro', }, default: 'shapiro' - The ranking algorithm to use, default is 'Shapiro-Wilk. - - features : list - A list of feature names to use. - If a DataFrame is passed to fit and features is None, feature - names are selected as the columns of the DataFrame. - - orient : 'h' or 'v' - Specifies a horizontal or vertical bar chart. - - show_feature_names : boolean, default: True - If True, the feature names are used to label the axis ticks in the - plot. - - Returns - ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. +def kendalltau(X): """ - # Instantiate the visualizer - visualizer = Rank1D(ax, algorithm, features, orient, show_feature_names, - **kwargs) - - # Fit and transform the visualizer (calls draw) - visualizer.fit(X, y, **kwargs) - visualizer.transform(X) - - # Return the axes object on the visualizer - return visualizer.ax - -def rank2d(X, y=None, ax=None, algorithm='pearson', features=None, - show_feature_names=True, colormap='RdBu_r', **kwargs): - """Displays pairwise comparisons of features with the algorithm and ranks - them in a lower-left triangle heatmap plot. - - This helper function is a quick wrapper to utilize the Rank2D Visualizer - (Transformer) for one-off analysis. + Accepts a matrix X and returns a correlation matrix so that each column + is the variable and each row is the observations. Parameters ---------- X : ndarray or DataFrame of shape n x m A matrix of n instances with m features - y : ndarray or Series of length n - An array or series of target or class values - - ax : matplotlib axes - the axis to plot the figure on. - - algorithm : one of {pearson, covariance, spearman} - the ranking algorithm to use, default is Pearson correlation. - - features : list - A list of feature names to use. - If a DataFrame is passed to fit and features is None, feature - names are selected as the columns of the DataFrame. - - show_feature_names : boolean, default: True - If True, the feature names are used to label the axis ticks in the - plot. - - colormap : string or cmap - optional string or matplotlib cmap to colorize lines - Use either color to colorize the lines on a per class basis or - colormap to color them on a continuous scale. - - Returns - ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. - """ - # Instantiate the visualizer - visualizer = Rank2D(ax, algorithm, features, colormap, show_feature_names, - **kwargs) - - # Fit and transform the visualizer (calls draw) - visualizer.fit(X, y, **kwargs) - visualizer.transform(X) - - # Return the axes object on the visualizer - return visualizer.ax + corrs = np.zeros((X.shape[1], X.shape[1])) + for idx, cola in enumerate(X.T): + for jdx, colb in enumerate(X.T): + corrs[idx, jdx] = sp_kendalltau(cola, colb)[0] + return corrs ########################################################################## ## Base Feature Visualizer ########################################################################## + class RankDBase(MultiFeatureVisualizer): """ Base visualizer for Rank1D and Rank2D @@ -151,6 +71,10 @@ class RankDBase(MultiFeatureVisualizer): The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + algorithm : string The ranking algorithm to use; options and defaults vary by subclass @@ -190,13 +114,20 @@ class RankDBase(MultiFeatureVisualizer): ranking_methods = {} - def __init__(self, ax=None, algorithm=None, features=None, - show_feature_names=True, **kwargs): + def __init__( + self, + ax=None, + fig=None, + algorithm=None, + features=None, + show_feature_names=True, + **kwargs + ): """ Initialize the class with the options required to rank and order features as well as visualize the result. """ - super(RankDBase, self).__init__(ax=ax, features=features, **kwargs) + super(RankDBase, self).__init__(ax=ax, fig=fig, features=features, **kwargs) # Data Parameters self.ranking_ = algorithm @@ -259,7 +190,7 @@ def rank(self, X, algorithm=None): # Extract matrix from dataframe if necessary if is_dataframe(X): - X = X.as_matrix() + X = X.values return self.ranking_methods[algorithm](X) @@ -274,7 +205,16 @@ def finalize(self, **kwargs): generic keyword arguments """ - # Set the title + # There is a known bug in matplotlib 3.1.1 that affects RankD plots + # See #912 and #914 for details. + if mpl.__version__ == "3.1.1": + msg = ( + "RankD plots may be clipped when using matplotlib v3.1.1, " + "upgrade to matplotlib v3.1.2 or later to fix the plots." + ) + warnings.warn(msg, YellowbrickWarning) + + # Set the title for all RankD visualizations. self.set_title( "{} Ranking of {} Features".format( self.ranking_.title(), len(self.features_) @@ -286,6 +226,7 @@ def finalize(self, **kwargs): ## Rank 1D Feature Visualizer ########################################################################## + class Rank1D(RankDBase): """ Rank1D computes a score for each feature in the data set with a specific @@ -306,13 +247,16 @@ class Rank1D(RankDBase): If a DataFrame is passed to fit and features is None, feature names are selected as the columns of the DataFrame. - orient : 'h' or 'v' + orient : 'h' or 'v', default='h' Specifies a horizontal or vertical bar chart. show_feature_names : boolean, default: True If True, the feature names are used to label the x and y ticks in the plot. + color: string + Specify color for barchart + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. @@ -331,29 +275,39 @@ class Rank1D(RankDBase): >>> visualizer.poof() """ - ranking_methods = { - 'shapiro': lambda X: np.array([shapiro(x)[0] for x in X.T]), - } - - def __init__(self, ax=None, algorithm='shapiro', features=None, - orient='h', show_feature_names=True, **kwargs): + ranking_methods = {"shapiro": lambda X: np.array([shapiro(x)[0] for x in X.T])} + + def __init__( + self, + ax=None, + algorithm="shapiro", + features=None, + orient="h", + show_feature_names=True, + color=None, + **kwargs + ): """ Initialize the class with the options required to rank and order features as well as visualize the result. """ super(Rank1D, self).__init__( - ax=ax, algorithm=algorithm, features=features, - show_feature_names=show_feature_names, **kwargs + ax=ax, + algorithm=algorithm, + features=features, + show_feature_names=show_feature_names, + **kwargs ) + self.color = color self.orientation_ = orient def draw(self, **kwargs): """ Draws the bar plot of the ranking array of features. """ - if self.orientation_ == 'h': + if self.orientation_ == "h": # Make the plot - self.ax.barh(np.arange(len(self.ranks_)), self.ranks_, color='b') + self.ax.barh(np.arange(len(self.ranks_)), self.ranks_, color=self.color) # Add ticks and tick labels self.ax.set_yticks(np.arange(len(self.ranks_))) @@ -368,9 +322,9 @@ def draw(self, **kwargs): # Turn off y grid lines self.ax.yaxis.grid(False) - elif self.orientation_ == 'v': + elif self.orientation_ == "v": # Make the plot - self.ax.bar(np.arange(len(self.ranks_)), self.ranks_, color='b') + self.ax.bar(np.arange(len(self.ranks_)), self.ranks_, color=self.color) # Add ticks and tick labels self.ax.set_xticks(np.arange(len(self.ranks_))) @@ -383,15 +337,14 @@ def draw(self, **kwargs): self.ax.xaxis.grid(False) else: - raise YellowbrickValueError( - "Orientation must be 'h' or 'v'" - ) + raise YellowbrickValueError("Orientation must be 'h' or 'v'") ########################################################################## ## Rank 2D Feature Visualizer ########################################################################## + class Rank2D(RankDBase): """ Rank2D performs pairwise comparisons of each feature in the data set with @@ -404,8 +357,9 @@ class Rank2D(RankDBase): The axis to plot the figure on. If None is passed in the current axes will be used (or generated if required). - algorithm : one of {'pearson', 'covariance', 'spearman'}, default: 'pearson' - The ranking algorithm to use, default is Pearson correlation. + algorithm : str, default: 'pearson' + The ranking algorithm to use, one of: + 'pearson', 'covariance', 'spearman', 'kendalltau'. features : list A list of feature names to use. @@ -446,22 +400,33 @@ class Rank2D(RankDBase): """ ranking_methods = { - 'pearson': lambda X: np.corrcoef(X.transpose()), - 'covariance': lambda X: np.cov(X.transpose()), - 'spearman': lambda X: spearmanr(X)[0], + "pearson": lambda X: np.corrcoef(X.transpose()), + "covariance": lambda X: np.cov(X.transpose()), + "spearman": lambda X: spearmanr(X, axis=0)[0], + "kendalltau": lambda X: kendalltau(X), } - def __init__(self, ax=None, algorithm='pearson', features=None, - colormap='RdBu_r', show_feature_names=True, **kwargs): + def __init__( + self, + ax=None, + algorithm="pearson", + features=None, + colormap="RdBu_r", + show_feature_names=True, + **kwargs + ): """ Initialize the class with the options required to rank and order features as well as visualize the result. """ super(Rank2D, self).__init__( - ax=ax, algorithm=algorithm, features=features, - show_feature_names=show_feature_names, **kwargs + ax=ax, + algorithm=algorithm, + features=features, + show_feature_names=show_feature_names, + **kwargs ) - self.colormap=colormap + self.colormap = colormap def draw(self, **kwargs): """ @@ -480,9 +445,7 @@ def draw(self, **kwargs): mesh = self.ax.pcolormesh(data, cmap=self.colormap, vmin=-1, vmax=1) # Set the Axis limits - self.ax.set( - xlim=(0, data.shape[1]), ylim=(0, data.shape[0]) - ) + self.ax.set(xlim=(0, data.shape[1]), ylim=(0, data.shape[0])) # Add the colorbar cb = self.ax.figure.colorbar(mesh, None, self.ax) @@ -500,3 +463,146 @@ def draw(self, **kwargs): else: self.ax.set_xticklabels([]) self.ax.set_yticklabels([]) + + +########################################################################## +## Quick Methods +########################################################################## + + +def rank1d( + X, + y=None, + ax=None, + algorithm="shapiro", + features=None, + orient="h", + show_feature_names=True, + color=None, + **kwargs +): + """Scores each feature with the algorithm and ranks them in a bar plot. + + This helper function is a quick wrapper to utilize the Rank1D Visualizer + (Transformer) for one-off analysis. + + Parameters + ---------- + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features + + y : ndarray or Series of length n + An array or series of target or class values + + ax : matplotlib axes + the axis to plot the figure on. + + algorithm : one of {'shapiro', }, default: 'shapiro' + The ranking algorithm to use, default is 'Shapiro-Wilk. + + features : list + A list of feature names to use. + If a DataFrame is passed to fit and features is None, feature + names are selected as the columns of the DataFrame. + + orient : 'h' or 'v' + Specifies a horizontal or vertical bar chart. + + show_feature_names : boolean, default: True + If True, the feature names are used to label the axis ticks in the + plot. + + color: string + Specify color for barchart + + Returns + ------- + viz : Rank1D + Returns the fitted, finalized visualizer. + + """ + # Instantiate the visualizer + visualizer = Rank1D( + ax=ax, + algorithm=algorithm, + features=features, + orient=orient, + show_feature_names=show_feature_names, + color=color, + **kwargs + ) + + # Fit and transform the visualizer (calls draw) + visualizer.fit(X, y, **kwargs) + visualizer.transform(X) + + # Return the visualizer object + return visualizer + + +def rank2d( + X, + y=None, + ax=None, + algorithm="pearson", + features=None, + show_feature_names=True, + colormap="RdBu_r", + **kwargs +): + """Displays pairwise comparisons of features with the algorithm and ranks + them in a lower-left triangle heatmap plot. + + This helper function is a quick wrapper to utilize the Rank2D Visualizer + (Transformer) for one-off analysis. + + Parameters + ---------- + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features + + y : ndarray or Series of length n + An array or series of target or class values + + ax : matplotlib axes + the axis to plot the figure on. + + algorithm : one of {pearson, covariance, spearman, kendalltau} + the ranking algorithm to use, default is Pearson correlation. + + features : list + A list of feature names to use. + If a DataFrame is passed to fit and features is None, feature + names are selected as the columns of the DataFrame. + + show_feature_names : boolean, default: True + If True, the feature names are used to label the axis ticks in the + plot. + + colormap : string or cmap + optional string or matplotlib cmap to colorize lines + Use either color to colorize the lines on a per class basis or + colormap to color them on a continuous scale. + + Returns + ------- + viz : Rank2D + Returns the fitted, finalized visualizer + + """ + # Instantiate the visualizer + visualizer = Rank2D( + ax=ax, + algorithm=algorithm, + features=features, + colormap=colormap, + show_feature_names=show_feature_names, + **kwargs + ) + + # Fit and transform the visualizer (calls draw) + visualizer.fit(X, y, **kwargs) + visualizer.transform(X) + + # Return the visualizer object + return visualizer diff --git a/yellowbrick/gridsearch/__init__.py b/yellowbrick/gridsearch/__init__.py index 76b9e73cc..c9e9237fa 100644 --- a/yellowbrick/gridsearch/__init__.py +++ b/yellowbrick/gridsearch/__init__.py @@ -1,3 +1,14 @@ +# yellowbrick.gridsearch +# Visualizers for the results of GridSearchCV. +# +# Author: Phillip Schafer +# Created: Sat Feb 3 10:18:33 2018 -0500 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: __init__.py [03724ed] pbs929@users.noreply.github.com $ + """ Visualizers for the results of GridSearchCV. """ diff --git a/yellowbrick/gridsearch/base.py b/yellowbrick/gridsearch/base.py index 7514b6ea5..c6abce57b 100644 --- a/yellowbrick/gridsearch/base.py +++ b/yellowbrick/gridsearch/base.py @@ -1,3 +1,14 @@ +# yellowbrick.gridsearch.base +# Base class for grid search visualizers +# +# Author: Phillip Schafer +# Created: Sat Feb 3 10:18:33 2018 -0500 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: base.py [03724ed] pbs929@users.noreply.github.com $ + """ Base class for grid search visualizers """ @@ -9,16 +20,19 @@ import numpy as np from ..utils import is_gridsearch from ..base import ModelVisualizer -from ..exceptions import (YellowbrickTypeError, - YellowbrickKeyError, - YellowbrickValueError) +from ..exceptions import ( + YellowbrickTypeError, + YellowbrickKeyError, + YellowbrickValueError, +) ########################################################################## ## Dimension reduction utility ########################################################################## -def param_projection(cv_results, x_param, y_param, metric='mean_test_score'): + +def param_projection(cv_results, x_param, y_param, metric="mean_test_score"): """ Projects the grid search results onto 2 dimensions. @@ -55,19 +69,24 @@ def param_projection(cv_results, x_param, y_param, metric='mean_test_score'): # These are masked arrays where the cases where each parameter is # non-applicable are masked. try: - x_vals = cv_results['param_' + x_param] + x_vals = cv_results["param_" + x_param] except KeyError: - raise YellowbrickKeyError("Parameter '{}' does not exist in the grid " - "search results".format(x_param)) + raise YellowbrickKeyError( + "Parameter '{}' does not exist in the grid " + "search results".format(x_param) + ) try: - y_vals = cv_results['param_' + y_param] + y_vals = cv_results["param_" + y_param] except KeyError: - raise YellowbrickKeyError("Parameter '{}' does not exist in the grid " - "search results".format(y_param)) + raise YellowbrickKeyError( + "Parameter '{}' does not exist in the grid " + "search results".format(y_param) + ) if metric not in cv_results: - raise YellowbrickKeyError("Metric '{}' does not exist in the grid " - "search results".format(metric)) + raise YellowbrickKeyError( + "Metric '{}' does not exist in the grid " "search results".format(metric) + ) # Get unique, unmasked values of the two display parameters unique_x_vals = sorted(list(set(x_vals.compressed()))) @@ -115,8 +134,8 @@ def param_projection(cv_results, x_param, y_param, metric='mean_test_score'): ## Base Grid Search Visualizer ########################################################################## -class GridSearchVisualizer(ModelVisualizer): +class GridSearchVisualizer(ModelVisualizer): def __init__(self, model, ax=None, **kwargs): """ Check to see if model is an instance of GridSearchCV. @@ -124,9 +143,7 @@ def __init__(self, model, ax=None, **kwargs): """ # A bit of type checking if not is_gridsearch(model): - raise YellowbrickTypeError( - "This estimator is not a GridSearchCV instance" - ) + raise YellowbrickTypeError("This estimator is not a GridSearchCV instance") # Initialize the super method. super(GridSearchVisualizer, self).__init__(model, ax=ax, **kwargs) diff --git a/yellowbrick/gridsearch/pcolor.py b/yellowbrick/gridsearch/pcolor.py index 6585403d1..39efe7177 100644 --- a/yellowbrick/gridsearch/pcolor.py +++ b/yellowbrick/gridsearch/pcolor.py @@ -1,3 +1,14 @@ +# yellowbrick.gridsearch.pcolor +# Colorplot visualizer for gridsearch results. +# +# Author: Phillip Schafer +# Created: Sat Feb 3 10:18:33 2018 -0500 +# +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: pcolor.py [03724ed] pbs929@users.noreply.github.com $ + """ Colorplot visualizer for gridsearch results. """ @@ -8,18 +19,15 @@ ## Packages for export -__all__ = [ - "GridSearchColorPlot", - "gridsearch_color_plot" -] +__all__ = ["GridSearchColorPlot", "gridsearch_color_plot"] ########################################################################## ## Quick method ########################################################################## -def gridsearch_color_plot(model, x_param, y_param, X=None, y=None, ax=None, - **kwargs): + +def gridsearch_color_plot(model, x_param, y_param, X=None, y=None, ax=None, **kwargs): """Quick method: Create a color plot showing the best grid search scores across two parameters. @@ -120,8 +128,16 @@ class GridSearchColorPlot(GridSearchVisualizer): >>> model.poof() """ - def __init__(self, model, x_param, y_param, metric='mean_test_score', - colormap='RdBu_r', ax=None, **kwargs): + def __init__( + self, + model, + x_param, + y_param, + metric="mean_test_score", + colormap="RdBu_r", + ax=None, + **kwargs + ): super(GridSearchColorPlot, self).__init__(model, ax=ax, **kwargs) self.x_param = x_param self.y_param = y_param @@ -138,9 +154,10 @@ def draw(self): data = np.ma.masked_invalid(best_scores) # Plot and fill in hatch for nans - mesh = self.ax.pcolor(data, cmap=self.colormap, - vmin=np.nanmin(data), vmax=np.nanmax(data)) - self.ax.patch.set(hatch='x', edgecolor='black') + mesh = self.ax.pcolor( + data, cmap=self.colormap, vmin=np.nanmin(data), vmax=np.nanmax(data) + ) + self.ax.patch.set(hatch="x", edgecolor="black") # Ticks and tick labels self.ax.set_xticks(np.arange(len(x_vals)) + 0.5) diff --git a/yellowbrick/model_selection/__init__.py b/yellowbrick/model_selection/__init__.py index 78c9c245a..06892aa4f 100644 --- a/yellowbrick/model_selection/__init__.py +++ b/yellowbrick/model_selection/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Fri Mar 30 10:36:12 2018 -0400 # -# ID: __init__.py [] benjamin@bengfort.com $ +# ID: __init__.py [c5355ee] benjamin@bengfort.com $ """ Visualizers that wrap the model selection libraries of Scikit-Learn @@ -17,3 +17,7 @@ from .learning_curve import LearningCurve, learning_curve from .validation_curve import ValidationCurve, validation_curve from .cross_validation import CVScores, cv_scores + +# RFECV and Feature Importances moved here as of YB v1.0 +from .importances import FeatureImportances, feature_importances +from .rfecv import RFECV, rfecv diff --git a/yellowbrick/model_selection/cross_validation.py b/yellowbrick/model_selection/cross_validation.py index dca1b5c02..09d1b3a0d 100644 --- a/yellowbrick/model_selection/cross_validation.py +++ b/yellowbrick/model_selection/cross_validation.py @@ -3,9 +3,12 @@ # # Author: Prema Damodaran Roman # Created: Wed June 6 2018 13:32:00 -0500 -# Author: Rebecca Bilbro +# Author: Rebecca Bilbro # Updated: Fri Aug 10 13:15:43 2018 -0500 # +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: cross_validation.py [7f47800] pdamo24@gmail.com $ """ @@ -13,7 +16,7 @@ """ ########################################################################## -## Imports +# Imports ########################################################################## import numpy as np @@ -24,9 +27,10 @@ ########################################################################## -## CVScores Visualizer +# CVScores Visualizer ########################################################################## + class CVScores(ModelVisualizer): """ CVScores displays cross-validated scores as a bar chart, with the @@ -63,10 +67,22 @@ class CVScores(ModelVisualizer): See scikit-learn `cross-validation guide `_ for more information on the possible metrics that can be used. + color: string + Specify color for barchart + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ------- + cv_scores_ : ndarray shape (n_splits, ) + The cross-validated scores from each subsection of the data + + cv_scores_mean_ : float + Average cross-validated score across all subsections of the data + + Examples -------- @@ -91,10 +107,11 @@ class CVScores(ModelVisualizer): """ - def __init__(self, model, ax=None, cv=None, scoring=None, **kwargs): + def __init__(self, model, ax=None, cv=None, scoring=None, color=None, **kwargs): super(CVScores, self).__init__(model, ax=ax, **kwargs) self.cv = cv + self.color = color self.scoring = scoring def fit(self, X, y, **kwargs): @@ -134,16 +151,17 @@ def draw(self, **kwargs): average value of the scores. """ - color = kwargs.pop("color", "b") width = kwargs.pop("width", 0.3) linewidth = kwargs.pop("linewidth", 1) xvals = np.arange(1, len(self.cv_scores_) + 1, 1) - self.ax.bar(xvals, self.cv_scores_, width=width) + self.ax.bar(xvals, self.cv_scores_, width=width, color=self.color) self.ax.axhline( - self.cv_scores_mean_, color=color, + self.cv_scores_mean_, + color=self.color, label="Mean score = {:0.3f}".format(self.cv_scores_mean_), - linestyle='--', linewidth=linewidth + linestyle="--", + linewidth=linewidth, ) return self.ax @@ -154,7 +172,7 @@ def finalize(self, **kwargs): """ # Set the title of the figure - self.set_title('Cross Validation Scores for {}'.format(self.name)) + self.set_title("Cross Validation Scores for {}".format(self.name)) # Add the legend loc = kwargs.pop("loc", "best") @@ -165,15 +183,16 @@ def finalize(self, **kwargs): self.ax.xaxis.set_major_locator(ticker.MultipleLocator(1)) # Set the axis labels - self.ax.set_xlabel('Training Instances') - self.ax.set_ylabel('Score') + self.ax.set_xlabel("Training Instances") + self.ax.set_ylabel("Score") ########################################################################## -## Quick Method +# Quick Method ########################################################################## -def cv_scores(model, X, y, ax=None, cv=None, scoring=None, **kwargs): + +def cv_scores(model, X, y, ax=None, cv=None, scoring=None, color=None, **kwargs): """ Displays cross validation scores as a bar chart and the average of the scores as a horizontal line @@ -221,21 +240,25 @@ def cv_scores(model, X, y, ax=None, cv=None, scoring=None, **kwargs): See scikit-learn `cross-validation guide `_ for more information on the possible metrics that can be used. + color: string + Specify color for barchart + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. Returns ------- - ax : matplotlib.Axes - The axes object that the validation curves were drawn on. + visualizer : CVScores + The fitted visualizer. """ # Initialize the visualizer - visualizer = CVScores(model, ax=ax, cv=cv, scoring=scoring) + visualizer = CVScores(model, ax=ax, cv=cv, scoring=scoring, color=None) # Fit and poof the visualizer visualizer.fit(X, y) visualizer.poof(**kwargs) - return visualizer.ax + + return visualizer diff --git a/yellowbrick/features/importances.py b/yellowbrick/model_selection/importances.py similarity index 71% rename from yellowbrick/features/importances.py rename to yellowbrick/model_selection/importances.py index a82671748..f0b8908d0 100644 --- a/yellowbrick/features/importances.py +++ b/yellowbrick/model_selection/importances.py @@ -1,12 +1,11 @@ -# yellowbrick.features.importances +# yellowbrick.model_selection.importances # Feature importance visualizer # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Fri Mar 02 15:21:36 2018 -0500 -# Author: Rebecca Bilbro -# Updated: Sun Jun 24 10:53:36 2018 -0500 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # # ID: importances.py [] benjamin@bengfort.com $ @@ -21,19 +20,21 @@ ## Imports ########################################################################## +import warnings import numpy as np import matplotlib.pyplot as plt -from yellowbrick.utils import is_dataframe, is_classifier +from yellowbrick.draw import bar_stack from yellowbrick.base import ModelVisualizer -from yellowbrick.exceptions import YellowbrickTypeError, NotFitted -from ..style.palettes import color_palette - +from yellowbrick.style.colors import resolve_colors +from yellowbrick.utils import is_dataframe, is_classifier +from yellowbrick.exceptions import YellowbrickTypeError, NotFitted, YellowbrickWarning ########################################################################## ## Feature Visualizer ########################################################################## + class FeatureImportances(ModelVisualizer): """ Displays the most informative features in a model by showing a bar chart @@ -50,7 +51,9 @@ class FeatureImportances(ModelVisualizer): ---------- model : Estimator A Scikit-Learn estimator that learns feature importances. Must support - either ``coef_`` or ``feature_importances_`` parameters. + either ``coef_`` or ``feature_importances_`` parameters. If the estimator + is not fitted, it is fit when the visualizer is fitted, unless otherwise + specified by ``is_fitted``. ax : matplotlib Axes, default: None The axis to plot the figure on. If None is passed in the current axes @@ -67,7 +70,7 @@ class FeatureImportances(ModelVisualizer): absolute : bool, default: False Make all coeficients absolute to more easily compare negative - coeficients with positive ones. + coefficients with positive ones. xlabel : str, default: None The label for the X-axis. If None is automatically determined by the @@ -78,6 +81,18 @@ class FeatureImportances(ModelVisualizer): then a stacked bar plot is plotted; otherwise the mean of the feature importance across classes are plotted. + colors: list of strings + Specify colors for each bar in the chart if ``stack==False``. + + colormap : string or matplotlib cmap + Specify a colormap to color the classes if ``stack==True``. + + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. @@ -91,7 +106,7 @@ class FeatureImportances(ModelVisualizer): The numeric value of the feature importance computed by the model classes_ : np.array - The classees labeled. Is not None only for classifier. + The classes labeled. Is not None only for classifier. Examples -------- @@ -102,14 +117,34 @@ class FeatureImportances(ModelVisualizer): >>> visualizer.poof() """ - def __init__(self, model, ax=None, labels=None, relative=True, - absolute=False, xlabel=None, stack=False, **kwargs): - super(FeatureImportances, self).__init__(model, ax, **kwargs) + def __init__( + self, + model, + ax=None, + labels=None, + relative=True, + absolute=False, + xlabel=None, + stack=False, + colors=None, + colormap=None, + is_fitted="auto", + **kwargs + ): + # Initialize the visualizer bases + super(FeatureImportances, self).__init__( + model, ax=ax, is_fitted=is_fitted, **kwargs + ) # Data Parameters self.set_params( - labels=labels, relative=relative, absolute=absolute, - xlabel=xlabel, stack=stack + labels=labels, + relative=relative, + absolute=absolute, + xlabel=xlabel, + stack=stack, + colors=colors, + colormap=colormap, ) def fit(self, X, y=None, **kwargs): @@ -133,6 +168,7 @@ def fit(self, X, y=None, **kwargs): self : visualizer The fit method must always return self to support pipelines. """ + # Super call fits the underlying estimator if it's not already fitted super(FeatureImportances, self).fit(X, y, **kwargs) # Get the feature importances from the model @@ -150,8 +186,14 @@ def fit(self, X, y=None, **kwargs): # therefore we flatten by taking the average by # column to get shape (n_features,) (see LogisticRegression) if not self.stack and self.feature_importances_.ndim > 1: - self.feature_importances_ = np.mean(self.feature_importances_, - axis=0) + self.feature_importances_ = np.mean(self.feature_importances_, axis=0) + warnings.warn( + ( + "detected multi-dimensional feature importances but stack=False, " + "using mean to aggregate them." + ), + YellowbrickWarning, + ) # Apply absolute value filter before normalization if self.absolute: @@ -196,7 +238,7 @@ def draw(self, **kwargs): Draws the feature importances as a bar chart; called from fit. """ # Quick validation - for param in ('feature_importances_', 'features_'): + for param in ("feature_importances_", "features_"): if not hasattr(self, param): raise NotFitted("missing required param '{}'".format(param)) @@ -205,30 +247,26 @@ def draw(self, **kwargs): # Plot the bar chart if self.stack: - colors = color_palette(kwargs.pop('colors', None), - len(self.classes_)) - zeros = np.zeros(self.feature_importances_.shape[1]) - left_arr = np.zeros((self.feature_importances_.shape[1], 2)) - - for idx in range(len(self.feature_importances_)): - left = [ - left_arr[j, int(self.feature_importances_[idx][j] > 0)] - for j in range(len(self.feature_importances_[idx])) - ] - - self.ax.barh(pos, self.feature_importances_[idx], left=left, - color=colors[idx], label=self.classes_[idx]) - - left_arr[:, 0] += np.minimum(self.feature_importances_[idx], - zeros) - left_arr[:, 1] += np.maximum(self.feature_importances_[idx], - zeros) + colors = resolve_colors(len(self.classes_), colormap=self.colormap) + legend_kws = {"bbox_to_anchor": (1.04, 0.5), "loc": "center left"} + bar_stack( + self.feature_importances_, + ax=self.ax, + labels=list(self.classes_), + ticks=self.features_, + orientation="h", + colors=colors, + legend_kws=legend_kws, + ) else: - self.ax.barh(pos, self.feature_importances_, align='center') + colors = resolve_colors( + len(self.features_), colormap=self.colormap, colors=self.colors + ) + self.ax.barh(pos, self.feature_importances_, color=colors, align="center") - # Set the labels for the bars - self.ax.set_yticks(pos) - self.ax.set_yticklabels(self.features_) + # Set the labels for the bars + self.ax.set_yticks(pos) + self.ax.set_yticklabels(self.features_) return self.ax @@ -237,17 +275,18 @@ def finalize(self, **kwargs): Finalize the drawing setting labels and title. """ # Set the title - self.set_title('Feature Importances of {} Features using {}'.format( - len(self.features_), self.name)) + self.set_title( + "Feature Importances of {} Features using {}".format( + len(self.features_), self.name + ) + ) # Set the xlabel self.ax.set_xlabel(self._get_xlabel()) # Remove the ygrid - self.ax.grid(False, axis='y') + self.ax.grid(False, axis="y") - if self.stack: - plt.legend(bbox_to_anchor=(1.04, 0.5), loc="center left") # Ensure we have a tight fit plt.tight_layout() @@ -306,17 +345,30 @@ def _is_fitted(self): """ Returns true if the visualizer has been fit. """ - return hasattr(self, 'feature_importances_') and hasattr(self, 'features_') + return hasattr(self, "feature_importances_") and hasattr(self, "features_") ########################################################################## ## Quick Method ########################################################################## -def feature_importances(model, X, y=None, ax=None, labels=None, - relative=True, absolute=False, xlabel=None, - stack=False, **kwargs): - """ + +def feature_importances( + model, + X, + y=None, + ax=None, + labels=None, + relative=True, + absolute=False, + xlabel=None, + stack=False, + colors=None, + colormap=None, + is_fitted="auto", + **kwargs +): + """Quick Method: Displays the most informative features in a model by showing a bar chart of features ranked by their importances. Although primarily a feature engineering mechanism, this visualizer requires a model that has either a @@ -326,7 +378,9 @@ def feature_importances(model, X, y=None, ax=None, labels=None, ---------- model : Estimator A Scikit-Learn estimator that learns feature importances. Must support - either ``coef_`` or ``feature_importances_`` parameters. + either ``coef_`` or ``feature_importances_`` parameters. If the estimator + is not fitted, it is fit when the visualizer is fitted, unless otherwise + specified by ``is_fitted``. X : ndarray or DataFrame of shape n x m A matrix of n instances with m features @@ -360,22 +414,45 @@ def feature_importances(model, X, y=None, ax=None, labels=None, then a stacked bar plot is plotted; otherwise the mean of the feature importance across classes are plotted. + colors: list of strings + Specify colors for each bar in the chart if ``stack==False``. + + colormap : string or matplotlib cmap + Specify a colormap to color the classes if ``stack==True``. + + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. Returns ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. + viz : FeatureImportances + The feature importances visualizer, fitted and finalized. """ # Instantiate the visualizer visualizer = FeatureImportances( - model, ax, labels, relative, absolute, xlabel, stack, **kwargs) + model, + ax=ax, + labels=labels, + relative=relative, + absolute=absolute, + xlabel=xlabel, + stack=stack, + colors=colors, + colormap=colormap, + is_fitted=is_fitted, + **kwargs + ) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y) visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/model_selection/learning_curve.py b/yellowbrick/model_selection/learning_curve.py index ff367af51..07ca55162 100644 --- a/yellowbrick/model_selection/learning_curve.py +++ b/yellowbrick/model_selection/learning_curve.py @@ -1,10 +1,13 @@ # yellowbrick.model_selection.learning_curve # Implements a learning curve visualization for model selection. # -# Author: Jason Keung +# Author: Jason Keung # Created: Mon May 22 09:22:00 2017 -0500 # -# ID: learning_curve.py [] jason.s.keung@gmail.com $ +# Copyright (C) 2017 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: learning_curve.py [c5355ee] benjamin@bengfort.com $ """ Implements a learning curve visualization for model selection. @@ -28,9 +31,10 @@ ########################################################################## -## LearningCurve Visualizer +# LearningCurve Visualizer ########################################################################## + class LearningCurve(ModelVisualizer): """ Visualizes the learning curve for both test and training data for @@ -84,7 +88,7 @@ class LearningCurve(ModelVisualizer): - An iterable yielding train/test splits. see the scikit-learn - `cross-validation guide `_ + `cross-validation guide `_ for more information on the possible strategies that can be used here. scoring : string, callable or None, optional, default: None @@ -156,18 +160,29 @@ class LearningCurve(ModelVisualizer): ----- This visualizer is essentially a wrapper for the ``sklearn.model_selection.learning_curve utility``, discussed in the - `validation curves `_ + `validation curves `__ documentation. .. seealso:: The documentation for the - `learning_curve `_ + `learning_curve `__ function, which this visualizer wraps. """ - def __init__(self, model, ax=None, groups=None, - train_sizes=DEFAULT_TRAIN_SIZES, cv=None, scoring=None, - exploit_incremental_learning=False, n_jobs=1, - pre_dispatch="all", shuffle=False, random_state=None, - **kwargs): + + def __init__( + self, + model, + ax=None, + groups=None, + train_sizes=DEFAULT_TRAIN_SIZES, + cv=None, + scoring=None, + exploit_incremental_learning=False, + n_jobs=1, + pre_dispatch="all", + shuffle=False, + random_state=None, + **kwargs + ): # Initialize the model visualizer super(LearningCurve, self).__init__(model, ax=ax, **kwargs) @@ -177,14 +192,20 @@ def __init__(self, model, ax=None, groups=None, if train_sizes.ndim != 1: raise YellowbrickValueError( "must specify array of train sizes, '{}' is not valid".format( - repr(train_sizes) - )) + repr(train_sizes) + ) + ) # Set the metric parameters to be used later self.set_params( - groups=groups, train_sizes=train_sizes, cv=cv, scoring=scoring, + groups=groups, + train_sizes=train_sizes, + cv=cv, + scoring=scoring, exploit_incremental_learning=exploit_incremental_learning, - n_jobs=n_jobs, pre_dispatch=pre_dispatch, shuffle=shuffle, + n_jobs=n_jobs, + pre_dispatch=pre_dispatch, + shuffle=shuffle, random_state=random_state, ) @@ -214,9 +235,15 @@ def fit(self, X, y=None): sklc_kwargs = { key: self.get_params()[key] for key in ( - 'groups', 'train_sizes', 'cv', 'scoring', - 'exploit_incremental_learning', 'n_jobs', - 'pre_dispatch', 'shuffle', 'random_state', + "groups", + "train_sizes", + "cv", + "scoring", + "exploit_incremental_learning", + "n_jobs", + "pre_dispatch", + "shuffle", + "random_state", ) } @@ -254,15 +281,13 @@ def draw(self, **kwargs): for idx, (mean, std) in enumerate(curves): # Plot one standard deviation above and below the mean self.ax.fill_between( - self.train_sizes_, mean - std, mean+std, alpha=0.25, - color=colors[idx], + self.train_sizes_, mean - std, mean + std, alpha=0.25, color=colors[idx] ) # Plot the mean curves so they are in front of the variance fill for idx, (mean, _) in enumerate(curves): self.ax.plot( - self.train_sizes_, mean, 'o-', color=colors[idx], - label=labels[idx], + self.train_sizes_, mean, "o-", color=colors[idx], label=labels[idx] ) return self.ax @@ -272,25 +297,37 @@ def finalize(self, **kwargs): Add the title, legend, and other visual final touches to the plot. """ # Set the title of the figure - self.set_title('Learning Curve for {}'.format(self.name)) + self.set_title("Learning Curve for {}".format(self.name)) # Add the legend - self.ax.legend(frameon=True, loc='best') + self.ax.legend(frameon=True, loc="best") # Set the axis labels - self.ax.set_xlabel('Training Instances') - self.ax.set_ylabel('Score') + self.ax.set_xlabel("Training Instances") + self.ax.set_ylabel("Score") ########################################################################## -## Quick Methods +# Quick Methods ########################################################################## -def learning_curve(model, X, y, ax=None, groups=None, - train_sizes=DEFAULT_TRAIN_SIZES, cv=None, scoring=None, - exploit_incremental_learning=False, n_jobs=1, - pre_dispatch="all", shuffle=False, random_state=None, - **kwargs): + +def learning_curve( + model, + X, + y, + ax=None, + groups=None, + train_sizes=DEFAULT_TRAIN_SIZES, + cv=None, + scoring=None, + exploit_incremental_learning=False, + n_jobs=1, + pre_dispatch="all", + shuffle=False, + random_state=None, + **kwargs +): """ Displays a learning curve based on number of samples vs training and cross validation scores. The learning curve aims to show how a model @@ -341,7 +378,7 @@ def learning_curve(model, X, y, ax=None, groups=None, - An iterable yielding train/test splits. see the scikit-learn - `cross-validation guide `_ + `cross-validation guide `_ for more information on the possible strategies that can be used here. scoring : string, callable or None, optional, default: None @@ -379,18 +416,25 @@ def learning_curve(model, X, y, ax=None, groups=None, Returns ------- - ax : matplotlib axes - Returns the axes that the learning curve were drawn on. + visualizer : LearningCurve + Returns the fitted visualizer. """ # Initialize the visualizer oz = LearningCurve( - model, ax=ax, groups=groups, train_sizes=train_sizes, cv=cv, - scoring=scoring, n_jobs=n_jobs, pre_dispatch=pre_dispatch, - shuffle=shuffle, random_state=random_state, + model, + ax=ax, + groups=groups, + train_sizes=train_sizes, + cv=cv, + scoring=scoring, + n_jobs=n_jobs, + pre_dispatch=pre_dispatch, + shuffle=shuffle, + random_state=random_state, exploit_incremental_learning=exploit_incremental_learning, ) # Fit and poof the visualizer oz.fit(X, y) oz.poof(**kwargs) - return oz.ax + return oz diff --git a/yellowbrick/features/rfecv.py b/yellowbrick/model_selection/rfecv.py similarity index 88% rename from yellowbrick/features/rfecv.py rename to yellowbrick/model_selection/rfecv.py index 0b5a65a14..3d75ff951 100644 --- a/yellowbrick/features/rfecv.py +++ b/yellowbrick/model_selection/rfecv.py @@ -1,10 +1,13 @@ -# yellowbrick.features.rfecv +# yellowbrick.model_selection.rfecv # Visualize the number of features selected with recursive feature elimination # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Apr 03 17:31:37 2018 -0400 # -# ID: rfecv.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: rfecv.py [a4599db] rebeccabilbro@users.noreply.github.com $ """ Visualize the number of features selected using recursive feature elimination @@ -28,6 +31,7 @@ ## Recursive Feature Elimination ########################################################################## + class RFECV(ModelVisualizer): """ Recursive Feature Elimination, Cross-Validated (RFECV) feature selection. @@ -112,6 +116,10 @@ class RFECV(ModelVisualizer): functions such as ``predict()`` and ``score()`` are passed through to this estimator (it rewraps the original model). + n_feature_subsets_ : array of shape [n_subsets_of_features] + The number of features removed on each iteration of RFE, computed by the + number of features in the dataset and the step parameter. + Notes ----- This model wraps ``sklearn.feature_selection.RFE`` and not @@ -129,8 +137,9 @@ class RFECV(ModelVisualizer): or ``feature_importances_`` attribute when fitted. """ - def __init__(self, model, ax=None, step=1, groups=None, cv=None, - scoring=None, **kwargs): + def __init__( + self, model, ax=None, step=1, groups=None, cv=None, scoring=None, **kwargs + ): # Initialize the model visualizer super(RFECV, self).__init__(model, ax=ax, **kwargs) @@ -167,23 +176,20 @@ def fit(self, X, y=None): else: step = int(self.step) - if step < 0: + if step <= 0: raise YellowbrickValueError("step must be >0") # Create the RFE model rfe = RFE(self.estimator, step=step) - n_feature_subsets = np.arange(1, n_features+1) + self.n_feature_subsets_ = np.arange(1, n_features + step, step) # Create the cross validation params # TODO: handle random state - cv_params = { - key: self.get_params()[key] - for key in ('groups', 'cv', 'scoring') - } + cv_params = {key: self.get_params()[key] for key in ("groups", "cv", "scoring")} # Perform cross-validation for each feature subset scores = [] - for n_features_to_select in n_feature_subsets: + for n_features_to_select in self.n_feature_subsets_: rfe.set_params(n_features_to_select=n_features_to_select) scores.append(cross_val_score(rfe, X, y, **cv_params)) @@ -192,7 +198,7 @@ def fit(self, X, y=None): # Find the best RFE model bestidx = self.cv_scores_.mean(axis=1).argmax() - self.n_features_ = n_feature_subsets[bestidx] + self.n_features_ = self.n_feature_subsets_[bestidx] # Fit the final RFE model for the number of features self.rfe_estimator_ = rfe @@ -214,23 +220,24 @@ def draw(self, **kwargs): Renders the rfecv curve. """ # Compute the curves - x = np.arange(1, len(self.cv_scores_)+1) + x = self.n_feature_subsets_ means = self.cv_scores_.mean(axis=1) sigmas = self.cv_scores_.std(axis=1) - # Plot one standard deviation above and below the mean - self.ax.fill_between(x, means - sigmas, means+sigmas, alpha=0.25) + self.ax.fill_between(x, means - sigmas, means + sigmas, alpha=0.25) # Plot the curve - self.ax.plot(x, means, 'o-') + self.ax.plot(x, means, "o-") # Plot the maximum number of features self.ax.axvline( - self.n_features_, c='k', ls='--', + self.n_features_, + c="k", + ls="--", label="n_features = {}\nscore = {:0.3f}".format( self.n_features_, self.cv_scores_.mean(axis=1).max() - ) + ), ) return self.ax @@ -240,22 +247,22 @@ def finalize(self, **kwargs): Add the title, legend, and other visual final touches to the plot. """ # Set the title of the figure - self.set_title('RFECV for {}'.format(self.name)) + self.set_title("RFECV for {}".format(self.name)) # Add the legend - self.ax.legend(frameon=True, loc='best') + self.ax.legend(frameon=True, loc="best") # Set the axis labels - self.ax.set_xlabel('Number of Features Selected') - self.ax.set_ylabel('Score') + self.ax.set_xlabel("Number of Features Selected") + self.ax.set_ylabel("Score") ########################################################################## ## Quick Methods ########################################################################## -def rfecv(model, X, y, ax=None, step=1, groups=None, cv=None, - scoring=None, **kwargs): + +def rfecv(model, X, y, ax=None, step=1, groups=None, cv=None, scoring=None, **kwargs): """ Performs recursive feature elimination with cross-validation to determine an optimal number of features for a model. Visualizes the feature subsets @@ -319,8 +326,8 @@ def rfecv(model, X, y, ax=None, step=1, groups=None, cv=None, Returns ------- - ax : matplotlib axes - Returns the axes that the rfecv were drawn on. + viz : RFECV + Returns the fitted, finalized visualizer. """ # Initialize the visualizer oz = RFECV(model, ax=ax, step=step, groups=groups, cv=cv, scoring=scoring) @@ -328,4 +335,5 @@ def rfecv(model, X, y, ax=None, step=1, groups=None, cv=None, # Fit and poof the visualizer oz.fit(X, y) oz.poof(**kwargs) - return oz.ax + + return oz diff --git a/yellowbrick/model_selection/validation_curve.py b/yellowbrick/model_selection/validation_curve.py index 3dd9ca356..f7b012f23 100644 --- a/yellowbrick/model_selection/validation_curve.py +++ b/yellowbrick/model_selection/validation_curve.py @@ -1,17 +1,20 @@ # yellowbrick.model_selection.validation_curve # Implements a visual validation curve for a hyperparameter. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Sat Mar 31 06:27:28 2018 -0400 # -# ID: validation_curve.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: validation_curve.py [c5355ee] benjamin@bengfort.com $ """ Implements a visual validation curve for a hyperparameter. """ ########################################################################## -## Imports +# Imports ########################################################################## import numpy as np @@ -24,9 +27,10 @@ ########################################################################## -## ValidationCurve visualizer +# ValidationCurve visualizer ########################################################################## + class ValidationCurve(ModelVisualizer): """ Visualizes the validation curve for both test and training data for a @@ -83,7 +87,7 @@ class ValidationCurve(ModelVisualizer): - An iterable yielding train/test splits. see the scikit-learn - `cross-validation guide `_ + `cross-validation guide `_ for more information on the possible strategies that can be used here. scoring : string, callable or None, optional, default: None @@ -137,18 +141,29 @@ class ValidationCurve(ModelVisualizer): Notes ----- This visualizer is essentially a wrapper for the - ``sklearn.model_selection.validation_curve utility``, discussed in the - `validation curves `_ + ``sklearn.model_selection.learning_curve utility``, discussed in the + `validation curves `__ documentation. .. seealso:: The documentation for the - `validation_curve `_ + `learning_curve `__ function, which this visualizer wraps. """ - def __init__(self, model, param_name, param_range, ax=None, logx=False, - groups=None, cv=None, scoring=None, n_jobs=1, - pre_dispatch="all", **kwargs): + def __init__( + self, + model, + param_name, + param_range, + ax=None, + logx=False, + groups=None, + cv=None, + scoring=None, + n_jobs=1, + pre_dispatch="all", + **kwargs + ): # Initialize the model visualizer super(ValidationCurve, self).__init__(model, ax=ax, **kwargs) @@ -159,16 +174,21 @@ def __init__(self, model, param_name, param_range, ax=None, logx=False, raise YellowbrickValueError( "must specify array of param values, '{}' is not valid".format( repr(param_range) - )) + ) + ) # Set the visual and validation curve parameters on the estimator self.set_params( - param_name=param_name, param_range=param_range, logx=logx, - groups=groups, cv=cv, scoring=scoring, n_jobs=n_jobs, + param_name=param_name, + param_range=param_range, + logx=logx, + groups=groups, + cv=cv, + scoring=scoring, + n_jobs=n_jobs, pre_dispatch=pre_dispatch, ) - def fit(self, X, y=None): """ Fits the validation curve with the wrapped estimator and parameter @@ -195,8 +215,13 @@ def fit(self, X, y=None): skvc_kwargs = { key: self.get_params()[key] for key in ( - 'param_name', 'param_range', 'groups', 'cv', 'scoring', - 'n_jobs', 'pre_dispatch', + "param_name", + "param_range", + "groups", + "cv", + "scoring", + "n_jobs", + "pre_dispatch", ) } @@ -234,19 +259,17 @@ def draw(self, **kwargs): for idx, (mean, std) in enumerate(curves): # Plot one standard deviation above and below the mean self.ax.fill_between( - self.param_range, mean - std, mean+std, alpha=0.25, - color=colors[idx], + self.param_range, mean - std, mean + std, alpha=0.25, color=colors[idx] ) # Plot the mean curves so they are in front of the variance fill for idx, (mean, _) in enumerate(curves): self.ax.plot( - self.param_range, mean, 'd-', color=colors[idx], - label=labels[idx], + self.param_range, mean, "d-", color=colors[idx], label=labels[idx] ) if self.logx: - self.ax.set_xscale('log') + self.ax.set_xscale("log") return self.ax @@ -255,23 +278,36 @@ def finalize(self, **kwargs): Add the title, legend, and other visual final touches to the plot. """ # Set the title of the figure - self.set_title('Validation Curve for {}'.format(self.name)) + self.set_title("Validation Curve for {}".format(self.name)) # Add the legend - self.ax.legend(frameon=True, loc='best') + self.ax.legend(frameon=True, loc="best") # Set the axis labels self.ax.set_xlabel(self.param_name) - self.ax.set_ylabel('score') + self.ax.set_ylabel("score") ########################################################################## -## Quick Method +# Quick Method ########################################################################## -def validation_curve(model, X, y, param_name, param_range, ax=None, logx=False, - groups=None, cv=None, scoring=None, n_jobs=1, - pre_dispatch="all", **kwargs): + +def validation_curve( + model, + X, + y, + param_name, + param_range, + ax=None, + logx=False, + groups=None, + cv=None, + scoring=None, + n_jobs=1, + pre_dispatch="all", + **kwargs +): """ Displays a validation curve for the specified param and values, plotting both the train and cross-validated test scores. The validation curve is a @@ -324,7 +360,7 @@ def validation_curve(model, X, y, param_name, param_range, ax=None, logx=False, - An iterable yielding train/test splits. see the scikit-learn - `cross-validation guide `_ + `cross-validation guide `_ for more information on the possible strategies that can be used here. scoring : string, callable or None, optional, default: None @@ -348,17 +384,25 @@ def validation_curve(model, X, y, param_name, param_range, ax=None, logx=False, Returns ------- - ax : matplotlib.Axes - The axes object that the validation curves were drawn on. + visualizer : ValidationCurve + The fitted visualizer """ # Initialize the visualizer oz = ValidationCurve( - model, param_name, param_range, ax=ax, logx=logx, groups=groups, - cv=cv, scoring=scoring, n_jobs=n_jobs, pre_dispatch=pre_dispatch + model, + param_name, + param_range, + ax=ax, + logx=logx, + groups=groups, + cv=cv, + scoring=scoring, + n_jobs=n_jobs, + pre_dispatch=pre_dispatch, ) # Fit and poof the visualizer oz.fit(X, y) oz.poof(**kwargs) - return oz.ax + return oz diff --git a/yellowbrick/pipeline.py b/yellowbrick/pipeline.py index 8541b3ea4..481cc8845 100644 --- a/yellowbrick/pipeline.py +++ b/yellowbrick/pipeline.py @@ -1,10 +1,10 @@ # yellowbrick.pipeline # Implements a visual pipeline that subclasses Scikit-Learn pipelines. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Oct 07 21:41:06 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The sckit-yb developers # For license information, see LICENSE.txt # # ID: pipeline.py [1efae1f] benjamin@bengfort.com $ @@ -27,6 +27,7 @@ ## Visual Pipeline ########################################################################## + class VisualPipeline(Pipeline): """Pipeline of transforms and visualizers with a final estimator. @@ -68,10 +69,7 @@ class VisualPipeline(Pipeline): @property def visual_steps(self): - return dict( - step for step in self.steps - if isinstance(step[1], Visualizer) - ) + return dict(step for step in self.steps if isinstance(step[1], Visualizer)) def poof(self, outdir=None, ext=".pdf", **kwargs): """ @@ -92,13 +90,18 @@ def poof(self, outdir=None, ext=".pdf", **kwargs): kwargs : dict Keyword arguments to pass to the ``poof()`` method of all steps. """ + axes = [] for name, step in self.visual_steps.items(): if outdir is not None: outpath = path.join(outdir, slugify(name) + ext) else: outpath = None - step.poof(outpath=outpath, **kwargs) + ax = step.poof(outpath=outpath, **kwargs) + axes.append(ax) + + # Return axes array to ensure figures are shown in notebook + return axes def fit_transform_poof(self, X, y=None, outpath=None, **kwargs): """ diff --git a/yellowbrick/regressor/__init__.py b/yellowbrick/regressor/__init__.py index 9270f3405..b908e7616 100644 --- a/yellowbrick/regressor/__init__.py +++ b/yellowbrick/regressor/__init__.py @@ -1,10 +1,10 @@ # yellowbrick.regressor # Visualizers for Regression analysis and diagnostics # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Mon Mar 06 12:23:55 2017 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [7d3f5e6] benjamin@bengfort.com $ @@ -22,3 +22,4 @@ from .base import * from .residuals import * from .alphas import * +from .influence import * diff --git a/yellowbrick/regressor/alphas.py b/yellowbrick/regressor/alphas.py index e299d6e27..77a2f7cf0 100644 --- a/yellowbrick/regressor/alphas.py +++ b/yellowbrick/regressor/alphas.py @@ -1,10 +1,11 @@ # yellowbrick.regressor.alphas # Implements alpha selection visualizers for regularization # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Mon Mar 06 19:22:07 2017 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: alphas.py [7d3f5e6] benjamin@bengfort.com $ @@ -21,22 +22,21 @@ from functools import partial -from .base import RegressionScoreVisualizer -from ..exceptions import YellowbrickTypeError -from ..exceptions import YellowbrickValueError +from yellowbrick.exceptions import YellowbrickTypeError +from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.regressor.base import RegressionScoreVisualizer from sklearn.model_selection import cross_val_score ## Packages for export -__all__ = [ - "AlphaSelection", "ManualAlphaSelection" -] +__all__ = ["AlphaSelection", "ManualAlphaSelection"] ########################################################################## ## AlphaSelection Visualizer ########################################################################## + class AlphaSelection(RegressionScoreVisualizer): """ The Alpha Selection Visualizer demonstrates how different values of alpha @@ -68,13 +68,20 @@ class AlphaSelection(RegressionScoreVisualizer): model : a Scikit-Learn regressor Should be an instance of a regressor, and specifically one whose name ends with "CV" otherwise a will raise a YellowbrickTypeError exception - on instantiation. To use non-CV regressors see: - ``ManualAlphaSelection``. + on instantiation. To use non-CV regressors see: ``ManualAlphaSelection``. + If the estimator is not fitted, it is fit when the visualizer is fitted, + unless otherwise specified by ``is_fitted``. ax : matplotlib Axes, default: None The axes to plot the figure on. If None is passed in the current axes will be used (or generated if required). + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. @@ -108,18 +115,20 @@ class AlphaSelection(RegressionScoreVisualizer): For RidgeCV, make sure ``store_cv_values=True``. """ - def __init__(self, model, ax=None, **kwargs): + def __init__(self, model, ax=None, is_fitted="auto", **kwargs): # Check to make sure this is a "RegressorCV" name = model.__class__.__name__ if not name.endswith("CV"): - raise YellowbrickTypeError(( - "'{}' is not a CV regularization model;" - " try ManualAlphaSelection instead." - ).format(name)) + raise YellowbrickTypeError( + ( + "'{}' is not a CV regularization model;" + " try ManualAlphaSelection instead." + ).format(name) + ) # Set the store_cv_values parameter on RidgeCV - if 'store_cv_values' in model.get_params().keys(): + if "store_cv_values" in model.get_params().keys(): model.set_params(store_cv_values=True) # Call super to initialize the class @@ -130,16 +139,13 @@ def fit(self, X, y, **kwargs): A simple pass-through method; calls fit on the estimator and then draws the alpha-error plot. """ - self.estimator.fit(X, y, **kwargs) + # Fit the underlying model + super(AlphaSelection, self).fit(X, y, **kwargs) + + # Draw the alpha to error curve self.draw() return self - def score(self, X, y, **kwargs): - """ - Simply returns the score of the underlying CV model - """ - return self.estimator.score(X, y, **kwargs) - def draw(self): """ Draws the alpha plot based on the values on the estimator. @@ -148,16 +154,15 @@ def draw(self): alphas = self._find_alphas_param() errors = self._find_errors_param() - - alpha = self.estimator.alpha_ # Get decision from the estimator - name = self.name[:-2].lower() # Remove the CV from the label + alpha = self.estimator.alpha_ # Get decision from the estimator + name = self.name[:-2].lower() # Remove the CV from the label # Plot the alpha against the error self.ax.plot(alphas, errors, label=name) # Draw a dashed vline at the alpha label = "$\\alpha={:0.3f}$".format(alpha) - self.ax.axvline(alpha, color='k', linestyle='dashed', label=label) + self.ax.axvline(alpha, color="k", linestyle="dashed", label=label) return self.ax @@ -167,16 +172,14 @@ def finalize(self): X and Y axis labels and adding the legend. """ # Set the title - self.set_title( - '{} Alpha Error'.format(self.name) - ) + self.set_title("{} Alpha Error".format(self.name)) # Set the x and y labels self.ax.set_xlabel("alpha") self.ax.set_ylabel("error (or score)") # Set the legend - self.ax.legend(loc='best', frameon=True) + self.ax.legend(loc="best", frameon=True) def _find_alphas_param(self): """ @@ -186,7 +189,7 @@ def _find_alphas_param(self): """ # NOTE: The order of the search is very important! - for attr in ("cv_alphas_", "alphas_", "alphas",): + for attr in ("cv_alphas_", "alphas_", "alphas"): try: return getattr(self.estimator, attr) except AttributeError: @@ -206,10 +209,10 @@ def _find_errors_param(self): """ # NOTE: The order of the search is very important! - if hasattr(self.estimator, 'mse_path_'): + if hasattr(self.estimator, "mse_path_"): return self.estimator.mse_path_.mean(1) - if hasattr(self.estimator, 'cv_values_'): + if hasattr(self.estimator, "cv_values_"): return self.estimator.cv_values_.mean(0) raise YellowbrickValueError( @@ -218,10 +221,12 @@ def _find_errors_param(self): ) ) + ########################################################################## ## ManualAlphaSelection Visualizer ########################################################################## + class ManualAlphaSelection(AlphaSelection): """ The ``AlphaSelection`` visualizer requires a "RegressorCV", that is a @@ -235,9 +240,9 @@ class ManualAlphaSelection(AlphaSelection): Parameters ---------- - model : a Scikit-Learn regressor - Should be an instance of a regressor, and specifically one whose name - doesn't end with "CV". The regressor must support a call to + model : an unfitted Scikit-Learn regressor + Should be an instance of an unfitted regressor, and specifically one + whose name doesn't end with "CV". The regressor must support a call to ``set_params(alpha=alpha)`` and be fit multiple times. If the regressor name ends with "CV" a ``YellowbrickValueError`` is raised. @@ -294,16 +299,16 @@ class ManualAlphaSelection(AlphaSelection): "RegressorCV" estimators. """ - def __init__(self, model, ax=None, alphas=None, - cv=None, scoring=None, **kwargs): + def __init__(self, model, ax=None, alphas=None, cv=None, scoring=None, **kwargs): # Check to make sure this is not a "RegressorCV" name = model.__class__.__name__ if name.endswith("CV"): - raise YellowbrickTypeError(( - "'{}' is a CV regularization model;" - " try AlphaSelection instead." - ).format(name)) + raise YellowbrickTypeError( + ( + "'{}' is a CV regularization model;" " try AlphaSelection instead." + ).format(name) + ) # Call super to initialize the class super(ManualAlphaSelection, self).__init__(model, ax=ax, **kwargs) @@ -334,12 +339,6 @@ def fit(self, X, y, **args): # Always make sure to return self from fit return self - def score(self, X, y, **kwargs): - """ - Simply returns the score of the underlying CV model - """ - return self.estimator.score(X, y, **kwargs) - def draw(self): """ Draws the alphas values against their associated error in a similar @@ -351,11 +350,69 @@ def draw(self): # Draw a dashed vline at the alpha with maximal error alpha = self.alphas[np.where(self.errors == self.errors.max())][0] label = "$\\alpha_{{max}}={:0.3f}$".format(alpha) - self.ax.axvline(alpha, color='k', linestyle='dashed', label=label) + self.ax.axvline(alpha, color="k", linestyle="dashed", label=label) # Draw a dashed vline at the alpha with minimal error alpha = self.alphas[np.where(self.errors == self.errors.min())][0] label = "$\\alpha_{{min}}={:0.3f}$".format(alpha) - self.ax.axvline(alpha, color='k', linestyle='dashed', label=label) + self.ax.axvline(alpha, color="k", linestyle="dashed", label=label) return self.ax + + +########################################################################## +## Quick Method +########################################################################## + + +def alphas(model, X, y=None, ax=None, is_fitted="auto", **kwargs): + """Quick Method: + The Alpha Selection Visualizer demonstrates how different values of alpha + influence model selection during the regularization of linear models. + Generally speaking, alpha increases the affect of regularization, e.g. if + alpha is zero there is no regularization and the higher the alpha, the + more the regularization parameter influences the final model. + + Parameters + ---------- + + model : a Scikit-Learn regressor + Should be an instance of a regressor, and specifically one whose name + ends with "CV" otherwise a will raise a YellowbrickTypeError exception + on instantiation. To use non-CV regressors see: ``ManualAlphaSelection``. + If the estimator is not fitted, it is fit when the visualizer is fitted, + unless otherwise specified by ``is_fitted``. + + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features. + + y : ndarray or Series of length n + An array or series of target values. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes + will be used (or generated if required). + + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + + kwargs : dict + Keyword arguments that are passed to the base class and may influence + the visualization as defined in other Visualizers. + + Returns + ------- + visualizer : AlphaSelection + Returns the alpha selection visualizer + """ + # Instantiate the visualizer + visualizer = AlphaSelection(model, ax, is_fitted=is_fitted, **kwargs) + + visualizer.score(X, y) + visualizer.finalize() + + # Return the visualizer + return visualizer diff --git a/yellowbrick/regressor/base.py b/yellowbrick/regressor/base.py index 6fcbc0a21..77782ccc4 100644 --- a/yellowbrick/regressor/base.py +++ b/yellowbrick/regressor/base.py @@ -1,11 +1,11 @@ # yellowbrick.regressor.base # Base classes for regressor Visualizers. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Fri Jun 03 10:30:36 2016 -0700 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: base.py [7d3f5e6] benjamin@bengfort.com $ @@ -24,43 +24,86 @@ ## Packages for export -__all__ = [ - "RegressionScoreVisualizer", -] +__all__ = ["RegressionScoreVisualizer"] ########################################################################## ## Regression Visualization Base Object ########################################################################## + class RegressionScoreVisualizer(ScoreVisualizer): - """ - Base class for all ScoreVisualizers that evaluate a regression estimator. + """Base class for regressor model selection. + + The RegressionScoreVisualizer wraps a regression model to produce a + visualization when the score method is called, usually to allow the user + to effectively compare the performance between models. + + The base class provides helper functionality to ensure that regression + visualizers consistently store the trained score for access post visualization + and that a correct regressor is passed to the visualizer. + + Parameters + ---------- + model : estimator + A scikit-learn estimator that should be a regressor. If the model is + not a regressor, an exception is raised. + + ax : matplotlib Axes, default: None + The axis to plot the figure on. If None is passed in the current axes + will be used (or generated if required). - The primary functionality of this class is to perform a check to ensure - the passed in estimator is a regressor, otherwise it raises a - ``YellowbrickTypeError``. + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + + force_model : bool, default: False + Do not check to ensure that the underlying estimator is a classifier. This + will prevent an exception when the visualizer is initialized but may result + in unexpected or unintended behavior. + + kwargs: dict + Keyword arguments passed to the super class. + + Attributes + ---------- + score_ : float + An evaluation metric of the regressor on test data produced when + ``score()`` is called. This metric is between 0 and 1 -- higher scores are + generally better. For regressors, this score is usually the r2_score, but + ensure you check the underlying model for more details about the metric. """ - def __init__(self, model, ax=None, **kwargs): - if not isregressor(model): + def __init__(self, model, ax=None, fig=None, force_model=False, **kwargs): + if not force_model and not isregressor(model): raise YellowbrickTypeError( "This estimator is not a regressor; try a classifier or " "clustering score visualizer instead!" - ) + ) - super(RegressionScoreVisualizer, self).__init__(model, ax=ax, **kwargs) + self.force_model = force_model + super(RegressionScoreVisualizer, self).__init__(model, ax=ax, fig=fig, **kwargs) def score(self, X, y, **kwargs): """ - The score method is the primary entry point for drawing. + The score function is the hook for visual interaction. Pass in test + data and the visualizer will create predictions on the data and + evaluate them with respect to the test values. The evaluation will + then be passed to draw() and the result of the estimator score will + be returned. + + Parameters + ---------- + X : array-like + X (also X_test) are the dependent variables of test set to predict + + y : array-like + y (also y_test) is the independent actual variables to score against Returns ------- score : float The R^2 score of the underlying regressor """ - raise NotImplementedError( - "Subclasses of RegressionScoreVisualizer must implement score " - " and return an R^2 score of the underlying estimator" - ) + self.score_ = self.estimator.score(X, y) + return self.score_ diff --git a/yellowbrick/regressor/influence.py b/yellowbrick/regressor/influence.py new file mode 100644 index 000000000..0be2da23b --- /dev/null +++ b/yellowbrick/regressor/influence.py @@ -0,0 +1,282 @@ +# yellowbrick.regressor.influence +# Visualize the influence and leverage of individual instances on a regression model. +# +# Author: Benjamin Bengfort +# Created: Sun Jun 09 15:21:17 2019 -0400 +# +# Copyright (C) 2019 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: influence.py [fe14cfd] benjamin@bengfort.com $ + +""" +Visualize the influence and leverage of individual instances on a regression model. +""" + +########################################################################## +## Imports +########################################################################## + +import numpy as np +import scipy as sp + +from yellowbrick.base import Visualizer +from sklearn.linear_model import LinearRegression + + +########################################################################## +## Cook's Distance +########################################################################## + + +class CooksDistance(Visualizer): + """ + Cook's Distance is a measure of how influential an instance is to the computation of + a regression, e.g. if the instance is removed would the estimated coeficients of the + underlying model be substantially changed? Because of this, Cook's Distance is + generally used to detect outliers in standard, OLS regression. In fact, a general + rule of thumb is that D(i) > 4/n is a good threshold for determining highly + influential points as outliers and this visualizer can report the percentage of data + that is above that threshold. + + This implementation of Cook's Distance assumes Ordinary Least Squares regression, + and therefore embeds a ``sklearn.linear_model.LinearRegression`` under the hood. + Distance is computed via the non-whitened leverage of the projection matrix, + computed inside of ``fit()``. The results of this visualizer are therefore similar + to, but not as advanced, as a similar computation using statsmodels. Computing the + influence for other regression models requires leave one out validation and can be + expensive to compute. + + .. seealso:: + For a longer discussion on detecting outliers in regression and computing + leverage and influence, see `linear regression in python, outliers/leverage + detect `_ by Huiming Song. + + Parameters + ---------- + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes + will be used (or generated if required). + + draw_threshold : bool, default: True + Draw a horizontal line at D(i) == 4/n to easily identify the most influential + points on the final regression. This will also draw a legend that specifies the + percentage of data points that are above the threshold. + + linefmt : str, default: 'C0-' + A string defining the properties of the vertical lines of the stem plot, usually + this will be a color or a color and a line style. The default is simply a solid + line with the first color of the color cycle. + + markerfmt : str, default: ',' + A string defining the properties of the markers at the stem plot heads. The + default is "pixel", e.g. basically no marker head at the top of the stem plot. + + kwargs : dict + Keyword arguments that are passed to the base class and may influence the final + visualization (e.g. size or title parameters). + + Attributes + ---------- + distance_ : array, 1D + The Cook's distance value for each instance specified in ``X``, e.g. an 1D array + with shape ``(X.shape[0],)``. + + p_values_ : array, 1D + The p values associated with the F-test of Cook's distance distribution. A 1D + array whose shape matches ``distance_``. + + influence_threshold_ : float + A rule of thumb influence threshold to determine outliers in the regression + model, defined as It=4/n. + + outlier_percentage_ : float + The percentage of instances whose Cook's distance is greater than the influnce + threshold, the percentage is 0.0 <= p <= 100.0. + + Notes + ----- + Cook's Distance is very similar to DFFITS, another diagnostic that is meant to show + how influential a point is in a statistical regression. Although the computed values + of Cook's and DFFITS are different, they are conceptually identical and there even + exists a closed-form formula to convert one value to another. Because of this, we + have chosen to implement Cook's distance rather than or in addition to DFFITS. + """ + + def __init__( + self, ax=None, draw_threshold=True, linefmt="C0-", markerfmt=",", **kwargs + ): + # Initialize the visualizer + super(CooksDistance, self).__init__(ax=ax, **kwargs) + + # Set "hyperparameters" + self.set_params( + draw_threshold=draw_threshold, linefmt=linefmt, markerfmt=markerfmt + ) + + # An internal LinearRegression used to compute the residuals and MSE + # This implementation doesn't support any regressor, it is OLS-specific + self._model = LinearRegression() + + def fit(self, X, y): + """ + Computes the leverage of X and uses the residuals of a + ``sklearn.linear_model.LinearRegression`` to compute the Cook's Distance of each + observation in X, their p-values and the number of outliers defined by the + number of observations supplied. + + Parameters + ---------- + X : array-like, 2D + The exogenous design matrix, e.g. training data. + + y : array-like, 1D + The endogenous response variable, e.g. target data. + + Returns + ------- + self : CooksDistance + Fit returns the visualizer instance. + """ + # Fit a linear model to X and y to compute MSE + self._model.fit(X, y) + + # Leverage is computed as the diagonal of the projection matrix of X + # TODO: whiten X before computing leverage + leverage = (X * np.linalg.pinv(X).T).sum(1) + + # Compute the rank and the degrees of freedom of the OLS model + rank = np.linalg.matrix_rank(X) + df = X.shape[0] - rank + + # Compute the MSE from the residuals + residuals = y - self._model.predict(X) + mse = np.dot(residuals, residuals) / df + + # Compute Cook's distance + residuals_studentized = residuals / np.sqrt(mse) / np.sqrt(1 - leverage) + self.distance_ = residuals_studentized ** 2 / X.shape[1] + self.distance_ *= leverage / (1 - leverage) + + # Compute the p-values of Cook's Distance + # TODO: honestly this was done because it was only in the statsmodels + # implementation... I have no idea what this is or why its important. + self.p_values_ = sp.stats.f.sf(self.distance_, X.shape[1], df) + + # Compute the influence threshold rule of thumb + self.influence_threshold_ = 4 / X.shape[0] + self.outlier_percentage_ = ( + sum(self.distance_ > self.influence_threshold_) / X.shape[0] + ) + self.outlier_percentage_ *= 100.0 + + self.draw() + return self + + def draw(self): + """ + Draws a stem plot where each stem is the Cook's Distance of the instance at the + index specified by the x axis. Optionaly draws a threshold line. + """ + # Draw a stem plot with the influence for each instance + _, _, baseline = self.ax.stem( + self.distance_, linefmt=self.linefmt, markerfmt=self.markerfmt + ) + + # No padding on either side of the instance index + self.ax.set_xlim(0, len(self.distance_)) + + # Draw the threshold for most influential points + if self.draw_threshold: + label = r"{:0.2f}% > $I_t$ ($I_t=\frac {{4}} {{n}}$)".format( + self.outlier_percentage_ + ) + self.ax.axhline( + self.influence_threshold_, + ls="--", + label=label, + c=baseline.get_color(), + lw=baseline.get_linewidth(), + ) + + return self.ax + + def finalize(self): + """ + Prepares the visualization for presentation and reporting. + """ + # Set the title and axis labels + self.set_title("Cook's Distance Outlier Detection") + self.ax.set_xlabel("instance index") + self.ax.set_ylabel("influence (I)") + + # Only add the legend if the influence threshold has been plotted + if self.draw_threshold: + self.ax.legend(loc="best", frameon=True) + + +def cooks_distance( + X, y, ax=None, draw_threshold=True, linefmt="C0-", markerfmt=",", **kwargs +): + """ + Cook's Distance is a measure of how influential an instance is to the computation of + a regression, e.g. if the instance is removed would the estimated coeficients of the + underlying model be substantially changed? Because of this, Cook's Distance is + generally used to detect outliers in standard, OLS regression. In fact, a general + rule of thumb is that D(i) > 4/n is a good threshold for determining highly + influential points as outliers and this visualizer can report the percentage of data + that is above that threshold. + + This implementation of Cook's Distance assumes Ordinary Least Squares regression, + and therefore embeds a ``sklearn.linear_model.LinearRegression`` under the hood. + Distance is computed via the non-whitened leverage of the projection matrix, + computed inside of ``fit()``. The results of this visualizer are therefore similar + to, but not as advanced, as a similar computation using statsmodels. Computing the + influence for other regression models requires leave one out validation and can be + expensive to compute. + + .. seealso:: + For a longer discussion on detecting outliers in regression and computing + leverage and influence, see `linear regression in python, outliers/leverage + detect `_ by Huiming Song. + + Parameters + ---------- + X : array-like, 2D + The exogenous design matrix, e.g. training data. + + y : array-like, 1D + The endogenous response variable, e.g. target data. + + ax : matplotlib Axes, default: None + The axes to plot the figure on. If None is passed in the current axes + will be used (or generated if required). + + draw_threshold : bool, default: True + Draw a horizontal line at D(i) == 4/n to easily identify the most influential + points on the final regression. This will also draw a legend that specifies the + percentage of data points that are above the threshold. + + linefmt : str, default: 'C0-' + A string defining the properties of the vertical lines of the stem plot, usually + this will be a color or a color and a line style. The default is simply a solid + line with the first color of the color cycle. + + markerfmt: str, default: ',' + A string defining the properties of the markers at the stem plot heads. The + default is "pixel", e.g. basically no marker head at the top of the stem plot. + + kwargs : dict + Keyword arguments that are passed to the base class and may influence the final + visualization (e.g. size or title parameters). + """ + viz = CooksDistance( + ax=ax, + draw_threshold=draw_threshold, + linefmt=linefmt, + markerfmt=markerfmt, + **kwargs + ) + viz.fit(X, y) + viz.finalize() + return viz diff --git a/yellowbrick/regressor/residuals.py b/yellowbrick/regressor/residuals.py index 4fa85c4c4..beb0de665 100644 --- a/yellowbrick/regressor/residuals.py +++ b/yellowbrick/regressor/residuals.py @@ -1,11 +1,11 @@ # yellowbrick.regressor.residuals # Regressor visualizers that score residuals: prediction vs. actual data. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Created: Fri Jun 03 10:30:36 2016 -0700 # -# Copyright (C) 2016 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: residuals.py [7d3f5e6] benjamin@bengfort.com $ @@ -29,30 +29,27 @@ from sklearn.model_selection import train_test_split -from .base import RegressionScoreVisualizer -from ..draw import manual_legend -from ..style.palettes import LINE_COLOR -from ..utils.decorators import memoized -from ..exceptions import YellowbrickValueError -from ..bestfit import draw_best_fit, draw_identity_line - +from yellowbrick.draw import manual_legend +from yellowbrick.utils.decorators import memoized +from yellowbrick.style.palettes import LINE_COLOR +from yellowbrick.exceptions import YellowbrickValueError +from yellowbrick.regressor.base import RegressionScoreVisualizer +from yellowbrick.bestfit import draw_best_fit, draw_identity_line ## Packages for export -__all__ = [ - "PredictionError", "prediction_error", - "ResidualsPlot", "residuals_plot" -] +__all__ = ["PredictionError", "prediction_error", "ResidualsPlot", "residuals_plot"] ########################################################################## ## Prediction Error Plots ########################################################################## + class PredictionError(RegressionScoreVisualizer): """ The prediction error visualizer plots the actual targets from the dataset against the predicted values generated by our model(s). This visualizer is - used to dectect noise or heteroscedasticity along a range of the target + used to detect noise or heteroscedasticity along a range of the target domain. Parameters @@ -61,6 +58,8 @@ class PredictionError(RegressionScoreVisualizer): model : a Scikit-Learn regressor Should be an instance of a regressor, otherwise will raise a YellowbrickTypeError exception on instantiation. + If the estimator is not fitted, it is fit when the visualizer is fitted, + unless otherwise specified by ``is_fitted``. ax : matplotlib Axes, default: None The axes to plot the figure on. If None is passed in the current axes @@ -79,7 +78,7 @@ class PredictionError(RegressionScoreVisualizer): predicted and measured value of the target variable. The color of the bestfit line is determined by the ``line_color`` argument. - identity: bool, default: True + identity : bool, default: True Draw the 45 degree identity line, y=x in order to better show the relationship or pattern of the residuals. E.g. to estimate if the model is over- or under- estimating the given values. The color of the @@ -95,10 +94,23 @@ class PredictionError(RegressionScoreVisualizer): Specify a transparency where 1 is completely opaque and 0 is completely transparent. This property makes densely clustered points more visible. + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ---------- + + score_ : float + The R^2 score that specifies the goodness of fit of the underlying + regression model to the test data. + Examples -------- @@ -116,15 +128,27 @@ class PredictionError(RegressionScoreVisualizer): its primary entry point is the `score()` method. """ - def __init__(self, model, ax=None, shared_limits=True, - bestfit=True, identity=True, alpha=0.75, **kwargs): + def __init__( + self, + model, + ax=None, + shared_limits=True, + bestfit=True, + identity=True, + alpha=0.75, + is_fitted="auto", + **kwargs + ): + # Whether or not to check if the model is already fitted + self.is_fitted = is_fitted + # Initialize the visualizer super(PredictionError, self).__init__(model, ax=ax, **kwargs) # Visual arguments self.colors = { - 'point': kwargs.pop('point_color', None), - 'line': kwargs.pop('line_color', LINE_COLOR), + "point": kwargs.pop("point_color", None), + "line": kwargs.pop("line_color", LINE_COLOR), } # Drawing arguments @@ -153,7 +177,8 @@ def score(self, X, y=None, **kwargs): ------- score : float """ - self.score_ = self.estimator.score(X, y, **kwargs) + # super will set score_ on the visualizer + super(PredictionError, self).score(X, y, **kwargs) y_pred = self.predict(X) self.draw(y, y_pred) @@ -171,30 +196,34 @@ def draw(self, y, y_pred): An array or series of predicted target values Returns - ------ - ax : the axis with the plotted figure + ------- + ax : matplotlib Axes + The axis with the plotted figure """ label = "$R^2 = {:0.3f}$".format(self.score_) self.ax.scatter( - y, - y_pred, - c=self.colors['point'], - alpha=self.alpha, - label=label) + y, y_pred, c=self.colors["point"], alpha=self.alpha, label=label + ) - # TODO If score is happening inside a loop, draw would get called multiple times. + # TODO If score happens inside a loop, draw gets called multiple times. # Ideally we'd want the best fit line to be drawn only once if self.bestfit: draw_best_fit( - y, y_pred, self.ax, 'linear', ls='--', lw=2, - c=self.colors['line'], label='best fit' + y, + y_pred, + self.ax, + "linear", + ls="--", + lw=2, + c=self.colors["line"], + label="best fit", ) # Set the axes limits based on the range of X and Y data # NOTE: shared_limits will be accounted for in finalize() # TODO: do better than add one for really small residuals - self.ax.set_xlim(y.min()-1, y.max()+1) - self.ax.set_ylim(y_pred.min()-1, y_pred.max()+1) + self.ax.set_xlim(y.min() - 1, y.max() + 1) + self.ax.set_ylim(y_pred.min() - 1, y_pred.max() + 1) return self.ax @@ -208,9 +237,7 @@ def finalize(self, **kwargs): kwargs: generic keyword arguments. """ # Set the title on the plot - self.set_title( - 'Prediction Error for {}'.format(self.name) - ) + self.set_title("Prediction Error for {}".format(self.name)) # Square the axes to ensure a 45 degree line if self.shared_limits: @@ -219,28 +246,29 @@ def finalize(self, **kwargs): xlim = self.ax.get_xlim() # Find the range that captures all data - bounds = ( - min(ylim[0], xlim[0]), - max(ylim[1], xlim[1]), - ) + bounds = (min(ylim[0], xlim[0]), max(ylim[1], xlim[1])) # Reset the limits self.ax.set_xlim(bounds) self.ax.set_ylim(bounds) # Ensure the aspect ratio is square - self.ax.set_aspect('equal', adjustable='box') + self.ax.set_aspect("equal", adjustable="box") # Draw the 45 degree line if self.identity: draw_identity_line( - ax=self.ax, ls='--', lw=2, c=self.colors['line'], - alpha=0.5, label="identity" + ax=self.ax, + ls="--", + lw=2, + c=self.colors["line"], + alpha=0.5, + label="identity", ) # Set the axes labels - self.ax.set_ylabel(r'$\hat{y}$') - self.ax.set_xlabel(r'$y$') + self.ax.set_ylabel(r"$\hat{y}$") + self.ax.set_xlabel(r"$y$") # Set the legend # Note: it would be nice to be able to use the manual_legend utility @@ -249,11 +277,11 @@ def finalize(self, **kwargs): # bit tricky because adding a manual legend here would override the # best fit and 45 degree line legend components. In particular, the # best fit is plotted in draw because it depends on y and y_pred. - self.ax.legend(loc='best', frameon=True) + self.ax.legend(loc="best", frameon=True) -def prediction_error(model, X, y=None, ax=None, alpha=0.75, **kwargs): - """ - Quick method: + +def prediction_error(model, X, y=None, ax=None, alpha=0.75, is_fitted="auto", **kwargs): + """Quickly plot a prediction error visualizer Plot the actual targets from the dataset against the predicted values generated by our model(s). @@ -264,6 +292,10 @@ def prediction_error(model, X, y=None, ax=None, alpha=0.75, **kwargs): Parameters ---------- model : the Scikit-Learn estimator (should be a regressor) + Should be an instance of a regressor, otherwise will raise a + YellowbrickTypeError exception on instantiation. + If the estimator is not fitted, it is fit when the visualizer is fitted, + unless otherwise specified by ``is_fitted``. X : ndarray or DataFrame of shape n x m A matrix of n instances with m features. @@ -282,7 +314,7 @@ def prediction_error(model, X, y=None, ax=None, alpha=0.75, **kwargs): shared_limits to False, but note that this will distort the figure and should be accounted for during analysis. - besfit : bool, default: True + bestfit : bool, default: True Draw a linear best fit line to estimate the correlation between the predicted and measured value of the target variable. The color of the bestfit line is determined by the ``line_color`` argument. @@ -303,6 +335,12 @@ def prediction_error(model, X, y=None, ax=None, alpha=0.75, **kwargs): Specify a transparency where 1 is completely opaque and 0 is completely transparent. This property makes densely clustered points more visible. + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. @@ -313,7 +351,7 @@ def prediction_error(model, X, y=None, ax=None, alpha=0.75, **kwargs): Returns the axes that the prediction error plot was drawn on. """ # Instantiate the visualizer - visualizer = PredictionError(model, ax, alpha=alpha, **kwargs) + visualizer = PredictionError(model, ax, alpha=alpha, is_fitted=is_fitted, **kwargs) # Create the train and test splits X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) @@ -324,13 +362,14 @@ def prediction_error(model, X, y=None, ax=None, alpha=0.75, **kwargs): visualizer.finalize() # Return the axes object on the visualizer - return visualizer.ax + return visualizer ########################################################################## ## Residuals Plots ########################################################################## + class ResidualsPlot(RegressionScoreVisualizer): """ A residual plot shows the residuals on the vertical axis and the @@ -345,6 +384,8 @@ class ResidualsPlot(RegressionScoreVisualizer): model : a Scikit-Learn regressor Should be an instance of a regressor, otherwise will raise a YellowbrickTypeError exception on instantiation. + If the estimator is not fitted, it is fit when the visualizer is fitted, + unless otherwise specified by ``is_fitted``. ax : matplotlib Axes, default: None The axes to plot the figure on. If None is passed in the current axes @@ -370,14 +411,37 @@ class ResidualsPlot(RegressionScoreVisualizer): line_color : color, default: dark grey Defines the color of the zero error line, can be any matplotlib color. - alpha : float, default: 0.75 - Specify a transparency where 1 is completely opaque and 0 is completely - transparent. This property makes densely clustered points more visible. + train_alpha : float, default: 0.75 + Specify a transparency for traininig data, where 1 is completely opaque + and 0 is completely transparent. This property makes densely clustered + points more visible. + + test_alpha : float, default: 0.75 + Specify a transparency for test data, where 1 is completely opaque + and 0 is completely transparent. This property makes densely clustered + points more visible. + + is_fitted : bool or str, default='auto' + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If 'auto' (default), a helper method will check if the estimator + is fitted before fitting it again. kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Attributes + ---------- + + train_score_ : float + The R^2 score that specifies the goodness of fit of the underlying + regression model to the training data. + + test_score_ : float + The R^2 score that specifies the goodness of fit of the underlying + regression model to the test data. + Examples -------- @@ -395,34 +459,48 @@ class ResidualsPlot(RegressionScoreVisualizer): The residuals histogram feature requires matplotlib 2.0.2 or greater. """ - def __init__(self, model, ax=None, hist=True, train_color='b', - test_color='g', line_color=LINE_COLOR, alpha=0.75, - **kwargs): + def __init__( + self, + model, + ax=None, + hist=True, + train_color="b", + test_color="g", + line_color=LINE_COLOR, + train_alpha=0.75, + test_alpha=0.75, + is_fitted="auto", + **kwargs + ): + # Whether or not to check if the model is already fitted + self.is_fitted = is_fitted + + # Initialize the visualizer base super(ResidualsPlot, self).__init__(model, ax=ax, **kwargs) # TODO: allow more scatter plot arguments for train and test points # See #475 (RE: ScatterPlotMixin) self.colors = { - 'train_point': train_color, - 'test_point': test_color, - 'line': line_color, + "train_point": train_color, + "test_point": test_color, + "line": line_color, } self.hist = hist - if self.hist not in {True, 'density', 'frequency', None, False}: - raise YellowbrickValueError( - "'{}' is an invalid argument for hist, use None, True, " \ - "False, 'density', or 'frequency'".format(hist) - ) + if self.hist not in {True, "density", "frequency", None, False}: + raise YellowbrickValueError( + "'{}' is an invalid argument for hist, use None, True, " + "False, 'density', or 'frequency'".format(hist) + ) - if self.hist in {True, 'density', 'frequency'}: - self.hax # If hist is True, test the version availability + if self.hist in {True, "density", "frequency"}: + self.hax # If hist is True, test the version availability # Store labels and colors for the legend ordered by call self._labels, self._colors = [], [] - self.alpha = alpha + self.alphas = {"train_point": train_alpha, "test_point": test_alpha} @memoized def hax(self): @@ -430,16 +508,18 @@ def hax(self): Returns the histogram axes, creating it only on demand. """ if make_axes_locatable is None: - raise YellowbrickValueError(( - "residuals histogram requires matplotlib 2.0.2 or greater " - "please upgrade matplotlib or set hist=False on the visualizer" - )) + raise YellowbrickValueError( + ( + "residuals histogram requires matplotlib 2.0.2 or greater " + "please upgrade matplotlib or set hist=False on the visualizer" + ) + ) divider = make_axes_locatable(self.ax) hax = divider.append_axes("right", size=1, pad=0.1, sharey=self.ax) hax.yaxis.tick_right() - hax.grid(False, axis='x') + hax.grid(False, axis="x") return hax @@ -457,8 +537,10 @@ def fit(self, X, y, **kwargs): Returns ------- - self : visualizer instance + self : ResidualsPlot + The visualizer instance """ + # fit the underlying model to the data super(ResidualsPlot, self).fit(X, y, **kwargs) self.score(X, y, train=True) return self @@ -482,11 +564,12 @@ def score(self, X, y=None, train=False, **kwargs): are the train data. Returns - ------ + ------- score : float The score of the underlying estimator, usually the R-squared score for regression estimators. """ + # Do not call super in order to differentiate train and test scores. score = self.estimator.score(X, y, **kwargs) if train: self.train_score_ = score @@ -494,8 +577,8 @@ def score(self, X, y=None, train=False, **kwargs): self.test_score_ = score y_pred = self.predict(X) - scores = y_pred - y - self.draw(y_pred, scores, train=train) + residuals = y_pred - y + self.draw(y_pred, residuals, train=train) return score @@ -521,30 +604,31 @@ def draw(self, y_pred, residuals, train=False, **kwargs): are the train data. Returns - ------ - ax : the axis with the plotted figure + ------- + ax : matplotlib Axes + The axis with the plotted figure """ if train: - color = self.colors['train_point'] + color = self.colors["train_point"] label = "Train $R^2 = {:0.3f}$".format(self.train_score_) + alpha = self.alphas["train_point"] else: - color = self.colors['test_point'] + color = self.colors["test_point"] label = "Test $R^2 = {:0.3f}$".format(self.test_score_) + alpha = self.alphas["test_point"] # Update the legend information self._labels.append(label) self._colors.append(color) # Draw the residuals scatter plot - self.ax.scatter( - y_pred, residuals, c=color, alpha=self.alpha, label=label - ) + self.ax.scatter(y_pred, residuals, c=color, alpha=alpha, label=label) # Add residuals histogram - if self.hist in {True, 'frequency'}: + if self.hist in {True, "frequency"}: self.hax.hist(residuals, bins=50, orientation="horizontal", color=color) - elif self.hist == 'density': + elif self.hist == "density": self.hax.hist( residuals, bins=50, orientation="horizontal", density=True, color=color ) @@ -563,38 +647,45 @@ def finalize(self, **kwargs): kwargs: generic keyword arguments. """ # Add the title to the plot - self.set_title('Residuals for {} Model'.format(self.name)) + self.set_title("Residuals for {} Model".format(self.name)) # Set the legend with full opacity patches using manual legend - manual_legend( - self, self._labels, self._colors, loc='best', frameon=True - ) + manual_legend(self, self._labels, self._colors, loc="best", frameon=True) # Create a full line across the figure at zero error. - self.ax.axhline(y=0, c=self.colors['line']) + self.ax.axhline(y=0, c=self.colors["line"]) # Set the axes labels - self.ax.set_ylabel('Residuals') + self.ax.set_ylabel("Residuals") self.ax.set_xlabel("Predicted Value") # Finalize the histogram axes if self.hist: - self.hax.axhline(y=0, c=self.colors['line']) + self.hax.axhline(y=0, c=self.colors["line"]) self.hax.set_xlabel("Distribution") -def residuals_plot(model, - X, - y, - ax=None, - hist=True, - test_size=0.25, - train_color='b', - test_color='g', - line_color=LINE_COLOR, - random_state=None, - alpha=0.75, - **kwargs): +########################################################################## +## Quick Method +########################################################################## + + +def residuals_plot( + model, + X, + y, + ax=None, + hist=True, + test_size=0.25, + train_color="b", + test_color="g", + line_color=LINE_COLOR, + random_state=None, + train_alpha=0.75, + test_alpha=0.75, + is_fitted="auto", + **kwargs +): """Quick method: Divides the dataset X, y into a train and test split (the size of the @@ -609,6 +700,8 @@ def residuals_plot(model, model : a Scikit-Learn regressor Should be an instance of a regressor, otherwise will raise a YellowbrickTypeError exception on instantiation. + If the estimator is not fitted, it is fit when the visualizer is fitted, + unless otherwise specified by ``is_fitted``. X : ndarray or DataFrame of shape n x m A matrix of n instances with m features. @@ -648,9 +741,21 @@ def residuals_plot(model, random_state : int, RandomState instance or None, optional Passed to the train_test_split function. - alpha : float, default: 0.75 - Specify a transparency where 1 is completely opaque and 0 is completely - transparent. This property makes densely clustered points more visible. + train_alpha : float, default: 0.75 + Specify a transparency for traininig data, where 1 is completely opaque + and 0 is completely transparent. This property makes densely clustered + points more visible. + + test_alpha : float, default: 0.75 + Specify a transparency for test data, where 1 is completely opaque and + 0 is completely transparent. This property makes densely clustered + points more visible. + + is_fitted : bool or str, default="auto" + Specify if the wrapped estimator is already fitted. If False, the estimator + will be fit when the visualizer is fit, otherwise, the estimator will not be + modified. If "auto" (default), a helper method will check if the estimator + is fitted before fitting it again. kwargs : dict Keyword arguments that are passed to the base class and may influence @@ -658,13 +763,21 @@ def residuals_plot(model, Returns ------- - ax : matplotlib axes - Returns the axes that the residuals plot was drawn on. + visualizer : ResidualsPlot + Returns the residuals plot visualizer """ # Instantiate the visualizer + visualizer = ResidualsPlot( - model=model, ax=ax, hist=hist, train_color=train_color, - test_color=test_color, line_color=line_color, alpha=alpha, + model=model, + ax=ax, + hist=hist, + train_color=train_color, + test_color=test_color, + line_color=line_color, + train_alpha=train_alpha, + test_alpha=test_alpha, + is_fitted=is_fitted, **kwargs ) @@ -678,5 +791,5 @@ def residuals_plot(model, visualizer.score(X_test, y_test) visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/style/__init__.py b/yellowbrick/style/__init__.py index 0b2045e71..e50917689 100644 --- a/yellowbrick/style/__init__.py +++ b/yellowbrick/style/__init__.py @@ -1,10 +1,10 @@ # yellowbrick.style # Manage the style and aesthetic of the yellowbrick library. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Tue Oct 04 15:09:48 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [c6aff34] benjamin@bengfort.com $ diff --git a/yellowbrick/style/colors.py b/yellowbrick/style/colors.py index d495947fe..a28590972 100644 --- a/yellowbrick/style/colors.py +++ b/yellowbrick/style/colors.py @@ -1,10 +1,10 @@ # yellowbrick.colors # Colors and color helpers brought in from a different library. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri Jun 24 17:02:53 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: colors.py [c6aff34] benjamin@bengfort.com $ @@ -25,12 +25,13 @@ import matplotlib.cm as cm from copy import copy -from six import string_types + from yellowbrick.exceptions import YellowbrickValueError # Check to see if matplotlib is at least sorta up to date from distutils.version import LooseVersion + mpl_ge_150 = LooseVersion(mpl.__version__) >= "1.5.0" @@ -38,20 +39,21 @@ ## Color Utilities ########################################################################## + def get_color_cycle(): """ Returns the current color cycle from matplotlib. """ if mpl_ge_150: - cyl = mpl.rcParams['axes.prop_cycle'] + cyl = mpl.rcParams["axes.prop_cycle"] # matplotlib 1.5 verifies that axes.prop_cycle *is* a cycler # but no garuantee that there's a `color` key. # so users could have a custom rcParams w/ no color... try: - return [x['color'] for x in cyl] + return [x["color"] for x in cyl] except KeyError: pass # just return axes.color style below - return mpl.rcParams['axes.color_cycle'] + return mpl.rcParams["axes.color_cycle"] def resolve_colors(n_colors=None, colormap=None, colors=None): @@ -68,11 +70,12 @@ def resolve_colors(n_colors=None, colormap=None, colors=None): truncate or multiple the colors available. If None the length of the colors will not be modified. - colormap : str, default: None + colormap : str, yellowbrick.style.palettes.ColorPalette, matplotlib.cm, default: None The name of the matplotlib color map with which to generate colors. colors : iterable, default: None - A collection of colors to use specifically with the plot. + A collection of colors to use specifically with the plot. Overrides + colormap if both are specified. Returns ------- @@ -87,26 +90,58 @@ def resolve_colors(n_colors=None, colormap=None, colors=None): # Work with the colormap if specified and colors is not if colormap is not None and colors is None: - if isinstance(colormap, string_types): + # Must import here to avoid recursive import + from .palettes import PALETTES, ColorPalette + + if isinstance(colormap, str): try: - colormap = cm.get_cmap(colormap) + + # try to get colormap from PALETTES first + _colormap = PALETTES.get(colormap, None) + + if _colormap is None: + + colormap = cm.get_cmap(colormap) + n_colors = n_colors or len(get_color_cycle()) + _colors = list(map(colormap, np.linspace(0, 1, num=n_colors))) + + else: + + _colors = ColorPalette(_colormap).as_rgb() + n_colors = n_colors or len(_colors) + except ValueError as e: + raise YellowbrickValueError(e) + # if yellowbrick color palette is provided as colormap + elif isinstance(colormap, ColorPalette): + + _colors = colormap.as_rgb() + n_colors = n_colors or len(_colors) - n_colors = n_colors or len(get_color_cycle()) - _colors = list(map(colormap, np.linspace(0, 1, num=n_colors))) + # if matplotlib color palette is provided as colormap + elif isinstance(colormap, mpl.colors.Colormap): + n_colors = n_colors or len(get_color_cycle()) + _colors = list(map(colormap, np.linspace(0, 1, num=n_colors))) + else: + raise YellowbrickValueError( + "Colormap type {} is not recognized. Possible types are: {}".format( + type(colormap), + ", ".join( + ["yellowbrick.style.ColorPalette,", "matplotlib.cm,", "str"] + ), + ) + ) # Work with the color list elif colors is not None: # Warn if both colormap and colors is specified. if colormap is not None: - warnings.warn( - "both colormap and colors specified; using colors" - ) + warnings.warn("both colormap and colors specified; using colors") - _colors = list(colors) # Ensure colors is a list + _colors = list(colors) # Ensure colors is a list # Get the default colors else: @@ -114,9 +149,7 @@ def resolve_colors(n_colors=None, colormap=None, colors=None): # Truncate or multiple the color list according to the number of colors if n_colors is not None and len(_colors) != n_colors: - _colors = [ - _colors[idx % len(_colors)] for idx in np.arange(n_colors) - ] + _colors = [_colors[idx % len(_colors)] for idx in np.arange(n_colors)] return _colors @@ -126,7 +159,7 @@ class ColorMap(object): A helper for mapping categorical values to colors on demand. """ - def __init__(self, colors='flatui', shuffle=False): + def __init__(self, colors="flatui", shuffle=False): """ Specify either a list of colors or one of the color names. If shuffle is True then the colors will be shuffled randomly. @@ -146,7 +179,7 @@ def colors(self, value): """ Converts color strings into a color listing. """ - if isinstance(value, string_types): + if isinstance(value, str): # Must import here to avoid recursive import from .palettes import PALETTES diff --git a/yellowbrick/style/palettes.py b/yellowbrick/style/palettes.py index d4b4c11a9..a1d5a6b0e 100644 --- a/yellowbrick/style/palettes.py +++ b/yellowbrick/style/palettes.py @@ -1,13 +1,13 @@ # yellowbrick.style.palettes # Implements the variety of colors that yellowbrick allows access to by name. # -# Author: Patrick O'Melveny +# Author: Patrick O'Melveny +# Author: Benjamin Bengfort # -# Copyright (C) 2016 District Data Lab +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: palettes.py [] pvomelveny@gmail.com +# ID: palettes.py [c6aff34] benjamin@bengfort.com $ """ Implements the variety of colors that yellowbrick allows access to by name. @@ -20,16 +20,12 @@ ## Imports ######################################################################### -from __future__ import division - import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.colors as mplcol from itertools import cycle -from six import string_types -from six.moves import range from .colors import get_color_cycle from yellowbrick.exceptions import YellowbrickValueError @@ -45,8 +41,8 @@ ## Special, Named Colors ########################################################################## -YB_KEY = '#111111' # The yellowbrick key (black) color is very dark grey -LINE_COLOR = YB_KEY # Colors for best fit lines, diagonals, etc. +YB_KEY = "#111111" # The yellowbrick key (black) color is very dark grey +LINE_COLOR = YB_KEY # Colors for best fit lines, diagonals, etc. ########################################################################## @@ -58,310 +54,1294 @@ PALETTES = { # "name": ['blue', 'green', 'red', 'maroon', 'yellow', 'cyan'] # The yellowbrick default palette - "yellowbrick": ['#0272a2', '#9fc377', '#ca0b03', '#a50258', '#d7c703', '#88cada'], - + "yellowbrick": ["#0272a2", "#9fc377", "#ca0b03", "#a50258", "#d7c703", "#88cada"], # The following are from ColorBrewer - "accent": ['#386cb0', '#7fc97f', '#f0027f', '#beaed4', '#ffff99', '#fdc086'], - "dark": ['#7570b3', '#66a61e', '#d95f02', '#e7298a', '#e6ab02', '#1b9e77'], - "pastel": ['#cbd5e8', '#b3e2cd', '#fdcdac', '#f4cae4', '#fff2ae', '#e6f5c9'], - "bold": ['#377eb8', '#4daf4a', '#e41a1c', '#984ea3', '#ffff33', '#ff7f00'], - "muted": ['#80b1d3', '#8dd3c7', '#fb8072', '#bebada', '#ffffb3', '#fdb462'], - + "accent": ["#386cb0", "#7fc97f", "#f0027f", "#beaed4", "#ffff99", "#fdc086"], + "dark": ["#7570b3", "#66a61e", "#d95f02", "#e7298a", "#e6ab02", "#1b9e77"], + "pastel": ["#cbd5e8", "#b3e2cd", "#fdcdac", "#f4cae4", "#fff2ae", "#e6f5c9"], + "bold": ["#377eb8", "#4daf4a", "#e41a1c", "#984ea3", "#ffff33", "#ff7f00"], + "muted": ["#80b1d3", "#8dd3c7", "#fb8072", "#bebada", "#ffffb3", "#fdb462"], # The reset colors back to the original mpl color codes - "reset": ['#0000ff', '#008000', '#ff0000', '#bf00bf', '#bfbf00', '#00bfbf', '#000000'], - + "reset": [ + "#0000ff", + "#008000", + "#ff0000", + "#bf00bf", + "#bfbf00", + "#00bfbf", + "#000000", + ], # Colorblind colors - "colorblind": ["#0072B2", "#009E73", "#D55E00", "#CC79A7", "#F0E442", "#56B4E9"], - "sns_colorblind": ["#0072B2", "#009E73", "#D55E00", "#CC79A7", "#F0E442", "#56B4E9"], - + "colorblind": ["#0072B2", "#009E73", "#D55E00", "#CC79A7", "#F0E442", "#56B4E9"], + "sns_colorblind": [ + "#0072B2", + "#009E73", + "#D55E00", + "#CC79A7", + "#F0E442", + "#56B4E9", + ], # The following are Seaborn colors - "sns_deep": ["#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974", "#64B5CD"], - "sns_muted": ["#4878CF", "#6ACC65", "#D65F5F", "#B47CC7", "#C4AD66", "#77BEDB"], + "sns_deep": ["#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974", "#64B5CD"], + "sns_muted": ["#4878CF", "#6ACC65", "#D65F5F", "#B47CC7", "#C4AD66", "#77BEDB"], "sns_pastel": ["#92C6FF", "#97F0AA", "#FF9F9A", "#D0BBFF", "#FFFEA3", "#B0E0E6"], "sns_bright": ["#003FFF", "#03ED3A", "#E8000B", "#8A2BE2", "#FFC400", "#00D7FF"], - "sns_dark": ["#001C7F", "#017517", "#8C0900", "#7600A1", "#B8860B", "#006374"], - + "sns_dark": ["#001C7F", "#017517", "#8C0900", "#7600A1", "#B8860B", "#006374"], # Other palettes - "flatui": ["#34495e", "#2ecc71", "#e74c3c", "#9b59b6", "#f4d03f", "#3498db"], - - "paired": ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", - "#cab2d6", "#6a3d9a", "#ffff99", "#b15928", "#fdbf6f", "#ff7f00"], - - "set1": ["#377eb8", "#4daf4a", "#e41a1c", "#984ea3", "#ffff33", "#ff7f00", - "#a65628", "#f781bf", "#999999"], - + "flatui": ["#34495e", "#2ecc71", "#e74c3c", "#9b59b6", "#f4d03f", "#3498db"], + "paired": [ + "#a6cee3", + "#1f78b4", + "#b2df8a", + "#33a02c", + "#fb9a99", + "#e31a1c", + "#cab2d6", + "#6a3d9a", + "#ffff99", + "#b15928", + "#fdbf6f", + "#ff7f00", + ], + "set1": [ + "#377eb8", + "#4daf4a", + "#e41a1c", + "#984ea3", + "#ffff33", + "#ff7f00", + "#a65628", + "#f781bf", + "#999999", + ], # colors extracted from this blog post during pycon2017: # http://lewisandquark.tumblr.com/ - "neural_paint": ["#167192", "#6e7548", "#c5a2ab", "#00ccff", "#de78ae", "#ffcc99", - "#3d3f42", "#ffffcc"], + "neural_paint": [ + "#167192", + "#6e7548", + "#c5a2ab", + "#00ccff", + "#de78ae", + "#ffcc99", + "#3d3f42", + "#ffffcc", + ], } SEQUENCES = { "ddl_heat": { - 12: ['#DBDBDB', '#DCD5CC', '#DCCEBE', '#DDC8AF', '#DEC2A0', '#DEBB91', '#DFB583', '#DFAE74', '#E0A865', '#E1A256', '#E19B48', '#E29539'], + 12: [ + "#DBDBDB", + "#DCD5CC", + "#DCCEBE", + "#DDC8AF", + "#DEC2A0", + "#DEBB91", + "#DFB583", + "#DFAE74", + "#E0A865", + "#E1A256", + "#E19B48", + "#E29539", + ] }, "YlGn": { 3: ["#f7fcb9", "#addd8e", "#31a354"], 4: ["#ffffcc", "#c2e699", "#78c679", "#238443"], 5: ["#ffffcc", "#c2e699", "#78c679", "#31a354", "#006837"], 6: ["#ffffcc", "#d9f0a3", "#addd8e", "#78c679", "#31a354", "#006837"], - 7: ["#ffffcc", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#005a32"], - - 8: ["#ffffe5", "#f7fcb9", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#005a32"], - 9: ["#ffffe5", "#f7fcb9", "#d9f0a3", "#addd8e", "#78c679", "#41ab5d", "#238443", "#006837", "#004529"], + 7: [ + "#ffffcc", + "#d9f0a3", + "#addd8e", + "#78c679", + "#41ab5d", + "#238443", + "#005a32", + ], + 8: [ + "#ffffe5", + "#f7fcb9", + "#d9f0a3", + "#addd8e", + "#78c679", + "#41ab5d", + "#238443", + "#005a32", + ], + 9: [ + "#ffffe5", + "#f7fcb9", + "#d9f0a3", + "#addd8e", + "#78c679", + "#41ab5d", + "#238443", + "#006837", + "#004529", + ], }, "YlGnBu": { 3: ["#edf8b1", "#7fcdbb", "#2c7fb8"], 4: ["#ffffcc", "#a1dab4", "#41b6c4", "#225ea8"], 5: ["#ffffcc", "#a1dab4", "#41b6c4", "#2c7fb8", "#253494"], 6: ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#2c7fb8", "#253494"], - 7: ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"], - 8: ["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"], - 9: ["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#253494", "#081d58"], + 7: [ + "#ffffcc", + "#c7e9b4", + "#7fcdbb", + "#41b6c4", + "#1d91c0", + "#225ea8", + "#0c2c84", + ], + 8: [ + "#ffffd9", + "#edf8b1", + "#c7e9b4", + "#7fcdbb", + "#41b6c4", + "#1d91c0", + "#225ea8", + "#0c2c84", + ], + 9: [ + "#ffffd9", + "#edf8b1", + "#c7e9b4", + "#7fcdbb", + "#41b6c4", + "#1d91c0", + "#225ea8", + "#253494", + "#081d58", + ], }, "GnBu": { 3: ["#e0f3db", "#a8ddb5", "#43a2ca"], 4: ["#f0f9e8", "#bae4bc", "#7bccc4", "#2b8cbe"], 5: ["#f0f9e8", "#bae4bc", "#7bccc4", "#43a2ca", "#0868ac"], 6: ["#f0f9e8", "#ccebc5", "#a8ddb5", "#7bccc4", "#43a2ca", "#0868ac"], - 7: ["#f0f9e8", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#08589e"], - 8: ["#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#08589e"], - 9: ["#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081"], + 7: [ + "#f0f9e8", + "#ccebc5", + "#a8ddb5", + "#7bccc4", + "#4eb3d3", + "#2b8cbe", + "#08589e", + ], + 8: [ + "#f7fcf0", + "#e0f3db", + "#ccebc5", + "#a8ddb5", + "#7bccc4", + "#4eb3d3", + "#2b8cbe", + "#08589e", + ], + 9: [ + "#f7fcf0", + "#e0f3db", + "#ccebc5", + "#a8ddb5", + "#7bccc4", + "#4eb3d3", + "#2b8cbe", + "#0868ac", + "#084081", + ], }, "BuGn": { 3: ["#e5f5f9", "#99d8c9", "#2ca25f"], 4: ["#edf8fb", "#b2e2e2", "#66c2a4", "#238b45"], 5: ["#edf8fb", "#b2e2e2", "#66c2a4", "#2ca25f", "#006d2c"], 6: ["#edf8fb", "#ccece6", "#99d8c9", "#66c2a4", "#2ca25f", "#006d2c"], - 7: ["#edf8fb", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#005824"], - 8: ["#f7fcfd", "#e5f5f9", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#005824"], - 9: ["#f7fcfd", "#e5f5f9", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#006d2c", "#00441b"], + 7: [ + "#edf8fb", + "#ccece6", + "#99d8c9", + "#66c2a4", + "#41ae76", + "#238b45", + "#005824", + ], + 8: [ + "#f7fcfd", + "#e5f5f9", + "#ccece6", + "#99d8c9", + "#66c2a4", + "#41ae76", + "#238b45", + "#005824", + ], + 9: [ + "#f7fcfd", + "#e5f5f9", + "#ccece6", + "#99d8c9", + "#66c2a4", + "#41ae76", + "#238b45", + "#006d2c", + "#00441b", + ], }, "PuBuGn": { 3: ["#ece2f0", "#a6bddb", "#1c9099"], 4: ["#f6eff7", "#bdc9e1", "#67a9cf", "#02818a"], 5: ["#f6eff7", "#bdc9e1", "#67a9cf", "#1c9099", "#016c59"], 6: ["#f6eff7", "#d0d1e6", "#a6bddb", "#67a9cf", "#1c9099", "#016c59"], - 7: ["#f6eff7", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016450"], - 8: ["#fff7fb", "#ece2f0", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016450"], - 9: ["#fff7fb", "#ece2f0", "#d0d1e6", "#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016c59", "#014636"], + 7: [ + "#f6eff7", + "#d0d1e6", + "#a6bddb", + "#67a9cf", + "#3690c0", + "#02818a", + "#016450", + ], + 8: [ + "#fff7fb", + "#ece2f0", + "#d0d1e6", + "#a6bddb", + "#67a9cf", + "#3690c0", + "#02818a", + "#016450", + ], + 9: [ + "#fff7fb", + "#ece2f0", + "#d0d1e6", + "#a6bddb", + "#67a9cf", + "#3690c0", + "#02818a", + "#016c59", + "#014636", + ], }, "PuBu": { 3: ["#ece7f2", "#a6bddb", "#2b8cbe"], 4: ["#f1eef6", "#bdc9e1", "#74a9cf", "#0570b0"], 5: ["#f1eef6", "#bdc9e1", "#74a9cf", "#2b8cbe", "#045a8d"], 6: ["#f1eef6", "#d0d1e6", "#a6bddb", "#74a9cf", "#2b8cbe", "#045a8d"], - 7: ["#f1eef6", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#034e7b"], - 8: ["#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#034e7b"], - 9: ["#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0", "#0570b0", "#045a8d", "#023858"], + 7: [ + "#f1eef6", + "#d0d1e6", + "#a6bddb", + "#74a9cf", + "#3690c0", + "#0570b0", + "#034e7b", + ], + 8: [ + "#fff7fb", + "#ece7f2", + "#d0d1e6", + "#a6bddb", + "#74a9cf", + "#3690c0", + "#0570b0", + "#034e7b", + ], + 9: [ + "#fff7fb", + "#ece7f2", + "#d0d1e6", + "#a6bddb", + "#74a9cf", + "#3690c0", + "#0570b0", + "#045a8d", + "#023858", + ], }, "BuPu": { 3: ["#e0ecf4", "#9ebcda", "#8856a7"], 4: ["#edf8fb", "#b3cde3", "#8c96c6", "#88419d"], 5: ["#edf8fb", "#b3cde3", "#8c96c6", "#8856a7", "#810f7c"], 6: ["#edf8fb", "#bfd3e6", "#9ebcda", "#8c96c6", "#8856a7", "#810f7c"], - 7: ["#edf8fb", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#6e016b"], - 8: ["#f7fcfd", "#e0ecf4", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#6e016b"], - 9: ["#f7fcfd", "#e0ecf4", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#810f7c", "#4d004b"], + 7: [ + "#edf8fb", + "#bfd3e6", + "#9ebcda", + "#8c96c6", + "#8c6bb1", + "#88419d", + "#6e016b", + ], + 8: [ + "#f7fcfd", + "#e0ecf4", + "#bfd3e6", + "#9ebcda", + "#8c96c6", + "#8c6bb1", + "#88419d", + "#6e016b", + ], + 9: [ + "#f7fcfd", + "#e0ecf4", + "#bfd3e6", + "#9ebcda", + "#8c96c6", + "#8c6bb1", + "#88419d", + "#810f7c", + "#4d004b", + ], }, "RdPu": { 3: ["#fde0dd", "#fa9fb5", "#c51b8a"], 4: ["#feebe2", "#fbb4b9", "#f768a1", "#ae017e"], 5: ["#feebe2", "#fbb4b9", "#f768a1", "#c51b8a", "#7a0177"], 6: ["#feebe2", "#fcc5c0", "#fa9fb5", "#f768a1", "#c51b8a", "#7a0177"], - 7: ["#feebe2", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177"], - 8: ["#fff7f3", "#fde0dd", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177"], - 9: ["#fff7f3", "#fde0dd", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497", "#ae017e", "#7a0177", "#49006a"], + 7: [ + "#feebe2", + "#fcc5c0", + "#fa9fb5", + "#f768a1", + "#dd3497", + "#ae017e", + "#7a0177", + ], + 8: [ + "#fff7f3", + "#fde0dd", + "#fcc5c0", + "#fa9fb5", + "#f768a1", + "#dd3497", + "#ae017e", + "#7a0177", + ], + 9: [ + "#fff7f3", + "#fde0dd", + "#fcc5c0", + "#fa9fb5", + "#f768a1", + "#dd3497", + "#ae017e", + "#7a0177", + "#49006a", + ], }, "PuRd": { 3: ["#e7e1ef", "#c994c7", "#dd1c77"], 4: ["#f1eef6", "#d7b5d8", "#df65b0", "#ce1256"], 5: ["#f1eef6", "#d7b5d8", "#df65b0", "#dd1c77", "#980043"], 6: ["#f1eef6", "#d4b9da", "#c994c7", "#df65b0", "#dd1c77", "#980043"], - 7: ["#f1eef6", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#91003f"], - 8: ["#f7f4f9", "#e7e1ef", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#91003f"], - 9: ["#f7f4f9", "#e7e1ef", "#d4b9da", "#c994c7", "#df65b0", "#e7298a", "#ce1256", "#980043", "#67001f"], + 7: [ + "#f1eef6", + "#d4b9da", + "#c994c7", + "#df65b0", + "#e7298a", + "#ce1256", + "#91003f", + ], + 8: [ + "#f7f4f9", + "#e7e1ef", + "#d4b9da", + "#c994c7", + "#df65b0", + "#e7298a", + "#ce1256", + "#91003f", + ], + 9: [ + "#f7f4f9", + "#e7e1ef", + "#d4b9da", + "#c994c7", + "#df65b0", + "#e7298a", + "#ce1256", + "#980043", + "#67001f", + ], }, "OrRd": { 3: ["#fee8c8", "#fdbb84", "#e34a33"], 4: ["#fef0d9", "#fdcc8a", "#fc8d59", "#d7301f"], 5: ["#fef0d9", "#fdcc8a", "#fc8d59", "#e34a33", "#b30000"], 6: ["#fef0d9", "#fdd49e", "#fdbb84", "#fc8d59", "#e34a33", "#b30000"], - 7: ["#fef0d9", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"], - 8: ["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"], - 9: ["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"], + 7: [ + "#fef0d9", + "#fdd49e", + "#fdbb84", + "#fc8d59", + "#ef6548", + "#d7301f", + "#990000", + ], + 8: [ + "#fff7ec", + "#fee8c8", + "#fdd49e", + "#fdbb84", + "#fc8d59", + "#ef6548", + "#d7301f", + "#990000", + ], + 9: [ + "#fff7ec", + "#fee8c8", + "#fdd49e", + "#fdbb84", + "#fc8d59", + "#ef6548", + "#d7301f", + "#b30000", + "#7f0000", + ], }, "YlOrRd": { 3: ["#ffeda0", "#feb24c", "#f03b20"], 4: ["#ffffb2", "#fecc5c", "#fd8d3c", "#e31a1c"], 5: ["#ffffb2", "#fecc5c", "#fd8d3c", "#f03b20", "#bd0026"], 6: ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#f03b20", "#bd0026"], - 7: ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"], - 8: ["#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"], - 9: ["#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#bd0026", "#800026"], + 7: [ + "#ffffb2", + "#fed976", + "#feb24c", + "#fd8d3c", + "#fc4e2a", + "#e31a1c", + "#b10026", + ], + 8: [ + "#ffffcc", + "#ffeda0", + "#fed976", + "#feb24c", + "#fd8d3c", + "#fc4e2a", + "#e31a1c", + "#b10026", + ], + 9: [ + "#ffffcc", + "#ffeda0", + "#fed976", + "#feb24c", + "#fd8d3c", + "#fc4e2a", + "#e31a1c", + "#bd0026", + "#800026", + ], }, "YlOrBr": { 3: ["#fff7bc", "#fec44f", "#d95f0e"], 4: ["#ffffd4", "#fed98e", "#fe9929", "#cc4c02"], 5: ["#ffffd4", "#fed98e", "#fe9929", "#d95f0e", "#993404"], 6: ["#ffffd4", "#fee391", "#fec44f", "#fe9929", "#d95f0e", "#993404"], - 7: ["#ffffd4", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#8c2d04"], - 8: ["#ffffe5", "#fff7bc", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#8c2d04"], - 9: ["#ffffe5", "#fff7bc", "#fee391", "#fec44f", "#fe9929", "#ec7014", "#cc4c02", "#993404", "#662506"], + 7: [ + "#ffffd4", + "#fee391", + "#fec44f", + "#fe9929", + "#ec7014", + "#cc4c02", + "#8c2d04", + ], + 8: [ + "#ffffe5", + "#fff7bc", + "#fee391", + "#fec44f", + "#fe9929", + "#ec7014", + "#cc4c02", + "#8c2d04", + ], + 9: [ + "#ffffe5", + "#fff7bc", + "#fee391", + "#fec44f", + "#fe9929", + "#ec7014", + "#cc4c02", + "#993404", + "#662506", + ], }, "Purples": { 3: ["#efedf5", "#bcbddc", "#756bb1"], 4: ["#f2f0f7", "#cbc9e2", "#9e9ac8", "#6a51a3"], 5: ["#f2f0f7", "#cbc9e2", "#9e9ac8", "#756bb1", "#54278f"], 6: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"], - 7: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#4a1486"], - 8: ["#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#4a1486"], - 9: ["#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3", "#54278f", "#3f007d"], + 7: [ + "#f2f0f7", + "#dadaeb", + "#bcbddc", + "#9e9ac8", + "#807dba", + "#6a51a3", + "#4a1486", + ], + 8: [ + "#fcfbfd", + "#efedf5", + "#dadaeb", + "#bcbddc", + "#9e9ac8", + "#807dba", + "#6a51a3", + "#4a1486", + ], + 9: [ + "#fcfbfd", + "#efedf5", + "#dadaeb", + "#bcbddc", + "#9e9ac8", + "#807dba", + "#6a51a3", + "#54278f", + "#3f007d", + ], }, "Blues": { 3: ["#deebf7", "#9ecae1", "#3182bd"], 4: ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"], 5: ["#eff3ff", "#bdd7e7", "#6baed6", "#3182bd", "#08519c"], 6: ["#eff3ff", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"], - 7: ["#eff3ff", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#084594"], - 8: ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#084594"], - 9: ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#08519c", "#08306b"], + 7: [ + "#eff3ff", + "#c6dbef", + "#9ecae1", + "#6baed6", + "#4292c6", + "#2171b5", + "#084594", + ], + 8: [ + "#f7fbff", + "#deebf7", + "#c6dbef", + "#9ecae1", + "#6baed6", + "#4292c6", + "#2171b5", + "#084594", + ], + 9: [ + "#f7fbff", + "#deebf7", + "#c6dbef", + "#9ecae1", + "#6baed6", + "#4292c6", + "#2171b5", + "#08519c", + "#08306b", + ], }, "Greens": { 3: ["#e5f5e0", "#a1d99b", "#31a354"], 4: ["#edf8e9", "#bae4b3", "#74c476", "#238b45"], 5: ["#edf8e9", "#bae4b3", "#74c476", "#31a354", "#006d2c"], 6: ["#edf8e9", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"], - 7: ["#edf8e9", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#005a32"], - 8: ["#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#005a32"], - 9: ["#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#006d2c", "#00441b"], + 7: [ + "#edf8e9", + "#c7e9c0", + "#a1d99b", + "#74c476", + "#41ab5d", + "#238b45", + "#005a32", + ], + 8: [ + "#f7fcf5", + "#e5f5e0", + "#c7e9c0", + "#a1d99b", + "#74c476", + "#41ab5d", + "#238b45", + "#005a32", + ], + 9: [ + "#f7fcf5", + "#e5f5e0", + "#c7e9c0", + "#a1d99b", + "#74c476", + "#41ab5d", + "#238b45", + "#006d2c", + "#00441b", + ], }, "Oranges": { 3: ["#fee6ce", "#fdae6b", "#e6550d"], 4: ["#feedde", "#fdbe85", "#fd8d3c", "#d94701"], 5: ["#feedde", "#fdbe85", "#fd8d3c", "#e6550d", "#a63603"], 6: ["#feedde", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"], - 7: ["#feedde", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#8c2d04"], - 8: ["#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#8c2d04"], - 9: ["#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#f16913", "#d94801", "#a63603", "#7f2704"], + 7: [ + "#feedde", + "#fdd0a2", + "#fdae6b", + "#fd8d3c", + "#f16913", + "#d94801", + "#8c2d04", + ], + 8: [ + "#fff5eb", + "#fee6ce", + "#fdd0a2", + "#fdae6b", + "#fd8d3c", + "#f16913", + "#d94801", + "#8c2d04", + ], + 9: [ + "#fff5eb", + "#fee6ce", + "#fdd0a2", + "#fdae6b", + "#fd8d3c", + "#f16913", + "#d94801", + "#a63603", + "#7f2704", + ], }, "Reds": { 3: ["#fee0d2", "#fc9272", "#de2d26"], 4: ["#fee5d9", "#fcae91", "#fb6a4a", "#cb181d"], 5: ["#fee5d9", "#fcae91", "#fb6a4a", "#de2d26", "#a50f15"], 6: ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#de2d26", "#a50f15"], - 7: ["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#99000d"], - 8: ["#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#99000d"], - 9: ["#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#a50f15", "#67000d"], + 7: [ + "#fee5d9", + "#fcbba1", + "#fc9272", + "#fb6a4a", + "#ef3b2c", + "#cb181d", + "#99000d", + ], + 8: [ + "#fff5f0", + "#fee0d2", + "#fcbba1", + "#fc9272", + "#fb6a4a", + "#ef3b2c", + "#cb181d", + "#99000d", + ], + 9: [ + "#fff5f0", + "#fee0d2", + "#fcbba1", + "#fc9272", + "#fb6a4a", + "#ef3b2c", + "#cb181d", + "#a50f15", + "#67000d", + ], }, "Greys": { 3: ["#f0f0f0", "#bdbdbd", "#636363"], 4: ["#f7f7f7", "#cccccc", "#969696", "#525252"], 5: ["#f7f7f7", "#cccccc", "#969696", "#636363", "#252525"], 6: ["#f7f7f7", "#d9d9d9", "#bdbdbd", "#969696", "#636363", "#252525"], - 7: ["#f7f7f7", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525"], - 8: ["#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525"], - 9: ["#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252", "#252525", "#000000"], + 7: [ + "#f7f7f7", + "#d9d9d9", + "#bdbdbd", + "#969696", + "#737373", + "#525252", + "#252525", + ], + 8: [ + "#ffffff", + "#f0f0f0", + "#d9d9d9", + "#bdbdbd", + "#969696", + "#737373", + "#525252", + "#252525", + ], + 9: [ + "#ffffff", + "#f0f0f0", + "#d9d9d9", + "#bdbdbd", + "#969696", + "#737373", + "#525252", + "#252525", + "#000000", + ], }, "PuOr": { 3: ["#f1a340", "#f7f7f7", "#998ec3"], 4: ["#e66101", "#fdb863", "#b2abd2", "#5e3c99"], 5: ["#e66101", "#fdb863", "#f7f7f7", "#b2abd2", "#5e3c99"], 6: ["#b35806", "#f1a340", "#fee0b6", "#d8daeb", "#998ec3", "#542788"], - 7: ["#b35806", "#f1a340", "#fee0b6", "#f7f7f7", "#d8daeb", "#998ec3", "#542788"], - 8: ["#b35806", "#e08214", "#fdb863", "#fee0b6", "#d8daeb", "#b2abd2", "#8073ac", "#542788"], - 9: ["#b35806", "#e08214", "#fdb863", "#fee0b6", "#f7f7f7", "#d8daeb", "#b2abd2", "#8073ac", "#542788"], - 10: ["#7f3b08", "#b35806", "#e08214", "#fdb863", "#fee0b6", "#d8daeb", "#b2abd2", "#8073ac", "#542788", "#2d004b"], - 11: ["#7f3b08", "#b35806", "#e08214", "#fdb863", "#fee0b6", "#f7f7f7", "#d8daeb", "#b2abd2", "#8073ac", "#542788", "#2d004b"], + 7: [ + "#b35806", + "#f1a340", + "#fee0b6", + "#f7f7f7", + "#d8daeb", + "#998ec3", + "#542788", + ], + 8: [ + "#b35806", + "#e08214", + "#fdb863", + "#fee0b6", + "#d8daeb", + "#b2abd2", + "#8073ac", + "#542788", + ], + 9: [ + "#b35806", + "#e08214", + "#fdb863", + "#fee0b6", + "#f7f7f7", + "#d8daeb", + "#b2abd2", + "#8073ac", + "#542788", + ], + 10: [ + "#7f3b08", + "#b35806", + "#e08214", + "#fdb863", + "#fee0b6", + "#d8daeb", + "#b2abd2", + "#8073ac", + "#542788", + "#2d004b", + ], + 11: [ + "#7f3b08", + "#b35806", + "#e08214", + "#fdb863", + "#fee0b6", + "#f7f7f7", + "#d8daeb", + "#b2abd2", + "#8073ac", + "#542788", + "#2d004b", + ], }, "BrBG": { 3: ["#d8b365", "#f5f5f5", "#5ab4ac"], 4: ["#a6611a", "#dfc27d", "#80cdc1", "#018571"], 5: ["#a6611a", "#dfc27d", "#f5f5f5", "#80cdc1", "#018571"], 6: ["#8c510a", "#d8b365", "#f6e8c3", "#c7eae5", "#5ab4ac", "#01665e"], - 7: ["#8c510a", "#d8b365", "#f6e8c3", "#f5f5f5", "#c7eae5", "#5ab4ac", "#01665e"], - 8: ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#c7eae5", "#80cdc1", "#35978f", "#01665e"], - 9: ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e"], - 10: ["#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"], - 11: ["#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"], + 7: [ + "#8c510a", + "#d8b365", + "#f6e8c3", + "#f5f5f5", + "#c7eae5", + "#5ab4ac", + "#01665e", + ], + 8: [ + "#8c510a", + "#bf812d", + "#dfc27d", + "#f6e8c3", + "#c7eae5", + "#80cdc1", + "#35978f", + "#01665e", + ], + 9: [ + "#8c510a", + "#bf812d", + "#dfc27d", + "#f6e8c3", + "#f5f5f5", + "#c7eae5", + "#80cdc1", + "#35978f", + "#01665e", + ], + 10: [ + "#543005", + "#8c510a", + "#bf812d", + "#dfc27d", + "#f6e8c3", + "#c7eae5", + "#80cdc1", + "#35978f", + "#01665e", + "#003c30", + ], + 11: [ + "#543005", + "#8c510a", + "#bf812d", + "#dfc27d", + "#f6e8c3", + "#f5f5f5", + "#c7eae5", + "#80cdc1", + "#35978f", + "#01665e", + "#003c30", + ], }, "PRGn": { 3: ["#af8dc3", "#f7f7f7", "#7fbf7b"], 4: ["#7b3294", "#c2a5cf", "#a6dba0", "#008837"], 5: ["#7b3294", "#c2a5cf", "#f7f7f7", "#a6dba0", "#008837"], 6: ["#762a83", "#af8dc3", "#e7d4e8", "#d9f0d3", "#7fbf7b", "#1b7837"], - 7: ["#762a83", "#af8dc3", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#7fbf7b", "#1b7837"], - 8: ["#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837"], - 9: ["#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837"], - 10: ["#40004b", "#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837", "#00441b"], - 11: ["#40004b", "#762a83", "#9970ab", "#c2a5cf", "#e7d4e8", "#f7f7f7", "#d9f0d3", "#a6dba0", "#5aae61", "#1b7837", "#00441b"], + 7: [ + "#762a83", + "#af8dc3", + "#e7d4e8", + "#f7f7f7", + "#d9f0d3", + "#7fbf7b", + "#1b7837", + ], + 8: [ + "#762a83", + "#9970ab", + "#c2a5cf", + "#e7d4e8", + "#d9f0d3", + "#a6dba0", + "#5aae61", + "#1b7837", + ], + 9: [ + "#762a83", + "#9970ab", + "#c2a5cf", + "#e7d4e8", + "#f7f7f7", + "#d9f0d3", + "#a6dba0", + "#5aae61", + "#1b7837", + ], + 10: [ + "#40004b", + "#762a83", + "#9970ab", + "#c2a5cf", + "#e7d4e8", + "#d9f0d3", + "#a6dba0", + "#5aae61", + "#1b7837", + "#00441b", + ], + 11: [ + "#40004b", + "#762a83", + "#9970ab", + "#c2a5cf", + "#e7d4e8", + "#f7f7f7", + "#d9f0d3", + "#a6dba0", + "#5aae61", + "#1b7837", + "#00441b", + ], }, "PiYG": { 3: ["#e9a3c9", "#f7f7f7", "#a1d76a"], 4: ["#d01c8b", "#f1b6da", "#b8e186", "#4dac26"], 5: ["#d01c8b", "#f1b6da", "#f7f7f7", "#b8e186", "#4dac26"], 6: ["#c51b7d", "#e9a3c9", "#fde0ef", "#e6f5d0", "#a1d76a", "#4d9221"], - 7: ["#c51b7d", "#e9a3c9", "#fde0ef", "#f7f7f7", "#e6f5d0", "#a1d76a", "#4d9221"], - 8: ["#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221"], - 9: ["#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221"], - 10: ["#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"], - 11: ["#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef", "#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"], + 7: [ + "#c51b7d", + "#e9a3c9", + "#fde0ef", + "#f7f7f7", + "#e6f5d0", + "#a1d76a", + "#4d9221", + ], + 8: [ + "#c51b7d", + "#de77ae", + "#f1b6da", + "#fde0ef", + "#e6f5d0", + "#b8e186", + "#7fbc41", + "#4d9221", + ], + 9: [ + "#c51b7d", + "#de77ae", + "#f1b6da", + "#fde0ef", + "#f7f7f7", + "#e6f5d0", + "#b8e186", + "#7fbc41", + "#4d9221", + ], + 10: [ + "#8e0152", + "#c51b7d", + "#de77ae", + "#f1b6da", + "#fde0ef", + "#e6f5d0", + "#b8e186", + "#7fbc41", + "#4d9221", + "#276419", + ], + 11: [ + "#8e0152", + "#c51b7d", + "#de77ae", + "#f1b6da", + "#fde0ef", + "#f7f7f7", + "#e6f5d0", + "#b8e186", + "#7fbc41", + "#4d9221", + "#276419", + ], }, "RdBu": { 3: ["#ef8a62", "#f7f7f7", "#67a9cf"], 4: ["#ca0020", "#f4a582", "#92c5de", "#0571b0"], 5: ["#ca0020", "#f4a582", "#f7f7f7", "#92c5de", "#0571b0"], 6: ["#b2182b", "#ef8a62", "#fddbc7", "#d1e5f0", "#67a9cf", "#2166ac"], - 7: ["#b2182b", "#ef8a62", "#fddbc7", "#f7f7f7", "#d1e5f0", "#67a9cf", "#2166ac"], - 8: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac"], - 9: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac"], - 10: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"], - 11: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"], + 7: [ + "#b2182b", + "#ef8a62", + "#fddbc7", + "#f7f7f7", + "#d1e5f0", + "#67a9cf", + "#2166ac", + ], + 8: [ + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#d1e5f0", + "#92c5de", + "#4393c3", + "#2166ac", + ], + 9: [ + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#f7f7f7", + "#d1e5f0", + "#92c5de", + "#4393c3", + "#2166ac", + ], + 10: [ + "#67001f", + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#d1e5f0", + "#92c5de", + "#4393c3", + "#2166ac", + "#053061", + ], + 11: [ + "#67001f", + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#f7f7f7", + "#d1e5f0", + "#92c5de", + "#4393c3", + "#2166ac", + "#053061", + ], }, "RdGy": { 3: ["#ef8a62", "#ffffff", "#999999"], 4: ["#ca0020", "#f4a582", "#bababa", "#404040"], 5: ["#ca0020", "#f4a582", "#ffffff", "#bababa", "#404040"], 6: ["#b2182b", "#ef8a62", "#fddbc7", "#e0e0e0", "#999999", "#4d4d4d"], - 7: ["#b2182b", "#ef8a62", "#fddbc7", "#ffffff", "#e0e0e0", "#999999", "#4d4d4d"], - 8: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#e0e0e0", "#bababa", "#878787", "#4d4d4d"], - 9: ["#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#ffffff", "#e0e0e0", "#bababa", "#878787", "#4d4d4d"], - 10: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#e0e0e0", "#bababa", "#878787", "#4d4d4d", "#1a1a1a"], - 11: ["#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7", "#ffffff", "#e0e0e0", "#bababa", "#878787", "#4d4d4d", "#1a1a1a"], + 7: [ + "#b2182b", + "#ef8a62", + "#fddbc7", + "#ffffff", + "#e0e0e0", + "#999999", + "#4d4d4d", + ], + 8: [ + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#e0e0e0", + "#bababa", + "#878787", + "#4d4d4d", + ], + 9: [ + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#ffffff", + "#e0e0e0", + "#bababa", + "#878787", + "#4d4d4d", + ], + 10: [ + "#67001f", + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#e0e0e0", + "#bababa", + "#878787", + "#4d4d4d", + "#1a1a1a", + ], + 11: [ + "#67001f", + "#b2182b", + "#d6604d", + "#f4a582", + "#fddbc7", + "#ffffff", + "#e0e0e0", + "#bababa", + "#878787", + "#4d4d4d", + "#1a1a1a", + ], }, "RdYlBu": { 3: ["#fc8d59", "#ffffbf", "#91bfdb"], 4: ["#d7191c", "#fdae61", "#abd9e9", "#2c7bb6"], 5: ["#d7191c", "#fdae61", "#ffffbf", "#abd9e9", "#2c7bb6"], 6: ["#d73027", "#fc8d59", "#fee090", "#e0f3f8", "#91bfdb", "#4575b4"], - 7: ["#d73027", "#fc8d59", "#fee090", "#ffffbf", "#e0f3f8", "#91bfdb", "#4575b4"], - 8: ["#d73027", "#f46d43", "#fdae61", "#fee090", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4"], - 9: ["#d73027", "#f46d43", "#fdae61", "#fee090", "#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4"], - 10: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee090", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"], - 11: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee090", "#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"], + 7: [ + "#d73027", + "#fc8d59", + "#fee090", + "#ffffbf", + "#e0f3f8", + "#91bfdb", + "#4575b4", + ], + 8: [ + "#d73027", + "#f46d43", + "#fdae61", + "#fee090", + "#e0f3f8", + "#abd9e9", + "#74add1", + "#4575b4", + ], + 9: [ + "#d73027", + "#f46d43", + "#fdae61", + "#fee090", + "#ffffbf", + "#e0f3f8", + "#abd9e9", + "#74add1", + "#4575b4", + ], + 10: [ + "#a50026", + "#d73027", + "#f46d43", + "#fdae61", + "#fee090", + "#e0f3f8", + "#abd9e9", + "#74add1", + "#4575b4", + "#313695", + ], + 11: [ + "#a50026", + "#d73027", + "#f46d43", + "#fdae61", + "#fee090", + "#ffffbf", + "#e0f3f8", + "#abd9e9", + "#74add1", + "#4575b4", + "#313695", + ], }, "Spectral": { 3: ["#fc8d59", "#ffffbf", "#99d594"], 4: ["#d7191c", "#fdae61", "#abdda4", "#2b83ba"], 5: ["#d7191c", "#fdae61", "#ffffbf", "#abdda4", "#2b83ba"], 6: ["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#99d594", "#3288bd"], - 7: ["#d53e4f", "#fc8d59", "#fee08b", "#ffffbf", "#e6f598", "#99d594", "#3288bd"], - 8: ["#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#e6f598", "#abdda4", "#66c2a5", "#3288bd"], - 9: ["#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd"], - 10: ["#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"], - 11: ["#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"], + 7: [ + "#d53e4f", + "#fc8d59", + "#fee08b", + "#ffffbf", + "#e6f598", + "#99d594", + "#3288bd", + ], + 8: [ + "#d53e4f", + "#f46d43", + "#fdae61", + "#fee08b", + "#e6f598", + "#abdda4", + "#66c2a5", + "#3288bd", + ], + 9: [ + "#d53e4f", + "#f46d43", + "#fdae61", + "#fee08b", + "#ffffbf", + "#e6f598", + "#abdda4", + "#66c2a5", + "#3288bd", + ], + 10: [ + "#9e0142", + "#d53e4f", + "#f46d43", + "#fdae61", + "#fee08b", + "#e6f598", + "#abdda4", + "#66c2a5", + "#3288bd", + "#5e4fa2", + ], + 11: [ + "#9e0142", + "#d53e4f", + "#f46d43", + "#fdae61", + "#fee08b", + "#ffffbf", + "#e6f598", + "#abdda4", + "#66c2a5", + "#3288bd", + "#5e4fa2", + ], }, "RdYlGn": { 3: ["#fc8d59", "#ffffbf", "#91cf60"], 4: ["#d7191c", "#fdae61", "#a6d96a", "#1a9641"], 5: ["#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641"], 6: ["#d73027", "#fc8d59", "#fee08b", "#d9ef8b", "#91cf60", "#1a9850"], - 7: ["#d73027", "#fc8d59", "#fee08b", "#ffffbf", "#d9ef8b", "#91cf60", "#1a9850"], - 8: ["#d73027", "#f46d43", "#fdae61", "#fee08b", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850"], - 9: ["#d73027", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850"], - 10: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"], - 11: ["#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b", "#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"], + 7: [ + "#d73027", + "#fc8d59", + "#fee08b", + "#ffffbf", + "#d9ef8b", + "#91cf60", + "#1a9850", + ], + 8: [ + "#d73027", + "#f46d43", + "#fdae61", + "#fee08b", + "#d9ef8b", + "#a6d96a", + "#66bd63", + "#1a9850", + ], + 9: [ + "#d73027", + "#f46d43", + "#fdae61", + "#fee08b", + "#ffffbf", + "#d9ef8b", + "#a6d96a", + "#66bd63", + "#1a9850", + ], + 10: [ + "#a50026", + "#d73027", + "#f46d43", + "#fdae61", + "#fee08b", + "#d9ef8b", + "#a6d96a", + "#66bd63", + "#1a9850", + "#006837", + ], + 11: [ + "#a50026", + "#d73027", + "#f46d43", + "#fdae61", + "#fee08b", + "#ffffbf", + "#d9ef8b", + "#a6d96a", + "#66bd63", + "#1a9850", + "#006837", + ], }, } @@ -376,6 +1356,7 @@ ## Palette Object ########################################################################## + class ColorPalette(list): """ A wrapper for functionality surrounding a list of colors, including a @@ -393,7 +1374,7 @@ def __init__(self, name_or_list): specify a palette name or a list of RGB or Hex values """ - if isinstance(name_or_list, string_types): + if isinstance(name_or_list, str): if name_or_list not in PALETTES: raise YellowbrickValueError( "'{}' is not a recognized palette!".format(name_or_list) @@ -408,6 +1389,7 @@ def __enter__(self): Open the context and assign the pallete to the mpl.rcParams """ from .rcmod import set_palette + self._orig_palette = color_palette() set_palette(self) return self @@ -417,6 +1399,7 @@ def __exit__(self, *args): Close the context and restore the original palette """ from .rcmod import set_palette + set_palette(self._orig_palette) def as_hex(self): @@ -446,11 +1429,14 @@ def plot(self, size=1): """ n = len(self) fig, ax = plt.subplots(1, 1, figsize=(n * size, size)) - ax.imshow(np.arange(n).reshape(1,n), - cmap=mpl.colors.ListedColormap(list(self)), - interpolation="nearest", aspect="auto") - ax.set_xticks(np.arange(n) - .5) - ax.set_yticks([-.5, .5]) + ax.imshow( + np.arange(n).reshape(1, n), + cmap=mpl.colors.ListedColormap(list(self)), + interpolation="nearest", + aspect="auto", + ) + ax.set_xticks(np.arange(n) - 0.5) + ax.set_yticks([-0.5, 0.5]) ax.set_xticklabels([]) ax.set_yticklabels([]) @@ -459,6 +1445,7 @@ def plot(self, size=1): ## Palette Functions ########################################################################## + def color_palette(palette=None, n_colors=None): """ Return a color palette object with color definition and handling. @@ -527,7 +1514,7 @@ def color_palette(palette=None, n_colors=None): if n_colors is None: n_colors = len(palette) - elif not isinstance(palette, string_types): + elif not isinstance(palette, str): if n_colors is None: n_colors = len(palette) @@ -576,9 +1563,7 @@ def set_color_codes(palette="accent"): """ if palette not in PALETTES: - raise YellowbrickValueError( - "'{}' is not a recognized palette!".format(palette) - ) + raise YellowbrickValueError("'{}' is not a recognized palette!".format(palette)) # Fetch the colors and adapt the length colors = PALETTES[palette] @@ -602,6 +1587,7 @@ def set_color_codes(palette="accent"): ## Sequence Functions ########################################################################## + def color_sequence(palette=None, n_colors=None): """ Return a `ListedColormap` object from a named sequence palette. Useful @@ -670,15 +1656,13 @@ def color_sequence(palette=None, n_colors=None): palette = palette or DEFAULT_SEQUENCE # Create a listed color map from the sequence - if not isinstance(palette, string_types): + if not isinstance(palette, str): return mplcol.ListedColormap(palette) # Otherwise perform a case-insensitive lookup sequences = {key.lower(): key for key in SEQUENCES.keys()} if palette.lower() not in sequences: - raise YellowbrickValueError( - "'{}' is not a recognized palette!".format(palette) - ) + raise YellowbrickValueError("'{}' is not a recognized palette!".format(palette)) # Collect the palette into the dictionary of lists. n_palettes = SEQUENCES[sequences[palette.lower()]] diff --git a/yellowbrick/style/rcmod.py b/yellowbrick/style/rcmod.py index 051e561e1..a41948d55 100644 --- a/yellowbrick/style/rcmod.py +++ b/yellowbrick/style/rcmod.py @@ -1,10 +1,10 @@ # yellowbrick.style.rcmod # Modifies the matplotlib rcParams in order to make yellowbrick appealing. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu Oct 06 08:45:38 2016 -0400 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: rcmod.py [c6aff34] benjamin@bengfort.com $ @@ -27,7 +27,8 @@ # Check to see if we have a slightly modern version of mpl from distutils.version import LooseVersion -mpl_ge_150 = LooseVersion(mpl.__version__) >= '1.5.0' + +mpl_ge_150 = LooseVersion(mpl.__version__) >= "1.5.0" from .. import _orig_rc_params @@ -38,10 +39,7 @@ ## Exports ########################################################################## -__all__ = [ - "set_aesthetic", "set_style", "set_palette", - "reset_defaults", "reset_orig", -] +__all__ = ["set_aesthetic", "set_style", "set_palette", "reset_defaults", "reset_orig"] ########################################################################## @@ -49,21 +47,16 @@ ########################################################################## _style_keys = ( - "axes.facecolor", "axes.edgecolor", "axes.grid", "axes.axisbelow", "axes.linewidth", "axes.labelcolor", - "figure.facecolor", - "grid.color", "grid.linestyle", - "text.color", - "xtick.color", "ytick.color", "xtick.direction", @@ -72,13 +65,10 @@ "ytick.major.size", "xtick.minor.size", "ytick.minor.size", - "legend.frameon", "legend.numpoints", "legend.scatterpoints", - "lines.solid_capstyle", - "image.cmap", "font.family", "font.sans-serif", @@ -86,27 +76,23 @@ _context_keys = ( "figure.figsize", - "font.size", "axes.labelsize", "axes.titlesize", "xtick.labelsize", "ytick.labelsize", "legend.fontsize", - "grid.linewidth", "lines.linewidth", "patch.linewidth", "lines.markersize", "lines.markeredgewidth", - "xtick.major.width", "ytick.major.width", "xtick.minor.width", "ytick.minor.width", - "xtick.major.pad", - "ytick.major.pad" + "ytick.major.pad", ) @@ -114,8 +100,10 @@ ## rcParams Keys ########################################################################## -def set_aesthetic(palette="yellowbrick", font="sans-serif", font_scale=1, - color_codes=True, rc=None): + +def set_aesthetic( + palette="yellowbrick", font="sans-serif", font_scale=1, color_codes=True, rc=None +): """ Set aesthetic parameters in one step. @@ -162,6 +150,7 @@ def reset_orig(): ## Axes Styles ########################################################################## + def _axes_style(style=None, rc=None): """ Return a parameter dict for the aesthetic style of the plots. @@ -210,8 +199,12 @@ def _axes_style(style=None, rc=None): "axes.axisbelow": True, "image.cmap": "Greys", "font.family": ["sans-serif"], - "font.sans-serif": ["Arial", "Liberation Sans", - "Bitstream Vera Sans", "sans-serif"], + "font.sans-serif": [ + "Arial", + "Liberation Sans", + "Bitstream Vera Sans", + "sans-serif", + ], "grid.linestyle": "-", "axes.grid": True, "lines.solid_capstyle": "round", @@ -260,6 +253,7 @@ def set_style(style=None, rc=None): ## Context ########################################################################## + def _plotting_context(context=None, font_scale=1, rc=None): """ Return a parameter dict to scale elements of the figure. @@ -296,7 +290,6 @@ def _plotting_context(context=None, font_scale=1, rc=None): else: # Set up dictionary of default parameters base_context = { - "figure.figsize": np.array([8, 5.5]), "font.size": 12, "axes.labelsize": 11, @@ -304,29 +297,32 @@ def _plotting_context(context=None, font_scale=1, rc=None): "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.fontsize": 10, - "grid.linewidth": 1, "lines.linewidth": 1.75, - "patch.linewidth": .3, + "patch.linewidth": 0.3, "lines.markersize": 7, "lines.markeredgewidth": 0, - "xtick.major.width": 1, "ytick.major.width": 1, - "xtick.minor.width": .5, - "ytick.minor.width": .5, - + "xtick.minor.width": 0.5, + "ytick.minor.width": 0.5, "xtick.major.pad": 7, "ytick.major.pad": 7, } # Scale all the parameters by the same factor depending on the context - scaling = dict(paper=.8, notebook=1, talk=1.3, poster=1.6)['notebook'] + scaling = dict(paper=0.8, notebook=1, talk=1.3, poster=1.6)["notebook"] context_dict = {k: v * scaling for k, v in base_context.items()} # Now independently scale the fonts - font_keys = ["axes.labelsize", "axes.titlesize", "legend.fontsize", - "xtick.labelsize", "ytick.labelsize", "font.size"] + font_keys = [ + "axes.labelsize", + "axes.titlesize", + "legend.fontsize", + "xtick.labelsize", + "ytick.labelsize", + "font.size", + ] font_dict = {k: context_dict[k] * font_scale for k in font_keys} context_dict.update(font_dict) @@ -393,17 +389,20 @@ def __call__(self, func): def wrapper(*args, **kwargs): with self: return func(*args, **kwargs) + return wrapper class _AxesStyle(_RCAesthetics): """Light wrapper on a dict to set style temporarily.""" + _keys = _style_keys _set = staticmethod(set_style) class _PlottingContext(_RCAesthetics): """Light wrapper on a dict to set context temporarily.""" + _keys = _context_keys _set = staticmethod(_set_context) @@ -412,6 +411,7 @@ class _PlottingContext(_RCAesthetics): ## Colors/Palettes ########################################################################## + def set_palette(palette, n_colors=None, color_codes=False): """ Set the matplotlib color cycle using a seaborn palette. @@ -432,8 +432,9 @@ def set_palette(palette, n_colors=None, color_codes=False): colors = color_palette(palette, n_colors) if mpl_ge_150: from cycler import cycler - cyl = cycler('color', colors) - mpl.rcParams['axes.prop_cycle'] = cyl + + cyl = cycler("color", colors) + mpl.rcParams["axes.prop_cycle"] = cyl else: mpl.rcParams["axes.color_cycle"] = list(colors) mpl.rcParams["patch.facecolor"] = colors[0] diff --git a/yellowbrick/style/utils.py b/yellowbrick/style/utils.py index 96c7fd82b..719fba0ec 100644 --- a/yellowbrick/style/utils.py +++ b/yellowbrick/style/utils.py @@ -1,3 +1,14 @@ +# yellowbrick.style.utils +# Utility functions for styles +# +# Author: Neal Humphrey +# Created: Wed Mar 22 12:39:35 2017 -0400 +# +# Copyright (C) 2017 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: utils.py [45268fc] humphrey.neal@gmail.com $ + """ Utility functions for styles """ @@ -30,19 +41,20 @@ def find_text_color(base_color, dark_color="black", light_color="white", coef_ch a list, user can enter 0 or 1 as list index. 0 is default. """ - #Coefficients: - # option 0: http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx - # option 1: http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color - coef_options = [np.array((.241, .691, .068, 0)), - np.array((.299, .587, .114, 0)) - ] + # Coefficients: + # option 0: http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx + # option 1: http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color + coef_options = [ + np.array((0.241, 0.691, 0.068, 0)), + np.array((0.299, 0.587, 0.114, 0)), + ] - coefs= coef_options[coef_choice] + coefs = coef_options[coef_choice] rgb = np.array(base_color) * 255 - brightness = np.sqrt(np.dot(coefs, rgb**2)) + brightness = np.sqrt(np.dot(coefs, rgb ** 2)) - #Threshold from option 0 link; determined by trial and error. - #base is light + # Threshold from option 0 link; determined by trial and error. + # base is light if brightness > 130: return dark_color return light_color diff --git a/yellowbrick/target/__init__.py b/yellowbrick/target/__init__.py index 9051abf6a..6d3193e1d 100644 --- a/yellowbrick/target/__init__.py +++ b/yellowbrick/target/__init__.py @@ -4,7 +4,7 @@ # Author: Benjamin Bengfort # Created: Thu Jul 19 08:57:05 2018 -0400 # -# ID: __init__.py [] benjamin@bengfort.com $ +# ID: __init__.py [d742c57] benjamin@bengfort.com $ """ Implements visualizers related to the dependent (target) variable, y. For @@ -22,3 +22,6 @@ from .class_balance import ClassBalance, class_balance from .binning import BalancedBinningReference, balanced_binning_reference from .feature_correlation import FeatureCorrelation + +# Alias the TargetType defined in yellowbrick.utils.target +from yellowbrick.utils.target import TargetType diff --git a/yellowbrick/target/base.py b/yellowbrick/target/base.py index 0ee0f6c7b..3a83ff120 100644 --- a/yellowbrick/target/base.py +++ b/yellowbrick/target/base.py @@ -1,31 +1,48 @@ # yellowbrick.target.base # Base classes for target visualizers # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu Jul 19 09:25:53 2018 -0400 # -# ID: base.py [] benjamin@bengfort.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: base.py [d742c57] benjamin@bengfort.com $ """ Base classes for target visualizers """ ########################################################################## -## Imports +# Imports ########################################################################## -from ..base import Visualizer +from yellowbrick.base import Visualizer ########################################################################## -## TargetVisualizer Base Class +# TargetVisualizer Base Class ########################################################################## + class TargetVisualizer(Visualizer): """ The base class for target visualizers, generic enough to support any computation on a single vector, y. This Visualizer is based on the LabelEncoder in sklearn.preprocessing, which only accepts a target y. + + Parameters + ---------- + ax : matplotlib Axes, default: None + The axis to plot the figure on. If None is passed in the current axes + will be used (or generated if required). + + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + + kwargs : dict + Keyword arguments that are passed to the base class """ def fit(self, y): @@ -35,6 +52,4 @@ def fit(self, y): of pipelines, but must be used separately; similar to how the LabelEncoder is used. """ - raise NotImplementedError( - "target visualizers must implement a fit method" - ) + raise NotImplementedError("target visualizers must implement a fit method") diff --git a/yellowbrick/target/binning.py b/yellowbrick/target/binning.py index 05327bef4..3dcd60d05 100644 --- a/yellowbrick/target/binning.py +++ b/yellowbrick/target/binning.py @@ -1,13 +1,12 @@ - # yellowbrick.target.binning # Implementations of histogram with vertical lines to help with balanced binning. # -# Author: Juan L. Kehoe (juanluo2008@gmail.com) -# Author: Prema Damodaran Roman (pdamo24@gmail.com) +# Author: Juan L. Kehoe +# Author: Prema Damodaran Roman # Created: Tue Mar 13 19:50:54 2018 -0400 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # # ID: binning.py @@ -17,18 +16,18 @@ """ ########################################################################## -## Imports +# Imports ########################################################################## -import matplotlib.pyplot as plt import numpy as np -from .base import TargetVisualizer +from yellowbrick.target.base import TargetVisualizer from yellowbrick.exceptions import YellowbrickValueError ########################################################################## -## Balanced Binning Reference +# Balanced Binning Reference ########################################################################## + class BalancedBinningReference(TargetVisualizer): """ BalancedBinningReference generates a histogram with vertical lines @@ -41,7 +40,7 @@ class BalancedBinningReference(TargetVisualizer): This is inherited from FeatureVisualizer and is defined within ``BalancedBinningReference``. - target : string, default: "Frequency" + target : string, default: "y" The name of the ``y`` variable bins : number of bins to generate the histogram, default: 4 @@ -49,10 +48,10 @@ class BalancedBinningReference(TargetVisualizer): kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. - + Attributes ---------- - bin_edges : binning reference values + bin_edges_ : binning reference values Examples -------- @@ -89,8 +88,10 @@ def draw(self, y, **kwargs): self.bin_edges_ = bin_edges self.ax.hist(y, bins=self.bins, color=kwargs.pop("color", "#6897bb"), **kwargs) - # add vetical line with binning reference values - plt.vlines(bin_edges,0,max(hist),colors=kwargs.pop("colors", "r")) + # add vertical line with binning reference values + self.ax.vlines(bin_edges, 0, max(hist), colors=kwargs.pop("colors", "r")) + + return self.ax def fit(self, y, **kwargs): """ @@ -107,26 +108,19 @@ def fit(self, y, **kwargs): """ - #throw an error if y has more than 1 column + # throw an error if y has more than 1 column if y.ndim > 1: - raise YellowbrickValueError("y needs to be an array or Series with one dimension") + raise YellowbrickValueError( + "y needs to be an array or Series with one dimension" + ) # Handle the target name if it is None. if self.target is None: - self.target = 'Frequency' + self.target = "y" self.draw(y) return self - - def poof(self, **kwargs): - """ - Creates the labels for the feature and target variables. - """ - - self.ax.set_xlabel(self.target) - self.finalize(**kwargs) - def finalize(self, **kwargs): """ Finalize executes any subclass-specific axes finalization steps. @@ -137,20 +131,21 @@ def finalize(self, **kwargs): kwargs: generic keyword arguments. """ - + self.ax.set_xlabel(self.target) for tk in self.ax.get_xticklabels(): tk.set_visible(True) - + for tk in self.ax.get_yticklabels(): tk.set_visible(True) - - + + ########################################################################## -## Quick Method +# Quick Method ########################################################################## - -def balanced_binning_reference(y, ax=None, target='Frequency', bins=4, **kwargs): - + + +def balanced_binning_reference(y, ax=None, target="y", bins=4, **kwargs): + """ BalancedBinningReference generates a histogram with vertical lines showing the recommended value point to bin your data so they can be evenly @@ -159,12 +154,12 @@ def balanced_binning_reference(y, ax=None, target='Frequency', bins=4, **kwargs) Parameters ---------- y : an array of one dimension or a pandas Series - + ax : matplotlib Axes, default: None This is inherited from FeatureVisualizer and is defined within ``BalancedBinningReference``. - target : string, default: "Frequency" + target : string, default: "y" The name of the ``y`` variable bins : number of bins to generate the histogram, default: 4 @@ -173,18 +168,17 @@ def balanced_binning_reference(y, ax=None, target='Frequency', bins=4, **kwargs) Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. + Returns + ------- + visualizer : BalancedBinningReference + Returns fitted visualizer """ # Initialize the visualizer visualizer = BalancedBinningReference(ax=ax, bins=bins, target=target, **kwargs) - + # Fit and poof the visualizer visualizer.fit(y) visualizer.poof() - - - - - - + return visualizer diff --git a/yellowbrick/target/class_balance.py b/yellowbrick/target/class_balance.py index 8da018634..4927bd3eb 100644 --- a/yellowbrick/target/class_balance.py +++ b/yellowbrick/target/class_balance.py @@ -1,11 +1,14 @@ # yellowbrick.classifier.class_balance # Class balance visualizer for showing per-class support. # -# Author: Rebecca Bilbro -# Author: Benjamin Bengfort +# Author: Rebecca Bilbro +# Author: Benjamin Bengfort # Author: Neal Humphrey # Created: Wed May 18 12:39:40 2016 -0400 # +# Copyright (C) 2016 The scikit-yb developers +# For license information, see LICENSE.txt +# # ID: class_balance.py [5388065] neal@nhumphrey.com $ """ @@ -13,14 +16,14 @@ """ ########################################################################## -## Imports +# Imports ########################################################################## import numpy as np -from .base import TargetVisualizer -from ..style.colors import resolve_colors -from ..exceptions import YellowbrickValueError +from yellowbrick.style.colors import resolve_colors +from yellowbrick.target.base import TargetVisualizer +from yellowbrick.exceptions import YellowbrickValueError from sklearn.utils.multiclass import unique_labels, type_of_target @@ -31,9 +34,10 @@ ########################################################################## -## Class Balance Chart +# Class Balance Chart ########################################################################## + class ClassBalance(TargetVisualizer): """ One of the biggest challenges for classification models is an imbalance of @@ -61,6 +65,12 @@ class ClassBalance(TargetVisualizer): LabelEncoder.classes\_ as this parameter. If not specified, the labels in the data will be used. + colors: list of strings + Specify colors for the barchart (will override colormap if both are provided). + + colormap : string or matplotlib cmap + Specify a colormap to color the classes. + kwargs: dict, optional Keyword arguments passed to the super class. Here, used to colorize the bars in the histogram. @@ -74,8 +84,8 @@ class ClassBalance(TargetVisualizer): A table representing the support of each class in the target. It is a vector when in balance mode, or a table with two rows in compare mode. - Example - ------- + Examples + -------- To simply observe the balance of classes in the target: >>> viz = ClassBalance().fit(y) @@ -89,8 +99,11 @@ class ClassBalance(TargetVisualizer): >>> viz.poof() """ - def __init__(self, ax=None, labels=None, **kwargs): + def __init__(self, ax=None, labels=None, colors=None, colormap=None, **kwargs): self.labels = labels + self.colors = colors + self.colormap = colormap + super(ClassBalance, self).__init__(ax, **kwargs) def fit(self, y_train, y_test=None): @@ -108,7 +121,7 @@ def fit(self, y_train, y_test=None): Parameters ---------- y_train : array-like - Array or list of shape (n,) that containes discrete data. + Array or list of shape (n,) that contains discrete data. y_test : array-like, optional Array or list of shape (m,) that contains discrete data. If @@ -117,10 +130,12 @@ def fit(self, y_train, y_test=None): # check to make sure that y_train is not a 2D array, e.g. X if y_train.ndim == 2: - raise YellowbrickValueError(( - "fit has changed to only require a 1D array, y " - "since version 0.9; please see the docs for more info" - )) + raise YellowbrickValueError( + ( + "fit has changed to only require a 1D array, y " + "since version 0.9; please see the docs for more info" + ) + ) # Check the target types for the y variables self._validate_target(y_train) @@ -133,27 +148,24 @@ def fit(self, y_train, y_test=None): # Validate the classes with the class names if self.labels is not None: if len(self.labels) != len(self.classes_): - raise YellowbrickValueError(( - "discovered {} classes in the data, does not match " - "the {} labels specified." - ).format(len(self.classes_), len(self.labels))) + raise YellowbrickValueError( + ( + "discovered {} classes in the data, does not match " + "the {} labels specified." + ).format(len(self.classes_), len(self.labels)) + ) # Determine if we're in compare or balance mode self._mode = BALANCE if y_test is None else COMPARE # Compute the support values if self._mode == BALANCE: - self.support_ = np.array([ - (y_train == idx).sum() for idx in self.classes_ - ]) + self.support_ = np.array([(y_train == idx).sum() for idx in self.classes_]) else: - self.support_ = np.array([ - [ - (y == idx).sum() for idx in self.classes_ - ] - for y in targets - ]) + self.support_ = np.array( + [[(y == idx).sum() for idx in self.classes_] for y in targets] + ) # Draw the bar chart self.draw() @@ -166,12 +178,17 @@ def draw(self): Renders the class balance chart on the specified axes from support. """ # Number of colors is either number of classes or 2 - colors = resolve_colors(len(self.support_)) + colors = resolve_colors( + len(self.support_), colormap=self.colormap, colors=self.colors + ) if self._mode == BALANCE: self.ax.bar( - np.arange(len(self.support_)), self.support_, - color=colors, align='center', width=0.5 + np.arange(len(self.support_)), + self.support_, + color=colors, + align="center", + width=0.5, ) # Compare mode @@ -185,8 +202,7 @@ def draw(self): index = index + bar_width self.ax.bar( - index, support, bar_width, - color=colors[idx], label=labels[idx] + index, support, bar_width, color=colors[idx], label=labels[idx] ) return self.ax @@ -202,22 +218,20 @@ def finalize(self, **kwargs): """ # Set the title - self.set_title( - 'Class Balance for {:,} Instances'.format(self.support_.sum()) - ) + self.set_title("Class Balance for {:,} Instances".format(self.support_.sum())) # Set the x ticks with the class names or labels if specified labels = self.labels if self.labels is not None else self.classes_ xticks = np.arange(len(labels)) if self._mode == COMPARE: - xticks = xticks + (0.35/2) + xticks = xticks + (0.35 / 2) self.ax.set_xticks(xticks) self.ax.set_xticklabels(labels) # Compute the ceiling for the y limit cmax = self.support_.max() - self.ax.set_ylim(0, cmax + cmax* 0.1) + self.ax.set_ylim(0, cmax + cmax * 0.1) self.ax.set_ylabel("support") # Remove the vertical grid @@ -237,16 +251,21 @@ def _validate_target(self, y): y_type = type_of_target(y) if y_type not in ("binary", "multiclass"): - raise YellowbrickValueError(( - "'{}' target type not supported, only binary and multiclass" - ).format(y_type)) + raise YellowbrickValueError( + ("'{}' target type not supported, only binary and multiclass").format( + y_type + ) + ) ########################################################################## -## Quick Method +# Quick Method ########################################################################## -def class_balance(y_train, y_test=None, ax=None, labels=None, **kwargs): + +def class_balance( + y_train, y_test=None, ax=None, labels=None, color=None, colormap=None, **kwargs +): """Quick method: One of the biggest challenges for classification models is an imbalance of @@ -282,21 +301,27 @@ def class_balance(y_train, y_test=None, ax=None, labels=None, **kwargs): LabelEncoder.classes\_ as this parameter. If not specified, the labels in the data will be used. + colors: list of strings + Specify colors for the barchart (will override colormap if both are provided). + + colormap : string or matplotlib cmap + Specify a colormap to color the classes. + kwargs: dict, optional Keyword arguments passed to the super class. Here, used to colorize the bars in the histogram. Returns ------- - ax : matplotlib axes - Returns the axes that the class balance plot was drawn on. + visualizer : ClassBalance + Returns the fitted visualizer """ # Instantiate the visualizer - visualizer = ClassBalance(ax=ax, labels=labels, **kwargs) + visualizer = ClassBalance(ax=ax, labels=labels, color=None, colormap=None, **kwargs) # Fit and transform the visualizer (calls draw) visualizer.fit(y_train, y_test) visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer + return visualizer diff --git a/yellowbrick/target/feature_correlation.py b/yellowbrick/target/feature_correlation.py index ffc5579d0..65a0b6cda 100644 --- a/yellowbrick/target/feature_correlation.py +++ b/yellowbrick/target/feature_correlation.py @@ -1,23 +1,26 @@ # yellowbrick.classifier.feature_correlation # Feature correlation to dependent variable visualizer. # -# Author Zijie (ZJ) Poh +# Author Zijie (ZJ) Poh # Created: Wed Jul 29 15:30:40 2018 -0700 # -# ID: feature_correlation.py [] poh.zijie@gmail.com $ +# Copyright (C) 2018 The scikit-yb developers +# For license information, see LICENSE.txt +# +# ID: feature_correlation.py [33aec16] 8103276+zjpoh@users.noreply.github.com $ """ Feature Correlation to Dependent Variable Visualizer. """ ########################################################################## -## Imports +# Imports ########################################################################## import numpy as np -from yellowbrick.target.base import TargetVisualizer from yellowbrick.utils import is_dataframe +from yellowbrick.target.base import TargetVisualizer from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning from sklearn.feature_selection import mutual_info_classif @@ -26,24 +29,25 @@ from scipy.stats import pearsonr ########################################################################## -## Supported Correlation Computations +# Supported Correlation Computations ########################################################################## CORRELATION_LABELS = { - 'pearson': 'Pearson Correlation', - 'mutual_info-regression': 'Mutual Information', - 'mutual_info-classification': 'Mutual Information' + "pearson": "Pearson Correlation", + "mutual_info-regression": "Mutual Information", + "mutual_info-classification": "Mutual Information", } CORRELATION_METHODS = { - 'mutual_info-regression': mutual_info_regression, - 'mutual_info-classification': mutual_info_classif + "mutual_info-regression": mutual_info_regression, + "mutual_info-classification": mutual_info_classif, } ########################################################################## -## Class Feature Correlation +# Class Feature Correlation ########################################################################## + class FeatureCorrelation(TargetVisualizer): """ Displays the correlation between features and dependent variables. @@ -85,6 +89,9 @@ class FeatureCorrelation(TargetVisualizer): Must have labels or the fitted data is a DataFrame with column names. If feature_index is provided, feature_names will be ignored. + color: string + Specify color for barchart + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. @@ -105,9 +112,17 @@ class FeatureCorrelation(TargetVisualizer): >>> viz.poof() """ - def __init__(self, ax=None, method='pearson', - labels=None, sort=False, feature_index=None, - feature_names=None, **kwargs): + def __init__( + self, + ax=None, + method="pearson", + labels=None, + sort=False, + feature_index=None, + feature_names=None, + color=None, + **kwargs + ): super(FeatureCorrelation, self).__init__(ax=None, **kwargs) self.correlation_labels = CORRELATION_LABELS @@ -115,18 +130,19 @@ def __init__(self, ax=None, method='pearson', if method not in self.correlation_labels: raise YellowbrickValueError( - 'Method {} not implement; choose from {}'.format( + "Method {} not implement; choose from {}".format( method, ", ".join(self.correlation_labels) ) ) # Parameters self.set_params( + sort=sort, + color=color, method=method, labels=labels, - sort=sort, feature_index=feature_index, - feature_names=feature_names + feature_names=feature_names, ) def fit(self, X, y, **kwargs): @@ -162,7 +178,7 @@ def fit(self, X, y, **kwargs): else: self.scores_ = np.array( self.correlation_methods[self.method](X, y, **kwargs) - ) + ) # If feature indices are given, plot only the given features if self.feature_index: @@ -184,7 +200,7 @@ def draw(self): """ pos = np.arange(self.scores_.shape[0]) + 0.5 - self.ax.barh(pos, self.scores_) + self.ax.barh(pos, self.scores_, color=self.color) # Set the labels for the bars self.ax.set_yticks(pos) @@ -196,11 +212,11 @@ def finalize(self): """ Finalize the drawing setting labels and title. """ - self.set_title('Features correlation with dependent variable') + self.set_title("Features correlation with dependent variable") self.ax.set_xlabel(self.correlation_labels[self.method]) - self.ax.grid(False, axis='y') + self.ax.grid(False, axis="y") def _create_labels_for_features(self, X): """ @@ -230,33 +246,38 @@ def _select_features_to_plot(self, X): if self.feature_index: if self.feature_names: raise YellowbrickWarning( - 'Both feature_index and feature_names ' - 'are specified. feature_names is ignored' + "Both feature_index and feature_names " + "are specified. feature_names is ignored" ) - if (min(self.feature_index) < 0 - or max(self.feature_index) >= X.shape[1]): - raise YellowbrickValueError('Feature index is out of range') + if min(self.feature_index) < 0 or max(self.feature_index) >= X.shape[1]: + raise YellowbrickValueError("Feature index is out of range") elif self.feature_names: self.feature_index = [] features_list = self.features_.tolist() for feature_name in self.feature_names: try: - self.feature_index.append( - features_list.index(feature_name) - ) + self.feature_index.append(features_list.index(feature_name)) except ValueError: - raise YellowbrickValueError( - '{} not in labels'.format(feature_name) - ) + raise YellowbrickValueError("{} not in labels".format(feature_name)) ########################################################################## -## Quick Method +# Quick Method ########################################################################## -def feature_correlation(X, y, ax=None, method='pearson', - labels=None, sort=False, feature_index=None, - feature_names=None, **kwargs): + +def feature_correlation( + X, + y, + ax=None, + method="pearson", + labels=None, + sort=False, + feature_index=None, + feature_names=None, + color=None, + **kwargs +): """ Displays the correlation between features and dependent variables. @@ -304,23 +325,34 @@ def feature_correlation(X, y, ax=None, method='pearson', Must have labels or the fitted data is a DataFrame with column names. If feature_index is provided, feature_names will be ignored. + color: string + Specify color for barchart + kwargs : dict Keyword arguments that are passed to the base class and may influence the visualization as defined in other Visualizers. Returns ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. + visualizer : FeatureCorrelation + Returns the fitted visualizer. """ # Instantiate the visualizer - viz = FeatureCorrelation(ax, method, labels, sort, - feature_index, feature_names, **kwargs) + viz = FeatureCorrelation( + ax=ax, + method=method, + labels=labels, + sort=sort, + color=color, + feature_index=feature_index, + feature_names=feature_names, + **kwargs + ) # Fit and transform the visualizer (calls draw) viz.fit(X, y, **kwargs) viz.finalize() - # Return the axes object on the visualizer - return viz.ax + # Return the visualizer + return viz diff --git a/yellowbrick/text/__init__.py b/yellowbrick/text/__init__.py index 3936eba84..3a69f3f40 100644 --- a/yellowbrick/text/__init__.py +++ b/yellowbrick/text/__init__.py @@ -1,10 +1,10 @@ # yellowbrick.text # Visualizers for text feature analysis and diagnostics. # -# Author: Rebecca Bilbro +# Author: Rebecca Bilbro # Created: 2017-01-20 14:42 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [75d9b20] rebecca.bilbro@bytecubed.com $ @@ -18,6 +18,7 @@ ########################################################################## from .tsne import TSNEVisualizer, tsne +from .umap_vis import UMAPVisualizer, umap from .freqdist import FreqDistVisualizer, freqdist from .postag import PosTagVisualizer from .dispersion import DispersionPlot, dispersion diff --git a/yellowbrick/text/base.py b/yellowbrick/text/base.py index 7a82c01d5..8306b8085 100644 --- a/yellowbrick/text/base.py +++ b/yellowbrick/text/base.py @@ -1,10 +1,10 @@ # yellowbrick.text.base # Base classes for text feature visualizers and feature selection tools. # -# Author: Rebecca Bilbro -# Created: 2017-01-20 14:44 +# Author: Rebecca Bilbro +# Created: Sat Jan 21 09:37:01 2017 -0500 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: base.py [75d9b20] rebecca.bilbro@bytecubed.com $ @@ -25,6 +25,7 @@ ## Text Visualizers ########################################################################## + class TextVisualizer(Visualizer, TransformerMixin): """ Base class for text feature visualization to investigate documents @@ -39,7 +40,7 @@ class TextVisualizer(Visualizer, TransformerMixin): Accepts as input a DataFrame or Numpy array. """ - def __init__(self, ax=None, **kwargs): + def __init__(self, ax=None, fig=None, **kwargs): """ These parameters can be influenced later on in the visualization process, but can and should be set as early as possible. @@ -49,11 +50,15 @@ def __init__(self, ax=None, **kwargs): ax : axes the axis to plot the figure on + fig : matplotlib Figure, default: None + The figure to plot the Visualizer on. If None is passed in the current + plot will be used (or generated if required). + kwargs : dict Pass generic arguments to the drawing method """ - super(TextVisualizer, self).__init__(ax=ax, **kwargs) + super(TextVisualizer, self).__init__(ax=ax, fig=fig, **kwargs) def fit(self, X, y=None, **fit_params): """ diff --git a/yellowbrick/text/dispersion.py b/yellowbrick/text/dispersion.py index 55d945636..f6eef357b 100644 --- a/yellowbrick/text/dispersion.py +++ b/yellowbrick/text/dispersion.py @@ -2,12 +2,12 @@ # Implementations of lexical dispersions for text visualization. # # Author: Larry Gray -# Created: 2018-06-21 10:06 +# Created: Fri Jun 22 15:40:49 2018 -0400 # -# Copyright (C) 2018 District Data Labs +# Copyright (C) 2018 The scikit-yb developers # For license information, see LICENSE.txt # -# ID: dispersion.py [] lwgray@gmail.com $ +# ID: dispersion.py [3822dd6] lwgray@gmail.com $ """ Implementation of lexical dispersion for text visualization @@ -31,6 +31,7 @@ ## Dispersion Plot Visualizer ########################################################################## + class DispersionPlot(TextVisualizer): """ DispersionPlotVisualizer allows for visualization of the lexical dispersion @@ -42,7 +43,7 @@ class DispersionPlot(TextVisualizer): ---------- target_words : list A list of target words whose dispersion across a corpus passed at fit - will be visualized. + will be visualized. ax : matplotlib axes, default: None The axes to plot the figure on. @@ -58,7 +59,7 @@ class DispersionPlot(TextVisualizer): Qualitative colormap for discrete target ignore_case : boolean, default: False - Specify whether input will be case-sensitive. + Specify whether input will be case-sensitive. annotate_docs : boolean, default: False Specify whether document boundaries will be displayed. Vertical lines @@ -74,8 +75,17 @@ class DispersionPlot(TextVisualizer): # NOTE: cannot be np.nan NULL_CLASS = None - def __init__(self, target_words, ax=None, colors=None, ignore_case=False, - annotate_docs=False, labels=None, colormap=None, **kwargs): + def __init__( + self, + target_words, + ax=None, + colors=None, + ignore_case=False, + annotate_docs=False, + labels=None, + colormap=None, + **kwargs + ): super(DispersionPlot, self).__init__(ax=ax, **kwargs) self.labels = labels @@ -90,7 +100,6 @@ def _compute_dispersion(self, text, y): self.boundaries_ = [] offset = 0 - if y is None: y = itertools.repeat(None) @@ -112,13 +121,14 @@ def _compute_dispersion(self, text, y): def _check_missing_words(self, points): for index in range(len(self.indexed_words_)): - if index in points[:,1]: + if index in points[:, 1]: pass else: - raise YellowbrickValueError(( - "The indexed word '{}' is not found in " - "this corpus" - ).format(self.indexed_words_[index])) + raise YellowbrickValueError( + ("The indexed word '{}' is not found in " "this corpus").format( + self.indexed_words_[index] + ) + ) def fit(self, X, y=None, **kwargs): """ @@ -129,7 +139,7 @@ def fit(self, X, y=None, **kwargs): ---------- X : list or generator Should be provided as a list of documents or a generator - that yields a list of documents that contain a list of + that yields a list of documents that contain a list of words in the order they appear in the document. y : ndarray or Series of length n @@ -162,13 +172,12 @@ def fit(self, X, y=None, **kwargs): try: points_target = np.stack(self._compute_dispersion(X, y)) except ValueError: - raise YellowbrickValueError(( - "No indexed words were found in the corpus" - )) - points = np.stack(zip(points_target[:,0].astype(int), - points_target[:,1].astype(int))) + raise YellowbrickValueError(("No indexed words were found in the corpus")) + points = np.stack( + zip(points_target[:, 0].astype(int), points_target[:, 1].astype(int)) + ) - self.target = points_target[:,2] + self.target = points_target[:, 2] self._check_missing_words(points) @@ -187,14 +196,17 @@ def draw(self, points, target=None, **kwargs): # Resolve the labels with the classes labels = self.labels if self.labels is not None else self.classes_ if len(labels) != len(self.classes_): - raise YellowbrickValueError(( - "number of supplied labels ({}) does not " - "match the number of classes ({})" - ).format(len(labels), len(self.classes_))) + raise YellowbrickValueError( + ( + "number of supplied labels ({}) does not " + "match the number of classes ({})" + ).format(len(labels), len(self.classes_)) + ) # Create the color mapping for the labels. color_values = resolve_colors( - n_colors=len(labels), colormap=self.colormap, colors=self.color) + n_colors=len(labels), colormap=self.colormap, colors=self.color + ) colors = dict(zip(labels, color_values)) # Transform labels into a map of class to label @@ -203,28 +215,36 @@ def draw(self, points, target=None, **kwargs): # Define boundaries with a vertical line if self.annotate_docs: for xcoords in self.boundaries_: - self.ax.axvline(x=xcoords, color='lightgray', linestyle='dashed') + self.ax.axvline(x=xcoords, color="lightgray", linestyle="dashed") - series = defaultdict(lambda: {'x':[], 'y':[]}) + series = defaultdict(lambda: {"x": [], "y": []}) if target is not None: for point, t in zip(points, target): label = labels[t] - series[label]['x'].append(point[0]) - series[label]['y'].append(point[1]) + series[label]["x"].append(point[0]) + series[label]["y"].append(point[1]) else: label = self.classes_[0] for x, y in points: - series[label]['x'].append(x) - series[label]['y'].append(y) + series[label]["x"].append(x) + series[label]["y"].append(y) for label, points in series.items(): - self.ax.scatter(points['x'], points['y'], marker='|', - c=colors[label], zorder=100, label=label) + self.ax.scatter( + points["x"], + points["y"], + marker="|", + c=colors[label], + zorder=100, + label=label, + ) self.ax.set_yticks(list(range(len(self.indexed_words_)))) self.ax.set_yticklabels(self.indexed_words_) + return self.ax + def finalize(self, **kwargs): """ The finalize method executes any subclass-specific axes @@ -243,17 +263,29 @@ def finalize(self, **kwargs): if not all(self.classes_ == np.array([self.NULL_CLASS])): box = self.ax.get_position() self.ax.set_position([box.x0, box.y0, box.width * 0.8, box.height]) - self.ax.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + self.ax.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + ########################################################################## ## Quick Method ########################################################################## -def dispersion(words, corpus, y=None, ax=None, colors=None, colormap=None, - labels=None, annotate_docs=False, ignore_case=False, **kwargs): + +def dispersion( + words, + corpus, + y=None, + ax=None, + colors=None, + colormap=None, + labels=None, + annotate_docs=False, + ignore_case=False, + **kwargs +): """ Displays lexical dispersion plot for words in a corpus - This helper function is a quick wrapper to utilize the DisperstionPlot + This helper function is a quick wrapper to utilize the DispersionPlot Visualizer for one-off analysis Parameters @@ -289,26 +321,32 @@ def dispersion(words, corpus, y=None, ax=None, colors=None, colormap=None, are positioned at the end of each document. ignore_case : boolean, default: False - Specify whether input will be case-sensitive. + Specify whether input will be case-sensitive. kwargs : dict Pass any additional keyword arguments to the super class. Returns ------- - ax: matplotlib axes - Returns the axes that the plot was drawn on + viz: DispersionPlot + Returns the fitted, finalized visualizer """ # Instantiate the visualizer visualizer = DispersionPlot( - words, ax=ax, colors=colors, colormap=colormap, - ignore_case=ignore_case, labels=labels, - annotate_docs=annotate_docs, **kwargs + words, + ax=ax, + colors=colors, + colormap=colormap, + ignore_case=ignore_case, + labels=labels, + annotate_docs=annotate_docs, + **kwargs ) # Fit and transform the visualizer (calls draw) visualizer.fit(corpus, y, **kwargs) + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer object + return visualizer diff --git a/yellowbrick/text/freqdist.py b/yellowbrick/text/freqdist.py index d1aa633da..186ea497b 100644 --- a/yellowbrick/text/freqdist.py +++ b/yellowbrick/text/freqdist.py @@ -1,10 +1,10 @@ # yellowbrick.text.freqdist # Implementations of frequency distributions for text visualization. # -# Author: Rebecca Bilbro -# Created: 2017-02-08 10:06 +# Author: Rebecca Bilbro +# Created: Mon Feb 20 12:38:20 2017 -0500 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: freqdist.py [67b2740] rebecca.bilbro@bytecubed.com $ @@ -29,7 +29,8 @@ ## Quick Method ########################################################################## -def freqdist(X, y=None, ax=None, color=None, N=50, **kwargs): + +def freqdist(X, y=None, ax=None, n=50, orient="h", color=None, **kwargs): """Displays frequency distribution plot for text. This helper function is a quick wrapper to utilize the FreqDist @@ -45,34 +46,36 @@ def freqdist(X, y=None, ax=None, color=None, N=50, **kwargs): y: ndarray or Series of length n An array or series of target or class values - ax: matplotlib axes + ax : matplotlib axes, default: None The axes to plot the figure on. - color: string - Specify color for barchart - - N: integer + n: integer, default: 50 Top N tokens to be plotted. + orient : 'h' or 'v', default: 'h' + Specifies a horizontal or vertical bar chart. + + color : string + Specify color for bars + kwargs: dict Keyword arguments passed to the super class. Returns ------- - ax: matplotlib axes - Returns the axes that the plot was drawn on. + visualizer: FreqDistVisualizer + Returns the fitted, finalized visualizer """ # Instantiate the visualizer - visualizer = FreqDistVisualizer( - ax, X, color, **kwargs - ) + visualizer = FreqDistVisualizer(ax=ax, n=n, orient=orient, color=color, **kwargs) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y, **kwargs) visualizer.transform(X) + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer object + return visualizer class FrequencyVisualizer(TextVisualizer): @@ -101,7 +104,7 @@ class FrequencyVisualizer(TextVisualizer): orient : 'h' or 'v', default: 'h' Specifies a horizontal or vertical bar chart. - color : list or tuple of colors + color : string Specify color for bars kwargs : dict @@ -111,15 +114,13 @@ class FrequencyVisualizer(TextVisualizer): process, but can and should be set as early as possible. """ - def __init__(self, features, ax=None, n=50, orient='h', color=None, **kwargs): + def __init__(self, features, ax=None, n=50, orient="h", color=None, **kwargs): super(FreqDistVisualizer, self).__init__(ax=ax, **kwargs) # Check that the orient is correct orient = orient.lower().strip() - if orient not in {'h', 'v'}: - raise YellowbrickValueError( - "Orientation must be 'h' or 'v'" - ) + if orient not in {"h", "v"}: + raise YellowbrickValueError("Orientation must be 'h' or 'v'") # Visualizer parameters self.N = n @@ -188,7 +189,7 @@ def fit(self, X, y=None): # Frequency distribution of entire corpus. self.freqdist_ = self.count(X) - self.sorted_ = self.freqdist_.argsort()[::-1] # Descending order + self.sorted_ = self.freqdist_.argsort()[::-1] # Descending order # Compute the number of words, vocab, and hapaxes self.vocab_ = self.freqdist_.shape[0] @@ -210,26 +211,24 @@ def draw(self, **kwargs): """ # Prepare the data - bins = np.arange(self.N) - words = [self.features[i] for i in self.sorted_[:self.N]] + bins = np.arange(self.N) + words = [self.features[i] for i in self.sorted_[: self.N]] freqs = {} # Set up the bar plots if self.conditional_freqdist_: - for label, values in sorted(self.conditional_freqdist_.items(), key=itemgetter(0)): - freqs[label] = [ - values[i] for i in self.sorted_[:self.N] - ] + for label, values in sorted( + self.conditional_freqdist_.items(), key=itemgetter(0) + ): + freqs[label] = [values[i] for i in self.sorted_[: self.N]] else: - freqs['corpus'] = [ - self.freqdist_[i] for i in self.sorted_[:self.N] - ] + freqs["corpus"] = [self.freqdist_[i] for i in self.sorted_[: self.N]] # Draw a horizontal barplot - if self.orient == 'h': + if self.orient == "h": # Add the barchart, stacking if necessary for label, freq in freqs.items(): - self.ax.barh(bins, freq, label=label, align='center') + self.ax.barh(bins, freq, label=label, color=self.color, align="center") # Set the y ticks to the words self.ax.set_yticks(bins) @@ -243,10 +242,10 @@ def draw(self, **kwargs): self.ax.xaxis.grid(True) # Draw a vertical barplot - elif self.orient == 'v': + elif self.orient == "v": # Add the barchart, stacking if necessary for label, freq in freqs.items(): - self.ax.bar(bins, freq, label=label, align='edge') + self.ax.bar(bins, freq, label=label, color=self.color, align="edge") # Set the y ticks to the words self.ax.set_xticks(bins) @@ -258,9 +257,9 @@ def draw(self, **kwargs): # Unknown state else: - raise YellowbrickValueError( - "Orientation must be 'h' or 'v'" - ) + raise YellowbrickValueError("Orientation must be 'h' or 'v'") + + return self.ax def finalize(self, **kwargs): """ @@ -273,21 +272,25 @@ def finalize(self, **kwargs): """ # Set the title - self.set_title( - 'Frequency Distribution of Top {} tokens'.format(self.N) - ) + self.set_title("Frequency Distribution of Top {} tokens".format(self.N)) # Create the vocab, count, and hapaxes labels infolabel = "vocab: {:,}\nwords: {:,}\nhapax: {:,}".format( self.vocab_, self.words_, self.hapaxes_ ) - self.ax.text(0.68, 0.97, infolabel, transform=self.ax.transAxes, - fontsize=9, verticalalignment='top', - bbox={'boxstyle':'round', 'facecolor':'white', 'alpha':.8}) + self.ax.text( + 0.68, + 0.97, + infolabel, + transform=self.ax.transAxes, + fontsize=9, + verticalalignment="top", + bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.8}, + ) # Set the legend and the grid - self.ax.legend(loc='upper right', frameon=True) + self.ax.legend(loc="upper right", frameon=True) # Backwards compatibility alias diff --git a/yellowbrick/text/postag.py b/yellowbrick/text/postag.py index 034eea138..b1119a068 100644 --- a/yellowbrick/text/postag.py +++ b/yellowbrick/text/postag.py @@ -1,10 +1,10 @@ # yellowbrick.text.postag # Implementation of part-of-speech visualization for text. # -# Author: Rebecca Bilbro -# Created: 2017-03-05 14:44 +# Author: Rebecca Bilbro +# Created: Sun Mar 5 18:07:06 2017 -0500 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The scikit-yb developers # For license information, see LICENSE.txt # # ID: postag.py [849f5a8] rebecca.bilbro@bytecubed.com $ @@ -19,126 +19,482 @@ # Imports ########################################################################## +import numpy as np + +from yellowbrick.draw import bar_stack from yellowbrick.text.base import TextVisualizer +from yellowbrick.style.colors import resolve_colors +from yellowbrick.exceptions import YellowbrickValueError + +########################################################################## +# Part-of-speech tag punctuation and labels +########################################################################## + +# NOTE: Penn Treebank converts all sentence closers (!,?,;) to periods +PUNCT_TAGS = [".", ":", ",", "``", "''", "(", ")", "#", "$"] + +TAGSET_NAMES = {"penn_treebank": "Penn Treebank", "universal": "Universal Dependencies"} + +PENN_TAGS = [ + "noun", + "verb", + "adjective", + "adverb", + "preposition", + "determiner", + "pronoun", + "conjunction", + "infinitive", + "wh- word", + "modal", + "possessive", + "existential", + "punctuation", + "digit", + "non-English", + "interjection", + "list", + "symbol", + "other", +] + +UNIVERSAL_TAGS = [ + "noun", + "verb", + "adjective", + "adverb", + "adposition", + "determiner", + "pronoun", + "conjunction", + "infinitive", + "punctuation", + "number", + "interjection", + "symbol", + "other", +] + ########################################################################## # PosTagVisualizer ########################################################################## + class PosTagVisualizer(TextVisualizer): """ - A part-of-speech tag visualizer colorizes text to enable - the user to visualize the proportions of nouns, verbs, etc. - and to use this information to make decisions about - part-of-speech tagging, text normalization (e.g. stemming - vs lemmatization) and vectorization. + Parts of speech (e.g. verbs, nouns, prepositions, adjectives) + indicate how a word is functioning within the context of a sentence. + In English as in many other languages, a single word can function in + multiple ways. Part-of-speech tagging lets us encode information not + only about a word’s definition, but also its use in context (for + example the words “ship” and “shop” can be either a verb or a noun, + depending on the context). + + The PosTagVisualizer creates a bar chart to visualize the relative + proportions of different parts-of-speech in a corpus. + + Note that the PosTagVisualizer requires documents to already be + part-of-speech tagged; the visualizer expects the corpus to come in + the form of a list of (document) lists of (sentence) lists of + (tag, token) tuples. Parameters ---------- + ax : matplotlib axes + The axes to plot the figure on. + + tagset: string + The tagset that was used to perform part-of-speech tagging. + Either "penn_treebank" or "universal", defaults to "penn_treebank". + Use "universal" if corpus has been tagged using SpaCy. + + colors : list or tuple of strings + Specify the colors for each individual part-of-speech. Will override + colormap if both are provided. + + colormap : string or matplotlib cmap + Specify a colormap to color the parts-of-speech. + + frequency: bool {True, False}, default: False + If set to True, part-of-speech tags will be plotted according to frequency, + from most to least frequent. + + stack : bool {True, False}, default : False + Plot the PosTag frequency chart as a per-class stacked bar chart. + Note that fit() requires y for this visualization. + kwargs : dict - Pass any additional keyword arguments to the super class. - cmap : dict - ANSII colormap + Pass any additional keyword arguments to the PosTagVisualizer. - These parameters can be influenced later on in the visualization - process, but can and should be set as early as possible. + Attributes + ---------- + pos_tag_counts_: dict + Mapping of part-of-speech tags to counts. + + Examples + -------- + >>> viz = PosTagVisualizer() + >>> viz.fit(X) + >>> viz.poof() """ - def __init__(self, ax=None, **kwargs): + + def __init__( + self, + ax=None, + tagset="penn_treebank", + colormap=None, + colors=None, + frequency=False, + stack=False, + **kwargs + ): + super(PosTagVisualizer, self).__init__(ax=ax, **kwargs) + + self.tagset_names = TAGSET_NAMES + + if tagset not in self.tagset_names: + raise YellowbrickValueError( + ("'{}' is an invalid tagset. Please choose one of {}.").format( + tagset, ", ".join(self.tagset_names.keys()) + ) + ) + else: + self.tagset = tagset + + self.punct_tags = frozenset(PUNCT_TAGS) + self.frequency = frequency + self.colormap = colormap + self.colors = colors + self.stack = stack + + def fit(self, X, y=None, **kwargs): """ - Initializes the base frequency distributions with many - of the options required in order to make this - visualization work. + Fits the corpus to the appropriate tag map. + Text documents must be tokenized & tagged before passing to fit. + + Parameters + ---------- + X : list or generator + Should be provided as a list of documents or a generator + that yields a list of documents that contain a list of + sentences that contain (token, tag) tuples. + + y : ndarray or Series of length n + An optional array of target values that are ignored by the + visualizer. + + kwargs : dict + Pass generic arguments to the drawing method + + Returns + ------- + self : instance + Returns the instance of the transformer/visualizer """ - super(PosTagVisualizer, self).__init__(ax=ax, **kwargs) + self.labels_ = ["documents"] + if self.stack: + if y is None: + raise YellowbrickValueError("Specify y for stack=True") + self.labels_ = np.unique(y) - # TODO: hard-coding in the ANSII colormap for now. - # Can we let the user reset the colors here? - self.COLORS = { - 'white' : "\033[0;37m{}\033[0m", - 'yellow' : "\033[0;33m{}\033[0m", - 'green' : "\033[0;32m{}\033[0m", - 'blue' : "\033[0;34m{}\033[0m", - 'cyan' : "\033[0;36m{}\033[0m", - 'red' : "\033[0;31m{}\033[0m", - 'magenta' : "\033[0;35m{}\033[0m", - 'black' : "\033[0;30m{}\033[0m", - 'darkwhite' : "\033[1;37m{}\033[0m", - 'darkyellow' : "\033[1;33m{}\033[0m", - 'darkgreen' : "\033[1;32m{}\033[0m", - 'darkblue' : "\033[1;34m{}\033[0m", - 'darkcyan' : "\033[1;36m{}\033[0m", - 'darkred' : "\033[1;31m{}\033[0m", - 'darkmagenta': "\033[1;35m{}\033[0m", - 'darkblack' : "\033[1;30m{}\033[0m", - None : "\033[0;0m{}\033[0m" - } + if self.tagset == "penn_treebank": + self.pos_tag_counts_ = self._penn_tag_map() + self._handle_treebank(X, y) - self.TAGS = { - 'NN' : 'green', - 'NNS' : 'green', - 'NNP' : 'green', - 'NNPS' : 'green', - 'VB' : 'blue', - 'VBD' : 'blue', - 'VBG' : 'blue', - 'VBN' : 'blue', - 'VBP' : 'blue', - 'VBZ' : 'blue', - 'JJ' : 'red', - 'JJR' : 'red', - 'JJS' : 'red', - 'RB' : 'cyan', - 'RBR' : 'cyan', - 'RBS' : 'cyan', - 'IN' : 'darkwhite', - 'POS' : 'darkyellow', - 'PRP$' : 'magenta', - 'DT' : 'black', - 'CC' : 'black', - 'CD' : 'black', - 'WDT' : 'black', - 'WP' : 'black', - 'WP$' : 'black', - 'WRB' : 'black', - 'EX' : 'yellow', - 'FW' : 'yellow', - 'LS' : 'yellow', - 'MD' : 'yellow', - 'PDT' : 'yellow', - 'RP' : 'yellow', - 'SYM' : 'yellow', - 'TO' : 'yellow', - 'None' : 'off' - } + elif self.tagset == "universal": + self.pos_tag_counts_ = self._uni_tag_map() + self._handle_universal(X, y) + + self.draw() + + return self + + def _penn_tag_map(self): + """ + Returns a Penn Treebank part-of-speech tag map. + """ + self._pos_tags = PENN_TAGS + return self._make_tag_map(PENN_TAGS) + + def _uni_tag_map(self): + """ + Returns a Universal Dependencies part-of-speech tag map. + """ + self._pos_tags = UNIVERSAL_TAGS + return self._make_tag_map(UNIVERSAL_TAGS) + + def _make_tag_map(self, tagset): + """ + Returns a map of the tagset to a counter unless stack=True then returns + a map of labels to a map of tagset to counters. + """ + # ensures the dict contains a zero counter per tag + zeros = [0] * len(tagset) + return {label: dict(zip(tagset, zeros)) for label in self.labels_} + return dict(zip(tagset, zeros)) - def colorize(self, token, color): + def _handle_universal(self, X, y=None): """ - Colorize text + Scan through the corpus to compute counts of each Universal + Dependencies part-of-speech. Parameters ---------- - token : str - A str representation of + X : list or generator + Should be provided as a list of documents or a generator + that yields a list of documents that contain a list of + sentences that contain (token, tag) tuples. + """ + jump = { + # combine proper and regular nouns + "NOUN": "noun", + "PROPN": "noun", + "ADJ": "adjective", + "VERB": "verb", + # include particles with adverbs + "ADV": "adverb", + "PART": "adverb", + "ADP": "adposition", + "PRON": "pronoun", + "CCONJ": "conjunction", + "PUNCT": "punctuation", + "DET": "determiner", + "NUM": "number", + "INTJ": "interjection", + "SYM": "symbol", + } + + for idx, tagged_doc in enumerate(X): + for tagged_sent in tagged_doc: + for _, tag in tagged_sent: + if tag == "SPACE": + continue + if self.stack: + counter = self.pos_tag_counts_[y[idx]] + else: + counter = self.pos_tag_counts_["documents"] + + counter[jump.get(tag, "other")] += 1 + + def _handle_treebank(self, X, y=None): + """ + Create a part-of-speech tag mapping using the Penn Treebank tags + Parameters + ---------- + X : list or generator + Should be provided as a list of documents or a generator + that yields a list of documents that contain a list of + sentences that contain (token, tag) tuples. """ - return self.COLORS[color].format(token) + for idx, tagged_doc in enumerate(X): + for tagged_sent in tagged_doc: + for _, tag in tagged_sent: + if self.stack: + counter = self.pos_tag_counts_[y[idx]] + else: + counter = self.pos_tag_counts_["documents"] - def transform(self, tagged_tuples): + if tag.startswith("N"): + counter["noun"] += 1 + elif tag.startswith("J"): + counter["adjective"] += 1 + elif tag.startswith("V"): + counter["verb"] += 1 + # include particles with adverbs + elif tag.startswith("RB") or tag == "RP": + counter["adverb"] += 1 + elif tag.startswith("PR"): + counter["pronoun"] += 1 + elif tag.startswith("W"): + counter["wh- word"] += 1 + elif tag == "CC": + counter["conjunction"] += 1 + elif tag == "CD": + counter["digit"] += 1 + # combine predeterminer and determiner + elif tag in ["DT" or "PDT"]: + counter["determiner"] += 1 + elif tag == "EX": + counter["existential"] += 1 + elif tag == "FW": + counter["non-English"] += 1 + elif tag == "IN": + counter["preposition"] += 1 + elif tag == "POS": + counter["possessive"] += 1 + elif tag == "LS": + counter["list"] += 1 + elif tag == "MD": + counter["modal"] += 1 + elif tag in self.punct_tags: + counter["punctuation"] += 1 + elif tag == "TO": + counter["infinitive"] += 1 + elif tag == "UH": + counter["interjection"] += 1 + elif tag == "SYM": + counter["symbol"] += 1 + else: + counter["other"] += 1 + + def draw(self, **kwargs): """ - The transform method transforms the raw text input for the - part-of-speech tagging visualization. It requires that - documents be in the form of (tag, token) tuples. + Called from the fit method, this method creates the canvas and + draws the part-of-speech tag mapping as a bar chart. Parameters ---------- - tagged_token_tuples : list of tuples - A list of (tag, token) tuples + kwargs: dict + generic keyword arguments. - Text documents must be tokenized and tagged before passing to fit() + Returns + ------- + ax : matplotlib axes + Axes on which the PosTagVisualizer was drawn. """ - self.tagged = [ - (self.TAGS.get(tag),tok) for tok, tag in tagged_tuples - ] - # - # print(' '.join((colorize(token, color) for color, token in self.tagged))) - # print('\n') + # Converts nested dict to nested list + pos_tag_counts = np.array( + [list(i.values()) for i in self.pos_tag_counts_.values()] + ) + # stores sum of nested list column wise + pos_tag_sum = np.sum(pos_tag_counts, axis=0) + + if self.frequency: + # sorts the count and tags by sum for frequency true + idx = (pos_tag_sum).argsort()[::-1] + self._pos_tags = np.array(self._pos_tags)[idx] + pos_tag_counts = pos_tag_counts[:, idx] + + if self.stack: + bar_stack( + pos_tag_counts, + ax=self.ax, + labels=list(self.labels_), + ticks=self._pos_tags, + colors=self.colors, + colormap=self.colormap, + ) + else: + xidx = np.arange(len(self._pos_tags)) + colors = resolve_colors( + n_colors=len(self._pos_tags), colormap=self.colormap, colors=self.colors + ) + self.ax.bar(xidx, pos_tag_counts[0], color=colors) + + return self.ax + + def finalize(self, **kwargs): + """ + Finalize the plot with ticks, labels, and title + + Parameters + ---------- + kwargs: dict + generic keyword arguments. + """ + # NOTE: not deduping here, so this is total, not unique + self.ax.set_ylabel("Count") + + if self.frequency: + self.ax.set_xlabel( + "{} part-of-speech tags, sorted by frequency".format( + self.tagset_names[self.tagset] + ) + ) + else: + self.ax.set_xlabel( + "{} part-of-speech tags".format(self.tagset_names[self.tagset]) + ) + + # bar stack(helper) sets the ticks if stack is true + if not self.stack: + self.ax.set_xticks(range(len(self._pos_tags))) + self.ax.set_xticklabels(self._pos_tags, rotation=90) + + self.set_title( + "PosTag plot for {}-token corpus".format( + (sum([sum(i.values()) for i in self.pos_tag_counts_.values()])) + ) + ) + + def poof(self, outpath=None, **kwargs): + if outpath is not None: + kwargs["bbox_inches"] = kwargs.get("bbox_inches", "tight") + return super(PosTagVisualizer, self).poof(outpath, **kwargs) + + +########################################################################## +## Quick Method +########################################################################## + + +def postag( + X, + y=None, + ax=None, + tagset="penn_treebank", + colormap=None, + colors=None, + frequency=False, + stack=False, + **kwargs +): + + """ + Display a barchart with the counts of different parts of speech + in X, which consists of a part-of-speech-tagged corpus, which the + visualizer expects to be a list of lists of lists of (token, tag) + tuples. + + Parameters + ---------- + X : list or generator + Should be provided as a list of documents or a generator + that yields a list of documents that contain a list of + sentences that contain (token, tag) tuples. + + ax : matplotlib axes + The axes to plot the figure on. + + tagset: string + The tagset that was used to perform part-of-speech tagging. + Either "penn_treebank" or "universal", defaults to "penn_treebank". + Use "universal" if corpus has been tagged using SpaCy. + + colors : list or tuple of colors + Specify the colors for each individual part-of-speech. + + colormap : string or matplotlib cmap + Specify a colormap to color the parts-of-speech. + + frequency: bool {True, False}, default: False + If set to True, part-of-speech tags will be plotted according to frequency, + from most to least frequent. + + kwargs : dict + Pass any additional keyword arguments to the PosTagVisualizer. + + Returns + ------- + visualizer: PosTagVisualizer + Returns the fitted, finalized visualizer + """ + # Instantiate the visualizer + visualizer = PosTagVisualizer( + ax=ax, + tagset=tagset, + colors=colors, + colormap=colormap, + frequency=frequency, + stack=stack, + **kwargs + ) + + # Fit and transform the visualizer (calls draw) + visualizer.fit(X, y=y, **kwargs) + visualizer.finalize() + + # Return the visualizer object + return visualizer diff --git a/yellowbrick/text/tsne.py b/yellowbrick/text/tsne.py index d60f7a24b..50ccaf435 100644 --- a/yellowbrick/text/tsne.py +++ b/yellowbrick/text/tsne.py @@ -1,11 +1,11 @@ # yellowbrick.text.tsne # Implements TSNE visualizations of documents in 2D space. # -# Author: Benjamin Bengfort -# Author: Rebecca Bilbro +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Mon Feb 20 06:33:29 2017 -0500 # -# Copyright (C) 2016 Bengfort.com +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: tsne.py [6aa9198] benjamin@bengfort.com $ @@ -35,8 +35,19 @@ ## Quick Methods ########################################################################## -def tsne(X, y=None, ax=None, decompose='svd', decompose_by=50, classes=None, - colors=None, colormap=None, alpha=0.7, **kwargs): + +def tsne( + X, + y=None, + ax=None, + decompose="svd", + decompose_by=50, + classes=None, + colors=None, + colormap=None, + alpha=0.7, + **kwargs +): """ Display a projection of a vectorized corpus in two dimensions using TSNE, a nonlinear dimensionality reduction method that is particularly well @@ -88,26 +99,35 @@ def tsne(X, y=None, ax=None, decompose='svd', decompose_by=50, classes=None, Returns ------- - ax : matplotlib axes - Returns the axes that the parallel coordinates were drawn on. + visualizer: TSNEVisualizer + Returns the fitted, finalized visualizer """ # Instantiate the visualizer visualizer = TSNEVisualizer( - ax, decompose, decompose_by, classes, colors, colormap, alpha, **kwargs + ax=ax, + decompose=decompose, + decompose_by=decompose_by, + classes=classes, + colors=colors, + colormap=colormap, + alpha=alpha, + **kwargs ) # Fit and transform the visualizer (calls draw) visualizer.fit(X, y, **kwargs) visualizer.transform(X) + visualizer.finalize() - # Return the axes object on the visualizer - return visualizer.ax + # Return the visualizer object + return visualizer ########################################################################## ## TSNEVisualizer ########################################################################## + class TSNEVisualizer(TextVisualizer): """ Display a projection of a vectorized corpus in two dimensions using TSNE, @@ -175,9 +195,19 @@ class TSNEVisualizer(TextVisualizer): # NOTE: cannot be np.nan NULL_CLASS = None - def __init__(self, ax=None, decompose='svd', decompose_by=50, - labels=None, classes=None, colors=None, colormap=None, - random_state=None, alpha=0.7, **kwargs): + def __init__( + self, + ax=None, + decompose="svd", + decompose_by=50, + labels=None, + classes=None, + colors=None, + colormap=None, + random_state=None, + alpha=0.7, + **kwargs + ): # Visual Parameters self.alpha = alpha @@ -188,16 +218,14 @@ def __init__(self, ax=None, decompose='svd', decompose_by=50, # Fetch TSNE kwargs from kwargs by popping only keys belonging to TSNE params tsne_kwargs = { - key: kwargs.pop(key) - for key in TSNE().get_params() - if key in kwargs + key: kwargs.pop(key) for key in TSNE().get_params() if key in kwargs } self.transformer_ = self.make_transformer(decompose, decompose_by, tsne_kwargs) # Call super at the end so that size and title are set correctly super(TSNEVisualizer, self).__init__(ax=ax, **kwargs) - def make_transformer(self, decompose='svd', decompose_by=50, tsne_kwargs={}): + def make_transformer(self, decompose="svd", decompose_by=50, tsne_kwargs={}): """ Creates an internal transformer pipeline to project the data set into 2D space using TSNE, applying an pre-decomposition technique ahead of @@ -226,10 +254,7 @@ def make_transformer(self, decompose='svd', decompose_by=50, tsne_kwargs={}): # TODO: detect decompose by inferring from sparse matrix or dense or # If number of features > 50 etc. - decompositions = { - 'svd': TruncatedSVD, - 'pca': PCA, - } + decompositions = {"svd": TruncatedSVD, "pca": PCA} if decompose and decompose.lower() not in decompositions: raise YellowbrickValueError( @@ -244,12 +269,20 @@ def make_transformer(self, decompose='svd', decompose_by=50, tsne_kwargs={}): # Add the pre-decomposition if decompose: klass = decompositions[decompose] - steps.append((decompose, klass( - n_components=decompose_by, random_state=self.random_state))) + steps.append( + ( + decompose, + klass(n_components=decompose_by, random_state=self.random_state), + ) + ) # Add the TSNE manifold - steps.append(('tsne', TSNE( - n_components=2, random_state=self.random_state, **tsne_kwargs))) + steps.append( + ( + "tsne", + TSNE(n_components=2, random_state=self.random_state, **tsne_kwargs), + ) + ) # return the pipeline return Pipeline(steps) @@ -312,15 +345,17 @@ def draw(self, points, target=None, **kwargs): # Resolve the labels with the classes labels = self.labels if self.labels is not None else self.classes_ if len(labels) != len(self.classes_): - raise YellowbrickValueError(( - "number of supplied labels ({}) does not " - "match the number of classes ({})" - ).format(len(labels), len(self.classes_))) - + raise YellowbrickValueError( + ( + "number of supplied labels ({}) does not " + "match the number of classes ({})" + ).format(len(labels), len(self.classes_)) + ) # Create the color mapping for the labels. self.color_values_ = resolve_colors( - n_colors=len(labels), colormap=self.colormap, colors=self.color) + n_colors=len(labels), colormap=self.colormap, colors=self.colors + ) colors = dict(zip(labels, self.color_values_)) # Transform labels into a map of class to label @@ -329,34 +364,33 @@ def draw(self, points, target=None, **kwargs): # Expand the points into vectors of x and y for scatter plotting, # assigning them to their label if the label has been passed in. # Additionally, filter classes not specified directly by the user. - series = defaultdict(lambda: {'x':[], 'y':[]}) + series = defaultdict(lambda: {"x": [], "y": []}) if target is not None: for t, point in zip(target, points): label = labels[t] - series[label]['x'].append(point[0]) - series[label]['y'].append(point[1]) + series[label]["x"].append(point[0]) + series[label]["y"].append(point[1]) else: label = self.classes_[0] - for x,y in points: - series[label]['x'].append(x) - series[label]['y'].append(y) + for x, y in points: + series[label]["x"].append(x) + series[label]["y"].append(y) # Plot the points for label, points in series.items(): self.ax.scatter( - points['x'], points['y'], c=colors[label], - alpha=self.alpha, label=label + points["x"], points["y"], c=colors[label], alpha=self.alpha, label=label ) + return self.ax + def finalize(self, **kwargs): """ Finalize the drawing by adding a title and legend, and removing the axes objects that do not convey information about TNSE. """ - self.set_title( - "TSNE Projection of {} Documents".format(self.n_instances_) - ) + self.set_title("TSNE Projection of {} Documents".format(self.n_instances_)) # Remove the ticks self.ax.set_yticks([]) @@ -367,6 +401,9 @@ def finalize(self, **kwargs): box = self.ax.get_position() self.ax.set_position([box.x0, box.y0, box.width * 0.8, box.height]) manual_legend( - self, self.classes_, self.color_values_, - loc='center left', bbox_to_anchor=(1, 0.5) + self, + self.classes_, + self.color_values_, + loc="center left", + bbox_to_anchor=(1, 0.5), ) diff --git a/yellowbrick/text/umap_vis.py b/yellowbrick/text/umap_vis.py new file mode 100644 index 000000000..e3545e3ac --- /dev/null +++ b/yellowbrick/text/umap_vis.py @@ -0,0 +1,367 @@ +# yellowbrick.text.umap_vis +# Implements UMAP visualizations of documents in 2D space. +# +# Author: John Healy +# Created: Mon Dec 03 14:00:00 2018 -0500 +# +# Copyright (C) 2019 The sckit-yb developers +# For license information, see LICENSE.txt +# +# ID: umap_vis.py [73a44e5] jchealy@gmail.com $ + +""" +Implements UMAP visualizations of documents in 2D space. +""" + +########################################################################## +## Imports +########################################################################## + +import warnings +import numpy as np + +from collections import defaultdict + +from yellowbrick.draw import manual_legend +from yellowbrick.text.base import TextVisualizer +from yellowbrick.style.colors import resolve_colors +from yellowbrick.exceptions import YellowbrickValueError + +from sklearn.pipeline import Pipeline + +try: + from umap import UMAP +except ImportError: + UMAP = None +except (RuntimeError, AttributeError): + UMAP = None + warnings.warn( + "Error Importing UMAP. UMAP does not support python 2.7 on Windows 32 bit." + ) + +########################################################################## +## Quick Methods +########################################################################## + + +def umap( + X, y=None, ax=None, classes=None, colors=None, colormap=None, alpha=0.7, **kwargs +): + """ + Display a projection of a vectorized corpus in two dimensions using UMAP (Uniform + Manifold Approximation and Projection), a nonlinear dimensionality reduction method + that is particularly well suited to embedding in two or three dimensions for + visualization as a scatter plot. UMAP is a relatively new technique but is often + used to visualize clusters or groups of data points and their relative proximities. + It typically is fast, scalable, and can be applied directly to sparse matrices + eliminating the need to run a ``TruncatedSVD`` as a pre-processing step. + + The current default for UMAP is Euclidean distance. Hellinger distance would be a + more appropriate distance function to use with CountVectorize data. That will be + released in a forthcoming version of UMAP. In the meantime cosine distance is likely + a better text default that Euclidean and can be set using the keyword argument + ``metric='cosine'``. + + Parameters + ---------- + + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features representing the corpus of + vectorized documents to visualize with umap. + + y : ndarray or Series of length n + An optional array or series of target or class values for instances. + If this is specified, then the points will be colored according to + their class. Often cluster labels are passed in to color the documents + in cluster space, so this method is used both for classification and + clustering methods. + + ax : matplotlib axes + The axes to plot the figure on. + + classes : list of strings + The names of the classes in the target, used to create a legend. + + colors : list or tuple of colors + Specify the colors for each individual class + + colormap : string or matplotlib cmap + Sequential colormap for continuous target + + alpha : float, default: 0.7 + Specify a transparency where 1 is completely opaque and 0 is completely + transparent. This property makes densely clustered points more visible. + + kwargs : dict + Pass any additional keyword arguments to the UMAP transformer. + + ------- + visualizer: UMAPVisualizer + Returns the fitted, finalized visualizer + """ + # Instantiate the visualizer + visualizer = UMAPVisualizer(ax, classes, colors, colormap, alpha, **kwargs) + + # Fit and transform the visualizer (calls draw) + visualizer.fit_transform(X, y, **kwargs) + visualizer.finalize() + + # Return the visualizer object + return visualizer + + +########################################################################## +## UMAPVisualizer +########################################################################## + + +class UMAPVisualizer(TextVisualizer): + """ + Display a projection of a vectorized corpus in two dimensions using UMAP (Uniform + Manifold Approximation and Projection), a nonlinear dimensionality reduction method + that is particularly well suited to embedding in two or three dimensions for + visualization as a scatter plot. UMAP is a relatively new technique but is often + used to visualize clusters or groups of data points and their relative proximities. + It typically is fast, scalable, and can be applied directly to sparse matrices + eliminating the need to run a ``TruncatedSVD`` as a pre-processing step. + + The current default for UMAP is Euclidean distance. Hellinger distance would be a + more appropriate distance function to use with CountVectorize data. That will be + released in a forthcoming version of UMAP. In the meantime cosine distance is likely + a better text default that Euclidean and can be set using the keyword argument + ``metric='cosine'``. + + For more, see https://github.com/lmcinnes/umap + + Parameters + ---------- + + ax : matplotlib axes + The axes to plot the figure on. + + labels : list of strings + The names of the classes in the target, used to create a legend. + Labels must match names of classes in sorted order. + + colors : list or tuple of colors + Specify the colors for each individual class + + colormap : string or matplotlib cmap + Sequential colormap for continuous target + + random_state : int, RandomState instance or None, optional, default: None + If int, random_state is the seed used by the random number generator; + If RandomState instance, random_state is the random number generator; + If None, the random number generator is the RandomState instance used + by np.random. The random state is applied to the preliminary + decomposition as well as UMAP. + + alpha : float, default: 0.7 + Specify a transparency where 1 is completely opaque and 0 is completely + transparent. This property makes densely clustered points more visible. + + kwargs : dict + Pass any additional keyword arguments to the UMAP transformer. + + Examples + -------- + + >>> model = MyVisualizer(metric='cosine') + >>> model.fit(X) + >>> model.poof() + + """ + + # NOTE: cannot be np.nan + NULL_CLASS = None + + def __init__( + self, + ax=None, + labels=None, + classes=None, + colors=None, + colormap=None, + random_state=None, + alpha=0.7, + **kwargs + ): + + if UMAP is None: + raise YellowbrickValueError( + ( + "umap package doesn't seem to be installed." + "Please install UMAP via: pip install umap-learn" + ) + ) + + # Visual Parameters + self.alpha = alpha + self.labels = labels + self.colors = colors + self.colormap = colormap + self.random_state = random_state + + # Fetch UMAP kwargs from kwargs by popping only keys belonging to UMAP params + umap_kwargs = { + key: kwargs.pop(key) for key in UMAP().get_params() if key in kwargs + } + + # UMAP doesn't require any pre-processing before embedding and thus doesn't + # require a pipeline. + self.transformer_ = self.make_transformer(umap_kwargs) + + # Call super at the end so that size and title are set correctly + super(UMAPVisualizer, self).__init__(ax=ax, **kwargs) + + def make_transformer(self, umap_kwargs={}): + """ + Creates an internal transformer pipeline to project the data set into + 2D space using UMAP. This method will reset the transformer on the + class. + + Parameters + ---------- + umap_kwargs : dict + Keyword arguments for the internal UMAP transformer + + Returns + ------- + transformer : Pipeline + Pipelined transformer for UMAP projections + """ + + # Create the pipeline steps + steps = [] + + # Add the UMAP manifold + steps.append( + ( + "umap", + UMAP(n_components=2, random_state=self.random_state, **umap_kwargs), + ) + ) + + # return the pipeline + return Pipeline(steps) + + def fit(self, X, y=None, **kwargs): + """ + The fit method is the primary drawing input for the UMAP projection + since the visualization requires both X and an optional y value. The + fit method expects an array of numeric vectors, so text documents must + be vectorized before passing them to this method. + + Parameters + ---------- + X : ndarray or DataFrame of shape n x m + A matrix of n instances with m features representing the corpus of + vectorized documents to visualize with UMAP. + + y : ndarray or Series of length n + An optional array or series of target or class values for + instances. If this is specified, then the points will be colored + according to their class. Often cluster labels are passed in to + color the documents in cluster space, so this method is used both + for classification and clustering methods. + + kwargs : dict + Pass generic arguments to the drawing method + + Returns + ------- + self : instance + Returns the instance of the transformer/visualizer + """ + + # Store the classes we observed in y + if y is not None: + self.classes_ = np.unique(y) + elif y is None and self.labels is not None: + self.classes_ = np.array([self.labels[0]]) + else: + self.classes_ = np.array([self.NULL_CLASS]) + + # Fit our internal transformer and transform the data. + vecs = self.transformer_.fit_transform(X) + self.n_instances_ = vecs.shape[0] + + # Draw the vectors + self.draw(vecs, y, **kwargs) + + # Fit always returns self. + return self + + def draw(self, points, target=None, **kwargs): + """ + Called from the fit method, this method draws the UMAP scatter plot, + from a set of decomposed points in 2 dimensions. This method also + accepts a third dimension, target, which is used to specify the colors + of each of the points. If the target is not specified, then the points + are plotted as a single cloud to show similar documents. + """ + # Resolve the labels with the classes + labels = self.labels if self.labels is not None else self.classes_ + if len(labels) != len(self.classes_): + raise YellowbrickValueError( + ( + "number of supplied labels ({}) does not " + "match the number of classes ({})" + ).format(len(labels), len(self.classes_)) + ) + + # Create the color mapping for the labels. + self.color_values_ = resolve_colors( + n_colors=len(labels), colormap=self.colormap, colors=self.colors + ) + colors = dict(zip(labels, self.color_values_)) + + # Transform labels into a map of class to label + labels = dict(zip(self.classes_, labels)) + + # Expand the points into vectors of x and y for scatter plotting, + # assigning them to their label if the label has been passed in. + # Additionally, filter classes not specified directly by the user. + series = defaultdict(lambda: {"x": [], "y": []}) + + if target is not None: + for t, point in zip(target, points): + label = labels[t] + series[label]["x"].append(point[0]) + series[label]["y"].append(point[1]) + else: + label = self.classes_[0] + for x, y in points: + series[label]["x"].append(x) + series[label]["y"].append(y) + + # Plot the points + for label, points in series.items(): + self.ax.scatter( + points["x"], points["y"], c=colors[label], alpha=self.alpha, label=label + ) + + return self.ax + + def finalize(self, **kwargs): + """ + Finalize the drawing by adding a title and legend, and removing the + axes objects that do not convey information about UMAP. + """ + self.set_title("UMAP Projection of {} Documents".format(self.n_instances_)) + + # Remove the ticks + self.ax.set_yticks([]) + self.ax.set_xticks([]) + + # Add the legend outside of the figure box. + if not all(self.classes_ == np.array([self.NULL_CLASS])): + box = self.ax.get_position() + self.ax.set_position([box.x0, box.y0, box.width * 0.8, box.height]) + manual_legend( + self, + self.classes_, + self.color_values_, + loc="center left", + bbox_to_anchor=(1, 0.5), + ) diff --git a/yellowbrick/utils/__init__.py b/yellowbrick/utils/__init__.py index 864802bf3..b3dc53916 100644 --- a/yellowbrick/utils/__init__.py +++ b/yellowbrick/utils/__init__.py @@ -1,13 +1,13 @@ # yellowbrick.utils # Utility functions and helpers for the Yellowbrick library. # -# Author: Jason Keung -# Author: Patrick O'Melveny -# Author: Benjamin Bengfort -# Author: Rebecca Bilbro +# Author: Jason Keung +# Author: Patrick O'Melveny +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Thu Jun 02 15:33:18 2016 -0500 # -# Copyright (C) 2016 District Data LAbs +# Copyright (C) 2016 The scikit-yb developers # For license information, see LICENSE.txt # # ID: __init__.py [79cd8cf] benjamin@bengfort.com $ @@ -22,3 +22,4 @@ from .helpers import * from .types import * +from .kneed import * diff --git a/yellowbrick/utils/decorators.py b/yellowbrick/utils/decorators.py index 81315fed9..73bb32c0d 100644 --- a/yellowbrick/utils/decorators.py +++ b/yellowbrick/utils/decorators.py @@ -1,10 +1,10 @@ # yellowbrick.utils.decorators # Decorators and descriptors for annotating yellowbrick library functions. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Thu May 18 15:13:33 2017 -0400 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The sckit-yb developers # For license information, see LICENSE.txt # # ID: decorators.py [79cd8cf] benjamin@bengfort.com $ @@ -24,6 +24,7 @@ ## Decorators ########################################################################## + def memoized(fget): """ Return a property attribute for new-style classes that only calls its @@ -40,7 +41,7 @@ def memoized(fget): python-memoized-property `python-memoized-property `_ """ - attr_name = '_{0}'.format(fget.__name__) + attr_name = "_{0}".format(fget.__name__) @wraps(fget) def fget_memoized(self): diff --git a/yellowbrick/utils/helpers.py b/yellowbrick/utils/helpers.py index 8882cbc85..01294ab47 100644 --- a/yellowbrick/utils/helpers.py +++ b/yellowbrick/utils/helpers.py @@ -1,10 +1,11 @@ # yellowbrick.utils.helpers # Helper functions and generic utilities for use in Yellowbrick code. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort +# Author: Rebecca Bilbro # Created: Fri May 19 10:39:30 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2019 The scikit-yb developers # For license information, see LICENSE.txt # # ID: helpers.py [79cd8cf] benjamin@bengfort.com $ @@ -17,21 +18,103 @@ ## Imports ########################################################################## -from __future__ import division - import re +import sklearn import numpy as np from sklearn.pipeline import Pipeline +from sklearn.utils.validation import check_is_fitted -from .types import is_estimator +from yellowbrick.utils.types import is_estimator from yellowbrick.exceptions import YellowbrickTypeError - ########################################################################## ## Model and Feature Information ########################################################################## + +def is_fitted(estimator): + """ + In order to ensure that we don't call ``fit`` on an already-fitted model, + this utility function calls ``predict`` on the estimator, returning ``False`` + if it raises a ``sklearn.exceptions.NotFittedError`` and ``True`` otherwise. + + NOTE: This is the solution proposed to scikit-yb: https://bit.ly/2LWQxZO (see + also: https://stackoverflow.com/a/39900933/6552250), though it remains unclear + how it will perform with sklearn-style Estimators and Transformers from other + 3rd party libraries like Keras, XGBoost, etc. + """ + try: + estimator.predict(np.zeros((7, 3))) + except sklearn.exceptions.NotFittedError: + return False + except AttributeError: + # Some clustering models (LDA, PCA, Agglomerative) don't implement ``predict`` + try: + check_is_fitted( + estimator, + [ + "coef_", + "estimator_", + "labels_", + "n_clusters_", + "children_", + "components_", + "n_components_", + "n_iter_", + "n_batch_iter_", + "explained_variance_", + "singular_values_", + "mean_", + ], + all_or_any=any, + ) + return True + except sklearn.exceptions.NotFittedError: + return False + except Exception: + # Assume it's fitted, since ``NotFittedError`` wasn't raised + return True + + return True + + +def check_fitted(estimator, is_fitted_by="auto", **kwargs): + """ + Determines whether or not to check if the model has been fitted, and will return + ``True`` if so. The ``is_fitted_by`` argument is set to ``'auto'`` by default, + such that the check leaves it to the ``is_fitted`` helper method to determine if + a ``NotFitted`` error is raised. However, if the user prefers to override this + automatic functionality (e.g. if a 3rd party sklearn-like estimator has been used + that doesn't precisely implement the sklearn API), and ``is_fitted_by`` has been + set to either ``True`` or ``False``, we assume the user has supplied the necessary + information about whether or not the model is fit using the Visualizer's + ``is_fitted`` parameter. + + .. todo:: add other measures for checking if an estimator is fitted e.g. by coefs + + Parameters + ----------- + estimator : sklearn.Estimator + The model to check fittedness + + is_fitted_by : bool or str, default: 'auto' + If bool, that value is returned, otherwise ``is_fitted`` is used to check + for an exception + + kwargs : dict + Other optional parameters specific to the ``is_fitted_by`` mechanism. + + Returns + -------- + is_fitted : bool + Whether or not the model is already fitted + """ + if isinstance(is_fitted_by, str) and is_fitted_by.lower() == "auto": + return is_fitted(estimator) + return bool(is_fitted_by) + + def get_model_name(model): """ Detects the model name for a Scikit-Learn model or pipeline. @@ -50,9 +133,7 @@ def get_model_name(model): """ if not is_estimator(model): raise YellowbrickTypeError( - "Cannot detect the model name for non estimator: '{}'".format( - type(model) - ) + "Cannot detect the model name for non estimator: '{}'".format(type(model)) ) else: @@ -65,12 +146,15 @@ def get_model_name(model): def has_ndarray_int_columns(features, X): """ Checks if numeric feature columns exist in ndarray """ _, ncols = X.shape - if not all(d.isdigit() for d in features if isinstance(d, str)) or not isinstance(X, np.ndarray): + if not all(d.isdigit() for d in features if isinstance(d, str)) or not isinstance( + X, np.ndarray + ): return False ndarray_columns = np.arange(0, ncols) feature_cols = np.unique([int(d) for d in features]) return all(np.in1d(feature_cols, ndarray_columns)) + # Alias for closer name to isinstance and issubclass hasndarrayintcolumns = has_ndarray_int_columns @@ -88,7 +172,7 @@ def is_monotonic(a, increasing=True): Test if the array is montonically increasing, otherwise test if the array is montonically decreasing. """ - a = np.asarray(a) # ensure a is array-like + a = np.asarray(a) # ensure a is array-like if a.ndim > 1: raise ValueError("not supported for multi-dimensonal arrays") @@ -105,8 +189,8 @@ def is_monotonic(a, increasing=True): ## Numeric Computations ########################################################################## -#From here: http://stackoverflow.com/questions/26248654/numpy-return-0-with-divide-by-zero -def div_safe( numerator, denominator ): +# From here: https://bit.ly/2xR64lI +def div_safe(numerator, denominator): """ Ufunc-extension that returns 0 instead of nan when dividing numpy arrays @@ -120,15 +204,15 @@ def div_safe( numerator, denominator ): example: div_safe( [-1, 0, 1], 0 ) == [0, 0, 0] """ - #First handle scalars + # First handle scalars if np.isscalar(numerator): raise ValueError("div_safe should only be used with an array-like numerator") - #Then numpy arrays + # Then numpy arrays try: - with np.errstate(divide='ignore', invalid='ignore'): - result = np.true_divide( numerator, denominator ) - result[ ~ np.isfinite( result )] = 0 # -inf inf NaN + with np.errstate(divide="ignore", invalid="ignore"): + result = np.true_divide(numerator, denominator) + result[~np.isfinite(result)] = 0 # -inf inf NaN return result except ValueError as e: raise e @@ -178,14 +262,14 @@ def prop_to_size(vals, mi=0.0, ma=5.0, power=0.5, log=False): if delta == 0.0: delta = 1.0 - return mi + (ma-mi) * ((vals -vals.min()) / delta) ** power - + return mi + (ma - mi) * ((vals - vals.min()) / delta) ** power ########################################################################## -## String Computations +# String Computations ########################################################################## + def slugify(text): """ Returns a slug of given text, normalizing unicode data for file-safe @@ -201,8 +285,8 @@ def slugify(text): slug : string A normalized slug representation of the text - .. seealso:: http://yashchandra.com/2014/05/08/how-to-generate-clean-url-or-a-slug-in-python/ + .. seealso:: https://bit.ly/2NW7s1j """ - slug = re.sub(r'[^\w]+', ' ', text) + slug = re.sub(r"[^\w]+", " ", text) slug = "-".join(slug.lower().strip().split()) return slug diff --git a/yellowbrick/utils/kneed.py b/yellowbrick/utils/kneed.py new file mode 100644 index 000000000..f14192c3a --- /dev/null +++ b/yellowbrick/utils/kneed.py @@ -0,0 +1,279 @@ +# yellowbrick.utils.kneed +# A port of the knee-point detection package, kneed. +# +# Author: Kevin Arvai +# Author: Pradeep Singh +# Created: Mon Apr 15 09:43:18 2019 -0400 +# +# Copyright (C) 2017 Kevin Arvai +# All rights reserved. +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list +# of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may +# be used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# ID: kneed.py [] pswaldia@no-reply.github.com $ + +""" +This package contains a port of the knee-point detection package, kneed, by +Kevin Arvai and hosted at https://github.com/arvkevi/kneed. This port is maintained +with permission by the Yellowbrick contributors. +""" +import numpy as np +from scipy import interpolate +from scipy.signal import argrelextrema +import warnings + +from yellowbrick.exceptions import YellowbrickWarning + + +class KneeLocator(object): + """ + Finds the "elbow" or "knee" which is a value corresponding to the point of maximum curvature + in an elbow curve, using knee point detection algorithm. This point is accessible via the + `knee` attribute. + + Parameters + ---------- + x : list + A list of k values representing the no. of clusters in KMeans Clustering algorithm. + + y : list + A list of silhouette score corresponding to each value of k. + + S : float, default: 1.0 + Sensitivity parameter that allows us to adjust how aggressive we want KneeLocator to + be when detecting "knees" or "elbows". + + curve_nature : string, default: 'concave' + A string that determines the nature of the elbow curve in which "knee" or "elbow" is + to be found. + + curve_direction : string, default: 'increasing' + A string that determines tha increasing or decreasing nature of the elbow curve in + which "knee" or "elbow" is to be found. + + Notes + ----- + The KneeLocator is implemented using the "knee point detection algorithm" which can be read at + `` + """ + + def __init__( + self, x, y, S=1.0, curve_nature="concave", curve_direction="increasing" + ): + + # Raw Input + self.x = x + self.y = y + self.curve_nature = curve_nature + self.curve_direction = curve_direction + self.N = len(self.x) + self.S = S + self.all_knees = set() + self.all_norm_knees = set() + + # Step 1: fit a smooth line + uspline = interpolate.interp1d(self.x, self.y) + self.x = np.array(x) + self.Ds_y = uspline(self.x) + + # Step 2: normalize values + self.x_normalized = self.__normalize(self.x) + self.y_normalized = self.__normalize(self.Ds_y) + + # Step 3: Calculate the Difference curve + self.x_normalized, self.y_normalized = self.transform_xy( + self.x_normalized, + self.y_normalized, + self.curve_direction, + self.curve_nature, + ) + # normalized difference curve + self.y_distance = self.y_normalized - self.x_normalized + self.x_distance = self.x_normalized.copy() + + # Step 4: Identify local maxima/minima + # local maxima + self.maxima_inidices = argrelextrema(self.y_distance, np.greater)[0] + self.x_distance_maxima = self.x_distance[self.maxima_inidices] + self.y_distance_maxima = self.y_distance[self.maxima_inidices] + + # local minima + self.minima_indices = argrelextrema(self.y_distance, np.less)[0] + self.x_distance_minima = self.x_distance[self.minima_indices] + self.y_distance_minima = self.y_distance[self.minima_indices] + + # Step 5: Calculate thresholds + self.Tmx = self.y_distance_maxima - ( + self.S * np.abs(np.diff(self.x_normalized).mean()) + ) + + # Step 6: find knee + self.find_knee() + if (self.all_knees or self.all_norm_knees) == set(): + warning_message = ( + "No 'knee' or 'elbow point' detected " + "This could be due to bad clustering, no " + "actual clusters being formed etc." + ) + warnings.warn(warning_message, YellowbrickWarning) + self.knee = None + self.norm_knee = None + else: + self.knee, self.norm_knee = min(self.all_knees), min(self.all_norm_knees) + + @staticmethod + def __normalize(a): + """ + Normalizes an array. + Parameters + ----------- + a : list + The array to normalize + """ + return (a - min(a)) / (max(a) - min(a)) + + @staticmethod + def transform_xy(x, y, direction, curve): + """transform x and y to concave, increasing based on curve_direction and curve_nature""" + # convert elbows to knees + if curve == "convex": + x = x.max() - x + y = y.max() - y + # flip decreasing functions to increasing + if direction == "decreasing": + y = np.flip(y) + + if curve == "convex": + x = np.flip(x) + y = np.flip(y) + + return x, y + + def find_knee(self,): + """This function finds and sets the knee value and the normalized knee value. """ + if not self.maxima_inidices.size: + warning_message = ( + 'No "knee" or "elbow point" detected ' + "This could be due to bad clustering, no " + "actual clusters being formed etc." + ) + warnings.warn(warning_message, YellowbrickWarning) + return None, None + + # artificially place a local max at the last item in the x_distance array + self.maxima_inidices = np.append(self.maxima_inidices, len(self.x_distance) - 1) + self.minima_indices = np.append(self.minima_indices, len(self.x_distance) - 1) + + # placeholder for which threshold region i is located in. + maxima_threshold_index = 0 + minima_threshold_index = 0 + # traverse the distance curve + for idx, i in enumerate(self.x_distance): + # reached the end of the curve + if i == 1.0: + break + # values in distance curve are at or after a local maximum + if idx >= self.maxima_inidices[maxima_threshold_index]: + threshold = self.Tmx[maxima_threshold_index] + threshold_index = idx + maxima_threshold_index += 1 + # values in distance curve are at or after a local minimum + if idx >= self.minima_indices[minima_threshold_index]: + threshold = 0.0 + minima_threshold_index += 1 + # Do not evaluate values in the distance curve before the first local maximum. + if idx < self.maxima_inidices[0]: + continue + + # evaluate the threshold + if self.y_distance[idx] < threshold: + if self.curve_nature == "convex": + if self.curve_direction == "decreasing": + knee = self.x[threshold_index] + self.all_knees.add(knee) + norm_knee = self.x_normalized[threshold_index] + self.all_norm_knees.add(norm_knee) + else: + knee = self.x[-(threshold_index + 1)] + self.all_knees.add(knee) + norm_knee = self.x_normalized[-(threshold_index + 1)] + self.all_norm_knees.add(norm_knee) + + elif self.curve_nature == "concave": + if self.curve_direction == "decreasing": + knee = self.x[-(threshold_index + 1)] + self.all_knees.add(knee) + norm_knee = self.x_normalized[-(threshold_index + 1)] + self.all_norm_knees.add(norm_knee) + else: + knee = self.x[threshold_index] + self.all_knees.add(knee) + norm_knee = self.x_normalized[threshold_index] + self.all_norm_knees.add(norm_knee) + + def plot_knee_normalized(self,): + """ + Plots the normalized curve, the distance curve (x_distance, y_normalized) and the + knee, if it exists. + """ + import matplotlib.pyplot as plt + + plt.figure(figsize=(8, 8)) + plt.plot(self.x_normalized, self.y_normalized) + plt.plot(self.x_distance, self.y_distance, "r") + plt.xticks( + np.arange(self.x_normalized.min(), self.x_normalized.max() + 0.1, 0.1) + ) + plt.yticks(np.arange(self.y_distance.min(), self.y_normalized.max() + 0.1, 0.1)) + + plt.vlines(self.norm_knee, plt.ylim()[0], plt.ylim()[1]) + + def plot_knee(self,): + """ + Plot the curve and the knee, if it exists + + """ + import matplotlib.pyplot as plt + + plt.figure(figsize=(8, 8)) + plt.plot(self.x, self.y) + plt.vlines(self.knee, plt.ylim()[0], plt.ylim()[1]) + + # Niceties for users working with elbows rather than knees + @property + def elbow(self): + return self.knee + + @property + def norm_elbow(self): + return self.norm_knee + + @property + def all_elbows(self): + return self.all_knees + + @property + def all_norm_elbows(self): + return self.all_norm_knees diff --git a/yellowbrick/utils/nan_warnings.py b/yellowbrick/utils/nan_warnings.py index 70a0cada1..eaa671028 100644 --- a/yellowbrick/utils/nan_warnings.py +++ b/yellowbrick/utils/nan_warnings.py @@ -1,3 +1,14 @@ +# yellowbrick.utils.nan_warnings +# Small helpers that help find and filter missing data. +# +# Author: Aylr +# Created: Thu Dec 28 11:37:42 2017 -0700 +# +# Copyright (C) 2018 The sckit-yb developers +# For license information, see LICENSE.txt +# +# ID: nan_warnings.py [d2276d6] Aylr@users.noreply.github.com $ +# """ Small helpers that help find and filter missing data. """ @@ -60,9 +71,10 @@ def warn_if_nans_exist(X): percent = 100 * null_count / total if null_count > 0: - warning_message = \ - 'Warning! Found {} rows of {} ({:0.2f}%) with nan values. Only ' \ - 'complete rows will be plotted.'.format(null_count, total, percent) + warning_message = ( + "Warning! Found {} rows of {} ({:0.2f}%) with nan values. Only " + "complete rows will be plotted.".format(null_count, total, percent) + ) warnings.warn(warning_message, DataWarning) diff --git a/yellowbrick/utils/target.py b/yellowbrick/utils/target.py new file mode 100644 index 000000000..2bd3a070d --- /dev/null +++ b/yellowbrick/utils/target.py @@ -0,0 +1,106 @@ +# yellowbrick.utils.target +# Helper functions related to the target variable. +# +# Author: Benjamin Bengfort +# Created: Thu Dec 27 20:16:18 2018 -0500 +# +# Copyright (C) 2018 The sckit-yb developers +# For license information, see LICENSE.txt +# +# ID: target.py [899c88a] benjamin@bengfort.com $ + +""" +Helper functions related to the target variable. +""" + +########################################################################## +## Imports and Module Variables +########################################################################## + +import numpy as np + +from enum import Enum +from sklearn.utils.multiclass import type_of_target +from yellowbrick.exceptions import YellowbrickValueError + + +__all__ = ["MAX_DISCRETE_CLASSES", "TargetType", "target_color_type"] + +MAX_DISCRETE_CLASSES = 12 + + +class TargetType(Enum): + """Constants for defining target colors by input type""" + + AUTO = "auto" + SINGLE = "single" + DISCRETE = "discrete" + CONTINUOUS = "continuous" + UNKNOWN = "unknown" + + @classmethod + def validate(klass, val): + if isinstance(val, klass): + return + + try: + klass(val) + except ValueError: + raise YellowbrickValueError("unknown target color type '{}'".format(val)) + + def __eq__(self, other): + if isinstance(other, str): + try: + return TargetType(other.lower()) == self + except ValueError: + return False + return super(TargetType, self).__eq__(other) + + +########################################################################## +## Helper Functions +########################################################################## + + +def target_color_type(y): + """ + Determines the type of color space that will best represent the target + variable y, e.g. either a discrete (categorical) color space or a + continuous color space that requires a colormap. This function can handle + both 1D or column vectors as well as multi-output targets. + + Parameters + ---------- + y : array-like + Must be a valid array-like data structure that can be passed to a + scikit-learn supervised estimator. + + Returns + ------- + color_type : string + One of: + + * 'discrete': `y` is either a binary target or a multiclass target + with <= 12 discrete classes. + * 'continuous': `y` is an array-like of floats that are not all + integers or a multiclass target with > 12 discrete classes. + * 'unknown': `y` is array-like but none of the above. For example + a multilabel-indicator or a 3D array. No exception is raised. + """ + if y is None or len(np.unique(y)) == 1: + return TargetType.SINGLE + + ttype = type_of_target(y) + + if ttype.startswith("continuous"): + return TargetType.CONTINUOUS + + if ttype.startswith("binary"): + return TargetType.DISCRETE + + if ttype.startswith("multiclass"): + if len(np.unique(y)) > MAX_DISCRETE_CLASSES: + return TargetType.CONTINUOUS + return TargetType.DISCRETE + + return TargetType.UNKNOWN diff --git a/yellowbrick/utils/timer.py b/yellowbrick/utils/timer.py index 6b63816d2..141911982 100644 --- a/yellowbrick/utils/timer.py +++ b/yellowbrick/utils/timer.py @@ -1,11 +1,13 @@ # yellowbrick.utils.timer # Timer utilities # -# Author: ZJ Poh +# Author: ZJ Poh # Created: Mon Jul 16 10:51:13 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The sckit-yb developers # For license information, see LICENSE.txt +# +# ID: timer.py [75b0f6a] 8103276+zjpoh@users.noreply.github.com $ """ Timer utilities """ @@ -32,8 +34,9 @@ class Timer: A context object timer. Usage: >>> with Timer() as timer: ... do_something() - >>> print timer.interval + >>> print(timer.interval) """ + def __init__(self): self.time = time.time diff --git a/yellowbrick/utils/types.py b/yellowbrick/utils/types.py index ee4ff8e7b..04bb67cec 100644 --- a/yellowbrick/utils/types.py +++ b/yellowbrick/utils/types.py @@ -1,10 +1,10 @@ # yellowbrick.utils.types # Detection utilities for Scikit-Learn and Numpy types for flexibility # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Fri May 19 10:51:13 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The sckit-yb developers # For license information, see LICENSE.txt # # ID: types.py [79cd8cf] benjamin@bengfort.com $ @@ -27,6 +27,7 @@ ## Model Type checking utilities ########################################################################## + def is_estimator(model): """ Determines if a model is an estimator using issubclass and isinstance. @@ -42,6 +43,7 @@ def is_estimator(model): return isinstance(model, BaseEstimator) + # Alias for closer name to isinstance and issubclass isestimator = is_estimator @@ -65,6 +67,7 @@ def is_classifier(estimator): # Test the _estimator_type property return getattr(estimator, "_estimator_type", None) == "classifier" + # Alias for closer name to isinstance and issubclass isclassifier = is_classifier @@ -88,6 +91,7 @@ def is_regressor(estimator): # Test the _estimator_type property return getattr(estimator, "_estimator_type", None) == "regressor" + # Alias for closer name to isinstance and issubclass isregressor = is_regressor @@ -106,6 +110,7 @@ def is_clusterer(estimator): # Test the _estimator_type property return getattr(estimator, "_estimator_type", None) == "clusterer" + # Alias for closer name to isinstance and issubclass isclusterer = is_clusterer @@ -144,10 +149,10 @@ def is_probabilistic(estimator): The object to test if is probabilistic, especially a Scikit-Learn estimator or Yellowbrick visualizer. """ - return any([ - hasattr(estimator, 'predict_proba'), - hasattr(estimator, 'decision_function'), - ]) + return any( + [hasattr(estimator, "predict_proba"), hasattr(estimator, "decision_function")] + ) + # Alias for closer name to isinstance and issubclass isprobabilistic = is_probabilistic @@ -157,6 +162,7 @@ def is_probabilistic(estimator): ## Data Type checking utilities ########################################################################## + def is_dataframe(obj): """ Returns True if the given object is a Pandas Data Frame. @@ -169,11 +175,13 @@ def is_dataframe(obj): try: # This is the best method of type checking from pandas import DataFrame + return isinstance(obj, DataFrame) except ImportError: # Pandas is not a dependency, so this is scary return obj.__class__.__name__ == "DataFrame" + # Alias for closer name to isinstance and issubclass isdataframe = is_dataframe @@ -190,11 +198,13 @@ def is_series(obj): try: # This is the best method of type checking from pandas import Series + return isinstance(obj, Series) except ImportError: # Pandas is not a dependency, so this is scary return obj.__class__.__name__ == "Series" + # Alias for closer name to isinstance and issubclass isseries = is_series @@ -208,7 +218,7 @@ def is_structured_array(obj): obj: instance The object to test whether or not is a Numpy Structured Array. """ - if isinstance(obj, np.ndarray) and hasattr(obj, 'dtype'): + if isinstance(obj, np.ndarray) and hasattr(obj, "dtype"): if obj.dtype.names is not None: return True return False diff --git a/yellowbrick/utils/wrapper.py b/yellowbrick/utils/wrapper.py index 1c8c77106..f5a586c0a 100644 --- a/yellowbrick/utils/wrapper.py +++ b/yellowbrick/utils/wrapper.py @@ -1,10 +1,10 @@ # yellowbrick.utils.wrapper # Utility package that provides a wrapper for new style classes. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Sun May 21 20:27:32 2017 -0700 # -# Copyright (C) 2017 District Data Labs +# Copyright (C) 2017 The sckit-yb developers # For license information, see LICENSE.txt # # ID: wrapper.py [b2ecd50] benjamin@bengfort.com $ diff --git a/yellowbrick/version.py b/yellowbrick/version.py index 5c2b96e3a..74c0811de 100644 --- a/yellowbrick/version.py +++ b/yellowbrick/version.py @@ -1,10 +1,10 @@ # yellowbrick.version # Maintains version and package information for deployment. # -# Author: Benjamin Bengfort +# Author: Benjamin Bengfort # Created: Mon Jan 25 14:22:52 2016 -0500 # -# Copyright (C) 2016 District Data Labs +# Copyright (C) 2016 The sckit-yb developers # For license information, see LICENSE.txt # # ID: version.py [0c5ba04] benjamin@bengfort.com $ @@ -18,26 +18,28 @@ ########################################################################## __version_info__ = { - 'major': 0, - 'minor': 9, - 'micro': 1, - 'releaselevel': 'final', - 'serial': 13, + "major": 1, + "minor": 0, + "micro": 0, + "releaselevel": "final", + "serial": 14, } ########################################################################## ## Helper Functions ########################################################################## + def get_version(short=False): """ Prints the version. """ - assert __version_info__['releaselevel'] in ('alpha', 'beta', 'final') - vers = ["%(major)i.%(minor)i" % __version_info__, ] - if __version_info__['micro']: + assert __version_info__["releaselevel"] in ("alpha", "beta", "final") + vers = ["%(major)i.%(minor)i" % __version_info__] + if __version_info__["micro"]: vers.append(".%(micro)i" % __version_info__) - if __version_info__['releaselevel'] != 'final' and not short: - vers.append('%s%i' % (__version_info__['releaselevel'][0], - __version_info__['serial'])) - return ''.join(vers) + if __version_info__["releaselevel"] != "final" and not short: + vers.append( + "%s%i" % (__version_info__["releaselevel"][0], __version_info__["serial"]) + ) + return "".join(vers)