1 Visão geral

Nesta vinheta, usaremos o objeto Seurat processado na etapa anterior do curso para inferir e visualizar comunicação célula-célula em dados de transcriptômica espacial. O fluxo reúne duas abordagens complementares:

  1. o CellChat estima interações ligante–receptor entre os clusters dos spots (Jin et al. 2021; Jin, Plikus, and Nie 2025); e
  2. o RGraphSpace/PathwaySpace representa essas interações entre spots vizinhos e projeta o sinal no tecido (Sysbiolab Team 2026; Tercan et al. 2025; Ellrott et al. 2025).

O objeto de entrada é obrigatoriamente data/brain.RDS, gerado pela vinheta de controle de qualidade, pré-processamento e clusterização. Embora ele contenha as seções anterior e posterior do conjunto stxBrain, manteremos somente a região anterior logo após o carregamento. Assim, todos os cálculos, resultados e gráficos desta vinheta serão produzidos apenas com essa região. O objeto também contém a expressão normalizada pelo SCTransform() e os clusters do Seurat (Hao et al. 2024).

Ao final, a tabela de comunicações projetadas entre spots será salva como:

resultados/spot-spot_commu_anterior.rds

Como interpretar o resultado: o CellChat estima a comunicação no nível dos grupos, que nesta análise são os clusters do Seurat. A tabela spot–spot é uma projeção dessas estimativas sobre pares de spots espacialmente próximos e compatíveis com os clusters de origem e destino. Portanto, ela não representa uma inferência independente do CellChat para cada par individual de spots.

2 Preparar o ambiente

2.1 Estrutura das pastas

Antes de executar o código, defina o diretório de trabalho como a pasta vinhetas/. No RStudio, use Session > Set Working Directory > To Source File Location.

Curso_ST/
|-- data/
|   `-- brain.RDS
|-- imagens/
|-- resultados/
`-- vinhetas/
    |-- CellChat_Comunicacao_Celular_PathwaySpace.Rmd
    `-- CellChat_Comunicacao_Celular_PathwaySpace.html
diretorio_vinhetas <- getwd()
diretorio_curso <- dirname(diretorio_vinhetas)
diretorio_dados <- file.path(diretorio_curso, "data")
diretorio_imagens <- file.path(diretorio_curso, "imagens")
diretorio_resultados <- file.path(diretorio_curso, "resultados")

dir.create(diretorio_imagens, showWarnings = FALSE, recursive = TRUE)
dir.create(diretorio_resultados, showWarnings = FALSE, recursive = TRUE)

2.2 Conferir os pacotes necessários

A instalação de todos os pacotes e de suas dependências está centralizada em Configuracao_do_ambiente_RStudio.Rmd. Execute a primeira vinheta, reinicie a sessão do R e então use o bloco abaixo apenas para confirmar que o ambiente está pronto.

pacotes_cellchat <- c(
  "CellChat", "RGraphSpace", "PathwaySpace", "Seurat", "SeuratObject",
  "dplyr", "tibble", "tidyr", "ggplot2", "RANN", "future"
)

pacotes_ausentes <- pacotes_cellchat[
  !vapply(pacotes_cellchat, requireNamespace, logical(1), quietly = TRUE)
]

if (length(pacotes_ausentes) > 0) {
  stop(
    "Execute primeiro Configuracao_do_ambiente_RStudio.Rmd. ",
    "Pacotes ausentes: ",
    paste(pacotes_ausentes, collapse = ", ")
  )
}

2.3 Carregar os pacotes

Carregamos os pacotes usados na inferência, na manipulação dos dados e nas visualizações. Esta vinheta adota o Seurat 5.5.1 como referência.

library("CellChat")
library("RGraphSpace")
library("PathwaySpace")
library("Seurat")
library("SeuratObject")
library("dplyr")
library("tibble")
library("tidyr")
library("ggplot2")

options(stringsAsFactors = FALSE)

packageVersion("Seurat")

3 Carregar o objeto espacial produzido anteriormente

Após carregar o objeto completo, o subset() descarta todos os spots da região posterior. A imagem espacial correspondente também é removida do objeto, mantendo apenas a seção anterior1 para as etapas seguintes.

brain <- readRDS(file.path(diretorio_dados, "brain.RDS"))
brain <- subset(brain, subset = region == "anterior")

DefaultAssay(brain) <- "SCT"
Idents(brain) <- "seurat_clusters"

if (length(Images(brain)) != 1L) {
  stop("O subset da região anterior deve manter exatamente uma imagem espacial.")
}

nome_imagem <- Images(brain)[1]

brain
table(brain$region)

O resultado esperado é um objeto com 2.696 spots e uma única imagem, denominada anterior1.

4 Preparar as entradas do CellChat

O CellChat requer quatro componentes principais para dados espaciais:

  • data.input: matriz normalizada de genes por spots;
  • meta: rótulos dos clusters e das amostras;
  • spatial.locs: coordenadas espaciais em resolução completa; e
  • spatial.factors: fatores usados para converter pixels em micrômetros.

4.1 Expressão normalizada e metadados

A camada normalizada do ensaio SCT fornece a expressão usada pelo CellChat. Os metadados associam cada spot ao seu cluster do Seurat e à única amostra espacial mantida.

data.input <- LayerData(brain, assay = "SCT", layer = "data")
spots_anterior <- Cells(brain[[nome_imagem]])
data.input <- data.input[, spots_anterior, drop = FALSE]

metadados_brain <- brain[[]]

meta <- data.frame(
  labels = paste0("cluster_",
    metadados_brain[colnames(data.input), "seurat_clusters"]),
  samples = nome_imagem,
  row.names = colnames(data.input)
)

meta$labels <- factor(meta$labels)
meta$samples <- factor(meta$samples, levels = nome_imagem)

table(meta$labels, meta$samples)

4.2 Coordenadas e fatores espaciais

No Seurat 5.5.1, GetTissueCoordinates() retorna as posições não escaladas do VisiumV2 nas colunas x e y. Depois de alinhar os spots às colunas da matriz de expressão, estimamos o fator que converte pixels em micrômetros. Para o Visium, adotamos 100 µm entre centros de spots adjacentes e 55 µm como diâmetro teórico do spot.

spatial.locs <- GetTissueCoordinates(
  brain, image = nome_imagem, scale = NULL)
spatial.locs <- as.data.frame(spatial.locs[, c("x", "y"), drop = FALSE])
spatial.locs <- spatial.locs[colnames(data.input), , drop = FALSE]

spot.size <- 55
distancia_centros <- 100
distancias <- computeCellDistance(coordinates = spatial.locs)
menor_distancia <- min(distancias[distancias > 0])

spatial.factors <- data.frame(
  ratio = distancia_centros / menor_distancia,
  tol = spot.size / 2,
  row.names = nome_imagem
)

spatial.factors
head(spatial.locs)

4.3 Criar o objeto CellChat

Com expressão, rótulos, coordenadas e escala na mesma ordem, construímos o objeto espacial que será usado na inferência.

cellchat <- createCellChat(
  object = data.input, meta = meta, group.by = "labels",
  datatype = "spatial", coordinates = spatial.locs,
  spatial.factors = spatial.factors
)

cellchat

5 Definir a base de interações ligante–receptor

Como o conjunto stxBrain é de camundongo, usaremos CellChatDB.mouse. Para manter o tempo de execução adequado à atividade didática, a análise será restrita à categoria Secreted Signaling.

CellChatDB <- CellChatDB.mouse
showDatabaseCategory(CellChatDB)

CellChatDB.use <- subsetDB(CellChatDB,
  search = "Secreted Signaling", key = "annotation")

cellchat@DB <- CellChatDB.use

6 Pré-processar os dados de expressão

O CellChat mantém os genes presentes na base selecionada e identifica genes e interações com expressão aumentada. A função plan() com opção multisession permite distribuir essa etapa em até quatro processos do R (cores).

cellchat <- subsetData(cellchat)

future::plan("multisession", workers = 4)

cellchat <- identifyOverExpressedGenes(cellchat)
cellchat <- identifyOverExpressedInteractions(cellchat, variable.both = FALSE)

7 Inferir a comunicação entre os clusters

Nesta etapa, as interações e comunicações dependentes de contato foram restringidas a 100 µm, aproximadamente a distância entre os centros de spots adjacentes no Visium. Esse corte ajuda a acelerar a análise.

Para demonstração de aula, nboot foi limitado a 10 para calcular mais rápido. nboot é número de testes de permutação utilizado para calcular os p-valores.

Cuidado:* Etapa mais custosa da vinheta, a execução pode levar vários minutos.

cellchat <- computeCommunProb(
  cellchat, type = "triMean", distance.use = TRUE,
  interaction.range = 100, scale.distance = 0.01,
  contact.range = 100, contact.dependent = TRUE,
  contact.knn.k = 6, nboot = 10
)

cellchat <- filterCommunication(cellchat, min.cells = 10)

Extraímos somente as interações com valor de p inferior a 0,05. A conversão dos grupos para texto facilitará as junções com as tabelas de spots.

df.net <- subsetCommunication(cellchat, thresh = 0.05)

df.net <- df.net %>%
  mutate(source = as.character(source), target = as.character(target))

head(df.net)

8 Projetar as comunicações para pares de spots

Agora identificaremos os pares de spots da região anterior separados por até 100 µm. Depois, cada par receberá os clusters de origem e destino e será combinado às interações inferidas pelo CellChat.

interaction.range <- 100

coordenadas_um <- spatial.locs * spatial.factors[nome_imagem, "ratio"]
matriz_distancias <- as.matrix(dist(coordenadas_um))
diag(matriz_distancias) <- Inf

indices <- which(matriz_distancias <= interaction.range, arr.ind = TRUE)

spot.pairs <- tibble(
  source_spot = rownames(matriz_distancias)[indices[, 1]],
  target_spot = colnames(matriz_distancias)[indices[, 2]],
  distance_um = matriz_distancias[indices],
  sample = nome_imagem
)

spot.groups <- tibble(spot = rownames(cellchat@meta),
  group = as.character(cellchat@idents))

spot.pairs <- spot.pairs %>%
  left_join(spot.groups, by = c("source_spot" = "spot")) %>%
  rename(source = group) %>%
  left_join(spot.groups, by = c("target_spot" = "spot")) %>%
  rename(target = group)

head(spot.pairs)

As interações inferidas para cada par de clusters são então associadas aos pares de spots compatíveis. A tabela resultante é ordenada e salva para reutilização.

spot.to.spot.communication <- spot.pairs %>%
  inner_join(df.net, by = c("source", "target")) %>%
  arrange(sample, source_spot, target_spot,
    pathway_name, interaction_name)

head(spot.to.spot.communication)

saveRDS(
  spot.to.spot.communication,
  file = file.path(diretorio_resultados,
    "spot-spot_commu_anterior.rds")
)

9 Construir a rede espacial no RGraphSpace

O GraphSpace reúne as coordenadas, os metadados, a expressão normalizada e a imagem da seção anterior. As coordenadas em baixa resolução permitem sobrepor corretamente a rede à imagem do tecido.

spots_visualizacao <- spots_anterior

coordenadas_gs <- GetTissueCoordinates(
  brain, image = nome_imagem, scale = "lowres"
)
coordenadas_gs <- coordenadas_gs[
  spots_visualizacao, c("x", "y"), drop = FALSE
]

metadados_gs <- brain[[]][spots_visualizacao, , drop = FALSE]
gs <- GraphSpace(cbind(coordenadas_gs, metadados_gs))

gs_fdata(gs) <- LayerData(
  brain, assay = "SCT", layer = "data"
)[, spots_visualizacao, drop = FALSE]

gs_image(gs) <- GetImage(
  brain[[nome_imagem]], mode = "raster"
)

gs <- normalizeGraphSpace(gs)

Primeiro, conferimos a posição dos clusters sobre a imagem antes de acrescentar as relações de proximidade.

spatial_theme <- theme_gspace_coords(
  theme = "th3", is_norm = TRUE,
  xlab = "Coordenada espacial 1", ylab = "Coordenada espacial 2"
)

cpal1 <- DiscretePalette(nlevels(gs$seurat_clusters), "polychrome")

ggplot(gs) +
  annotation_gspace_image(gs) +
  geom_nodespace(mapping = aes(colour = seurat_clusters),
    size = 1, pch = 19) +
  scale_colour_manual(values = cpal1) +
  theme_gspace_legend(discrete_colour = TRUE) +
  spatial_theme

9.1 Adicionar as relações de proximidade

A função abaixo conecta cada spot aos seis vizinhos mais próximos, respeitando o arranjo hexagonal esperado da lâmina Visium. Spots das bordas podem ter menos vizinhos.

proximityNetwork <- function(gs, nnei = 6) {
  nodes <- gs@nodes

  vizinhos <- RANN::nn2(nodes[, c("x", "y")],
    nodes[, c("x", "y")], k = nnei + 1)

  distancias <- as.data.frame(vizinhos$nn.dists[, 2:(nnei + 1)])
  indices <- as.data.frame(vizinhos$nn.idx[, 2:(nnei + 1)])

  distancia_tipica <- 1 / mean(1 / distancias$V1)
  manter <- distancias
  manter[, 1] <- TRUE
  manter[, 2:ncol(manter)] <-
    distancias[, 2:ncol(distancias)] >= 0.95 * distancia_tipica &
    distancias[, 2:ncol(distancias)] <= 1.05 * distancia_tipica

  matriz_arestas <- manter * indices
  matriz_arestas$from <- row.names(matriz_arestas)

  arestas <- matriz_arestas %>%
    pivot_longer(cols = -from, names_to = "vizinho", values_to = "to") %>%
    filter(to != 0) %>%
    transmute(from = as.numeric(from), to = as.numeric(to),
      v1 = pmin(from, to), v2 = pmax(from, to)) %>%
    distinct(v1, v2) %>%
    transmute(from = nodes$name[v1], to = nodes$name[v2])

  gs_add_edges(gs) <- arestas
  gs
}

Antes de criar as arestas, ampliamos uma área central para observar apenas os spots. Essa visualização servirá de comparação para a rede construída no bloco seguinte.

ggplot(gs) +
  annotation_gspace_image(gs) +
  geom_edgespace() +
  geom_nodespace(mapping = aes(colour = seurat_clusters),
    size = 3, pch = 19) +
  scale_colour_manual(values = cpal1) +
  theme_gspace_legend(discrete_colour = TRUE) +
  spatial_theme +
  coord_cartesian(xlim = c(0.4, 0.6), ylim = c(0.4, 0.6))

Agora aplicamos proximityNetwork() e repetimos o recorte. As linhas mostram quais spots serão considerados vizinhos na projeção das comunicações.

gs <- proximityNetwork(gs)

ggplot(gs) +
  annotation_gspace_image(gs) +
  geom_edgespace() +
  geom_nodespace(mapping = aes(colour = seurat_clusters),
    size = 3, pch = 19) +
  scale_colour_manual(values = cpal1) +
  theme_gspace_legend(discrete_colour = TRUE) +
  spatial_theme +
  coord_cartesian(xlim = c(0.4, 0.6), ylim = c(0.4, 0.6))

10 Associar as comunicações às arestas de proximidade

As arestas do GraphSpace são não direcionadas, mas a comunicação ligante–receptor possui uma origem e um destino. Por isso, criaremos as duas orientações de cada aresta antes de associá-las às comunicações do CellChat.

arestas <- gs@edges %>% select(name1, name2)

arestas_direcionadas <- bind_rows(
  arestas,
  arestas %>% transmute(name1 = name2, name2 = name1)
) %>%
  distinct()

spotCom_dt <- arestas_direcionadas %>%
  inner_join(
    spot.to.spot.communication %>% filter(sample == nome_imagem),
    by = c("name1" = "source_spot", "name2" = "target_spot")
  )

spotCom_dt$arrowType <- 1

interacoes_disponiveis <- spotCom_dt %>%
  select(ligand, receptor, interaction_name) %>%
  filter(!is.na(ligand), !is.na(receptor)) %>%
  distinct()

head(interacoes_disponiveis)

11 Projetar uma interação com PathwaySpace

A função communication_pathwayspace() mantém os spots envolvidos em uma interação, constrói o objeto PathwaySpace, soma a expressão do ligante e do receptor e projeta esse sinal na rede. Nomes separados por sublinhado são tratados como complexos formados por mais de um gene.

communication_pathwayspace <- function(gs, lig, recep, spotCom_dt) {
  comunicacoes <- spotCom_dt %>% filter(ligand == lig, receptor == recep)

  spots_selecionados <- unique(c(comunicacoes$name1, comunicacoes$name2))

  gs_selecionado <- gs_subset_nodes(gs, spots_selecionados)
  pspace_obj <- buildPathwaySpace(gs_selecionado)

  distancia_spots <- getNearestNode(pspace_obj)
  pdist <- mean(distancia_spots$dist)

  pspace_obj <- silhouetteMapping(pspace_obj, baseline = 0.1)

  resolver_features <- function(rotulo, features_disponiveis) {
    if (rotulo %in% features_disponiveis) {
      return(rotulo)
    }

    componentes <- strsplit(rotulo, "_", fixed = TRUE)[[1]]
    features_disponiveis[match(tolower(componentes),
      tolower(features_disponiveis))]
  }

  features_disponiveis <- colnames(pspace_obj@fdata)
  features_ligante <- resolver_features(lig, features_disponiveis)
  features_receptor <- resolver_features(recep, features_disponiveis)

  if (anyNA(c(features_ligante, features_receptor))) {
    stop("O ligante ou o receptor não foi encontrado nos dados de expressão.")
  }

  sinal_ligante <- rowSums(as.matrix(
    pspace_obj@fdata[, features_ligante, drop = FALSE]))
  sinal_receptor <- rowSums(as.matrix(
    pspace_obj@fdata[, features_receptor, drop = FALSE]))

  vertexSignal(pspace_obj) <- sinal_ligante + sinal_receptor

  pspace_obj <- circularProjection(
    pspace_obj, decay.fun = weibullDecay(decay = 0.5, pdist = pdist),
    aggregate.fun = signalAggregation("wmean"),
    k = gs_vcount(pspace_obj))

  pspace_obj
}

11.1 Visualizar um exemplo

Usaremos a primeira interação disponível na região anterior. Se preferir investigar um par específico, substitua ligante_exemplo e receptor_exemplo pelos símbolos desejados.

if (nrow(interacoes_disponiveis) == 0) {
  stop("Nenhuma interação foi associada às arestas da região anterior.")
}

ligante_exemplo <- interacoes_disponiveis$ligand[1]
receptor_exemplo <- interacoes_disponiveis$receptor[1]

pspace_obj <- communication_pathwayspace(
  gs = gs, lig = ligante_exemplo, recep = receptor_exemplo,
  spotCom_dt = spotCom_dt)

grafico_exemplo <- ggplot(pspace_obj) +
  annotation_gspace_image(pspace_obj) +
  annotation_pspace_signal(pspace_obj, si.alpha = 0.25) +
  spatial_theme + labs(
    title = paste(ligante_exemplo, "-", receptor_exemplo))

grafico_exemplo

12 Salvar as figuras das interações

Para a atividade em sala, serão exportadas as três primeiras interações. Para gerar todas as figuras, substitua a definição de numero_interacoes por nrow(interacoes_disponiveis).

numero_interacoes <- min(3, nrow(interacoes_disponiveis))

for (i in seq_len(numero_interacoes)) {
  ligante <- interacoes_disponiveis$ligand[i]
  receptor <- interacoes_disponiveis$receptor[i]

  pspace_i <- communication_pathwayspace(
    gs = gs,lig = ligante,recep = receptor,spotCom_dt = spotCom_dt)

  grafico_i <- ggplot(pspace_i) +
    annotation_gspace_image(pspace_i) +
    annotation_pspace_signal(pspace_i, si.alpha = 0.25) +
    spatial_theme + labs(title = paste(ligante, "-", receptor))

  nome_arquivo <- paste0("CellChat_PathwaySpace_",nome_imagem,
    "_",ligante,"-",receptor,".png")

  nome_arquivo <- gsub("[^[:alnum:]_.-]","_",nome_arquivo)

  ggsave(filename = file.path(diretorio_imagens, nome_arquivo),
    plot = grafico_i,width = 9,height = 6,units = "in",dpi = 120,bg = "white")

  rm(pspace_i, grafico_i)
  gc()
}

13 Arquivos produzidos

Ao concluir a vinheta, estarão disponíveis:

  • resultados/spot-spot_commu_anterior.rds: tabela de comunicações projetadas entre spots da região anterior;
  • imagens/CellChat_PathwaySpace_anterior1_<ligante>-<receptor>.png: projeções das interações selecionadas; e
  • os objetos intermediários cellchat, gs, spot.to.spot.communication e spotCom_dt na sessão do R.

14 Desafio do curso!

Repita essa análise de comunicação celular utilizando o dado produzido pelo RCTD

Em vez de utilizar a informação de clusters como nessa vinheta, utilize tipos celulares inferidos pela vinheta anterior de deconvolução.

Você pode utilizar o objeto Seurat com as anotações do RCTD salvo no repositório na pasta resultados/. Você pode baixar em: stxbrain_Seurat_com_RCTD.rds

15 Informações da sessão

Por fim, registre as versões dos pacotes e do R usadas na execução.

sessionInfo()

Referências

Ellrott, Kyle, Christopher K. Wong, Christina Yau, Mauro A. A. Castro, Jordan A. Lee, Brian J. Karlberg, Jasleen K. Grewal, et al. 2025. “Classification of Non-TCGA Cancer Samples to TCGA Molecular Subtypes Using Compact Feature Sets.” Cancer Cell 43 (2): 195–212.e11. https://doi.org/10.1016/j.ccell.2024.12.002.
Hao, Yuhan, Tim Stuart, Madeline H. Kowalski, Saket Choudhary, Paul Hoffman, Austin Hartman, Avi Srivastava, et al. 2024. “Dictionary Learning for Integrative, Multimodal and Scalable Single-Cell Analysis.” Nature Biotechnology 42: 293–304. https://doi.org/10.1038/s41587-023-01767-y.
Jin, Suoqin, Christian F. Guerrero-Juarez, Lihua Zhang, Ivan Chang, Raul Ramos, Chen-Hsiang Kuan, Peggy Myung, Maksim V. Plikus, and Qing Nie. 2021. “Inference and Analysis of Cell-Cell Communication Using CellChat.” Nature Communications 12 (1): 1088. https://doi.org/10.1038/s41467-021-21246-9.
Jin, Suoqin, Maksim V. Plikus, and Qing Nie. 2025. “CellChat for Systematic Analysis of Cell–Cell Communication from Single-Cell Transcriptomics.” Nature Protocols 20 (1): 180–219. https://doi.org/10.1038/s41596-024-01045-4.
Sysbiolab Team. 2026. RGraphSpace: A Lightweight Interface Between ’Igraph’ and ’Ggplot2’ Graphics. https://sysbiolab.github.io/RGraphSpace/.
Tercan, Bahar, Victor H. Apolonio, Vinicius S. Chagas, Christopher K. Wong, Jordan A. Lee, Christina Yau, Christopher C. Benz, et al. 2025. “Protocol for Assessing Distances in Pathway Space for Classifier Feature Sets from Machine Learning Methods.” STAR Protocols 6 (2): 103681. https://doi.org/10.1016/j.xpro.2025.103681.