Copy disabled (too large)
Download .txt
Showing preview only (20,047K chars total). Download the full file to get everything.
Repository: lmcinnes/umap
Branch: master
Commit: 1642ec62e4a7
Files: 120
Total size: 31.2 MB
Directory structure:
gitextract_a48sspai/
├── .gitattributes
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── inspectionProfiles/
│ │ └── profiles_settings.xml
│ └── umap-nan.iml
├── .pep8speaks.yml
├── .readthedocs.yaml
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── Makefile
├── README.rst
├── appveyor.yml
├── azure-pipelines.yml
├── ci_scripts/
│ ├── install.sh
│ ├── success.sh
│ └── test.sh
├── doc/
│ ├── .gitignore
│ ├── Makefile
│ ├── _static/
│ │ └── .gitkeep
│ ├── aligned_umap_basic_usage.rst
│ ├── aligned_umap_plotly_plot.html
│ ├── aligned_umap_politics_demo.rst
│ ├── api.rst
│ ├── basic_usage.rst
│ ├── basic_usage_bokeh_example.html
│ ├── benchmarking.rst
│ ├── bokeh_digits_plot.py
│ ├── clustering.rst
│ ├── composing_models.rst
│ ├── conf.py
│ ├── densmap_demo.rst
│ ├── doc_requirements.txt
│ ├── document_embedding.rst
│ ├── embedding_space.rst
│ ├── exploratory_analysis.rst
│ ├── faq.rst
│ ├── how_umap_works.rst
│ ├── index.rst
│ ├── interactive_viz.rst
│ ├── inverse_transform.rst
│ ├── make.bat
│ ├── mutual_nn_umap.rst
│ ├── outliers.rst
│ ├── parameters.rst
│ ├── parametric_umap.rst
│ ├── performance.rst
│ ├── plotting.rst
│ ├── plotting_example_interactive.py
│ ├── plotting_interactive_example.html
│ ├── precomputed_k-nn.rst
│ ├── release_notes.rst
│ ├── reproducibility.rst
│ ├── scientific_papers.rst
│ ├── sparse.rst
│ ├── supervised.rst
│ ├── transform.rst
│ └── transform_landmarked_pumap.rst
├── docs_requirements.txt
├── examples/
│ ├── README.txt
│ ├── digits/
│ │ ├── digits.html
│ │ └── digits.py
│ ├── galaxy10sdss.py
│ ├── inverse_transform_example.py
│ ├── iris/
│ │ ├── iris.html
│ │ └── iris.py
│ ├── mnist_torus_sphere_example.py
│ ├── mnist_transform_new_data.py
│ ├── plot_algorithm_comparison.py
│ ├── plot_fashion-mnist_example.py
│ ├── plot_feature_extraction_classification.py
│ └── plot_mnist_example.py
├── notebooks/
│ ├── AnimatingUMAP.ipynb
│ ├── Document embedding using UMAP.ipynb
│ ├── MNIST_Landmarks.ipynb
│ ├── Parametric_UMAP/
│ │ ├── 01.0-parametric-umap-mnist-embedding-basic.ipynb
│ │ ├── 02.0-parametric-umap-mnist-embedding-convnet.ipynb
│ │ ├── 03.0-parametric-umap-mnist-embedding-convnet-with-reconstruction.ipynb
│ │ ├── 04.0-parametric-umap-mnist-embedding-convnet-with-autoencoder-loss.ipynb
│ │ ├── 05.0-parametric-umap-with-callback.ipynb
│ │ ├── 06.0-nonparametric-umap.ipynb
│ │ └── 07.0-parametric-umap-global-loss.ipynb
│ └── UMAP usage and parameters.ipynb
├── paper.bib
├── paper.md
├── pyproject.toml
├── setup.py
└── umap/
├── __init__.py
├── aligned_umap.py
├── distances.py
├── layouts.py
├── parametric_umap.py
├── plot.py
├── sparse.py
├── spectral.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── digits_embedding_42.npy
│ ├── test_aligned_umap.py
│ ├── test_chunked_parallel_spatial_metric.py
│ ├── test_composite_models.py
│ ├── test_data_input.py
│ ├── test_densmap.py
│ ├── test_parametric_umap.py
│ ├── test_plot.py
│ ├── test_spectral.py
│ ├── test_umap.py
│ ├── test_umap_get_feature_names_out.py
│ ├── test_umap_grads.py
│ ├── test_umap_metrics.py
│ ├── test_umap_nn.py
│ ├── test_umap_on_iris.py
│ ├── test_umap_ops.py
│ ├── test_umap_repeated_data.py
│ ├── test_umap_trustworthiness.py
│ └── test_umap_validation_params.py
├── umap_.py
├── utils.py
└── validation.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.ipynb linguist-language=Python
================================================
FILE: .gitignore
================================================
# virtual environment
venv
# non-stylistic pycharm configs
.idea/misc.xml
.idea/modules.xml
.idea/umap.iml
.idea/vcs.xml
.idea/workspace.xml
.idea/dictionaries
.idea/other.xml
# Mac Finder layout
.DS_Store
# IPython/Jupyter notebook checkpoints
*.ipynb_checkpoints
# Python 2.x & 3.x bytecode cache
*.pyc
*__pycache__
# metadata from pip-installing repo
umap_learn.egg-info
# docs
doc/auto_examples
doc/_build
# build and dist
build
dist
# coverage
.coverage
.coverage.*
.coverage.xml
================================================
FILE: .idea/.gitignore
================================================
# Default ignored files
/shelf/
/workspace.xml
================================================
FILE: .idea/inspectionProfiles/profiles_settings.xml
================================================
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
================================================
FILE: .idea/umap-nan.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.10 (umap-nan)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="NUMPY" />
<option name="myDocStringFormat" value="NumPy" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>
================================================
FILE: .pep8speaks.yml
================================================
pycodestyle: # Same as scanner.linter value. Other option is flake8
max-line-length: 88 # Default is 79 in PEP 8
================================================
FILE: .readthedocs.yaml
================================================
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: doc/conf.py
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs_requirements.txt
- method: pip
path: .
================================================
FILE: .travis.yml
================================================
language: python
cache:
apt: true
# We use three different cache directory
# to work around a Travis bug with multi-platform cache
directories:
- $HOME/.cache/pip
- $HOME/download
env:
global:
# Directory where tests are run from
- TEST_DIR=/tmp/test_dir/
- MODULE=umap
matrix:
include:
- python: 3.6
os: linux
- env: DISTRIB="conda" PYTHON_VERSION="3.7" NUMPY_VERSION="1.17" SCIPY_VERSION="1.3.1"
os: linux
- env: DISTRIB="conda" PYTHON_VERSION="3.8" NUMPY_VERSION="1.20.0" SCIPY_VERSION="1.6.0"
os: linux
- env: DISTRIB="conda" PYTHON_VERSION="3.8" COVERAGE="true" NUMPY_VERSION="1.20.0" SCIPY_VERSION="1.6.0"
os: linux
# - env: DISTRIB="conda" PYTHON_VERSION="3.7" NUMBA_VERSION="0.51.2"
# os: osx
# language: generic
# - env: DISTRIB="conda" PYTHON_VERSION="3.8" NUMBA_VERSION="0.51.2"
# os: osx
# language: generic
install: source ci_scripts/install.sh
script: travis_wait 90 bash ci_scripts/test.sh
after_success: source ci_scripts/success.sh
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leland.mcinnes@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Contributions of all kinds are welcome. In particular pull requests are appreciated.
The authors will endeavour to help walk you through any issues in the pull request
discussion, so please feel free to open a pull request even if you are new to such things.
## Issues
The easiest contribution to make is to [file an issue](https://github.com/lmcinnes/umap/issues/new).
It is beneficial if you check the [FAQ](https://umap-learn.readthedocs.io/en/latest/faq.html),
and do a cursory search of [existing issues](https://github.com/lmcinnes/umap/issues?utf8=%E2%9C%93&q=is%3Aissue).
It is also helpful, but not necessary, if you can provide clear instruction for
how to reproduce a problem. If you have resolved an issue yourself please consider
contributing to the FAQ to add your problem, and its resolution, so others can
benefit from your work.
## Documentation
Contributing to documentation is the easiest way to get started. Providing simple
clear or helpful documentation for new users is critical. Anything that *you* as
a new user found hard to understand, or difficult to work out, are excellent places
to begin. Contributions to more detailed and descriptive error messages is
especially appreciated. To contribute to the documentation please
[fork the project](https://github.com/lmcinnes/umap/issues#fork-destination-box)
into your own repository, make changes there, and then submit a pull request.
### Building the Documentation Locally
To build the docs locally, install the documentation tools requirements:
```bash
pip install -r docs_requirements.txt
```
Then run:
```bash
sphinx-build -b html doc doc/_build
```
This will build the documentation in HTML format. You will be able to find the output
in the `doc/_build` folder.
## Code
Code contributions are always welcome, from simple bug fixes, to new features. To
contribute code please
[fork the project](https://github.com/lmcinnes/umap/issues#fork-destination-box)
into your own repository, make changes there, and then submit a pull request. If
you are fixing a known issue please add the issue number to the PR message. If you
are fixing a new issue feel free to file an issue and then reference it in the PR.
You can [browse open issues](https://github.com/lmcinnes/umap/issues),
or consult the [project roadmap](https://github.com/lmcinnes/umap/issues/15), for potential code
contributions. Fixes for issues tagged with 'help wanted' are especially appreciated.
### Code formatting
If possible, install the [black code formatter](https://github.com/python/black) (e.g.
`pip install black`) and run it before submitting a pull request. This helps maintain consistency
across the code, but also there is a check in the Travis-CI continuous integration system which
will show up as a failure in the pull request if `black` detects that it hasn't been run.
Formatting is as simple as running:
```bash
black .
```
in the root of the project.
================================================
FILE: LICENSE.txt
================================================
BSD 3-Clause License
Copyright (c) 2017, Leland McInnes
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* 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.
* 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.
================================================
FILE: Makefile
================================================
# make gh-pages in repo base directory to automatically build and deploy documents to github
gh-pages:
echo "Make gh-pages"
cd doc; make html
git checkout gh-pages
rm -rf _sources _static _modules _downloads _images auto_examples
mv -fv doc/_build/html/* .
rm -rf doc
git add -A
git commit -m "Generated gh-pages for `git log master -1 --pretty=short --abbrev-commit`" && git push origin gh-pages ; git checkout master
================================================
FILE: README.rst
================================================
.. -*- mode: rst -*-
.. image:: doc/logo_large.png
:width: 600
:alt: UMAP logo
:align: center
|pypi_version|_ |pypi_downloads|_
|conda_version|_ |conda_downloads|_
|License|_ |build_status|_ |Coverage|_
|Docs|_ |joss_paper|_
.. |pypi_version| image:: https://img.shields.io/pypi/v/umap-learn.svg
.. _pypi_version: https://pypi.python.org/pypi/umap-learn/
.. |pypi_downloads| image:: https://pepy.tech/badge/umap-learn/month
.. _pypi_downloads: https://pepy.tech/project/umap-learn
.. |conda_version| image:: https://anaconda.org/conda-forge/umap-learn/badges/version.svg
.. _conda_version: https://anaconda.org/conda-forge/umap-learn
.. |conda_downloads| image:: https://anaconda.org/conda-forge/umap-learn/badges/downloads.svg
.. _conda_downloads: https://anaconda.org/conda-forge/umap-learn
.. |License| image:: https://img.shields.io/pypi/l/umap-learn.svg
.. _License: https://github.com/lmcinnes/umap/blob/master/LICENSE.txt
.. |build_status| image:: https://dev.azure.com/TutteInstitute/build-pipelines/_apis/build/status/lmcinnes.umap?branchName=master
.. _build_status: https://dev.azure.com/TutteInstitute/build-pipelines/_build/latest?definitionId=2&branchName=master
.. |Coverage| image:: https://coveralls.io/repos/github/lmcinnes/umap/badge.svg
.. _Coverage: https://coveralls.io/github/lmcinnes/umap
.. |Docs| image:: https://readthedocs.org/projects/umap-learn/badge/?version=latest
.. _Docs: https://umap-learn.readthedocs.io/en/latest/?badge=latest
.. |joss_paper| image:: http://joss.theoj.org/papers/10.21105/joss.00861/status.svg
.. _joss_paper: https://doi.org/10.21105/joss.00861
====
UMAP
====
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:
1. The data is uniformly distributed on a Riemannian manifold;
2. The Riemannian metric is locally constant (or can be approximated as such);
3. The manifold is locally connected.
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.
The details for the underlying mathematics can be found in
`our paper on ArXiv <https://arxiv.org/abs/1802.03426>`_:
McInnes, L, Healy, J, *UMAP: Uniform Manifold Approximation and Projection
for Dimension Reduction*, ArXiv e-prints 1802.03426, 2018
A broader introduction to UMAP targetted the scientific community can be found
in our `paper published in Nature Review Methods Primers <https://doi.org/10.1038/s43586-024-00363-x>`_:
Healy, J., McInnes, L. *Uniform manifold approximation and projection*. Nat Rev Methods
Primers 4, 82 (2024).
A read only version of this paper can accessed via `link <https://rdcu.be/d0YZT>`_
The important thing is that you don't need to worry about that—you can use
UMAP right now for dimension reduction and visualisation as easily as a drop
in replacement for scikit-learn's t-SNE.
Documentation is `available via Read the Docs <https://umap-learn.readthedocs.io/>`_.
**New: this package now also provides support for densMAP.** The densMAP algorithm augments UMAP
to preserve local density information in addition to the topological structure of the data.
Details of this method are described in the following `paper <https://doi.org/10.1038/s41587-020-00801-7>`_:
Narayan, A, Berger, B, Cho, H, *Assessing Single-Cell Transcriptomic Variability
through Density-Preserving Data Visualization*, Nature Biotechnology, 2021
----------
Installing
----------
UMAP depends upon ``scikit-learn``, and thus ``scikit-learn``'s dependencies
such as ``numpy`` and ``scipy``. UMAP adds a requirement for ``numba`` for
performance reasons. The original version used Cython, but the improved code
clarity, simplicity and performance of Numba made the transition necessary.
Requirements:
* Python 3.6 or greater
* numpy
* scipy
* scikit-learn
* numba
* tqdm
* `pynndescent <https://github.com/lmcinnes/pynndescent>`_
Recommended packages:
* For plotting
* matplotlib
* datashader
* holoviews
* for Parametric UMAP
* tensorflow > 2.0.0
**Install Options**
Conda install, via the excellent work of the conda-forge team:
.. code:: bash
conda install -c conda-forge umap-learn
The conda-forge packages are available for Linux, OS X, and Windows 64 bit.
PyPI install, presuming you have numba and sklearn and all its requirements
(numpy and scipy) installed:
.. code:: bash
pip install umap-learn
If you wish to use the plotting functionality you can use
.. code:: bash
pip install umap-learn[plot]
to install all the plotting dependencies.
If you wish to use Parametric UMAP, you need to install Tensorflow, which can be
installed either using the instructions at https://www.tensorflow.org/install
(recommended) or using
.. code:: bash
pip install umap-learn[parametric_umap]
for a CPU-only version of Tensorflow.
If you're on an x86 processor, you can also optionally install `tbb`, which will
provide additional CPU optimizations:
.. code:: bash
pip install umap-learn[tbb]
If pip is having difficulties pulling the dependencies then we'd suggest installing
the dependencies manually using anaconda followed by pulling umap from pip:
.. code:: bash
conda install numpy scipy
conda install scikit-learn
conda install numba
pip install umap-learn
For a manual install get this package:
.. code:: bash
wget https://github.com/lmcinnes/umap/archive/master.zip
unzip master.zip
rm master.zip
cd umap-master
Optionally, install the requirements through Conda:
.. code:: bash
conda install scikit-learn numba
Then install the package
.. code:: bash
python -m pip install -e .
---------------
How to use UMAP
---------------
The umap package inherits from sklearn classes, and thus drops in neatly
next to other sklearn transformers with an identical calling API.
.. code:: python
import umap
from sklearn.datasets import load_digits
digits = load_digits()
embedding = umap.UMAP().fit_transform(digits.data)
There are a number of parameters that can be set for the UMAP class; the
major ones are as follows:
- ``n_neighbors``: This determines the number of neighboring points used in
local approximations of manifold structure. Larger values will result in
more global structure being preserved at the loss of detailed local
structure. In general this parameter should often be in the range 5 to
50, with a choice of 10 to 15 being a sensible default.
- ``min_dist``: This controls how tightly the embedding is allowed compress
points together. Larger values ensure embedded points are more evenly
distributed, while smaller values allow the algorithm to optimise more
accurately with regard to local structure. Sensible values are in the
range 0.001 to 0.5, with 0.1 being a reasonable default.
- ``metric``: This determines the choice of metric used to measure distance
in the input space. A wide variety of metrics are already coded, and a user
defined function can be passed as long as it has been JITd by numba.
An example of making use of these options:
.. code:: python
import umap
from sklearn.datasets import load_digits
digits = load_digits()
embedding = umap.UMAP(n_neighbors=5,
min_dist=0.3,
metric='correlation').fit_transform(digits.data)
UMAP also supports fitting to sparse matrix data. For more details
please see `the UMAP documentation <https://umap-learn.readthedocs.io/>`_
----------------
Benefits of UMAP
----------------
UMAP has a few signficant wins in its current incarnation.
First of all UMAP is *fast*. It can handle large datasets and high
dimensional data without too much difficulty, scaling beyond what most t-SNE
packages can manage. This includes very high dimensional sparse datasets. UMAP
has successfully been used directly on data with over a million dimensions.
Second, UMAP scales well in embedding dimension—it isn't just for
visualisation! You can use UMAP as a general purpose dimension reduction
technique as a preliminary step to other machine learning tasks. With a
little care it partners well with the `hdbscan
<https://github.com/scikit-learn-contrib/hdbscan>`_ clustering library (for
more details please see `Using UMAP for Clustering
<https://umap-learn.readthedocs.io/en/latest/clustering.html>`_).
Third, UMAP often performs better at preserving some aspects of global structure
of the data than most implementations of t-SNE. This means that it can often
provide a better "big picture" view of your data as well as preserving local neighbor
relations.
Fourth, UMAP supports a wide variety of distance functions, including
non-metric distance functions such as *cosine distance* and *correlation
distance*. You can finally embed word vectors properly using cosine distance!
Fifth, UMAP supports adding new points to an existing embedding via
the standard sklearn ``transform`` method. This means that UMAP can be
used as a preprocessing transformer in sklearn pipelines.
Sixth, UMAP supports supervised and semi-supervised dimension reduction.
This means that if you have label information that you wish to use as
extra information for dimension reduction (even if it is just partial
labelling) you can do that—as simply as providing it as the ``y``
parameter in the fit method.
Seventh, UMAP supports a variety of additional experimental features including: an
"inverse transform" that can approximate a high dimensional sample that would map to
a given position in the embedding space; the ability to embed into non-euclidean
spaces including hyperbolic embeddings, and embeddings with uncertainty; very
preliminary support for embedding dataframes also exists.
Finally, UMAP has solid theoretical foundations in manifold learning
(see `our paper on ArXiv <https://arxiv.org/abs/1802.03426>`_).
This both justifies the approach and allows for further
extensions that will soon be added to the library.
------------------------
Performance and Examples
------------------------
UMAP is very efficient at embedding large high dimensional datasets. In
particular it scales well with both input dimension and embedding dimension.
For the best possible performance we recommend installing the nearest neighbor
computation library `pynndescent <https://github.com/lmcinnes/pynndescent>`_ .
UMAP will work without it, but if installed it will run faster, particularly on
multicore machines.
For a problem such as the 784-dimensional MNIST digits dataset with
70000 data samples, UMAP can complete the embedding in under a minute (as
compared with around 45 minutes for scikit-learn's t-SNE implementation).
Despite this runtime efficiency, UMAP still produces high quality embeddings.
The obligatory MNIST digits dataset, embedded in 42
seconds (with pynndescent installed and after numba jit warmup)
using a 3.1 GHz Intel Core i7 processor (n_neighbors=10, min_dist=0.001):
.. image:: images/umap_example_mnist1.png
:alt: UMAP embedding of MNIST digits
The MNIST digits dataset is fairly straightforward, however. A better test is
the more recent "Fashion MNIST" dataset of images of fashion items (again
70000 data sample in 784 dimensions). UMAP
produced this embedding in 49 seconds (n_neighbors=5, min_dist=0.1):
.. image:: images/umap_example_fashion_mnist1.png
:alt: UMAP embedding of "Fashion MNIST"
The UCI shuttle dataset (43500 sample in 8 dimensions) embeds well under
*correlation* distance in 44 seconds (note the longer time
required for correlation distance computations):
.. image:: images/umap_example_shuttle.png
:alt: UMAP embedding the UCI Shuttle dataset
The following is a densMAP visualization of the MNIST digits dataset with 784 features
based on the same parameters as above (n_neighbors=10, min_dist=0.001). densMAP reveals
that the cluster corresponding to digit 1 is noticeably denser, suggesting that
there are fewer degrees of freedom in the images of 1 compared to other digits.
.. image:: images/densmap_example_mnist.png
:alt: densMAP embedding of the MNIST dataset
--------
Plotting
--------
UMAP includes a subpackage ``umap.plot`` for plotting the results of UMAP embeddings.
This package needs to be imported separately since it has extra requirements
(matplotlib, datashader and holoviews). It allows for fast and simple plotting and
attempts to make sensible decisions to avoid overplotting and other pitfalls. An
example of use:
.. code:: python
import umap
import umap.plot
from sklearn.datasets import load_digits
digits = load_digits()
mapper = umap.UMAP().fit(digits.data)
umap.plot.points(mapper, labels=digits.target)
The plotting package offers basic plots, as well as interactive plots with hover
tools and various diagnostic plotting options. See the documentation for more details.
---------------
Parametric UMAP
---------------
Parametric UMAP provides support for training a neural network to learn a UMAP based
transformation of data. This can be used to support faster inference of new unseen
data, more robust inverse transforms, autoencoder versions of UMAP and
semi-supervised classification (particularly for data well separated by UMAP and very
limited amounts of labelled data). See the
`documentation of Parametric UMAP <https://umap-learn.readthedocs.io/en/0.5dev/parametric_umap.html>`_
or the
`example notebooks <https://github.com/lmcinnes/umap/tree/master/notebooks/Parametric_UMAP>`_
for more.
-------
densMAP
-------
The densMAP algorithm augments UMAP to additionally preserve local density information
in addition to the topological structure captured by UMAP. One can easily run densMAP
using the umap package by setting the ``densmap`` input flag:
.. code:: python
embedding = umap.UMAP(densmap=True).fit_transform(data)
This functionality is built upon the densMAP `implementation <https://github.com/hhcho/densvis>`_ provided by the developers
of densMAP, who also contributed to integrating densMAP into the umap package.
densMAP inherits all of the parameters of UMAP. The following is a list of additional
parameters that can be set for densMAP:
- ``dens_frac``: This determines the fraction of epochs (a value between 0 and 1) that will include the density-preservation term in the optimization objective. This parameter is set to 0.3 by default. Note that densMAP switches density optimization on after an initial phase of optimizing the embedding using UMAP.
- ``dens_lambda``: This determines the weight of the density-preservation objective. Higher values prioritize density preservation, and lower values (closer to zero) prioritize the UMAP objective. Setting this parameter to zero reduces the algorithm to UMAP. Default value is 2.0.
- ``dens_var_shift``: Regularization term added to the variance of local densities in the embedding for numerical stability. We recommend setting this parameter to 0.1, which consistently works well in many settings.
- ``output_dens``: When this flag is True, the call to ``fit_transform`` returns, in addition to the embedding, the local radii (inverse measure of local density defined in the `densMAP paper <https://doi.org/10.1101/2020.05.12.077776>`_) for the original dataset and for the embedding. The output is a tuple ``(embedding, radii_original, radii_embedding)``. Note that the radii are log-transformed. If False, only the embedding is returned. This flag can also be used with UMAP to explore the local densities of UMAP embeddings. By default this flag is False.
For densMAP we recommend larger values of ``n_neighbors`` (e.g. 30) for reliable estimation of local density.
An example of making use of these options (based on a subsample of the mnist_784 dataset):
.. code:: python
import umap
from sklearn.datasets import fetch_openml
from sklearn.utils import resample
digits = fetch_openml(name='mnist_784')
subsample, subsample_labels = resample(digits.data, digits.target, n_samples=7000,
stratify=digits.target, random_state=1)
embedding, r_orig, r_emb = umap.UMAP(densmap=True, dens_lambda=2.0, n_neighbors=30,
output_dens=True).fit_transform(subsample)
See `the documentation <https://umap-learn.readthedocs.io/en/0.5dev/densmap_demo.html>`_ for more details.
---------------------------------
GPU-Accelerated UMAP with torchdr
---------------------------------
For GPU-accelerated UMAP computations, `torchdr <https://github.com/TorchDR/TorchDR>`_ provides a PyTorch-based implementation that significantly speed up the algorithm.
torchdr accelerates **every step** of the dimensionality reduction pipeline on GPU: kNN computation, affinity construction and embedding optimization.
Using torchdr with UMAP is straightforward:
.. code:: python
from torchdr import UMAP as torchdrUMAP
umap_gpu = torchdrUMAP(
n_neighbors=15,
min_dist=0.1,
n_components=2,
device='cuda'
)
embedding = umap_gpu.fit_transform(data-maps)
For more information and advanced usage, see the `torchdr documentation <https://torchdr.github.io/index.html>`_.
----------------
Help and Support
----------------
Documentation is at `Read the Docs <https://umap-learn.readthedocs.io/>`_.
The documentation `includes a FAQ <https://umap-learn.readthedocs.io/en/latest/faq.html>`_ that
may answer your questions. If you still have questions then please
`open an issue <https://github.com/lmcinnes/umap/issues/new>`_
and I will try to provide any help and guidance that I can.
--------
Citation
--------
If you make use of this software for your work we would appreciate it if you
would cite the paper from the Journal of Open Source Software:
.. code:: bibtex
@article{mcinnes2018umap-software,
title={UMAP: Uniform Manifold Approximation and Projection},
author={McInnes, Leland and Healy, John and Saul, Nathaniel and Grossberger, Lukas},
journal={The Journal of Open Source Software},
volume={3},
number={29},
pages={861},
year={2018}
}
If you would like to cite this algorithm in your work the ArXiv paper is the
current reference:
.. code:: bibtex
@article{2018arXivUMAP,
author = {{McInnes}, L. and {Healy}, J. and {Melville}, J.},
title = "{UMAP: Uniform Manifold Approximation
and Projection for Dimension Reduction}",
journal = {ArXiv e-prints},
archivePrefix = "arXiv",
eprint = {1802.03426},
primaryClass = "stat.ML",
keywords = {Statistics - Machine Learning,
Computer Science - Computational Geometry,
Computer Science - Learning},
year = 2018,
month = feb,
}
If you found the Nature Primer introduction useful please cite the following reference:
.. code:: bibtex
@article{Healy2024,
author={Healy, John
and McInnes, Leland},
title={Uniform manifold approximation and projection},
journal={Nature Reviews Methods Primers},
year={2024},
month={Nov},
day={21},
volume={4},
number={1},
pages={82},
abstract={Uniform manifold approximation and projection is a nonlinear dimension reduction method often used for visualizing data and as pre-processing for further machine-learning tasks such as clustering. In this Primer, we provide an introduction to the uniform manifold approximation and projection algorithm, the intuitions behind how it works, how best to apply it on data and how to interpret and understand results.},
issn={2662-8449},
doi={10.1038/s43586-024-00363-x},
url={https://doi.org/10.1038/s43586-024-00363-x}
}
Additionally, if you use the densMAP algorithm in your work please cite the following reference:
.. code:: bibtex
@article {NBC2020,
author = {Narayan, Ashwin and Berger, Bonnie and Cho, Hyunghoon},
title = {Assessing Single-Cell Transcriptomic Variability through Density-Preserving Data Visualization},
journal = {Nature Biotechnology},
year = {2021},
doi = {10.1038/s41587-020-00801-7},
publisher = {Springer Nature},
URL = {https://doi.org/10.1038/s41587-020-00801-7},
eprint = {https://www.biorxiv.org/content/early/2020/05/14/2020.05.12.077776.full.pdf},
}
If you use the Parametric UMAP algorithm in your work please cite the following reference:
.. code:: bibtex
@article {SMG2020,
author = {Sainburg, Tim and McInnes, Leland and Gentner, Timothy Q.},
title = {Parametric UMAP: learning embeddings with deep neural networks for representation and semi-supervised learning},
journal = {ArXiv e-prints},
archivePrefix = "arXiv",
eprint = {2009.12981},
primaryClass = "stat.ML",
keywords = {Statistics - Machine Learning,
Computer Science - Computational Geometry,
Computer Science - Learning},
year = 2020,
}
-------
License
-------
The umap package is 3-clause BSD licensed.
We would like to note that the umap package makes heavy use of
NumFOCUS sponsored projects, and would not be possible without
their support of those projects, so please `consider contributing to NumFOCUS <https://www.numfocus.org/membership>`_.
------------
Contributing
------------
Contributions are more than welcome! There are lots of opportunities
for potential projects, so please get in touch if you would like to
help out. Everything from code to notebooks to
examples and documentation are all *equally valuable* so please don't feel
you can't contribute. To contribute please
`fork the project <https://github.com/lmcinnes/umap/issues#fork-destination-box>`_
make your changes and
submit a pull request. We will do our best to work through any issues with
you and get your code merged into the main branch.
================================================
FILE: appveyor.yml
================================================
build: "off"
environment:
matrix:
- PYTHON_VERSION: "3.7"
MINICONDA: C:\Miniconda3-x64
- PYTHON_VERSION: "3.8"
MINICONDA: C:\Miniconda3-x64
init:
- "ECHO %PYTHON_VERSION% %MINICONDA%"
install:
- "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%"
- conda config --set always_yes yes --set changeps1 no
- conda update -q conda
- conda info -a
- "conda create -q -n test-environment python=%PYTHON_VERSION% numpy scipy scikit-learn numba pandas bokeh holoviews datashader scikit-image pytest"
- activate test-environment
- pip install "tensorflow>=2.1"
- pip install pytest-benchmark
- pip install -e .
test_script:
- pytest --show-capture=no -v --disable-warnings
================================================
FILE: azure-pipelines.yml
================================================
# Trigger a build when there is a push to the main branch or a tag starts with release-
trigger:
branches:
include:
- master
tags:
include:
- release-*
# Trigger a build when there is a pull request to the main branch
# Ignore PRs that are just updating the docs
pr:
branches:
include:
- master
exclude:
- doc/*
- README.rst
parameters:
- name: includeReleaseCandidates
displayName: "Allow pre-release dependencies"
type: boolean
default: false
variables:
triggeredByPullRequest: $[eq(variables['Build.Reason'], 'PullRequest')]
stages:
- stage: RunAllTests
displayName: Run test suite
jobs:
- job: run_platform_tests
strategy:
matrix:
mac_py39:
imageName: 'macOS-latest'
python.version: '3.9'
linux_py39:
imageName: 'ubuntu-latest'
python.version: '3.9'
windows_py39:
imageName: 'windows-latest'
python.version: '3.9'
mac_py310:
imageName: 'macOS-latest'
python.version: '3.10'
linux_py310:
imageName: 'ubuntu-latest'
python.version: '3.10'
windows_py310:
imageName: 'windows-latest'
python.version: '3.10'
mac_py311:
imageName: 'macOS-latest'
python.version: '3.11'
linux_py311:
imageName: 'ubuntu-latest'
python.version: '3.11'
windows_py311:
imageName: 'windows-latest'
python.version: '3.11'
mac_py312:
imageName: 'macOS-latest'
python.version: '3.12'
linux_py312:
imageName: 'ubuntu-latest'
python.version: '3.12'
windows_py312:
imageName: 'windows-latest'
python.version: '3.12'
# # Disable macOS tests on 3.13 since tensorflow only provides pre-built wheels
# # for ARM macs and the runner is x86
# mac_py313:
# imageName: 'macOS-latest'
# python.version: '3.13'
linux_py313:
imageName: 'ubuntu-latest'
python.version: '3.13'
windows_py313:
imageName: 'windows-latest'
python.version: '3.13'
pool:
vmImage: $(imageName)
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
displayName: 'Use Python $(python.version)'
- script: |
python -m pip install --upgrade pip
displayName: 'Upgrade pip'
# 1. Install the full LLVM package only if the OS is macOS
- script: |
brew install llvm@20
# Homebrew formula names can change, so we ensure it links correctly if necessary
brew link --force --overwrite llvm@20
displayName: 'Install LLVM via Homebrew (macOS only)'
condition: eq(variables['Agent.OS'], 'Darwin')
# 2. Find the Homebrew install path and set the environment variable only on macOS
- script: |
# Determine the LLVM install prefix dynamically
LLVM_PREFIX=$(brew --prefix llvm@20)
# Set the LLVM_CONFIG environment variable used by llvmlite's build script
echo "##vso[task.setvariable variable=LLVM_CONFIG]$LLVM_PREFIX/bin/llvm-config"
echo "LLVM_CONFIG set to: $LLVM_CONFIG"
# Also set CMAKE_PREFIX_PATH in case other dependencies need it
echo "##vso[task.setvariable variable=CMAKE_PREFIX_PATH]$LLVM_PREFIX/lib/cmake"
displayName: 'Configure LLVM Environment Variables (macOS only)'
condition: eq(variables['Agent.OS'], 'Darwin')
- script: |
pip install -e .
pip install .[plot]
pip install .[parametric_umap]
env:
# Ensure that the LLVM_CONFIG environment variable is available during installation
LLVM_CONFIG: $(LLVM_CONFIG)
CMAKE_PREFIX_PATH: $(CMAKE_PREFIX_PATH)
displayName: 'Install dependencies'
condition: ${{ eq(parameters.includeReleaseCandidates, false) }}
- script: |
pip install --pre -e .
pip install --pre .[plot]
pip install --pre .[parametric_umap]
displayName: 'Install dependencies (allow pre-releases)'
condition: ${{ eq(parameters.includeReleaseCandidates, true) }}
- script: |
pip install pytest pytest-azurepipelines pytest-cov pytest-benchmark coveralls
displayName: 'Install pytest'
- script: |
# export NUMBA_DISABLE_JIT=1 # Disable numba coverage so tests run on time for now.
pytest umap/tests --show-capture=no -v --disable-warnings --junitxml=junit/test-results.xml --cov=umap/ --cov-report=xml --cov-report=html
displayName: 'Run tests'
- bash: |
coveralls
displayName: 'Publish to coveralls'
# Don't run this for PRs because they can't access pipeline secrets
# The python client for coveralls currently does not support python 3.13
# https://github.com/TheKevJames/coveralls-python/pull/542
condition: and(succeeded(), eq(variables.triggeredByPullRequest, false), ne(variables['python.version'], '3.13'), ne(variables['Agent.OS'], 'Windows'))
env:
COVERALLS_REPO_TOKEN: $(COVERALLS_TOKEN)
- task: PublishTestResults@2
inputs:
testResultsFiles: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version)'
condition: succeededOrFailed()
- stage: BuildPublishArtifact
dependsOn: RunAllTests
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'), eq(variables.triggeredByPullRequest, false))
jobs:
- job: BuildArtifacts
displayName: Build source dists and wheels
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.10'
displayName: 'Use Python 3.10'
- script: |
python -m pip install --upgrade pip
pip install wheel
pip install -e .
displayName: 'Install package locally'
- bash: |
pip install build
python -m build --wheel --sdist --outdir dist/ .
ls -l dist/
displayName: 'Build package'
- bash: |
export PACKAGE_VERSION="$(python setup.py --version)"
echo "Package Version: ${PACKAGE_VERSION}"
echo "##vso[task.setvariable variable=packageVersionFormatted;]release-${PACKAGE_VERSION}"
displayName: 'Get package version'
- script: |
echo "Version in git tag $(Build.SourceBranchName) does not match version derived from setup.py $(packageVersionFormatted)"
exit 1
displayName: Raise error if version doesnt match tag
condition: and(succeeded(), ne(variables['Build.SourceBranchName'], variables['packageVersionFormatted']))
- task: DownloadSecureFile@1
name: PYPIRC_CONFIG
displayName: 'Download pypirc'
inputs:
secureFile: 'pypirc'
- script: |
pip install twine
twine upload --repository pypi --config-file $(PYPIRC_CONFIG.secureFilePath) dist/*
displayName: 'Upload to PyPI'
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], variables['packageVersionFormatted']))
================================================
FILE: ci_scripts/install.sh
================================================
if [[ "$DISTRIB" == "conda" ]]; then
# Deactivate the travis-provided virtual environment and setup a
# conda-based environment instead
if [ $TRAVIS_OS_NAME = 'linux' ]; then
# Only Linux has a virtual environment activated; Mac does not.
deactivate
fi
# Use the miniconda installer for faster download / install of conda
# itself
pushd .
cd
mkdir -p download
cd download
echo "Cached in $HOME/download :"
ls -l
echo
# For now, ignoring the cached file.
# if [[ ! -f miniconda.sh ]]
# then
if [ $TRAVIS_OS_NAME = 'osx' ]; then
# MacOS URL found here: https://docs.conda.io/en/latest/miniconda.html
wget \
https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh \
-O miniconda.sh
else
wget \
http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \
-O miniconda.sh
fi
# fi
chmod +x miniconda.sh && ./miniconda.sh -b -p $HOME/miniconda
cd ..
export PATH=$HOME/miniconda/bin:$HOME/miniconda3/bin:$PATH
conda update --yes conda
popd
# Configure the conda environment and put it in the path using the
# provided versions
# conda create -n testenv --yes python=$PYTHON_VERSION pip \
# numpy=$NUMPY_VERSION scipy=$SCIPY_VERSION numba=$NUMBA_VERSION scikit-learn \
# pytest "tensorflow-mkl>=2.2.0"
if [ $TRAVIS_OS_NAME = 'osx' ]; then
conda create -q -n testenv --yes python=$PYTHON_VERSION numpy scipy scikit-learn \
numba pytest pandas
# pip install bokeh
# pip install datashader
# pip install holoviews
conda install --yes "tensorflow>=2.0.0"
else
conda create -q -n testenv --yes python=$PYTHON_VERSION numpy scipy scikit-learn \
numba pandas bokeh holoviews datashader scikit-image pytest pytest-benchmark \
"tensorflow-mkl>=2.2.0"
fi
source activate testenv
# black requires Python 3.x; don't try to install for Python 2.7 test
if [[ "$PYTHON_VERSION" != "2.7" ]]; then
pip install black
pip install pynndescent
fi
if [[ "$COVERAGE" == "true" ]]; then
pip install coverage coveralls
pip install pytest-cov pytest-benchmark # pytest coverage plugin
fi
python --version
python -c "import numpy; print('numpy %s' % numpy.__version__)"
python -c "import scipy; print('scipy %s' % scipy.__version__)"
python -c "import numba; print('numba %s' % numba.__version__)"
python -c "import sklearn; print('scikit-learn %s' % sklearn.__version__)"
python setup.py develop
else
pip install pynndescent # test with optional pynndescent dependency
pip install pandas
pip install bokeh
pip install datashader
pip install matplotlib
pip install holoviews
pip install scikit-image
pip install "tensorflow>=2.2.0"
pip install -e .
fi
================================================
FILE: ci_scripts/success.sh
================================================
set -e
if [[ "$COVERAGE" == "true" ]]; then
# # Need to run coveralls from a git checkout, so we copy .coverage
# # from TEST_DIR where nosetests has been run
# cp $TEST_DIR/.coverage $TRAVIS_BUILD_DIR
# cd $TRAVIS_BUILD_DIR
# Ignore coveralls failures as the coveralls server is not
# very reliable but we don't want travis to report a failure
# in the github UI just because the coverage report failed to
# be published.
coveralls || echo "Coveralls upload failed"
fi
================================================
FILE: ci_scripts/test.sh
================================================
set -e
#if [[ "$COVERAGE" == "true" ]]; then
# black --check $MODULE
#fi
if [[ "$COVERAGE" == "true" ]]; then
export NUMBA_DISABLE_JIT=1
pytest --cov=umap/ --cov-report=xml --cov-report=html --show-capture=no -v --disable-warnings
else
pytest --show-capture=no -v --disable-warnings
fi
================================================
FILE: doc/.gitignore
================================================
venv
umap
setup.py
paper.md
paper.bib
LICENSE.txt
CODE_OF_CONDUCT.md
================================================
FILE: doc/Makefile
================================================
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = umap
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
================================================
FILE: doc/_static/.gitkeep
================================================
================================================
FILE: doc/aligned_umap_basic_usage.rst
================================================
How to use AlignedUMAP
======================
It may happen that it would be beneficial to have different UMAP
embeddings aligned with each other. There are several ways to go about
doing this. One simple approach is to simply embed each dataset with
UMAP independently and then solve for a `Procrustes
transformation <https://en.wikipedia.org/wiki/Procrustes_transformation>`__
on shared points. An alternative approach is to embed the first dataset
and then construct an initial embedding for the second dataset based on
locations of shared points in the first embedding and then go from
there. A third approach, which will provide better alignments in
general, is to optimize both embeddings at the same time with some form
of constraint as to how far shared points can take different locations
in different embeddings *during* the optimization. This last option is
possible, but is not easily tractable to implement yourself (unlike the
first two options). To remedy this issue it has been implemented as a
separate model class in ``umap-learn`` called ``AlignedUMAP``. The
resulting class is quite flexible, but here we will walk through simple
usage on some basic (and somewhat contrived) data just to demonstrate
how to get it running on data.
.. code:: python3
import numpy as np
import sklearn.datasets
import umap
import umap.plot
import umap.utils as utils
import umap.aligned_umap
import matplotlib.pyplot as plt
For our demonstration we’ll just use the pendigits dataset from sklearn.
.. code:: python3
digits = sklearn.datasets.load_digits()
To make a sequence of datasets with some shared points between each
different dataset we’ll first sort the data so we have some vaguely
sensible progression. In this case we’ll sort by the total amount of
“ink” in the handwritten digit. This isn’t meant to be meaningful, it is
merely meant to provide something useful to slicing into overlapping
chunks that we will want to embed separately and yet keep aligned.
.. code:: python3
ordered_digits = digits.data[np.argsort(digits.data.sum(axis=1))]
ordered_target = digits.target[np.argsort(digits.data.sum(axis=1))]
plt.matshow(ordered_digits[-1].reshape((8,8)))
.. image:: images/aligned_umap_basic_usage_5_1.png
We can then divide up the dataset into slices of 400 samples, moving
along in chunks of 150 to ensure that there are overlaps between
consecutive slices. This will give us a list of ten different datasets
that we can embed, with the goal being to ensure that the positions of
points in the embeddings are relatively consistent.
.. code:: python3
slices = [ordered_digits[150 * i:min(ordered_digits.shape[0], 150 * i + 400)] for i in range(10)]
To ensure that consistency ``AlignedUMAP`` will need more information
than *just* the datasets – we also need some information about how the
datasets relate to one another. These take the form of dictionaries that
relate the indices of one dataset to the indices of another. Currently
``AlignedUMAP`` only supports sequences of datasets with relations
between each consecutive pair in the sequence. To construct the
relations for this dataset we note that the last 250 samples of one
dataset are going to be the same samples as the first 250 samples of the
next dataset – this makes it easy to construct the dictionary: it is
mapping
::
150 --> 0
151 --> 1
...
398 --> 248
399 --> 249
which we can construct easily using a dictionary comprehension. We will
have the same relation between each consecutive pair, so to make the
list of relations between pairs we can just duplicate the constructed
relation the requisite number of times.
.. code:: python3
relation_dict = {i+150:i for i in range(400-150)}
relation_dicts = [relation_dict.copy() for i in range(len(slices) - 1)]
Note that while in this case the relation defines a map between
identical samples in different datasets it can be much more general –
see the politics example later for a case where the relation is
constructed from external information (representatives names and
states).
Now that we have both a list of data slices and a list of relations
between the consecutive pairs we can use the ``AlignedUMAP`` class to
generate a list of embeddings. The ``AlignedUMAP`` class takes most of
the parameters that UMAP accepts. The major difference is that the fit
method requires a *list* of datasets, and a keyword argument
``relations`` that specifies the relation dictionaries between
consecutive pairs of datasets. Other than that things are essentially
push-button.
.. code:: python3
%%time
aligned_mapper = umap.AlignedUMAP().fit(slices, relations=relation_dicts)
.. parsed-literal::
CPU times: user 57.4 s, sys: 8.43 s, total: 1min 5s
Wall time: 57.4 s
You will note that this took a non-trivial amount of time to run,
despite being on the relatively small pendigits dataset. This is because
we are completing 10 different UMAP embeddings at once, so on average we
are taking about five seconds per embedding, which is more reasonable –
the alignment does have overhead cost however.
The next step is to look at the results. To ensure that the plots we
produce have a consistent x and y axis we’ll use a small function to
compute a set of axis bounds for plotting.
.. code:: python3
def axis_bounds(embedding):
left, right = embedding.T[0].min(), embedding.T[0].max()
bottom, top = embedding.T[1].min(), embedding.T[1].max()
adj_h, adj_v = (right - left) * 0.1, (top - bottom) * 0.1
return [left - adj_h, right + adj_h, bottom - adj_v, top + adj_v]
Now it is just a matter of plotting the results in ten different scatter
plots. We can do this most easily with matplotlib directly, setting up a
grid of plots. Note that the progression proceeds by row then column, so
read the progression as if you were reading a page of text (across, then
down).
.. code:: python3
fig, axs = plt.subplots(5,2, figsize=(10, 20))
ax_bound = axis_bounds(np.vstack(aligned_mapper.embeddings_))
for i, ax in enumerate(axs.flatten()):
current_target = ordered_target[150 * i:min(ordered_target.shape[0], 150 * i + 400)]
ax.scatter(*aligned_mapper.embeddings_[i].T, s=2, c=current_target, cmap="Spectral")
ax.axis(ax_bound)
ax.set(xticks=[], yticks=[])
plt.tight_layout()
.. image:: images/aligned_umap_basic_usage_15_0.png
So despite being different embeddings on different datasets, the
clusters keep their general alignment – the top left plot and bottom
right plot have the same rough positions for specific digit clusters. We
can also, to a degree, see how the structure changes over the course of
the different slices. Thus we are keeping the various embeddings
aligned, but allowing the changes dictated by the differing structures
of each different slice of data.
Online updating of aligned embeddings
-------------------------------------
It may be the case that we have incoming temporal data and would like to
have embeddings of time-windows that, ideally, align with the embeddings
of prior time-windows. As long as we overlap the time-windows we use to
allow for relations between time windows then this is possible – except
that the previous code required all the time-windows to be input *at
once* for fitting. We would instead like to train an initial model and
then update it as we go. This is possible via the ``update`` method
which we’ll demonstrate below.
First we need to fit a base ``AlignedUMAP`` model; we’ll use the first
two slices and the first relation dict to do so.
.. code:: python3
%%time
updating_mapper = umap.AlignedUMAP().fit(slices[:2], relations=relation_dicts[:1])
.. parsed-literal::
CPU times: user 9.32 s, sys: 1.47 s, total: 10.8 s
Wall time: 9.17 s
Note that this is fairly quick, since we are only fitting two slices.
Given the trained model the update method requires a new slice of data
to add, along with a relation dictionary (passed in with the
``relations`` keyword argument as with ``fit``). This will append a new
embedding to the ``embeddings_`` attribute of the model for the new
data, aligned with what has been seen so far.
.. code:: python3
for i in range(2,len(slices)):
%time updating_mapper.update(slices[i], relations={v:k for k,v in relation_dicts[i-1].items()})
.. parsed-literal::
CPU times: user 7.78 s, sys: 1.15 s, total: 8.93 s
Wall time: 7.92 s
CPU times: user 6.64 s, sys: 1.17 s, total: 7.81 s
Wall time: 6.6 s
CPU times: user 6.94 s, sys: 1.17 s, total: 8.11 s
Wall time: 6.81 s
CPU times: user 6.45 s, sys: 1.51 s, total: 7.96 s
Wall time: 6.45 s
CPU times: user 7.44 s, sys: 1.32 s, total: 8.76 s
Wall time: 7.16 s
CPU times: user 7.68 s, sys: 1.73 s, total: 9.41 s
Wall time: 7.59 s
CPU times: user 7.88 s, sys: 1.65 s, total: 9.54 s
Wall time: 7.39 s
CPU times: user 7.82 s, sys: 1.98 s, total: 9.8 s
Wall time: 7.7 s
Note that each new slice takes a relatively short period of time, as we
might hope. The downside of this, as you can imagine, is that we have no
“forward” relations – the windows over slices only look backward. This
means the results are less good, but we are trading that for the ability
to quickly and easily update as we go.
We can look at how we did using essentially the same code as before.
.. code:: python3
fig, axs = plt.subplots(5,2, figsize=(10, 20))
ax_bound = axis_bounds(np.vstack(updating_mapper.embeddings_))
for i, ax in enumerate(axs.flatten()):
current_target = ordered_target[150 * i:min(ordered_target.shape[0], 150 * i + 400)]
ax.scatter(*updating_mapper.embeddings_[i].T, s=2, c=current_target, cmap="Spectral")
ax.axis(ax_bound)
ax.set(xticks=[], yticks=[])
plt.tight_layout()
.. image:: images/aligned_umap_basic_usage_22_0.png
We see that the alignment is indeed working, so new slices remain
comparable with previously trained slices. As noted the overall
alignments and progression is not as nice as the previous version, but
it does have the significant benefit of allowing an update as you go
approach.
Note that right now this model keeps all the previous data, so it will
only really work in a batch streaming approach where occasionally a
fresh model is trained, dropping some of the historical data before
continuing with updates.
Aligning varying parameters
---------------------------
It is possible to align UMAP embedding that vary in the parameters used
instead of the data. To demonstrate how this can work we’ll continue to
use the pendigits dataset, but instead of slicing the data as we did
before, we’ll use the full dataset. That means that our relations
between datasets are simply constant relations. We can construct those
ahead of time:
.. code:: python3
constant_dict = {i:i for i in range(digits.data.shape[0])}
constant_relations = [constant_dict for i in range(9)]
To run AlignedUMAP over a range of parameters you simply need to pass in
a *list* of the sequence of parameters you wish to use. You can do this
for several different parameters – just ensure that all the lists are
the same length! In this case we’ll try looking at how the embeddings
change if we change ``n_neighbors`` and ``min_dist``. This means that
when we create the AlignedUMAP object we pass a list, instead of a
single value, to each of those parameters. To make the visualization a
little more interesting we’ll also vary some of the alignment parameters
(there are only two of major consequence). Specifically we’ll adjust the
``alignment_window_size``, which controls how far forward and backward
across the datasets we look when doing alignment, and the
``alignment_regularisation`` which controls how heavily we weight the
alignment aspect versus the UMAP layout. Larger values of
``alignment_regularisation`` will work harder to keep points aligned
across embeddings (at the cost of the embedding quality at each slice),
while smaller values will allow the optimisation to focus more on the
individual embeddings and put less emphasis on aligning the embeddings
with each other.
Given a model we can then fit it. As before we need to hand it a list of
datasets, and a list of relations. Since we are using the same data each
time (and varying the parameters) we can just repeat the full pendigits
dataset. Note that the number of datasets needs to match the number of
parameter values being used. The same goes for the number of relations
(one less than the number of parameter values).
.. code:: python3
neighbors_mapper = umap.AlignedUMAP(
n_neighbors=[3,4,5,7,11,16,22,29,37,45,54],
min_dist=[0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45],
alignment_window_size=2,
alignment_regularisation=1e-3,
).fit(
[digits.data for i in range(10)], relations=constant_relations
)
As before we can look at the results by plotting each of the embeddings.
.. code:: python3
fig, axs = plt.subplots(5,2, figsize=(10, 20))
ax_bound = axis_bounds(np.vstack(neighbors_mapper.embeddings_))
for i, ax in enumerate(axs.flatten()):
ax.scatter(*neighbors_mapper.embeddings_[i].T, s=2, c=digits.target, cmap="Spectral")
ax.axis(ax_bound)
ax.set(xticks=[], yticks=[])
plt.tight_layout()
.. image:: images/aligned_umap_basic_usage_29_1.png
To get a better feel for the evolution of the embedding over the change
in parameter values we can plot the data in three dimensions, with the
third dimension being the parameter value chosen. To better show how
data points in the embedding *move* with respect to the changing
parameters we can plot them not as points, but as *curves* connecting
the same point in each sequential embedding. For three dimensional plots
like this we’ll make use of the `plotly <https://plotly.com>`__ plotting
library.
.. code:: python3
import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
The first thing we’ll have to do is wrangle the data into a suitable
format for plotly. That’s the reason we loaded up pandas as well –
plotly likes dataframes. This involves stacking all the embeddings
together, and then assigning an extra ``z`` value according to which
embedding we are in. For the purposes of visualization we’ll just have a
linear scale from 0 to 1 of the appropriate length for the z
coordinates.
.. code:: python3
n_embeddings = len(neighbors_mapper.embeddings_)
es = neighbors_mapper.embeddings_
embedding_df = pd.DataFrame(np.vstack(es), columns=('x', 'y'))
embedding_df['z'] = np.repeat(np.linspace(0, 1.0, n_embeddings), es[0].shape[0])
embedding_df['id'] = np.tile(np.arange(es[0].shape[0]), n_embeddings)
embedding_df['digit'] = np.tile(digits.target, n_embeddings)
The next thing we can do to improve the visualization is to smooth out
the curves rather than leaving them as piecewise linear lines. To to
this we can use the ``scipy.interpolate`` functionality to create smooth
cubic splines that pass through all the points of the curve we wish to
create.
.. code:: python3
import scipy.interpolate
The interpolate module has a function ``interp1d`` that generates a
(vector of) smooth function given a one dimensional set of datapoints
that it needs to pass through. We can generate separate functions for
the x and y coordinates for each pendigit sample, allowing us to
generate smooth curves in three dimensions.
.. code:: python3
fx = scipy.interpolate.interp1d(
embedding_df.z[embedding_df.id == 0], embedding_df.x.values.reshape(n_embeddings, digits.data.shape[0]).T, kind="cubic"
)
fy = scipy.interpolate.interp1d(
embedding_df.z[embedding_df.id == 0], embedding_df.y.values.reshape(n_embeddings, digits.data.shape[0]).T, kind="cubic"
)
z = np.linspace(0, 1.0, 100)
With that in hand it is just a matter of plotting all the curves. In
plotly parlance each curve is a “trace” and we generate each one
separately (along with a suitable colour given by the digit the sample
represents). We then add all the traces to a figure, and display the
figure.
.. code:: python3
palette = px.colors.diverging.Spectral
interpolated_traces = [fx(z), fy(z)]
traces = [
go.Scatter3d(
x=interpolated_traces[0][i],
y=interpolated_traces[1][i],
z=z*3.0,
mode="lines",
line=dict(
color=palette[digits.target[i]],
width=3.0
),
opacity=1.0,
)
for i in range(digits.data.shape[0])
]
fig = go.Figure(data=traces)
fig.update_layout(
width=800,
height=700,
autosize=False,
showlegend=False,
)
fig.show()
.. image:: images/aligned_umap_pendigits_3d_1.png
Since it is tricky to get the interactive plotly figure embedded in
documentation we have a static image here, but if you run this yourself
you will have a fully interactive view of the data.
Alternatively, we can visualize the third dimension as an evolution of the
embeddings through time by rendering each z-slice as a frame in an animated
GIF. To do this, we'll first need to import some notebook display tools and
matplotlib's `animation <https://matplotlib.org/stable/api/animation_api.html>`_
module.
.. code:: python3
from IPython.display import display, Image, HTML
from matplotlib import animation
Next, we'll create a new figure, initialize a blank scatter plot, then use
``FuncAnimation`` to update the point positions (called "offsets") one frame at
a time.
.. code:: python3
fig = plt.figure(figsize=(4, 4), dpi=150)
ax = fig.add_subplot(1, 1, 1)
scat = ax.scatter([], [], s=2)
scat.set_array(digits.target)
scat.set_cmap('Spectral')
text = ax.text(ax_bound[0] + 0.5, ax_bound[2] + 0.5, '')
ax.axis(ax_bound)
ax.set(xticks=[], yticks=[])
plt.tight_layout()
offsets = np.array(interpolated_traces).T
num_frames = offsets.shape[0]
def animate(i):
scat.set_offsets(offsets[i])
text.set_text(f'Frame {i}')
return scat
anim = animation.FuncAnimation(
fig,
init_func=None,
func=animate,
frames=num_frames,
interval=40)
Then we can save the animation as a GIF and close our animation. Depending on
your machine, you may need to change which writer the save method uses.
.. code:: python3
anim.save("aligned_umap_pendigits_anim.gif", writer="pillow")
plt.close(anim._fig)
Finally, we can read in our rendered GIF and display it in the notebook.
.. code:: python3
with open("aligned_umap_pendigits_anim.gif", "rb") as f:
display(Image(f.read()))
.. image:: images/aligned_umap_pendigits_anim.gif
================================================
FILE: doc/aligned_umap_plotly_plot.html
================================================
[File too large to display: 12.1 MB]
================================================
FILE: doc/aligned_umap_politics_demo.rst
================================================
AlignedUMAP for Time Varying Data
=================================
It is not uncommon to have datasets that can be partitioned into
segments, often with respect to time, where we want to understand not
only the structure of each segment, but how that structure changes over
the different segments. An example of this is the relative political
leanings of the US congress over time. In determining the relative
political leanings we can look at the representatives voting record on
roll call votes, with the presumption that representatives with similar
political principles will have similar voting records. We can, of
course, look at such data for any given congress, but since
representatives are commonly re-elected we can also consider how their
relative position in congress changes with time – an ideal use case for
AlignedUMAP.
First we’ll need a selection of libraries. Aside from UMAP we will need
to do a little bit of data wrangling; for that we’ll need pandas, and
also for matching up names of representatives we’ll make use of the
library ``fuzzywuzzy`` which provides easy to use fuzzy string matching.
.. code:: python3
import umap
import umap.utils as utils
import umap.aligned_umap
import sklearn.decomposition
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from fuzzywuzzy import fuzz, process
import re
.. code:: python3
sns.set(style="darkgrid", color_codes=True)
Next we’ll need to voting records for the representatives, along with
the associated metadata from the roll call vote records. You can obtain
the data https://clerk.house.gov; a notebook demonstrating how to pull
down the data and parse it into the csv files used here is available
`here <https://github.com/lmcinnes/umap_doc_notebooks/blob/master/parse_voting_records.ipynb>`__.
Processing Congressional Voting Records
---------------------------------------
The voting records provide a row for each representative with a -1 for
“No”, 0 for “Present” or “Not Voting”, and 1 for “Aye”. A separate csv
file contains the raw data of all the votes with a row for each
legislators vote on each roll-call item. We really just need some
metadata – which state they represent and the party they represent so we
can decorate the results with this kind of information later. For that
we just need to extra the names, states, and parties for each year. We
can grab those columns and then drop duplicates. A catch: the party is
very occasionally entered incorrectly, and occasionally representatives
switch parties, making duplicated rows. We’ll just take the first entry
of such duplciates for now.
.. code:: python3
votes = [pd.read_csv(f"house_votes/{year}_voting_record.csv", index_col=0).sort_index()
for year in range(1990,2021)]
metadata = [pd.read_csv(
f"house_votes/{year}_full.csv",
index_col=0
)[["legislator", "state", "party"]].drop_duplicates(["legislator", "state"]).sort_values('legislator')
for year in range(1990,2021)]
Let’s take a look at the voting record for a single year to see what
sort of data we are looking at:
.. code:: python3
votes[5]
.. raw:: html
<div>
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>104-1st-1</th>
<th>104-1st-10</th>
<th>104-1st-100</th>
<th>104-1st-101</th>
<th>104-1st-102</th>
<th>104-1st-103</th>
<th>104-1st-104</th>
<th>104-1st-105</th>
<th>104-1st-106</th>
<th>104-1st-107</th>
<th>...</th>
<th>104-1st-90</th>
<th>104-1st-91</th>
<th>104-1st-92</th>
<th>104-1st-93</th>
<th>104-1st-94</th>
<th>104-1st-95</th>
<th>104-1st-96</th>
<th>104-1st-97</th>
<th>104-1st-98</th>
<th>104-1st-99</th>
</tr>
<tr>
<th>legislator</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>Abercrombie</th>
<td>0.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>...</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
</tr>
<tr>
<th>Ackerman</th>
<td>0.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>...</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
</tr>
<tr>
<th>Allard</th>
<td>0.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>...</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>0.0</td>
<td>-1.0</td>
</tr>
<tr>
<th>Andrews</th>
<td>0.0</td>
<td>1.0</td>
<td>0.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>0.0</td>
<td>0.0</td>
<td>0.0</td>
<td>...</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
</tr>
<tr>
<th>Archer</th>
<td>0.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>...</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>0.0</td>
</tr>
<tr>
<th>...</th>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>
<th>Young (AK)</th>
<td>0.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>...</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
</tr>
<tr>
<th>Young (FL)</th>
<td>0.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>...</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
</tr>
<tr>
<th>Zeliff</th>
<td>0.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>...</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
</tr>
<tr>
<th>Zimmer</th>
<td>0.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>...</td>
<td>-1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>-1.0</td>
</tr>
<tr>
<th>de la Garza</th>
<td>0.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>...</td>
<td>0.0</td>
<td>-1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
<td>1.0</td>
<td>-1.0</td>
<td>1.0</td>
</tr>
</tbody>
</table>
<p>438 rows × 885 columns</p>
</div>
We can examine the associated metadata for the same year.
.. code:: python3
metadata[5]
.. raw:: html
<div>
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>legislator</th>
<th>state</th>
<th>party</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>Abercrombie</td>
<td>HI</td>
<td>D</td>
</tr>
<tr>
<th>1</th>
<td>Ackerman</td>
<td>NY</td>
<td>D</td>
</tr>
<tr>
<th>2</th>
<td>Allard</td>
<td>CO</td>
<td>R</td>
</tr>
<tr>
<th>3</th>
<td>Andrews</td>
<td>NJ</td>
<td>D</td>
</tr>
<tr>
<th>4</th>
<td>Archer</td>
<td>TX</td>
<td>R</td>
</tr>
<tr>
<th>...</th>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>
<th>430</th>
<td>Young (AK)</td>
<td>AK</td>
<td>R</td>
</tr>
<tr>
<th>431</th>
<td>Young (FL)</td>
<td>FL</td>
<td>R</td>
</tr>
<tr>
<th>432</th>
<td>Zeliff</td>
<td>NH</td>
<td>R</td>
</tr>
<tr>
<th>433</th>
<td>Zimmer</td>
<td>NJ</td>
<td>R</td>
</tr>
<tr>
<th>89</th>
<td>de la Garza</td>
<td>TX</td>
<td>D</td>
</tr>
</tbody>
</table>
<p>438 rows × 3 columns</p>
</div>
You may note that sometimes representatives names list a state in
parenthesis afterwards. This is to provide disambiguation for
representatives that happen to have the last name. This actually
complicates matters for us since the disambiguation is only applied in
those cases where there is a name collision in that sitting of congress.
That means that for several years a representative may have simply their
last name, but then switch to being disambiguated, before potentially
switching back again. This would make it much harder to consistently
treck representatives over their entire career in congress. To fix this
up we’ll simply re-index by a unique representative ID that has their
last name, party, and state all listed over all the voting dataframes.
We’ll need a function to generate those from the metadata, and then
we’ll need to apply it to all the records. Importantly we’ll have to
finesse those situations where representatives are listed twice (under
un-ambiguous and disambiguated names) with some groupby tricks.
.. code:: python3
def unique_legislator(row):
name, state, party = row.legislator, row.state, row.party
# Strip of disambiguating state designators
if re.search(r'(\w+) \([A-Z]{2}\)', name) is not None:
name = name[:-5]
return f"{name} ({party}, {state})"
.. code:: python3
for i, _ in enumerate(votes):
votes[i].index = pd.Index(metadata[i].apply(unique_legislator, axis=1), name="legislator_index")
votes[i] = votes[i].groupby(level=0).sum()
metadata[i].index = pd.Index(metadata[i].apply(unique_legislator, axis=1), name="legislator_index")
metadata[i] = metadata[i].groupby(level=0).first()
Now that we have the data at least a little wrangled into order there is
the question of ensuring some degree of continuity fore representatives.
To make this a little easier we’ll use voting records over *four year
spans* instead of over single years. Equally importantly we’ll do this
in a sliding window fashion so that we consider the record for 1990-1994
and then the record for 1991-1995 and so on. By overlapping the windows
in this way we can ensure a little greater continuity of political
stance through the years. To make this happen we just have to merge data
frames in a sliding set of pairs, and then merge the pairs via the same
approach:
.. code:: python3
votes = [
pd.merge(
v1, v2, how="outer", on="legislator_index"
).fillna(0.0).sort_index()
for v1, v2 in zip(votes[:-1], votes[1:])
] + votes[-1:]
metadata = [
pd.concat([m1, m2]).groupby("legislator_index").first().sort_index()
for m1, m2 in zip(metadata[:-1], metadata[1:])
] + metadata[-1:]
That’s the pairs of years; now we merge these pairwise to get sets of
four years worth of votes.
.. code:: python3
votes = [
pd.merge(
v1, v2, how="outer", on="legislator_index"
).fillna(0.0).sort_index()
for v1, v2 in zip(votes[:-1], votes[1:])
] + votes[-1:]
metadata = [
pd.concat([m1, m2]).groupby(level=0).first().sort_index()
for m1, m2 in zip(metadata[:-1], metadata[1:])
] + metadata[-1:]
Applying AlignedUMAP
--------------------
To make use of AlignedUMAP we need to generate relations between
consecutive dataset slices. In this case that means we need to have a
relation describing row from one four year slice corresponds to a row
from the following four year slice for the same representative. For
AlignedUMAP to work this should be formatted as a list of dictionaries;
each dictionary gives a mapping from indices of one slice to indices of
the next. Importantly this mapping can be partial – it only has to
relate indices for which there is a match between the two slices.
The vote dataframes that we are using for slices are already indexed
with unique identifiers for representatives, so to make relations we
simply have to match them up, creating a dictionary of indices from one
to the other. In practice we can do this relatively efficiently by using
pandas to merge dataframes on the pandas indexes of the two vote
dataframes with the data being simply the numeric indices of the rows.
The resulting dictionary is then just the dictionary of pairs given by
the inner join.
.. code:: python3
def make_relation(from_df, to_df):
left = pd.DataFrame(data=np.arange(len(from_df)), index=from_df.index)
right = pd.DataFrame(data=np.arange(len(to_df)), index=to_df.index)
merge = pd.merge(left, right, left_index=True, right_index=True)
return dict(merge.values)
With a function for relation creation in place we simply need to apply
it to each consecutive pair of vote dataframes.
.. code:: python3
relations = [make_relation(x,y) for x, y in zip(votes[:-1], votes[1:])]
If you are still unsure of what these relations are it might be
beneficial to look at a few of the dictionaries, along with the
corresponding pairs of vote dataframes. Here is (part of) the first
relation dictionary:
.. code:: python3
relations[0]
.. parsed-literal::
{0: 0,
1: 1,
3: 2,
4: 3,
5: 4,
6: 5,
7: 6,
8: 7,
9: 8,
10: 9,
11: 10,
12: 11,
13: 12,
14: 13,
15: 14,
...
475: 547,
476: 549,
477: 550,
478: 552,
479: 553,
480: 554,
481: 555,
482: 556,
483: 557,
484: 559}
Now we are finally in a position to run AlignedUMAP. Most of the
standard UMAP parameters are available for use, including choosing a
metric and a number of neighbors. Here we will also make use of the
extra AlignedUMAP parameters ``alignment_regularisation`` and
``alignment_window_size``. The first is a value that weights how
important retaining alignment is. Typically the value is much smaller
than this (the default is 0.01), but given the relatively high
volatility in voting records we are going to increase it here. The
second parameter, ``alignment_window_size`` determines how far out on
either side AlignedUMAP will look when aligning embeddings – even though
the relations are specified only between consecutive slices it will
chain them together to construct relations reaching further. In this
case we’ll have it look as far out as 5 slices either side.
.. code:: python3
%%time
aligned_mapper = umap.aligned_umap.AlignedUMAP(
metric="cosine",
n_neighbors=20,
alignment_regularisation=0.1,
alignment_window_size=5,
n_epochs=200,
random_state=42,
).fit(votes, relations=relations)
embeddings = aligned_mapper.embeddings_
.. parsed-literal::
CPU times: user 6min 7s, sys: 30.6 s, total: 6min 37s
Wall time: 5min 57s
Visualizing the Results
-----------------------
Now we need to plot the data somehow. To make the visualization
interesting it would be beneficial to have some colour variation –
ideally showing a different view of the relative political stance. For
that we want to attempt to get an idea of the position of each candidate
from an alternative source. To do this we can try to extract the vote
margin that the representative won by. The catch here is that while the
election data can be collected and processed, the names don’t match
perfectly as they come from a different source. That means we need to do
our best to get a name match for each candidate. We’ll use fuzzy string
matching restricted to the relevant year and state to try to get a good
match. A notebook providing details for obtaining and processing the
election winners data can be found
`here <https://github.com/lmcinnes/umap_doc_notebooks/blob/master/voting_data_by_district.ipynb>`__.
.. code:: python3
election_winners = pd.read_csv('election_winners_1976-2018.csv', index_col=0)
election_winners.head()
.. raw:: html
<div>
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>year</th>
<th>state</th>
<th>district</th>
<th>winner</th>
<th>party</th>
<th>winning_ratio</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>1976</td>
<td>AK</td>
<td>0</td>
<td>Don Young</td>
<td>republican</td>
<td>0.289986</td>
</tr>
<tr>
<th>0</th>
<td>1976</td>
<td>AL</td>
<td>1</td>
<td>Jack Edwards</td>
<td>republican</td>
<td>0.374808</td>
</tr>
<tr>
<th>0</th>
<td>1976</td>
<td>AL</td>
<td>2</td>
<td>William L. \\"Bill\"\" Dickinson"</td>
<td>republican</td>
<td>0.423953</td>
</tr>
<tr>
<th>0</th>
<td>1976</td>
<td>AL</td>
<td>3</td>
<td>Bill Nichols</td>
<td>democrat</td>
<td>1.000000</td>
</tr>
<tr>
<th>0</th>
<td>1976</td>
<td>AL</td>
<td>4</td>
<td>Tom Bevill</td>
<td>democrat</td>
<td>0.803825</td>
</tr>
</tbody>
</table>
</div>
Now we need to simply go through the metadata and fill it out with the
extra information we can glean from the election winners data. Since we
can’t do exact name matching (the data for both is somewhat messy when
it comes to text fields like names) we can’t simply perform a join, but
must instead process things year by year and representative by
representative, finding the best string match on name that we can for
the given year and state election. In practice we are undoubtedly going
to get some of these wrong, and if the goal was a rigorous analysis
based on this data a lot more care would need to be taken. Since this is
just a demonstration and we’ll only be using this extra information as a
colour channel in plots we can excuise a few errors here and there from
in-exact data processing.
.. code:: python3
n_name_misses = 0
for year, df in enumerate(metadata, 1990):
df["partisan_lean"] = 0.5
df["district"] = np.full(len(df), -1, dtype=np.int8)
for idx, (loc, row) in enumerate(df.iterrows()):
name, state, party = row.legislator, row.state, row.party
# Strip of disambiguating state designators
if re.search(r'(\w+) \([A-Z]{2}\)', name) is not None:
name = name[:-5]
# Get a party designator matching the election_winners data
party = "republican" if party == "R" else "democrat"
# Restrict to the right state and time-frame
state_election_winners = election_winners[(election_winners.state == state)
& (election_winners.year <= year + 4)
& (election_winners.year >= year - 4)]
# Try to match a name; and fail "gracefully"
try:
matched_name = process.extractOne(
name,
state_election_winners.winner.tolist(),
scorer=fuzz.partial_token_sort_ratio,
score_cutoff=50,
)
except:
matched_name = None
# If we got a unique match, get the election data
if matched_name is not None:
winner = state_election_winners[state_election_winners.winner == matched_name[0]]
else:
winner = []
# We either have none, one, or *several* match elections. Take a best guess.
if len(winner) < 1:
df.loc[loc, ["partisan_lean"]] = 0.25 if party == "republican" else 0.75
n_name_misses += 1
elif len(winner) > 1:
df.iloc[idx, 4] = int(winner.district.values[-1])
df.iloc[idx, 3] = float(winner.winning_ratio.values[-1])
else:
df.iloc[idx, 4] = int(winner.district.values)
df.iloc[idx, 3] = float(winner.winning_ratio.values[0])
print(f"Failed to match a name {n_name_misses} times")
.. parsed-literal::
Failed to match a name 100 times
Now that we have the relative partisan leanings based on district
election margins we can color the plot. We can obviously label the plot
with the representatives names. The last remaining catch (when using
matplotlib for the plotting) is the get the plot bounds (since we will
be placing text markers directly into the plot, and thus not
autogenerating bounds). This is a simple enough matter of computing some
bounds as an adjustment a little outside the data limits.
.. code:: python3
def axis_bounds(embedding):
left = embedding.T[0].min()
right = embedding.T[0].max()
bottom = embedding.T[1].min()
top = embedding.T[1].max()
width = right - left
height = top - bottom
adj_h = width * 0.1
adj_v = height * 0.05
return [left - adj_h, right + adj_h, bottom - adj_v, top + adj_v]
Now for the plot. Let’s pick a random time slice (you are welcome to try
others) and draw the representatives names in their embedded locations
for that slice, coloured by their relative election victory margin.
.. code:: python3
fig, ax = plt.subplots(figsize=(12,12))
e = 5
ax.axis(axis_bounds(embeddings[e]))
ax.set_aspect('equal')
for i in range(embeddings[e].shape[0]):
ax.text(embeddings[e][i, 0],
embeddings[e][i, 1],
metadata[e].index.values[i],
color=plt.cm.RdBu(np.float32(metadata[e]["partisan_lean"].values[i])),
fontsize=8,
horizontalalignment='center',
verticalalignment='center',
)
.. image:: images/aligned_umap_politics_demo_31_0.png
This gives a good idea of the layout in a single time slices, and by
plotting different time slices we can get some idea of how things have
evolved. We can go further, however, by plotting a representative as
curve through time as their relative political position in congress
changes. For that we will need a 3D plot – we need both the UMAP x and y
coordinates, as well as a third coordinate giving the year. I found this
easiest to do in plotly, so let’s import that. To make nice smooth
curves through time we will also import the ``scipy.interpolate`` module
which will let is interpolate a smooth curve from the discrete positions
that a representatives appears in over time.
.. code:: python3
import plotly.graph_objects as go
import scipy.interpolate
Wrangling the data into shape for this is the next step; first let’s get
everything in a single dataframe that we can extract relevant data from
on an as-needed basis.
.. code:: python3
df = pd.DataFrame(np.vstack(embeddings), columns=('x', 'y'))
df['z'] = np.concatenate([[year] * len(embeddings[i]) for i, year in enumerate(range(1990, 2021))])
df['representative_id'] = np.concatenate([v.index for v in votes])
df['partisan_lean'] = np.concatenate([m["partisan_lean"].values for m in metadata])
Next we’ll need that interpolation of the curve for a given
representative. We’ll write a function to handle that as there is a
little bit of case-based logic that makes it non-trivial. We are going
to get handed year data and want to interpolate the UMAP x and y
coordinates for a single representative.
The first major catch is that many representatives don’t have a single
contiguous block of years for which they were in congress: they were
elected for several years, missed re-election, and then came back to
congress several years later (possibly in another district). Each such
block of contiguous years needs to be a separate path, and we shouldn’t
connect them. We therefore need some logic to find the contiguous blocks
and generate smooth paths for each of them.
Another catch is that some representatives have only been in office for
a year or two (special elections and so forth) and we can’t do a cubic
spline interpolation for that; we can devolve to linear interpolation or
quadratic splines for those cases, so simply add the point itself for
the odd single year cases.
With those issues in hand we can then simply use the scipy ``interp1d``
function to generate smooth curves through the points.
.. code:: python3
INTERP_KIND = {2:"linear", 3:"quadratic", 4:"cubic"}
def interpolate_paths(z, x, y, c, rep_id):
consecutive_year_blocks = np.where(np.diff(z) != 1)[0] + 1
z_blocks = np.split(z, consecutive_year_blocks)
x_blocks = np.split(x, consecutive_year_blocks)
y_blocks = np.split(y, consecutive_year_blocks)
c_blocks = np.split(c, consecutive_year_blocks)
paths = []
for block_idx, zs in enumerate(z_blocks):
text = f"{rep_id} -- partisan_lean: {np.mean(c_blocks[block_idx]):.2f}"
if len(zs) > 1:
kind = INTERP_KIND.get(len(zs), "cubic")
else:
paths.append(
(zs, x_blocks[block_idx], y_blocks[block_idx], c_blocks[block_idx], text)
)
continue
z = np.linspace(np.min(zs), np.max(zs), 100)
x = scipy.interpolate.interp1d(zs, x_blocks[block_idx], kind=kind)(z)
y = scipy.interpolate.interp1d(zs, y_blocks[block_idx], kind=kind)(z)
c = scipy.interpolate.interp1d(zs, c_blocks[block_idx], kind="linear")(z)
paths.append((z, x, y, c, text))
return paths
And now we can use plotly to draw the resulting curves. For plotly we
use the ``Scatter3D`` method, which supports a “lines” mode that can
draw curves in 3D space. We can colour the curves by the partisan lean
score we derived from the election data – in fact the colour can vary
through the trace as the election margins vary. Since this is a plotly
plot it is interactive, so you can rotate it around and view it from all
angles.
Unfortunately the interactive plotly plot does not embed into the documentation
well, so we present here a static image. If you run this yourself, however,
you will get the interactive version.
.. code:: python3
traces = []
for rep in df.representative_id.unique():
z = df.z[df.representative_id == rep].values
x = df.x[df.representative_id == rep].values
y = df.y[df.representative_id == rep].values
c = df.partisan_lean[df.representative_id == rep]
for z, x, y, c, text in interpolate_paths(z, x, y, c, rep):
trace = go.Scatter3d(
x=x, y=z, z=y,
mode="lines",
hovertext=text,
hoverinfo="text",
line=dict(
color=c,
cmin=0.0,
cmid=0.5,
cmax=1.0,
cauto=False,
colorscale="RdBu",
colorbar=dict(),
width=2.5,
),
opacity=1.0,
)
traces.append(trace)
fig = go.Figure(data=traces)
fig.update_layout(
width=800,
height=600,
scene=dict(
aspectratio = dict( x=0.5, y=1.25, z=0.5 ),
yaxis_title="Year",
xaxis_title="UMAP-X",
zaxis_title="UMAP-Y",
),
scene_camera=dict(eye=dict( x=0.5, y=0.8, z=0.75 )),
autosize=False,
showlegend=False,
)
fig_widget = go.FigureWidget(fig)
fig_widget
.. image:: images/aligned_umap_politics_demo_spaghetti.png
..
.. raw:: html
:file: aligned_umap_plotly_plot.html
This concludes our exploration for now.
================================================
FILE: doc/api.rst
================================================
UMAP API Guide
==============
UMAP has only two classes, :class:`UMAP`, and :class:`ParametricUMAP`, which inherits from it.
UMAP
----
.. autoclass:: umap.umap_.UMAP
:members:
ParametricUMAP
----
.. autoclass:: umap.parametric_umap.ParametricUMAP
:members:
A number of internal functions can also be accessed separately for more fine tuned work.
Useful Functions
----------------
.. automodule:: umap.umap_
:members:
:exclude-members: UMAP
================================================
FILE: doc/basic_usage.rst
================================================
How to Use UMAP
===============
UMAP is a general purpose manifold learning and dimension reduction
algorithm. It is designed to be compatible with
`scikit-learn <http://scikit-learn.org/stable/index.html>`__, making use
of the same API and able to be added to sklearn pipelines. If you are
already familiar with sklearn you should be able to use UMAP as a drop
in replacement for t-SNE and other dimension reduction classes. If you
are not so familiar with sklearn this tutorial will step you through the
basics of using UMAP to transform and visualise data.
First we'll need to import a bunch of useful tools. We will need numpy
obviously, but we'll use some of the datasets available in sklearn, as
well as the ``train_test_split`` function to divide up data. Finally
we'll need some plotting tools (matplotlib and seaborn) to help us
visualise the results of UMAP, and pandas to make that a little easier.
.. code:: python3
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
%matplotlib inline
.. code:: python3
sns.set(style='white', context='notebook', rc={'figure.figsize':(14,10)})
Penguin data
------------
.. image:: https://raw.githubusercontent.com/allisonhorst/palmerpenguins/c19a904462482430170bfe2c718775ddb7dbb885/man/figures/lter_penguins.png
:width: 300px
:align: center
:alt: Penguins
The next step is to get some data to work with. To ease us into things
we'll start with the `penguin
dataset <https://github.com/allisonhorst/penguins>`__. It isn't very
representative of what real data would look like, but it is small both
in number of points and number of features, and will let us get an idea
of what the dimension reduction is doing.
.. code:: python3
penguins = pd.read_csv("https://raw.githubusercontent.com/allisonhorst/palmerpenguins/c19a904462482430170bfe2c718775ddb7dbb885/inst/extdata/penguins.csv")
penguins.head()
.. raw:: html
<div>
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>species</th>
<th>island</th>
<th>bill_length_mm</th>
<th>bill_depth_mm</th>
<th>flipper_length_mm</th>
<th>body_mass_g</th>
<th>sex</th>
<th>year</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>Adelie</td>
<td>Torgersen</td>
<td>39.1</td>
<td>18.7</td>
<td>181.0</td>
<td>3750.0</td>
<td>male</td>
<td>2007</td>
</tr>
<tr>
<th>1</th>
<td>Adelie</td>
<td>Torgersen</td>
<td>39.5</td>
<td>17.4</td>
<td>186.0</td>
<td>3800.0</td>
<td>female</td>
<td>2007</td>
</tr>
<tr>
<th>2</th>
<td>Adelie</td>
<td>Torgersen</td>
<td>40.3</td>
<td>18.0</td>
<td>195.0</td>
<td>3250.0</td>
<td>female</td>
<td>2007</td>
</tr>
<tr>
<th>3</th>
<td>Adelie</td>
<td>Torgersen</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>2007</td>
</tr>
<tr>
<th>4</th>
<td>Adelie</td>
<td>Torgersen</td>
<td>36.7</td>
<td>19.3</td>
<td>193.0</td>
<td>3450.0</td>
<td>female</td>
<td>2007</td>
</tr>
</tbody>
</table>
</div>
Since this is for demonstration purposes we will get rid of the NAs in
the data; in a real world setting one would wish to take more care with
proper handling of missing data.
.. code:: python3
penguins = penguins.dropna()
penguins.species.value_counts()
.. parsed-literal::
Adelie 146
Gentoo 119
Chinstrap 68
Name: species, dtype: int64
.. image:: https://github.com/allisonhorst/palmerpenguins/blob/c19a904462482430170bfe2c718775ddb7dbb885/man/figures/culmen_depth.png?raw=true
:width: 300px
:align: center
:alt: Diagram of culmen measurements on a penguin
See the `github repostiory <https://github.com/allisonhorst/penguins>`__
for more details about the dataset itself. It consists of measurements
of bill (culmen) and flippers and weights of three species of penguins,
along with some other metadata about the penguins. In total we have 333
different penguins measured. Visualizing this data is a little bit
tricky since we can't plot in 4 dimensions easily. Fortunately four is
not that large a number, so we can just do a pairwise feature
scatterplot matrix to get an idea of what is going on. Seaborn makes
this easy.
.. code:: python3
sns.pairplot(penguins.drop("year", axis=1), hue='species');
.. image:: images/basic_usage_8_1.png
This gives us some idea of what the data looks like by giving as all the
2D views of the data. Four dimensions is low enough that we can (sort
of) reconstruct what the full dimensional data looks like in our heads.
Now that we sort of know what we are looking at, the question is what
can a dimension reduction technique like UMAP do for us? By reducing the
dimension in a way that preserves as much of the structure of the data
as possible we can get a visualisable representation of the data
allowing us to "see" the data and its structure and begin to get some
intuition about the data itself.
To use UMAP for this task we need to first construct a UMAP object that
will do the job for us. That is as simple as instantiating the class. So
let's import the umap library and do that.
.. code:: python3
import umap
.. code:: python3
reducer = umap.UMAP()
Before we can do any work with the data it will help to clean up it a
little. We won't need NAs, we just want the measurement columns, and
since the measurements are on entirely different scales it will be
helpful to convert each feature into z-scores (number of standard
deviations from the mean) for comparability.
.. code:: python3
penguin_data = penguins[
[
"bill_length_mm",
"bill_depth_mm",
"flipper_length_mm",
"body_mass_g",
]
].values
scaled_penguin_data = StandardScaler().fit_transform(penguin_data)
Now we need to train our reducer, letting it learn about the manifold.
For this UMAP follows the sklearn API and has a method ``fit`` which we
pass the data we want the model to learn from. Since, at the end of the
day, we are going to want to reduced representation of the data we will
use, instead, the ``fit_transform`` method which first calls ``fit`` and
then returns the transformed data as a numpy array.
.. code:: python3
embedding = reducer.fit_transform(scaled_penguin_data)
embedding.shape
.. parsed-literal::
(333, 2)
The result is an array with 333 samples, but only two feature columns
(instead of the four we started with). This is because, by default, UMAP
reduces down to 2D. Each row of the array is a 2-dimensional
representation of the corresponding penguin. Thus we can plot the
``embedding`` as a standard scatterplot and color by the target array
(since it applies to the transformed data which is in the same order as
the original).
.. code:: python3
plt.scatter(
embedding[:, 0],
embedding[:, 1],
c=[sns.color_palette()[x] for x in penguins.species.map({"Adelie":0, "Chinstrap":1, "Gentoo":2})])
plt.gca().set_aspect('equal', 'datalim')
plt.title('UMAP projection of the Penguin dataset', fontsize=24);
.. image:: images/basic_usage_17_1.png
This does a useful job of capturing the structure of the data, and as
can be seen from the matrix of scatterplots this is relatively accurate.
Of course we learned at least this much just from that matrix of
scatterplots -- which we could do since we only had four different
dimensions to analyse. If we had data with a larger number of dimensions
the scatterplot matrix would quickly become unwieldy to plot, and far
harder to interpret. So moving on from the Penguin dataset, let's consider
the digits dataset.
Digits data
-----------
First we will load the dataset from sklearn.
.. code:: python3
digits = load_digits()
print(digits.DESCR)
.. parsed-literal::
.. _digits_dataset:
Optical recognition of handwritten digits dataset
--------------------------------------------------
**Data Set Characteristics:**
:Number of Instances: 5620
:Number of Attributes: 64
:Attribute Information: 8x8 image of integer pixels in the range 0..16.
:Missing Attribute Values: None
:Creator: E. Alpaydin (alpaydin '@' boun.edu.tr)
:Date: July; 1998
This is a copy of the test set of the UCI ML hand-written digits datasets
https://archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits
The data set contains images of hand-written digits: 10 classes where
each class refers to a digit.
Preprocessing programs made available by NIST were used to extract
normalized bitmaps of handwritten digits from a preprinted form. From a
total of 43 people, 30 contributed to the training set and different 13
to the test set. 32x32 bitmaps are divided into nonoverlapping blocks of
4x4 and the number of on pixels are counted in each block. This generates
an input matrix of 8x8 where each element is an integer in the range
0..16. This reduces dimensionality and gives invariance to small
distortions.
For info on NIST preprocessing routines, see M. D. Garris, J. L. Blue, G.
T. Candela, D. L. Dimmick, J. Geist, P. J. Grother, S. A. Janet, and C.
L. Wilson, NIST Form-Based Handprint Recognition System, NISTIR 5469,
1994.
.. topic:: References
- C. Kaynak (1995) Methods of Combining Multiple Classifiers and Their
Applications to Handwritten Digit Recognition, MSc Thesis, Institute of
Graduate Studies in Science and Engineering, Bogazici University.
- E. Alpaydin, C. Kaynak (1998) Cascading Classifiers, Kybernetika.
- Ken Tang and Ponnuthurai N. Suganthan and Xi Yao and A. Kai Qin.
Linear dimensionalityreduction using relevance weighted LDA. School of
Electrical and Electronic Engineering Nanyang Technological University.
2005.
- Claudio Gentile. A New Approximate Maximal Margin Classification
Algorithm. NIPS. 2000.
We can plot a number of the images to get an idea of what we are looking
at. This just involves matplotlib building a grid of axes and then
looping through them plotting an image into each one in turn.
.. code:: python3
fig, ax_array = plt.subplots(20, 20)
axes = ax_array.flatten()
for i, ax in enumerate(axes):
ax.imshow(digits.images[i], cmap='gray_r')
plt.setp(axes, xticks=[], yticks=[], frame_on=False)
plt.tight_layout(h_pad=0.5, w_pad=0.01)
.. image:: images/basic_usage_22_0.png
As you can see these are quite low resolution images -- for the most
part they are recognisable as digits, but there are a number of cases
that are sufficiently blurred as to be questionable even for a human to
guess at. The zeros do stand out as the easiest to pick out as notably
different and clearly zeros. Beyond that things get a little harder:
some of the squashed thing eights look awfully like ones, some of the
threes start to look a little like crossed sevens when drawn badly, and
so on.
Each image can be unfolded into a 64 element long vector of grayscale
values. It is these 64 dimensional vectors that we wish to analyse: how
much of the digits structure can we discern? At least in principle 64
dimensions is overkill for this task, and we would reasonably expect
that there should be some smaller number of "latent" features that would
be sufficient to describe the data reasonably well. We can try a
scatterplot matrix -- in this case just of the first 10 dimensions so
that it is at least plottable, but as you can quickly see that approach
is not going to be sufficient for this data.
.. code:: python3
digits_df = pd.DataFrame(digits.data[:,1:11])
digits_df['digit'] = pd.Series(digits.target).map(lambda x: 'Digit {}'.format(x))
sns.pairplot(digits_df, hue='digit', palette='Spectral');
.. image:: images/basic_usage_24_2.png
In contrast we can try using UMAP again. It works exactly as before:
construct a model, train the model, and then look at the transformed
data. To demonstrate more of UMAP we'll go about it differently this
time and simply use the ``fit`` method rather than the ``fit_transform``
approach we used for Penguins.
.. code:: python3
reducer = umap.UMAP(random_state=42)
reducer.fit(digits.data)
.. parsed-literal::
UMAP(a=None, angular_rp_forest=False, b=None,
force_approximation_algorithm=False, init='spectral', learning_rate=1.0,
local_connectivity=1.0, low_memory=False, metric='euclidean',
metric_kwds=None, min_dist=0.1, n_components=2, n_epochs=None,
n_neighbors=15, negative_sample_rate=5, output_metric='euclidean',
output_metric_kwds=None, random_state=42, repulsion_strength=1.0,
set_op_mix_ratio=1.0, spread=1.0, target_metric='categorical',
target_metric_kwds=None, target_n_neighbors=-1, target_weight=0.5,
transform_queue_size=4.0, transform_seed=42, unique=False, verbose=False)
Now, instead of returning an embedding we simply get back the reducer
object, now having trained on the dataset we passed it. To access the
resulting transform we can either look at the ``embedding_`` attribute
of the reducer object, or call transform on the original data.
.. code:: python3
embedding = reducer.transform(digits.data)
# Verify that the result of calling transform is
# idenitical to accessing the embedding_ attribute
assert(np.all(embedding == reducer.embedding_))
embedding.shape
.. parsed-literal::
(1797, 2)
We now have a dataset with 1797 rows (one for each hand-written digit
sample), but only 2 columns. As with the Penguins example we can now plot
the resulting embedding, coloring the data points by the class that
they belong to (i.e. the digit they represent).
.. code:: python3
plt.scatter(embedding[:, 0], embedding[:, 1], c=digits.target, cmap='Spectral', s=5)
plt.gca().set_aspect('equal', 'datalim')
plt.colorbar(boundaries=np.arange(11)-0.5).set_ticks(np.arange(10))
plt.title('UMAP projection of the Digits dataset', fontsize=24);
.. image:: images/basic_usage_30_1.png
We see that UMAP has successfully captured the digit classes. There are
also some interesting effects as some digit classes blend into one
another (see the eights, ones, and sevens, with some nines in between),
and also cases where digits are pushed away as clearly distinct (the
zeros on the right, the fours at the top, and a small subcluster of ones
at the bottom come to mind). To get a better idea of why UMAP chose to
do this it is helpful to see the actual digits involve. One can do this
using `bokeh <https://bokeh.pydata.org/en/latest/>`__ and mouseover
tooltips of the images.
First we'll need to encode all the images for inclusion in a dataframe.
.. code:: python3
from io import BytesIO
from PIL import Image
import base64
.. code:: python3
def embeddable_image(data):
img_data = 255 - 15 * data.astype(np.uint8)
image = Image.fromarray(img_data, mode='L').resize((64, 64), Image.Resampling.BICUBIC)
buffer = BytesIO()
image.save(buffer, format='png')
for_encoding = buffer.getvalue()
return 'data:image/png;base64,' + base64.b64encode(for_encoding).decode()
Next we need to load up bokeh and the various tools from it that will be
needed to generate a suitable interactive plot.
.. code:: python3
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import HoverTool, ColumnDataSource, CategoricalColorMapper
from bokeh.palettes import Spectral10
output_notebook()
.. raw:: html
<div class="bk-root">
<a href="https://bokeh.org" target="_blank" class="bk-logo bk-logo-small bk-logo-notebook"></a>
<span id="1001">Loading BokehJS ...</span>
</div>
Finally we generate the plot itself with a custom hover tooltip that
embeds the image of the digit in question in it, along with the digit
class that the digit is actually from (this can be useful for digits
that are hard even for humans to classify correctly).
.. code:: python3
digits_df = pd.DataFrame(embedding, columns=('x', 'y'))
digits_df['digit'] = [str(x) for x in digits.target]
digits_df['image'] = list(map(embeddable_image, digits.images))
datasource = ColumnDataSource(digits_df)
color_mapping = CategoricalColorMapper(factors=[str(9 - x) for x in digits.target_names],
palette=Spectral10)
plot_figure = figure(
title='UMAP projection of the Digits dataset',
width=600,
height=600,
tools=('pan, wheel_zoom, reset')
)
plot_figure.add_tools(HoverTool(tooltips="""
<div>
<div>
<img src='@image' style='float: left; margin: 5px 5px 5px 5px'/>
</div>
<div>
<span style='font-size: 16px; color: #224499'>Digit:</span>
<span style='font-size: 18px'>@digit</span>
</div>
</div>
"""))
plot_figure.scatter(
'x',
'y',
source=datasource,
color=dict(field='digit', transform=color_mapping),
line_alpha=0.6,
fill_alpha=0.6,
size=4
)
show(plot_figure)
.. raw:: html
:file: basic_usage_bokeh_example.html
As can be seen, the nines that blend between the ones and the sevens are
odd looking nines (that aren't very rounded) and do, indeed, interpolate
surprisingly well between ones with hats and crossed sevens. In contrast
the small disjoint cluster of ones at the bottom of the plot is made up
of ones with feet (a horizontal line at the base of the one) which are,
indeed, quite distinct from the general mass of ones.
This concludes our introduction to basic UMAP usage -- hopefully this
has given you the tools to get started for yourself. Further tutorials,
covering UMAP parameters and more advanced usage are also available when
you wish to dive deeper.
--------------
.. raw:: html
<h3>
Penguin data information
.. raw:: html
</h3>
Peguin data are from:
**Gorman KB, Williams TD, Fraser WR** (2014) Ecological Sexual
Dimorphism and Environmental Variability within a Community of Antarctic
Penguins (Genus *Pygoscelis*). PLoS ONE 9(3): e90081.
doi:10.1371/journal.pone.0090081
See the full paper
`HERE <https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0090081>`__.
.. raw:: html
<h4>
Original data access and use
.. raw:: html
</h4>
From Gorman et al.: "Data reported here are publicly available within
the PAL-LTER data system (datasets #219, 220, and 221):
http://oceaninformatics.ucsd.edu/datazoo/data/pallter/datasets. These
data are additionally archived within the United States (US) LTER
Network's Information System Data Portal: https://portal.lternet.edu/.
Individuals interested in using these data are therefore expected to
follow the US LTER Network's Data Access Policy, Requirements and Use
Agreement: https://lternet.edu/data-access-policy/."
Anyone interested in publishing the data should contact `Dr. Kristen
Gorman <https://www.uaf.edu/cfos/people/faculty/detail/kristen-gorman.php>`__
about analysis and working together on any final products.
Penguin images by Alison Horst.
================================================
FILE: doc/basic_usage_bokeh_example.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bokeh Plot</title>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-2.0.0.min.js" integrity="sha384-5Y+xuMRAbgBj/2WKUiL8yzV4fBFic1HJPo2hT3pq2IsEzbsJjj8kT2i0b1lZ7C2N" crossorigin="anonymous"></script>
<script type="text/javascript">
Bokeh.set_log_level("info");
</script>
</head>
<body>
<div class="bk-root" id="9584024b-4d3f-44a1-9bf6-f58d2e4535c5" data-root-id="1088"></div>
<script type="application/json" id="1261">
{"d689e807-24c5-4a70-852d-845dc7ab161f":{"roots":{"references":[{"attributes":{"below":[{"id":"1099"}],"center":[{"id":"1102"},{"id":"1106"}],"left":[{"id":"1103"}],"renderers":[{"id":"1119"}],"title":{"id":"1089"},"toolbar":{"id":"1110"},"x_range":{"id":"1091"},"x_scale":{"id":"1095"},"y_range":{"id":"1093"},"y_scale":{"id":"1097"}},"id":"1088","subtype":"Figure","type":"Plot"},{"attributes":{},"id":"1108","type":"WheelZoomTool"},{"attributes":{},"id":"1132","type":"BasicTickFormatter"},{"attributes":{},"id":"1109","type":"ResetTool"},{"attributes":{"active_drag":"auto","active_inspect":"auto","active_multi":null,"active_scroll":"auto","active_tap":"auto","tools":[{"id":"1107"},{"id":"1108"},{"id":"1109"},{"id":"1114"}]},"id":"1110","type":"Toolbar"},{"attributes":{},"id":"1133","type":"Selection"},{"attributes":{"source":{"id":"1086"}},"id":"1120","type":"CDSView"},{"attributes":{},"id":"1134","type":"UnionRenderers"},{"attributes":{"axis":{"id":"1103"},"dimension":1,"ticker":null},"id":"1106","type":"Grid"},{"attributes":{"data_source":{"id":"1086"},"glyph":{"id":"1117"},"hover_glyph":null,"muted_glyph":null,"nonselection_glyph":{"id":"1118"},"selection_glyph":null,"view":{"id":"1120"}},"id":"1119","type":"GlyphRenderer"},{"attributes":{},"id":"1130","type":"BasicTickFormatter"},{"attributes":{},"id":"1091","type":"DataRange1d"},{"attributes":{},"id":"1107","type":"PanTool"},{"attributes":{"callback":null,"tooltips":"\n<div>\n <div>\n <img src='@image' style='float: left; margin: 5px 5px 5px 5px'/>\n </div>\n <div>\n <span style='font-size: 16px; color: #224499'>Digit:</span>\n <span style='font-size: 18px'>@digit</span>\n </div>\n</div>\n"},"id":"1114","type":"HoverTool"},{"attributes":{},"id":"1097","type":"LinearScale"},{"attributes":{},"id":"1093","type":"DataRange1d"},{"attributes":{"data":{"digit":["0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","3","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","2","5","7","9","5","4","4","9","0","8","9","8","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","1","2","3","4","5","6","7","8","9","0","9","5","5","6","5","0","9","8","9","8","4","1","7","7","3","5","1","0","0","2","2","7","8","2","0","1","2","6","3","3","7","3","3","4","6","6","6","4","9","1","5","0","9","5","2","8","2","0","0","1","7","6","3","2","1","7","4","6","3","1","3","9","1","7","6","8","4","3","1","4","0","5","3","6","9","6","1","7","5","4","4","7","2","8","2","2","5","7","9","5","4","8","8","4","9","0","8","9","8"],"image":["data:image/png;base64,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","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAE20lEQVR4nI1XyW7kRgwlWavaSQaT//+7IEAuY9hqq1QbyRwktd1qtUcE7INd9fS4PbJQ4dHK63///vPvf7+uubGS9fHlx8+ff//88efL5TJE78yXs3RwHwAAcP0BAP1iD+eeACAAIm63RWWxA4xnDHAxUFURYbnZHsE+dQEXCioozH01FtkxOARApNVQVQGptVpLKaGGzmcYICKRMYYIQEUETM455+BdaLxDOAYgImOsMYQqHUjJzSEG71wIXeSUC8YYYy0hCAswWu+j99bF2vhMEHGBIAThxsBgnE/eu5Av+yA8ywISEiKCcu9KapyPMYdcG8sJBrdsqzBzF8LWaq21deYTAArCdybKzPKklo8YCDP33lprrXdmVtA1tURrgX/PQJlbqzXnXGrrLGtarLXWGNohHPWCcGs157wgsAKSsc4575yze4QDAOHWSslzSinn1lkWAB9CCM4aumfw6IJyq6XM8zzPc661CwCu92Pw7rcAwnW9Pc+5lMaAQGSdDzGG4J019y7sAERan1OapjTPcym1d0ECJGO9D/EbBgqqoNqZa0vpfbxOKdfaO4sgARBZ67333ltL9AigKsrCwq21XNI0/np9u6ayyIeqIhrrffyGgayFU0qe52ka315fx1S6KCiAApCxLsZhGIJ3RzFQ6bWUnEua0zRdp+t1HMepsgKgAgAiOR/iMMTgrTlgoNJrTlNKHx8f1+s4TdOU5lxZt7NkrA9DHGL01hzEAIRbTtfrNI7v7+M4TbmU3poAICqAAhrrwpIFS0cuKPea03R9f3t7exunNX4KgAiggEjWuRBC8PsyWBmocO+t5DynaZqmubIAwGfvIpJx1jlrDcG3zYRL1y4jBeATgYiMMYaIDtsZyVjrQhxq7ULW59ZWDYEFBW+jasnLDgDRWB8vDEg2DGlKaZ5LKXcKpNvvw8lExkVRE8Lwx18pp4+P63j9ENab/qmCiD5Oxo0BWa9o/OUl55LrfH1/jUZbUbkFYhuxiACPaUSyQNYPqw7O4+tguEx0+56qijB3FtxnYXUByDgWYWFhye8XaunqCGT9PKgId+bOKHjEABGMbj0NOWIZfwWL+hmCRamZcdfNmx7cwXpJlyHYrydFZFkQSMAcAdwZheDvxVN17XdHtKuDwxVnmSAbKQRV4d5ara11lvtMHgKoqt4VjAr3VsoyZ04wYBaRGwQiqHBr9TSA9r4wvUVBhXuttdba+X5BOQTonVc5WBgsMeit1tY76+9jwHf3AWBF6L097nlPV92dVyLCzCK7Fes5wG6b0O1PD+38NI2q+oUswm133SnSEwYKqqJy+/omSA8LynEaQXcurFJJ9IjwlAHc1+IXAr9fcXAT0cN/nXABEJGQyJjtkoKqiOoyre/OPlu2jbXOua6ggAAqm6Ccei8gGetCjLGKiAqiKvdeSym1NT6xrSMZ5+NweWHoS/MIt1ryHHKMZ5ZtMtaHePkjMZTWOqDKMjh9iEPjEwyMdWG4vLywEgIIgvRW8xz9kGs/sa0jWRficHnpvJSj3lworZ94cKAx1oU4DLX1zl0FhHurpZRS2z4Ph4WEZK3zfltt8VNU+zlNpK0OrDVEWx2s78YTkva5T5j1gaCqy+uVReEEA0QkQjKGEBGWB6zq9ng+w2Ah8SkA3zzf/wdaTzy7ie84PwAAAABJRU5ErkJggg==","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAFTUlEQVR4nI1Xy3IjNwwEQHI48m5OOSSpyv9/VpKqHFNeS5oXSbxyoGRJtiQvTjoIPU0QaDTR4Un89+8/f/3973/Hxh6H3csvv/7+x59//PbrLwO+/4We5YO5u4ODAzg4uLu7A1x/9CmAmplfov8GvyH9HEDV9IxhZh0P/BoiPsh1AAcRET0loqmqageBrwD6ca01ZhFVVXUQYj4huiE+BwBzN9Vaa+tZ4uYQW20sqmbugE8B3M1UuJQTAqujOw21NWZRNXrOwN1MhMu2lVIbs7A6quBQa2tnCgCAjxmYSKvbsq5bqU1Y1IGEaq2NRVTtUsb7AGbS6jbP87KVyiyiDgjhxN/cwZ/WwF2llXWepmUtjUVVAXpbnLviKwY9/zjNa2li5gAIgOe4/utdAARt23w87A/TsjWxU25MKcUYAhF+BeDa1mn/Y/+2n7amDgREGMaccx6GFAN92UhalsOP1/3bflqrOAakEOJutxvHPKQUryg8uEYu8+HH69vhOK1NnYBCjMPLy8vLbszDEOlSiAdF5DLv3368Hed1awoYQkzDuHt56RTClwykrvNx//Y2L7WpAmEH6EcYYsQvaiC1rMt0PE7zxuqORCGmNOSc85BijA9v4SRbsC3LMs/LsmxFDSEAEVEIMcYQPtziDYC7qampbofjNC/rViubE3rvA0JEInrSSG4i3Jh5fX3bT+tWm4gDBLhJuUn/AKDcSimlLK+v+2mpLGpw1uCzJrs/BHA3KeuyLOv8+rqftiZXU+vgJ019DACmXNfpeJym/ethLqz2zvn8ffvE4ZqBKddtOuwPx8N+WqtYn8F3gqel8BSgbet83B+P01JY/VK98zDDp3gH6DrYWlmXZVm22sSu/oZE1HuA7t6Cg4OpSGulbOuybKWKObx/EpFiSmlIKYYPNOL791W4ca1l27atVBZzRCR3AEAMMQ15zDmnEO41krupMnNrtdZSa2U1P6c7AFJMeRx3Y06J6DMDdzMT4XYKFjVACgBg5uBAIQ557FoQ7h/BVEVEWFjUzAEpICGggiEAUIgp55yHGzm7KaKZvq89DMEhmBFxdxeIGGKMKcYY6EERve9+IIpxMAjRzYQRQQzBoU/ip0u8bWXEENKQR/bAqm4mlRAcANy6O1C1B7OAiBRCTOPuW/OQq4iaKpdIYO5uylxrKbWxmN0BQECikLKwOA7fSmUVFa5rJDcXV25bHELK3wrL3VlACn3cKObvpTZmllZLDqAi6toIEYHSt62KfmaASOARkCgM4/faWmtcuWxrQpPaRMTBzB3T97Wy3WGAQIBIIQ7jC7P2jlzXOVgrQ0BXNVF3yvNW5V4NACGQBUtZVc201Vq2ZY7WtmWI6GrK4h52a7nRqesiOpIDdGOqrZZtTARtHXMkdBMEQ8qltLNOfeoDvNbbIcVALrzLKQYCMHfgwMyiH27xwWoL2U2GIZ06tw/lyTd/iAdWt+tPCERwdraISPRZ1h57ZQTEbnk7AoUYQggfIR4AmJlfmAMA4Hk3fpin+wDeTTVcnTqEGO9M84MinqcOEQH7cSiEEMOn3XqfQZcGJMR3GUB6X8wPF8t1IJ4MwckS9P12b73erwFi9yQ9UowB8SwqP/PgQEQKSXPO4ziOObOBgatI98rqjyzOBYEC+MB57AisLm7KzMwiakZfAiABgLQ8juM45iam7iqtA+jXbyYkQIRhyD1qC2QmIvzxtfDQKyM5wqWKqSEYCIv03fE1AwCEvku6Qyfqy+snb6FDnAbyPJRg5/jZlyteZhoRLzbtthWfPX1PvYyndXo3/znAVcAZ4qMm/Q+kA8dCz0mDUAAAAABJRU5ErkJggg==","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAHL0lEQVR4nH1X2XLryg3EMispWfLxzf9/Xm6lUteWxW0WAHmgbMtb5kmiapoNoNGA0ODuqJn0tl6f//Pvv//+7+sq5ELKKcSUUh7yOB6Pp/GQ7q8QfD5mqqqqpvZ+9O2h7M/hVwAzU1Hp0kVUVd/vioj0/vb8E4L7TEBVemu1td5FVRVFVaQTN+LKPrTeVZV/BVDtrZZSSqmtdxFB7MSEeKPrYm1dlPBXBtLrti7zvKxbqbV1FFWT3nyrtXcFF1LunRDxRwA0aWWZp+t1mpZ1K5sYt9aC8877uOamHHLOkRkNfwIAlbLO18vry+U6zetauhE77xw750LKpVNIeUjuNwZgvSzXl5fX55fLdV7W0hWJnWMi5hBzEY7DoTRPBAhg+A1A2ja/vjy/vLy8TvO61aaIxESISC4MRX0+LqVGNbSfGJjUdbq8/PNyuVyntdTWFAGJEMyA/dIwHaa1NLmTwicA6WWdr5eXy+U6b7WrAQACgKmIoKsYjstaWhezb0k0ALBey7ZM1+t1XkszZCAjQkLoDUQbbqXU1uWTFt3tupmplW1d5mma5mVrho4MyDlmtFrWtant7aBm9iUEAzVVkW1dpmm6TvNWBZiROMQQHGmZr9e1AuFOFQDeY7gxMBVpbV3m+TpN81I6kHPOh2EYUiRZL8/hdRHPdxo0wDsAFWl1W5Z5mqd5Xpsxx5jieHw4joll+ic7ouod3yC+VsFM9y5Y5mme103JuTSM4/Hx8fwwcL8enIou7N2e1K8h2N7GZVuXdVnWrZqnkA+nw+PT09PpwP3i+7Zuio6JaC/tVwam0lqpZdu2rVTyFPLx9PDnr389PR5ci+36PMQNCBEA7+9/6EBVe2+t1lpbd+jicDydzufT+XxwVS45Oka1L372AWBmpirSe+9dFDnm4+nxdH44HoZMHD0jqKioKgAifhTjXUhvdiqiCuTT4fTn6eH0MKZI4AhNpXftXc2QiAi/+cHNf9XMADkOx/Ofv47jYQgMACbSW6tamyjgjnAr5RdLMzAzIBeHh/PT0yEN0ROA9t5qLVVb7wqAP1oavke29/7h9Phn9CEyAEhvtdXWtHVRBdwLsVvCDoCAb8wQAcmFPD6czpmZCQC070ely23kfGWAuN8n2j/5kIbxkGHndJtVezuKdBHFrzlAInLOMdH+jX2I8SNBiIjMgqDSW2uN7K0O7u13Yu9DcI4IdwR2Hj7Q2XnvidGkbktAC472d7k9BUTsfEwxeEcIBgaAd/MHyYeUUg9sfVsmNpXoHOIdA2IXtKUUHONNFHZ33/mYhq0FhrZNGc0UgPguhD1ySTF4RjDb3et9dBOHlIetOWd9naMZAJG7LyMSILLkGBwTgqlK7y2+U3A+5mGr5KBvswck9l4+kohGhkROYvSeCW65rm8ASOxjSpmAoZfVIYeYon0SEiCwheA97wyk11LyR5Gd8yEoEGgr3sfSuupnHQAAOueYiUhBey3bmtJN3UTEzMyGu32KvA+nT81EREyEANrrts6zV8fMCO8aNbwVyN599fN+sAsaQXtZp9eRNMTIAMxMt+LuortzlM/D9daTpm2bXy8ZdUD2AOiYEFTFCHAP5r2jv+wHAESEqK0s15doquwNAQjRVEXAYBe1Y/6Zwd7UaLLNr0MwoJCUABBtn9CG5HwI3jumHxiYIbFzrqO0dXoNQCEP3d1GX+9Chux82OWC+B0AkZzzvqP1slw9ujSMJRD0Lr2L6FufB//W+F9ygOR8iFGMrJclcBgO8+BCL6W01lUJyPmwj+yfQgBkH1JKqgRSVk8+Dyla7Nd5WUttQsgupJzS3vbfdUDOxzyM0IC0bQwupeAkyOu+cwkD+ZiGYUjB8Q85AHIhDYfNqqD1DYxDcFijXJ8v13mr5pFDHIZxSMHzDzlAcjEP4wZbVZBqRs4TlKjXl8u0bBUNXcjDOA7R/8ogj8cKRK1rEwX2CFvU6fl12qowsI8pD+Pg+U1JXwFiGg4VENGagAA7hBJseZ3W2g3Z+ZjykDPjbdH4FkLI49YMTKWrCLInaMG2aWuC7EJMKaWUAgHCzwxCGkpTE+mtm7SyMmmAUrpxgDQMOacY/N2tLwx8HIooomnvvZu2wqyBWgc/eE7n08OY4/39LwzYpaEbMJr0LtpBW2HtZMo5gh/OT+dDDo5+A0DycVBABu29dQEDaZsJM/nILo2np8djDvf/eL4nUQGJtLdaajcxbWjqfAgx5WF8OJ8P6ROB70kEREJp27puTUFMmpkxhfFwOI7Hh4cx+d8BkNgjIlrbhpyC76pmzQADhnw8PxwO42EI/OOad0NgIESTLacYvK+EqgjIii4Oh4fjmIfo/y8AAgFoj7tpEYKqGbIC+Zhzzjl6R/8nBwgEYM7vqwYhmqmhqsFuZcH7dyf5BQDBeD90W6pNVXez9G5f1z/nHb4ewtumtKv9NogQ97F1v+EBAMD/APLFxFB5KbsRAAAAAElFTkSuQmCC","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAFcklEQVR4nI1XSbPzxg0EMAsp2XHlklziKvv//6n4kFx8yHuiSM6GJQcOtTyJ7zMOqpJK09PA9DQwaPAahgCmJX38+Z9///HHf/+cEgAAnH75x79++/33X//59/PgCAAQAOjN+o4C1j++jfcA21KzDvJd+KPtwWxD+AGFwxRMt/h++QEA7vs/M9h/+gsMNgRV7WV4BIBnjIMU8JbDVwbwhHlcAzNVeV4Pt5T+AgNTERFmEdWnpF+O9UAHIsy1ttZEzQDvqSEAPnw/AhBprZScS2ssNwQiQiLCJ4RnIVnn37jUlFJKubLoJnoE7713zhE97uqflxsYmEmrOa/L9bqsqbAaIiIixRhjCN47InwL0C+AaitlXZfrNE1zKk0MCJGcG8dxHGIIwRHiWwAzUDPhWtZ5nqfLdLkuuYmhQ3I+nE6ncRyGGDy+ZWBmCqoqraR1nqbLZfqc5lTFkMj5OJzO59NpjME7BzeIHcDQzFRNVLjkdb5eLp+XaZqWXMWQnI9x/JaBGaiKqrDUsi7X6fL5MU3Xec1NDJxzPg73GuDXGhiYmSiLMLealut0uVymyzwvubIaIjnv4/0UXnWgqsrMjVvtBC7TNC9rLo0VgIjI+ZsOHpXoN+UoC9dWa80l53Wep+kyXec15drUjBCph6OHI3xIQVqrueSc0rquy7LM87wsKZfaNiUiIm1yer4JNwbccl7XdV2u8zIvS1pTyrnWyk3UAAAR+8fX6ACyaec6TZfpuiy51MqtX2fYNobdSuwVAFS45OU6TR+f/7tM81obq5qp6MNtvtvkKwMTbiWn5Tp9fnx8zqmJASKaPvifmZnpi6v2IpoKt1prTuuyLIkViBzd/2ybx6o9YT7oYCv0YxARgd4ccG8T+rXPbABIzvk4nMo55ybkWQCR0FRVrC8XYRH5SmADQHQ+jk3MFNEPc2qsZmDKwiS6Zegat/Zisp0BuShqFIIfxp+vy1orCytzq60iq6khOm6NWeRLDhsD8tHQDeN4+tsv12VZS661tFqzz1DVQAGAfGutyXOruAG4gD62808/p2Vd1zWlNaeUsqOtv4ABEDdmEX17jITkZRCppeSc0pLmZV6WGBCEmQDUDJj5EADRyIKZtcallLSu52sMDkC4Fodopoh7q3xXg1u38UHCUIYYEFS5cQnP7vEmnm0dgyPvPIJwrTlG73cHRyTnNjd55wf3oICIJjUPMe49BDel+BCC90Rv/OCJhAPT3fzu+zvvQ3yH8Npckfzufo6QeoXIhRhjDN45/AEA9GwdEd0YkPM+hBi8+2KJb9v7juDoVjJEcq5b8g9qAIDo3J1EP+B+xREBDvzgvh6Aehm8p30YsN1SVFXtgcTbCYWIblngNnB1x2qtscjjjX476iJ2hF5FVSBuOeaSS22NFej9fLCHIewIRIimokiIPqw519pYDmekRw7Oua4kMwUAMwrrmnNtLJuYDA8ZwNaPu/jB1ABUKKSUS6mNvd3q+B7ANpv13ncEE1N2MeVcauvtzr5JAZHIBR9C8ME7IjQThVprrbU1FvG3czgYdYm8D32i6Po3ExHe4qE/vGdA5H2MY9mmolEM1dTAVDuG32beIwBEcpHHVk+n0/l0KmIoKkYIYMLcGvPesY9q4HwYhDmffzqfU1WgJqSOEFSFmVmICI+PEZC8qgifz+fzOVU1QEbzjhBMmbmFff1xCsFU2jiezqdx5K1BekcA2p8RpvhNEZEcbADDMI5jY1EDc47QTFVERJS+OwVEAvUhxjgMMcZYvRc1wk3XujXp74QESLApsYcjItuu4Jc54YABAJGjuy9tXvTuPXnEoJsCbc8cJEQ0BHx9Ux+/3u/DTh8v92Gtv2v63/4PzxSFumf3u9oAAAAASUVORK5CYII=","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAGTUlEQVR4nIVX2ZIjuQ3ExaNKV8/sRNj7/59mO+yHDY+uungAfmBJLXWr23yTgshKJIEEiQaf1/TXf/71j3/++6/zMKcK5Pu3X3//88+//bHbBu+EGR/20ot4UFNVMzMzAAB8tec7gFpLrQ0CGsJtvUB7BVByKbVUrWpmBi3wBvNxszz/NDDTtCwp5VKqqtoaS0SvM3kGMFXVMs3zNC9LLrlUNaSVOCK9oPARoJSaxmEYx2lZUi6lGplZ409IiB9FeAbQWlKah8vlOozjvKRcKkBLA+k1wgcGJc3TeD2fz9dhnJaUigGZASARElFD+I5BSdNwuZxO5/N1nOYlVyBDImJiXuO/A7Ca5/FyOh+Pp/N1mJdclACJ2YkwN4hvU9Cal/F6Ph6Pp+swzSkrADnvgw/eixC/OIbHQjItaR6v5/P5fB2mJRUFEh+6zWbTdcGLvCqGdwZmVvIyDefT8Xi6DNOSq5GEfrfd7w/7bR+9vGJwBzCwWtI8XM7H3w0gVWDf7Q6Hw/7tx37bBSffamBa8zINl+Pv/57O4zgtxZjDZv/j54/94cePXR8c8xeFZLgCpHk4n46/T5dlnlMmkbg9/PHr52F/2O030Us7RfzMwBAMtORlGq7n0/E05JxKRXJxc/j569dht+s3MTgm+hh/S8EQzGpOyzRczpfLoLVWQ/Ld9vD2x6/DdhOjc/Iig7sGZqa15DRP43AdJjMzIHGh3+4Pb4dN7x2zfA5/FFFVa8lpWZZlKQAAyM7H2G82m80mOkF6aX8PDExrrbXknAsAILEPMXZd1/ddDMxfWKM8xKuqVl1tiJ2Lseu6GEMI3r38OsCtlK1BmGlzDwB2PvZ918UYvBP5Mv7WC2amptq8hwhIXOj6vu9i8E746/inU1gHARIbiQ9d18UQnPBXfvqsgaqqKgBgAxAfQwzeyWvxP2pgZk3AlgMzsTjvnDAhgL2af88agKpqvR8BALZRgKvVfw2xMlDT2sbR+96VVq216tcUbiloLbnkUm57zVS11lpyyblU/X8AWmspOedSGwNb/0lt5fp9CgamrYbvAFprzimlZZnnecnlSwpyT6GWe7wZWC0lpTTP0zRFJ0T8RS3cAN7lspaT1pzzsszTOAYmRArfAcCtl9Y7SXOHktIyjePVIZqZhm/b+eNSVS0lzfN4daSac4rBMyIgIgLC3dpWgFY3RM01bXWHnObBCdRl7vouBC/EzHKbkg1B3uPb9LshqNaSFxGCMvfnGKP33jnnffDOCTMTAT4AELOIMDMhmJmiqZY8I2qZrzE478V7H2K3aS3uTIAfUyAWca15cJW01gygeRm8d8zC4kO/2e33222pCohodwaIRCzOORFu7QdmqhVMS5qcCCEiiYvb/duYcq0KSGTwqAHXFWFtQTTTaloSMxGBGQD7uH+bshkAEDHzIwApi8hNBQRs7YSACIhgVdXYdWNSFGZiEVdbydzqgIhWBCIiqgBgCgZga6cYgCzGLnTBi3M+qD6l8HAMjYGZot1Wa8Yy+3G49sGxOB+CPjJAJBYRlpZBm5V6B1ibuU1f79j52JUnBneEGwUweIpdC7yk6eocu9D35YEBGiCBCa850O2M2hXXAGsTCjQvoxP2/TY9MQAEaCKsSqrdr+iAYIiqAOTYappEXDfN+TkFQzQQcc55570voECt5wARQE3NgIXQSpp9nOaUn48RoVFwPsQuJiNVBNCVA6CRAZAwgtaSUy715v/vfoBIIj52m+1kLLWaQm2zsvU3AN0mxaO4j4ZC7ONm/7aYH6aUa9GiZgZoLUkgEXHOOe8fBuYTgPhuNy0m/XWc55RyrtW0HWhj6HwIXd93XfTuBQCy73ZJOWyv12GcpiXlsj7fAACJnQ9d7Lfbbd8FJ4SfAVyXlVy3vZwv16swE5eq2pqCSJwPXb/Z7XbbLrr10vAEQC5WYB/7LnihVqBYwcAQkNYz2my3203fNnwAAJZgJN5Hx2i1FFUDA1Awa93mQuj6ftN30Qvj5xRQDIgdC2hOyxxKrqqmzceJmEV8CDE+XjyeGJAJIBFAmbvg3c0cDMFW12/O6Zzw7eLyPFjIBAG0hHs4NktqciBRc/+b/X8CQEIwEyfC7eu3eFhnEiHR7e30igEgECvfH1htjLURhO+PcHp4Rf8PR5cuNP3WKXIAAAAASUVORK5CYII=","data:image/png;base64,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","data:image/png;base64,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","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAGDUlEQVR4nJWX2XIjNwxFAZC9aLOT///GZDKTFknseWjZGUuyM2Gp1CWpeHQJkOAFJvw7dPv25x9/fPtr267bYIuynC+vv72+ni8vL5fTaYEngx6+yZ9HRESER0REPpl+D0h4m/b2dDczN/PI54Q7BQlv/3r7bzdVEVUzj3gGqHef91nu7uGRpsLcpzIvouZB+F+AjAy3fagFAlEpiFDqsop5gUfCB0BC3jSLqJqDR2SYW5ZlPTyXcK8gTFWEWZjNQVVNWCTLcjiJTY85ewC4qTAzs4h6AlWeWx9R1uOZzemRcAdINxUeg5nZLAJxqlPjrMfzC6vXRABI/AyQuwIRYRE1c8sopXTB9fLaWayWhyA8xMDNVFX
gitextract_a48sspai/
├── .gitattributes
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── inspectionProfiles/
│ │ └── profiles_settings.xml
│ └── umap-nan.iml
├── .pep8speaks.yml
├── .readthedocs.yaml
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── Makefile
├── README.rst
├── appveyor.yml
├── azure-pipelines.yml
├── ci_scripts/
│ ├── install.sh
│ ├── success.sh
│ └── test.sh
├── doc/
│ ├── .gitignore
│ ├── Makefile
│ ├── _static/
│ │ └── .gitkeep
│ ├── aligned_umap_basic_usage.rst
│ ├── aligned_umap_plotly_plot.html
│ ├── aligned_umap_politics_demo.rst
│ ├── api.rst
│ ├── basic_usage.rst
│ ├── basic_usage_bokeh_example.html
│ ├── benchmarking.rst
│ ├── bokeh_digits_plot.py
│ ├── clustering.rst
│ ├── composing_models.rst
│ ├── conf.py
│ ├── densmap_demo.rst
│ ├── doc_requirements.txt
│ ├── document_embedding.rst
│ ├── embedding_space.rst
│ ├── exploratory_analysis.rst
│ ├── faq.rst
│ ├── how_umap_works.rst
│ ├── index.rst
│ ├── interactive_viz.rst
│ ├── inverse_transform.rst
│ ├── make.bat
│ ├── mutual_nn_umap.rst
│ ├── outliers.rst
│ ├── parameters.rst
│ ├── parametric_umap.rst
│ ├── performance.rst
│ ├── plotting.rst
│ ├── plotting_example_interactive.py
│ ├── plotting_interactive_example.html
│ ├── precomputed_k-nn.rst
│ ├── release_notes.rst
│ ├── reproducibility.rst
│ ├── scientific_papers.rst
│ ├── sparse.rst
│ ├── supervised.rst
│ ├── transform.rst
│ └── transform_landmarked_pumap.rst
├── docs_requirements.txt
├── examples/
│ ├── README.txt
│ ├── digits/
│ │ ├── digits.html
│ │ └── digits.py
│ ├── galaxy10sdss.py
│ ├── inverse_transform_example.py
│ ├── iris/
│ │ ├── iris.html
│ │ └── iris.py
│ ├── mnist_torus_sphere_example.py
│ ├── mnist_transform_new_data.py
│ ├── plot_algorithm_comparison.py
│ ├── plot_fashion-mnist_example.py
│ ├── plot_feature_extraction_classification.py
│ └── plot_mnist_example.py
├── notebooks/
│ ├── AnimatingUMAP.ipynb
│ ├── Document embedding using UMAP.ipynb
│ ├── MNIST_Landmarks.ipynb
│ ├── Parametric_UMAP/
│ │ ├── 01.0-parametric-umap-mnist-embedding-basic.ipynb
│ │ ├── 02.0-parametric-umap-mnist-embedding-convnet.ipynb
│ │ ├── 03.0-parametric-umap-mnist-embedding-convnet-with-reconstruction.ipynb
│ │ ├── 04.0-parametric-umap-mnist-embedding-convnet-with-autoencoder-loss.ipynb
│ │ ├── 05.0-parametric-umap-with-callback.ipynb
│ │ ├── 06.0-nonparametric-umap.ipynb
│ │ └── 07.0-parametric-umap-global-loss.ipynb
│ └── UMAP usage and parameters.ipynb
├── paper.bib
├── paper.md
├── pyproject.toml
├── setup.py
└── umap/
├── __init__.py
├── aligned_umap.py
├── distances.py
├── layouts.py
├── parametric_umap.py
├── plot.py
├── sparse.py
├── spectral.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── digits_embedding_42.npy
│ ├── test_aligned_umap.py
│ ├── test_chunked_parallel_spatial_metric.py
│ ├── test_composite_models.py
│ ├── test_data_input.py
│ ├── test_densmap.py
│ ├── test_parametric_umap.py
│ ├── test_plot.py
│ ├── test_spectral.py
│ ├── test_umap.py
│ ├── test_umap_get_feature_names_out.py
│ ├── test_umap_grads.py
│ ├── test_umap_metrics.py
│ ├── test_umap_nn.py
│ ├── test_umap_on_iris.py
│ ├── test_umap_ops.py
│ ├── test_umap_repeated_data.py
│ ├── test_umap_trustworthiness.py
│ └── test_umap_validation_params.py
├── umap_.py
├── utils.py
└── validation.py
SYMBOL INDEX (471 symbols across 32 files)
FILE: doc/bokeh_digits_plot.py
function embeddable_image (line 17) | def embeddable_image(data):
FILE: doc/conf.py
function setup (line 230) | def setup(app):
FILE: examples/mnist_torus_sphere_example.py
function torus_euclidean_grad (line 42) | def torus_euclidean_grad(x, y, torus_dimensions=(2 * np.pi, 2 * np.pi)):
FILE: umap/__init__.py
class ParametricUMAP (line 15) | class ParametricUMAP(object):
method __init__ (line 16) | def __init__(self, **kwds):
FILE: umap/aligned_umap.py
function in1d (line 17) | def in1d(arr, test_set):
function invert_dict (line 29) | def invert_dict(d):
function procrustes_align (line 34) | def procrustes_align(embedding_base, embedding_to_align, anchors):
function expand_relations (line 43) | def expand_relations(relation_dicts, window_size=3):
function build_neighborhood_similarities (line 86) | def build_neighborhood_similarities(graphs_indptr, graphs_indices, relat...
function get_nth_item_or_val (line 132) | def get_nth_item_or_val(iterable_or_val, n):
function set_aligned_params (line 167) | def set_aligned_params(new_params, existing_params, n_models, param_name...
function init_from_existing_internal (line 188) | def init_from_existing_internal(
function init_from_existing (line 215) | def init_from_existing(previous_embedding, graph, relations):
class AlignedUMAP (line 228) | class AlignedUMAP(BaseEstimator):
method __init__ (line 229) | def __init__(
method fit (line 295) | def fit(self, X, y=None, **fit_params):
method fit_transform (line 442) | def fit_transform(self, X, y=None, **fit_params):
method update (line 446) | def update(self, X, y=None, **fit_params):
FILE: umap/distances.py
function sign (line 15) | def sign(a):
function softmax (line 23) | def softmax(z):
function euclidean (line 49) | def euclidean(x, y):
function euclidean_grad (line 62) | def euclidean_grad(x, y):
function standardised_euclidean (line 78) | def standardised_euclidean(x, y, sigma=_mock_ones):
function standardised_euclidean_grad (line 93) | def standardised_euclidean_grad(x, y, sigma=_mock_ones):
function manhattan (line 109) | def manhattan(x, y):
function manhattan_grad (line 123) | def manhattan_grad(x, y):
function chebyshev (line 138) | def chebyshev(x, y):
function chebyshev_grad (line 152) | def chebyshev_grad(x, y):
function minkowski (line 172) | def minkowski(x, y, p=2):
function minkowski_grad (line 191) | def minkowski_grad(x, y, p=2.0):
function poincare (line 220) | def poincare(u, v):
function hyperboloid_grad (line 234) | def hyperboloid_grad(x, y):
function weighted_minkowski (line 257) | def weighted_minkowski(x, y, w=_mock_ones, p=2):
function weighted_minkowski_grad (line 275) | def weighted_minkowski_grad(x, y, w=_mock_ones, p=2.0):
function mahalanobis (line 305) | def mahalanobis(x, y, vinv=_mock_identity):
function mahalanobis_f64 (line 323) | def mahalanobis_f64(x, y, vinv=_mock_identity):
function mahalanobis_grad (line 342) | def mahalanobis_grad(x, y, vinv=_mock_identity):
function hamming (line 363) | def hamming(x, y):
function canberra (line 373) | def canberra(x, y):
function canberra_grad (line 384) | def canberra_grad(x, y):
function bray_curtis (line 400) | def bray_curtis(x, y):
function bray_curtis_grad (line 414) | def bray_curtis_grad(x, y):
function jaccard (line 432) | def jaccard(x, y):
function matching (line 448) | def matching(x, y):
function dice (line 459) | def dice(x, y):
function kulsinski (line 475) | def kulsinski(x, y):
function rogers_tanimoto (line 493) | def rogers_tanimoto(x, y):
function russellrao (line 504) | def russellrao(x, y):
function sokal_michener (line 518) | def sokal_michener(x, y):
function sokal_sneath (line 529) | def sokal_sneath(x, y):
function haversine (line 545) | def haversine(x, y):
function haversine_grad (line 555) | def haversine_grad(x, y):
function yule (line 585) | def yule(x, y):
function cosine (line 607) | def cosine(x, y):
function cosine_grad (line 625) | def cosine_grad(x, y):
function correlation (line 657) | def correlation(x, y):
function hellinger (line 687) | def hellinger(x, y):
function hellinger_grad (line 706) | def hellinger_grad(x, y):
function softmax_hellinger (line 748) | def softmax_hellinger(x, y):
function softmax_hellinger_grad (line 759) | def softmax_hellinger_grad(x, y):
function approx_log_Gamma (line 780) | def approx_log_Gamma(x):
function log_beta (line 792) | def log_beta(x, y):
function log_single_beta (line 805) | def log_single_beta(x):
function ll_dirichlet (line 818) | def ll_dirichlet(data1, data2):
function symmetric_kl (line 853) | def symmetric_kl(x, y, z=1e-11): # pragma: no cover
function symmetric_kl_grad (line 884) | def symmetric_kl_grad(x, y, z=1e-11): # pragma: no cover
function correlation_grad (line 916) | def correlation_grad(x, y):
function sinkhorn_distance (line 967) | def sinkhorn_distance(
function spherical_gaussian_energy_grad (line 993) | def spherical_gaussian_energy_grad(x, y): # pragma: no cover
function diagonal_gaussian_energy_grad (line 1011) | def diagonal_gaussian_energy_grad(x, y): # pragma: no cover
function gaussian_energy_grad (line 1046) | def gaussian_energy_grad(x, y): # pragma: no cover
function spherical_gaussian_grad (line 1119) | def spherical_gaussian_grad(x, y): # pragma: no cover
function get_discrete_params (line 1146) | def get_discrete_params(data, metric):
function categorical_distance (line 1170) | def categorical_distance(x, y):
function hierarchical_categorical_distance (line 1178) | def hierarchical_categorical_distance(x, y, cat_hierarchy=[{}]):
function ordinal_distance (line 1188) | def ordinal_distance(x, y, support_size=1.0):
function count_distance (line 1193) | def count_distance(x, y, poisson_lambda=1.0, normalisation=1.0):
function levenshtein (line 1218) | def levenshtein(x, y, normalisation=1.0, max_distance=20):
function levenshtein_myers_ascii (line 1270) | def levenshtein_myers_ascii(x, y, normalisation=1.0, max_distance=20):
function parallel_special_metric (line 1452) | def parallel_special_metric(X, Y=None, metric=hellinger):
function chunked_parallel_special_metric (line 1473) | def chunked_parallel_special_metric(X, Y=None, metric=hellinger, chunk_s...
function pairwise_special_metric (line 1495) | def pairwise_special_metric(
FILE: umap/layouts.py
function clip (line 10) | def clip(val):
function rdist (line 42) | def rdist(x, y):
function _optimize_layout_euclidean_single_epoch (line 63) | def _optimize_layout_euclidean_single_epoch(
function _optimize_layout_euclidean_densmap_epoch_init (line 189) | def _optimize_layout_euclidean_densmap_epoch_init(
function _get_optimize_layout_euclidean_single_epoch_fn (line 231) | def _get_optimize_layout_euclidean_single_epoch_fn(parallel: bool = False):
function optimize_layout_euclidean (line 238) | def optimize_layout_euclidean(
function _optimize_layout_generic_single_epoch (line 446) | def _optimize_layout_generic_single_epoch(
function optimize_layout_generic (line 528) | def optimize_layout_generic(
function _optimize_layout_inverse_single_epoch (line 663) | def _optimize_layout_inverse_single_epoch(
function optimize_layout_inverse (line 735) | def optimize_layout_inverse(
function _optimize_layout_aligned_euclidean_single_epoch (line 877) | def _optimize_layout_aligned_euclidean_single_epoch(
function optimize_layout_aligned_euclidean (line 1028) | def optimize_layout_aligned_euclidean(
FILE: umap/parametric_umap.py
class ParametricUMAP (line 45) | class ParametricUMAP(UMAP):
method __init__ (line 46) | def __init__(
method fit (line 149) | def fit(self, X, y=None, precomputed_distances=None, landmark_position...
method fit_transform (line 206) | def fit_transform(
method transform (line 268) | def transform(self, X, batch_size=None):
method inverse_transform (line 290) | def inverse_transform(self, X):
method _define_model (line 310) | def _define_model(self):
method _fit_embed_data (line 329) | def _fit_embed_data(self, X, n_epochs, init, random_state, landmark_po...
method __getstate__ (line 445) | def __getstate__(self):
method save (line 454) | def save(self, save_location, verbose=True, exclude_raw_data=False):
method add_landmarks (line 508) | def add_landmarks(
method remove_landmarks (line 568) | def remove_landmarks(self):
method to_ONNX (line 571) | def to_ONNX(self, save_location):
function get_graph_elements (line 585) | def get_graph_elements(graph_, n_epochs):
function init_embedding_from_graph (line 639) | def init_embedding_from_graph(
function convert_distance_to_log_probability (line 697) | def convert_distance_to_log_probability(distances, a=1.0, b=1.0):
function compute_cross_entropy (line 719) | def compute_cross_entropy(
function prepare_networks (line 762) | def prepare_networks(
function construct_edge_dataset (line 830) | def construct_edge_dataset(
function should_pickle (line 937) | def should_pickle(key, val):
function load_ParametricUMAP (line 977) | def load_ParametricUMAP(save_location, verbose=True):
function covariance (line 1024) | def covariance(x, y=None, keepdims=False):
function correlation (line 1083) | def correlation(x, y=None, keepdims=False):
class StopGradient (line 1090) | class StopGradient(keras.layers.Layer):
method call (line 1091) | def call(self, x):
method get_config (line 1094) | def get_config(self):
function _default_landmark_loss (line 1098) | def _default_landmark_loss(y, y_pred):
class UMAPModel (line 1107) | class UMAPModel(keras.Model):
method __init__ (line 1108) | def __init__(
method call (line 1158) | def call(self, inputs):
method compute_loss (line 1177) | def compute_loss(self, x=None, y=None, y_pred=None, sample_weight=None...
method _umap_loss (line 1200) | def _umap_loss(self, y_pred, repulsion_strength=1.0):
method _global_correlation_loss (line 1252) | def _global_correlation_loss(self, y, y_pred):
method _parametric_reconstruction_loss (line 1280) | def _parametric_reconstruction_loss(self, y, y_pred):
method _landmark_loss (line 1286) | def _landmark_loss(self, y, y_pred):
class PumapNet (line 1314) | class PumapNet(nn.Module):
method __init__ (line 1316) | def __init__(self, indim, outdim):
method forward (line 1336) | def forward(self, x):
function weight_copier (line 1351) | def weight_copier(km, pm):
FILE: umap/plot.py
function _to_hex (line 73) | def _to_hex(arr):
function _red (line 78) | def _red(x):
function _green (line 83) | def _green(x):
function _blue (line 88) | def _blue(x):
function _get_embedding (line 152) | def _get_embedding(umap_object):
function _get_metric (line 161) | def _get_metric(umap_object):
function _get_metric_kwds (line 169) | def _get_metric_kwds(umap_object):
function _embed_datashader_in_an_axis (line 177) | def _embed_datashader_in_an_axis(datashader_image, ax):
function _nhood_search (line 184) | def _nhood_search(umap_object, nhood_size):
function _nhood_compare (line 204) | def _nhood_compare(indices_left, indices_right):
function _get_extent (line 219) | def _get_extent(points):
function _select_font_color (line 236) | def _select_font_color(background):
function _datashade_points (line 254) | def _datashade_points(
function _matplotlib_points (line 364) | def _matplotlib_points(
function show (line 458) | def show(plot_to_show):
function points (line 482) | def points(
function connectivity (line 727) | def connectivity(
function diagnostic (line 952) | def diagnostic(
function interactive (line 1274) | def interactive(
function nearest_neighbour_distribution (line 1651) | def nearest_neighbour_distribution(umap_object, bins=25, ax=None):
FILE: umap/sparse.py
function arr_unique (line 19) | def arr_unique(arr):
function arr_union (line 27) | def arr_union(ar1, ar2):
function arr_intersect (line 39) | def arr_intersect(ar1, ar2):
function sparse_sum (line 46) | def sparse_sum(ind1, data1, ind2, data2):
function sparse_diff (line 107) | def sparse_diff(ind1, data1, ind2, data2):
function sparse_mul (line 112) | def sparse_mul(ind1, data1, ind2, data2):
function general_sset_intersection (line 146) | def general_sset_intersection(
function general_sset_union (line 201) | def general_sset_union(
function sparse_euclidean (line 235) | def sparse_euclidean(ind1, data1, ind2, data2):
function sparse_manhattan (line 244) | def sparse_manhattan(ind1, data1, ind2, data2):
function sparse_chebyshev (line 253) | def sparse_chebyshev(ind1, data1, ind2, data2):
function sparse_minkowski (line 262) | def sparse_minkowski(ind1, data1, ind2, data2, p=2.0):
function sparse_hamming (line 271) | def sparse_hamming(ind1, data1, ind2, data2, n_features):
function sparse_canberra (line 277) | def sparse_canberra(ind1, data1, ind2, data2):
function sparse_bray_curtis (line 291) | def sparse_bray_curtis(ind1, data1, ind2, data2): # pragma: no cover
function sparse_jaccard (line 312) | def sparse_jaccard(ind1, data1, ind2, data2):
function sparse_matching (line 323) | def sparse_matching(ind1, data1, ind2, data2, n_features):
function sparse_dice (line 332) | def sparse_dice(ind1, data1, ind2, data2):
function sparse_kulsinski (line 344) | def sparse_kulsinski(ind1, data1, ind2, data2, n_features):
function sparse_rogers_tanimoto (line 358) | def sparse_rogers_tanimoto(ind1, data1, ind2, data2, n_features):
function sparse_russellrao (line 367) | def sparse_russellrao(ind1, data1, ind2, data2, n_features):
function sparse_sokal_michener (line 380) | def sparse_sokal_michener(ind1, data1, ind2, data2, n_features):
function sparse_sokal_sneath (line 389) | def sparse_sokal_sneath(ind1, data1, ind2, data2):
function sparse_cosine (line 401) | def sparse_cosine(ind1, data1, ind2, data2):
function sparse_hellinger (line 419) | def sparse_hellinger(ind1, data1, ind2, data2):
function sparse_correlation (line 440) | def sparse_correlation(ind1, data1, ind2, data2, n_features):
function approx_log_Gamma (line 501) | def approx_log_Gamma(x):
function log_beta (line 515) | def log_beta(x, y):
function log_single_beta (line 528) | def log_single_beta(x):
function sparse_ll_dirichlet (line 539) | def sparse_ll_dirichlet(ind1, data1, ind2, data2):
FILE: umap/spectral.py
function component_layout (line 18) | def component_layout(
function multi_component_layout (line 145) | def multi_component_layout(
function spectral_layout (line 263) | def spectral_layout(
function tswspectral_layout (line 317) | def tswspectral_layout(
function _spectral_layout (line 395) | def _spectral_layout(
FILE: umap/tests/conftest.py
function spatial_data (line 19) | def spatial_data():
function binary_data (line 27) | def binary_data():
function sparse_spatial_data (line 37) | def sparse_spatial_data(spatial_data, binary_data):
function sparse_binary_data (line 42) | def sparse_binary_data(binary_data):
function nn_data (line 49) | def nn_data():
function binary_nn_data (line 58) | def binary_nn_data():
function sparse_nn_data (line 69) | def sparse_nn_data():
function repetition_dense (line 78) | def repetition_dense():
function spatial_repeats (line 96) | def spatial_repeats(spatial_data):
function binary_repeats (line 107) | def binary_repeats(binary_data):
function sparse_spatial_data_repeats (line 120) | def sparse_spatial_data_repeats(spatial_repeats, binary_repeats):
function sparse_binary_data_repeats (line 125) | def sparse_binary_data_repeats(binary_repeats):
function sparse_test_data (line 130) | def sparse_test_data(nn_data, binary_nn_data):
function iris (line 135) | def iris():
function iris_selection (line 140) | def iris_selection():
function aligned_iris (line 145) | def aligned_iris(iris):
function aligned_iris_relations (line 152) | def aligned_iris_relations():
function iris_model (line 157) | def iris_model(iris):
function iris_model_large (line 162) | def iris_model_large(iris):
function iris_subset_model (line 172) | def iris_subset_model(iris, iris_selection):
function iris_subset_model_large (line 179) | def iris_subset_model_large(iris, iris_selection):
function supervised_iris_model (line 189) | def supervised_iris_model(iris):
function aligned_iris_model (line 196) | def aligned_iris_model(aligned_iris, aligned_iris_relations):
function spatial_distances (line 206) | def spatial_distances():
function binary_distances (line 221) | def binary_distances():
FILE: umap/tests/test_aligned_umap.py
function nn_accuracy (line 13) | def nn_accuracy(true_nn, embd_nn):
function test_neighbor_local_neighbor_accuracy (line 20) | def test_neighbor_local_neighbor_accuracy(aligned_iris, aligned_iris_mod...
function test_local_clustering (line 30) | def test_local_clustering(aligned_iris, aligned_iris_model):
function test_aligned_update (line 44) | def test_aligned_update(aligned_iris, aligned_iris_relations):
function test_aligned_update_params (line 57) | def test_aligned_update_params(aligned_iris, aligned_iris_relations):
function test_aligned_update_array_error (line 74) | def test_aligned_update_array_error(aligned_iris, aligned_iris_relations):
FILE: umap/tests/test_chunked_parallel_spatial_metric.py
function stashed_previous_impl_for_regression_test (line 22) | def stashed_previous_impl_for_regression_test():
function workaround_590_impl (line 72) | def workaround_590_impl():
function benchmark_data (line 121) | def benchmark_data(request):
function test_chunked_parallel_alternative_implementations (line 131) | def test_chunked_parallel_alternative_implementations(
function test_chunked_parallel_special_metric_implementation_hellinger (line 159) | def test_chunked_parallel_special_metric_implementation_hellinger(
function test_benchmark_chunked_parallel_special_metric_x_only (line 242) | def test_benchmark_chunked_parallel_special_metric_x_only(
function test_benchmark_workaround_590_x_only (line 266) | def test_benchmark_workaround_590_x_only(
function test_benchmark_chunked_parallel_special_metric_x_y (line 296) | def test_benchmark_chunked_parallel_special_metric_x_y(
function test_benchmark_workaround_590_x_y (line 320) | def test_benchmark_workaround_590_x_y(
FILE: umap/tests/test_composite_models.py
function test_composite_trustworthiness (line 15) | def test_composite_trustworthiness(nn_data, iris_model):
function test_composite_trustworthiness_random_init (line 47) | def test_composite_trustworthiness_random_init(nn_data): # pragma: no c...
function test_composite_trustworthiness_on_iris (line 75) | def test_composite_trustworthiness_on_iris(iris):
function test_contrastive_trustworthiness_on_iris (line 100) | def test_contrastive_trustworthiness_on_iris(iris):
FILE: umap/tests/test_data_input.py
function all_finite_data (line 8) | def all_finite_data():
function inverse_data (line 13) | def inverse_data():
function nan_dist (line 18) | def nan_dist(a: np.ndarray, b: np.ndarray):
function test_check_input_data (line 24) | def test_check_input_data(all_finite_data, inverse_data):
FILE: umap/tests/test_densmap.py
function test_densmap_trustworthiness (line 15) | def test_densmap_trustworthiness(nn_data):
function test_densmap_trustworthiness_random_init (line 32) | def test_densmap_trustworthiness_random_init(nn_data): # pragma: no cover
function test_densmap_trustworthiness_on_iris (line 47) | def test_densmap_trustworthiness_on_iris(iris):
function test_densmap_trustworthiness_on_iris_supervised (line 68) | def test_densmap_trustworthiness_on_iris_supervised(iris):
FILE: umap/tests/test_parametric_umap.py
function moon_dataset (line 25) | def moon_dataset():
function test_create_model (line 31) | def test_create_model(moon_dataset):
function test_global_loss (line 41) | def test_global_loss(moon_dataset):
function test_inverse_transform (line 51) | def test_inverse_transform(moon_dataset):
function test_custom_encoder_decoder (line 67) | def test_custom_encoder_decoder(moon_dataset):
function test_validation (line 109) | def test_validation(moon_dataset):
function test_landmark_retraining_no_nan (line 149) | def test_landmark_retraining_no_nan():
FILE: umap/tests/test_plot.py
function mapper (line 20) | def mapper(iris):
function test_plot_runs_at_all (line 28) | def test_plot_runs_at_all(mapper, iris, iris_selection):
FILE: umap/tests/test_spectral.py
function test_tsw_spectral_init (line 19) | def test_tsw_spectral_init(iris):
function test_ensure_fallback_to_random_on_spectral_failure (line 42) | def test_ensure_fallback_to_random_on_spectral_failure():
FILE: umap/tests/test_umap_get_feature_names_out.py
function test_get_feature_names_out (line 8) | def test_get_feature_names_out():
function test_get_feature_names_out_default (line 24) | def test_get_feature_names_out_default():
function test_get_feature_names_out_multicomponent (line 39) | def test_get_feature_names_out_multicomponent():
function test_get_feature_names_out_featureunion (line 56) | def test_get_feature_names_out_featureunion():
FILE: umap/tests/test_umap_grads.py
function numerical_gradient (line 7) | def numerical_gradient(f, x, eps=1e-6, forward_only=False):
function numerical_grad_x (line 42) | def numerical_grad_x(dist, x, y, eps=1e-6, dist_kwargs=None, forward_onl...
function sample_normal_pairs (line 49) | def sample_normal_pairs(n, d, rng=None):
function sample_dirichlet_pairs (line 58) | def sample_dirichlet_pairs(n, d, alpha=1.0, rng=None):
function sample_abundance_pairs (line 67) | def sample_abundance_pairs(n, d, shape=2.0, scale=1.0, rng=None):
function assert_gradient_matches_finite_diff (line 76) | def assert_gradient_matches_finite_diff(
function test_euclidean_gradient (line 130) | def test_euclidean_gradient(
function test_minkowski_gradient (line 143) | def test_minkowski_gradient(dim, p):
function test_weighted_minkowski_gradient (line 156) | def test_weighted_minkowski_gradient(dim, p):
function test_cosine_gradient (line 169) | def test_cosine_gradient(
function test_manhattan_gradient (line 181) | def test_manhattan_gradient(
function test_chebyshev_gradient (line 195) | def test_chebyshev_gradient(
function test_correlation_gradient (line 207) | def test_correlation_gradient(
function test_braycurtis_gradient (line 219) | def test_braycurtis_gradient(
function test_hellinger_gradient (line 231) | def test_hellinger_gradient(dim):
function test_standardised_euclidean_gradient (line 242) | def test_standardised_euclidean_gradient(dim):
function test_mahalanobis_gradient (line 255) | def test_mahalanobis_gradient(dim):
function test_softmax_hellinger_gradient (line 270) | def test_softmax_hellinger_gradient(
FILE: umap/tests/test_umap_metrics.py
function run_test_metric (line 31) | def run_test_metric(metric, test_data, dist_matrix, with_grad=False):
function spatial_check (line 54) | def spatial_check(metric, spatial_data, spatial_distances, with_grad=Fal...
function binary_check (line 71) | def binary_check(metric, binary_data, binary_distances):
function run_test_sparse_metric (line 88) | def run_test_sparse_metric(metric, sparse_test_data, dist_matrix):
function sparse_spatial_check (line 129) | def sparse_spatial_check(metric, sparse_spatial_data):
function sparse_binary_check (line 150) | def sparse_binary_check(metric, sparse_binary_data):
function test_euclidean (line 175) | def test_euclidean(spatial_data, spatial_distances):
function test_manhattan (line 179) | def test_manhattan(spatial_data, spatial_distances):
function test_chebyshev (line 183) | def test_chebyshev(spatial_data, spatial_distances):
function test_minkowski (line 187) | def test_minkowski(spatial_data, spatial_distances):
function test_hamming (line 191) | def test_hamming(spatial_data, spatial_distances):
function test_canberra (line 195) | def test_canberra(spatial_data, spatial_distances):
function test_braycurtis (line 199) | def test_braycurtis(spatial_data, spatial_distances):
function test_cosine (line 203) | def test_cosine(spatial_data, spatial_distances):
function test_correlation (line 207) | def test_correlation(spatial_data, spatial_distances):
function test_jaccard (line 216) | def test_jaccard(binary_data, binary_distances):
function test_matching (line 220) | def test_matching(binary_data, binary_distances):
function test_dice (line 224) | def test_dice(binary_data, binary_distances):
function test_kulsinski (line 231) | def test_kulsinski(binary_data, binary_distances):
function test_rogerstanimoto (line 235) | def test_rogerstanimoto(binary_data, binary_distances):
function test_russellrao (line 239) | def test_russellrao(binary_data, binary_distances):
function test_sokalmichener (line 244) | def test_sokalmichener(binary_data, binary_distances):
function test_sokalsneath (line 248) | def test_sokalsneath(binary_data, binary_distances):
function test_yule (line 252) | def test_yule(binary_data, binary_distances):
function test_sparse_euclidean (line 261) | def test_sparse_euclidean(sparse_spatial_data):
function test_sparse_manhattan (line 265) | def test_sparse_manhattan(sparse_spatial_data):
function test_sparse_chebyshev (line 269) | def test_sparse_chebyshev(sparse_spatial_data):
function test_sparse_minkowski (line 273) | def test_sparse_minkowski(sparse_spatial_data):
function test_sparse_hamming (line 277) | def test_sparse_hamming(sparse_spatial_data):
function test_sparse_canberra (line 281) | def test_sparse_canberra(sparse_spatial_data):
function test_sparse_cosine (line 285) | def test_sparse_cosine(sparse_spatial_data):
function test_sparse_correlation (line 289) | def test_sparse_correlation(sparse_spatial_data):
function test_sparse_braycurtis (line 293) | def test_sparse_braycurtis(sparse_spatial_data):
function test_sparse_jaccard (line 302) | def test_sparse_jaccard(sparse_binary_data):
function test_sparse_matching (line 306) | def test_sparse_matching(sparse_binary_data):
function test_sparse_dice (line 310) | def test_sparse_dice(sparse_binary_data):
function test_sparse_kulsinski (line 317) | def test_sparse_kulsinski(sparse_binary_data):
function test_sparse_rogerstanimoto (line 321) | def test_sparse_rogerstanimoto(sparse_binary_data):
function test_sparse_russellrao (line 325) | def test_sparse_russellrao(sparse_binary_data):
function test_sparse_sokalmichener (line 330) | def test_sparse_sokalmichener(sparse_binary_data):
function test_sparse_sokalsneath (line 335) | def test_sparse_sokalsneath(sparse_binary_data):
function test_seuclidean (line 342) | def test_seuclidean(spatial_data):
function test_weighted_minkowski (line 364) | def test_weighted_minkowski(spatial_data):
function test_mahalanobis (line 383) | def test_mahalanobis(spatial_data):
function test_haversine (line 402) | def test_haversine(spatial_data):
function test_hellinger (line 422) | def test_hellinger(spatial_data):
function test_sparse_hellinger (line 449) | def test_sparse_hellinger(sparse_spatial_data):
function test_grad_metrics_match_metrics (line 495) | def test_grad_metrics_match_metrics(spatial_data, spatial_distances):
function levenshtein_fn (line 584) | def levenshtein_fn(request):
function test_core_distances (line 624) | def test_core_distances(levenshtein_fn, x, y, expected):
function test_ascii_boundaries (line 638) | def test_ascii_boundaries(levenshtein_fn, x, y, expected):
function test_length_difference_guard (line 642) | def test_length_difference_guard(levenshtein_fn):
function test_length_difference_guard_normalised (line 650) | def test_length_difference_guard_normalised(levenshtein_fn):
function test_max_dist_guard (line 658) | def test_max_dist_guard(levenshtein_fn):
function test_max_dist_guard_normalised (line 666) | def test_max_dist_guard_normalised(levenshtein_fn):
function test_fallback_path (line 674) | def test_fallback_path(levenshtein_fn):
FILE: umap/tests/test_umap_nn.py
function test_nn_bad_metric (line 21) | def test_nn_bad_metric(nn_data):
function test_nn_bad_metric_sparse_data (line 26) | def test_nn_bad_metric_sparse_data(sparse_nn_data):
function knn (line 43) | def knn(indices, nn_data): # pragma: no cover
function smooth_knn (line 52) | def smooth_knn(nn_data, local_connectivity=1.0):
function test_nn_descent_neighbor_accuracy (line 67) | def test_nn_descent_neighbor_accuracy(nn_data): # pragma: no cover
function test_nn_descent_neighbor_accuracy_low_memory (line 78) | def test_nn_descent_neighbor_accuracy_low_memory(nn_data): # pragma: no...
function test_angular_nn_descent_neighbor_accuracy (line 89) | def test_angular_nn_descent_neighbor_accuracy(nn_data): # pragma: no cover
function test_sparse_nn_descent_neighbor_accuracy (line 101) | def test_sparse_nn_descent_neighbor_accuracy(sparse_nn_data): # pragma:...
function test_sparse_nn_descent_neighbor_accuracy_low_memory (line 112) | def test_sparse_nn_descent_neighbor_accuracy_low_memory(
function test_nn_descent_neighbor_accuracy_callable_metric (line 125) | def test_nn_descent_neighbor_accuracy_callable_metric(nn_data): # pragm...
function test_sparse_angular_nn_descent_neighbor_accuracy (line 137) | def test_sparse_angular_nn_descent_neighbor_accuracy(
function test_smooth_knn_dist_l1norms (line 150) | def test_smooth_knn_dist_l1norms(nn_data):
function test_smooth_knn_dist_l1norms_w_connectivity (line 160) | def test_smooth_knn_dist_l1norms_w_connectivity(nn_data):
FILE: umap/tests/test_umap_on_iris.py
function test_umap_trustworthiness_on_iris (line 29) | def test_umap_trustworthiness_on_iris(iris, iris_model):
function test_initialized_umap_trustworthiness_on_iris (line 37) | def test_initialized_umap_trustworthiness_on_iris(iris):
function test_umap_trustworthiness_on_sphere_iris (line 52) | def test_umap_trustworthiness_on_sphere_iris(
function test_umap_transform_on_iris (line 85) | def test_umap_transform_on_iris(iris, iris_subset_model, iris_selection):
function test_umap_transform_on_iris_w_pynndescent (line 97) | def test_umap_transform_on_iris_w_pynndescent(iris, iris_selection):
function test_umap_transform_on_iris_modified_dtype (line 116) | def test_umap_transform_on_iris_modified_dtype(iris, iris_subset_model, ...
function test_umap_sparse_transform_on_iris (line 129) | def test_umap_sparse_transform_on_iris(iris, iris_selection):
function test_precomputed_transform_on_iris (line 152) | def test_precomputed_transform_on_iris(iris, iris_selection):
function test_precomputed_sparse_transform_on_iris (line 176) | def test_precomputed_sparse_transform_on_iris(iris, iris_selection):
function test_umap_clusterability_on_supervised_iris (line 200) | def test_umap_clusterability_on_supervised_iris(supervised_iris_model, i...
function test_umap_inverse_transform_on_iris (line 208) | def test_umap_inverse_transform_on_iris(iris, iris_model):
function test_precomputed_knn_on_iris (line 223) | def test_precomputed_knn_on_iris(iris, iris_selection, iris_subset_model):
FILE: umap/tests/test_umap_ops.py
function test_blobs_cluster (line 41) | def test_blobs_cluster():
function test_multi_component_layout (line 48) | def test_multi_component_layout():
function test_multi_component_layout_precomputed (line 75) | def test_multi_component_layout_precomputed():
function test_disconnected_data (line 107) | def test_disconnected_data(num_isolates, metric, force_approximation):
function test_disconnected_data_precomputed (line 145) | def test_disconnected_data_precomputed(num_isolates, sparse):
function test_bad_transform_data (line 176) | def test_bad_transform_data(nn_data):
function test_umap_transform_embedding_stability (line 184) | def test_umap_transform_embedding_stability(iris, iris_subset_model, iri...
function test_umap_update (line 224) | def test_umap_update(iris, iris_subset_model, iris_selection, iris_model):
function test_umap_update_large (line 242) | def test_umap_update_large(
function test_umap_graph_layout (line 268) | def test_umap_graph_layout():
function test_component_layout_options (line 286) | def test_component_layout_options(nn_data):
FILE: umap/tests/test_umap_repeated_data.py
function test_repeated_points_large_sparse_spatial (line 13) | def test_repeated_points_large_sparse_spatial(sparse_spatial_data_repeats):
function test_repeated_points_small_sparse_spatial (line 24) | def test_repeated_points_small_sparse_spatial(sparse_spatial_data_repeats):
function test_repeated_points_large_dense_spatial (line 33) | def test_repeated_points_large_dense_spatial(spatial_repeats):
function test_repeated_points_small_dense_spatial (line 40) | def test_repeated_points_small_dense_spatial(spatial_repeats):
function test_repeated_points_large_sparse_binary (line 53) | def test_repeated_points_large_sparse_binary(sparse_binary_data_repeats):
function test_repeated_points_small_sparse_binary (line 60) | def test_repeated_points_small_sparse_binary(sparse_binary_data_repeats):
function test_repeated_points_large_dense_binary (line 69) | def test_repeated_points_large_dense_binary(binary_repeats):
function test_repeated_points_small_dense_binary (line 76) | def test_repeated_points_small_dense_binary(binary_repeats):
function test_repeated_points_large_n (line 92) | def test_repeated_points_large_n(repetition_dense):
FILE: umap/tests/test_umap_trustworthiness.py
function test_umap_sparse_trustworthiness (line 22) | def test_umap_sparse_trustworthiness(sparse_test_data):
function test_umap_trustworthiness_fast_approx (line 30) | def test_umap_trustworthiness_fast_approx(nn_data):
function test_umap_trustworthiness_random_init (line 45) | def test_umap_trustworthiness_random_init(nn_data):
function test_supervised_umap_trustworthiness (line 56) | def test_supervised_umap_trustworthiness():
function test_semisupervised_umap_trustworthiness (line 67) | def test_semisupervised_umap_trustworthiness():
function test_metric_supervised_umap_trustworthiness (line 79) | def test_metric_supervised_umap_trustworthiness():
function test_string_metric_supervised_umap_trustworthiness (line 95) | def test_string_metric_supervised_umap_trustworthiness():
function test_discrete_metric_supervised_umap_trustworthiness (line 112) | def test_discrete_metric_supervised_umap_trustworthiness():
function test_count_metric_supervised_umap_trustworthiness (line 128) | def test_count_metric_supervised_umap_trustworthiness():
function test_sparse_precomputed_metric_umap_trustworthiness (line 145) | def test_sparse_precomputed_metric_umap_trustworthiness():
FILE: umap/tests/test_umap_validation_params.py
function test_umap_negative_op (line 18) | def test_umap_negative_op(nn_data):
function test_umap_too_large_op (line 24) | def test_umap_too_large_op(nn_data):
function test_umap_bad_too_large_min_dist (line 30) | def test_umap_bad_too_large_min_dist(nn_data):
function test_umap_negative_min_dist (line 40) | def test_umap_negative_min_dist(nn_data):
function test_umap_negative_n_components (line 46) | def test_umap_negative_n_components(nn_data):
function test_umap_non_integer_n_components (line 52) | def test_umap_non_integer_n_components(nn_data):
function test_umap_too_small_n_neighbours (line 58) | def test_umap_too_small_n_neighbours(nn_data):
function test_umap_negative_n_neighbours (line 64) | def test_umap_negative_n_neighbours(nn_data):
function test_umap_bad_metric (line 70) | def test_umap_bad_metric(nn_data):
function test_umap_negative_learning_rate (line 76) | def test_umap_negative_learning_rate(nn_data):
function test_umap_negative_repulsion (line 82) | def test_umap_negative_repulsion(nn_data):
function test_umap_negative_sample_rate (line 88) | def test_umap_negative_sample_rate(nn_data):
function test_umap_bad_init (line 94) | def test_umap_bad_init(nn_data):
function test_umap_bad_numeric_init (line 100) | def test_umap_bad_numeric_init(nn_data):
function test_umap_bad_matrix_init (line 106) | def test_umap_bad_matrix_init(nn_data):
function test_umap_negative_n_epochs (line 112) | def test_umap_negative_n_epochs(nn_data):
function test_umap_negative_target_n_neighbours (line 118) | def test_umap_negative_target_n_neighbours(nn_data):
function test_umap_bad_output_metric (line 124) | def test_umap_bad_output_metric(nn_data):
function test_haversine_on_highd (line 136) | def test_haversine_on_highd(nn_data):
function test_umap_haversine_embed_to_highd (line 142) | def test_umap_haversine_embed_to_highd(nn_data):
function test_umap_too_many_neighbors_warns (line 148) | def test_umap_too_many_neighbors_warns(nn_data):
function test_densmap_lambda (line 155) | def test_densmap_lambda(nn_data):
function test_densmap_var_shift (line 161) | def test_densmap_var_shift(nn_data):
function test_densmap_frac (line 167) | def test_densmap_frac(nn_data):
function test_umap_unique_and_precomputed (line 176) | def test_umap_unique_and_precomputed(nn_data):
function test_densmap_bad_output_metric (line 182) | def test_densmap_bad_output_metric(nn_data):
function test_umap_bad_n_components (line 188) | def test_umap_bad_n_components(nn_data):
function test_umap_bad_metrics (line 200) | def test_umap_bad_metrics(nn_data):
function test_umap_bad_n_jobs (line 219) | def test_umap_bad_n_jobs(nn_data):
function test_umap_custom_distance_w_grad (line 228) | def test_umap_custom_distance_w_grad(nn_data):
function test_umap_bad_output_metric_no_grad (line 248) | def test_umap_bad_output_metric_no_grad(nn_data):
function test_umap_bad_hellinger_data (line 258) | def test_umap_bad_hellinger_data(nn_data):
function test_umap_update_bad_params (line 264) | def test_umap_update_bad_params(nn_data):
function test_umap_fit_data_and_targets_compliant (line 277) | def test_umap_fit_data_and_targets_compliant():
function test_umap_fit_instance_returned (line 297) | def test_umap_fit_instance_returned():
function test_umap_inverse_transform_fails_expectedly (line 314) | def test_umap_inverse_transform_fails_expectedly(sparse_spatial_data, nn...
FILE: umap/umap_.py
function flatten_iter (line 71) | def flatten_iter(container):
function flattened (line 80) | def flattened(container):
function breadth_first_search (line 84) | def breadth_first_search(adjmat, start, min_vertices):
function raise_disconnected_warning (line 110) | def raise_disconnected_warning(
function smooth_knn_dist (line 152) | def smooth_knn_dist(distances, k, n_iter=64, local_connectivity=1.0, ban...
function nearest_neighbors (line 256) | def nearest_neighbors(
function compute_membership_strengths (line 361) | def compute_membership_strengths(
function fuzzy_simplicial_set (line 442) | def fuzzy_simplicial_set(
function fast_intersection (line 621) | def fast_intersection(rows, cols, values, target, unknown_dist=1.0, far_...
function fast_metric_intersection (line 664) | def fast_metric_intersection(
function reprocess_row (line 707) | def reprocess_row(probabilities, k=15, n_iters=32):
function reset_local_metrics (line 737) | def reset_local_metrics(simplicial_set_indptr, simplicial_set_data):
function reset_local_connectivity (line 749) | def reset_local_connectivity(simplicial_set, reset_local_metric=False):
function discrete_metric_simplicial_set_intersection (line 780) | def discrete_metric_simplicial_set_intersection(
function general_simplicial_set_intersection (line 858) | def general_simplicial_set_intersection(
function general_simplicial_set_union (line 886) | def general_simplicial_set_union(simplicial_set1, simplicial_set2):
function make_epochs_per_sample (line 906) | def make_epochs_per_sample(weights, n_epochs):
function noisy_scale_coords (line 930) | def noisy_scale_coords(coords, random_state, max_coord=10.0, noise=0.0001):
function simplicial_set_embedding (line 938) | def simplicial_set_embedding(
function init_transform (line 1305) | def init_transform(indices, weights, embedding):
function init_graph_transform (line 1337) | def init_graph_transform(graph, embedding):
function init_update (line 1379) | def init_update(current_init, n_original_samples, indices):
function find_ab_params (line 1393) | def find_ab_params(spread, min_dist):
class UMAP (line 1411) | class UMAP(BaseEstimator, ClassNamePrefixFeaturesOutMixin):
method __init__ (line 1665) | def __init__(
method _validate_parameters (line 1752) | def _validate_parameters(self):
method _check_custom_metric (line 2056) | def _check_custom_metric(self, metric, kwds, data=None):
method _populate_combined_params (line 2076) | def _populate_combined_params(self, *models):
method __mul__ (line 2125) | def __mul__(self, other):
method __add__ (line 2197) | def __add__(self, other):
method __sub__ (line 2267) | def __sub__(self, other):
method fit (line 2339) | def fit(self, X, y=None, ensure_all_finite=True, **kwargs):
method _fit_embed_data (line 2867) | def _fit_embed_data(self, X, n_epochs, init, random_state, **kwargs):
method fit_transform (line 2897) | def fit_transform(self, X, y=None, ensure_all_finite=True, **kwargs):
method transform (line 2950) | def transform(self, X, ensure_all_finite=True):
method inverse_transform (line 3190) | def inverse_transform(self, X):
method update (line 3357) | def update(self, X, ensure_all_finite=True):
method __repr__ (line 3588) | def __repr__(self):
FILE: umap/utils.py
function fast_knn_indices (line 15) | def fast_knn_indices(X, n_neighbors):
function tau_rand_int (line 41) | def tau_rand_int(state):
function tau_rand (line 67) | def tau_rand(state):
function norm (line 84) | def norm(vec):
function submatrix (line 102) | def submatrix(dmat, indices_col, n_neighbors):
function ts (line 130) | def ts():
function csr_unique (line 136) | def csr_unique(matrix, return_index=True, return_inverse=True, return_co...
function disconnected_vertices (line 171) | def disconnected_vertices(model):
function average_nn_distance (line 196) | def average_nn_distance(dist_matrix):
FILE: umap/validation.py
function trustworthiness_vector_bulk (line 9) | def trustworthiness_vector_bulk(
function make_trustworthiness_calculator (line 35) | def make_trustworthiness_calculator(metric): # pragma: no cover
function trustworthiness_vector (line 72) | def trustworthiness_vector(
Copy disabled (too large)
Download .json
Condensed preview — 120 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (20,790K chars).
[
{
"path": ".gitattributes",
"chars": 32,
"preview": "*.ipynb linguist-language=Python"
},
{
"path": ".gitignore",
"chars": 492,
"preview": "# virtual environment\nvenv\n\n# non-stylistic pycharm configs\n.idea/misc.xml\n.idea/modules.xml\n.idea/umap.iml\n.idea/vcs.xm"
},
{
"path": ".idea/.gitignore",
"chars": 47,
"preview": "# Default ignored files\n/shelf/\n/workspace.xml\n"
},
{
"path": ".idea/inspectionProfiles/profiles_settings.xml",
"chars": 229,
"preview": "<component name=\"InspectionProjectProfileManager\">\n <settings>\n <option name=\"PROJECT_PROFILE\" value=\"Default\" />\n "
},
{
"path": ".idea/umap-nan.iml",
"chars": 598,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n <component name=\"NewModuleRootManager"
},
{
"path": ".pep8speaks.yml",
"chars": 119,
"preview": "pycodestyle: # Same as scanner.linter value. Other option is flake8\n max-line-length: 88 # Default is 79 in PEP 8\n"
},
{
"path": ".readthedocs.yaml",
"chars": 636,
"preview": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html f"
},
{
"path": ".travis.yml",
"chars": 1046,
"preview": "language: python\n\ncache:\n apt: true\n # We use three different cache directory\n # to work around a Travis bug with mul"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3221,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 2953,
"preview": "# Contributing\n\nContributions of all kinds are welcome. In particular pull requests are appreciated. \nThe authors will e"
},
{
"path": "LICENSE.txt",
"chars": 1514,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2017, Leland McInnes\nAll rights reserved.\n\nRedistribution and use in source and bina"
},
{
"path": "Makefile",
"chars": 428,
"preview": "# make gh-pages in repo base directory to automatically build and deploy documents to github\n\ngh-pages:\n\techo \"Make gh-p"
},
{
"path": "README.rst",
"chars": 22255,
"preview": ".. -*- mode: rst -*-\n\n.. image:: doc/logo_large.png\n :width: 600\n :alt: UMAP logo\n :align: center\n\n|pypi_version|_ |p"
},
{
"path": "appveyor.yml",
"chars": 712,
"preview": "build: \"off\"\n\nenvironment:\n matrix:\n - PYTHON_VERSION: \"3.7\"\n MINICONDA: C:\\Miniconda3-x64\n - PYTHON_VERSION"
},
{
"path": "azure-pipelines.yml",
"chars": 7851,
"preview": "# Trigger a build when there is a push to the main branch or a tag starts with release-\ntrigger:\n branches:\n include"
},
{
"path": "ci_scripts/install.sh",
"chars": 2807,
"preview": "if [[ \"$DISTRIB\" == \"conda\" ]]; then\n\n # Deactivate the travis-provided virtual environment and setup a\n # conda-bas"
},
{
"path": "ci_scripts/success.sh",
"chars": 503,
"preview": "set -e\n\nif [[ \"$COVERAGE\" == \"true\" ]]; then\n# # Need to run coveralls from a git checkout, so we copy .coverage\n# "
},
{
"path": "ci_scripts/test.sh",
"chars": 303,
"preview": "set -e\n\n#if [[ \"$COVERAGE\" == \"true\" ]]; then\n# black --check $MODULE\n#fi\n\nif [[ \"$COVERAGE\" == \"true\" ]]; then\n e"
},
{
"path": "doc/.gitignore",
"chars": 69,
"preview": "venv\numap\nsetup.py\npaper.md\npaper.bib\nLICENSE.txt\nCODE_OF_CONDUCT.md\n"
},
{
"path": "doc/Makefile",
"chars": 601,
"preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS =\nSPHI"
},
{
"path": "doc/_static/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "doc/aligned_umap_basic_usage.rst",
"chars": 18889,
"preview": "How to use AlignedUMAP\n======================\n\nIt may happen that it would be beneficial to have different UMAP\nembeddin"
},
{
"path": "doc/aligned_umap_politics_demo.rst",
"chars": 33267,
"preview": "AlignedUMAP for Time Varying Data\n=================================\n\nIt is not uncommon to have datasets that can be par"
},
{
"path": "doc/api.rst",
"chars": 461,
"preview": "UMAP API Guide\n==============\n\nUMAP has only two classes, :class:`UMAP`, and :class:`ParametricUMAP`, which inherits fro"
},
{
"path": "doc/basic_usage.rst",
"chars": 20372,
"preview": "How to Use UMAP\n===============\n\nUMAP is a general purpose manifold learning and dimension reduction\nalgorithm. It is de"
},
{
"path": "doc/basic_usage_bokeh_example.html",
"chars": 4257926,
"preview": "\n\n\n\n<!DOCTYPE html>\n<html lang=\"en\">\n \n <head>\n \n <meta charset=\"utf-8\">\n <title>Bokeh Plot</title>\n "
},
{
"path": "doc/benchmarking.rst",
"chars": 8794,
"preview": "\nPerformance Comparison of Dimension Reduction Implementations\n========================================================="
},
{
"path": "doc/bokeh_digits_plot.py",
"chars": 1809,
"preview": "import numpy as np\nfrom sklearn.datasets import load_digits\nimport pandas as pd\n\ndigits = load_digits()\n\nimport umap\n\nre"
},
{
"path": "doc/clustering.rst",
"chars": 14600,
"preview": "Using UMAP for Clustering\n=========================\n\nUMAP can be used as an effective preprocessing step to boost the\npe"
},
{
"path": "doc/composing_models.rst",
"chars": 21812,
"preview": "Combining multiple UMAP models\n==============================\n\nIt is possible to combine together multiple UMAP models, "
},
{
"path": "doc/conf.py",
"chars": 7692,
"preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# umap documentation build configuration file, created by\n# sphinx-quic"
},
{
"path": "doc/densmap_demo.rst",
"chars": 12585,
"preview": "Better Preserving Local Density with DensMAP\n============================================\n\nA notable assumption in UMAP "
},
{
"path": "doc/doc_requirements.txt",
"chars": 125,
"preview": "numpy>=1.13\nscipy>=0.19\nscikit-learn>=0.19\nnumba>=0.37\nbokeh>=0.13\ndatashader>=0.6\nseaborn>=0.8\ntqdm\nsphinx-gallery\nnump"
},
{
"path": "doc/document_embedding.rst",
"chars": 10258,
"preview": "Document embedding using UMAP\n=============================\n\nThis is a tutorial of using UMAP to embed text (but this ca"
},
{
"path": "doc/embedding_space.rst",
"chars": 22786,
"preview": "Embedding to non-Euclidean spaces\n=================================\n\nBy default UMAP embeds data into Euclidean space. F"
},
{
"path": "doc/exploratory_analysis.rst",
"chars": 5392,
"preview": "Exploratory Analysis of Interesting Datasets\n============================================\n\nUMAP is a useful tool for gen"
},
{
"path": "doc/faq.rst",
"chars": 13434,
"preview": "Frequently Asked Questions\n==========================\n\nCompiled here are a set of frequently asked questions,\nalong with"
},
{
"path": "doc/how_umap_works.rst",
"chars": 29849,
"preview": ".. _how_umap_works:\n\nHow UMAP Works\n==============\n\nUMAP is an algorithm for dimension reduction based on manifold learn"
},
{
"path": "doc/index.rst",
"chars": 2654,
"preview": ".. umap documentation master file, created by\n sphinx-quickstart on Fri Jun 8 10:09:40 2018.\n You can adapt this fi"
},
{
"path": "doc/interactive_viz.rst",
"chars": 6595,
"preview": "Interactive Visualizations\n==========================\n\nUMAP has found use in a number of interesting interactive visuali"
},
{
"path": "doc/inverse_transform.rst",
"chars": 8932,
"preview": "\nInverse transforms\n==================\n\nUMAP has some support for inverse transforms -- generating a high\ndimensional da"
},
{
"path": "doc/make.bat",
"chars": 772,
"preview": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-bu"
},
{
"path": "doc/mutual_nn_umap.rst",
"chars": 7957,
"preview": "Improving the Separation Between Similar Classes Using a Mutual k-NN Graph\n============================================="
},
{
"path": "doc/outliers.rst",
"chars": 9331,
"preview": "Outlier detection using UMAP\n============================\n\nWhile an earlier tutorial looked at using `UMAP for\nclusterin"
},
{
"path": "doc/parameters.rst",
"chars": 15093,
"preview": "\nBasic UMAP Parameters\n=====================\n\nUMAP is a fairly flexible non-linear dimension reduction algorithm. It\nsee"
},
{
"path": "doc/parametric_umap.rst",
"chars": 11056,
"preview": "Parametric (neural network) Embedding\n=====================================\n\n.. role:: python(code)\n :language: python"
},
{
"path": "doc/performance.rst",
"chars": 11045,
"preview": "Performance Comparison of Dimension Reduction Implementations\n=========================================================="
},
{
"path": "doc/plotting.rst",
"chars": 17913,
"preview": "Plotting UMAP results\n=====================\n\nUMAP is often used for visualization by reducing data to 2-dimensions.\nSinc"
},
{
"path": "doc/plotting_example_interactive.py",
"chars": 763,
"preview": "import sklearn.datasets\nimport pandas as pd\nimport numpy as np\nimport umap\nimport umap.plot\n\nfmnist = sklearn.datasets.f"
},
{
"path": "doc/plotting_interactive_example.html",
"chars": 5059815,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\n <meta charset=\"utf-8\">\n <title>Bokeh Plot</title>\n\n\n <link rel=\"styl"
},
{
"path": "doc/precomputed_k-nn.rst",
"chars": 13579,
"preview": "Precomputed k-nn\n===================\n\nThe purpose of this tutorial is to explore some cases where using a\nprecomputed_kn"
},
{
"path": "doc/release_notes.rst",
"chars": 1973,
"preview": "Release Notes\n=============\n\nSome notes on new features in various releases\n\nWhat's new in 0.5\n-----------------\n\n* Para"
},
{
"path": "doc/reproducibility.rst",
"chars": 6363,
"preview": "\nUMAP Reproducibility\n====================\n\nUMAP is a stochastic algorithm -- it makes use of randomness both to\nspeed u"
},
{
"path": "doc/scientific_papers.rst",
"chars": 4753,
"preview": "Scientific Papers\n=================\n\nUMAP has been used in a wide variety of scientific publications from a diverse rang"
},
{
"path": "doc/sparse.rst",
"chars": 15536,
"preview": "\nUMAP on sparse data\n===================\n\nSometimes datasets get very large, and potentially very very high\ndimensional."
},
{
"path": "doc/supervised.rst",
"chars": 18032,
"preview": "\nUMAP for Supervised Dimension Reduction and Metric Learning\n==========================================================="
},
{
"path": "doc/transform.rst",
"chars": 9248,
"preview": "\nTransforming New Data with UMAP\n===============================\n\nUMAP is useful for generating visualisations, but if y"
},
{
"path": "doc/transform_landmarked_pumap.rst",
"chars": 11157,
"preview": "\nTransforming New Data with Parametric UMAP\n==========================================\n\nThere are many cases where one m"
},
{
"path": "docs_requirements.txt",
"chars": 77,
"preview": "sphinx>=1.8\nsphinx_gallery\nmatplotlib\npillow\nsphinx_rtd_theme\nnumpydoc\nscipy\n"
},
{
"path": "examples/README.txt",
"chars": 245,
"preview": "Gallery of Examples of UMAP usage\n---------------------------------\n\nA small gallery collection examples of UMAP usage. "
},
{
"path": "examples/digits/digits.html",
"chars": 84481,
"preview": "\n<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title>Bokeh Plot</title>\n \n<"
},
{
"path": "examples/digits/digits.py",
"chars": 768,
"preview": "from bokeh.plotting import figure, output_file, show\nfrom bokeh.models import CategoricalColorMapper, ColumnDataSource\nf"
},
{
"path": "examples/galaxy10sdss.py",
"chars": 8586,
"preview": "\"\"\"\nUMAP on the Galaxy10SDSS dataset\n---------------------------------------------------------\n\nThis is an example of us"
},
{
"path": "examples/inverse_transform_example.py",
"chars": 1334,
"preview": "#!/usr/bin/env python\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom sklearn.datasets import fetch_openml\n\nimp"
},
{
"path": "examples/iris/iris.html",
"chars": 16600,
"preview": "\n<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title>Bokeh Plot</title>\n \n<"
},
{
"path": "examples/iris/iris.py",
"chars": 841,
"preview": "from bokeh.plotting import figure, output_file, show\nfrom bokeh.models import CategoricalColorMapper, ColumnDataSource\nf"
},
{
"path": "examples/mnist_torus_sphere_example.py",
"chars": 3460,
"preview": "#!/usr/bin/env python\n\nimport matplotlib.pyplot as plt\nimport numba\nimport numpy as np\nfrom mayavi import mlab\nfrom skle"
},
{
"path": "examples/mnist_transform_new_data.py",
"chars": 1586,
"preview": "#!/usr/bin/env python\n\n\"\"\"\nUMAP on the MNIST Digits dataset\n--------------------------------\n\nA simple example demonstra"
},
{
"path": "examples/plot_algorithm_comparison.py",
"chars": 4676,
"preview": "\"\"\"\nComparison of Dimension Reduction Techniques\n--------------------------------------------\n\nA comparison of several d"
},
{
"path": "examples/plot_fashion-mnist_example.py",
"chars": 2126,
"preview": "\"\"\"\nUMAP on the Fashion MNIST Digits dataset using Datashader\n---------------------------------------------------------\n"
},
{
"path": "examples/plot_feature_extraction_classification.py",
"chars": 2438,
"preview": "\"\"\"\nUMAP as a Feature Extraction Technique for Classification\n---------------------------------------------------------\n"
},
{
"path": "examples/plot_mnist_example.py",
"chars": 1084,
"preview": "\"\"\"\nUMAP on the MNIST Digits dataset\n--------------------------------\n\nA simple example demonstrating how to use UMAP on"
},
{
"path": "notebooks/AnimatingUMAP.ipynb",
"chars": 446487,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"# Making Animations of UMAP Hyper-p"
},
{
"path": "notebooks/Document embedding using UMAP.ipynb",
"chars": 4439058,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Overview\\n\",\n \"\\n\",\n \"Thi"
},
{
"path": "notebooks/MNIST_Landmarks.ipynb",
"chars": 1570721,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"id\": \"9cedf393-2d45-44b6-b202-00544167e5fa\",\n \"metadata\": {},\n \"so"
},
{
"path": "notebooks/Parametric_UMAP/01.0-parametric-umap-mnist-embedding-basic.ipynb",
"chars": 190207,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Simple UMAP example\\n\",\n \"Th"
},
{
"path": "notebooks/Parametric_UMAP/02.0-parametric-umap-mnist-embedding-convnet.ipynb",
"chars": 87164,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Custom embedder for parametric "
},
{
"path": "notebooks/Parametric_UMAP/03.0-parametric-umap-mnist-embedding-convnet-with-reconstruction.ipynb",
"chars": 168378,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Reconstruction with a custom ne"
},
{
"path": "notebooks/Parametric_UMAP/04.0-parametric-umap-mnist-embedding-convnet-with-autoencoder-loss.ipynb",
"chars": 165412,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Autoencoder + UMAP\\n\",\n \"Thi"
},
{
"path": "notebooks/Parametric_UMAP/05.0-parametric-umap-with-callback.ipynb",
"chars": 159344,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Adding a custom callback for ke"
},
{
"path": "notebooks/Parametric_UMAP/06.0-nonparametric-umap.ipynb",
"chars": 177226,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Non-parametric embedding with U"
},
{
"path": "notebooks/Parametric_UMAP/07.0-parametric-umap-global-loss.ipynb",
"chars": 431741,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"### Custom embedder for parametric "
},
{
"path": "notebooks/UMAP usage and parameters.ipynb",
"chars": 1801259,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"metadata\": {},\n \"source\": [\n \"# Basic UMAP Usage and Parameters\\n"
},
{
"path": "paper.bib",
"chars": 690,
"preview": "@article{umap_arxiv,\n author = {{McInnes}, L. and {Healy}, J.},\n title = \"{UMAP: Uniform Manifold Approximation\n"
},
{
"path": "paper.md",
"chars": 1654,
"preview": "---\ntitle: 'UMAP: Uniform Manifold Approximation and Projection'\ntags:\n - manifold learning\n - dimension reduction\n -"
},
{
"path": "pyproject.toml",
"chars": 1717,
"preview": "[build-system]\nrequires = [\"setuptools>=61.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"umap-"
},
{
"path": "setup.py",
"chars": 187,
"preview": "from setuptools import setup\n\n# All package metadata is now defined in pyproject.toml as per PEP 621.\n# This setup() cal"
},
{
"path": "umap/__init__.py",
"chars": 1210,
"preview": "from warnings import warn, catch_warnings, simplefilter\nfrom .umap_ import UMAP\n\ntry:\n with catch_warnings():\n "
},
{
"path": "umap/aligned_umap.py",
"chars": 20553,
"preview": "import numpy as np\nimport numba\nfrom sklearn.base import BaseEstimator\nfrom sklearn.utils import check_array\n\nfrom umap."
},
{
"path": "umap/distances.py",
"chars": 39486,
"preview": "# Author: Leland McInnes <leland.mcinnes@gmail.com>\n#\n# License: BSD 3 clause\nimport numba\nimport numpy as np\nimport sci"
},
{
"path": "umap/layouts.py",
"chars": 34506,
"preview": "import numba\nimport numpy as np\nfrom tqdm.auto import tqdm\n\nimport umap.distances as dist\nfrom umap.utils import tau_ran"
},
{
"path": "umap/parametric_umap.py",
"chars": 49360,
"preview": "import numpy as np\nfrom umap import UMAP\nfrom warnings import warn, catch_warnings, filterwarnings\nfrom numba import Typ"
},
{
"path": "umap/plot.py",
"chars": 58381,
"preview": "import numpy as np\nimport numba\nfrom warnings import warn\n\ntry:\n import pandas as pd\n import datashader as ds\n "
},
{
"path": "umap/sparse.py",
"chars": 16708,
"preview": "# Author: Leland McInnes <leland.mcinnes@gmail.com>\n# Enough simple sparse operations in numba to enable sparse UMAP\n#\n#"
},
{
"path": "umap/spectral.py",
"chars": 19766,
"preview": "import warnings\n\nfrom warnings import warn\n\nimport numpy as np\n\nimport scipy.sparse\nimport scipy.sparse.csgraph\nfrom skl"
},
{
"path": "umap/tests/__init__.py",
"chars": 1549,
"preview": "\"\"\"\nTest Suite for UMAP to ensure things are working as expected.\n\nThe test suite comprises multiple testing modules,\nin"
},
{
"path": "umap/tests/conftest.py",
"chars": 5868,
"preview": "# ===========================\n# Testing (session) Fixture\n# ==========================\n\nimport pytest\nimport numpy as n"
},
{
"path": "umap/tests/test_aligned_umap.py",
"chars": 3228,
"preview": "import pytest\nfrom umap import AlignedUMAP\nfrom sklearn.metrics import pairwise_distances\nfrom sklearn.cluster import KM"
},
{
"path": "umap/tests/test_chunked_parallel_spatial_metric.py",
"chars": 10249,
"preview": "import pytest\nimport numba\nimport os\nimport numpy as np\nfrom numpy.testing import assert_array_equal\nfrom umap import di"
},
{
"path": "umap/tests/test_composite_models.py",
"chars": 3681,
"preview": "from umap import UMAP\nimport pytest\n\ntry:\n # works for sklearn>=0.22\n from sklearn.manifold import trustworthiness"
},
{
"path": "umap/tests/test_data_input.py",
"chars": 2540,
"preview": "import numpy as np\nimport pytest as pytest\nfrom numba import njit\nfrom umap import UMAP\n\n\n@pytest.fixture(scope=\"session"
},
{
"path": "umap/tests/test_densmap.py",
"chars": 2455,
"preview": "from umap import UMAP\nimport pytest\n\ntry:\n # works for sklearn>=0.22\n from sklearn.manifold import trustworthiness"
},
{
"path": "umap/tests/test_parametric_umap.py",
"chars": 4807,
"preview": "import numpy as np\nimport tempfile\nimport pytest\nfrom sklearn.datasets import make_moons\nfrom sklearn.model_selection im"
},
{
"path": "umap/tests/test_plot.py",
"chars": 1806,
"preview": "import numpy as np\nimport pytest\nimport umap\n\n# Globals, used for all the tests\nSEED = 189212 # 0b101110001100011100\nnp"
},
{
"path": "umap/tests/test_spectral.py",
"chars": 1757,
"preview": "from umap.spectral import spectral_layout, tswspectral_layout\n\nimport numpy as np\nimport pytest\nimport re\nfrom scipy.ver"
},
{
"path": "umap/tests/test_umap.py",
"chars": 0,
"preview": ""
},
{
"path": "umap/tests/test_umap_get_feature_names_out.py",
"chars": 2643,
"preview": "import numpy as np\nfrom sklearn.datasets import make_classification\nfrom sklearn.pipeline import Pipeline, FeatureUnion\n"
},
{
"path": "umap/tests/test_umap_grads.py",
"chars": 7300,
"preview": "import numpy as np\nimport pytest\n\nimport umap.distances as dist\n\n\ndef numerical_gradient(f, x, eps=1e-6, forward_only=Fa"
},
{
"path": "umap/tests/test_umap_metrics.py",
"chars": 20845,
"preview": "import numpy as np\nfrom numpy.testing import assert_array_almost_equal\nimport umap.distances as dist\nimport umap.sparse "
},
{
"path": "umap/tests/test_umap_nn.py",
"chars": 5349,
"preview": "import numpy as np\nimport pytest\nfrom numpy.testing import assert_array_almost_equal\nfrom sklearn.neighbors import KDTre"
},
{
"path": "umap/tests/test_umap_on_iris.py",
"chars": 9600,
"preview": "from umap import UMAP\nfrom umap.umap_ import nearest_neighbors\nfrom scipy import sparse\nimport numpy as np\nfrom sklearn."
},
{
"path": "umap/tests/test_umap_ops.py",
"chars": 10841,
"preview": "# ===================================================\n# UMAP Fit and Transform Operations Test cases\n# (not really fit"
},
{
"path": "umap/tests/test_umap_repeated_data.py",
"chars": 3502,
"preview": "import numpy as np\nfrom umap import UMAP\n\n\n# ===================================================\n# Spatial Data Test ca"
},
{
"path": "umap/tests/test_umap_trustworthiness.py",
"chars": 5421,
"preview": "from umap import UMAP\nfrom sklearn.datasets import make_blobs\nfrom sklearn.metrics import pairwise_distances\nimport nump"
},
{
"path": "umap/tests/test_umap_validation_params.py",
"chars": 8236,
"preview": "# ===============================\n# UMAP fit Parameters Validation\n# ===============================\n\nimport warnings\ni"
},
{
"path": "umap/umap_.py",
"chars": 137883,
"preview": "# Author: Leland McInnes <leland.mcinnes@gmail.com>\n#\n# License: BSD 3 clause\nfrom __future__ import print_function\n\nimp"
},
{
"path": "umap/utils.py",
"chars": 6709,
"preview": "# Author: Leland McInnes <leland.mcinnes@gmail.com>\n#\n# License: BSD 3 clause\n\nimport time\nfrom warnings import warn\n\nim"
},
{
"path": "umap/validation.py",
"chars": 2460,
"preview": "import numpy as np\nimport numba\n\nfrom sklearn.neighbors import KDTree\nfrom umap.distances import named_distances\n\n\n@numb"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the lmcinnes/umap GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 120 files (31.2 MB), approximately 5.0M tokens, and a symbol index with 471 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.