---
title: "🗓️ Week 07: Introduction and Dimensionality Reduction"
subtitle: "Theme: Unsupervised Learning"
author: "Dr. Ghita Berrada"
format: 
  html:
    from: markdown+emoji
    page-layout: full
    toc: true
    toc-depth: 2
self-contained: true
---

Welcome to Week 07! This week, we leave supervised learning behind (sort of :joy_cat:) and start exploring the (brave new) world of unsupervised learning... So, let's get started!

# :gear: Setup

Before you can load the libraries below, you might have to do the following package installs (feel free to skip if you already have done the installs before)

``` r
install.packages("naniar")
install.packages("plotly")
install.packages("skimr")
install.packages("dials")
install.packages("UpSetR")
install.packages("themis")
install.packages("embed")
```

Then, let's load the libraries we'll need today.

```{r, echo=FALSE, message=FALSE, warning=FALSE}
# Tidyverse packages
library(tidyr)
library(dplyr)
library(lubridate)
library(ggplot2)
library(readr)
library(plotly)

# Tidymodels packages, spelt out individually for clarity
library(parsnip)
library(yardstick)
library(recipes)
library(rsample)
library(tune)
library(dials)
library(workflows)
library(workflowsets)

#library that adds step_umap
library(embed)

#libraries to explore missing data patterns
library(naniar)
library(UpSetR)

#library to do EDA
library(skimr)

#library that contains steps that can be added to recipes to deal with unbalanced data
library(themis)


```

```{r}
custom_theme <- function() {
  theme_minimal() +
    theme(panel.grid.minor = element_blank(),
          legend.position = "bottom")
}
```

## Part 1: Some recaps on supervised learning and data pre-processing

For this part, we'll look at data about water sources in Africa made available on [TidyTuesday](https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-05-04/water.csv) and discussed in [Julia Silge's Blog](https://juliasilge.com/blog/water-sources/)

Let's load our data:

```{r}
water_raw <- read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-05-04/water.csv")
```

Let's have an overview of our data:

```{r}
skim(water_raw)
```

The first thing that jumps out at us is this is a dataset with a significant amount of missing values!

It also has a large amount of rows so we'll need to trim things down (e.g by focusing our analysis on a single country) to keep computation cost/time manageable!

So, let's check which countries are represented in the data and the number of rows for each

```{r}
water_raw %>% count(country_name,sort=TRUE) %>% print(n=Inf)
```

Let's focus on South Sudan which is reasonably sized without being overwhelming.

```{r}
water_set <- water_raw %>% filter(country_name=="South Sudan")
head(water_set)
```

Let's do a quick sanity check on the geographical variables linked to the data to check for outliers!

```{r}
water_set %>%
  filter(
    status_id %in% c("y", "n")
  ) %>%
  ggplot(aes(lon_deg, lat_deg, color = status_id)) +
  geom_point(alpha = 0.2)
```

A quick look at [Wikipedia](https://en.wikipedia.org/wiki/South_Sudan#Geography) tells me that "South Sudan lies between latitudes [3°](https://en.wikipedia.org/wiki/3rd_parallel_north "3rd parallel north") and [13°N](https://en.wikipedia.org/wiki/13th_parallel_north "13th parallel north"), and longitudes [24°](https://en.wikipedia.org/wiki/24th_meridian_east "24th meridian east") and [36°E](https://en.wikipedia.org/wiki/36th_meridian_east "36th meridian east")". We'll filter any outliers on this basis.

```{r}
water_set <- water_set %>%
             filter(lat_deg > 3, lat_deg < 13, lon_deg>24, lon_deg < 36)
```

If I redo my earlier plot, this is what I get:

```{r}
water_set %>%
  filter(
    status_id %in% c("y", "n")
  ) %>%
  ggplot(aes(lon_deg, lat_deg, color = status_id)) +
  geom_point(alpha = 0.2)
```

Now that we've done this, let's have a look at our missing data:

```{r}
vis_miss(water_set)
```

Unfortunately for us, both the proportion of missing data in the variables `install_year` , `installer` and `pay` are too high to be able to do anything useful with these variables (imputation is simply not possible). We'll drop these variables.

The other variables are salvageable e.g `water_source`,`facility_type` and `status`. However, the pattern of missingness here is likely not MCAR ( missing completely at random i.e missing data values do not relate to any other data in the dataset and there is no pattern to the actual values of the missing data themselves) since it looks there is a correlation between the patterns of missingness of the various variables with missing data: this means the pattern of missingness is most likely MAR (missing at random). We can't simply delete missing values (we'd skew the data distribution if we did!) and we need to impute the missing data values.

We'll do our imputation as part of a recipe, prior to modeling.

We also need to check whether our classes are imbalanced: our outcome variable is `status_id` which tells us whether a water source is or not available.

```{r}
water_set %>%
  group_by(status_id) %>%
  summarise(n = n()) %>%
  mutate(Freq = n/sum(n))
```

The dataset is heavily imbalanced (and we have a multiclass problem on our hands too!).

Now that we're done with basic data exploration, let's get to the modeling.

```{r}
water_set <- water_set %>%
             mutate(across(where(is.character), as.factor))

set.seed(123)
water_split <- initial_split(water_set, strata = status_id)
water_train <- training(water_split)
water_test <- testing(water_split)

set.seed(234)
water_folds <- vfold_cv(water_train, strata = status_id)
water_folds
```

```{r}
rec <- recipe(status_id~.,data=water_train) %>%
       update_role(row_id, new_role = "id") %>%
       update_role(pay,installer,install_year,country_name,report_date, new_role = "uninformative") %>%
       step_impute_mode(water_source,water_tech,facility_type,status) %>%
       step_dummy(all_factor_predictors()) %>%
       step_smote(status_id)
  

```

There are many ways to do imputation: in this case, we chose `step_impute_mode` that substitutes **nominal data using the most common value (in the training set) since all the variables that have missing data in our dataset are nominal**.

When dealing with class imbalance, we can look at metrics that are not sensitive to class imbalance (e.g F1-score, precision, recall). But we can also intervene at the dataset level by balancing out the classes either by downsampling the majority class i.e reducing the number of examples of your majority class to (roughly) equal the number of samples of the minority class (you could use `step_downsample` for that -see the Julia Silge blog that discusses this dataset and the `themis` library documentation) or by oversampling your minority class i.e creating synthetic examples of the minority class based on existing ones (a method that does this - which we used here through `step_smote` is called [SMOTE](https://medium.com/@corymaklin/synthetic-minority-over-sampling-technique-smote-7d419696b88c#id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFkYzBmMTcyZThkNmVmMzgyZDZkM2EyMzFmNmMxOTdkZDY4Y2U1ZWYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIyMTYyOTYwMzU4MzQtazFrNnFlMDYwczJ0cDJhMmphbTRsamRjbXMwMHN0dGcuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMTYyOTYwMzU4MzQtazFrNnFlMDYwczJ0cDJhMmphbTRsamRjbXMwMHN0dGcuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTU0NDAwMjI2NDg3MDk0MTU4NDkiLCJlbWFpbCI6ImJlcnJhZGEuZ2hpdGFAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5iZiI6MTczMTY0NjQ3MiwibmFtZSI6IkdoaXRhIEJlcnJhZGEiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jSXdDODZ6Z1ZFTkpSNU9yRDhSenJEWXBKT3ZtNU5YUDU3ejNFNkQ3YnlDMjUySDJ6SHdNQT1zOTYtYyIsImdpdmVuX25hbWUiOiJHaGl0YSIsImZhbWlseV9uYW1lIjoiQmVycmFkYSIsImlhdCI6MTczMTY0Njc3MiwiZXhwIjoxNzMxNjUwMzcyLCJqdGkiOiJmZGFkZDZhOTE5ZmVlZDFmOGYyYWUxMGQxNWU0MjMwMTI4ZDZjMDZiIn0.AKZzkGa_6h6fhv4XyI8ag8dk-QxceD4CQTbqcbu8jG6z2c-aUDojRDfHYzcRvVFkEyZhACS0daSuBsFr_XhRFr_RmZM84_kXmBYswohA1DCsEg0aLCCi6F-1Ym_WBwT66CBQarJ3QBX2rDXpLT4r1O4YW0LC3zFS7MAxTNcoaYJCeMa72WB8axup2v2BLqPr4DdByc9JWuO33lP-THXDN70YlJ9hwt7g_gIEJ-AdqhidYBlBsKHynNCsDZA5Hk64qVhZarVvWlqGSBXAvFqRT7mbEYfKhBR0AATOHj9PB2SMMQtocdbl5YfZDgE2lO61Ywi9CFluf_soYf4x0FwVsg). You'll find the original paper that describes SMOTE [here](here)).

Both downsampling and oversampling have downsides:

-   if you have too few data points, downsampling is not a method you should use as it leaves you with even fewer data points, compromising further model training

-   On the flip side, oversampling may lead to overfitting since the new minority class data points are generated from pre-existing ones and there might not be enough diversity in the training set (though SMOTE is designed to try and circumvent this problem)

Let's quickly fit

```{r}
class_metrics <- metric_set(precision, recall, f_meas)
lr_model <- multinom_reg(penalty=tune(), mixture=1) %>%
  set_engine('glmnet')
xgb_model <- boost_tree(mtry = .cols(), trees = 250, learn_rate = tune()) %>% 
  set_mode("classification") %>% 
  set_engine("xgboost")
```

```{r}
water_models <- 
  workflow_set(
    preproc = list(preproc=rec),
    models = list(logit = lr_model, xgboost= xgb_model),
    cross = TRUE
  )
```

```{r}
xgb_grid <- tibble(learn_rate = c(0.0001, 0.001, 0.01, 0.05, 0.1,0.3, 0.5,1))
logit_grid<-tibble(penalty =c(0.001, 0.005, 0.01, 0.02, 0.05, 0.08))
water_res <- water_models %>%
  workflow_map("tune_grid", 
               resamples = water_folds, 
                grid = 5,
    , verbose = TRUE)
```

```{r}
autoplot(water_res, metric="roc_auc")
```

```{r}
water_res %>% 
  rank_results()
```

## Part 2: Motivations for Unsupervised Learning

Sometimes, we don't have a target variable to predict. We just want to understand the structure of our data. This is where **unsupervised learning** comes in.

Unsupervised learning is used in many different contexts and comes in four main families:

### Types of Unsupervised Learning

<div>

### Clustering

-   Consists in dividing data points into clusters, i.e groups of points that are similar between themselves and are dissimilar to data points belonging to other cluster
-   Examples:
    -   segmenting patients with a particular condition (e.g cancer) into groups depending on their response to particular treatment drugs (aim: treatment personalisation)
    -   customer segmentation according to common traits and/purchasing behaviour (aim: guiding marketing and business strategies)
    -   Image segmentation (e.g in radiology or pathology)
    -   Object recognition (in computer vision)
    -   Natural language processing (NLP): Google News uses clustering to categorize articles based on the same story from various news outlets; for example, the results of the football transfer window would all be categorized under football.

### Association rule mining

-   Series of algorithms that try to find rules to discover the probability of the co-occurrence of items in a collection (e.g Apriori, Eclat, FPGrowth)
-   E.g:
    -   Given a collection of supermarket purchase records/transactions, can we tell how likely a customer is to buy milk if he/she has bought bread? (Market Basket analysis)
    -   Can we learn how likely a series of medical diagnoses are, given the symptoms to which they are associated?
    -   Recommendation engines (e.g Netflix or Youtube video recommendations, Amazon recommendations)

### Dimensionality reduction

-   Techniques that reduce the number of features, or dimensions, in a dataset (prior to applying a supervised/unsupervised learning algorithm)

    Example:

    -   Principal component analysis (PCA - the topic of this week)
    -   or Singular value decomposition (SVD)
    -   UMAP

### Anomaly detection

-   Techniques that discover unusual data points in a dataset i.e with patterns that deviate from normal dataset patterns

-   E.g:

    -   Fraud detection (credit card fraud, insurance fraud, detection of money laundering)
    -   Rare events detection e.g detection of earthquakes, detection of epileptic seizures
    -   Cyberattack detection
    -   Also useful in predicting whether a patient has or doesn't have a disease
    -   Detecting faulty equipment

</div>

Let's now discover our first family of unsupervised learning techniques: dimensionality reduction.

## Part 3: Dimensionality reduction

### 3.1 Let's meet our new dataset

The dataset is an adaptation for this [Kaggle dataset](https://www.kaggle.com/datasets/parisrohan/credit-score-classification) about credit score classification.

The scenario is as follows:

"You work as a data scientist at a global finance company. Thoughout the years, the company has been collecting basic bank details and gathered a lot of credit-related information about their customers. The management of the company wants to build an intelligent system to divide people into credit score brackets in order to reduce manual efforts.

The task is to build a machine learning model that can predict the credit score given person's credit-related details."

```{r}
data_url <- "https://assets.datacamp.com/production/repositories/6081/datasets/e02471e553bc28edddc1fe862666d36e04daed80/credit_score.csv"

df <- read_csv(data_url)

df <- df%>% 
       mutate(credit_score = factor(credit_score, 
               levels = c("Poor", "Standard", "Good")))
```

Let's have a quick overview of the data:

```{r}
skim(df)
```

No missing values here. Quite a large dataframe (18965 rows and 23 columns).

Do you think we can visualise this many features?

Here are a few visualisations we can make:

```{r}
# Plot of annual income distribution for different credit score levels
df %>% ggplot(aes(x = annual_income, color = credit_score)) +
  geom_density() + xlim(0, 200000)
```

Annual income is a useful predictor variable. From the plot, people with a low annual income have poorer credit scores.

```{r}
#Age Density Plot
df %>% ggplot(aes(x = age, color = credit_score)) + geom_density()
```

It also seems that older people have better credit scores, making the `age` feature a great predictor.

We could go on...

### 3.2 Principal component analysis (PCA)

Principal Component Analysis is a **principled way** to reduce features prior to applying a supervised or unsupervised learning technique.

The goal is to reduce computational cost and make it possible to visualise the results:

-   Suppose that you wish to visualize $n$ observations with measurements on a set of $p$ features, $X_1$, $X_2$,..., $X_p$. You could conceivably visualise them with a series of two-dimensional scatterplots, each representing all the $n$ for two of the $p$ features. But, you'd need $(^P_2)=\frac{p(p-1)}{2}$ scatterplots!

    In our previous credit score example, this would mean 253(!) plots!

-   on top of this, it's highly likely that none of the 2D-plots will be informative enough since they each contain just a small fraction of the total information present in the data set

    Enter PCA (Principal Component Analysis).

    The aim of PCA is to find a low-dimensional representation of a data set that contains as much as possible of the variance of the data.

    Each of the dimensions found by PCA is a linear combination of the $p$ features (the total of dimensions of dimensions found by PCA is equal to the number of dimensions in the data i.e $p$ so that the total variance preserved by all dimensions is 100% - to reduce dimensions you then set a threshold of variance kept, which gives you the number of PCA dimensions to keep).

    The first principal component of a set of features $X_1$, $X_2$,..., $X_p$ is the normalized linear combination of the features $Z_1 = φ_{11}X1 + φ_{21}X_2 + ··· + φ_{p1}X_p$ that has the largest variance ( with normalized meaning $\sum_{j=1}^p \phi_{j1}^2$ ). The elements $φ_{11}$,..., $φ_{p1}$ are the loadings of the first principal loading component.

    The second principal component is chosen as the normalized linear combination of features $X_1$,..., $X_p$ that maximizes the (remaining) variance out of all linear combinations that are uncorrelated with $Z_1$.

    And so on and so forth until all principal components are computed.

    In practice, just as you saw on Monday/Tuesday...

```{r}
pca_recipe <- recipe(credit_score ~ ., data = df) %>% 
    update_role(month,occupation,credit_mix,payment_of_min_amount,payment_behaviour,new_role = "ID") %>%
  step_zv(all_numeric_predictors()) %>%
  step_normalize(all_numeric_predictors()) %>% 
  step_pca(all_numeric_predictors(), keep_original_cols=TRUE , num_comp = 2)
```

```{r}
pca_results <- pca_recipe%>% prep() %>%
               bake(df)
pca_results %>%
  select(PC1, PC2) %>%
  head()
```

```{r}
ggplot(pca_results, aes(PC1, PC2)) +
  geom_point(alpha=0.3,color="#7038d1") +
  ggtitle("Principal Components", subtitle = "2-dimensional representation of our 20+ numeric predictors")
```

```{r}
# this is a little awkward but we just want to 
# extract the fitted PCA that is hidden inside the recipe
pca_recipe_prepped <- pca_recipe %>% prep()
pca_fit <- pca_recipe_prepped$steps[[3]]$res

# the eigenvalues determine the amount of variance explained
eigenvalues <-
  pca_fit %>%
  broom::tidy(matrix = "eigenvalues")

eigenvalues %>%
  filter(PC <= 23) %>%
  ggplot(aes(PC, cumulative)) +
  geom_col()+
  geom_text(aes(label = cumulative), vjust = -0.2)
```

My first two principal components only explain about 24% of the variance of my data. If I were to only keep these two, quite some information would be lost.

To keep between from 71 to 83 of my variance, I need to keep from 10 to 12 PCs.

How do I make the difference? Generally it comes down to the difference in the model, I build after this. In this case, I am interested in predicting credit score.

Let me build an XGBoost model to predict such a score.

But before I do, for computational reasons, I'll take a slice of the dataset.

```{r}
df_slice <- df %>% slice_sample(prop = 0.05)
```

Let's split our data

```{r}
set.seed(12345)
data_split <- initial_split(df_slice, strata = "credit_score", prop = 0.8)

train_data <- training(data_split)
test_data  <- testing(data_split)

paste("We have", nrow(train_data), "training points.")
paste("And", nrow(test_data), "test points.")
```

And prepare for cross-validation

```{r}
folds <- vfold_cv(train_data, v = 5)
```

Let's modify our recipe slightly to tune the number of principal components

```{r}
pca_tune_recipe <-
  recipe(credit_score ~ ., data = train_data) %>%
  update_role(month,occupation,credit_mix,payment_of_min_amount,payment_behaviour,new_role = "ID") %>%
  step_zv(all_numeric_predictors()) %>%
  step_normalize(all_numeric_predictors()) %>%
  step_pca(all_numeric_predictors(), num_comp=tune())
```

```{r}
xgb_model <-
  boost_tree(mtry = .cols(), trees = 250,
             learn_rate = tune()) %>% 
  set_mode("classification") %>% 
  set_engine("xgboost")
```

```{r}
xgb_wf <-
  workflow() %>% 
  add_recipe(pca_recipe) %>% 
  add_model(xgb_model)
```

```{r}
tune_workflow <- 
  workflow() %>% 
  add_model(xgb_model) %>% 
  add_recipe(pca_tune_recipe)


pca_grid <- expand.grid(num_comp = c(5,10,11,12,15), 
                        learn_rate=c(0.001, 0.01,0.1,0.2))

tune_results <-
  tune_grid(tune_workflow, 
            resamples = folds, 
            grid = pca_grid)


collect_metrics(tune_results) %>%
  filter(.metric == "roc_auc") %>%
  ggplot(aes(x = factor(num_comp), y = factor(learn_rate), fill = mean)) +
  geom_tile() +
  scale_fill_viridis_c() +
  labs(
    title="Grid Search Results", x = 'Number of Principal Components', 
    y = 'XGBoost Learn Rate',fill = 'AUC'
  ) +
  geom_text(aes(label = round(mean, 3)), color = "white", size = 3, fontface = "bold") +
  custom_theme()
```

What is the best model based on this plot?

Finally, we choose a final model, retrain it on the full dataset, and compute the final test set metrics.

We'll select a model by using the `select_by_one_std_err` function which selects the simplest model that is within one standard error of the best model.

```{r}
chosen_model = tune_results %>%
  select_by_one_std_err(metric = "roc_auc", desc(learn_rate), desc(num_comp))

final_wf <- 
  tune_workflow %>% 
  finalize_workflow(chosen_model)

final_fit <- 
  final_wf %>%
  last_fit(data_split)

final_fit %>%
  collect_metrics()
```

If you want additional explanations on PCA, check out this [Youtube video](https://www.youtube.com/watch?v=FgakZw6K1QQ) or this [link](https://www.sartorius.com/en/knowledge/science-snippets/what-is-principal-component-analysis-pca-and-how-it-is-used-507186#:~:text=Principal%20component%20analysis%2C%20or%20PCA,more%20easily%20visualized%20and%20analyzed.) or [this link](https://towardsdatascience.com/theory-of-principal-component-analysis-pca-and-implementation-on-python-5d4839f9ae89) (more theoretical).

For even more details on PCA, have a look at chapter 12 of Gareth, James, Witten Daniela, Hastie Trevor, and Tibshirani Robert. *An introduction to statistical learning: with applications in R*. Second Edition. Springer, 2021 (it's available online \[here\](<https://hastie.su.domains/ISLR2/ISLRv2_corrected_June_2023.pdf.download.html>)).

### What are the shortcomings of PCA?

-   It needs variables to be normalized before being applied (otherwise some might have an outsize effect on the selection!). And similarly, it is sensitive to outliers (it is recommended to remove outliers before applying PCA - which you might not want to do depending on your application!).

-   It assumes linear relationships between features. The algorithm is not well suited for capturing non-linear relationships.

-   It assumes a correlation between features. If the features are not correlated, PCA will be unable to determine principal components.

-   It assumes no missing values in the data so missing values need to be handled prior to applying it.

-   Rather obvious: it only applies to numerical values!

### 3.3 UMAP

The fact that we had to select so many principal components indicates that the geometry of our data is more complex than can be represented by a linear dimensionality reduction technique.

That's where a technique like UMAP (**Uniform Manifold Approximation and Projection**) comes in.

Let's have a look at [this video](https://www.youtube.com/watch?v=eN0wFzBA4Sc) to understand how UMAP works.

Let's apply it to our data.

The only difference here is in the recipe:

```{r}
umap_tune_recipe <-
  recipe(credit_score ~ ., data = train_data) %>%
  update_role(month,occupation,credit_mix,payment_of_min_amount,payment_behaviour,new_role = "ID") %>%
  step_zv(all_numeric_predictors()) %>%
  step_normalize(all_numeric_predictors()) %>%
  step_umap(all_numeric_predictors(), num_comp=tune())
```

Just as before, we tune our XGBoost model:

```{r}
tune_workflow <- 
  workflow() %>% 
  add_model(xgb_model) %>% 
  add_recipe(umap_tune_recipe)


umap_grid <- expand.grid(num_comp = c(5,10,11,12,15), 
                        learn_rate=c(0.001, 0.01,0.1,0.2))

tune_results <-
  tune_grid(tune_workflow, 
            resamples = folds, 
            grid = umap_grid)


collect_metrics(tune_results) %>%
  filter(.metric == "roc_auc") %>%
  ggplot(aes(x = factor(num_comp), y = factor(learn_rate), fill = mean)) +
  geom_tile() +
  scale_fill_viridis_c() +
  labs(
    title="Grid Search Results", x = 'Number of UMAP Components', 
    y = 'XGBoost Learn Rate',fill = 'AUC'
  ) +
  geom_text(aes(label = round(mean, 3)), color = "white", size = 3, fontface = "bold") +
  custom_theme()
```

UMAP retains fewer dimensions than PCA (at the expense of a slight loss of performance). But let's do like earlier and choose a final model, retrain it on the full dataset, and compute the final test set metrics.

```{r}
chosen_model = tune_results %>%
  select_by_one_std_err(metric = "roc_auc", desc(learn_rate), desc(num_comp))

final_wf <- 
  tune_workflow %>% 
  finalize_workflow(chosen_model)

final_fit <- 
  final_wf %>%
  last_fit(data_split)

final_fit %>%
  collect_metrics()
```

When you look at final metrics, UMAP actually has a slight edge over PCA (and would also have a computational edge since the number of dimensions retained is lower!).

### **Advantages of UMAP**

1.  UMAP is capable of preserving the global structure of the data, including topological structure and distances between points, making it a good choice for visualization and exploration.

2.  UMAP is computationally efficient and scalable, making it suitable for large datasets.

3.  UMAP can handle high-dimensional data, unlike some other dimensionality reduction methods that are limited to (rather) low-dimensional data.

### **Disadvantages of UMAP**

1.  UMAP is a relatively new method and has not been as thoroughly tested and validated as some other dimensionality reduction methods.

2.  UMAP may not perform well on data with complex non-linear structure or on data with non-uniform density.

    See [here](https://umap-learn.readthedocs.io/en/latest/faq.html) for further information/advice on UMAP.
