You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ML-For-Beginners/translations/en/5-Clustering/1-Visualize/solution/R/lesson_14-R.ipynb

493 lines
26 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"cells": [
{
"cell_type": "markdown",
"source": [
"## **Nigerian Music scraped from Spotify - an analysis**\n",
"\n",
"Clustering is a type of [Unsupervised Learning](https://wikipedia.org/wiki/Unsupervised_learning) that assumes a dataset is unlabelled or that its inputs are not paired with predefined outputs. It uses various algorithms to sift through unlabeled data and create groupings based on patterns it identifies in the data.\n",
"\n",
"[**Pre-lecture quiz**](https://gray-sand-07a10f403.1.azurestaticapps.net/quiz/27/)\n",
"\n",
"### **Introduction**\n",
"\n",
"[Clustering](https://link.springer.com/referenceworkentry/10.1007%2F978-0-387-30164-8_124) is highly useful for exploring data. Let's see if it can help uncover trends and patterns in how Nigerian audiences consume music.\n",
"\n",
"> ✅ Take a moment to think about the applications of clustering. In everyday life, clustering happens when you sort a pile of laundry into family members' clothes 🧦👕👖🩲. In data science, clustering occurs when analyzing user preferences or identifying characteristics in an unlabeled dataset. Clustering, in a way, helps bring order to chaos, like organizing a sock drawer.\n",
"\n",
"In a professional context, clustering can be used for tasks like market segmentation, such as identifying which age groups purchase specific items. Another application is anomaly detection, for instance, identifying fraud in a dataset of credit card transactions. It can also be used to detect tumors in medical scans.\n",
"\n",
"✅ Take a moment to think about how you might have encountered clustering in real-world scenarios, such as in banking, e-commerce, or business.\n",
"\n",
"> 🎓 Interestingly, cluster analysis originated in the fields of Anthropology and Psychology in the 1930s. Can you imagine how it might have been applied back then?\n",
"\n",
"Alternatively, clustering can be used to group search results—for example, by shopping links, images, or reviews. It is particularly useful for large datasets that need to be reduced for more detailed analysis, making it a valuable technique for understanding data before building other models.\n",
"\n",
"✅ Once your data is organized into clusters, you assign it a cluster ID. This approach can be helpful for preserving a dataset's privacy, as you can refer to a data point by its cluster ID rather than by more identifiable information. Can you think of other reasons why you might use a cluster ID instead of other elements of the cluster for identification?\n",
"\n",
"### Getting started with clustering\n",
"\n",
"> 🎓 How we create clusters depends heavily on how we group data points. Let's break down some key terms:\n",
">\n",
"> 🎓 ['Transductive' vs. 'inductive'](https://wikipedia.org/wiki/Transduction_(machine_learning))\n",
">\n",
"> Transductive inference is derived from observed training cases that map to specific test cases. Inductive inference is derived from training cases that map to general rules, which are then applied to test cases.\n",
">\n",
"> Example: Imagine you have a dataset that is only partially labeled. Some items are 'records,' some are 'CDs,' and others are blank. Your task is to label the blanks. If you use an inductive approach, you'd train a model to identify 'records' and 'CDs' and apply those labels to the unlabeled data. This approach might struggle to classify items that are actually 'cassettes.' A transductive approach, however, handles unknown data more effectively by grouping similar items together and then applying a label to the group. In this case, clusters might represent 'round musical items' and 'square musical items.'\n",
">\n",
"> 🎓 ['Non-flat' vs. 'flat' geometry](https://datascience.stackexchange.com/questions/52260/terminology-flat-geometry-in-the-context-of-clustering)\n",
">\n",
"> Derived from mathematical terminology, non-flat vs. flat geometry refers to how distances between points are measured—either using 'flat' ([Euclidean](https://wikipedia.org/wiki/Euclidean_geometry)) or 'non-flat' (non-Euclidean) methods.\n",
">\n",
"> 'Flat' refers to Euclidean geometry (often taught as 'plane' geometry), while 'non-flat' refers to non-Euclidean geometry. What does geometry have to do with machine learning? Since both fields are rooted in mathematics, there must be a common way to measure distances between points in clusters. This can be done in a 'flat' or 'non-flat' manner, depending on the nature of the data. [Euclidean distances](https://wikipedia.org/wiki/Euclidean_distance) are measured as the length of a straight line between two points. [Non-Euclidean distances](https://wikipedia.org/wiki/Non-Euclidean_geometry) are measured along a curve. If your data, when visualized, doesn't exist on a plane, you may need a specialized algorithm to handle it.\n",
"\n",
"<p>\n",
" <img src=\"../../images/flat-nonflat.png\"\n",
" width=\"600\"/>\n",
" <figcaption>Infographic by Dasani Madipalli</figcaption>\n",
"\n",
"> 🎓 ['Distances'](https://web.stanford.edu/class/cs345a/slides/12-clustering.pdf)\n",
">\n",
"> Clusters are defined by their distance matrix, which measures the distances between points. This distance can be calculated in several ways. Euclidean clusters are defined by the average of the point values and have a 'centroid' or center point. Distances are measured relative to that centroid. Non-Euclidean distances refer to 'clustroids,' the point closest to other points. Clustroids can be defined in various ways.\n",
">\n",
"> 🎓 ['Constrained'](https://wikipedia.org/wiki/Constrained_clustering)\n",
">\n",
"> [Constrained Clustering](https://web.cs.ucdavis.edu/~davidson/Publications/ICDMTutorial.pdf) introduces 'semi-supervised' learning into this unsupervised method. Relationships between points are flagged as 'cannot link' or 'must-link,' imposing some rules on the dataset.\n",
">\n",
"> Example: If an algorithm is applied to a batch of unlabeled or semi-labeled data, the resulting clusters may be of poor quality. For instance, the clusters might group 'round musical items,' 'square musical items,' 'triangular items,' and 'cookies.' By adding constraints or rules (\"the item must be made of plastic,\" \"the item must produce music\"), the algorithm can make better choices.\n",
">\n",
"> 🎓 'Density'\n",
">\n",
"> Data that is 'noisy' is considered 'dense.' The distances between points in its clusters may vary, requiring the use of appropriate clustering methods. [This article](https://www.kdnuggets.com/2020/02/understanding-density-based-clustering.html) explains the difference between using K-Means clustering and HDBSCAN algorithms to analyze a noisy dataset with uneven cluster density.\n",
"\n",
"Deepen your understanding of clustering techniques in this [Learn module](https://docs.microsoft.com/learn/modules/train-evaluate-cluster-models?WT.mc_id=academic-77952-leestott)\n",
"\n",
"### **Clustering algorithms**\n",
"\n",
"There are over 100 clustering algorithms, and their application depends on the nature of the data. Let's explore some of the major ones:\n",
"\n",
"- **Hierarchical clustering**. Objects are classified based on their proximity to nearby objects rather than distant ones. Clusters are formed by repeatedly combining two clusters.\n",
"\n",
"<p>\n",
" <img src=\"../../images/hierarchical.png\"\n",
" width=\"600\"/>\n",
" <figcaption>Infographic by Dasani Madipalli</figcaption>\n",
"\n",
"- **Centroid clustering**. This popular algorithm requires selecting 'k,' the number of clusters to form. The algorithm then determines the center point of a cluster and gathers data around it. [K-means clustering](https://wikipedia.org/wiki/K-means_clustering) is a widely used version of centroid clustering that divides a dataset into pre-defined K groups. The center is determined by the nearest mean, hence the name. The squared distance from the cluster is minimized.\n",
"\n",
"<p>\n",
" <img src=\"../../images/centroid.png\"\n",
" width=\"600\"/>\n",
" <figcaption>Infographic by Dasani Madipalli</figcaption>\n",
"\n",
"- **Distribution-based clustering**. Based on statistical modeling, this method assigns data points to clusters based on the probability of their belonging to a cluster. Gaussian mixture methods fall under this category.\n",
"\n",
"- **Density-based clustering**. Data points are grouped into clusters based on their density or proximity to one another. Points far from the group are considered outliers or noise. DBSCAN, Mean-shift, and OPTICS are examples of this type of clustering.\n",
"\n",
"- **Grid-based clustering**. For multi-dimensional datasets, a grid is created, and the data is divided among the grid's cells, forming clusters.\n",
"\n",
"The best way to learn about clustering is to try it yourself, which you'll do in this exercise.\n",
"\n",
"We'll need some packages to complete this module. You can install them using: `install.packages(c('tidyverse', 'tidymodels', 'DataExplorer', 'summarytools', 'plotly', 'paletteer', 'corrplot', 'patchwork'))`\n",
"\n",
"Alternatively, the script below checks whether you have the required packages and installs any missing ones for you.\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"suppressWarnings(if(!require(\"pacman\")) install.packages(\"pacman\"))\r\n",
"\r\n",
"pacman::p_load('tidyverse', 'tidymodels', 'DataExplorer', 'summarytools', 'plotly', 'paletteer', 'corrplot', 'patchwork')\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"## Exercise - cluster your data\n",
"\n",
"Clustering is a technique that benefits significantly from good visualization, so let's begin by visualizing our music data. This exercise will help us determine which clustering method would be most effective for the characteristics of this data.\n",
"\n",
"Let's dive right in by importing the data.\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Load the core tidyverse and make it available in your current R session\r\n",
"library(tidyverse)\r\n",
"\r\n",
"# Import the data into a tibble\r\n",
"df <- read_csv(file = \"https://raw.githubusercontent.com/microsoft/ML-For-Beginners/main/5-Clustering/data/nigerian-songs.csv\")\r\n",
"\r\n",
"# View the first 5 rows of the data set\r\n",
"df %>% \r\n",
" slice_head(n = 5)\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Sometimes, we might want a bit more information about our data. We can examine the `data` and `its structure` using the [*glimpse()*](https://pillar.r-lib.org/reference/glimpse.html) function:\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Glimpse into the data set\r\n",
"df %>% \r\n",
" glimpse()\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Good job!💪\n",
"\n",
"We can see that `glimpse()` provides the total number of rows (observations) and columns (variables), followed by the first few entries of each variable in a row after the variable name. Additionally, the *data type* of the variable is displayed right after the variable's name within `< >`.\n",
"\n",
"`DataExplorer::introduce()` can organize this information in a concise way:\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Describe basic information for our data\r\n",
"df %>% \r\n",
" introduce()\r\n",
"\r\n",
"# A visual display of the same\r\n",
"df %>% \r\n",
" plot_intro()\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Awesome! We just learned that our data doesn't have any missing values.\n",
"\n",
"While we're at it, we can explore common statistics for central tendency (e.g., [mean](https://en.wikipedia.org/wiki/Arithmetic_mean) and [median](https://en.wikipedia.org/wiki/Median)) and measures of dispersion (e.g., [standard deviation](https://en.wikipedia.org/wiki/Standard_deviation)) using `summarytools::descr()`.\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Describe common statistics\r\n",
"df %>% \r\n",
" descr(stats = \"common\")\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Let's examine the general values of the data. Keep in mind that popularity can be `0`, which indicates songs that have no ranking. We'll filter those out shortly.\n",
"\n",
"> 🤔 If we're using clustering, an unsupervised method that doesn't rely on labeled data, why are we displaying this data with labels? During the data exploration phase, labels can be useful, but they aren't required for clustering algorithms to function.\n",
"\n",
"### 1. Explore popular genres\n",
"\n",
"Let's dive in and identify the most popular genres 🎶 by counting how often each one appears.\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Popular genres\r\n",
"top_genres <- df %>% \r\n",
" count(artist_top_genre, sort = TRUE) %>% \r\n",
"# Encode to categorical and reorder the according to count\r\n",
" mutate(artist_top_genre = factor(artist_top_genre) %>% fct_inorder())\r\n",
"\r\n",
"# Print the top genres\r\n",
"top_genres\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"That went well! They say a picture is worth a thousand rows of a data frame (actually, nobody ever says that 😅). But you get the idea, right?\n",
"\n",
"One way to visualize categorical data (character or factor variables) is by using bar plots. Let's create a bar plot for the top 10 genres:\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Change the default gray theme\r\n",
"theme_set(theme_light())\r\n",
"\r\n",
"# Visualize popular genres\r\n",
"top_genres %>%\r\n",
" slice(1:10) %>% \r\n",
" ggplot(mapping = aes(x = artist_top_genre, y = n,\r\n",
" fill = artist_top_genre)) +\r\n",
" geom_col(alpha = 0.8) +\r\n",
" paletteer::scale_fill_paletteer_d(\"rcartocolor::Vivid\") +\r\n",
" ggtitle(\"Top genres\") +\r\n",
" theme(plot.title = element_text(hjust = 0.5),\r\n",
" # Rotates the X markers (so we can read them)\r\n",
" axis.text.x = element_text(angle = 90))\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Now it's much easier to spot that we have `missing` genres 🧐!\n",
"\n",
"> A good visualization will reveal things you didn't anticipate or spark new questions about the data - Hadley Wickham and Garrett Grolemund, [R For Data Science](https://r4ds.had.co.nz/introduction.html)\n",
"\n",
"Keep in mind, when the top genre is labeled as `Missing`, it means Spotify didn't categorize it, so let's remove it.\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Visualize popular genres\r\n",
"top_genres %>%\r\n",
" filter(artist_top_genre != \"Missing\") %>% \r\n",
" slice(1:10) %>% \r\n",
" ggplot(mapping = aes(x = artist_top_genre, y = n,\r\n",
" fill = artist_top_genre)) +\r\n",
" geom_col(alpha = 0.8) +\r\n",
" paletteer::scale_fill_paletteer_d(\"rcartocolor::Vivid\") +\r\n",
" ggtitle(\"Top genres\") +\r\n",
" theme(plot.title = element_text(hjust = 0.5),\r\n",
" # Rotates the X markers (so we can read them)\r\n",
" axis.text.x = element_text(angle = 90))\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"From the initial data exploration, we observe that the top three genres dominate this dataset. Let's focus on `afro dancehall`, `afropop`, and `nigerian pop`, and further filter the dataset to exclude any entries with a popularity value of 0 (indicating they were not assigned a popularity score in the dataset and can be considered irrelevant for our analysis):\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"nigerian_songs <- df %>% \r\n",
" # Concentrate on top 3 genres\r\n",
" filter(artist_top_genre %in% c(\"afro dancehall\", \"afropop\",\"nigerian pop\")) %>% \r\n",
" # Remove unclassified observations\r\n",
" filter(popularity != 0)\r\n",
"\r\n",
"\r\n",
"\r\n",
"# Visualize popular genres\r\n",
"nigerian_songs %>%\r\n",
" count(artist_top_genre) %>%\r\n",
" ggplot(mapping = aes(x = artist_top_genre, y = n,\r\n",
" fill = artist_top_genre)) +\r\n",
" geom_col(alpha = 0.8) +\r\n",
" paletteer::scale_fill_paletteer_d(\"ggsci::category10_d3\") +\r\n",
" ggtitle(\"Top genres\") +\r\n",
" theme(plot.title = element_text(hjust = 0.5))\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"Let's examine whether there is any obvious linear relationship among the numerical variables in our dataset. This relationship is measured mathematically using the [correlation statistic](https://en.wikipedia.org/wiki/Correlation).\n",
"\n",
"The correlation statistic is a value ranging from -1 to 1 that reflects the strength of a relationship. Values greater than 0 indicate a *positive* correlation (high values of one variable are generally associated with high values of the other), whereas values less than 0 indicate a *negative* correlation (high values of one variable are generally associated with low values of the other).\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Narrow down to numeric variables and fid correlation\r\n",
"corr_mat <- nigerian_songs %>% \r\n",
" select(where(is.numeric)) %>% \r\n",
" cor()\r\n",
"\r\n",
"# Visualize correlation matrix\r\n",
"corrplot(corr_mat, order = 'AOE', col = c('white', 'black'), bg = 'gold2') \r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"The data doesn't show strong correlations, except between `energy` and `loudness`, which makes sense since loud music is typically quite energetic. `Popularity` is related to `release date`, which also makes sense because newer songs are likely to be more popular. There also seems to be a correlation between length and energy.\n",
"\n",
"It will be interesting to see what insights a clustering algorithm can uncover from this data!\n",
"\n",
"> 🎓 Remember, correlation does not imply causation! While we can confirm correlation, we have no evidence of causation. An [entertaining website](https://tylervigen.com/spurious-correlations) provides visuals that highlight this concept.\n",
"\n",
"### 2. Explore data distribution\n",
"\n",
"Lets dive deeper with some nuanced questions. Are genres significantly different in how their danceability is perceived, based on their popularity? To investigate, lets analyze the data distribution for popularity and danceability in our top three genres along a specified x and y axis using [density plots](https://www.khanacademy.org/math/ap-statistics/density-curves-normal-distribution-ap/density-curves/v/density-curves).\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Perform 2D kernel density estimation\r\n",
"density_estimate_2d <- nigerian_songs %>% \r\n",
" ggplot(mapping = aes(x = popularity, y = danceability, color = artist_top_genre)) +\r\n",
" geom_density_2d(bins = 5, size = 1) +\r\n",
" paletteer::scale_color_paletteer_d(\"RSkittleBrewer::wildberry\") +\r\n",
" xlim(-20, 80) +\r\n",
" ylim(0, 1.2)\r\n",
"\r\n",
"# Density plot based on the popularity\r\n",
"density_estimate_pop <- nigerian_songs %>% \r\n",
" ggplot(mapping = aes(x = popularity, fill = artist_top_genre, color = artist_top_genre)) +\r\n",
" geom_density(size = 1, alpha = 0.5) +\r\n",
" paletteer::scale_fill_paletteer_d(\"RSkittleBrewer::wildberry\") +\r\n",
" paletteer::scale_color_paletteer_d(\"RSkittleBrewer::wildberry\") +\r\n",
" theme(legend.position = \"none\")\r\n",
"\r\n",
"# Density plot based on the danceability\r\n",
"density_estimate_dance <- nigerian_songs %>% \r\n",
" ggplot(mapping = aes(x = danceability, fill = artist_top_genre, color = artist_top_genre)) +\r\n",
" geom_density(size = 1, alpha = 0.5) +\r\n",
" paletteer::scale_fill_paletteer_d(\"RSkittleBrewer::wildberry\") +\r\n",
" paletteer::scale_color_paletteer_d(\"RSkittleBrewer::wildberry\")\r\n",
"\r\n",
"\r\n",
"# Patch everything together\r\n",
"library(patchwork)\r\n",
"density_estimate_2d / (density_estimate_pop + density_estimate_dance)\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"We observe concentric circles aligning, regardless of the genre. Could it be that Nigerian preferences converge at a specific level of danceability for this genre?\n",
"\n",
"Overall, the three genres show similarities in terms of their popularity and danceability. Identifying clusters within this loosely aligned data will be tricky. Let's check if a scatter plot can provide some clarity.\n"
],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# A scatter plot of popularity and danceability\r\n",
"scatter_plot <- nigerian_songs %>% \r\n",
" ggplot(mapping = aes(x = popularity, y = danceability, color = artist_top_genre, shape = artist_top_genre)) +\r\n",
" geom_point(size = 2, alpha = 0.8) +\r\n",
" paletteer::scale_color_paletteer_d(\"futurevisions::mars\")\r\n",
"\r\n",
"# Add a touch of interactivity\r\n",
"ggplotly(scatter_plot)\r\n"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "markdown",
"source": [
"A scatterplot of the same axes reveals a similar pattern of convergence.\n",
"\n",
"In general, scatterplots are useful for visualizing clusters in data, making them an essential tool for clustering tasks. In the next lesson, we will take this filtered data and apply k-means clustering to identify groups within the data that exhibit interesting overlaps.\n",
"\n",
"## **🚀 Challenge**\n",
"\n",
"To prepare for the next lesson, create a chart outlining various clustering algorithms that you might encounter and use in a production environment. What types of problems are these clustering methods designed to solve?\n",
"\n",
"## [**Post-lecture quiz**](https://gray-sand-07a10f403.1.azurestaticapps.net/quiz/28/)\n",
"\n",
"## **Review & Self Study**\n",
"\n",
"Before applying clustering algorithms, as we've learned, it's important to understand the characteristics of your dataset. Learn more about this topic [here](https://www.kdnuggets.com/2019/10/right-clustering-algorithm.html).\n",
"\n",
"Expand your knowledge of clustering techniques:\n",
"\n",
"- [Train and Evaluate Clustering Models using Tidymodels and friends](https://rpubs.com/eR_ic/clustering)\n",
"\n",
"- Bradley Boehmke & Brandon Greenwell, [*Hands-On Machine Learning with R*](https://bradleyboehmke.github.io/HOML/)*.*\n",
"\n",
"## **Assignment**\n",
"\n",
"[Explore other visualizations for clustering](https://github.com/microsoft/ML-For-Beginners/blob/main/5-Clustering/1-Visualize/assignment.md)\n",
"\n",
"## THANK YOU TO:\n",
"\n",
"[Jen Looper](https://www.twitter.com/jenlooper) for creating the original Python version of this module ♥️\n",
"\n",
"[`Dasani Madipalli`](https://twitter.com/dasani_decoded) for designing the incredible illustrations that make machine learning concepts more accessible and easier to grasp.\n",
"\n",
"Happy Learning,\n",
"\n",
"[Eric](https://twitter.com/ericntay), Gold Microsoft Learn Student Ambassador.\n"
],
"metadata": {}
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Disclaimer**: \nThis document has been translated using the AI translation service [Co-op Translator](https://github.com/Azure/co-op-translator). While we aim for accuracy, please note that automated translations may include errors or inaccuracies. The original document in its native language should be regarded as the authoritative source. For critical information, professional human translation is advised. We are not responsible for any misunderstandings or misinterpretations resulting from the use of this translation.\n"
]
}
],
"metadata": {
"anaconda-cloud": "",
"kernelspec": {
"display_name": "R",
"language": "R",
"name": "ir"
},
"language_info": {
"codemirror_mode": "r",
"file_extension": ".r",
"mimetype": "text/x-r-source",
"name": "R",
"pygments_lexer": "r",
"version": "3.4.1"
},
"coopTranslator": {
"original_hash": "99c36449cad3708a435f6798cfa39972",
"translation_date": "2025-09-06T15:35:17+00:00",
"source_file": "5-Clustering/1-Visualize/solution/R/lesson_14-R.ipynb",
"language_code": "en"
}
},
"nbformat": 4,
"nbformat_minor": 1
}