Showing preview only (1,208K chars total). Download the full file or copy to clipboard to get everything.
Repository: TutteInstitute/evoc
Branch: main
Commit: a58b66e402bd
Files: 68
Total size: 1.1 MB
Directory structure:
gitextract_hnrqw2lv/
├── .gitignore
├── .readthedocs.yaml
├── LICENSE
├── README.rst
├── azure-pipelines.yml
├── doc/
│ ├── Makefile
│ ├── README.md
│ ├── build_docs.bat
│ ├── build_docs.sh
│ ├── requirements.txt
│ └── source/
│ ├── _static/
│ │ └── custom.css
│ ├── api/
│ │ ├── evoc.cluster_trees.rst
│ │ ├── evoc.clustering.rst
│ │ ├── evoc.clustering_utilities.rst
│ │ ├── generated/
│ │ │ ├── evoc.EVoC.rst
│ │ │ ├── evoc.boruvka.parallel_boruvka.rst
│ │ │ ├── evoc.cluster_trees.condense_tree.rst
│ │ │ ├── evoc.cluster_trees.extract_leaves.rst
│ │ │ ├── evoc.cluster_trees.get_cluster_label_vector.rst
│ │ │ ├── evoc.cluster_trees.get_point_membership_strength_vector.rst
│ │ │ ├── evoc.cluster_trees.mst_to_linkage_tree.rst
│ │ │ ├── evoc.clustering_utilities.binary_search_for_n_clusters.rst
│ │ │ ├── evoc.clustering_utilities.build_cluster_tree.rst
│ │ │ ├── evoc.clustering_utilities.find_duplicates.rst
│ │ │ ├── evoc.clustering_utilities.find_peaks.rst
│ │ │ ├── evoc.clustering_utilities.select_diverse_peaks.rst
│ │ │ ├── evoc.evoc_clusters.rst
│ │ │ ├── evoc.graph_construction.neighbor_graph_matrix.rst
│ │ │ ├── evoc.knn_graph.knn_graph.rst
│ │ │ ├── evoc.label_propagation.label_propagation_init.rst
│ │ │ ├── evoc.node_embedding.node_embedding.rst
│ │ │ └── evoc.numba_kdtree.build_kdtree.rst
│ │ └── index.rst
│ ├── benchmarks.ipynb
│ ├── changelog.rst
│ ├── conf.py
│ ├── examples.rst
│ ├── index.rst
│ ├── installation.rst
│ ├── quickstart.rst
│ └── user_guide.rst
├── evoc/
│ ├── __init__.py
│ ├── boruvka.py
│ ├── cluster_trees.py
│ ├── clustering.py
│ ├── clustering_utilities.py
│ ├── common_nndescent.py
│ ├── disjoint_set.py
│ ├── float_nndescent.py
│ ├── graph_construction.py
│ ├── int8_nndescent.py
│ ├── knn_graph.py
│ ├── label_propagation.py
│ ├── nested_parallelism.py
│ ├── node_embedding.py
│ ├── numba_kdtree.py
│ ├── tests/
│ │ ├── test_boruvka.py
│ │ ├── test_cluster_trees.py
│ │ ├── test_clustering.py
│ │ ├── test_knn_graph.py
│ │ ├── test_knn_graph_performance.py
│ │ ├── test_numba_kdtree.py
│ │ └── test_numba_kdtree_performance.py
│ └── uint8_nndescent.py
├── pyproject.toml
├── pytest.ini
├── scripts/
│ └── run_performance_tests.py
└── setup.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/.gitignore
.idea/evoc.iml
.idea/misc.xml
.idea/modules.xml
.idea/vcs.xml
.idea/inspectionProfiles/profiles_settings.xml
.idea/inspectionProfiles/Project_Default.xml
.vscode/settings.json
================================================
FILE: .readthedocs.yaml
================================================
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# You can also specify other tools here
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: doc/source/conf.py
fail_on_warning: false
# If using Sphinx, optionally build your docs in additional formats such as PDF
formats:
- pdf
- epub
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: doc/requirements.txt
- method: pip
path: .
extra_requirements:
- docs
# Optional but recommended, specify the Python version to use
# https://docs.readthedocs.io/en/stable/config-file/v2.html#python
================================================
FILE: LICENSE
================================================
BSD 2-Clause License
Copyright (c) 2024, Tutte Institute for Mathematics and Computing
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
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: README.rst
================================================
.. image:: doc/evoc_logo_horizontal.png
:width: 600
:align: center
:alt: EVōC Logo
====
EVōC
====
EVōC (pronounced as "evoke") provides Embedding Vector Oriented Clustering.
EVōC is a library for fast and flexible clustering of large datasets of high dimensional embedding vectors.
If you have CLIP-vectors, outputs from sentence-transformers, or openAI, or Cohere embed, and you want
to quickly get good clusters out this is the library for you. EVōC takes all the good parts of the
combination of UMAP + HDBSCAN for embedding clustering, improves upon them, and removes all
the time-consuming parts. By specializing directly to embedding vectors we can get good
quality clustering with fewer hyper-parameters to tune and in a fraction of the time.
EVōC is the library to use if you want:
* Fast clustering of embedding vectors on CPU
* Multi-granularity clustering, and automatic selection of the number of clusters
* Clustering of int8 or binary quantized embedding vectors that works out-of-the-box
As of now this is very much an early beta version of the library. Things can and will break right now.
We would welcome feedback, use cases and feature suggestions however.
-------------
Documentation
-------------
The full documentation is available on Read the Docs:
`https://evoc.readthedocs.io/en/latest/ <https://evoc.readthedocs.io/en/latest/>`_
-----------
Basic Usage
-----------
EVōC follows the scikit-learn API, so it should be familiar to most users. You can use EVōC wherever
you might have previously been using other sklearn clustering algorithms. Here is a simple example
.. code-block:: python
import evoc
from sklearn.datasets import make_blobs
data, _ = make_blobs(n_samples=100_000, n_features=1024, centers=100)
clusterer = evoc.EVoC()
cluster_labels = clusterer.fit_predict(data)
Some more unique features include the generation of multiple layers of cluster granularity,
the ability to extract a hierarchy of clusters across those layers, and automatic duplicate
(or very near duplicate) detection.
.. code-block:: python
import evoc
from sklearn.datasets import make_blobs
data, _ = make_blobs(n_samples=100_000, n_features=1024, centers=100)
clusterer = evoc.EVoC()
cluster_labels = clusterer.fit_predict(data)
cluster_layers = clusterer.cluster_layers_
hierarchy = clusterer.cluster_tree_
potential_duplicates = clusterer.duplicates_
The cluster layers are a list of cluster label vectors with the first being the finest grained
and later layers being coarser grained. This is ideal for layered topic modelling and use with
`DataMapPlot <https://github.com/TutteInstitute/datamapplot>`_. See
`this data map <https://lmcinnes.github.io/datamapplot_examples/ArXiv_data_map_example.html>`_
for an example of using these layered clusters in topic modelling (zoom in to access finer
grained topics).
------------
Installation
------------
EVōC has a small set of dependencies:
* numpy
* scikit-learn
* numba
* tqdm
* tbb
You can install EVōC from PyPI using pip:
.. code-block:: bash
pip install evoc
To install the latest version of EVōC from source:
.. code-block:: bash
pip install git+https://github.com/TutteInstitute/evoc.git
----------
References
----------
The algorithm implemented in EVōC is not published anywhere at this time. If you would like
to cite something in reference to EVōC, I would encourage you to cite the PLSCAN paper
on which the cluster extraction in EVōC is based:
Please cite:
D.M. Bot, L. McInnes, J. Aerts.
*Persistent Multiscale Density-based Clustering.*
In: arXiv preprint arXiv:2512.16558, 2025.
https://arxiv.org/abs/2512.16558.
-------
License
-------
EVōC is BSD (2-clause) licensed. See the LICENSE file for details.
------------
Contributing
------------
Contributions are more than welcome! If you have ideas for features of projects please get in touch. 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/TutteInstitute/evoc/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 in.
================================================
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:
- main
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:
- main
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_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'
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'
mac_py314:
imageName: 'macOS-latest'
python.version: '3.14'
linux_py314:
imageName: 'ubuntu-latest'
python.version: '3.14'
windows_py314:
imageName: 'windows-latest'
python.version: '3.14'
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: |
python -m pip install -U uv
uv sync --group cicd
env:
# Ensure that the LLVM_CONFIG environment variable is available during installation
LLVM_CONFIG: $(LLVM_CONFIG)
CMAKE_PREFIX_PATH: $(CMAKE_PREFIX_PATH)
displayName: 'Install package and dependencies'
- script: |
uv run pytest evoc/tests --show-capture=no -v --disable-warnings --junitxml=junit/test-results.xml --cov=evoc/ --cov-report=xml --cov-report=html
displayName: 'Run tests'
condition: ne(variables['Agent.OS'], 'Darwin')
- script: |
uv run pytest evoc/tests -v --capture=tee-sys --disable-warnings --junitxml=junit/test-results.xml --cov=evoc/ --cov-report=xml --cov-report=html
displayName: 'Run tests'
condition: eq(variables['Agent.OS'], 'Darwin')
- 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/release-'), eq(variables.triggeredByPullRequest, false))
jobs:
- job: BuildArtifacts
displayName: Build source dists and wheels
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.13'
displayName: 'Use Python 3.13'
- script: |
python -m pip install --upgrade pip
python -m pip install -U uv
uv sync
displayName: 'Install dependencies'
- script: |
uv build --no-sources --sdist --wheel
displayName: 'Build package'
- bash: |
export PACKAGE_VERSION="$(uv version --short)"
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: |
uvx twine check dist/*
uvx twine upload --repository pypi --config-file $(PYPIRC_CONFIG.secureFilePath) dist/*
displayName: 'Upload to PyPI'
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], variables['packageVersionFormatted']))
================================================
FILE: doc/Makefile
================================================
# Makefile for Sphinx documentation
# You can set these variables from the command line
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = source
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-build
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Custom targets
clean:
@$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
html:
@$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
livehtml:
sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
linkcheck:
@$(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)/linkcheck" $(SPHINXOPTS) $(O)
@echo "Link check complete; look for any errors in the above output or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
@$(SPHINXBUILD) -b doctest "$(SOURCEDIR)" "$(BUILDDIR)/doctest" $(SPHINXOPTS) $(O)
@echo "Testing of doctests in the sources finished, look at the results in $(BUILDDIR)/doctest/output.txt."
================================================
FILE: doc/README.md
================================================
# EVoC Documentation
This directory contains the Sphinx documentation for EVoC.
## Structure
```
doc/
├── build/ # Generated documentation (HTML, PDF, etc.)
├── source/ # Source files for documentation
│ ├── _static/ # Static files (CSS, images, etc.)
│ ├── _templates/ # Custom Sphinx templates
│ ├── api/ # API documentation files
│ ├── notebooks/ # Jupyter notebook examples
│ ├── tutorials/ # Step-by-step tutorials
│ ├── conf.py # Sphinx configuration
│ ├── index.rst # Main documentation page
│ └── *.rst # Other documentation pages
├── requirements.txt # Documentation dependencies
├── Makefile # Build commands (Unix)
├── build_docs.sh # Automated build script (Unix)
├── build_docs.bat # Automated build script (Windows)
└── README.md # This file
```
## Building the Documentation
### Prerequisites
1. Python 3.8 or later
2. Git (for development installation)
### Quick Build
**Unix/macOS:**
```bash
cd doc
./build_docs.sh
```
**Windows:**
```cmd
cd doc
build_docs.bat
```
### Manual Build
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Install EVoC in development mode:
```bash
pip install -e ../..
```
3. Build documentation:
```bash
make html
```
4. Open `build/html/index.html` in your browser
### Advanced Options
**Clean build:**
```bash
make clean html
```
**Check links:**
```bash
make linkcheck
```
**Run doctests:**
```bash
make doctest
```
**Live reload during development:**
```bash
pip install sphinx-autobuild
make livehtml
```
## Features
- **Sphinx RTD Theme**: Professional appearance matching ReadTheDocs
- **Numpydoc**: Automatic parsing of NumPy-style docstrings
- **Nbsphinx**: Integration of Jupyter notebooks as documentation
- **Autodoc**: Automatic API documentation generation
- **ReadTheDocs Ready**: Configured for automatic deployment
## Adding Content
### New Documentation Pages
1. Create `.rst` files in `source/`
2. Add them to the `toctree` in `index.rst`
3. Rebuild documentation
### Jupyter Notebooks
1. Add `.ipynb` files to `source/notebooks/`
2. Add them to `source/notebooks/index.rst`
3. Notebooks are automatically converted during build
### API Documentation
API documentation is automatically generated from docstrings. To add new modules:
1. Add the module to `source/api/index.rst`
2. Create a dedicated `.rst` file if needed
3. Rebuild documentation
## ReadTheDocs Integration
This documentation is configured for ReadTheDocs deployment:
- Configuration: `.readthedocs.yaml` in project root
- Requirements: `doc/requirements.txt`
- Python version: 3.11 (configurable in `.readthedocs.yaml`)
## Troubleshooting
**Import errors during build:**
- Ensure EVoC is installed in development mode: `pip install -e ../..`
- Check that all dependencies are installed: `pip install -r requirements.txt`
**Missing modules in API docs:**
- Verify the module paths in `source/api/index.rst`
- Check that modules are importable from the documentation directory
**Notebook execution errors:**
- Notebooks are not executed by default (`nbsphinx_execute = 'never'`)
- To execute notebooks during build, change to `nbsphinx_execute = 'always'` in `conf.py`
**Theme or styling issues:**
- Check `source/_static/custom.css` for customizations
- Verify `sphinx_rtd_theme` is installed
## Contributing
When adding new documentation:
1. Follow reStructuredText formatting
2. Use NumPy-style docstrings for API documentation
3. Include code examples where appropriate
4. Test build locally before submitting
5. Keep notebook outputs clear for examples
For more details, see the main EVoC contributing guidelines.
================================================
FILE: doc/build_docs.bat
================================================
@echo off
REM Documentation build script for EVoC (Windows)
echo Building EVoC Documentation
echo ==========================
REM Check if we're in the right directory
if not exist "source\conf.py" (
echo Error: Run this script from the doc directory
exit /b 1
)
REM Check if virtual environment exists, create if needed
if not exist "venv" (
echo Creating virtual environment...
python -m venv venv
)
REM Activate virtual environment
call venv\Scripts\activate.bat
REM Install requirements
echo Installing documentation requirements...
pip install -r requirements.txt
REM Install EVoC in development mode
echo Installing EVoC in development mode...
pip install -e ..\..
REM Clean previous build
echo Cleaning previous build...
make clean
REM Build HTML documentation
echo Building HTML documentation...
make html
if %ERRORLEVEL% equ 0 (
echo Documentation built successfully!
echo Open build\html\index.html in your browser to view
) else (
echo Build failed with errors
exit /b 1
)
echo Build complete!
================================================
FILE: doc/build_docs.sh
================================================
#!/bin/bash
# Documentation build script for EVoC
set -e # Exit on any error
echo "Building EVoC Documentation"
echo "=========================="
# Check if we're in the right directory
if [ ! -f "source/conf.py" ]; then
echo "Error: Run this script from the doc directory"
exit 1
fi
# Check if virtual environment exists, create if needed
if [ ! -d "venv" ]; then
echo "Creating virtual environment..."
python -m venv venv
fi
# Activate virtual environment
source venv/bin/activate
# Install requirements
echo "Installing documentation requirements..."
pip install -r requirements.txt
# Install EVoC in development mode
echo "Installing EVoC in development mode..."
pip install -e ../.
# Clean previous build
echo "Cleaning previous build..."
make clean
# Build HTML documentation
echo "Building HTML documentation..."
make html
# Check for warnings
if [ $? -eq 0 ]; then
echo "Documentation built successfully!"
echo "Open build/html/index.html in your browser to view"
else
echo "Build failed with errors"
exit 1
fi
# Optional: Run link check
if [ "$1" = "--check-links" ]; then
echo "Checking links..."
make linkcheck
fi
# Optional: Run doctests
if [ "$1" = "--test" ]; then
echo "Running doctests..."
make doctest
fi
echo "Build complete!"
================================================
FILE: doc/requirements.txt
================================================
# Sphinx documentation requirements
sphinx>=7.0.0
sphinx-rtd-theme>=2.0.0
numpydoc>=1.6.0
nbsphinx>=0.9.0
ipython>=8.0.0
ipykernel>=6.0.0
jupyter>=1.0.0
matplotlib>=3.5.0
numpy>=1.21.0
scipy>=1.7.0
scikit-learn>=1.0.0
pandas>=1.3.0
numba>=0.56.0
# Optional but recommended for better notebook handling
pandoc>=2.0
ipywidgets>=8.0.0
================================================
FILE: doc/source/_static/custom.css
================================================
/* Custom CSS for EVoC documentation */
/* Improve code block styling */
.highlight {
background-color: #f8f8f8;
border: 1px solid #e1e4e5;
border-radius: 4px;
padding: 8px;
margin: 12px 0;
}
/* Better parameter list formatting */
.field-list {
margin: 1em 0;
}
.field-list dt {
font-weight: bold;
color: #2980b9;
}
/* Notebook cell styling */
.nbinput .highlight,
.nboutput .highlight {
border-left: 4px solid #1f8c8c;
margin: 0.5em 0;
}
/* API documentation improvements */
.py.class dt {
background-color: #f0f0f0;
border-left: 4px solid #3498db;
padding: 8px;
margin-top: 20px;
}
.py.method dt {
background-color: #f9f9f9;
border-left: 3px solid #95a5a6;
padding: 6px;
margin-top: 15px;
}
/* Parameter tables */
.docutils th {
background-color: #34495e;
color: white;
padding: 8px;
}
.docutils td {
padding: 6px 8px;
border-bottom: 1px solid #ecf0f1;
}
/* Admonition improvements */
.admonition {
margin: 20px 0;
padding: 15px;
border-radius: 6px;
}
.admonition.note {
background-color: #e8f4fd;
border-left: 4px solid #3498db;
}
.admonition.warning {
background-color: #fdf4e8;
border-left: 4px solid #f39c12;
}
/* Code span improvements */
code.literal {
background-color: #f1f2f3;
color: #e74c3c;
padding: 2px 4px;
border-radius: 3px;
font-size: 90%;
}
/* Sidebar improvements */
.wy-nav-side {
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
}
/* Footer customization */
.rst-footer-buttons {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e1e4e5;
}
================================================
FILE: doc/source/api/evoc.cluster_trees.rst
================================================
evoc.cluster_trees
==================
.. automodule:: evoc.cluster_trees
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: doc/source/api/evoc.clustering.rst
================================================
evoc.clustering
===============
.. automodule:: evoc.clustering
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: doc/source/api/evoc.clustering_utilities.rst
================================================
evoc.clustering_utilities
=========================
.. automodule:: evoc.clustering_utilities
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: doc/source/api/generated/evoc.EVoC.rst
================================================
evoc.EVoC
=========
.. currentmodule:: evoc
.. autoclass:: EVoC
.. automethod:: __init__
.. rubric:: Methods
.. autosummary::
~EVoC.__init__
~EVoC.fit
~EVoC.fit_predict
~EVoC.get_metadata_routing
~EVoC.get_params
~EVoC.set_params
.. rubric:: Attributes
.. autosummary::
~EVoC.cluster_tree_
================================================
FILE: doc/source/api/generated/evoc.boruvka.parallel_boruvka.rst
================================================
evoc.boruvka.parallel\_boruvka
==============================
.. currentmodule:: evoc.boruvka
.. autofunction:: parallel_boruvka
================================================
FILE: doc/source/api/generated/evoc.cluster_trees.condense_tree.rst
================================================
evoc.cluster\_trees.condense\_tree
==================================
.. currentmodule:: evoc.cluster_trees
.. autofunction:: condense_tree
================================================
FILE: doc/source/api/generated/evoc.cluster_trees.extract_leaves.rst
================================================
evoc.cluster\_trees.extract\_leaves
===================================
.. currentmodule:: evoc.cluster_trees
.. autofunction:: extract_leaves
================================================
FILE: doc/source/api/generated/evoc.cluster_trees.get_cluster_label_vector.rst
================================================
evoc.cluster\_trees.get\_cluster\_label\_vector
===============================================
.. currentmodule:: evoc.cluster_trees
.. autofunction:: get_cluster_label_vector
================================================
FILE: doc/source/api/generated/evoc.cluster_trees.get_point_membership_strength_vector.rst
================================================
evoc.cluster\_trees.get\_point\_membership\_strength\_vector
============================================================
.. currentmodule:: evoc.cluster_trees
.. autofunction:: get_point_membership_strength_vector
================================================
FILE: doc/source/api/generated/evoc.cluster_trees.mst_to_linkage_tree.rst
================================================
evoc.cluster\_trees.mst\_to\_linkage\_tree
==========================================
.. currentmodule:: evoc.cluster_trees
.. autofunction:: mst_to_linkage_tree
================================================
FILE: doc/source/api/generated/evoc.clustering_utilities.binary_search_for_n_clusters.rst
================================================
evoc.clustering\_utilities.binary\_search\_for\_n\_clusters
===========================================================
.. currentmodule:: evoc.clustering_utilities
.. autofunction:: binary_search_for_n_clusters
================================================
FILE: doc/source/api/generated/evoc.clustering_utilities.build_cluster_tree.rst
================================================
evoc.clustering\_utilities.build\_cluster\_tree
===============================================
.. currentmodule:: evoc.clustering_utilities
.. autofunction:: build_cluster_tree
================================================
FILE: doc/source/api/generated/evoc.clustering_utilities.find_duplicates.rst
================================================
evoc.clustering\_utilities.find\_duplicates
===========================================
.. currentmodule:: evoc.clustering_utilities
.. autofunction:: find_duplicates
================================================
FILE: doc/source/api/generated/evoc.clustering_utilities.find_peaks.rst
================================================
evoc.clustering\_utilities.find\_peaks
======================================
.. currentmodule:: evoc.clustering_utilities
.. autofunction:: find_peaks
================================================
FILE: doc/source/api/generated/evoc.clustering_utilities.select_diverse_peaks.rst
================================================
evoc.clustering\_utilities.select\_diverse\_peaks
=================================================
.. currentmodule:: evoc.clustering_utilities
.. autofunction:: select_diverse_peaks
================================================
FILE: doc/source/api/generated/evoc.evoc_clusters.rst
================================================
evoc.evoc\_clusters
===================
.. currentmodule:: evoc
.. autofunction:: evoc_clusters
================================================
FILE: doc/source/api/generated/evoc.graph_construction.neighbor_graph_matrix.rst
================================================
evoc.graph\_construction.neighbor\_graph\_matrix
================================================
.. currentmodule:: evoc.graph_construction
.. autofunction:: neighbor_graph_matrix
================================================
FILE: doc/source/api/generated/evoc.knn_graph.knn_graph.rst
================================================
evoc.knn\_graph.knn\_graph
==========================
.. currentmodule:: evoc.knn_graph
.. autofunction:: knn_graph
================================================
FILE: doc/source/api/generated/evoc.label_propagation.label_propagation_init.rst
================================================
evoc.label\_propagation.label\_propagation\_init
================================================
.. currentmodule:: evoc.label_propagation
.. autofunction:: label_propagation_init
================================================
FILE: doc/source/api/generated/evoc.node_embedding.node_embedding.rst
================================================
evoc.node\_embedding.node\_embedding
====================================
.. currentmodule:: evoc.node_embedding
.. autofunction:: node_embedding
================================================
FILE: doc/source/api/generated/evoc.numba_kdtree.build_kdtree.rst
================================================
evoc.numba\_kdtree.build\_kdtree
================================
.. currentmodule:: evoc.numba_kdtree
.. autofunction:: build_kdtree
================================================
FILE: doc/source/api/index.rst
================================================
API Reference
=============
This section contains the complete API reference for EVoC.
Main Classes and Functions
--------------------------
.. currentmodule:: evoc
.. autosummary::
:toctree: generated/
:nosignatures:
EVoC
evoc_clusters
Core Clustering
---------------
.. autoclass:: EVoC
:members:
:inherited-members:
:show-inheritance:
.. autofunction:: evoc_clusters
Utility Functions
-----------------
.. currentmodule:: evoc.clustering_utilities
.. autosummary::
:toctree: generated/
:nosignatures:
find_peaks
binary_search_for_n_clusters
select_diverse_peaks
build_cluster_tree
find_duplicates
.. autofunction:: find_peaks
.. autofunction:: binary_search_for_n_clusters
.. autofunction:: select_diverse_peaks
.. autofunction:: build_cluster_tree
.. autofunction:: find_duplicates
Tree Operations
---------------
.. currentmodule:: evoc.cluster_trees
.. autosummary::
:toctree: generated/
:nosignatures:
mst_to_linkage_tree
condense_tree
extract_leaves
get_cluster_label_vector
get_point_membership_strength_vector
.. autofunction:: mst_to_linkage_tree
.. autofunction:: condense_tree
.. autofunction:: extract_leaves
.. autofunction:: get_cluster_label_vector
.. autofunction:: get_point_membership_strength_vector
Graph Construction
------------------
.. currentmodule:: evoc.knn_graph
.. autosummary::
:toctree: generated/
:nosignatures:
knn_graph
.. autofunction:: knn_graph
.. currentmodule:: evoc.graph_construction
.. autosummary::
:toctree: generated/
:nosignatures:
neighbor_graph_matrix
.. autofunction:: neighbor_graph_matrix
Node Embedding
--------------
.. currentmodule:: evoc.node_embedding
.. autosummary::
:toctree: generated/
:nosignatures:
node_embedding
.. autofunction:: node_embedding
.. currentmodule:: evoc.label_propagation
.. autosummary::
:toctree: generated/
:nosignatures:
label_propagation_init
.. autofunction:: label_propagation_init
Algorithm Components
--------------------
.. currentmodule:: evoc.boruvka
.. autosummary::
:toctree: generated/
:nosignatures:
parallel_boruvka
.. autofunction:: parallel_boruvka
.. currentmodule:: evoc.numba_kdtree
.. autosummary::
:toctree: generated/
:nosignatures:
build_kdtree
.. autofunction:: build_kdtree
================================================
FILE: doc/source/benchmarks.ipynb
================================================
{
"cells": [
{
"cell_type": "markdown",
"id": "93984a77-bccc-46fc-a7e1-4eb862d10f6e",
"metadata": {},
"source": [
"# Performance benchmarks\n",
"\n",
"This notebook provides performance benchmarks for EVoC in comparison to some commonly used options for embedding vector clustering. The goal of these benchmarks is not to be comprehensive, but rather to give a sense of where EVoC's strengths lie, particulary as compared with other standard options. The aim is to look at both clustering quality and compute time and focus primarily on real-world datasets. To ensure you can try running these benchmarks on your hardware we will provide all the code to run the benchmarks yourself. So to start let's get all the libraries we'll need, both to run the benchmarks and comparisons, and visualise the results."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a7ae0fff-dec4-49b8-9063-aafef992c764",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:11.007014Z",
"iopub.status.busy": "2026-03-25T20:36:11.006894Z",
"iopub.status.idle": "2026-03-25T20:36:16.807511Z",
"shell.execute_reply": "2026-03-25T20:36:16.806699Z",
"shell.execute_reply.started": "2026-03-25T20:36:11.007000Z"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"import time\n",
"import evoc\n",
"import umap\n",
"import hdbscan\n",
"import pandas as pd\n",
"import sklearn.cluster\n",
"import sklearn.metrics\n",
"import warnings\n",
"warnings.filterwarnings(\"ignore\")"
]
},
{
"cell_type": "markdown",
"id": "5a6ef4ce-bf5f-4e45-857b-bd60f6228fb4",
"metadata": {},
"source": [
"Now we will need come clustering alternatives to compare to. There are, of course, an endless array of clustering algorithms and implementations out there, but for the purposes of giving a basic comparison we will focus on the most common options for embedding vector clustering, such as those used in BERTopic. That means we'll need an implementation of UMAP + HDBSCAN for comparison, and we'll also compare with the standard workhorse: sklearn's KMeans. Sine we'll be benchmarking these we'll build a common calling format so we can build a benchmark harness around them easily. We'll start with UMAP + HDBSCAN which requires a little bit of work to glue together. "
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "9823192a-9d7e-4a1f-b912-84883f8273e8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:16.808127Z",
"iopub.status.busy": "2026-03-25T20:36:16.807874Z",
"iopub.status.idle": "2026-03-25T20:36:16.811159Z",
"shell.execute_reply": "2026-03-25T20:36:16.810725Z",
"shell.execute_reply.started": "2026-03-25T20:36:16.808110Z"
}
},
"outputs": [],
"source": [
"def umap_hdbscan(\n",
" data,\n",
" metric=\"euclidean\",\n",
" n_neighbors=15,\n",
" n_components=2,\n",
" min_samples=5,\n",
" min_cluster_size=10,\n",
" min_dist=0.1,\n",
" cluster_selection_method=\"eom\",\n",
" n_epochs=None,\n",
" negative_sample_rate=5,\n",
"):\n",
" embedding = umap.UMAP(\n",
" metric=metric,\n",
" n_neighbors=n_neighbors,\n",
" n_components=n_components,\n",
" min_dist=min_dist,\n",
" n_epochs=n_epochs,\n",
" negative_sample_rate=negative_sample_rate,\n",
" n_jobs=8,\n",
" ).fit_transform(data)\n",
" clustering = hdbscan.HDBSCAN(\n",
" min_samples=min_samples,\n",
" min_cluster_size=min_cluster_size,\n",
" cluster_selection_method=cluster_selection_method,\n",
" ).fit_predict(embedding)\n",
" return clustering"
]
},
{
"cell_type": "markdown",
"id": "f9985b20-ab5a-4001-b834-fea29fb90796",
"metadata": {},
"source": [
"Next up is KMeans. We don't need much of a wrapper here -- we can call on sklearn's implementation fairly directly."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e3730e80-f580-41d1-a103-9f3d2440bf15",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:16.811753Z",
"iopub.status.busy": "2026-03-25T20:36:16.811617Z",
"iopub.status.idle": "2026-03-25T20:36:16.825930Z",
"shell.execute_reply": "2026-03-25T20:36:16.825498Z",
"shell.execute_reply.started": "2026-03-25T20:36:16.811741Z"
}
},
"outputs": [],
"source": [
"def kmeans(data, n_clusters=10, kmeans_algorithm=\"lloyd\"):\n",
" return sklearn.cluster.KMeans(\n",
" n_clusters=n_clusters, \n",
" n_init=\"auto\", \n",
" algorithm=kmeans_algorithm\n",
" ).fit_predict(\n",
" data\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "e4e47881-987e-4944-a116-3acd14437b5a",
"metadata": {},
"source": [
"Lastly we need an EVoC function that works with the same pattern. We'll even be sure to use the reproducible (fixed ``random_state``) code-path that is a little slower, but vary the seed to ensure we get variation in results. Since EVoC can provide a few layers of clustering, we'll give it a little bonus by selecting out the best cluster layer compared to a target clustering. In practice EVoC usually selects a very good cluster layer, but for some of the datasets we'll be using the class labels are not exactly the most natural clustering, so we'll let the function choose a different layer in those cases. Note that we are not tuning any other EVoC parameters to optimize the results, just using defaults and selecting among the layers produced (usally 2-5 total layers for any of these datasets, so few enough that you could easily look througbn them by hand). In contrast we did spend time tuning parameters for the other algorithms to try to produce the best accuracy/quality scores we could. Sometimes this involved non-obvious choices of parameters."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "75a6f8bf-71eb-4053-943c-48d3f75d7052",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:16.826407Z",
"iopub.status.busy": "2026-03-25T20:36:16.826278Z",
"iopub.status.idle": "2026-03-25T20:36:16.837721Z",
"shell.execute_reply": "2026-03-25T20:36:16.837043Z",
"shell.execute_reply.started": "2026-03-25T20:36:16.826395Z"
}
},
"outputs": [],
"source": [
"def EVoC(data, test_target=None, random_state=None):\n",
" if random_state is None:\n",
" random_state = np.random.randint(65536)\n",
" cls = evoc.EVoC(random_state=random_state).fit(data)\n",
" if test_target is None:\n",
" return cls.labels_\n",
" result = np.full(data.shape[0], -1)\n",
" best_ari = 0.0\n",
" for labels in cls.cluster_layers_:\n",
" ari = sklearn.metrics.adjusted_rand_score(\n",
" test_target[labels >= 0], labels[labels >= 0]\n",
" )\n",
" if ari > best_ari:\n",
" best_ari = ari\n",
" result = labels\n",
" return result"
]
},
{
"cell_type": "markdown",
"id": "5d644252-ab58-4de7-9ca2-b4db23bc38af",
"metadata": {},
"source": [
"Now we need a test harness. To asses the quality of a clustering we'll use datasets that come equipped with class labels, and specifically datasets where there is good reason to expect that the clusters should align reasonably with the class labels. This let's us use robust scores such as Adjusted Rand Index (ARI) and Adjusted Mutual Information (AMI) that compare a clustering against ground-truth labels. Since both UMAP + HDBSCAN and EVoC have a notion of noise points we'll exclude those from the ARI and AMI computations (as they don't make sense as a single class, and confuse things). But we will keep track of how much of the data is clustered, and also track a \"clustering score\" that is a weighted geometric mean of the ARI and the amount of data clustered (weighted 2:1 in favour of ARI accuracy, so we mostly care about being right, but also need to cluster a reasonable amount of data). We will also keep track of how long it takes to run any of these methods."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "8c2abe82-1c74-45fa-b6cc-5c36647b8c74",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:16.838269Z",
"iopub.status.busy": "2026-03-25T20:36:16.838138Z",
"iopub.status.idle": "2026-03-25T20:36:16.850442Z",
"shell.execute_reply": "2026-03-25T20:36:16.849785Z",
"shell.execute_reply.started": "2026-03-25T20:36:16.838257Z"
}
},
"outputs": [],
"source": [
"def score_clustering(data, target, clustering_function, n_runs=16, **kwargs):\n",
" result = np.zeros((n_runs, 5), dtype=np.float32)\n",
" for i in range(n_runs):\n",
" start_time = time.time()\n",
" clustering = clustering_function(data, **kwargs)\n",
" result[i, 0] = time.time() - start_time\n",
" result[i, 1] = sklearn.metrics.adjusted_rand_score(\n",
" target[clustering >= 0], clustering[clustering >= 0]\n",
" )\n",
" result[i, 2] = sklearn.metrics.adjusted_mutual_info_score(\n",
" target[clustering >= 0], clustering[clustering >= 0]\n",
" )\n",
" result[i, 3] = np.sum(clustering >= 0) / clustering.shape[0]\n",
" result[i, 4] = np.cbrt((result[i, 1] ** 2) * result[i, 3])\n",
"\n",
" result = pd.DataFrame(\n",
" result,\n",
" columns=(\n",
" \"Elapsed time\",\n",
" \"Adjusted Rand Index\",\n",
" \"Adjusted Mutual Information\",\n",
" \"Proportion clustered\",\n",
" \"Clustering Score\",\n",
" ),\n",
" )\n",
" result[\"algorithm\"] = clustering_function.__name__.replace(\"_\", \"\\n\")\n",
" result = result.melt(\n",
" id_vars=[\"algorithm\"],\n",
" value_vars=[\n",
" \"Elapsed time\",\n",
" \"Adjusted Rand Index\",\n",
" \"Adjusted Mutual Information\",\n",
" \"Proportion clustered\",\n",
" \"Clustering Score\",\n",
" ],\n",
" var_name=\"measure\",\n",
" )\n",
" return result"
]
},
{
"cell_type": "markdown",
"id": "af6fd5e6-2f44-478b-9a34-b1e935b141be",
"metadata": {},
"source": [
"Lastly we'll need a simple function to run all our clustering algorithm benchmarks for each method across a given dataset and provide a nice table of results that we can easily pass to seaborn for plotting."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "40c1aa9a-fb16-473a-ad2c-02ac59bad712",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:16.850912Z",
"iopub.status.busy": "2026-03-25T20:36:16.850774Z",
"iopub.status.idle": "2026-03-25T20:36:16.862692Z",
"shell.execute_reply": "2026-03-25T20:36:16.862360Z",
"shell.execute_reply.started": "2026-03-25T20:36:16.850901Z"
}
},
"outputs": [],
"source": [
"def run_dataset_benchmarks(data, target, n_runs, kmeans_kwargs, umap_hdbscan_kwargs):\n",
" \"\"\"Score all three algorithms on a dataset and return combined results.\"\"\"\n",
" kmeans_results = score_clustering(\n",
" data, target, kmeans, n_runs=n_runs, **kmeans_kwargs\n",
" )\n",
" umap_results = score_clustering(\n",
" data, target, umap_hdbscan, n_runs=n_runs, **umap_hdbscan_kwargs\n",
" )\n",
" evoc_results = score_clustering(\n",
" data, target, EVoC, test_target=target, n_runs=n_runs\n",
" )\n",
" return pd.concat(\n",
" [kmeans_results, umap_results, evoc_results.assign(algorithm=\"EVoC\")],\n",
" ignore_index=True,\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "9b36e588-23ee-42bf-b931-f2a797250807",
"metadata": {},
"source": [
"## Image embeddings\n",
"\n",
"Let's start by looking at image embeddings. For a \"real-world\" dataset we'll use the tried and true CIFAR-100 dataset. The CIFAR-100 dataset is a popular computer vision benchmark containing 60,000 32x32 color images across 100 classes. In our case we don't need to worry about the images themselves, but rather their embeddings. We have built a dataset with embedding vectors generated by CLIP and provided it on Huggingface datasets so we don't have to worry about the time/cost of embedding all the images. If you wish you can generate your own embeddings, potentially with other models such as SigLIP. Let's pull that data down. "
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "2bb0aaaa-35f0-4588-ad58-466f0cae8ceb",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:16.863280Z",
"iopub.status.busy": "2026-03-25T20:36:16.863153Z",
"iopub.status.idle": "2026-03-25T20:36:19.367650Z",
"shell.execute_reply": "2026-03-25T20:36:19.367253Z",
"shell.execute_reply.started": "2026-03-25T20:36:16.863268Z"
}
},
"outputs": [],
"source": [
"from datasets import load_dataset"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "2046299e-7e86-490b-805c-4ccc92088877",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:19.368857Z",
"iopub.status.busy": "2026-03-25T20:36:19.368555Z",
"iopub.status.idle": "2026-03-25T20:36:49.643729Z",
"shell.execute_reply": "2026-03-25T20:36:49.642787Z",
"shell.execute_reply.started": "2026-03-25T20:36:19.368842Z"
}
},
"outputs": [],
"source": [
"ds_cifar = load_dataset(\"lmcinnes/evoc_bench_cifar100\")\n",
"cifar_data = np.asarray(ds_cifar[\"train\"][\"embedding\"])\n",
"cifar_target = np.asarray(ds_cifar[\"train\"][\"target\"])"
]
},
{
"cell_type": "markdown",
"id": "44a1a510-281d-493c-b9fb-38e89d68cd99",
"metadata": {},
"source": [
"Now we just need to run the benchmarks. Here we had to rune parameters a little. Oddly enough KMeans actually works better if you ask for 125 clusters (instead of the 100 classes that exist) because with more clusters it does a better job of breaking up some of the more easily confused classes that otherwise notably drag down ARI scores. For UMAP + HDBSCAN we need to manage pick the right parameters; in general UMAP doesn't need too much tuning for this, but HDBSCAN works best with leaf clustering and a carefully well-chosen ``min_cluster_size``. A little experimentation finds around 120 works well for this data."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "3fa73eb5-68c7-49be-80a7-bd3527264111",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:36:49.644685Z",
"iopub.status.busy": "2026-03-25T20:36:49.644502Z",
"iopub.status.idle": "2026-03-25T20:42:55.905457Z",
"shell.execute_reply": "2026-03-25T20:42:55.904368Z",
"shell.execute_reply.started": "2026-03-25T20:36:49.644670Z"
}
},
"outputs": [],
"source": [
"cifar_results = run_dataset_benchmarks(\n",
" cifar_data, \n",
" cifar_target, \n",
" n_runs=16, \n",
" kmeans_kwargs={\"n_clusters\":125}, \n",
" umap_hdbscan_kwargs={\n",
" \"min_samples\":5,\n",
" \"min_cluster_size\":120, \n",
" \"metric\":\"cosine\", \n",
" \"cluster_selection_method\":\"leaf\"\n",
" }\n",
")"
]
},
{
"cell_type": "markdown",
"id": "83150948-cc2d-429f-a913-eac665250739",
"metadata": {},
"source": [
"Before we look at quality, let's compare how long these different approaches took to run:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "9233e308-90ed-4c57-8a1e-738a7a7e898f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:42:55.906750Z",
"iopub.status.busy": "2026-03-25T20:42:55.906539Z",
"iopub.status.idle": "2026-03-25T20:42:56.203555Z",
"shell.execute_reply": "2026-03-25T20:42:56.202959Z",
"shell.execute_reply.started": "2026-03-25T20:42:55.906732Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"<seaborn.axisgrid.FacetGrid at 0x742050969b80>"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAMVCAYAAADqKmIJAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAUCJJREFUeJzt3Qd8VeX9P/Bv2IIkgjIFEUVw4Bb33triat216q9a66pWrf5s3Vat2mqHdbV1VH9WW1et1r1FoEq1WkUEREFFUEYCyCb/13P4JyZwY9FDchPyfr9e95Wc55x77pNzc5PzOc84JZWVlZUBAACQQ4s8TwYAABAsAACA5UKLBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYANEolJSXx4IMPxopYz+eeey573vTp0+utXgANTbAAoMEdc8wx2Yn1ko+99957hXs3dt555zj99NNrlW277bYxceLEKCsrK1q9AJa3Vst9jwA0CZWVlbFw4cJo1ao4/wpSiLj11ltrlbVt2zaagzZt2kT37t2LXQ2A5UqLBcDXuAJ96qmnZlehO3XqFN26dYubb745Zs2aFccee2x07Ngx1l577Xj00UdrPe/tt9+OfffdN1ZeeeXsOUcddVR89tln1esfe+yx2H777WOVVVaJVVddNb75zW/G2LFjq9fPmzcvTjnllOjRo0e0a9cu1lxzzbjiiiuyde+//352xf/111+v3j51s0llqdtNze43jz/+eGyxxRbZSfyLL76YBYyrrroq1lprrVhppZVi4403jnvvvbfefy/S66eT65qPdDzrcs4550T//v2jffv2WV3PP//8mD9/fvX6iy66KDbZZJO46aabonfv3tl2Bx98cK3uRukYbLnlltGhQ4fsOG+33XbxwQcfVK//+9//Hptvvnl2fNNrXHzxxbFgwYLq9aNHj44dd9wxW7/++uvHk08++V9bZp5//vn49a9/Xd0qk96rJbtC3XbbbVl9Hn744RgwYEBW929/+9vZ79Ttt9+evdfp2KTfuxQGa/5OnH322bH66qtnP9NWW21V/X4DNDTBAuBrSCd7q622Wvzzn//MTvZOPPHE7CQ2dXH517/+FXvttVcWHD7//PNs+9TtZaeddspOfF999dUsREyaNCkOOeSQ6n2mk8gzzjgjXnnllXj66aejRYsWceCBB8aiRYuy9b/5zW/ioYceir/85S8xatSouPPOO7MTzq8qnYimQDJy5MjYaKON4rzzzstaDm644YZ466234kc/+lF85zvfyU6I6/KDH/wgC0hf9hg/fvxy/d1KgS2dgKeAlk7Uf//738e1115ba5sxY8ZkxycFhHSMU9A6+eSTs3UpIBxwwAHZ+/DGG2/E0KFD4/vf/352gp+kwJV+7h/+8IfZa6SAkl7vsssuy9an9+Gggw6Kli1bxrBhw+LGG2/Mws6XSfXcZptt4vjjj89+B9IjhZ5C0u9Keo/vvvvurO4pIKTX+8c//pE97rjjjizA1gx9KcgOGTIke076mdLvYGoJSgEIoMFVAvCV7LTTTpXbb7999fKCBQsqO3ToUHnUUUdVl02cOLEy/YkdOnRotnz++edX7rnnnrX2M2HChGybUaNGFXydyZMnZ+vffPPNbPnUU0+t3HXXXSsXLVq01Lbjxo3Ltn3ttdeqy6ZNm5aVPfvss9ly+pqWH3zwweptZs6cWdmuXbvKl19+udb+vve971UefvjhdR6DSZMmVY4ePfpLH/Pnz6/z+UcffXRly5Yts+NW83HJJZdUb5Pq+sADD9S5j6uuuqpy8803r16+8MILs32m41rl0UcfrWzRokX2fkyZMiXb53PPPVdwfzvssEPl5ZdfXqvsjjvuqOzRo0f2/eOPP15w//+tnun35bTTTqtVVvVepPcoufXWW7PlMWPGVG9zwgknVLZv375yxowZ1WV77bVXVp6kbUtKSio/+uijWvvebbfdKs8999w66wNQX4yxAPga0pX+KukKduq6tOGGG1aXpa5OyeTJk7OvI0aMiGeffTa7kr+k1N0pdfFJX1P3nnQ1PHWRqmqpSFf+Bw4cmHWr2WOPPbKuMumqdOoqteeee37luqduUFXSlfk5c+Zk+60pdbHZdNNN69xH165ds0ceu+yyS9ZKUlPnzp3r3D5dqf/Vr36VtUrMnDkza4EoLS2ttc0aa6wRvXr1ql5OrQXpOKYWntRSkY5hak1KP+/uu++etRilrmVV71FqLapqoUhSt6N0fFJrQmrhKbT/5SV1f0pd6Gr+DqUWqZq/M6ms6ncqtYyl/JV+d2qaO3du9vsI0NAEC4CvoXXr1rWWU3eammVV3WuqwkH6Onjw4LjyyiuX2lfViW1an7rJpC4+PXv2zJ6TAkU6yU8222yzGDduXDZ246mnnspOitPJcTrhTt2mksUX+herOf6gptQXv0pV/R555JGsn/6yDqROXaFSV6wvk0JLOhGvS6pHv379YlmksHXYYYdlYx5SMEizKaXuP7/85S+/9HlV70PV19TlK3V1Sl2N7rnnnqwbWBonsfXWW2fHIu0/dT9aUhpTUfPYLrn/hvidqiqr+TuVQm0KROlrTYUCLEB9EywAGkAKBffdd192BbrQLExTpkzJroinfv077LBDVvbSSy8ttV26Qn/ooYdmjzS4N7VcTJ06Nbp06ZKtT334q1oaag7krksagJwCRGoVSVf0l9Ull1wSZ5111pduk8LR8pLGEfTp0yd++tOfVpfVHHRdJf0cH3/8cfVrp3EUKXTVvKqfjk96nHvuuVmLw1133ZUFi/QepZaNusJOOlaF9r8sM0DVHHC9vKSfIe03tWBU/c4AFJNgAdAA0gDi1BJx+OGHx49//ONs4Hfq0pOuuqfyNONP6r6SBuemFox0Avu///u/tfaRBiqndWkAeDpZ/utf/5rNpJRmE0rL6eT45z//eRZeUleqdDV+WQZEp4CQBmynK+BpVqqKiop4+eWXs6veRx99dL11hUpddj755JNaZSl0pWOzpHSyn45JOl6DBg3KWlgeeOCBgi0Lqc6/+MUvsp8jtU6klp10nFJrTzq+++23XxYMUoh4991347vf/W723AsuuCDrXpZajdIg6HRM04DoN998M372s59lrUOpG1raPrWUpP3XDDp1Se/H8OHDs9mg0jH9su5eX0UKS0ceeWR1fVLQSO/7M888k3XLSzOQATQks0IBNIB0IpuuuqcrzKkrT+ridNppp2VdetIJbHqkk+bUrSWtSyf6V199da19pJPS1JUqjZFIJ9fpRDXNFlTVDeqWW27Juj+l9Wnf6WR4WVx66aXZSXWaKWq99dbL6pdmVerbt2/Up9QdKQWlmo8UbArZf//9s2OSpttNwSoFnzQepVAASV2Z0kl1Gn+SjuX1119fPYbhnXfeiW9961vZSXmaESrt74QTTsjWp587Tfeaukal45uC2jXXXJO1lCTpOKcwkwJRmrL2uOOOqzUeoy4puKWuSqnFI7UsLc/ZslLXrhQszjzzzCz0pNCUQkxdM08B1KeSNIK7Xl8BABpAuo/Fgw8+uExdwABY/rRYAAAAuQkWAABAbrpCAQAAuWmxAAAAchMsAACA3Fb4YJEmvUpzjZv8CgAA6s8KHyxmzJiRzROfvgIAAPVjhQ8WAABA/RMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgt1b5dwEAX8P0CRGv/D5i8jsRXfpHDDo+olMfhxKgiSqprKysjBVYRUVFlJWVRXl5eZSWlha7OgAkk0dG3LJ3xJzpXxyPtmURxz4S0X1DxwigCdIVCoCG98zPaoeKZG55xNOXejcAmijBAoCG9/6LhcvHvdDQNQFgOREsAGh47VcrXN6hjnIAGj3BAoCGt8Wxhcs3P6ahawLAciJYANDwtj45YptTIlq1W7zcsm3EVj+I2P5H3g2AJsqsUAAUz+xpEVPHRXRaM6J9Z+8EQBPmPhYAFM9KnSJW7+QdAFgB6AoFAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAAAIFgAAQPFpsQAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAoGkHiyuuuCIGDRoUHTt2jK5du8YBBxwQo0aNqrXNMcccEyUlJbUeW2+9ddHqDAAANLJg8fzzz8fJJ58cw4YNiyeffDIWLFgQe+65Z8yaNavWdnvvvXdMnDix+vGPf/yjaHUGAACW1iqK6LHHHqu1fOutt2YtFyNGjIgdd9yxurxt27bRvXv3Zdrn3Llzs0eVioqK5VhjAACg0Y+xKC8vz7527ty5Vvlzzz2XBY7+/fvH8ccfH5MnT/7S7lVlZWXVj969e9d7vQEAoLkrqaysrIxGIFVj//33j2nTpsWLL75YXX7PPffEyiuvHH369Ilx48bF+eefn3WZSq0aqSVjWVosUrhIoaW0tLTBfh4AAGhOGk2wSGMtHnnkkXjppZeiV69edW6XxlikkHH33XfHQQcd9F/3m4JFarkQLAAAYAUdY1Hl1FNPjYceeiheeOGFLw0VSY8ePbJgMXr06AarHwAA0IiDRWosSaHigQceyMZR9O3b978+Z8qUKTFhwoQsYAAAAI1Di2J3f7rzzjvjrrvuyu5l8cknn2SP2bNnZ+tnzpwZZ511VgwdOjTef//9LHwMHjw4VltttTjwwAOLWXUAAKCxjLFIN7srJE07m26MlwJGumnea6+9FtOnT89aKXbZZZe49NJLl3m2J2MsAACgGQ3eri+CBQAANLP7WAAAAE2TYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAAIIFAABQfFosAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgKYdLK644ooYNGhQdOzYMbp27RoHHHBAjBo1qtY2lZWVcdFFF0XPnj1jpZVWip133jneeuutotUZAABoZMHi+eefj5NPPjmGDRsWTz75ZCxYsCD23HPPmDVrVvU2V111VVxzzTVx3XXXxSuvvBLdu3ePPfbYI2bMmFHMqgMAADWUVKYmgUbi008/zVouUuDYcccds9aK1FJx+umnxznnnJNtM3fu3OjWrVtceeWVccIJJyy1j7Q+PapUVFRE7969o7y8PEpLSxv05wEAgOaiUY2xSCf/SefOnbOv48aNi08++SRrxajStm3b2GmnneLll1+us3tVWVlZ9SOFCgAAoJkEi9Q6ccYZZ8T2228fAwcOzMpSqEhSC0VNablq3ZLOPffcLKBUPSZMmNAAtQcAgOatVTQSp5xySrzxxhvx0ksvLbWupKRkqRCyZFnNFo30AAAAmlmLxamnnhoPPfRQPPvss9GrV6/q8jRQO1mydWLy5MlLtWIAAADNNFiklofUUnH//ffHM888E3379q21Pi2ncJFmjKoyb968bHD3tttuW4QaAwAAja4rVJpq9q677oq//e1v2b0sqlom0qDrdM+K1N0pzQh1+eWXxzrrrJM90vft27ePI444ophVBwAAGst0s3WNk7j11lvjmGOOyb5P1bv44ovjpptuimnTpsVWW20Vv/vd76oHeP83abrZFFRMNwsAAM3kPhb1QbAAAIBmMngbAABo2gQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAgAAyE2wAAAAchMsAACA3AQLAABAsAAAAIpPiwUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAAAC5CRYAAEBuggUAAJCbYAEAAOQmWAAAALkJFgAAQG6CBQAAkJtgAQAA5CZYAAAAuQkWAABAboIFAACQm2ABAADkJlgAUL8m/jti7DMRc2csvW7h/IiKjyMWzPMuADRxrYpdAQBWUNMnRNzznYiJry9ebrNyxO4XRWx5/OLll38b8dKvIj7/LGKlzhHbnhqxwxlFrTIAX59gAUD9uPfYL0JFMm9mxD/Oiui+UcSn70Q8cd4X62ZPjXj64oi2Hb8IHgA0KbpCAbD8fToq4sNXCq97/c6I4TcWXjfsBu8GQBMlWACw/M2p+JJ15RHlHxVeV1FHOQCNnmABwPLXY+OI9qsVXtdv94heWxRe12uQdwOgiRIsAFj+WrWJ2PeqiJKWtcvX3CFio0MjdvlJRKt2tde1bLu4HIAmqaSysrIyVmAVFRVRVlYW5eXlUVpaWuzqADQvk0dGvHZnxOdTI9baOWKDAxeHjuSTNyOG/i5i8tsRqw2I2ObkiJ6bFLvGAHxNggUAxbNgbsSMiRErd49ovUQLBgBNiulmASiOF36x+F4Wc6ZHtCuL2PqkiJ3OiSgp8Y4ANEHGWADQ8F75Y8Qzly4OFVUzRT13Rd3T0ALQ6AkWADS8f95cuHz4TQ1dEwCKHSzGjBkTjz/+eMyePTtbXsHHgAOwPFVMLFyexlsA0DyCxZQpU2L33XeP/v37x7777hsTJy7+J3DcccfFmWeeWR91BGBFs8ZWhct711EOwIoXLH70ox9Fq1atYvz48dG+ffvq8kMPPTQee+yx5V0/AFZEO58b0fqL/yGZVitF7HpesWoEQEPPCvXEE09kXaB69epVq3ydddaJDz74IG99AGgOVt8s4vhnI4al+1iM/P/3sTgpotsGxa4ZAA0VLGbNmlWrpaLKZ599Fm3btv269QCguem6bsR+vy12LQAoVleoHXfcMf70pz9VL5eUlMSiRYvi6quvjl122WV51QuAFd3MTyOevzri3v+JeO7KiBmTil0jABryzttvv/127LzzzrH55pvHM888E/vtt1+89dZbMXXq1BgyZEisvfba0ZhUVFREWVlZlJeXR2lpabGrA0AyZWzELXtHzJr8xfFov2rEsY9GdBngGAE0hxaL9ddfP954443YcsstY4899si6Rh100EHx2muvNbpQAUAjlW6OVzNUJJ9PiXj6kmLVCICGbrFoarRYADRCP+/zxV23a0ozRf3UvSwAmsXg7RdeeOG/jsEAgC/VrqxwsGi3igMH0FyCRRpfsaQ0gLvKwoUL89cKgBXbZt9d3B1qqfKjilEbAIoxxmLatGm1HpMnT85ujDdo0KDsHhcA8F9td3rEZkdHlLRcvJy+bvKdiB1/7OABNPcxFqmLVLor94gRI77Sc9I0tek5EydOjAceeCAOOOCA6vXHHHNM3H777bWes9VWW8WwYcOW+TWMsQBoxComRkwZHbFqv4jSnsWuDQAN2RWqLl26dIlRo0Z9peekGaU23njjOPbYY+Nb3/pWwW323nvvuPXWW6uX27Rpk7uuADQSpT0WPwBofsEiTTVbU2rwSK0NP//5z7OQ8FXss88+2ePLpLt5d+/efZn3OXfu3OxRs8UCAABoZMFik002yQZrL9mDauutt45bbrkllrfnnnsuunbtGqusskrstNNOcdlll2XLdbniiivi4osvXu71AAAAluMYiw8++KDWcosWLbJuUO3atctXkZKSpcZY3HPPPbHyyitHnz59Yty4cXH++efHggULsjEZqSVjWVssevfu7c7bAADQmFos0kl+Qzn00EOrvx84cGBsscUW2es/8sgj2d2+C0mBo67QAQAAFDFY/OY3v1nmHf7whz+M+tKjR48sWIwePbreXgMAAKinYHHttdcuc3em+gwWU6ZMiQkTJmQBAwAAaGLBIo1vqA8zZ86MMWPG1Hqd119/PTp37pw9Lrroomwa2hQk3n///fjJT34Sq622Whx44IH1Uh8AAKDI97H4Ol599dXYZZddqpfPOOOM7OvRRx8dN9xwQ7z55pvxpz/9KaZPn56Fi7RtGtDdsWPHItYaAABYLnfe/vDDD+Ohhx6K8ePHx7x582qtu+aaa6IxcedtAABohC0WTz/9dOy3337Rt2/f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GJo2n+HmoaMWi4gWLVpEr169iv1e0ABSqBAsoOnyGYamzWcYg7cBAIDcBAsAACA3wYImr23btnHhhRdmX4Gmx2cYmjafYZrN4G0AAKD+abEAAAByEywAAIDcBAsAACA3wYJ6s/POO8fpp5/uCAMANAOCBQBAM3bMMcdESUnJUo9dd901VltttfjZz35W8HlXXHFFtn7evHnL9DrPPvts7LvvvrHqqqtG+/btY/31148zzzwzPvroo+X8E1EsggUAQDO39957x8SJE2s97rvvvvjOd74Tt912WxSaRPTWW2+No446Ktq0afNf93/TTTfF7rvvHt27d8/2+/bbb8eNN94Y5eXl8ctf/rKefioammBBg3nssceirKws/vSnP2VXRw444IC4/PLLo1u3brHKKqvExRdfHAsWLIgf//jH0blz5+jVq1fccssttfaRrmoceuih0alTp+yKx/777x/vv/9+9fpXXnkl9thjj+wKSnqtnXbaKf71r3/V2ke6CvOHP/whDjzwwOyKyTrrrBMPPfRQ9fpp06bFkUceGV26dImVVlopW5/+eAK1rbnmmvGrX/2qVtkmm2wSF110UfVnLZ1MfPOb38w+a+utt14MHTo0xowZk3WV7NChQ2yzzTYxduzY6uen79PnOv1dWHnllWPQoEHx1FNPLfW6l156aRxxxBHZNj179ozf/va33h7IeS+KdNJf85H+137ve9/LPpcvvPBCre1ffPHFGD16dLZ+0aJFcckll2T/t9N+0t+B9D+/yocffhg//OEPs0f6v54+/+lzvOOOO2b/jy+44ALv3QpCsKBB3H333XHIIYdkoeK73/1uVvbMM8/Exx9/nP2xuuaaa7KTkXQCkv6QDR8+PH7wgx9kjwkTJmTbf/7557HLLrtkJxLpOS+99FL2fbrKUtUMO2PGjDj66KOzP3jDhg3LQkFqdk3lNaUQk+rzxhtvZOtTkJg6dWq27vzzz8+upDz66KMxcuTIuOGGG7KgAnx1KQCkz/zrr78e6667bhYGTjjhhDj33HPj1VdfzbY55ZRTqrefOXNm9plMYeK1116LvfbaKwYPHhzjx4+vtd+rr746Ntpoo+zCQdrXj370o3jyySe9RbCcbbjhhlnAX/ICWwoIW265ZQwcODB+/etfZ60Ov/jFL7L/q+lzu99++2XBI/nrX/+a/Z8+++yzC75GurjICiLdIA/qw0477VR52mmnVf7ud7+rLCsrq3zmmWeq1x199NGVffr0qVy4cGF12YABAyp32GGH6uUFCxZUdujQofLPf/5ztvzHP/4x22bRokXV28ydO7dypZVWqnz88ccL1iHto2PHjpV///vfq8vSr/15551XvTxz5szKkpKSykcffTRbHjx4cOWxxx673I4DrKjSZ/jaa6+tVbbxxhtXXnjhhQU/a0OHDs3K0me5Svp8t2vX7ktfZ/3116/87W9/W+t1995771rbHHrooZX77LNP7p8JmqP0P7lly5bZ/9yaj0suuSRbf8MNN2TLM2bMyJbT17R80003Zcs9e/asvOyyy2rtc9CgQZUnnXRS9v2JJ55YWVpa2uA/Fw1PiwX1KvWjTDNDPfHEE1lrQ00bbLBBtGjxxa9g6vqQroxUadmyZdbdafLkydnyiBEjsi4UHTt2zFoq0iN1mZozZ051V4q0bWrl6N+/f9YVKj3SFdAlr3amK51VUneMtM+q1znxxBOzFpbUlJuurrz88sv1dHRgxVfzs5Y+40nNz3kqS5/hioqKbHnWrFnZ5y4N6kxXMdPn/J133lnqM5y6UC25nFoYga8n/Y9OLYs1HyeffHK27vDDD8+6O91zzz3Zcvqarh0cdthh2Wc39T7Ybrvtau0vLVd9JtO2qWskK75Wxa4AK7Z0cp66KqQm1NSUWvMPS+vWrWttm9YVKkt/zJL0dfPNN4//+7//W+p10niIJI3d+PTTT7N+33369Mn6eqYTjiVnrPiy19lnn33igw8+iEceeSTrjrHbbrtlf1xTEy/whXRhYMkBnfPnz6/zs1b1+S9UVvX5S2OsHn/88ezz1q9fv2yc07e//e1lmnXGiQt8fekiW/rMFZIu0qXPYfpfnsZUpK9pubS0tPqiwJKfv5phIl3sS4O004DwHj16eJtWYFosqFdrr712Nr3c3/72tzj11FNz7WuzzTbL+mt27do1++NX85H+6CVpbEUaHJb6aKcWkRQsPvvss6/8WimopJBy5513ZiHl5ptvzlV3WBGlz0k6UaiSTjDGjRuXa5/pM5w+e2lyhdSykQaQ1pygoUoaQ7XkchrDAdSPFCiGDBkSDz/8cPY1LScpXKQJFNK4x5pSa3+asCFJISTNHHXVVVcV3Pf06dO9bSsILRbUu3SlIoWLNAtEq1atlppFZlmlAdZpwGaaMaZq9onUPeL+++/PrnKm5RQy7rjjjthiiy2yk5xUnq54fhVpdorUMpKCydy5c7M/olV/HIEvpDnu0zSUaXB1mnQhTXyQujDmkT7D6TOd9pmudqZ9VrVm1JRObNJJSppdLg3aToNDUysj8PWk/3effPJJrbL0P7tq8pI0y2L6fKbJGNLXNKNTlfS/9sILL8wuJqaeCqlFI3Wlquph0Lt377j22muziRrS/+a0jzQrVJotKk3qkro8mnJ2xSBY0CAGDBiQzQKVwsXXPfFI01Wm2aDOOeecOOigg7KZnlZfffWsq1K6YlI1S8X3v//92HTTTWONNdbIprM966yzvtLrpKsqaZaZdJU0hZIddtghG3MB1JY+J++99142m1tqNUwzQOVtsUgnH//zP/8T2267bXZCkz7vVV0tako31UrjrtIMb2mMVDopSTPRAF9Pmh52yW5K6X93GuNUJX02f/KTn2RBoqbUUyB9TtPnMo1XTGOk0jTuaWbGKieddFJ2oTF1c0wtkrNnz87CRfr7ccYZZ3jbVhAlaQR3sSsBAMsqnYykSSHSA4DGwxgLAAAgN8ECAADITVcoAAAgNy0WAABAboIFALWk2du+6sDoNDXsgw8+mH2fZlRLy2m6SQCaD8ECAADITbAAAAByEywAWEq62/XZZ58dnTt3ju7du8dFF11UvW706NHZXXfbtWuX3Qgr3fm6kHRjrXSju7RdupP9c889V71u2rRpceSRR0aXLl2yG1GmG2mlu/VWSXfkPeyww7LX79ChQ2yxxRYxfPjwbN3YsWNj//33j27dumV37B00aFA89dRTS93rIt0gM93QK91AL90w8+abb/ZOA9QjwQKApdx+++3ZCX06mb/qqqvikksuyQJEChzpzvctW7aMYcOGxY033pjdHbuQdHfedCfe1157LQsY++23X0yZMiVbd/7558fbb78djz76aIwcOTJuuOGG7E7bycyZM2OnnXaKjz/+OLt777///e8s5KTXrlq/7777ZmEi7TvdcXvw4MExfvz4Wq+f7sadAknaJt3198QTT6x1F2EAli/TzQKw1ODthQsXxosvvlhdtuWWW8auu+6aPdJJfRqg3atXr2zdY489Fvvss0888MADccABB2Tr+vbtGz//+c+rQ8eCBQuyslNPPTULCSlkpCBxyy23LHX0U8vCWWedle0ntVgsi9QikoLDKaecUt1iscMOO8Qdd9yRLVdWVmYtLxdffHH84Ac/8I4D1AMtFgAsZaONNqq13KNHj5g8eXLWupC6FVWFimSbbbYpeARrlrdq1SprPUjPT1IIuPvuu2OTTTbJgsbLL79cvW2aTWrTTTetM1TMmjUre07qhrXKKqtk3aFSS8SSLRY1f4Y0S1UKFulnAKB+CBYALKV169a1ltOJeeqKlK78LymtW1ZV26YWjg8++CCb1jZ1edptt92yVookjbn4MqmL1X333ReXXXZZ1qqSgsiGG24Y8+bNW6afAYD6IVgAsMxSK0FqGUhhoMrQoUMLbpvGYFRJXaFGjBgR6667bnVZGrh9zDHHxJ133hm/+tWvqgdXp5aGFBamTp1acL8pTKTnHXjggVmgSC0RqdsUAMUlWACwzHbfffcYMGBAfPe7380GVaeT/J/+9KcFt/3d736XjbtI3ZROPvnkbCaoNEtTcsEFF8Tf/va3GDNmTLz11lvx8MMPx3rrrZetO/zww7OwkMZrDBkyJN57772shaIqwPTr1y/uv//+LHykOhxxxBFaIgAaAcECgGX/p9GiRRYW5s6dmw3oPu6447IuSYWkwdtXXnllbLzxxlkASUGiauanNm3axLnnnpu1TqSpa9MsU2nMRdW6J554Irp27ZoNFE+tEmlfaZvk2muvjU6dOmUzTaXZoNKsUJtttpl3EaDIzAoFAADkpsUCAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgNwECwAAIDfBAoAv9f7770dJSUm8/vrrjea1dt555zj99NPrvT4ALDvBAoBGo3fv3jFx4sQYOHBgtvzcc89lQWP69OnFrhoA/0Wr/7YBADSEefPmRZs2baJ79+4OOEATpMUCgHjsscdi++23j1VWWSVWXXXV+OY3vxljx46t88g89NBDsc4668RKK60Uu+yyS9x+++1LtSzcd999scEGG0Tbtm1jzTXXjF/+8pe19pHKfvazn8UxxxwTZWVlcfzxx9fqCpW+T/tOOnXqlJWnbassWrQozj777OjcuXMWRi666KJa+0/b33TTTdnP0r59+1hvvfVi6NChMWbMmKwrVYcOHWKbbbb50p8TgGUnWAAQs2bNijPOOCNeeeWVePrpp6NFixZx4IEHZifvS0on/N/+9rfjgAMOyALACSecED/96U9rbTNixIg45JBD4rDDDos333wzO+k///zz47bbbqu13dVXX511e0rbp/VLdotK4SQZNWpU1kXq17/+dfX6FGZSOBg+fHhcddVVcckll8STTz5Zax+XXnppfPe7383que6668YRRxyR1ffcc8+NV199NdvmlFNO8RsAsDxUAsASJk+eXJn+Rbz55puV48aNy75/7bXXsnXnnHNO5cCBA2tt/9Of/jTbZtq0adnyEUccUbnHHnvU2ubHP/5x5frrr1+93KdPn8oDDjig1jZLvtazzz5ba79Vdtppp8rtt9++VtmgQYOyulVJzzvvvPOql4cOHZqV/fGPf6wu+/Of/1zZrl077z/AcqDFAoCsO1C6mr/WWmtFaWlp9O3bNzsq48ePX+ropNaDQYMG1Srbcsstay2PHDkytttuu1plaXn06NGxcOHC6rItttjiax/9jTbaqNZyjx49YvLkyXVu061bt+zrhhtuWKtszpw5UVFR8bXrAcBiBm8DEIMHD866Hv3+97+Pnj17Zl2gUhelNKB6SakxII1fWLLsq26TpK5MX1fr1q1rLafXW7LrVs1tqupTqKxQly8AvhrBAqCZmzJlStbCkAY677DDDlnZSy+9VOf2aazCP/7xj1plVeMVqqy//vpL7ePll1+O/v37R8uWLZe5bmmWqKRmKwcAjZOuUADNXJpxKc0EdfPNN2czJj3zzDPZQO66pMHP77zzTpxzzjnx7rvvxl/+8pfqQdlVLQBnnnlmNgg8DZ5O26SB1tddd12cddZZX6luffr0yfb58MMPx6effhozZ87M+dMCUF8EC4BmLs0Adffdd2czM6XuTz/60Y+y2ZrqksZf3HvvvXH//fdnYxhuuOGG6lmh0tSyyWabbZYFjrTftM8LLrggm7Wp5nSxy2L11VePiy++OP73f/83Gw9hBieAxqskjeAudiUAaNouu+yyuPHGG2PChAnFrgoARWKMBQBf2fXXX5/NDJW6UA0ZMiRr4dCaANC8CRYAfGVp2th01+ypU6fGGmuskY2pSDedA6D50hUKAADIzeBtAAAgN8ECAADITbAAAAByEywAAIDcBAsAACA3wQIAAMhNsAAAAHITLAAAgMjr/wHvmRycAd54wAAAAABJRU5ErkJggg==",
"text/plain": [
"<Figure size 800x800 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"sns.catplot(\n",
" cifar_results[cifar_results.measure == \"Elapsed time\"], \n",
" x=\"algorithm\", \n",
" y=\"value\", \n",
" hue=\"algorithm\", \n",
" kind=\"swarm\", \n",
" col=\"measure\",\n",
" height=8,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "eface2b3-8171-4d9e-b1a1-585af3e31990",
"metadata": {},
"source": [
"As expected KMeans is noticeably quicker than UMAP + HDBSCAN. The one very long time for UMAP + HDBSCAN is due to numba's JIT compilation time (subsequent runs don't require compilation -- so we won't see this again). The surprise is that EVoC actually ran faster than KMeans here. Often KMeans is chosen simply based on runtime-constraints: it is usually faster than most other options you could pick. Here, however, we see that EVoC is not only competitive with KMeans, it's actually the faster option!\n",
"\n",
"How about the quality of the clusterings we get out?"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "6c0bcb69-473f-417f-b438-6fc5894d2159",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:42:56.204347Z",
"iopub.status.busy": "2026-03-25T20:42:56.204186Z",
"iopub.status.idle": "2026-03-25T20:42:56.875156Z",
"shell.execute_reply": "2026-03-25T20:42:56.874734Z",
"shell.execute_reply.started": "2026-03-25T20:42:56.204333Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"<seaborn.axisgrid.FacetGrid at 0x7420509d75f0>"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAPeCAYAAAARWnkoAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAA5a1JREFUeJzs3Qd4FNX6x/FfCkloCb33DiK9IygWiqIiXsCGipVrRdR7LyoW1D9W7KCoiFgAFVFUUMAGCKIgCALSe0+AhJ4Q9v+8J+6ym2yo2RT4fp5nDDttZ2bXPfPOOec9YR6PxyMAAAAAAJDlwrN+lwAAAAAAgKAbAAAAAIAQoqYbAAAAAIAQIegGAAAAACBECLoBAAAAAAgRgm4AAAAAAEKEoBsAAAAAgBAh6AYAAAAAIEQIugEAAAAACBGCbgBZ5vHHH1ejRo0yfZ2bnHfeeerXr59yqxtvvFHdunU7bd4HAPIiyrWcU6VKFb388stZus99+/bpyiuvVGxsrMLCwrRr1y7lBT/99FOeOl5kRNANIFMzZ85URESEOnfufFJX6YEHHtD333+fJwPlkSNHugLOO5UuXVqXXnqpFi1apNyAAhgAThzlWpjq1q2b4bp88sknrqyzQDevPMA+mQf777//vqZPn+6+B5s3b1ZcXJxym2DXtE2bNrn2eHF8CLqBXMrj8ejQoUM5egwjRozQ3XffrRkzZmjdunUnvH2hQoVUvHhx5VX2JNwKuU2bNumbb77R3r17dckllyg5OTmnDw0A8hzKtZxXsGBBbdu2TbNmzcpQ3leqVEmnu5UrV7qHDvXr11eZMmXcg4YTlZqaqsOHDys7RUVFnfTxIncg6MZpx54QWqBoTwmLFi3qaiiHDx/uAqY+ffqocOHCql69uiZNmhSw3eLFi3XxxRe7QNG26d27t+Lj433Lv/32W51zzjkqUqSICyS7du3qfry9LBC76667VLZsWcXExLinxYMHD3bL1qxZ434o58+f71vfmgjZPKux9K+5/O6779SsWTNFR0e7p7F2k/Lcc8+pWrVqyp8/vxo2bKjPPvss5NfRrpc9+f73v//tztVqftN75pln3LWya3rzzTfrwIEDR30KHezprTVttibOXkOHDlXNmjXdNbR9/+tf/3LzbZ2ff/5Zr7zyiq/22a7r8Xx2di7XX3+9W26fz4svvnhc18Dewwo528Y+k/vuu09r167V0qVLfesMGTJEZ599truRqVixou644w7t2bPHt9yum31n7HO1gt6OwVoOWDDvX4D379/f9936z3/+4z73E5FV73O075stu/DCC91+vdvZ99hu1B5++OETOl4Ax49yLWtQrkmRkZG65pprXJDttWHDBncPYvOP1f3IynD7Ph6tXPaWR/6++OKLgIDR7p8uv/xyV2ZbedW8eXNNnTr1lD5f7/G+8MILrty2cu7OO+9USkqKW27HbeX/tGnT3LF4z2Pnzp3uHsHuGQsUKKAuXbpo+fLlvv16z+frr79WvXr13P2Z3QvYfd5TTz3lu7+oXLmyvvzyS23fvt2dm82z+4M5c+b49pWQkKCrr75aFSpUcO9ly0ePHh1wDsGuabDWbePGjdNZZ53ljseOJf29jc37v//7P910003uPs3KarsfRs4g6MZpyZoPlShRQr/99psLwC1w7NGjh2ue88cff6hTp04uMLO+PcYCk3PPPdcFiPbjaAH21q1b1bNnz4DC2gKW33//3TWZDg8P1xVXXOF72vnqq69qwoQJLlC1oOzDDz884WZaxgIhC9aXLFmiBg0a6JFHHtF7772nYcOGuabNFvhdd9117kc5M3379nU/9kebjlVzPXbsWNWuXdtN9n52DP4Bmp3nY489pqefftpdMyvgLGA+Fbafe+65R4MGDXLX0D6H9u3bu2VWALVu3Vq33nqr+7xssiD3eD67Bx98UD/++KPGjx+vyZMnu8Jr7ty5J3RsVtB9/PHH7t/58uXzzbfvgX32f/31l/ve/fDDD+4z9GffM7sJ+OCDD1xhb9femt57WUFpN0Dvvvuua1WwY8cOd6wnKive52jfNyvw7Rzt/ys7Z+93zW6a7AELgNChXKNcy6pyzR6SWxnvvQeyoNIeptpv+YnIrFw+HvZw2h6WW6A9b948d19mXbhOplWdP7smFtDbX/t/xs7NW2nw+eefu2O1Y7ZjtdfeQNfuH+wezloA2L2OHZs3WDd2reze7J133nFlY6lSpdz8l156SW3btnXnYC3h7N7SgnArN+1+s0aNGu619/7JKieaNm3qAni7b7jtttvcNrNnzz6ha2qftX0frrrqKi1cuNCVwQMHDsxQQWLlvlUa2PFZpYDdD//999+ndI1xkjzAaebcc8/1nHPOOb7Xhw4d8hQsWNDTu3dv37zN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IegGAAAAACBECLoBAAAAAAgRgm4AAAAAAEKEoBsAAAAAgBAh6AYAAAAAIEQIugEAAAAACBGCbgAAAAAAQoSgGwAAAACAECHoBgAAAAAgRAi6AQAAAAAIEYJuAAAAAABO16B76NChqlq1qmJiYtS0aVNNnz79qOu/8cYbqlu3rvLnz6/atWtr1KhRActHjhypsLCwDNOBAwdCfCYAAAAAAASKVA4aO3as+vXr5wLvtm3b6q233lKXLl20ePFiVapUKcP6w4YN04ABA/T222+refPm+u2333TrrbeqaNGiuvTSS33rxcbGaunSpQHbWlAPAAAAAEB2CvN4PB7lkJYtW6pJkyYumPayWuxu3bpp8ODBGdZv06aNC86ff/553zwL2ufMmaMZM2b4arpt3q5du076uJKSkhQXF6fExEQXwAMAAMpQAADyVPPy5ORkzZ07Vx07dgyYb69nzpwZdJuDBw9mqLG2ZuZW452SkuKbt2fPHlWuXFkVKlRQ165dNW/evKMei+3XAm3/CQAAHBtlKAAAuTTojo+PV2pqqkqXLh0w315v2bIl6DadOnXSO++844J1q6C3Gu4RI0a4gNv2Z+rUqeNquydMmKDRo0e7IN1qx5cvX57psVitutVse6eKFStm8dkCAHB6ogwFACCXNi/ftGmTypcv72q1W7du7Zv/9NNP64MPPtDff/+dYZv9+/frzjvvdMvtsC1Av+666/Tcc89p69atKlWqVIZtDh8+7Jqwt2/fXq+++mqmT+lt8rKabgu8aV4OAMDRUYYCAJBLa7pLlCihiIiIDLXa27Zty1D77d+U3Gq29+3bpzVr1mjdunWqUqWKChcu7PYXTHh4uEu6drSa7ujoaNd3238CAADHRhkKAEAuDbqjoqLcEGFTpkwJmG+vLWHa0eTLl8/117agfcyYMa7ftgXXwViN+Pz581W2bNksPX4AAAAAAHL1kGH9+/dX79691axZM9fEfPjw4a72um/fvm65DQ+2ceNG31jcy5Ytc0nTLOv5zp07NWTIEP311196//33fft84okn1KpVK9WsWdM1E7cm5RZ02/jeAAAAAACcMUF3r169lJCQoEGDBmnz5s2qX7++Jk6c6DKPG5tnQbiXJV578cUX3RjcVtvdoUMH1yfcmph72VBht912m2u2bknRGjdurGnTpqlFixY5co4AAAAAgDNX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d97R4sWLValSpQzrz5gxQ+eee65eeuklXXrppdq4caP69u2rmjVravz48W6dWbNmuaD9ySefdIG2zX/00Ufdti1btjyu40pKSnIBfWJiomJjGQMTAIDjRRkKAMBJBN2hYkFwkyZNNGzYMN+8unXrqlu3bho8eHCG9a1G29a1Gmyv1157zdV8r1+/3r3u1auXK/AnTZrkW8dqw4sWLarRo0cf13FxwwAAwMmhDAUAIETjdJ+o5ORkzZ07Vx07dgyYb69nzpwZdJs2bdpow4YNmjhxosuevnXrVn322We65JJLfOtYTXf6fXbq1CnTfZqDBw+6mwT/CQAAHBtlKAAAWZxIzVjgO2HCBK1bt84Fz/6GDBlyXPuIj49XamqqSpcuHTDfXm/ZsiXToNv6dFtt9oEDB3To0CFddtllrrbby7Y9kX0aq1V/4oknjuu4AQAAZSgAACGr6f7+++9Vu3Zt1w/7xRdf1I8//qj33nvPjdE9f/78E92d6wfuz2qw08/zsr7elqjN+mhbLfm3337rhjCzft0nu08zYMAA13/bO3mbqgMAgKOjDAUAIItruq1wvf/++zVo0CAVLlzYZQovVaqUrr32Wtd3+niVKFFCERERGWqgt23blqGm2r9G2hKuPfjgg+51gwYNVLBgQZc47amnnlLZsmVVpkyZE9qniY6OdhMAADgxlKEAAGRxTfeSJUt0ww03uH9HRkZq//79KlSokAvCn3322ePeT1RUlJo2baopU6YEzLfX1ow8mH379ik8PPCQLXA33nxwrVu3zrDPyZMnZ7pPAAAAAAByTU231Sxb0hRTrlw5l0ncxtf29tM+Ef3793dDhjVr1swFy8OHD3f9xL3Nxa1W3YYFGzVqlHttw4TdeuutLoO5JUezsb379eunFi1auGMx9957r9q3b+8eAFx++eX68ssvNXXqVDdkGADg9LI3Za+iIqKULzxfTh8KAABA1gTdrVq10i+//KJ69eq5rOHW1HzhwoX6/PPP3bITYQnREhISXC25BdD169d3mckrV67slts8C8K9brzxRu3evVuvv/66e98iRYro/PPPD6hhtxrtMWPG6JFHHtHAgQNVvXp1Nx748Y7RDSCH7NshbfhdKlhCKt+UjwFHNXfrXL3w+wv6K+EvFYgsoG41uql/s/6KjohWSmqKRi0epUmrJ+nQ4UO6oPIFuqn+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bNmy7smrNWsePHiwrzC1p8nz58/3rW9Plm2ePWn2f+JsBWyzZs3cU24Ldq0wswLYnujmz59fDRs21GeffRby62jvX6ZMGfek/ZprrtG1117rq131NiEbMWKE74m8Hee6det0+eWXu2sYGxurnj17auvWrb59erd766233H4LFCigHj16BDxlP3z4sAYNGqQKFSq4/dr6du29vNfyk08+cZ+1XesPP/zQfbaJiYm+Gnp7r2DNy4/3GD/44AO3bVxcnK666irt3r37qNfrl19+0bnnnuvOyb53nTp10s6dO4+7ptq+V1Zbf6zvkv3bXHHFFW4/3tfmq6++UtOmTd029rk88cQTOnToUMD7vvnmm+78CxYsqKeeeuq4tlu+fLnat2/vltsNzpQpU456LQCcWSh3T90dd9zhfqMtmPzXv/6lWrVq6ayzzlL//v3166+/HndNtd1n2DwrK83atWtdbbmVS/a7b/ucOHGiW96hQwe3ji2zbayVmznWfUdm9yvpm5fb/rp166YXXnjBlWd272QPEPwflG/evFmXXHKJe5+qVavq448/Pma3MCuzHnroIXfPZuta+WX3fTfccINvnYMHD+o///mPu9ew46tZs6beffdd3/Kff/5ZLVq0cMvs2P73v/8FlHv2nbZy2K5/iRIldNFFFx3XvSKQHkE3Tmvvv/+++5G0wst+iP/973+74K5Nmzb6448/XEBkP5T79u3z/ehbwGSFxZw5c1yQZ4GYBWReFrTbj681V7bmU+Hh4S7wsSDRvPrqq5owYYILBpcuXeoCQf+A6HhZIWEB1pIlS9yT7kceecTVOFsN86JFi3TffffpuuuucwXG0Z4Ce584ZzZZ8HkirED0LyhXrFjhznXcuHG+hwlWuO7YscMdmwVm9lCiV69eAfvxbmeFpl1n29YKYa9XXnlFL774oiukFyxY4D6ryy67zAV+/qxp2T333OOu0wUXXOAKaAui7bO0yZq5pWc3EsdzjDbPgmJrqmaTrfvMM89kem3sHOwY7GZm1qxZmjFjhrvJsSf/J+No3yX7/hn7Tth5el/bzY99L+ya2E2BPdiwIP7pp58O2Pdjjz3mgu6FCxfqpptuOuZ29v3u3r27IiIi3I2fBe127QHAH+XuyZe7ViZZeWhloQXG6QXrOnW8bJ8WgE6bNs397j/77LPuWCwYtfLbWDlj5YmVv+Z47zvS368E8+OPP7oy1f7ad8TKF+8DZnP99ddr06ZNLpC347FKkm3bth31nKxCwB4cHO1huO13zJgxrjy147Oyy1sLv3HjRhc4N2/eXH/++ac7TwvIvQ+ivex4rdWDPVS3svF47hWBDDzAaercc8/1nHPOOb7Xhw4d8hQsWNDTu3dv37zNmzd77H+DWbNmudcDBw70dOzYMWA/69evd+ssXbo06Pts27bNLV+4cKF7fffdd3vOP/98z+HDhzOsu3r1arfuvHnzfPN27tzp5v3444/utf2111988YVvnT179nhiYmI8M2fODNjfzTff7Ln66qszvQZbt271LF++/KhTSkpKptvfcMMNnssvv9z3evbs2Z7ixYt7evbs6V4/9thjnnz58rlr4DV58mRPRESEZ926db55ixYtcuf022+/+bazdezaek2aNMkTHh7uPhNTrlw5z9NPPx1wPM2bN/fccccdAdfy5ZdfDljnvffe88TFxWU4l8qVK3teeumlEzrGAgUKeJKSknzrPPjgg56WLVtmer3ss2jbtu1Rv5P33nuv77W93/jx4wPWsWO3czjWdymz7du1a+f5v//7v4B5H3zwgads2bIB2/Xr1++Etvvuu++CfmbBjgHAmYly99TKXStj7Tf1888/P+a19v/t9d432P2El91n2DwrK83ZZ5/tefzxx4PuK9j2x3PfEex+xVt+NmzYMOBewspguw/z6tGjh6dXr17u30uWLHH7+f33333L7TrZPG+5HczPP//sqVChgrsPadasmSvXZsyY4Vtu9222jylTpgTd/qGHHvLUrl07oIx94403PIUKFfKkpqb6vtONGjUK2O5k7hUB+nTjtOb/xNVq6KxJ09lnn+2bZ02CjPdp6ty5c91T2GB9kewJrTXzsr8DBw50tX3WlMhbw21PruvXr++aUVnzo9q1a7v+0Nb8vGPHjid87NZUy8tqHg8cOOBr1uRlzY8bN26c6T5KlSrlplNhNbx2Pay5ldVwW+3oa6+95lteuXLlgP7S9iTZnpzb5GVNke0JvS2zJ8qmUqVKrum4V+vWrd21tCft1jTbnnhbvzZ/9tqeRmd2nY7X8R6j1SpbNwQva3p2tCfvVtNtLSmyysl8l+w7bLXe/jXbVtNu3x9r0WHXNth1O9Z2dl2CfWYA4I9y9+TL3bRYOq0LUFazVkzW2m/y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9VcdOHDAflkdNWqUDY5MQGS+yK5cuVL9+/e3y969e+3+p06dUtu2bW1gY45ZunSp/bf59czZffP48eO677777BfeP/74wwbMprumKU/OBPjmev755x+73QTZR44csdteeOEF28o0e/Zsbdq0SWPHjrVBPDI/Exyb99/atWtVpUoVGyg//PDDGjJkiFatWmX3+d///pe0/4kTJ+z7wwTaa9asUadOndS1a1eFhYW5nPftt99WrVq17A885lxPPfWU5s+fn+bPD2mvZs2a9seYlD/MmeC5UaNGqlGjht5//33bWv3OO+/YzxzzPrr55pttUG58//339jPs2Wef9fgY5kdJZG/UocgIqENxrVGHpjEHsqTWrVs7nnzyScdHH33kCA4Odvz6669J2+677z5HaGioIz4+Pqnsuuuuc7Rs2TJp/dy5c448efI4Jk2aZNcnTJhg90lISEjaJzY21hEYGOiYO3eux2sw58iXL59j5syZSWXmLTds2LCk9RMnTjh8fHwcs2fPtutdu3Z19O3b95rdB6QN83567733XMpq167teOmllzy+7itWrLBl5n3lZN5rAQEBF3ycatWqOcaMGePyuJ07d3bZp2fPno4uXbpc9XNCxmA+r/z8/OznUfJl+PDhdvvYsWPt+vHjx+26+WvWx40bZ9dLlCjheO2111zO2bBhQ8ejjz5q//3II484goKC0vx5IWOjDkVaog6Ft1CHZhy0dGdhZmyiyWA+b94820qdXPXq1eXre/7lN114zS9eTn5+frYLeWRkpF1fvXq17QqcL18+28JtFtMN/cyZM0ldgs2+pnW8cuXKtnu5WUxrZcqWSdMq6WS6FZtzOh/nkUcesa0KpguoaXlavny5l+4O0lry192834zk7zlTZt5PMTExdv3kyZP2PWASWpnWRvOe27x5s9v7yXRLT7luekkg6zCfX6aHRPLlscces9vuuusu24V8ypQpdt38Nb/z3Hnnnfa9ZHr0NG/e3OV8Zt35HjH7muEPQErUochIqENxpahDM4Yc6X0B8B4TuJout6brpemCmfyLZc6cOV32Nds8lZkvs4b5W79+fX3zzTduj2PGXxtmrPihQ4fs2N7Q0FA7hsQEQCmzB1/ocbp06aI9e/bol19+sd2Kr7/+evvl2nQNRcZlfsBJmczq7Nmzqb7uzveipzLne8HkF5g7d6597StWrGjH+N92222XlI2aICprMT/OmfeAJ+bHPfO+MJ9zZgy3+WvWg4KCkn7ASfl+SB5omx8JTcI0k5ytePHiafBskFlQhyKtUIfCm6hDMwZaurOwChUq2GlwfvrpJz3++ONXda569erZMZBFixa1X36TL+ZLr2HGcptkRGYcrmlJN0H34cOHL/uxTBBvAvivv/7aBvDjx4+/qmuH95nXzAQtTibY2bVr11Wd07yfzPvAJN0zLeImeVbyxH1OJn9AynUzZhzZhwm2ly1bpp9//tn+NeuGCbxNcj2TgyI504PGJPMzTIBuMpy/9dZbHs997NixNHgGyIioQ5FWqEORnqhD0wYt3VmcacUxgbfJyJsjRw63DNOXyiQ7MwmrTDZpZyZg0833hx9+sC2SZt0E4F999ZUaNGhggy5TblonL4fJFGxa1E3QHhsba79EO78cI+MycyabqZtMojOTjM8kxDNDFK6GeT+Z95c5p2mVNOd0toInZ4IsEzCZjPwmgZpJjGV6SiDrMJ8FERERLmXm88yZZNHMlGDeLyZRn/lrMo87mc+hl156yQZQpuXStISb7unOXjulS5fWe++9Z5P4mc8tcw6TSdhkNTfJJ82wBqYNy76oQ5EWqEPhTdShGQNBdzZw3XXX2WzlJvC+0kDITPNkspYPHjxYt9xyi81IXrJkSdv927QmOTMGP/TQQ6pbt67KlCljpyQbNGjQZT2OaXEyGahNi6YJ2Fu2bGnHeCNjM6/Zzp07bQZ80/PBZFm92pZuEwjdf//9atasmQ2uzHvP2V04uaefftrmHDBZ8U1+ABMgmQzVyDrMFF8pu36bzzUzxt/JvFeef/55G2QnZ3rfmPeNeZ+Y3BEmR4CZptDMruD06KOP2uDKDGUwPStOnz5tA2/zfh44cGAaPENkZNSh8DbqUHgTdWjG4GOyqaX3RQDAlTCBkUkWaBYAAEAdCmREjOkGAAAAAMBLCLoBAAAAAPASupcDAAAAAOAltHQDAAAAAOAlBN0ALshkvb/cRGVmiq8ff/zR/ttkojfrZpomAACyC+pPAE4E3QAAAAAAeAlBNwAAAAAAXkLQDeCiEhIS9Oyzz6pgwYIqVqyYXn755aRt27ZtU6tWrRQQEKBq1app/vz5Hs+xefNmNWvWzO5XvXp1LVq0KGnb0aNHdffdd6tIkSIKDAxUpUqV9NlnnyVt37dvn+688077+Hny5FGDBg20cuVKu23Hjh3q1q2bQkJClDdvXjVs2FALFixwm8/79ddf1/333698+fKpTJkyGj9+PK88AMCrqD8BGATdAC7qiy++sMGuCXTfeustDR8+3AbX5svELbfcIj8/P/3xxx/65JNPNHjwYI/neOaZZ/T0009rzZo1Nvi++eabFRUVZbe98MIL2rhxo2bPnq1NmzZp7NixKly4sN124sQJtW7dWgcOHNCMGTO0bt06+wOAeWzn9htuuMEG2ubcnTp1UteuXRUWFuby+O+++64N1s0+jz76qB555BH7QwAAAN5C/QnAcgDABbRu3drRokULl7KGDRs6Bg8e7Jg7d67Dz8/PsXfv3qRts2fPdpiPlunTp9v1Xbt22fU33ngjaZ+zZ886SpUq5XjzzTfteteuXR19+/b1+Pjjxo1z5MuXzxEVFXXJr1O1atUcY8aMSVoPDQ113HPPPUnrCQkJjqJFizrGjh3Law8A8ArqTwBOtHQDuKhatWq5rBcvXlyRkZG2Vdp01S5VqlTStqZNm3o8R/LyHDly2FZnc7xhWp0nT56sOnXq2Fbs5cuXJ+1rsp7XrVvXdi335OTJk/YY07U9f/78tou5acFO2dKd/DmYbOqmm7x5DgAAeAv1JwCDoBvAReXMmdNl3QStpnu3w2EaseW27VI59+3SpYv27NljpyYz3civv/56DRo0yG4zY7wvxHRbnzZtml577TUtWbLEBuk1a9ZUXFzcJT0HAAC8hfoTgEHQDeCKmdZl06JsAmWnFStWeNzXjPl2OnfunFavXq0qVaoklZkkan369NHXX3+t0aNHJyU6M60EJpA+cuSIx/OaQNsc16NHDxtsmxZsMzc4AAAZFfUnkL0QdAO4Yu3bt9d1112ne++91yY4MwHw0KFDPe770Ucfafr06bbr92OPPWYzlpts4saLL76on376Sdu3b9eGDRv0888/q2rVqnbbXXfdZQPp7t27a9myZdq5c6dt2XYG9xUrVtQPP/xgA3NzDb169aIFGwCQoVF/AtkLQTeAK/8A8fW1gXRsbKwaNWqkBx54wHbz9uSNN97Qm2++qdq1a9vg3ATZzgzl/v7+GjJkiG3VNtOPmWzoZoy3c9u8efNUtGhRm6XctGabc5l9jPfee08FChSwGdFN1nKTvbxevXq8qgCADIv6E8hefEw2tfS+CAAAAAAAsiJaugEAAAAA8BKCbgAAAAAAvISgGwAAAAAALyHoBgAAAADASwi6AQAAAADwEoJuAAAAAAC8hKAbAAAAAAAvIegGAAAAAMBLCLoBXLLdu3fLx8dHa9euzTCP1aZNGw0YMMDr1wMAwNWgDgWyL4JuABlS6dKlFR4erho1atj1RYsW2SD82LFj6X1pAABkaNShQMaSI70vAABSiouLk7+/v4oVK8bNAQDgMlCHAhkPLd0AXMyZM0ctWrRQ/vz5VahQId10003asWNHqndpxowZqlSpkgIDA9W2bVt98cUXbi3S06ZNU/Xq1ZUrVy6VLVtW7777rss5TNmIESPUp08fBQcH68EHH3Tphmf+bc5tFChQwJabfZ0SEhL07LPPqmDBgjZQf/nll13Ob/YfN26cfS65c+dW1apVtWLFCm3fvt12T8+TJ4+aNm16wecJAMDFUIcC8MgBAMlMnTrVMW3aNMfWrVsda9ascXTt2tVRs2ZNR3x8vGPXrl0O87Fhyg2znjNnTsegQYMcmzdvdkyaNMlRsmRJu8/Ro0ftPqtWrXL4+vo6hg8f7tiyZYvjs88+cwQGBtq/TqGhoY6goCDH22+/7di2bZtdkj/WuXPn7DWZdXOO8PBwx7Fjx+yxrVu3tse+/PLL9pq/+OILh4+Pj2PevHlJ5zfHmeuaMmWKPb579+6OsmXLOtq1a+eYM2eOY+PGjY4mTZo4OnfuzHsBAHDFqEMBeELQDeCCIiMjbdC6fv16t6B78ODBjho1arjsP3ToUJegu1evXo4OHTq47PPMM884qlWr5hJ0m0A4uZSP9dtvv7mc18kE3S1atHApa9iwob22pA86yTFs2LCk9RUrVtiyCRMmJJWZHwwCAgJ4NwAArhnqUAAG3csBuDBdrHv16qXy5csrKChI5cqVs+VhYWFud2rLli1q2LChS1mjRo1c1jdt2qTmzZu7lJn1bdu2KT4+PqmsQYMGV/xK1KpVy2W9ePHiioyMTHWfkJAQ+7dmzZouZWfOnFFMTMwVXwcAIHujDqUOBTwhkRoAF127drVZT//v//5PJUqUsOOlTQZxk5glJdOIbMZLpyy73H0MM676SuXMmdNl3Tyeue7U9nFej6eylMcBAHCpqEOpQwFPCLoBJImKirIt0ybpWMuWLW3Z0qVLU71DVapU0axZs1zKVq1a5bJerVo1t3MsX75clStXlp+f3yXffZPN3EjeOg4AQEZBHQogNXQvB5DEZAY3GcvHjx9vM3v/+uuvGjhwYKp36OGHH9bmzZs1ePBgbd26Vd99950+//xzl5bjp59+WgsXLtSrr75q9zHZzT/88EMNGjTosu58aGioPefPP/+sQ4cO6cSJE7xyAIAMgzoUQGoIugGc/0Dw9dXkyZO1evVq26X8qaee0ttvv53qHTLjvadOnaoffvjBjpkeO3ashg4dareZ6cGMevXq2WDcnNec88UXX9Tw4cNdpvy6FCVLltQrr7yi5557zo6//t///scrBwDIMKhDAaTG57/MvgBwTbz22mv65JNPtHfvXu4oAADUoUC2x5huAFfl448/thnMTbf0ZcuW2ZZxWqEBAKAOBZCIoBvAVTFTf40YMUJHjhxRmTJl7BjuIUOGcFcBAKAOBUD3cgAAAAAAvIdEagAAAAAAeAlBNwAAAAAAXkLQDQAAAACAlxB0AwAAAADgJQTdAAAAAAB4CUE3AAAAAABeQtANAAAAAICXEHQDAAAAAOAlBN0AAAAAAMg7/h/DTwIGfgoUKwAAAABJRU5ErkJggg==",
"text/plain": [
"<Figure size 1000x1000 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"sns.catplot(\n",
" cifar_results[cifar_results.measure != \"Elapsed time\"], \n",
" x=\"algorithm\", \n",
" y=\"value\", \n",
" hue=\"algorithm\", \n",
" col=\"measure\", \n",
" kind=\"swarm\", \n",
" col_wrap=2,\n",
" height=5,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "02e215d9-75bf-4395-9dfd-08b36606eff8",
"metadata": {},
"source": [
"Here we seem some of KMeans weakness -- it can run very fast, but the quality of the clusters is not always that great, particularly for very high dimensional data such as that produced by embedding models. The one upside of KMeans is that it does cluster all of your data, so the \"proportion clustered\" is perfect, and this significantly improves the clustering score since the other algoirithms only clustered around 80% of the data (but found much cleaner more accurate clusters by doing so). In contrast UMAP + HDBSCAN was much slower than KMeans, and we hope it managed to produce better clusters by doing so. And that is largely born out here. The ARI and AMI for UMAP + HDBSCAN are significantly better than KMeans, so we produced better clusters by expending more compute. Of course this did require not clustering 20% of the data -- but that 20% is likely the \"hard to classify\" samples that would simply make the ARI and AMI a lot worse if we tried to force them to be assigned somewhere. That means that on clustering score UMAP + HDBSCAN comes out a little ahead of KMeans. Last we have EVoC, which ran faster than KMeans and yet somehow also produced better clusters than UMAP + HDBSCAN. EVoC's edge in clustering quality over UMAP + HDBSCAN is not huge here, but it is relevant. The real strength of EVoC on this dataset is the raw speed with which it can produce those good results."
]
},
{
"cell_type": "markdown",
"id": "92b4f389-c9b3-4c57-ac92-c2be35f12133",
"metadata": {},
"source": [
"## Text embeddings\n",
"\n",
"For a text dataset we'll use the venerable 20-newsgroups dataset. The 20-newsgroups dataset is a dataset of NNTP newsgroup posts from the 1990s to twenty different newsgroup sections (think subreddits if you never used NNTP). As with the image dataset we have computed text embeddings ahead of time and put the dataset on Huggingface datasets for ease of access. This should be a challenging dataset to get good matches with the class labels on -- while the newsgroups are mostly distinct in topic, posts can easily run off-topic, be very short, or have more text from signature blocks that weren't properly stripped than actual content. That means the data can be very noisy, with a lot of posts that can be very hard to cluster with other posts from the same newsgroup. To make matters worse there are some overarching categories of newsgroups (there are multiple tech/computing newsgroups included, and multiple discussion groups for religion and politics that can tend to have overlaps). Natural clustering may not line up well with the full set of twenty distinct labels provided. "
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "71bad192-181f-4586-8241-d2674a5101ce",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:42:56.875997Z",
"iopub.status.busy": "2026-03-25T20:42:56.875696Z",
"iopub.status.idle": "2026-03-25T20:43:02.962330Z",
"shell.execute_reply": "2026-03-25T20:43:02.961712Z",
"shell.execute_reply.started": "2026-03-25T20:42:56.875982Z"
}
},
"outputs": [],
"source": [
"ds_news = load_dataset(\"lmcinnes/evoc_bench_20newsgroups\")\n",
"news_data = np.asarray(ds_news[\"train\"][\"embedding\"])\n",
"news_target = np.asarray(ds_news[\"train\"][\"target\"])"
]
},
{
"cell_type": "markdown",
"id": "22404769-36a7-4aff-bc63-49e1262daed1",
"metadata": {},
"source": [
"Okay, let's run the benchmarks. Due to the noisiness careful parameter selection was required. EVoC will just make use of it's \"pick the best layer\" approach; KMeans again benefits from asking for more clusters than classes to help force it to break up some of the meta-categories to better match with the class labels. UMAP + HDBSCAN required careful tuning of ``min_cluster_size`` to get good results."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "7d5837d7-97b5-4c25-b596-1fb6b6e5a7d0",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:43:02.963427Z",
"iopub.status.busy": "2026-03-25T20:43:02.963202Z",
"iopub.status.idle": "2026-03-25T20:45:35.272333Z",
"shell.execute_reply": "2026-03-25T20:45:35.271486Z",
"shell.execute_reply.started": "2026-03-25T20:43:02.963411Z"
}
},
"outputs": [],
"source": [
"news_results = run_dataset_benchmarks(\n",
" news_data, \n",
" news_target, \n",
" n_runs=32, \n",
" kmeans_kwargs={\"n_clusters\":25}, \n",
" umap_hdbscan_kwargs={\n",
" \"min_samples\":5,\n",
" \"min_cluster_size\":180, \n",
" \"metric\":\"cosine\", \n",
" \"cluster_selection_method\":\"leaf\"\n",
" }\n",
")"
]
},
{
"cell_type": "markdown",
"id": "a6357957-1518-497f-b3e9-d54583be7dcc",
"metadata": {},
"source": [
"Again, let's start with the time taken. Since we have fewer samples everything will be faster, and since we are asking KMeans for fewer clusters we also expect it to be faster as well. What do we see in practice?"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "b64c2dee-159b-48cd-b7c0-cfb5766d4cc4",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-25T20:45:35.273371Z",
"iopub.status.busy": "2026-03-25T20:45:35.273197Z",
"iopub.status.idle": "2026-03-25T20:45:35.523724Z",
"shell.execute_reply": "2026-03-25T20:45:35.523041Z",
"shell.execute_reply.started": "2026-03-25T20:45:35.273356Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"<seaborn.axisgrid.FacetGrid at 0x74201cd48e30>"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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n6/ly5enrJ8yZYo6d+7svimx5+rYsaOmT58esg/7tuXdd9/VBRdc4L4ZqV27tn744YeU9du2bVPPnj1VunRpFSxY0K23X5IAwqtWrZpeffXVkGUnnHCCHn/88ZTPnF08nHvuue4zV79+fU2cOFGLFy92XSYLFy6stm3basmSJSmPt/+3z7f9fihSpIhatWql33//Pd3zPvnkk7r88svdNhUqVNAbb7zB2wRk8nwUdtGf+mZ/g6+++mr3OR0zZkzI9mPHjtWiRYvc+oMHD6pfv37u77ntx34v2LVA0OrVq3Xbbbe5m/29t98H9rnu0KGD+zv96KOP8l7mIgQLZJrBgwfrkksucaGid+/ebtmIESO0du1a90vp5ZdfdhchduFhv7AmT56sG264wd1WrVrltt+9e7c6derkLiDsMePGjXP/b9+mBJtbd+zYoT59+rhfbJMmTXKhwJpXbXlqFmLseGbNmuXWW5DYunWrW/fII4+4b0yGDh2qefPmacCAAS6oADh6FgDssz9z5kzVq1fPhYHrr79effv21dSpU902t9xyS8r2O3fudJ9NCxMzZszQmWeeqa5du2rlypUh+33hhRfUpEkT9wWC7evOO+/Ub7/9xlsFZLHGjRu7wJ/2izcLCK1bt1ajRo302muvuVaHF1980f29tc/xeeed54KH+eqrr9zf7/vuuy/sc9iXjshFbII84Gh17NgxcPvttwf++9//BuLi4gIjRoxIWdenT59A1apVA0lJSSnL6tatG2jfvn3K/QMHDgQKFy4c+Pzzz9399957z21z8ODBlG0SExMDBQsWDAwfPjzsMdg+ihYtGvjxxx9TltmP9sMPP5xyf+fOnYGoqKjA0KFD3f2uXbsGrrrqKt544AjZZ/mVV14JWda0adPAY489FvYzN3HiRLfMPtNB9jkvUKDAYZ+nQYMGgTfeeCPkec8666yQbS699NLA2WefzXsHZAL7Wx0dHe3+Fqe+9evXz60fMGCAu79jxw533/61+wMHDnT3K1SoEHj66adD9tmqVavATTfd5P7/xhtvDMTGxvJeHSdosYA36y9pI0P9+uuvrrUhtYYNGypPnkM/Ztblwb4BCYqOjnbdnTZu3OjuT5s2zXWdKFq0qGupsJt1mdq7d29KFwrb1lo56tSp47pC2c2++Uz7Lad9wxlk3TBsn8HnufHGG10LizXZ2rcoEyZM4CcB8JT6M2efdZP6827L7LOckJDg7u/atct9/qyI0761tM/7/Pnz032WrQtV2vvW0gggc9jfbmtpTH27+eab3boePXq47k5ffPGFu2//2ncJl112mfssW6+Ek08+OWR/dj/4GbVtraskjg8xkT4A5Hx2cW5dFKyp1JpMU/8CyZs3b8i2ti7cMvulZezfFi1a6H//+1+657F6CGO1G5s2bXL9vatWrer6dNqFRtqRKQ73PGeffbZWrFihn3/+2XXDOO2009wvUWvKBZCefUGQtoBz//79GX7mgr8Hwi0Lfg6t1mr48OHuc1erVi1X79S9e/cjGmWGCxUg89iXb/YZDMe+vLPPpf2Nt5oK+9fux8bGpnxJkPbzmDpM2JeAVqRtBeHly5fnbcvlaLGAt5o1a7ph5L7//nvdeuutXvtq3ry565dZpkwZ90su9c1+uRmrrbAiMOubbS0iFiw2b978r5/LgoqFlE8//dSFlEGDBnkdO5Cb2efFLgyC7IJi2bJlXvu0z7J9Bm2QBWvZsILR1AM1BFktVdr7VsMB4NiwQDF+/Hj99NNP7l+7byxc2IAKVg+ZmvUCsAEcjIUQGznq+eefD7vv7du3H4NXgGOFFgtkCvtGwsKFjfYQExOTbvSYI2UF1laoaSPFBEeZsG4R33zzjft20+5byPjkk0/UsmVLd3Fjy+2bzn/DRqGwlhELJomJie6XZfCXIID0bEx7G3bSiqtt8AUbAMG6Mvqwz7J9tm2f9u2m7TPYmpGaXcjYRYmNMmdF21YMaq2NADKH/R1cv359yDL7Wx4c1MRGX7TPqw3OYP/aiE5B9jf4sccec18yWg8Ga9GwrlTBngeVK1fWK6+84gZusL/Ztg8bFcpGi7LBXqwLJEPO5h4EC2SaunXrulGgLFwc7QWHDVNpo0Hdf//9uvDCC91ITxUrVnRdleybkeBoFNddd52aNWumKlWquOFs77nnnn/1PPbtiY0uY9+OWihp3769q7kAEJ59XpYuXepGdbPWQxsByrfFwi42/vOf/+ikk05yFzD2uQ92rUjNJtGy+isb6c1qpewixEaeAZA5bHjYtN2U7G+61TwF2Wf1wQcfdEEiNetBYJ9b+5xaHaPVTNnw7jZiY9BNN93kvoC0bo/WQrlnzx4XLuz3yV133cXbmItEWQV3pA8CAIBw7OLDBoewGwAge6PGAgAAAIA3ggUAAAAAb3SFAgAAAOCNFgsAAAAA3ggWAHCcs5Hc/m1xtA0P+91337n/t9HV7L4NMQkAOH4RLAAAAAB4I1gAAAAA8EawAAC4Ga/vu+8+lShRQuXKldPjjz+eclYWLVrkZtotUKCAm/zKZr8OxybTssnubDub1X7UqFEp67Zt26aePXuqdOnSblJKmzzLZugNsll4L7vsMvf8hQsXVsuWLTV58mS3bsmSJTr//PNVtmxZN0tvq1at9Pvvv6eb78Imy7RJvGwSPZs8c9CgQbyzAHAMESwAAProo4/cBb1dzD///PPq16+fCxAWOC688EJFR0dr0qRJevvtt90M2eHYjLw2++6MGTNcwDjvvPO0ZcsWt+6RRx7R3LlzNXT
gitextract_hnrqw2lv/ ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.rst ├── azure-pipelines.yml ├── doc/ │ ├── Makefile │ ├── README.md │ ├── build_docs.bat │ ├── build_docs.sh │ ├── requirements.txt │ └── source/ │ ├── _static/ │ │ └── custom.css │ ├── api/ │ │ ├── evoc.cluster_trees.rst │ │ ├── evoc.clustering.rst │ │ ├── evoc.clustering_utilities.rst │ │ ├── generated/ │ │ │ ├── evoc.EVoC.rst │ │ │ ├── evoc.boruvka.parallel_boruvka.rst │ │ │ ├── evoc.cluster_trees.condense_tree.rst │ │ │ ├── evoc.cluster_trees.extract_leaves.rst │ │ │ ├── evoc.cluster_trees.get_cluster_label_vector.rst │ │ │ ├── evoc.cluster_trees.get_point_membership_strength_vector.rst │ │ │ ├── evoc.cluster_trees.mst_to_linkage_tree.rst │ │ │ ├── evoc.clustering_utilities.binary_search_for_n_clusters.rst │ │ │ ├── evoc.clustering_utilities.build_cluster_tree.rst │ │ │ ├── evoc.clustering_utilities.find_duplicates.rst │ │ │ ├── evoc.clustering_utilities.find_peaks.rst │ │ │ ├── evoc.clustering_utilities.select_diverse_peaks.rst │ │ │ ├── evoc.evoc_clusters.rst │ │ │ ├── evoc.graph_construction.neighbor_graph_matrix.rst │ │ │ ├── evoc.knn_graph.knn_graph.rst │ │ │ ├── evoc.label_propagation.label_propagation_init.rst │ │ │ ├── evoc.node_embedding.node_embedding.rst │ │ │ └── evoc.numba_kdtree.build_kdtree.rst │ │ └── index.rst │ ├── benchmarks.ipynb │ ├── changelog.rst │ ├── conf.py │ ├── examples.rst │ ├── index.rst │ ├── installation.rst │ ├── quickstart.rst │ └── user_guide.rst ├── evoc/ │ ├── __init__.py │ ├── boruvka.py │ ├── cluster_trees.py │ ├── clustering.py │ ├── clustering_utilities.py │ ├── common_nndescent.py │ ├── disjoint_set.py │ ├── float_nndescent.py │ ├── graph_construction.py │ ├── int8_nndescent.py │ ├── knn_graph.py │ ├── label_propagation.py │ ├── nested_parallelism.py │ ├── node_embedding.py │ ├── numba_kdtree.py │ ├── tests/ │ │ ├── test_boruvka.py │ │ ├── test_cluster_trees.py │ │ ├── test_clustering.py │ │ ├── test_knn_graph.py │ │ ├── test_knn_graph_performance.py │ │ ├── test_numba_kdtree.py │ │ └── test_numba_kdtree_performance.py │ └── uint8_nndescent.py ├── pyproject.toml ├── pytest.ini ├── scripts/ │ └── run_performance_tests.py └── setup.py
SYMBOL INDEX (371 symbols across 23 files)
FILE: evoc/boruvka.py
function merge_components (line 24) | def merge_components(
function update_component_vectors (line 87) | def update_component_vectors(tree, disjoint_set, node_components, point_...
function component_aware_query_recursion (line 147) | def component_aware_query_recursion(
function boruvka_tree_query (line 299) | def boruvka_tree_query(tree, node_components, point_components, core_dis...
function calculate_block_size (line 334) | def calculate_block_size(n_components, n_points, num_threads):
function update_component_bounds_from_block (line 369) | def update_component_bounds_from_block(
function boruvka_tree_query_reproducible (line 404) | def boruvka_tree_query_reproducible(
function initialize_boruvka_from_knn (line 485) | def initialize_boruvka_from_knn(
function parallel_boruvka (line 535) | def parallel_boruvka(tree, n_threads, min_samples=10, reproducible=False):
FILE: evoc/cluster_trees.py
function create_linkage_merge_data (line 12) | def create_linkage_merge_data(base_size):
function linkage_merge_find (line 23) | def linkage_merge_find(linkage_merge, node):
function linkage_merge_join (line 40) | def linkage_merge_join(linkage_merge, left, right):
function mst_to_linkage_tree (line 50) | def mst_to_linkage_tree(sorted_mst):
function bfs_from_hierarchy (line 83) | def bfs_from_hierarchy(hierarchy, bfs_root, num_points):
function eliminate_branch (line 101) | def eliminate_branch(
function condense_tree (line 138) | def condense_tree(hierarchy, min_cluster_size=10):
function extract_leaves (line 268) | def extract_leaves(condensed_tree, allow_single_cluster=True):
function score_condensed_tree_nodes (line 286) | def score_condensed_tree_nodes(condensed_tree):
function cluster_tree_from_condensed_tree (line 313) | def cluster_tree_from_condensed_tree(condensed_tree):
function mask_condensed_tree (line 323) | def mask_condensed_tree(condensed_tree, mask):
function unselect_below_node (line 332) | def unselect_below_node(node, cluster_tree, selected_clusters):
function eom_recursion (line 339) | def eom_recursion(node, cluster_tree, node_scores, selected_clusters):
function extract_eom_clusters (line 359) | def extract_eom_clusters(condensed_tree, cluster_tree, allow_single_clus...
function cluster_epsilon_search (line 381) | def cluster_epsilon_search(clusters, cluster_tree, min_persistence=0.0):
function traverse_upwards (line 401) | def traverse_upwards(cluster_tree, min_persistence, root, segment):
function segments_in_branch (line 413) | def segments_in_branch(cluster_tree, segment):
function in_set_parallel (line 429) | def in_set_parallel(values, targets):
function get_cluster_labelling_at_cut (line 437) | def get_cluster_labelling_at_cut(linkage_tree, cut, min_cluster_size):
function get_single_cluster_label_vector (line 475) | def get_single_cluster_label_vector(
function get_cluster_label_vector (line 502) | def get_cluster_label_vector(
function max_lambdas (line 539) | def max_lambdas(tree, clusters):
function get_point_membership_strength_vector (line 551) | def get_point_membership_strength_vector(tree, clusters, labels):
FILE: evoc/clustering.py
function build_cluster_layers (line 35) | def build_cluster_layers(
function evoc_clusters (line 191) | def evoc_clusters(
class EVoC (line 413) | class EVoC(BaseEstimator, ClusterMixin):
method __init__ (line 540) | def __init__(
method fit_predict (line 574) | def fit_predict(self, X, y=None, **fit_params):
method fit (line 646) | def fit(self, X, y=None, **fit_params):
method cluster_tree_ (line 677) | def cluster_tree_(self):
FILE: evoc/clustering_utilities.py
function find_peaks (line 33) | def find_peaks(x):
function _binary_search_for_n_clusters (line 65) | def _binary_search_for_n_clusters(uncondensed_tree, approx_n_clusters, n...
function binary_search_for_n_clusters (line 142) | def binary_search_for_n_clusters(
function min_cluster_size_barcode (line 165) | def min_cluster_size_barcode(cluster_tree, n_points, min_size):
function compute_total_persistence (line 194) | def compute_total_persistence(births, deaths, lambda_deaths):
function extract_clusters_by_id (line 229) | def extract_clusters_by_id(condensed_tree, selected_ids):
function jaccard_similarity (line 243) | def jaccard_similarity(set_a_array, set_b_array):
function estimate_cluster_similarity (line 259) | def estimate_cluster_similarity(births, deaths, birth_a, birth_b):
function select_diverse_peaks (line 284) | def select_diverse_peaks(
function _build_cluster_tree (line 333) | def _build_cluster_tree(labels):
function build_cluster_tree (line 360) | def build_cluster_tree(labels):
function find_duplicates (line 373) | def find_duplicates(knn_inds, knn_dists):
FILE: evoc/common_nndescent.py
function seed (line 6) | def seed(rng_state, seed):
function tau_rand_int (line 12) | def tau_rand_int(state):
function tau_rand (line 38) | def tau_rand(state):
function make_heap (line 55) | def make_heap(n_points, size):
function siftdown (line 65) | def siftdown(heap1, heap2, elt):
function deheap_sort (line 89) | def deheap_sort(indices, distances):
function build_candidates_heap_push (line 132) | def build_candidates_heap_push(priorities, indices, p, n):
function build_candidates (line 183) | def build_candidates(current_graph, max_candidates, rng_state, n_threads):
function flagged_heap_push (line 296) | def flagged_heap_push(priorities, indices, flags, p, n):
function apply_graph_update_array (line 372) | def apply_graph_update_array(
function apply_sorted_graph_updates (line 433) | def apply_sorted_graph_updates(
FILE: evoc/disjoint_set.py
function ds_rank_create (line 22) | def ds_rank_create(n_elements):
function ds_size_create (line 29) | def ds_size_create(n_elements):
function ds_find (line 36) | def ds_find(disjoint_set, x):
function ds_union_by_rank (line 54) | def ds_union_by_rank(disjoint_set, x, y):
function ds_union_by_size (line 77) | def ds_union_by_size(disjoint_set, x, y):
FILE: evoc/float_nndescent.py
function fast_cosine (line 43) | def fast_cosine(x, y):
function float_random_projection_split (line 95) | def float_random_projection_split(data, indices, rng_state):
function make_float_tree (line 221) | def make_float_tree(
function make_float_leaf_array_parallel (line 286) | def make_float_leaf_array_parallel(data, rng_state, leaf_size=30, max_de...
function make_float_leaf_array_serial (line 330) | def make_float_leaf_array_serial(data, rng_state, leaf_size=30, max_dept...
function make_float_forest_no_nested_parallelism (line 371) | def make_float_forest_no_nested_parallelism(data, rng_states, leaf_size,...
function make_float_forest_with_nested_parallelism (line 390) | def make_float_forest_with_nested_parallelism(data, rng_states, leaf_siz...
function make_float_forest (line 399) | def make_float_forest(data, rng_states, leaf_size=30, max_depth=200):
function generate_leaf_updates_float (line 433) | def generate_leaf_updates_float(
function init_rp_tree_float (line 501) | def init_rp_tree_float(data, current_graph, leaf_array, n_threads):
function init_random_float (line 572) | def init_random_float(n_neighbors, data, heap, rng_state):
function generate_graph_update_array_float_basic (line 612) | def generate_graph_update_array_float_basic(
function generate_graph_update_array_float (line 722) | def generate_graph_update_array_float(
function generate_sorted_graph_update_array_float (line 872) | def generate_sorted_graph_update_array_float(
function nn_descent_float (line 1005) | def nn_descent_float(
function nn_descent_float_sorted (line 1120) | def nn_descent_float_sorted(
FILE: evoc/graph_construction.py
function smooth_knn_dist (line 24) | def smooth_knn_dist(distances, k, n_iter=64, bandwidth=1.0):
function compute_membership_strengths (line 90) | def compute_membership_strengths(
function neighbor_graph_matrix (line 124) | def neighbor_graph_matrix(
FILE: evoc/int8_nndescent.py
function fast_int_inner_product_dissimilarity (line 40) | def fast_int_inner_product_dissimilarity(x, y):
function int8_random_projection_split (line 73) | def int8_random_projection_split(data, indices, rng_state):
function make_int8_tree (line 205) | def make_int8_tree(
function make_int8_leaf_array_parallel (line 254) | def make_int8_leaf_array_parallel(data, rng_state, leaf_size=30, max_dep...
function make_int8_leaf_array_serial (line 296) | def make_int8_leaf_array_serial(data, rng_state, leaf_size=30, max_depth...
function make_int8_forest_no_nested_parallelism (line 336) | def make_int8_forest_no_nested_parallelism(data, rng_states, leaf_size, ...
function make_int8_forest_with_nested_parallelism (line 355) | def make_int8_forest_with_nested_parallelism(data, rng_states, leaf_size...
function make_int8_forest (line 364) | def make_int8_forest(data, rng_states, leaf_size=30, max_depth=200):
function generate_leaf_updates_int8 (line 397) | def generate_leaf_updates_int8(
function init_rp_tree_int8 (line 458) | def init_rp_tree_int8(data, current_graph, leaf_array, n_threads):
function init_random_int8 (line 529) | def init_random_int8(n_neighbors, data, heap, rng_state):
function generate_graph_update_array_int8 (line 558) | def generate_graph_update_array_int8(
function generate_sorted_graph_update_array_int8 (line 655) | def generate_sorted_graph_update_array_int8(
function nn_descent_int8 (line 776) | def nn_descent_int8(
function nn_descent_int8_sorted (line 891) | def nn_descent_int8_sorted(
FILE: evoc/knn_graph.py
function ts (line 25) | def ts():
function make_forest (line 29) | def make_forest(
function nn_descent (line 93) | def nn_descent(
function knn_graph (line 188) | def knn_graph(
FILE: evoc/label_propagation.py
function label_prop_iteration (line 17) | def label_prop_iteration(
function original_label_prop_iteration (line 63) | def original_label_prop_iteration(
function label_outliers (line 107) | def label_outliers(indptr, indices, labels, rng_state):
function remap_labels (line 139) | def remap_labels(labels):
function label_prop_loop (line 157) | def label_prop_loop(
function original_label_prop_loop (line 172) | def original_label_prop_loop(
function label_propagation_init (line 189) | def label_propagation_init(
FILE: evoc/nested_parallelism.py
function supports_safe_nesting (line 6) | def supports_safe_nesting():
FILE: evoc/node_embedding.py
function make_epochs_per_sample (line 10) | def make_epochs_per_sample(weights, n_epochs):
function rdist (line 28) | def rdist(x, y):
function clip (line 39) | def clip(val, lo, hi):
function node_embedding_epoch (line 66) | def node_embedding_epoch(
function node_embedding_epoch_repr (line 150) | def node_embedding_epoch_repr(
function node_embedding (line 231) | def node_embedding(
FILE: evoc/numba_kdtree.py
function kdtree_to_numba (line 34) | def kdtree_to_numba(sklearn_kdtree):
function _init_node (line 59) | def _init_node(
function _find_node_split_dim (line 113) | def _find_node_split_dim(data, idx_array, idx_start, idx_end):
function _compare_indices (line 144) | def _compare_indices(data, axis, idx1, idx2):
function _insertion_sort_indices (line 172) | def _insertion_sort_indices(data, idx_array, axis, left, right):
function _sift_down_indices (line 194) | def _sift_down_indices(data, idx_array, axis, offset, start, end):
function _heapsort_indices (line 236) | def _heapsort_indices(data, idx_array, axis, left, right):
function _median_of_three_pivot (line 260) | def _median_of_three_pivot(data, idx_array, axis, left, right):
function _partition_indices (line 293) | def _partition_indices(data, idx_array, axis, left, right, pivot_idx):
function _introselect_impl (line 341) | def _introselect_impl(data, idx_array, axis, left, right, nth, depth_lim...
function _introselect (line 377) | def _introselect(data, idx_array, axis, left, right, nth):
function _recursive_build_tree (line 394) | def _recursive_build_tree(
function build_kdtree (line 458) | def build_kdtree(data, leaf_size=40):
function rdist (line 518) | def rdist(x, y):
function point_to_node_lower_bound_rdist (line 545) | def point_to_node_lower_bound_rdist(upper, lower, pt):
function simple_heap_push (line 572) | def simple_heap_push(priorities, indices, p, n):
function siftdown (line 626) | def siftdown(heap1, heap2, elt):
function deheap_sort (line 647) | def deheap_sort(distances, indices):
function tree_query_recursion (line 684) | def tree_query_recursion(
function parallel_tree_query (line 759) | def parallel_tree_query(
FILE: evoc/tests/test_boruvka.py
class TestMergeComponents (line 28) | class TestMergeComponents:
method test_merge_components_basic (line 31) | def test_merge_components_basic(self):
method test_merge_components_empty (line 59) | def test_merge_components_empty(self):
method test_merge_components_best_edge_selection (line 76) | def test_merge_components_best_edge_selection(self):
class TestUpdateComponentVectors (line 113) | class TestUpdateComponentVectors:
method simple_tree_and_components (line 117) | def simple_tree_and_components(self):
method test_update_component_vectors_basic (line 148) | def test_update_component_vectors_basic(self, simple_tree_and_componen...
method test_update_component_vectors_leaf_nodes (line 165) | def test_update_component_vectors_leaf_nodes(self, simple_tree_and_com...
class TestBoruvkaTreeQuery (line 197) | class TestBoruvkaTreeQuery:
method query_test_data (line 201) | def query_test_data(self):
method test_boruvka_tree_query_basic (line 223) | def test_boruvka_tree_query_basic(self, query_test_data):
method test_boruvka_tree_query_reproducible (line 256) | def test_boruvka_tree_query_reproducible(self, query_test_data):
method test_boruvka_query_different_block_sizes (line 293) | def test_boruvka_query_different_block_sizes(self, query_test_data):
class TestInitializeBoruvkaFromKNN (line 330) | class TestInitializeBoruvkaFromKNN:
method test_initialize_boruvka_from_knn_basic (line 333) | def test_initialize_boruvka_from_knn_basic(self):
method test_initialize_boruvka_core_distance_constraint (line 370) | def test_initialize_boruvka_core_distance_constraint(self):
class TestCalculateBlockSize (line 406) | class TestCalculateBlockSize:
method test_calculate_block_size_basic (line 409) | def test_calculate_block_size_basic(self):
method test_calculate_block_size_extremes (line 429) | def test_calculate_block_size_extremes(self):
class TestParallelBoruvka (line 446) | class TestParallelBoruvka:
method boruvka_test_data (line 450) | def boruvka_test_data(self):
method test_parallel_boruvka_basic (line 465) | def test_parallel_boruvka_basic(self, boruvka_test_data):
method test_parallel_boruvka_reproducible (line 489) | def test_parallel_boruvka_reproducible(self, boruvka_test_data):
method test_parallel_boruvka_vs_non_reproducible (line 508) | def test_parallel_boruvka_vs_non_reproducible(self, boruvka_test_data):
method test_parallel_boruvka_single_point (line 530) | def test_parallel_boruvka_single_point(self):
method test_parallel_boruvka_two_points (line 541) | def test_parallel_boruvka_two_points(self):
method test_parallel_boruvka_different_min_samples (line 561) | def test_parallel_boruvka_different_min_samples(self, boruvka_test_data):
method test_parallel_boruvka_different_num_threads (line 587) | def test_parallel_boruvka_different_num_threads(self, boruvka_test_data):
class TestEdgeCases (line 624) | class TestEdgeCases:
method test_empty_data_handling (line 627) | def test_empty_data_handling(self):
method test_single_dimension_data (line 641) | def test_single_dimension_data(self):
method test_high_dimensional_data (line 653) | def test_high_dimensional_data(self):
FILE: evoc/tests/test_cluster_trees.py
class TestLinkageMergeData (line 28) | class TestLinkageMergeData:
method test_create_linkage_merge_data (line 31) | def test_create_linkage_merge_data(self):
method test_linkage_merge_find_and_join (line 48) | def test_linkage_merge_find_and_join(self):
class TestMSTToLinkageTree (line 75) | class TestMSTToLinkageTree:
method simple_mst (line 79) | def simple_mst(self):
method test_mst_to_linkage_tree_basic (line 88) | def test_mst_to_linkage_tree_basic(self, simple_mst):
method test_mst_to_linkage_tree_ordering (line 105) | def test_mst_to_linkage_tree_ordering(self, simple_mst):
method test_mst_to_linkage_tree_random (line 113) | def test_mst_to_linkage_tree_random(self):
class TestBFSFromHierarchy (line 133) | class TestBFSFromHierarchy:
method simple_hierarchy (line 137) | def simple_hierarchy(self):
method test_bfs_leaf_node (line 153) | def test_bfs_leaf_node(self, simple_hierarchy):
method test_bfs_internal_node (line 158) | def test_bfs_internal_node(self, simple_hierarchy):
method test_bfs_root_node (line 165) | def test_bfs_root_node(self, simple_hierarchy):
class TestCondenseTree (line 173) | class TestCondenseTree:
method sample_hierarchy (line 177) | def sample_hierarchy(self):
method test_condense_tree_basic (line 188) | def test_condense_tree_basic(self, sample_hierarchy):
method test_condense_tree_min_cluster_size_effect (line 205) | def test_condense_tree_min_cluster_size_effect(self, sample_hierarchy):
method test_condense_tree_lambda_values (line 215) | def test_condense_tree_lambda_values(self, sample_hierarchy):
class TestExtractLeaves (line 224) | class TestExtractLeaves:
method test_extract_leaves_simple (line 227) | def test_extract_leaves_simple(self):
method test_extract_leaves_hierarchical (line 241) | def test_extract_leaves_hierarchical(self):
class TestClusterLabeling (line 263) | class TestClusterLabeling:
method sample_condensed_tree (line 267) | def sample_condensed_tree(self):
method test_get_cluster_label_vector_single_cluster (line 275) | def test_get_cluster_label_vector_single_cluster(self, sample_condense...
method test_get_cluster_label_vector_multiple_clusters (line 291) | def test_get_cluster_label_vector_multiple_clusters(self, sample_conde...
method test_get_point_membership_strength_vector (line 303) | def test_get_point_membership_strength_vector(self, sample_condensed_t...
class TestIntegrationWithRealData (line 324) | class TestIntegrationWithRealData:
method clustered_data (line 328) | def clustered_data(self):
method test_full_pipeline_simple_mst (line 335) | def test_full_pipeline_simple_mst(self, clustered_data):
method test_score_condensed_tree_nodes (line 371) | def test_score_condensed_tree_nodes(self):
class TestEdgeCases (line 387) | class TestEdgeCases:
method test_extract_leaves_empty_tree (line 390) | def test_extract_leaves_empty_tree(self):
method test_single_point_mst (line 403) | def test_single_point_mst(self):
method test_zero_distance_edges (line 411) | def test_zero_distance_edges(self):
class TestBFSEdgeCases (line 429) | class TestBFSEdgeCases:
method test_bfs_single_point_hierarchy (line 432) | def test_bfs_single_point_hierarchy(self):
method test_eliminate_branch_leaf (line 440) | def test_eliminate_branch_leaf(self):
function test_cluster_trees_integration (line 461) | def test_cluster_trees_integration():
function test_linkage_merge_data_comprehensive (line 496) | def test_linkage_merge_data_comprehensive():
FILE: evoc/tests/test_clustering.py
function simple_embedding_data (line 34) | def simple_embedding_data():
function complex_embedding_data (line 46) | def complex_embedding_data():
function small_embedding_data (line 60) | def small_embedding_data():
function duplicate_embedding_data (line 72) | def duplicate_embedding_data():
function quantized_embedding_data (line 84) | def quantized_embedding_data():
function binary_embedding_data (line 94) | def binary_embedding_data():
function small_linkage_tree (line 103) | def small_linkage_tree():
class TestBinarySearchForNClusters (line 116) | class TestBinarySearchForNClusters:
method test_binary_search_basic (line 119) | def test_binary_search_basic(self, small_linkage_tree):
method test_binary_search_edge_cases (line 144) | def test_binary_search_edge_cases(self, small_linkage_tree):
method test_binary_search_wrapper_function (line 160) | def test_binary_search_wrapper_function(self, simple_embedding_data):
class TestBuildClusterLayers (line 181) | class TestBuildClusterLayers:
method test_build_cluster_layers_basic (line 184) | def test_build_cluster_layers_basic(self, simple_embedding_data):
method test_build_cluster_layers_with_base_n_clusters (line 206) | def test_build_cluster_layers_with_base_n_clusters(self, simple_embedd...
method test_build_cluster_layers_reproducible (line 224) | def test_build_cluster_layers_reproducible(self, simple_embedding_data):
class TestFindDuplicates (line 242) | class TestFindDuplicates:
method test_find_duplicates_basic (line 245) | def test_find_duplicates_basic(self):
method test_find_duplicates_no_duplicates (line 279) | def test_find_duplicates_no_duplicates(self):
class TestBuildClusterTree (line 293) | class TestBuildClusterTree:
method test_build_cluster_tree_basic (line 296) | def test_build_cluster_tree_basic(self):
method test_build_cluster_tree_empty (line 323) | def test_build_cluster_tree_empty(self):
method test_build_cluster_tree_single_layer (line 331) | def test_build_cluster_tree_single_layer(self):
class TestEvocClusters (line 338) | class TestEvocClusters:
method test_evoc_clusters_basic (line 341) | def test_evoc_clusters_basic(self, simple_embedding_data):
method test_evoc_clusters_with_approx_n_clusters (line 367) | def test_evoc_clusters_with_approx_n_clusters(self, simple_embedding_d...
method test_evoc_clusters_with_duplicates (line 389) | def test_evoc_clusters_with_duplicates(self, duplicate_embedding_data):
method test_evoc_clusters_different_data_types (line 411) | def test_evoc_clusters_different_data_types(
class TestEVoCClass (line 452) | class TestEVoCClass:
method test_evoc_init (line 455) | def test_evoc_init(self):
method test_evoc_fit_predict (line 472) | def test_evoc_fit_predict(self, simple_embedding_data):
method test_evoc_fit (line 496) | def test_evoc_fit(self, simple_embedding_data):
method test_evoc_with_approx_n_clusters (line 513) | def test_evoc_with_approx_n_clusters(self, simple_embedding_data):
method test_evoc_cluster_tree_property (line 528) | def test_evoc_cluster_tree_property(self, simple_embedding_data):
method test_evoc_cluster_tree_not_fitted (line 542) | def test_evoc_cluster_tree_not_fitted(self):
method test_evoc_reproducibility (line 549) | def test_evoc_reproducibility(self, simple_embedding_data):
class TestClusteringQuality (line 568) | class TestClusteringQuality:
method test_clustering_quality_on_embeddings (line 571) | def test_clustering_quality_on_embeddings(self, simple_embedding_data):
method test_clustering_quality_on_blobs (line 603) | def test_clustering_quality_on_blobs(self):
method test_clustering_on_small_dataset (line 639) | def test_clustering_on_small_dataset(self):
method test_clustering_on_high_dimensional_data (line 664) | def test_clustering_on_high_dimensional_data(self):
method test_edge_case_single_cluster (line 682) | def test_edge_case_single_cluster(self):
method test_parameter_validation (line 703) | def test_parameter_validation(self):
method test_clustering_on_clip_like_embeddings (line 716) | def test_clustering_on_clip_like_embeddings(self):
method test_clustering_on_sentence_transformer_like_embeddings (line 764) | def test_clustering_on_sentence_transformer_like_embeddings(self):
method test_clustering_on_quantized_embeddings (line 800) | def test_clustering_on_quantized_embeddings(self, quantized_embedding_...
method test_clustering_on_binary_embeddings (line 820) | def test_clustering_on_binary_embeddings(self, binary_embedding_data):
FILE: evoc/tests/test_knn_graph.py
class TestUtilityFunctions (line 26) | class TestUtilityFunctions:
method test_ts_returns_string (line 29) | def test_ts_returns_string(self):
method test_ts_consistency (line 53) | def test_ts_consistency(self):
method test_constants (line 65) | def test_constants(self):
class TestMakeForest (line 72) | class TestMakeForest:
method float_data (line 76) | def float_data(self):
method uint8_data (line 86) | def uint8_data(self):
method int8_data (line 92) | def int8_data(self):
method test_make_forest_float32 (line 97) | def test_make_forest_float32(self, float_data):
method test_make_forest_uint8 (line 112) | def test_make_forest_uint8(self, uint8_data):
method test_make_forest_int8 (line 127) | def test_make_forest_int8(self, int8_data):
method test_make_forest_default_leaf_size (line 142) | def test_make_forest_default_leaf_size(self, float_data):
method test_make_forest_different_max_depth (line 156) | def test_make_forest_different_max_depth(self, float_data):
method test_make_forest_exception_handling (line 192) | def test_make_forest_exception_handling(self, mock_make_float_forest, ...
class TestNNDescent (line 210) | class TestNNDescent:
method float_data (line 214) | def float_data(self):
method uint8_data (line 223) | def uint8_data(self):
method int8_data (line 229) | def int8_data(self):
method test_nn_descent_float32 (line 234) | def test_nn_descent_float32(self, float_data):
method test_nn_descent_uint8 (line 270) | def test_nn_descent_uint8(self, uint8_data):
method test_nn_descent_int8 (line 304) | def test_nn_descent_int8(self, int8_data):
class TestKNNGraph (line 338) | class TestKNNGraph:
method float_test_data (line 342) | def float_test_data(self):
method uint8_test_data (line 351) | def uint8_test_data(self):
method int8_test_data (line 357) | def int8_test_data(self):
method test_knn_graph_float_data (line 362) | def test_knn_graph_float_data(self, float_test_data):
method test_knn_graph_uint8_data (line 383) | def test_knn_graph_uint8_data(self, uint8_test_data):
method test_knn_graph_int8_data (line 397) | def test_knn_graph_int8_data(self, int8_test_data):
method test_knn_graph_parameters (line 409) | def test_knn_graph_parameters(self, float_test_data):
method test_knn_graph_default_parameters (line 430) | def test_knn_graph_default_parameters(self, float_test_data):
method test_knn_graph_n_jobs_setting (line 439) | def test_knn_graph_n_jobs_setting(self, float_test_data):
method test_knn_graph_auto_parameters (line 458) | def test_knn_graph_auto_parameters(self, float_test_data):
method test_knn_graph_warning_on_failure (line 472) | def test_knn_graph_warning_on_failure(self, float_test_data):
method test_knn_graph_data_validation (line 485) | def test_knn_graph_data_validation(self):
method test_knn_graph_float_normalization (line 493) | def test_knn_graph_float_normalization(self):
method test_knn_graph_zero_norm_handling (line 506) | def test_knn_graph_zero_norm_handling(self):
class TestIntegration (line 520) | class TestIntegration:
method test_small_dataset_complete_pipeline (line 523) | def test_small_dataset_complete_pipeline(self):
method test_reproducibility (line 544) | def test_reproducibility(self):
method test_different_data_types_consistency (line 554) | def test_different_data_types_consistency(self):
FILE: evoc/tests/test_knn_graph_performance.py
class PerformanceMetrics (line 28) | class PerformanceMetrics:
method __init__ (line 31) | def __init__(self):
method _get_hardware_info (line 35) | def _get_hardware_info(self) -> Dict[str, Any]:
method record_metric (line 59) | def record_metric(self, test_name: str, metric_name: str, value: float):
method get_metric (line 65) | def get_metric(self, test_name: str, metric_name: str) -> float:
function time_execution (line 71) | def time_execution():
function time_function (line 79) | def time_function(func, *args, **kwargs) -> Tuple[Any, float]:
class TestKNNGraphPerformance (line 87) | class TestKNNGraphPerformance:
method perf_metrics (line 91) | def perf_metrics(self):
method dataset_config (line 102) | def dataset_config(self, request):
method performance_data (line 108) | def performance_data(self, dataset_config):
method test_knn_graph_scaling_performance (line 129) | def test_knn_graph_scaling_performance(self, performance_data, perf_me...
method test_knn_graph_parameter_performance (line 178) | def test_knn_graph_parameter_performance(self, perf_metrics):
method test_knn_graph_data_type_performance (line 230) | def test_knn_graph_data_type_performance(self, perf_metrics):
method test_knn_graph_threading_performance (line 281) | def test_knn_graph_threading_performance(self, perf_metrics):
method test_memory_usage_scaling (line 349) | def test_memory_usage_scaling(self, perf_metrics):
method test_reproducibility_performance (line 407) | def test_reproducibility_performance(self, perf_metrics):
class TestPerformanceRegression (line 460) | class TestPerformanceRegression:
method test_baseline_performance_check (line 463) | def test_baseline_performance_check(self):
FILE: evoc/tests/test_numba_kdtree.py
class TestKDTreeCompatibility (line 16) | class TestKDTreeCompatibility:
method test_data (line 27) | def test_data(self, request):
method leaf_size (line 34) | def leaf_size(self, request):
method test_tree_structure_compatibility (line 38) | def test_tree_structure_compatibility(self, test_data, leaf_size):
method test_node_partitioning_equivalence (line 72) | def test_node_partitioning_equivalence(self, test_data, leaf_size):
method test_data_ordering_equivalence (line 125) | def test_data_ordering_equivalence(self, test_data, leaf_size):
method test_query_results_compatibility (line 205) | def test_query_results_compatibility(self, test_data, leaf_size):
method test_tree_bounds_compatibility (line 263) | def test_tree_bounds_compatibility(self, test_data, leaf_size):
class TestKDTreeEdgeCases (line 282) | class TestKDTreeEdgeCases:
method test_single_point (line 285) | def test_single_point(self):
method test_duplicate_points (line 296) | def test_duplicate_points(self):
method test_high_dimensional_data (line 323) | def test_high_dimensional_data(self):
function test_full_pipeline_compatibility (line 345) | def test_full_pipeline_compatibility():
FILE: evoc/tests/test_numba_kdtree_performance.py
function time_function (line 38) | def time_function(func, *args, **kwargs) -> Tuple[Any, float]:
class KDTreePerformanceMetrics (line 46) | class KDTreePerformanceMetrics:
method __init__ (line 49) | def __init__(self):
method _get_hardware_info (line 53) | def _get_hardware_info(self) -> Dict[str, Any]:
method record_metric (line 76) | def record_metric(self, test_name: str, metric_name: str, value: float):
method get_metric (line 82) | def get_metric(self, test_name: str, metric_name: str) -> float:
class TestKDTreePerformance (line 88) | class TestKDTreePerformance:
method perf_metrics (line 92) | def perf_metrics(self):
method dataset_config (line 104) | def dataset_config(self, request):
method performance_data (line 110) | def performance_data(self, dataset_config):
method test_kdtree_construction_performance (line 132) | def test_kdtree_construction_performance(self, performance_data, perf_...
method test_kdtree_query_performance_large_batch (line 187) | def test_kdtree_query_performance_large_batch(self, performance_data, ...
method test_kdtree_query_performance_massive_batch (line 276) | def test_kdtree_query_performance_massive_batch(
method test_kdtree_accuracy_comparison (line 353) | def test_kdtree_accuracy_comparison(self, performance_data, perf_metri...
method test_kdtree_scaling_performance (line 419) | def test_kdtree_scaling_performance(self, perf_metrics):
method test_kdtree_different_k_values (line 473) | def test_kdtree_different_k_values(self, perf_metrics):
method test_kdtree_query_batch_scaling (line 526) | def test_kdtree_query_batch_scaling(self, perf_metrics):
method test_kdtree_query_performance_ultra_large_batch (line 608) | def test_kdtree_query_performance_ultra_large_batch(self, perf_metrics):
class TestKDTreeRegressionBaseline (line 687) | class TestKDTreeRegressionBaseline:
method test_kdtree_baseline_performance (line 690) | def test_kdtree_baseline_performance(self):
FILE: evoc/uint8_nndescent.py
function popcnt_u8 (line 29) | def popcnt_u8(typingctx, val):
function popcnt_u64 (line 48) | def popcnt_u64(typingctx, val):
function fast_bit_jaccard (line 77) | def fast_bit_jaccard(x, y):
function load_u64_from_u8_array (line 96) | def load_u64_from_u8_array(typingctx, arr, offset):
function fast_bit_jaccard_u64 (line 135) | def fast_bit_jaccard_u64(x, y):
function uint8_random_projection_split (line 188) | def uint8_random_projection_split(data, indices, rng_state):
function make_uint8_tree (line 315) | def make_uint8_tree(
function make_uint8_leaf_array_parallel (line 364) | def make_uint8_leaf_array_parallel(data, rng_state, leaf_size=30, max_de...
function make_uint8_leaf_array_serial (line 406) | def make_uint8_leaf_array_serial(data, rng_state, leaf_size=30, max_dept...
function make_uint8_forest_no_nested_parallelism (line 446) | def make_uint8_forest_no_nested_parallelism(data, rng_states, leaf_size,...
function make_uint8_forest_with_nested_parallelism (line 465) | def make_uint8_forest_with_nested_parallelism(data, rng_states, leaf_siz...
function make_uint8_forest (line 474) | def make_uint8_forest(data, rng_states, leaf_size=30, max_depth=200):
function generate_leaf_updates_uint8 (line 507) | def generate_leaf_updates_uint8(
function init_rp_tree_uint8 (line 568) | def init_rp_tree_uint8(data, current_graph, leaf_array, n_threads):
function init_random_uint8 (line 639) | def init_random_uint8(n_neighbors, data, heap, rng_state):
function generate_graph_update_array_uint8 (line 668) | def generate_graph_update_array_uint8(
function generate_sorted_graph_update_array_uint8 (line 765) | def generate_sorted_graph_update_array_uint8(
function nn_descent_uint8 (line 886) | def nn_descent_uint8(
function nn_descent_uint8_sorted (line 1001) | def nn_descent_uint8_sorted(
FILE: scripts/run_performance_tests.py
function run_performance_tests (line 18) | def run_performance_tests(output_file=None, verbose=False):
function generate_performance_report (line 63) | def generate_performance_report(test_results_file, output_file):
function check_performance_regression (line 107) | def check_performance_regression(current_file, baseline_file, threshold=...
function main (line 180) | def main():
Condensed preview — 68 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,223K chars).
[
{
"path": ".gitignore",
"chars": 3271,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": ".readthedocs.yaml",
"chars": 883,
"preview": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html f"
},
{
"path": "LICENSE",
"chars": 1334,
"preview": "BSD 2-Clause License\n\nCopyright (c) 2024, Tutte Institute for Mathematics and Computing\n\nRedistribution and use in sourc"
},
{
"path": "README.rst",
"chars": 4334,
"preview": ".. image:: doc/evoc_logo_horizontal.png\n :width: 600\n :align: center\n :alt: EVōC Logo\n\n====\nEVōC\n====\n\nEVōC (pronounc"
},
{
"path": "azure-pipelines.yml",
"chars": 6800,
"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": "doc/Makefile",
"chars": 1239,
"preview": "# Makefile for Sphinx documentation\n\n# You can set these variables from the command line\nSPHINXOPTS =\nSPHINXBUILD ="
},
{
"path": "doc/README.md",
"chars": 3788,
"preview": "# EVoC Documentation\n\nThis directory contains the Sphinx documentation for EVoC.\n\n## Structure\n\n```\ndoc/\n├── build/ "
},
{
"path": "doc/build_docs.bat",
"chars": 1046,
"preview": "@echo off\nREM Documentation build script for EVoC (Windows)\n\necho Building EVoC Documentation\necho ====================="
},
{
"path": "doc/build_docs.sh",
"chars": 1310,
"preview": "#!/bin/bash\n\n# Documentation build script for EVoC\n\nset -e # Exit on any error\n\necho \"Building EVoC Documentation\"\necho"
},
{
"path": "doc/requirements.txt",
"chars": 333,
"preview": "# Sphinx documentation requirements\nsphinx>=7.0.0\nsphinx-rtd-theme>=2.0.0\nnumpydoc>=1.6.0\nnbsphinx>=0.9.0\nipython>=8.0.0"
},
{
"path": "doc/source/_static/custom.css",
"chars": 1656,
"preview": "/* Custom CSS for EVoC documentation */\n\n/* Improve code block styling */\n.highlight {\n background-color: #f8f8f8;\n "
},
{
"path": "doc/source/api/evoc.cluster_trees.rst",
"chars": 128,
"preview": "evoc.cluster_trees\n==================\n\n.. automodule:: evoc.cluster_trees\n :members:\n :undoc-members:\n :show-inher"
},
{
"path": "doc/source/api/evoc.clustering.rst",
"chars": 119,
"preview": "evoc.clustering\n===============\n\n.. automodule:: evoc.clustering\n :members:\n :undoc-members:\n :show-inheritance:\n"
},
{
"path": "doc/source/api/evoc.clustering_utilities.rst",
"chars": 151,
"preview": "evoc.clustering_utilities \n=========================\n\n.. automodule:: evoc.clustering_utilities\n :members:\n :undoc-"
},
{
"path": "doc/source/api/generated/evoc.EVoC.rst",
"chars": 394,
"preview": "evoc.EVoC\n=========\n\n.. currentmodule:: evoc\n\n.. autoclass:: EVoC\n\n \n .. automethod:: __init__\n\n \n .. rubric:: "
},
{
"path": "doc/source/api/generated/evoc.boruvka.parallel_boruvka.rst",
"chars": 131,
"preview": "evoc.boruvka.parallel\\_boruvka\n==============================\n\n.. currentmodule:: evoc.boruvka\n\n.. autofunction:: paral"
},
{
"path": "doc/source/api/generated/evoc.cluster_trees.condense_tree.rst",
"chars": 142,
"preview": "evoc.cluster\\_trees.condense\\_tree\n==================================\n\n.. currentmodule:: evoc.cluster_trees\n\n.. autofu"
},
{
"path": "doc/source/api/generated/evoc.cluster_trees.extract_leaves.rst",
"chars": 145,
"preview": "evoc.cluster\\_trees.extract\\_leaves\n===================================\n\n.. currentmodule:: evoc.cluster_trees\n\n.. auto"
},
{
"path": "doc/source/api/generated/evoc.cluster_trees.get_cluster_label_vector.rst",
"chars": 179,
"preview": "evoc.cluster\\_trees.get\\_cluster\\_label\\_vector\n===============================================\n\n.. currentmodule:: evo"
},
{
"path": "doc/source/api/generated/evoc.cluster_trees.get_point_membership_strength_vector.rst",
"chars": 217,
"preview": "evoc.cluster\\_trees.get\\_point\\_membership\\_strength\\_vector\n=========================================================="
},
{
"path": "doc/source/api/generated/evoc.cluster_trees.mst_to_linkage_tree.rst",
"chars": 164,
"preview": "evoc.cluster\\_trees.mst\\_to\\_linkage\\_tree\n==========================================\n\n.. currentmodule:: evoc.cluster_"
},
{
"path": "doc/source/api/generated/evoc.clustering_utilities.binary_search_for_n_clusters.rst",
"chars": 214,
"preview": "evoc.clustering\\_utilities.binary\\_search\\_for\\_n\\_clusters\n==========================================================="
},
{
"path": "doc/source/api/generated/evoc.clustering_utilities.build_cluster_tree.rst",
"chars": 180,
"preview": "evoc.clustering\\_utilities.build\\_cluster\\_tree\n===============================================\n\n.. currentmodule:: evo"
},
{
"path": "doc/source/api/generated/evoc.clustering_utilities.find_duplicates.rst",
"chars": 169,
"preview": "evoc.clustering\\_utilities.find\\_duplicates\n===========================================\n\n.. currentmodule:: evoc.cluste"
},
{
"path": "doc/source/api/generated/evoc.clustering_utilities.find_peaks.rst",
"chars": 154,
"preview": "evoc.clustering\\_utilities.find\\_peaks\n======================================\n\n.. currentmodule:: evoc.clustering_utili"
},
{
"path": "doc/source/api/generated/evoc.clustering_utilities.select_diverse_peaks.rst",
"chars": 186,
"preview": "evoc.clustering\\_utilities.select\\_diverse\\_peaks\n=================================================\n\n.. currentmodule::"
},
{
"path": "doc/source/api/generated/evoc.evoc_clusters.rst",
"chars": 98,
"preview": "evoc.evoc\\_clusters\n===================\n\n.. currentmodule:: evoc\n\n.. autofunction:: evoc_clusters"
},
{
"path": "doc/source/api/generated/evoc.graph_construction.neighbor_graph_matrix.rst",
"chars": 183,
"preview": "evoc.graph\\_construction.neighbor\\_graph\\_matrix\n================================================\n\n.. currentmodule:: e"
},
{
"path": "doc/source/api/generated/evoc.knn_graph.knn_graph.rst",
"chars": 118,
"preview": "evoc.knn\\_graph.knn\\_graph\n==========================\n\n.. currentmodule:: evoc.knn_graph\n\n.. autofunction:: knn_graph"
},
{
"path": "doc/source/api/generated/evoc.label_propagation.label_propagation_init.rst",
"chars": 183,
"preview": "evoc.label\\_propagation.label\\_propagation\\_init\n================================================\n\n.. currentmodule:: e"
},
{
"path": "doc/source/api/generated/evoc.node_embedding.node_embedding.rst",
"chars": 148,
"preview": "evoc.node\\_embedding.node\\_embedding\n====================================\n\n.. currentmodule:: evoc.node_embedding\n\n.. a"
},
{
"path": "doc/source/api/generated/evoc.numba_kdtree.build_kdtree.rst",
"chars": 136,
"preview": "evoc.numba\\_kdtree.build\\_kdtree\n================================\n\n.. currentmodule:: evoc.numba_kdtree\n\n.. autofunctio"
},
{
"path": "doc/source/api/index.rst",
"chars": 2348,
"preview": "API Reference\n=============\n\nThis section contains the complete API reference for EVoC.\n\nMain Classes and Functions\n----"
},
{
"path": "doc/source/benchmarks.ipynb",
"chars": 684231,
"preview": "{\n \"cells\": [\n {\n \"cell_type\": \"markdown\",\n \"id\": \"93984a77-bccc-46fc-a7e1-4eb862d10f6e\",\n \"metadata\": {},\n \"so"
},
{
"path": "doc/source/changelog.rst",
"chars": 1022,
"preview": "Changelog\n=========\n\nThis document records all notable changes to EVoC.\n\nVersion 0.1.0 (TBD)\n-------------------\n\nInitia"
},
{
"path": "doc/source/conf.py",
"chars": 6185,
"preview": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see t"
},
{
"path": "doc/source/examples.rst",
"chars": 6764,
"preview": "Examples\n========\n\nCollection of practical examples demonstrating EVoC usage in different scenarios.\n\nBasic Examples\n---"
},
{
"path": "doc/source/index.rst",
"chars": 2146,
"preview": ".. image:: evoc_logo_horizontal.png\n :width: 600\n :align: center\n :alt: EVōC Logo\n\nEVōC: Embedding Vector Oriented Cl"
},
{
"path": "doc/source/installation.rst",
"chars": 1405,
"preview": "Installation\n============\n\nRequirements\n------------\n\nEVoC requires Python 3.8 or later and the following dependencies:\n"
},
{
"path": "doc/source/quickstart.rst",
"chars": 5793,
"preview": "Quick Start Guide\n================\n\nThis guide provides a quick introduction to using EVōC for clustering high-dimension"
},
{
"path": "doc/source/user_guide.rst",
"chars": 9560,
"preview": "User Guide\n==========\n\nThis end-user oriented guide covers EVoC's features, parameters, and best practices for different"
},
{
"path": "evoc/__init__.py",
"chars": 44,
"preview": "from .clustering import evoc_clusters, EVoC\n"
},
{
"path": "evoc/boruvka.py",
"chars": 20440,
"preview": "import numba\nimport numpy as np\n\nfrom .disjoint_set import RankDisjointSetType, ds_rank_create, ds_find, ds_union_by_ran"
},
{
"path": "evoc/cluster_trees.py",
"chars": 17602,
"preview": "import numba\nimport numpy as np\n\nfrom collections import namedtuple\n\nfrom .disjoint_set import ds_rank_create, ds_find, "
},
{
"path": "evoc/clustering.py",
"chars": 29505,
"preview": "import numpy as np\nimport numba\n\nfrom sklearn.base import BaseEstimator, ClusterMixin\nfrom sklearn.utils import check_ar"
},
{
"path": "evoc/clustering_utilities.py",
"chars": 13420,
"preview": "import numpy as np\nimport numba\n\nfrom .numba_kdtree import build_kdtree\nfrom .boruvka import parallel_boruvka\nfrom .clus"
},
{
"path": "evoc/common_nndescent.py",
"chars": 14366,
"preview": "import numpy as np\nimport numba\n\n\n@numba.njit(\"void(i8[:], i8)\", cache=True)\ndef seed(rng_state, seed):\n \"\"\"Seed the "
},
{
"path": "evoc/disjoint_set.py",
"chars": 2027,
"preview": "import numba\nimport numpy as np\n\nfrom collections import namedtuple\n\nRankDisjointSet = namedtuple(\"RankDisjointSet\", [\"p"
},
{
"path": "evoc/float_nndescent.py",
"chars": 42814,
"preview": "import numba\nimport numpy as np\n\nfrom .common_nndescent import (\n tau_rand_int,\n make_heap,\n deheap_sort,\n f"
},
{
"path": "evoc/graph_construction.py",
"chars": 5456,
"preview": "import numpy as np\nimport numba\n\nfrom scipy.sparse import coo_array\n\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = "
},
{
"path": "evoc/int8_nndescent.py",
"chars": 34031,
"preview": "import numba\nimport numpy as np\n\nfrom .common_nndescent import (\n tau_rand_int,\n make_heap,\n deheap_sort,\n f"
},
{
"path": "evoc/knn_graph.py",
"chars": 11020,
"preview": "import numpy as np\nimport numba\nimport time\n\nfrom sklearn.utils import check_array, check_random_state\n\nfrom warnings im"
},
{
"path": "evoc/label_propagation.py",
"chars": 13048,
"preview": "import numpy as np\nimport numba\n\nfrom scipy.sparse import csr_matrix\nfrom sklearn.preprocessing import normalize\nfrom sk"
},
{
"path": "evoc/nested_parallelism.py",
"chars": 847,
"preview": "import os\nimport sys\nimport numba\n\n\ndef supports_safe_nesting():\n # Check if user explicitly set a layer\n layer = "
},
{
"path": "evoc/node_embedding.py",
"chars": 12385,
"preview": "import numpy as np\nimport numba\n\nfrom tqdm import tqdm\n\nINT32_MIN = np.iinfo(np.int32).min + 1\nINT32_MAX = np.iinfo(np.i"
},
{
"path": "evoc/numba_kdtree.py",
"chars": 20719,
"preview": "import numba\nimport numpy as np\n\nfrom collections import namedtuple\n\nNumbaKDTree = namedtuple(\n \"NumbaKDTree\",\n [\""
},
{
"path": "evoc/tests/test_boruvka.py",
"chars": 25836,
"preview": "\"\"\"\nComprehensive test suite for the boruvka module.\n\nThis module tests Boruvka's algorithm implementation for minimum s"
},
{
"path": "evoc/tests/test_cluster_trees.py",
"chars": 20017,
"preview": "import numpy as np\nimport pytest\nfrom sklearn.datasets import make_blobs\nfrom sklearn.utils import shuffle\nfrom sklearn."
},
{
"path": "evoc/tests/test_clustering.py",
"chars": 30312,
"preview": "\"\"\"\nComprehensive test suite for the clustering module.\n\nThis module tests the EVoC clustering algorithm implementation,"
},
{
"path": "evoc/tests/test_knn_graph.py",
"chars": 20538,
"preview": "\"\"\"\nComprehensive test suite for the knn_graph module.\n\nThis module tests the k-nearest neighbor graph construction func"
},
{
"path": "evoc/tests/test_knn_graph_performance.py",
"chars": 19428,
"preview": "\"\"\"\nPerformance benchmark tests for the knn_graph module.\n\nThis module provides performance regression testing for the k"
},
{
"path": "evoc/tests/test_numba_kdtree.py",
"chars": 13658,
"preview": "\"\"\"\nTest suite for NumbaKDTree compatibility with sklearn KDTree.\n\nThis module tests that our NumbaKDTree implementation"
},
{
"path": "evoc/tests/test_numba_kdtree_performance.py",
"chars": 32817,
"preview": "\"\"\"\nPerformance benchmark tests for the numba_kdtree module.\n\nThis module provides performance regression testing and co"
},
{
"path": "evoc/uint8_nndescent.py",
"chars": 36865,
"preview": "import numba\nimport numpy as np\nfrom numba import types\nfrom numba.core import cgutils\nfrom numba.extending import intri"
},
{
"path": "pyproject.toml",
"chars": 1118,
"preview": "[build-system]\nrequires = [\"setuptools>=61.2\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"evoc\"\nversion "
},
{
"path": "pytest.ini",
"chars": 377,
"preview": "[pytest]\nmarkers =\n performance: marks tests as performance tests (deselect with '-m \"not performance\"')\n slow: ma"
},
{
"path": "scripts/run_performance_tests.py",
"chars": 7432,
"preview": "#!/usr/bin/env python3\n\"\"\"\nPerformance benchmark runner for evoc knn_graph module.\n\nThis script runs performance tests a"
},
{
"path": "setup.py",
"chars": 68,
"preview": "from setuptools import setup\n\nif __name__ == '__main__':\n setup()"
}
]
About this extraction
This page contains the full source code of the TutteInstitute/evoc GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 68 files (1.1 MB), approximately 557.7k tokens, and a symbol index with 371 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.