{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Miscellaneous workflows with Datalab\n", "\n", "This tutorial demonstrates various useful things you can do with `Datalab` that may not be covered in other tutorials. First get familiar with `Datalab` via the [quickstart](datalab_quickstart.html)/[advanced](datalab_advanced.html) tutorials before going through this one." ] }, { "cell_type": "markdown", "metadata": { "jp-MarkdownHeadingCollapsed": true }, "source": [ "## Accelerate Issue Checks with Pre-computed kNN Graphs\n", "\n", "By default, `Datalab` will detect certain types of issues by constructing a k-nearest neighbors graph of your dataset using the [scikit-learn](https://scikit-learn.org/stable/modules/neighbors.html) package. Here we demonstrate how to use your own pre-computed k-nearest neighbors (kNN) graphs with `Datalab`. This allows you to use more efficient approximate kNN graphs to scale to bigger datasets.\n", "\n", "Using pre-computed kNN graphs is optional and not required for `Datalab` to function. `Datalab` can automatically compute these graphs for you.\n", "\n", "While we use a toy dataset for demonstration, these steps can be applied to any dataset." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Load and Prepare Your Dataset\n", "\n", "Here we'll generate a synthetic dataset, but you should replace this with your own dataset loading process." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from sklearn.datasets import make_classification\n", "\n", "# Set seed for reproducibility\n", "np.random.seed(0)\n", "\n", "# Replace this section with your own dataset loading\n", "# For demonstration, we create a synthetic classification dataset\n", "X, y = make_classification(\n", " n_samples=5000,\n", " n_features=5,\n", " n_informative=5,\n", " n_redundant=0,\n", " n_repeated=0,\n", " n_classes=2,\n", " n_clusters_per_class=2,\n", " flip_y=0.02,\n", " class_sep=2.0,\n", " shuffle=False,\n", " random_state=0,\n", ")\n", "\n", "\n", "# Example: Add a duplicate example to the dataset\n", "X[-1] = X[-2] + np.random.rand(5) * 0.001" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Compute kNN Graph\n", "\n", "We will compute the kNN graph using [FAISS](https://github.com/facebookresearch/faiss), a library for efficient similarity search. This step involves creating a kNN graph that represents the nearest neighbors for each point in your dataset." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import faiss\n", "import numpy as np\n", "\n", "# Faiss uses single precision, so we need to convert the data type\n", "X_faiss = np.float32(X)\n", "\n", "# Normalize the vectors for inner product similarity (effectively cosine similarity)\n", "faiss.normalize_L2(X_faiss)\n", "\n", "# Build the index using FAISS\n", "index = faiss.index_factory(X_faiss.shape[1], \"HNSW32,Flat\", faiss.METRIC_INNER_PRODUCT)\n", "\n", "# Add the dataset to the index\n", "index.add(X_faiss)\n", "\n", "# Perform the search to find k-nearest neighbors\n", "k = 10 # Number of neighbors to consider\n", "D, I = index.search(X_faiss, k + 1) # Include the point itself during search\n", "\n", "# Remove the first column (self-distances)\n", "D, I = D[:, 1:], I[:, 1:]\n", "\n", "# Convert cosine similarity to cosine distance\n", "np.clip(1 - D, a_min=0, a_max=None, out=D)\n", "\n", "# Create the kNN graph\n", "from scipy.sparse import csr_matrix\n", "\n", "\n", "def create_knn_graph(distances: np.ndarray, indices: np.ndarray) -> csr_matrix:\n", " \"\"\"\n", " Create a K-nearest neighbors (KNN) graph in CSR format from provided distances and indices.\n", "\n", " Parameters:\n", " distances (np.ndarray): 2D array of shape (n_samples, n_neighbors) containing distances to nearest neighbors.\n", " indices (np.ndarray): 2D array of shape (n_samples, n_neighbors) containing indices of nearest neighbors.\n", "\n", " Returns:\n", " scipy.sparse.csr_matrix: KNN graph in CSR format.\n", " \"\"\"\n", " assert distances.shape == indices.shape, \"distances and indices must have the same shape\"\n", "\n", " n_samples, n_neighbors = distances.shape\n", "\n", " # Convert to 1D arrays for CSR matrix creation\n", " indices_1d = indices.ravel()\n", " distances_1d = distances.ravel()\n", " indptr = np.arange(0, n_samples * n_neighbors + 1, n_neighbors)\n", "\n", " # Create the CSR matrix\n", " return csr_matrix((distances_1d, indices_1d, indptr), shape=(n_samples, n_samples))\n", "\n", "\n", "knn_graph = create_knn_graph(D, I)\n", "\n", "# Ensure the kNN graph is sorted by row values\n", "from sklearn.neighbors import sort_graph_by_row_values\n", "sort_graph_by_row_values(knn_graph, copy=False, warn_when_not_sorted=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Train a Classifier and Obtain Predicted Probabilities\n", "\n", "Predicted class probabilities from a model trained on your dataset are used to identify label issues." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.linear_model import LogisticRegression\n", "from sklearn.model_selection import cross_val_predict\n", "\n", "# Obtain predicted probabilities using cross-validation\n", "clf = LogisticRegression()\n", "pred_probs = cross_val_predict(clf, X, y, cv=3, method=\"predict_proba\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. Identify Data Issues Using Datalab\n", "Use the pre-computed kNN graph and predicted probabilities to find issues in the dataset using `Datalab`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from cleanlab import Datalab\n", "\n", "# Initialize Datalab with the dataset\n", "lab = Datalab(data={\"X\": X, \"y\": y}, label_name=\"y\", task=\"classification\")\n", "\n", "# Perform issue detection using the kNN graph and predicted probabilities, when possible\n", "lab.find_issues(knn_graph=knn_graph, pred_probs=pred_probs, features=X)\n", "\n", "# Collect the identified issues and a summary\n", "issues = lab.get_issues()\n", "issue_summary = lab.get_issue_summary()\n", "\n", "# Display the issues and summary\n", "display(issue_summary)\n", "display(issues)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Explanation:\n", "\n", "**Creating the kNN Graph:**\n", "\n", "- Compute the kNN graph using FAISS or another library, ensuring the self-points (points referring to themselves) are omitted from the neighbors.\n", " - Some distance kernels or search algorithms (like those in FAISS) may return negative distances or suffer from numerical instability when comparing\n", " points that are extremely close to each other. This can lead to incorrect results when constructing the kNN graph.\n", " - **Note**: kNN graphs are generally poorly suited for detecting exact duplicates, especially when the number of exact duplicates exceeds the number of requested neighbors. The strengths of this data structure lie in the assumption that data points are similar but not identical, allowing efficient similarity searches and proximity-based analyses.\n", " - If you are comfortable with exploring non-public API functions in the library, you can use the following helper function to ensure that exact duplicate sets are correctly represented in the kNN graph. Please note, this function is not officially supported and is not part of the public API:\n", "\n", " ```python\n", " from cleanlab.internal.neighbor.knn_graph import correct_knn_graph\n", "\n", " knn_graph = correct_knn_graph(features=X_faiss, knn_graph=knn_graph)\n", " ```\n", "- You may need to handle self-points yourself with third-party libraries.\n", "- Construct the CSR (Compressed Sparse Row) matrix from the distances and indices arrays.\n", " - `Datalab` can automatically construct a kNN graph from a numerical `features` array if one is not provided, in an accurate and reliable manner.\n", "- Sort the kNN graph by row values.\n", "\n", "When using approximate kNN graphs, it is important to understand their strengths and limitations to apply them effectively." ] }, { "cell_type": "markdown", "metadata": { "jp-MarkdownHeadingCollapsed": true }, "source": [ "## Data Valuation\n", "\n", "In this section, we will show how to use `Datalab` to estimate how much each data point contributes to a trained classifier model. Data valuation helps you understand the importance of each data point, where you can identify more/less valuable data points for your machine learning models.\n", "\n", "We will use a text dataset for this example, but this approach can be applied to any dataset." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Load and Prepare the Dataset\n", "We will use a subset of the 20 Newsgroups dataset, which is a collection of newsgroup documents suitable for text classification tasks.\n", "For demonstration purposes, we'll classify documents from two categories: \"alt.atheism\" and \"sci.space\"." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.datasets import fetch_20newsgroups\n", "import pandas as pd\n", "\n", "# Load the 20 Newsgroups dataset\n", "newsgroups_train = fetch_20newsgroups(subset='train', categories=['alt.atheism', 'sci.space'], remove=('headers', 'footers', 'quotes'))\n", "\n", "# Create a DataFrame with the text data and labels\n", "df_text = pd.DataFrame({\"Text\": newsgroups_train.data, \"Label\": newsgroups_train.target})\n", "df_text[\"Label\"] = df_text[\"Label\"].map({i: category for (i, category) in enumerate(newsgroups_train.target_names)})\n", "\n", "# Display the first few samples\n", "df_text.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Vectorize the Text Data\n", "We will use a `TfidfVectorizer` to convert the text data into a numerical format suitable for machine learning models." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "\n", "# Initialize the TfidfVectorizer\n", "vectorizer = TfidfVectorizer()\n", "\n", "# Transform the text data into a feature matrix\n", "X_vectorized = vectorizer.fit_transform(df_text[\"Text\"])\n", "\n", "# Convert the sparse matrix to a dense matrix\n", "X = X_vectorized.toarray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Perform Data Valuation with Datalab\n", "\n", "Next, we will initialize `Datalab` and perform data valuation to assess the value of each data point in the dataset." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from cleanlab import Datalab\n", "\n", "# Initialize Datalab with the dataset\n", "lab = Datalab(data=df_text, label_name=\"Label\", task=\"classification\")\n", "\n", "# Perform data valuation\n", "lab.find_issues(features=X, issue_types={\"data_valuation\": {}})\n", "\n", "# Collect the identified issues\n", "data_valuation_issues = lab.get_issues(\"data_valuation\")\n", "\n", "# Display the data valuation issues\n", "display(data_valuation_issues)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. (Optional) Visualize Data Valuation Scores\n", "Let's visualize the data valuation scores across our dataset.\n", "\n", "Cleanlab's Shapely scores are transformed to lie between 0 and 1 such that: a score below 0.5 indicates a negative contribution to the model's training performance, while a score above 0.5 indicates a positive contribution.\n", "\n", "By examining the scores across different classes, we can identify whether positive or negative contributions are disproportionately concentrated in a single class. This can help detect biases in the training data." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import seaborn as sns\n", "import matplotlib.pyplot as plt\n", "\n", "# Prepare the data for plotting\n", "plot_data = (\n", " data_valuation_issues\n", " # Optionally, add a 'given_label' column to distinguish between labels in the histogram\n", " .join(pd.DataFrame({\"given_label\": df_text[\"Label\"]}))\n", ")\n", "\n", "# Plot strip plots of data valuation scores for each label\n", "sns.stripplot(\n", " data=plot_data,\n", " x=\"data_valuation_score\",\n", " hue=\"given_label\", # Comment out if no labels should be used in the visualization\n", " dodge=True,\n", " jitter=0.3,\n", " alpha=0.5,\n", ")\n", "\n", "plt.axvline(lab.info[\"data_valuation\"][\"threshold\"], color=\"red\", linestyle=\"--\", label=\"Issue Threshold\")\n", "\n", "plt.title(\"Strip plot of Data Valuation Scores by Label\")\n", "plt.xlabel(\"Data Valuation Score\")\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Learn more about the data valuation issue type in the [Issue Type Guide](../../cleanlab/datalab/guide/issue_type_description.html#data-valuation-issue)." ] }, { "cell_type": "markdown", "metadata": { "jp-MarkdownHeadingCollapsed": true }, "source": [ "## Find Underperforming Groups in a Dataset\n", "\n", "Here we will demonstrate how to use `Datalab` to identify subgroups in a dataset over which the ML model is producing consistently worse predictions than for the overall dataset.\n", "\n", "`Datalab` will automatically find underperforming groups if you provide numerical embeddings and predicted probabilities from any model.\n", "For this section, we'll determine which data subgroups to consider ourselves, such as by using clustering.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Generate a Synthetic Dataset\n", "\n", "First, we will generate a synthetic dataset with blobs. This dataset will include some noisy labels in one of the blobs." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.datasets import make_blobs\n", "import numpy as np\n", "\n", "# Generate synthetic data with blobs\n", "X, y = make_blobs(n_samples=100, centers=3, n_features=2, random_state=42, cluster_std=1.0, shuffle=False)\n", "\n", "# Add noise to the labels\n", "n_noisy_labels = 30\n", "y[:n_noisy_labels] = np.random.randint(0, 2, n_noisy_labels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Train a Classifier and Obtain Predicted Probabilities\n", "\n", "Next, we will train a basic classifier (you can use any type of model) and obtain predicted probabilities for the dataset using cross-validation." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.linear_model import LogisticRegression\n", "from sklearn.model_selection import cross_val_predict\n", "\n", "# Obtain predicted probabilities using cross-validation\n", "clf = LogisticRegression(random_state=0)\n", "pred_probs = cross_val_predict(clf, X, y, cv=3, method=\"predict_proba\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. (Optional) Cluster the Data\n", "\n", "Datalab identifies meaningful data subgroups by automatically clustering your dataset.\n", "You can optionally provide your own clusters to control this process. Here we show how to use KMeans clustering, but this manual clustering is entirely optional." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.cluster import KMeans\n", "from sklearn.metrics import silhouette_score\n", "from sklearn.model_selection import GridSearchCV\n", "\n", "\n", "# Function to use in GridSearchCV for silhouette score\n", "def silhouette_scorer(estimator, X):\n", " cluster_labels = estimator.fit_predict(X)\n", " return silhouette_score(X, cluster_labels)\n", "\n", "\n", "# Use GridSearchCV to determine the optimal number of clusters\n", "param_grid = {\"n_clusters\": range(2, 10)}\n", "grid_search = GridSearchCV(KMeans(random_state=0), param_grid, cv=3, scoring=silhouette_scorer)\n", "grid_search.fit(X)\n", "\n", "# Get the best estimator and predict clusters\n", "best_kmeans = grid_search.best_estimator_\n", "cluster_ids = best_kmeans.fit_predict(X)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. Identify Underperforming Groups with Datalab\n", "\n", "We will use `Datalab` to find underperforming groups in the dataset based on the predicted probabilities and optionally the cluster assignments." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from cleanlab import Datalab\n", "import pandas as pd\n", "\n", "# Initialize Datalab with the dataset\n", "lab = Datalab(data={\"X\": X, \"y\": y}, label_name=\"y\", task=\"classification\")\n", "\n", "# Find issues related to underperforming groups, optionally using cluster_ids\n", "lab.find_issues(\n", " # features=X # Uncomment this line if 'cluster_ids' is not provided to allow Datalab to run clustering automatically.\n", " pred_probs=pred_probs,\n", " issue_types={\n", " \"underperforming_group\": {\n", " \"threshold\": 0.75, # Set a custom threshold for identifying underperforming groups.\n", " # The default threshold is lower, optimized for higher precision (fewer false positives),\n", " # but for this toy example, a higher threshold increases sensitivity to underperforming groups.\n", " \"cluster_ids\": cluster_ids # Optional: Provide cluster IDs if clustering is used.\n", " # If not provided, Datalab will automatically run clustering under the hood.\n", " # In that case, you need to provide the 'features' array as an additional argument.\n", " },\n", " },\n", ")\n", "\n", "# Collect the identified issues\n", "underperforming_group_issues = lab.get_issues(\"underperforming_group\").query(\"is_underperforming_group_issue\")\n", "\n", "# Display the issues along with given and predicted labels\n", "display(underperforming_group_issues.join(pd.DataFrame({\"given_label\": y, \"predicted_label\": pred_probs.argmax(axis=1)})))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5. (Optional) Visualize the Results\n", "\n", "Finally, we will optionally visualize the dataset, highlighting the underperforming groups identified by `Datalab`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "# Plot the original data points\n", "plt.scatter(X[:, 0], X[:, 1], c=y, cmap=\"tab10\")\n", "\n", "# Highlight the underperforming group (if any issues are detected)\n", "if not underperforming_group_issues.empty:\n", " plt.scatter(\n", " X[underperforming_group_issues.index, 0], X[underperforming_group_issues.index, 1],\n", " s=100, facecolors='none', edgecolors='r', alpha=0.3, label=\"Underperforming Group\", linewidths=2.0\n", " )\n", "else:\n", " print(\"No underperforming group issues detected.\")\n", "\n", "# Add title and legend\n", "plt.title(\"Underperforming Groups in the Dataset\")\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Learn more about the underperforming group issue type in the [Issue Type Guide](../../cleanlab/datalab/guide/issue_type_description.html#underperforming-group-issue)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Predefining Data Slices for Detecting Underperforming Groups\n", "\n", "Instead of clustering the data to determine what data slices are considered when detecting underperforming groups, you can define these slices yourself.\n", "For say a tabular dataset, you can use the values of a categorical column as cluster IDs to predefine the relevant data subgroups/slices to consider. This allows you to focus on meaningful slices of your data defined by domain knowledge or specific attributes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Load and Prepare the Dataset\n", "\n", "We'll work with a toy tabular dataset with several categorical and numerical columns, just to illustrate how to use predefined data slices for detecting underperforming groups." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Define the dataset as a multi-line string\n", "dataset_tsv = \"\"\"\n", "Age\tGender\tLocation\tEducation\tExperience\tHighSalary\n", "60\tOther\tIndiana\tPhD\t21\t0\n", "50\tMale\tIndiana\tBachelor's\t21\t0\n", "36\tFemale\tIndiana\tPhD\t21\t0\n", "64\tMale\tKansas\tHigh School\t37\t1\n", "29\tMale\tKansas\tPhD\t14\t0\n", "42\tMale\tOhio\tPhD\t7\t0\n", "60\tMale\tKansas\tHigh School\t26\t0\n", "40\tOther\tOhio\tBachelor's\t25\t0\n", "44\tMale\tIndiana\tHigh School\t29\t0\n", "32\tMale\tOhio\tPhD\t17\t0\n", "32\tMale\tKansas\tBachelor's\t17\t0\n", "45\tOther\tOhio\tPhD\t30\t0\n", "57\tMale\tCalifornia\tHigh School\t27\t1\n", "61\tMale\tKansas\tHigh School\t32\t0\n", "45\tOther\tIndiana\tPhD\t4\t0\n", "24\tOther\tKansas\tBachelor's\t9\t0\n", "43\tOther\tOhio\tMaster's\t3\t0\n", "23\tMale\tOhio\tHigh School\t8\t0\n", "45\tOther\tKansas\tHigh School\t16\t0\n", "51\tOther\tOhio\tMaster's\t27\t0\n", "59\tMale\tOhio\tMaster's\t29\t0\n", "23\tOther\tIndiana\tBachelor's\t8\t0\n", "42\tMale\tKansas\tPhD\t5\t0\n", "54\tFemale\tKansas\tMaster's\t34\t0\n", "33\tOther\tKansas\tPhD\t18\t0\n", "43\tFemale\tKansas\tPhD\t23\t0\n", "46\tMale\tOhio\tBachelor's\t28\t0\n", "48\tOther\tOhio\tPhD\t30\t0\n", "63\tMale\tKansas\tHigh School\t34\t0\n", "49\tFemale\tKansas\tPhD\t32\t1\n", "37\tMale\tKansas\tPhD\t20\t0\n", "36\tOther\tIndiana\tMaster's\t21\t1\n", "24\tOther\tIndiana\tHigh School\t9\t0\n", "58\tFemale\tKansas\tPhD\t32\t0\n", "28\tMale\tCalifornia\tMaster's\t2\t0\n", "42\tOther\tKansas\tBachelor's\t17\t0\n", "30\tFemale\tCalifornia\tPhD\t15\t1\n", "60\tOther\tOhio\tPhD\t30\t0\n", "39\tOther\tKansas\tBachelor's\t2\t0\n", "25\tMale\tOhio\tMaster's\t10\t0\n", "46\tOther\tIndiana\tPhD\t23\t0\n", "35\tMale\tIndiana\tBachelor's\t20\t0\n", "30\tOther\tOhio\tHigh School\t15\t0\n", "47\tFemale\tOhio\tMaster's\t22\t0\n", "23\tOther\tOhio\tHigh School\t1\t0\n", "41\tMale\tOhio\tHigh School\t26\t0\n", "49\tMale\tKansas\tBachelor's\t1\t0\n", "28\tFemale\tOhio\tMaster's\t13\t0\n", "29\tOther\tKansas\tBachelor's\t14\t0\n", "56\tOther\tIndiana\tBachelor's\t39\t1\n", "35\tFemale\tOhio\tBachelor's\t20\t0\n", "38\tOther\tCalifornia\tBachelor's\t8\t1\n", "57\tOther\tOhio\tMaster's\t38\t1\n", "61\tMale\tIndiana\tPhD\t28\t0\n", "25\tOther\tIndiana\tHigh School\t10\t0\n", "23\tOther\tKansas\tHigh School\t8\t0\n", "27\tFemale\tOhio\tMaster's\t12\t0\n", "63\tFemale\tIndiana\tHigh School\t23\t0\n", "25\tMale\tIndiana\tMaster's\t10\t0\n", "50\tOther\tOhio\tHigh School\t6\t0\n", "39\tOther\tKansas\tBachelor's\t24\t0\n", "47\tOther\tIndiana\tHigh School\t19\t0\n", "55\tMale\tIndiana\tPhD\t0\t0\n", "31\tMale\tOhio\tPhD\t7\t0\n", "57\tFemale\tKansas\tPhD\t15\t0\n", "35\tMale\tCalifornia\tPhD\t13\t0\n", "52\tOther\tOhio\tPhD\t11\t0\n", "36\tOther\tOhio\tMaster's\t21\t0\n", "29\tMale\tIndiana\tMaster's\t14\t0\n", "35\tOther\tIndiana\tHigh School\t20\t0\n", "44\tOther\tIndiana\tPhD\t29\t1\n", "61\tMale\tKansas\tHigh School\t1\t0\n", "42\tMale\tOhio\tPhD\t27\t0\n", "37\tOther\tIndiana\tPhD\t22\t0\n", "39\tOther\tKansas\tMaster's\t21\t0\n", "\"\"\"\n", "\n", "# Import necessary libraries\n", "from io import StringIO\n", "import pandas as pd\n", "\n", "# Load the dataset into a DataFrame\n", "df = pd.read_csv(\n", " StringIO(dataset_tsv),\n", " sep='\\t',\n", ")\n", "\n", "# Display the original DataFrame\n", "display(df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Optional**: The categorical features of the dataset can encoded to numerical values for easier. For simplicity, y, we will use `OrdinalEncoder` from [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OrdinalEncoder.html)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.preprocessing import OrdinalEncoder\n", "\n", "# Encode the categorical columns\n", "columns_to_encode = [\"Gender\", \"Location\", \"Education\"]\n", "encoded_df = df.copy()\n", "encoder = OrdinalEncoder(dtype=int)\n", "encoded_df[columns_to_encode] = encoder.fit_transform(encoded_df[columns_to_encode])\n", "# encoded_df.drop(columns=[\"Salary\"], inplace=True)\n", "\n", "# Display the encoded DataFrame\n", "display(encoded_df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Train a Classifier and Obtain Predicted Probabilities\n", "\n", "Next, we will train a basic classifier (you can use any type of model) and obtain predicted probabilities for the dataset using cross-validation." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.linear_model import LogisticRegression\n", "from sklearn.model_selection import cross_val_predict\n", "\n", "# Split data\n", "X = encoded_df.drop(columns=[\"HighSalary\"])\n", "y = encoded_df[\"HighSalary\"]\n", "\n", "# Obtain predicted probabilities using cross-validation\n", "clf = LogisticRegression(random_state=0)\n", "pred_probs = cross_val_predict(clf, X, y, cv=3, method=\"predict_proba\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Define a Data Slice\n", "\n", "For a tabular dataset, you can use a categorical column’s values as pre-computed data slices, so that Datalab skips its default clustering step and directly uses the encoded values for each row in the\n", "dataset.\n", "\n", "For this example, we'll focus our attention to the `\"Location\"` column which has 4 unique categorical values." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cluster_ids = encoded_df[\"Location\"].to_numpy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. Identify Underperforming Groups with Datalab\n", "\n", "Now use `Datalab` to detect underperforming groups in the dataset based on the model predicted probabilities and our predefined data slices." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from cleanlab import Datalab\n", "\n", "# Initialize Datalab with the dataset\n", "lab = Datalab(data=df, label_name=\"HighSalary\", task=\"classification\")\n", "\n", "# Find issues related to underperforming groups, optionally using cluster_ids\n", "lab.find_issues(\n", " # features=X # Uncomment this line if 'cluster_ids' is not provided to allow Datalab to run clustering automatically.\n", " pred_probs=pred_probs,\n", " issue_types={\n", " \"underperforming_group\": {\n", " \"threshold\": 0.75, # Set a custom threshold for identifying underperforming groups.\n", " # The default threshold is lower, optimized for higher precision (fewer false positives),\n", " # but for this toy example, a higher threshold increases sensitivity to underperforming groups.\n", " \"cluster_ids\": cluster_ids # Optional: Provide cluster IDs if manual data-slicing is used.\n", " # If not provided, Datalab will automatically run clustering under the hood.\n", " # In that case, you need to provide the 'features' array as an additional argument.\n", " },\n", " },\n", ")\n", "\n", "# Collect the identified issues\n", "underperforming_group_issues = lab.get_issues(\"underperforming_group\").query(\"is_underperforming_group_issue\")\n", "\n", "# Display the issues along with given and predicted labels\n", "display(underperforming_group_issues.join(pd.DataFrame({\"given_label\": y, \"predicted_label\": pred_probs.argmax(axis=1)})))" ] }, { "cell_type": "markdown", "metadata": { "jp-MarkdownHeadingCollapsed": true }, "source": [ "## Detect if your dataset is non-IID\n", "\n", "Here we demonstrate how to discover when your data violates the foundational IID assumption that underpins most machine learning and analytics.\n", "Common violations (that can be caught with `Datalab`) include: data drift, or lack of statistical independence where different data points affect one another.\n", "This demonstration uses a toy 2D dataset." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Load Dataset\n", "\n", "For simplicity, we'll use a numerical dataset. If your data are not numerical, we recommend providing numeric representations of the data (neural network embeddings, or featurization like one-hot encoding, etc).\n", "\n", "By default, the non-IID issue check is automatically run by `Datalab` whenever you provide numerical data embeddings or predicted probabilities." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "np.random.seed(0) # Set seed for reproducibility\n", "\n", "\n", "def generate_data_dependent(num_samples):\n", " a1, a2, a3 = 0.6, 0.375, -0.975\n", " X = [np.random.normal(1, 1, 2) for _ in range(3)]\n", " X.extend(a1 * X[i-1] + a2 * X[i-2] + a3 * X[i-3] for i in range(3, num_samples))\n", " return np.array(X)\n", "\n", "\n", "X = generate_data_dependent(50)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Run Datalab to test the IID assumption" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Datalab` computes a p-value to test whether your data violates the IID assumption. A low p-value (close to 0) indicates strong evidence against the null hypothesis that the data was sampled IID, either because the data appear to be drifting in distribution or inter-dependent across samples." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from cleanlab import Datalab\n", "\n", "# Initialize Datalab with the dataset\n", "lab = Datalab(data={\"X\": X})\n", "\n", "# Check only for the non-IID issue, not other types of data issues\n", "lab.find_issues(features=X, issue_types={\"non_iid\": {}})\n", "\n", "print(\"p-value of the non-IID test:\", lab.get_issue_summary(\"non_iid\")[\"score\"].item())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Unlike certain issue types detected by `Datalab`, the non-IID issue is a property of the overall dataset as opposed to individual data points. As with other issue types, an overall issue score for the dataset is available via `get_issue_summary()`. For the non-IID issue type, this overall score is the p-value of a statistical test for violations of the IID assumption. The lower the p-value, the more evidence there is that your data are not IID.\n", "\n", "### 3. (Optional) Understand the nature of IID violations in your dataset" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To understand why our data appear non-IID, we can optionally investigate non-IID issues at the level of individual data points. But note that the IID assumption applies to the overall dataset, not to any individual data point. This individual data point analysis should only be used for further investigation, rather than to draw definitive conclusions about specific data points." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Per data point issues\n", "non_iid_issues = lab.get_issues(\"non_iid\")\n", "\n", "display(non_iid_issues.head(10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's visualize the non-IID issues detected by `Datalab`. Remember: the individual per data point non-IID scores are not particularly meaningful, but their trends across the dataset may reveal how the dataset is non-IID.\n", "If your overall dataset is detected to be non-IID, then the data point with the lowest non-IID score is automatically assigned the `is_non_iid_issue` flag (but do not focus on this specific data point and instead try to understand your dataset as a whole)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "non_iid_issues[\"non_iid_score\"].plot()\n", "\n", "# Highlight the point assigned as a non-iid issue\n", "idx = non_iid_issues.query(\"is_non_iid_issue\").index\n", "plt.scatter(idx, non_iid_issues.loc[idx, \"non_iid_score\"], color='red', label='Non-iid Issue', s=100)\n", "plt.title(\"Non-iid Scores\")\n", "plt.xlabel(\"Sample Index\")\n", "plt.ylabel(\"Non-iid Score\")\n", "plt.legend()\n", "plt.show()\n", "\n", "# Visualize dataset ordering\n", "plt.scatter(X[:, 0], X[:, 1], c=range(len(X)), cmap='coolwarm', s=100)\n", "plt.title(\"Dataset with data-dependent ordering\")\n", "plt.xlabel('Feature 1')\n", "plt.ylabel('Feature 2')\n", "\n", "plt.colorbar(label='Sample Index')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Plotting the non-IID scores for each data point vs. the ordering of these data points in the dataset (index) may reveal: distribution drift, statistical dependence, or other concerns regarding how the dataset was collected.\n", "\n", "Learn more about the non-IID issue type in the [Issue Type Guide](../../cleanlab/datalab/guide/issue_type_description.html#non-iid-issue)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "