Tutorial2: Using scMagnifier on multi-batch datasets

This tutorial demonstrates how to use scMagnifier on multi-batch data. Here we adopt the approach of combining scMagnifier with batch effect correction algorithms (Harmony, Scanorama, scVI) to process multi-batch datasets. The running workflow of scMagnifier is consistent with that for a single batch.
Here we use the Pancreas dataset as our example, and perform batch effect correction using the Harmony algorithm.

Step1: Data preprocessing

In this step, we define multi_preprocess() function to preprocess the dataset. The parameter settings for this function are consistent with those of the preprocessing function for single-batch data. You need to input the path to the h5ad file in the function (here, the gene expression matrix in the h5ad file is required to be the raw matrix). You can select the clustering method (Leiden is the default method, and you can also choose Louvain by setting cluster_method="Louvain"). You can input the clustering resolution you wish to use (the default value is 0.75, which can be adjusted via resolution=""). Additionally, if there are cell annotations in your h5ad file, you can input the name of the annotation column (set label_key=""), thereby obtaining the visualization results of cell annotations on UMAP. Finally, two key points should be emphasized: first, you need to input the name of the batch column by setting batch_key=""; second, you can customize the batch effect correction method by specifying the parameter method="harmony/scanorama/scvi".
You can refer to the multi_preprocess_core.py file for the remaining detailed parameters.

from scMagnifier import multi_preprocess
multi_preprocess(input_path="/mnt/disk1/hzh/Pancreas.h5ad", batch_key="batch", method="harmony")
Loading data from: /mnt/disk1/hzh/Pancreas.h5ad
Original data shape: (8569, 20125)
Filtering genes with min_counts >= 1 ...
After gene filtering shape: (8569, 17499)
Normalizing per cell (total counts -> target_sum=1e4) ...
Selecting top 2000 highly variable genes (on non-log data) ...
Number of HVG selected: 2000
Subsetting AnnData to HVGs ...
After subsetting shape: (8569, 2000)
Re-normalizing per cell after gene filtering ...
Saving un-logged copy to adata.raw and adata.layers['raw_count'] ...
Applying log1p and scaling ...
Before log1p: min= 0.0 max= 8925.588458618073 mean= 5.0
Running PCA (n_comps=20) ...


2026-01-20 18:33:37,376 - harmonypy - INFO - Computing initial centroids with sklearn.KMeans...


Performing Harmony batch correction using key 'batch' ...


2026-01-20 18:33:38,213 - harmonypy - INFO - sklearn.KMeans initialization complete.
2026-01-20 18:33:38,255 - harmonypy - INFO - Iteration 1 of 10
2026-01-20 18:33:40,133 - harmonypy - INFO - Iteration 2 of 10
2026-01-20 18:33:41,968 - harmonypy - INFO - Iteration 3 of 10
2026-01-20 18:33:43,833 - harmonypy - INFO - Iteration 4 of 10
2026-01-20 18:33:45,690 - harmonypy - INFO - Iteration 5 of 10
2026-01-20 18:33:47,591 - harmonypy - INFO - Iteration 6 of 10
2026-01-20 18:33:49,442 - harmonypy - INFO - Iteration 7 of 10
2026-01-20 18:33:50,284 - harmonypy - INFO - Iteration 8 of 10
2026-01-20 18:33:50,886 - harmonypy - INFO - Iteration 9 of 10
2026-01-20 18:33:51,485 - harmonypy - INFO - Iteration 10 of 10
2026-01-20 18:33:52,067 - harmonypy - INFO - Stopped before convergence


Harmony integration complete. Corrected PCs saved in adata.obsm['X_pca_harmony'].
Computing neighbors (n_neighbors=10) using 'X_pca_harmony' ...
Computing UMAP based on harmony-corrected embedding ...
Running Leiden clustering (resolution=0.75) ...
Plotting UMAP colored by leiden clusters ...
UMAP figure saved to: preprocessed_result/umap_leiden_harmony.png
Trying to plot UMAP using label_key='assigned_cluster' ...
Plotting UMAP colored by assigned_cluster ...
UMAP (label_key) figure saved to: preprocessed_result/umap_assigned_cluster_harmony.png
Preprocessed AnnData saved to: preprocessed_result/preprocessed.h5ad
Preprocessing finished. Outputs saved in preprocessed_result
Note: adata.raw and adata.layers['raw_count'] contain un-logged data for downstream use.

Then you can find the preprocessed h5ad file and generated images in the preprocessed_result directory.

# You do not need to execute this code
from IPython.display import HTML
import base64

def img_to_base64(img_path):
    with open(img_path, "rb") as f:
        return base64.b64encode(f.read()).decode()

img_path1 = "/mnt/disk1/hzh/Tutorial2/preprocessed_result/umap_leiden_harmony.png"
img_path2 = "/mnt/disk1/hzh/Tutorial2/preprocessed_result/umap_assigned_cluster_harmony.png"
img1_b64 = img_to_base64(img_path1)
img2_b64 = img_to_base64(img_path2)
HTML(f"""
<div style="display: flex; gap: 20px; scope: local;">
    <img src="data:image/png;base64,{img1_b64}" width="300">
    <img src="data:image/png;base64,{img2_b64}" width="400">
</div>
""")

Step2: GRN construction

This step uses the same GRN() function as that employed for processing single-batch datasets.

from scMagnifier import GRN
GRN()

Step3: TF perturbation

In this step, we define multi_perturb() function to simulate the process of gene perturbation propagation. The parameter settings for the perturbation function are consistent with those of the perturbation function for single-batch data. If you used the Louvain method during the preprocessing step, you need to set cluster_key="louvain" in the perturb() function to maintain consistency. Additionally, if you customized the resolution parameter during preprocessing, you should also set resolution="" here to keep it consistent. You can also adjust the perturbation multiplier by modifying the multiple="" parameter. If your dataset contains cell annotation information, you can specify the column name via label_key="". Notably, two critical parameters specific to multi-batch perturbation must be set: you need to input batch_key="" to specify the name of the batch column, and method="harmony/scanorama/scvi" to customize the batch effect correction method in the multi_perturb() function.
You can refer to the multi_perturb_core.py file for the remaining detailed parameters.

Next, we use two genes (AHR and ARX) as examples to demonstrate the perturbation process.You can perform perturbation for all genes by running the same code.

from scMagnifier import multi_perturb
multi_perturb(batch_key="batch", method="harmony")
[INFO] Running multipliers: ['0.1', '-0.1'] (method=harmony)

[INFO] === Running for multiplier=0.1 (method=harmony) ===
[INFO] Results will be saved under: perturb_results/0p1
[INFO] Found coefficient matrix in oracle.coef_matrix_per_cluster
[INFO] Found 14 clusters in leiden: ['0', '1', '10', '11', '12', '13', '2', '3', '4', '5', '6', '7', '8', '9']
[SAVED] Original expression matrix: perturb_results/0p1/original_matrix.csv
[INFO] Loaded 2 genes from txt file.
================================================================================
[RUNNING] Perturbing gene: AHR  (multiplier=0.1)
[SAVED] Perturbed matrix for AHR: perturb_results/0p1/perturbed_matrix_AHR_0.1.csv
[INFO] Running log1p + scale + PCA + Harmony + neighbors + UMAP + Clustering ...
WARNING: adata.X seems to be already log-transformed.


2026-01-20 19:33:49,667 - harmonypy - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:33:49,667 - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:33:50,356 - harmonypy - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:33:50,356 - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:33:50,387 - harmonypy - INFO - Iteration 1 of 10
2026-01-20 19:33:50,387 - INFO - Iteration 1 of 10
2026-01-20 19:33:51,895 - harmonypy - INFO - Iteration 2 of 10
2026-01-20 19:33:51,895 - INFO - Iteration 2 of 10
2026-01-20 19:33:53,857 - harmonypy - INFO - Iteration 3 of 10
2026-01-20 19:33:53,857 - INFO - Iteration 3 of 10
2026-01-20 19:33:55,837 - harmonypy - INFO - Iteration 4 of 10
2026-01-20 19:33:55,837 - INFO - Iteration 4 of 10
2026-01-20 19:33:57,540 - harmonypy - INFO - Iteration 5 of 10
2026-01-20 19:33:57,540 - INFO - Iteration 5 of 10
2026-01-20 19:33:59,007 - harmonypy - INFO - Iteration 6 of 10
2026-01-20 19:33:59,007 - INFO - Iteration 6 of 10
2026-01-20 19:34:00,524 - harmonypy - INFO - Iteration 7 of 10
2026-01-20 19:34:00,524 - INFO - Iteration 7 of 10
2026-01-20 19:34:01,470 - harmonypy - INFO - Iteration 8 of 10
2026-01-20 19:34:01,470 - INFO - Iteration 8 of 10
2026-01-20 19:34:01,935 - harmonypy - INFO - Iteration 9 of 10
2026-01-20 19:34:01,935 - INFO - Iteration 9 of 10
2026-01-20 19:34:02,407 - harmonypy - INFO - Iteration 10 of 10
2026-01-20 19:34:02,407 - INFO - Iteration 10 of 10
2026-01-20 19:34:02,872 - harmonypy - INFO - Stopped before convergence
2026-01-20 19:34:02,872 - INFO - Stopped before convergence


[INFO] Harmony batch correction completed. Proceeding with neighbor graph...
[SAVED] perturb_results/0p1/cluster_AHR_0.1.csv


... storing 'pert_cluster_str' as categorical


[DONE] Perturbation for AHR completed.
================================================================================
================================================================================
[RUNNING] Perturbing gene: ARX  (multiplier=0.1)
[SAVED] Perturbed matrix for ARX: perturb_results/0p1/perturbed_matrix_ARX_0.1.csv
[INFO] Running log1p + scale + PCA + Harmony + neighbors + UMAP + Clustering ...
WARNING: adata.X seems to be already log-transformed.


2026-01-20 19:34:26,058 - harmonypy - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:34:26,058 - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:34:26,668 - harmonypy - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:34:26,668 - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:34:26,698 - harmonypy - INFO - Iteration 1 of 10
2026-01-20 19:34:26,698 - INFO - Iteration 1 of 10
2026-01-20 19:34:28,106 - harmonypy - INFO - Iteration 2 of 10
2026-01-20 19:34:28,106 - INFO - Iteration 2 of 10
2026-01-20 19:34:29,478 - harmonypy - INFO - Iteration 3 of 10
2026-01-20 19:34:29,478 - INFO - Iteration 3 of 10
2026-01-20 19:34:30,928 - harmonypy - INFO - Iteration 4 of 10
2026-01-20 19:34:30,928 - INFO - Iteration 4 of 10
2026-01-20 19:34:32,440 - harmonypy - INFO - Iteration 5 of 10
2026-01-20 19:34:32,440 - INFO - Iteration 5 of 10
2026-01-20 19:34:33,863 - harmonypy - INFO - Iteration 6 of 10
2026-01-20 19:34:33,863 - INFO - Iteration 6 of 10
2026-01-20 19:34:35,303 - harmonypy - INFO - Iteration 7 of 10
2026-01-20 19:34:35,303 - INFO - Iteration 7 of 10
2026-01-20 19:34:35,962 - harmonypy - INFO - Iteration 8 of 10
2026-01-20 19:34:35,962 - INFO - Iteration 8 of 10
2026-01-20 19:34:36,694 - harmonypy - INFO - Iteration 9 of 10
2026-01-20 19:34:36,694 - INFO - Iteration 9 of 10
2026-01-20 19:34:37,533 - harmonypy - INFO - Iteration 10 of 10
2026-01-20 19:34:37,533 - INFO - Iteration 10 of 10
2026-01-20 19:34:37,983 - harmonypy - INFO - Stopped before convergence
2026-01-20 19:34:37,983 - INFO - Stopped before convergence


[INFO] Harmony batch correction completed. Proceeding with neighbor graph...
[SAVED] perturb_results/0p1/cluster_ARX_0.1.csv


... storing 'pert_cluster_str' as categorical


[DONE] Perturbation for ARX completed.
================================================================================

[INFO] All genes done for multiplier=0.1.

[INFO] === Running for multiplier=-0.1 (method=harmony) ===
[INFO] Results will be saved under: perturb_results/neg0p1
[INFO] Found coefficient matrix in oracle.coef_matrix_per_cluster
[INFO] Found 14 clusters in leiden: ['0', '1', '10', '11', '12', '13', '2', '3', '4', '5', '6', '7', '8', '9']
[SAVED] Original expression matrix: perturb_results/neg0p1/original_matrix.csv
[INFO] Loaded 2 genes from txt file.
================================================================================
[RUNNING] Perturbing gene: AHR  (multiplier=-0.1)
[SAVED] Perturbed matrix for AHR: perturb_results/neg0p1/perturbed_matrix_AHR_-0.1.csv
[INFO] Running log1p + scale + PCA + Harmony + neighbors + UMAP + Clustering ...
WARNING: adata.X seems to be already log-transformed.


2026-01-20 19:35:16,222 - harmonypy - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:35:16,222 - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:35:17,034 - harmonypy - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:35:17,034 - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:35:17,075 - harmonypy - INFO - Iteration 1 of 10
2026-01-20 19:35:17,075 - INFO - Iteration 1 of 10
2026-01-20 19:35:18,926 - harmonypy - INFO - Iteration 2 of 10
2026-01-20 19:35:18,926 - INFO - Iteration 2 of 10
2026-01-20 19:35:20,934 - harmonypy - INFO - Iteration 3 of 10
2026-01-20 19:35:20,934 - INFO - Iteration 3 of 10
2026-01-20 19:35:22,879 - harmonypy - INFO - Iteration 4 of 10
2026-01-20 19:35:22,879 - INFO - Iteration 4 of 10
2026-01-20 19:35:24,864 - harmonypy - INFO - Iteration 5 of 10
2026-01-20 19:35:24,864 - INFO - Iteration 5 of 10
2026-01-20 19:35:26,477 - harmonypy - INFO - Iteration 6 of 10
2026-01-20 19:35:26,477 - INFO - Iteration 6 of 10
2026-01-20 19:35:27,911 - harmonypy - INFO - Iteration 7 of 10
2026-01-20 19:35:27,911 - INFO - Iteration 7 of 10
2026-01-20 19:35:28,549 - harmonypy - INFO - Iteration 8 of 10
2026-01-20 19:35:28,549 - INFO - Iteration 8 of 10
2026-01-20 19:35:28,996 - harmonypy - INFO - Iteration 9 of 10
2026-01-20 19:35:28,996 - INFO - Iteration 9 of 10
2026-01-20 19:35:29,445 - harmonypy - INFO - Converged after 9 iterations
2026-01-20 19:35:29,445 - INFO - Converged after 9 iterations


[INFO] Harmony batch correction completed. Proceeding with neighbor graph...
[SAVED] perturb_results/neg0p1/cluster_AHR_-0.1.csv


... storing 'pert_cluster_str' as categorical


[DONE] Perturbation for AHR completed.
================================================================================
================================================================================
[RUNNING] Perturbing gene: ARX  (multiplier=-0.1)
[SAVED] Perturbed matrix for ARX: perturb_results/neg0p1/perturbed_matrix_ARX_-0.1.csv
[INFO] Running log1p + scale + PCA + Harmony + neighbors + UMAP + Clustering ...
WARNING: adata.X seems to be already log-transformed.


2026-01-20 19:35:53,065 - harmonypy - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:35:53,065 - INFO - Computing initial centroids with sklearn.KMeans...
2026-01-20 19:35:53,679 - harmonypy - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:35:53,679 - INFO - sklearn.KMeans initialization complete.
2026-01-20 19:35:53,711 - harmonypy - INFO - Iteration 1 of 10
2026-01-20 19:35:53,711 - INFO - Iteration 1 of 10
2026-01-20 19:35:55,138 - harmonypy - INFO - Iteration 2 of 10
2026-01-20 19:35:55,138 - INFO - Iteration 2 of 10
2026-01-20 19:35:56,607 - harmonypy - INFO - Iteration 3 of 10
2026-01-20 19:35:56,607 - INFO - Iteration 3 of 10
2026-01-20 19:35:58,029 - harmonypy - INFO - Iteration 4 of 10
2026-01-20 19:35:58,029 - INFO - Iteration 4 of 10
2026-01-20 19:35:59,461 - harmonypy - INFO - Iteration 5 of 10
2026-01-20 19:35:59,461 - INFO - Iteration 5 of 10
2026-01-20 19:36:00,936 - harmonypy - INFO - Iteration 6 of 10
2026-01-20 19:36:00,936 - INFO - Iteration 6 of 10
2026-01-20 19:36:02,384 - harmonypy - INFO - Iteration 7 of 10
2026-01-20 19:36:02,384 - INFO - Iteration 7 of 10
2026-01-20 19:36:03,047 - harmonypy - INFO - Iteration 8 of 10
2026-01-20 19:36:03,047 - INFO - Iteration 8 of 10
2026-01-20 19:36:03,517 - harmonypy - INFO - Iteration 9 of 10
2026-01-20 19:36:03,517 - INFO - Iteration 9 of 10
2026-01-20 19:36:03,998 - harmonypy - INFO - Iteration 10 of 10
2026-01-20 19:36:03,998 - INFO - Iteration 10 of 10
2026-01-20 19:36:04,481 - harmonypy - INFO - Stopped before convergence
2026-01-20 19:36:04,481 - INFO - Stopped before convergence


[INFO] Harmony batch correction completed. Proceeding with neighbor graph...
[SAVED] perturb_results/neg0p1/cluster_ARX_-0.1.csv


... storing 'pert_cluster_str' as categorical


[DONE] Perturbation for ARX completed.
================================================================================

[INFO] All genes done for multiplier=-0.1.

[INFO] All multipliers completed.

After perturbation, the perturb_results directory will store the clustering results following each gene perturbation (e.g., cluster_AHR_0.1.csv), the perturbed gene expression matrix (e.g., perturbed_matrix_AHR_0.1.csv), as well as the visualization results of the perturbed clustering results on the new UMAP, the visualization results of the perturbed clustering results on the original UMAP, and the visualization results of cell annotations on the new UMAP.

Next, we demonstrate the visualization plots of the new clustering results on the new UMAP, the original UMAP, and cell annotations on the new UMAP following perturbation of the AHR gene.

# You do not need to execute this code
img_path1 = "/mnt/disk1/hzh/Tutorial2/perturb_results/0p1/umap_new_leiden_perturbed_AHR_0.1.png"
img_path2 = "/mnt/disk1/hzh/Tutorial2/perturb_results/0p1/umap_original_on_new_AHR_0.1.png"
img_path3 = "/mnt/disk1/hzh/Tutorial2/perturb_results/0p1/umap_label_AHR_0.1.png"
img1_b64 = img_to_base64(img_path1)
img2_b64 = img_to_base64(img_path2)
img3_b64 = img_to_base64(img_path3)
HTML(f"""
<div style="display: flex; gap: 20px; scope: local;">
    <img src="data:image/png;base64,{img1_b64}" width="300">
    <img src="data:image/png;base64,{img2_b64}" width="300">
    <img src="data:image/png;base64,{img3_b64}" width="400">
</div>
""")

Step4: Consensus clustering

In this step, we define multi_consensus() function to perform consensus clustering. If you have used the Louvain method for clustering in the preceding steps, you need to set cluster_method="louvain" in the function for consistency. In addition, if cell annotations are available, you can input the column name of the cell annotations for label_key="". We use a default resolution of 1.5 here; if you want to modify this value, you can set resolution="". Finally, the parameter method="harmony/scanorama/scvi" must be incorporated into the function.
You can refer to the multi_consensus_core.py file for the remaining detailed parameters.

from scMagnifier import multi_consensus
multi_consensus(method="harmony")
[INFO] Found 298 cluster CSV files (method=harmony).
[INFO] Loaded h5ad with 8569 cells.
[INFO] 8569 cells after alignment.
[INFO] One-hot matrix shape: (8569, 4175)
[INFO] Using Harmony-corrected PCA (X_pca_harmony)
[INFO] Embedding matrix shape: (8569, 20)
[INFO] Computing embedding (Euclidean) distance matrix ...
[INFO] Computing One-hot (cosine) distance matrix ...
[INFO] Running new UMAP (precomputed distances) ...
[INFO] New UMAP embedding shape: (8569, 2)
[SAVED] Custom UMAP + combined_distance saved to consensus_result/adata_with_rpcumap.h5ad

[INFO] Building kNN graph from the combined distance matrix (precomputed)...
[INFO] kNN graph placed into adata_cluster.obsp

[INFO] Running leiden clustering at resolution=1.5
[SAVED] consensus_result/leiden_res1.5.csv

[DONE] All resolutions processed (method=harmony).

After consensus clustering, you can obtain the h5ad file storing the rpcUMAP embeddings (the embedding information is saved in "X_umap_custom") in the consensus_result folder, along with the results of consensus clustering in leiden_res1.5.csv, the visualizations of consensus clustering results on the original UMAP and rpcUMAP, and the visualization of cell annotations on rpcUMAP.

Next, we present the visualizations of the consensus clustering results on the original UMAP and rpcUMAP, as well as the visualization of the cell annotation results on rpcUMAP.

# You do not need to execute this code
img_path1 = "/mnt/disk1/hzh/Tutorial2/consensus_result/leiden_UMAP_res1.5.png"
img_path2 = "/mnt/disk1/hzh/Tutorial2/consensus_result/leiden_rpcUMAP_res1.5.png"
img_path3 = "/mnt/disk1/hzh/Tutorial2/consensus_result/rpcumap_realLabel_assigned_cluster.png"
img1_b64 = img_to_base64(img_path1)
img2_b64 = img_to_base64(img_path2)
img3_b64 = img_to_base64(img_path3)
HTML(f"""
<div style="display: flex; gap: 20px; scope: local;">
    <img src="data:image/png;base64,{img1_b64}" width="300">
    <img src="data:image/png;base64,{img2_b64}" width="300">
    <img src="data:image/png;base64,{img3_b64}" width="400">
</div>
""")

Step5: cluster merging

In this step, we define multi_merge() function to perform cluster merging. In the function, we set the default value of min_size_fraction to 0.01. In practical use, you can lower this value—especially for tasks involving the identification of rare cells, where you can set it to 0.001.Additionally, if you need to control the number of clusters in the final output, you can also do so by adjusting the value of min_size_fraction. Finally, the parameter method="harmony/scanorama/scvi" must be incorporated into the function.
You can refer to the multi_merge_core.py file for the remaining detailed parameters.

from scMagnifier import multi_merge
multi_merge(method="harmony")
[INFO] Auto-selected cluster CSV: consensus_result/leiden_res1.5.csv
[INFO] Starting cluster merging (method=harmony)...
[INFO] Input h5ad: consensus_result/adata_with_rpcumap.h5ad
[INFO] Input cluster CSV: consensus_result/leiden_res1.5.csv
[INFO] Output directory: merged_result
[INFO] Loading h5ad and cluster CSV ...
[INFO] Read cluster CSV with 8569 rows.
[INFO] 8569 cells remaining after cell ID alignment.
[INFO] Number of initial high-resolution clusters: 27
[INFO] Centroid calculation matrix shape (cells x features): (8569, 2000)
[INFO] Computed 27 cluster centroids.
[INFO] THM (raw) = 9.15816, scaling factor = 0.75, final THM = 6.86862
[INFO] Number of clusters after threshold-based merging: 17
[INFO] Number of clusters after first merge step: 17
[INFO] Minimum cluster size threshold: 86 cells (1.00% of total cells)
[INFO] Final number of clusters after merging small clusters: 11
[SAVED] Merged cluster results: merged_result/merged_clusters.csv
[SAVED] Old UMAP visualization: merged_result/umap_merged.png
[SAVED] New UMAP visualization: merged_result/rpcumap_merged.png
[DONE] Cluster merging + visualization completed!

After cluster merging, we obtain the final results, including the final clustering results saved in merged_clusters.csv, as well as the visualizations of the clustering results on the original UMAP and rpcUMAP.

Next, we demonstrate the visualization plots of the final clustering results on the original UMAP and the rpcUMAP.

# You do not need to execute this code
img_path1 = "/mnt/disk1/hzh/Tutorial2/merged_result/umap_merged.png"
img_path2 = "/mnt/disk1/hzh/Tutorial2/merged_result/rpcumap_merged.png"
img1_b64 = img_to_base64(img_path1)
img2_b64 = img_to_base64(img_path2)
HTML(f"""
<div style="display: flex; gap: 20px; scope: local;">
    <img src="data:image/png;base64,{img1_b64}" width="300">
    <img src="data:image/png;base64,{img2_b64}" width="300">
</div>
""")