Colored point cloud of high-dimensional data, illustrating UMAP dimensionality reduction

Dimensionality reduction with Uniform Manifold Approximation and Projection (UMAP)

High-dimensional data appears across many real-world problems, from clinical datasets to economic indicators. While these rich feature spaces capture important detail, they also make it difficult to understand relationships between observations. Uniform Manifold Approximation and Projection, or UMAP, provides a practical way to uncover the big-picture structure by mapping complex data into a lower-dimensional space that can be plotted and explored directly.

UMAP in practice

Exploratory data analysis often begins with a simple question. How are observations related across many variables? In high-dimensional settings, this question becomes difficult to answer. Distance metrics lose interpretability, and visualization is no longer feasible beyond a few dimensions.

UMAP addresses this challenge by producing embeddings where nearby points in the original space remain close in the reduced space. At the same time, meaningful groupings can emerge, though the method optimizes only for those local neighborhoods and leaves the geometry between groups to fall where it will. That focus makes it especially useful for cluster discovery and for comparing subpopulations within a dataset.

In applied settings such as country-level socioeconomic analysis, UMAP can reveal how nations with similar profiles across indicators like income, health, and demographics naturally group together, even when those relationships are not obvious in the raw data.

Figure 1 traces this journey one stage at a time, from a table with many features per observation to a two-dimensional layout where related observations gather together.

Figure 1 · Interactive

How UMAP transforms high-dimensional data

Follow the method from many features per observation through to a two-dimensional layout. Move through the five stages with the numbered steps or the Previous and Next buttons.

Observations Neighbors Weights Weighted graph Embedding UMAP pipeline: from features to a two-dimensional layout

Key idea: UMAP connects each observation to its nearest neighbors, weights those links, merges them into one weighted graph, then places the points in two dimensions so that strongly linked observations sit close together. The interactive version with animated stages loads when JavaScript is available.

Figure 1. A conceptual walkthrough of the UMAP pipeline on 24 synthetic points arranged in three loose groups. UMAP emphasizes local neighborhood relationships when it builds the layout. Read a two-dimensional embedding as an exploratory map of similarity, and confirm important distances with quantitative checks.

The stages preview the rest of this section. A nearest-neighbor search and a weighting step build a graph of local relationships, and an optimization step arranges the points in two dimensions. A two-dimensional embedding is a compact summary of those relationships, so it supports exploration and is best read alongside quantitative checks of the original distances.

The UMAP method

UMAP is based on manifold learning. The central assumption is that high-dimensional data lies on a lower-dimensional manifold embedded within the larger space. The goal is to learn that structure.

The method proceeds in two main steps:

• Constructing a neighborhood graph
Each data point is connected to its nearest neighbors, forming a weighted graph that represents local relationships in the original space.

• Optimizing a low-dimensional embedding
UMAP then finds a configuration in lower dimensions that preserves this graph structure as closely as possible. Points that are similar are pulled together, while dissimilar points are pushed apart.

This combination of attraction and repulsion is what settles the layout. What it optimizes for is the local neighborhoods. The geometry between groups is whatever the optimization happened to produce. Unlike linear methods such as principal component analysis (PCA), UMAP can represent non-linear relationships, which are common in real-world data.

The weighted graph rewards a closer look, because the weights are where UMAP adapts to the shape of the data. Figure 2 shows how the distance to each neighbor becomes a connection strength, and how two one-way strengths combine into a single shared weight.

Figure 2 · Interactive

From local distances to a fuzzy neighborhood graph

See how the distance to each neighbor becomes a connection strength, how the local scale adapts to dense and sparse regions, and how two one-way strengths merge into a single shared weight. Adjust the neighbor count, switch regions, and select a neighbor to inspect its numbers.

Connection strength decays smoothly with distance 1.0 0.0 rho distance from point i membership strength

Key idea: point i connects to its nearest neighbor with full strength at the radius rho, and the strength fades with distance at a rate set by a local scale sigma. The scale is chosen so the neighbor strengths sum to log2 of the neighbor count, which keeps dense and sparse regions comparable. Two one-way strengths p(j|i) and p(i|j) are combined into one shared weight with the fuzzy union p(i,j) = p(j|i) + p(i|j) minus p(j|i) times p(i|j). The interactive controls load when JavaScript is available.

Figure 2. Connection strengths are graph weights between 0 and 1 that decay with distance under a locally adapted scale. They express neighborhood membership for building the graph and behave as relative link strengths.

Two ideas carry the most weight here. A local scale adjusts each neighborhood so dense and sparse regions contribute comparable strengths, and the two directed strengths on a link merge into one symmetric weight. The result is a single weighted graph that records which observations sit close together in the original space.

The role of preprocessing

The quality of a UMAP embedding depends heavily on preprocessing. Since the algorithm relies on nearest neighbor relationships, the input feature space must be carefully prepared.

A typical workflow includes:

• Imputation of missing values
Instead of simple strategies such as median imputation, a more expressive approach is to use the IterativeImputer from scikit-learn. This method models each feature with missing values as a function of other features and iteratively refines those estimates. It helps most in structured datasets where variables are correlated, because it preserves multivariate relationships rather than collapsing missing values to a single summary statistic.

• Feature scaling
Standardizing features to zero mean and unit variance stops a variable with a large range from dominating the distance calculations simply because of its units. It levels the spreads without deciding how much any one feature ends up mattering. Without scaling, variables with larger numeric ranges can distort the neighborhood graph. Poor preprocessing can lead to misleading embeddings that show scale artifacts dressed up as structure.

Key parameters and their effects

Two parameters play a central role in shaping the UMAP embedding:

• n_neighbors
This controls the size of the local neighborhood used to build the graph.
Smaller values emphasize fine-grained local structure and can separate tight clusters. Larger values incorporate more global context and produce smoother layouts.

• min_dist
This determines how tightly points can cluster in the embedding.
Lower values create compact clusters, which can make dense groupings easier to see. Higher values spread points out. The setting controls how tightly the layout may pack points. It does not improve how faithfully the map preserves the original structure.

A commonly effective setting for min_dist is around 0.1, which balances separation and interpretability. Another important detail is the use of a fixed random_state. UMAP includes stochastic elements, and setting a seed ensures reproducibility, which is important for both research and production workflows.

Figure 3 makes these parameters tangible. It runs a small attraction and repulsion layout, in which n_neighbors and min_dist can be varied and the result replayed to watch the arrangement respond.

Figure 3 · Interactive

Optimization and the effects of UMAP parameters

Strong links pull related points together while a repulsion term keeps unrelated points apart, and the layout settles into groups. Adjust n_neighbors and min_dist, replay the layout, and try a new seed to see how the picture responds.

UMAP dimension 1 UMAP dimension 2

Key idea: attraction along strong links and repulsion between unrelated points settle the layout into groups. Smaller n_neighbors emphasizes local groups, larger n_neighbors emphasizes broad structure, smaller min_dist packs points tightly, and larger min_dist spreads them out. These parameters shape the view. Questions about cluster structure, global distances, and causation are settled on the data itself. The interactive controls and animation load when JavaScript is available.

Figure 3. A conceptual view of the attraction and repulsion that drive UMAP optimization, along with the influence of n_neighbors and min_dist. Group sizes, the gaps between groups, and axis orientation are outcomes of the layout, so a pattern in the embedding is a hypothesis to test on the data itself.

The controls shape the view without fixing any ground truth. Group sizes, the gaps between groups, and the axis orientation all follow from the optimization, so an embedding is best treated as an exploratory map that is confirmed with quantitative analysis. The worked example below compares several views and reports how often near neighbors share a continent, a figure measured on the embedding rather than on the original indicators.

Example: UMAP dimensionality reduction on country data

UMAP Embeddings. How UMAP turns nine socioeconomic and health indicators for 167 countries into a three-dimensional map that keeps similar nations close while broader structure emerges. Key topics covered: Country indicator data, Optimize the layout, 2D embedding views, Impute and scale, Neighbor graph, Key parameters, Interactive 3D view, Reading the embedding.

Hover any card to explore

Data

The dataset used in this notebook contains country-level development indicators, with each row representing one country and each feature capturing a different aspect of national conditions such as mortality, trade, health expenditure, income, inflation, life expectancy, fertility, and gross domestic product (GDP) per capita.

This notebook treats the file as an analysis-ready tabular dataset for dimensionality reduction and visualization. Further information about the dataset can be found by accessing the link: https://www.kaggle.com/datasets/vipulgohel/clustering-pca-assignment/data

Notebook overview

This notebook follows a simple analytical sequence:

  1. Import the required packages
  2. Define reusable constants and configuration values
  3. Load and validate the country dataset
  4. Build a clean feature matrix for modeling
  5. Compute a three-dimensional UMAP embedding
  6. Visualize the embedding from multiple views

The emphasis is on a transparent workflow where each computational step is documented close to the code that performs it.

Import dependencies

This section collects all required libraries in one place so the notebook can be reviewed and rerun more easily.

The imported packages serve distinct purposes:

  • pandas for tabular data handling
  • IterativeImputer and StandardScaler for preprocessing
  • umap-learn for nonlinear dimensionality reduction
  • plotly for interactive visualization
Python
import pandas as pd
from sklearn.experimental import enable_iterative_imputer  # noqa: F401
from sklearn.impute import IterativeImputer
from sklearn.preprocessing import StandardScaler
import umap
import plotly.express as px
import plotly.io as pio

Grouping imports at the top is especially helpful in notebooks intended for publication or reuse because readers can immediately see the required software stack and package choices without scanning the entire file.

Define constants

Rather than scattering hard-coded values throughout the notebook, the main analysis settings are defined once here.

This improves readability and makes the notebook easier to modify. The number of neighbors, the random seed, the plotted dimensions, and the feature list can be changed from a single location without searching through later cells.

Python
# Set Plotly theme and figure dimensions
pio.templates.default = "plotly_white"
FIG_WIDTH = 800
FIG_HEIGHT = 700

# Define feature columns and other constants
FEATURE_COLS = [
    "child_mort", "exports", "health", "imports", "income", "inflation", "life_expec", "total_fer", "gdpp",
]
REQUIRED_COLS = {"country", "continent", *FEATURE_COLS}
IMPUTER_MAX_ITER = 25
DEFAULT_N_NEIGHBORS = 20
# For reproducibility
RANDOM_SEED = 42

The feature list is defined explicitly instead of selecting columns by position. That reduces the risk of accidentally including identifiers or metadata in the embedding if the source CSV changes shape in the future.

Load and validate data

Before any modeling begins, the notebook checks that the input file has the expected structure.

These checks matter because downstream dimensionality-reduction results are only as trustworthy as the data entering the pipeline. In practical terms, this section verifies that:

  • all required columns are present
  • country identifiers are populated
  • each country appears only once
  • modeled features can be interpreted numerically

Catching structural issues early prevents silent failures later in the analysis.

Python
# Path to data file
csv_path = "Country-data.csv"
# Load CSV into DataFrame
df = pd.read_csv(csv_path)
# Check for required columns
missing = [c for c in REQUIRED_COLS if c not in df.columns]
if missing:
    raise ValueError(
        f"CSV is missing required columns: {missing}\n"
        f"Found columns: {list(df.columns)}"
    )

# Work with a copy to avoid side effects
df = df.copy()

# Ensure categorical identifiers are present and unique
if df[["country", "continent"]].isna().any().any():
    raise ValueError("Columns 'country' and 'continent' must not contain missing values.")

duplicate_countries = df["country"].duplicated(keep=False)
if duplicate_countries.any():
    duplicate_labels = sorted(df.loc[duplicate_countries, "country"].unique())
    raise ValueError(f"Duplicate country entries detected: {duplicate_labels}")

# Coerce feature columns to numeric so any parsing issues are surfaced as missing values
df[FEATURE_COLS] = df[FEATURE_COLS].apply(pd.to_numeric, errors="coerce")
missing_by_feature = df[FEATURE_COLS].isna().sum().sort_values(ascending=False)
missing_total = int(missing_by_feature.sum())

# Print dataset diagnostics
print(f"Rows: {len(df):,}")
print(f"Countries: {df['country'].nunique():,}")
if missing_total:
    print("Missing values by feature:")
    print(missing_by_feature[missing_by_feature > 0].to_string())
else:
    print("Missing values by feature: none")
Rows: 167
Countries: 167
Missing values by feature: none

Result note. The validation output indicates a clean one-row-per-country dataset with 167 observations and no missing values in the modeled features. In this file the later imputation logic is effectively dormant, which is a useful sanity check before fitting the embedding.

Prepare feature matrix and preprocess

This section creates the numeric matrix that will be passed into UMAP.

Two preprocessing steps are kept explicit:

  • missing-value handling
  • feature scaling

Even when the current file is complete, it is useful to keep the preprocessing pipeline in place so the notebook remains robust if the dataset is updated later with partially observed values.

Python
# Select features for UMAP
X_raw = df[FEATURE_COLS].copy()
feature_bounds = pd.DataFrame({"min": X_raw.min(), "max": X_raw.max()})

# Iterative, model-based imputation preserves multivariate structure better than a univariate fill
imputer = IterativeImputer(
    random_state=RANDOM_SEED,
    initial_strategy="median",
    max_iter=IMPUTER_MAX_ITER,
    skip_complete=True,
    min_value=feature_bounds["min"].to_numpy(),
    max_value=feature_bounds["max"].to_numpy(),
)
X_imputed = pd.DataFrame(
    imputer.fit_transform(X_raw),
    columns=FEATURE_COLS,
    index=df.index,
)

# Standardize features before distance-based manifold learning
scaler = StandardScaler()
X = scaler.fit_transform(X_imputed)

if X_raw.isna().any().any():
    print(f"Iterative imputer converged in {imputer.n_iter_} round(s).")
else:
    print("No missing values detected; the iterative imputer remains in place for reproducible future runs.")
No missing values detected; the iterative imputer remains in place for reproducible future runs.

The imputer and scaler are applied in separate, visible steps rather than being hidden inside a pipeline. For an educational notebook, that makes it easier to inspect intermediate objects and explain what happened to the data before the embedding was learned. Because the current dataset has no missing feature values, the iterative imputer leaves the numeric matrix unchanged and preprocessing reduces to scaling. That means the notebook is ready for incomplete data without altering the present analysis unnecessarily.

Compute UMAP embedding and prepare plot dataframe

Once the features are prepared, the notebook fits a three-dimensional UMAP model and stores the resulting coordinates in a plotting dataframe.

A few interpretation points are worth keeping in mind:

  • the UMAP axes are learned coordinates with no scientific meaning of their own
  • which observations share a neighborhood is the informative part, while the distances themselves, the gaps between groups, the group sizes and the axis orientation all come out of the optimization
  • the embedding is built for exploration and will not support formal inference

Attaching the coordinates back to country and continent labels makes the visualization stage much easier to interpret.

Python
# Configure UMAP
n_neighbors = min(DEFAULT_N_NEIGHBORS, len(df) - 1)
if n_neighbors < 2:
    raise ValueError("UMAP requires at least two observations.")

umap_3d = umap.UMAP(
    n_neighbors=n_neighbors,
    min_dist=0.1,
    n_components=3,
    metric="euclidean",
    random_state=RANDOM_SEED,
    transform_seed=RANDOM_SEED,
)
# Fit and transform features
embedding_3d = umap_3d.fit_transform(X)
# Prepare DataFrame for plotting
plot_df = df[["country", "continent"]].copy()
# Add UMAP1
plot_df["UMAP1"] = embedding_3d[:, 0]
# Add UMAP2
plot_df["UMAP2"] = embedding_3d[:, 1]
# Add UMAP3
plot_df["UMAP3"] = embedding_3d[:, 2]

Choosing three components provides some flexibility during interpretation. It allows the notebook to show pairwise projections as well as a fully interactive 3D view without refitting a second model. The embedding has the expected shape of 167 countries by 3 coordinates, so every observation is carried through into the visualization.

Plot UMAP interactive results

The block generates three two-dimensional views plus a three-dimensional interactive plot.

Using multiple views is helpful because any single projection can hide separation that becomes visible from another angle. The saved HTML outputs also make it easy to share the figures outside the notebook while preserving hover labels and rotation controls.

Python
# Tooltip for UMAP1 vs UMAP2
hovertemplate_12 = "<b>%{customdata[2]}</b><br>UMAP1: %{x:.3f}<br>UMAP2: %{y:.3f}<extra></extra>"
# Tooltip for UMAP2 vs UMAP3
hovertemplate_23 = "<b>%{customdata[2]}</b><br>UMAP2: %{x:.3f}<br>UMAP3: %{y:.3f}<extra></extra>"
# Tooltip for UMAP1 vs UMAP3
hovertemplate_13 = "<b>%{customdata[2]}</b><br>UMAP1: %{x:.3f}<br>UMAP3: %{y:.3f}<extra></extra>"
# Tooltip for 3D plot
hovertemplate_3d = "<b>%{customdata[3]}</b><br>UMAP1: %{x:.3f}<br>UMAP2: %{y:.3f}<br>UMAP3: %{z:.3f}<extra></extra>"

# Plot UMAP 1 vs UMAP 2
fig_12 = px.scatter(
    plot_df, x="UMAP1", y="UMAP2", color="continent", custom_data=["UMAP1", "UMAP2", "country"], title="UMAP 1 vs UMAP 2"
)
fig_12.update_traces(marker=dict(size=9, line=dict(width=0.5, color="rgba(0,0,0,0.4)")), hovertemplate=hovertemplate_12)
fig_12.update_layout(legend_title_text="Continent", xaxis_title="UMAP 1", yaxis_title="UMAP 2", dragmode="pan", width=FIG_WIDTH, height=FIG_HEIGHT)
fig_12.update_yaxes(scaleanchor="x", scaleratio=1)
fig_12.show()
# Save plot as HTML
fig_12.write_html("umap_1_vs_2_nt.html")

# Plot UMAP 2 vs UMAP 3
fig_23 = px.scatter(
    plot_df, x="UMAP2", y="UMAP3", color="continent", custom_data=["UMAP2", "UMAP3", "country"], title="UMAP 2 vs UMAP 3"
)
fig_23.update_traces(marker=dict(size=9, line=dict(width=0.5, color="rgba(0,0,0,0.4)")), hovertemplate=hovertemplate_23)
fig_23.update_layout(legend_title_text="Continent", xaxis_title="UMAP 2", yaxis_title="UMAP 3", dragmode="pan", width=FIG_WIDTH, height=FIG_HEIGHT)
fig_23.update_yaxes(scaleanchor="x", scaleratio=1)
fig_23.show()
# Save plot as HTML
fig_23.write_html("umap_2_vs_3_nt.html")

# Plot UMAP 1 vs UMAP 3
fig_13 = px.scatter(
    plot_df, x="UMAP1", y="UMAP3", color="continent", custom_data=["UMAP1", "UMAP3", "country"], title="UMAP 1 vs UMAP 3"
)
fig_13.update_traces(marker=dict(size=9, line=dict(width=0.5, color="rgba(0,0,0,0.4)")), hovertemplate=hovertemplate_13)
fig_13.update_layout(legend_title_text="Continent", xaxis_title="UMAP 1", yaxis_title="UMAP 3", dragmode="pan", width=FIG_WIDTH, height=FIG_HEIGHT)
fig_13.update_yaxes(scaleanchor="x", scaleratio=1)
fig_13.show()
# Save plot as HTML
fig_13.write_html("umap_1_vs_3_nt.html")

# Plot UMAP 3D embedding
fig3d = px.scatter_3d(
    plot_df, x="UMAP1", y="UMAP2", z="UMAP3", color="continent", custom_data=["UMAP1", "UMAP2", "UMAP3", "country"], title="UMAP of Countries (3D)"
    )
fig3d.update_traces(marker=dict(size=4), hovertemplate=hovertemplate_3d)
fig3d.update_layout(legend_title_text="Continent", width=FIG_WIDTH, height=FIG_HEIGHT)
fig3d.show()
# Save plot as HTML
fig3d.write_html("umap_3d_nt.html")
Figure 4

UMAP 1 against UMAP 2 by continent

Open this figure at full size in a new tab
Figure 4. The first two coordinates of the three-dimensional embedding fitted above, drawn with Plotly Express from nine development indicators for 167 countries and colored by continent. Europe tends toward higher UMAP 2 while Africa sits lower, and Asia, North America, Oceania, and South America overlap substantially. The axes are learned coordinates with no units, and which countries share a neighborhood is what the layout reports. Hover any point for its country and coordinates.
Figure 5

UMAP 2 against UMAP 3 by continent

Open this figure at full size in a new tab
Figure 5. The same embedding seen through its second and third coordinates, with no new UMAP run behind it, so the points are identical and only the pair of axes on display has changed. Europe tends to sit high on both coordinates shown here, as it does on UMAP 1, with Africa lower on UMAP 2. Comparing views is worthwhile because each pair of axes shows a different slice of the embedding.
Figure 6

UMAP 1 against UMAP 3 by continent

Open this figure at full size in a new tab
Figure 6. The third pairwise slice of the same fitted embedding, plotting the first coordinate against the third. As in the two views above, both axes are drawn on a common scale, and together the three exhaust the pairwise projections available from three components. Gaps between colored groups, the size of each group, and the orientation of the axes are outcomes of the layout optimization, so treat an apparently isolated point as a prompt to hover and read its label.
Figure 7

Rotatable three-dimensional UMAP embedding

Open this figure at full size in a new tab
Figure 7. All three learned coordinates at once, drawn with px.scatter_3d from the same fitted model that produced the three flat views above, with continent used only to color the points. Rotating the scene shows that continent tracks part of the arrangement. UMAP is optimized to keep near neighbors near, so the plot can show which countries a given point sits among, and the apparent size of any gap depends on the chosen angle.

These figures are a map of multivariable similarity, and they are neither a map of geography nor the output of a supervised classifier.

A few practical interpretation habits help:

  • look for broad neighborhoods before focusing on isolated points
  • compare patterns across the different 2D views
  • treat continent coloring as a label laid over the result, since the model never saw it
  • use hover labels to inspect countries that appear unusually separated or unexpectedly close

The plots suggest continent explains part of the structure, but not all of it. Europe tends to occupy a relatively distinct higher UMAP2 and UMAP3 region, Africa is often lower on UMAP2, and Asia, North America, Oceania, and South America overlap more substantially. A simple diagnostic finds that about 54% of each country’s five nearest neighbors share its continent, well above the roughly 22% expected if neighbors were drawn at random from the same table. Because that count is measured on the embedding and not on the nine indicators, it restates the pattern the layout already shows without checking it against the original features. Read the map as a broad socioeconomic similarity structure. It captures continent only loosely.

Summary

Key Takeaways

Local neighborhoods are what the graph encodes

The graph is built from each point’s nearest neighbors, and the layout pulls linked points together, so observations close in the original feature space are drawn close in the map. That is the relationship the method supports.

Distance between groups is not a measurement

Group sizes, the gaps between groups, and axis direction all follow from the optimization, so a wide gap does not measure how different two groups are. Treat the embedding as an exploratory map and check any distance claim against the original features.

Preprocessing decides the neighbors

UMAP finds neighbors with a distance metric, Euclidean in this example, so an unscaled feature with a large range would dominate the graph. The notebook standardizes all nine indicators to zero mean and unit variance before fitting.

n_neighbors sets the scale of the view

Smaller values emphasize fine local structure and can separate tight groups, while larger values fold in more context and smooth the layout. The example asks for 20 neighbors, falling back to one less than the row count on a smaller table.

min_dist controls packing only

min_dist sets how tightly points may sit in the embedding, and it is 0.1 here. Lower values make groups look compact and higher values spread them out, but neither setting changes which observations are actually similar.

Fix the seed before comparing runs

UMAP has stochastic elements, so two runs on the same matrix can produce different pictures. The example sets random_state and transform_seed to 42. Only random_state fixes this layout, while transform_seed applies when transform is called on new data.

Data & License

Dataset

Country-level socio-economic and health indicators for 167 countries (the “HELP International” data), published on Kaggle by Vipul Gohel. Source: kaggle.com/datasets/vipulgohel/clustering-pca-assignment.License: CC0 1.0 Universal (Public Domain Dedication) — no rights reserved; attribution is not legally required and is given here as a courtesy. CC0 1.0

Article

© 2025 Philip Sarajlic. All rights reserved for the article’s original text and figures; the third-party data remains under the license shown above.

Code

Code examples in this article are licensed under the Common Public Attribution License Version 1.0 (CPAL-1.0), an OSI-approved copyleft license based on the Mozilla Public License 1.1. Initial Developer: Philip Sarajlic.

Attribution required by CPAL Exhibit B: © 2025 Philip Sarajlic · “Based on code by Philip Sarajlic” · philipsarajlic.com · no graphic image. This attribution must be displayed in Larger Works.

Modifications must be released in Source Code form under CPAL-1.0. Making the code usable by anyone other than you over a network is External Deployment under the license and is treated as distribution, so the Source Code must be made available to those users.

Full text: opensource.org/license/cpal-1-0 (SPDX identifier CPAL-1.0)

Scroll to Top

Free diagnostic

Would your model hold up to an external review?

Answer 39 questions in about nine minutes and get a clear picture of where your model stands.

You’ll receive a readiness score, a breakdown across 15 areas, your biggest evidence gaps, and the five questions an external reviewer would be most likely to ask first.

The assessment draws on guidance from:

  • TRIPOD+AI
  • PROBAST+AI
  • FDA GMLP
  • NIST AI RMF
  • SR 11-7

We’ll send you one email with your link, and your results when you finish. Your email address is carried through when you complete the assessment, so you won’t need to enter it again.

We don’t ask for your data, and there are no free-text fields in the assessment. See how we use your email address.