Quantifying Neoplastic Cell Accessibility
Introduction¶
This tutorial introduces a workflow to quantify tumor cell accessibility by analyzing the composition of a tumor cell’s immediate neighborhood. The tumor microenvironment (TME) is a battleground where the ratio of anti-tumor immune cells to pro-tumor stromal cells can tell us about the potential for immune infiltration. Tumor cell neighborhoods rich in immune cells indicates greater accessibility for the immune system to target these cancer cells, while an abundance of stromal cells in the neighborhoods can create a dense, fibrotic barriers that physically impedes immune infiltration and promotes an immunosuppressive state.
This guide will demonstrate how to use Histolytics to define and analyze the cellular neighborhoods surrounding individual tumor cells. By quantifying the ratio of immune cells to stromal cells in these local regions, you can objectively assess the level of tumor accessibility and gain a deeper understanding of the interplay between cancer cells and their surrounding microenvironment.
from histolytics.spatial_graph.graph import fit_graph
from histolytics.utils.gdf import set_uid
from histolytics.data import hgsc_cancer_nuclei
nuc = hgsc_cancer_nuclei()
nuc = set_uid(nuc) # Ensure the GeoDataFrame has a unique identifier
# fit spatial weights using Delaunay triangulation
w, w_gdf = fit_graph(nuc, "delaunay", id_col="uid", threshold=100)
ax = nuc.plot(figsize=(10, 10), column="class_name", aspect=1)
w_gdf.plot(ax=ax, linewidth=1, column="class_name", legend=True, aspect=1)
w_gdf.head(5)
ax.set_axis_off()
Neoplastic Immune-accessibility¶
Next, we'll showcase how you can create custom metrics to analyze spatial neighborhood relationships between nuclei. We will define a custom metric to analyze the local inflammatory and connective nuclear microenvironment around the neoplastic nuclei. This metric will help us understand how neoplastic nuclei are surrounded by inflammatory and connective tissue cells and potentially physically blocked from immune infiltration. To do this, we calculate for each neoplastic nucleus, the ratio of inflammatory neighbors to the total number of inflammatory and connective neighbors. This highlights neoplastic nuclei that are surrounded predominantly by inflammatory nuclei versus connective nuclei, providing insights whether the neoplastic nuclei are physically blocked by the connective nuclei.
import numpy as np
import pandas as pd
from typing import List, Tuple
from histolytics.spatial_agg.local_values import local_vals
# Custom function to compute the ratio of inflammatory to connective neighbors
def compute_inflammatory_ratio(row):
if row["inflammatory_cnt"] == 0 and row["connective_cnt"] == 0:
return 0
return row["inflammatory_cnt"] / (row["inflammatory_cnt"] + row["connective_cnt"])
def compute_neighbor_cnt(nhood_classes: List[str]) -> Tuple[float, float]:
inflammatory_count = np.sum(np.array(nhood_classes) == "inflammatory")
connective_count = np.sum(np.array(nhood_classes) == "connective")
return inflammatory_count, connective_count
# get the neighborhood classes for each nucleus
nuc = local_vals(
nuc, w, val_col="class_name", new_col_name="nhood_classes", id_col="uid"
)
# Filter for neoplastic nuclei
neo_nuc = nuc[nuc["class_name"] == "neoplastic"].copy()
# Compute neighborhood inflammatory and connective counts
neo_nuc[["inflammatory_cnt", "connective_cnt"]] = (
neo_nuc["nhood_classes"].apply(compute_neighbor_cnt).apply(pd.Series)
)
# Compute the ratio of inflammatory to connective neighbors around neoplastic nuclei
neo_nuc["inflammatory_ratio"] = neo_nuc.apply(compute_inflammatory_ratio, axis=1)
# keep only the neoplastic nuclei with inflammatory or connective neighbors
filtered_neo_nuc = neo_nuc[
(neo_nuc["inflammatory_cnt"] > 0) | (neo_nuc["connective_cnt"] > 0)
]
# Filter the links to only include inflammatory-neoplastic and connective-neoplastic
imm_conn_neo_links = w_gdf[
w_gdf["class_name"].isin(["inflammatory-neoplastic", "connective-neoplastic"])
]
# Let's visualize the neoplastic nuclei with their inflammatory ratio
ax = nuc.plot(figsize=(10, 10), column="class_name", aspect=1)
filtered_neo_nuc.plot(ax=ax, column="inflammatory_ratio", legend=True, aspect=1)
imm_conn_neo_links.plot(
ax=ax, linewidth=1, column="class_name", legend=True, aspect=1, cmap="Set1"
)
filtered_neo_nuc.head(3)
ax.set_axis_off()
In this example, most of the neoplastic nuclei are surrounded by inflammatory nuclei, making them potentially more susceptible to immune infiltration. This is indicated by the high ratio of yellow to blue nuclei in the image above. The custom metric can be easily adapted to include other cell types or different heuristics based on the research question.
Neoplastic cell packing density¶
Next we will quantify the neoplastic cell packing density in the tumor nest by simply counting the number of neoplastic nuclei within a defined radius around each neoplastic nucleus. For example, in HGSC, the cell packing density is heavily related to different tumor growth patterns e.g. solid, papillary etc.. To get the neighborhoods based on radius, we will use the distband graph with 64 pixel threshold (32 microns).
import pandas as pd
from histolytics.spatial_agg.local_values import local_vals
# Custom function to compute the ratio of inflammatory to connective neighbors
def compute_neoplastic_cardinality(row):
return len(row["nhood_classes"])
# Filter for neoplastic nuclei
neo_nuc = nuc[nuc["class_name"] == "neoplastic"].copy()
neo_nuc = set_uid(neo_nuc) # Ensure the GeoDataFrame has a unique identifier
# Let's use the distband to get the neighborhoods based on radius only
w, w_gdf = fit_graph(neo_nuc, "distband", id_col="uid", threshold=64)
# get the neighborhood classes for each nucleus
neo_nuc = local_vals(
neo_nuc, w, val_col="class_name", new_col_name="nhood_classes", id_col="uid"
)
neo_nuc["packing_density"] = neo_nuc.apply(compute_neoplastic_cardinality, axis=1)
# filter the links to only include neoplastic-neoplastic for visualization
neo_neo_links = w_gdf[w_gdf["class_name"].isin(["neoplastic-neoplastic"])]
# Let's visualize the neoplastic nuclei with their inflammatory ratio
ax = nuc.plot(figsize=(10, 10), column="class_name", aspect=1)
neo_nuc.plot(ax=ax, column="packing_density", legend=True, aspect=1)
neo_neo_links.plot(
ax=ax, linewidth=0.5, column="class_name", legend=True, aspect=1, cmap="Set1"
)
neo_nuc.head(3)
ax.set_axis_off()
Here we see that the neoplastic nuclei are densely packed in the tumor nests, which is typical for this type of solid growth pattern in HGSC. Again, these types of custom metrics can be easily adapted in Histolytics to explore different research questions in mind.