6  Análises Downstream

Author

Dalmolin Systems Biology Group

Na etapa anterior, identificamos quais genes da Candida albicans tiveram sua expressão significativamente alterada pelo tratamento com vancomicina. Mas uma lista de genes, por si só, diz pouco sobre a biologia do organismo. Para extrairmos significado biológico, precisamos perguntar: esses genes pertencem a vias biológicas específicas? Há processos celulares mais afetados do que o esperado ao acaso?

É exatamente isso que a análise de enriquecimento funcional nos permite responder.


7 Enriquecimento Funcional

A análise de enriquecimento funcional verifica se, entre os genes diferencialmente expressos, há um número desproporcional de genes pertencentes a uma determinada via biológica ou processo celular. Se encontrarmos mais genes alterados em uma via do que o esperado por acaso, dizemos que essa via está enriquecida.

A abordagem que utilizaremos é a ORA (Over-Representation Analysis), que parte da nossa lista de genes significativos e aplica um teste hipergeométrico para verificar quais vias estão sobre-representadas. Para os bancos de dados, utilizaremos o KEGG Pathways e o Gene Ontology (GO).

7.1 O que são GO e KEGG?

7.1.1 Gene Ontology (GO)

O Gene Ontology é um sistema de ontologias que descreve a função dos genes em três dimensões independentes (ontologias):

  • Biological Process (BP): o processo celular de que o gene participa (ex: resposta a estresse oxidativo, biossíntese da parede celular).
  • Molecular Function (MF): a atividade molecular que a proteína exerce (ex: atividade de quinase, ligação a ATP).
  • Cellular Component (CC): onde no espaço celular a proteína atua (ex: núcleo, membrana plasmática).

A GO é organizada como um grafo acíclico dirigido (DAG): termos mais gerais (ex: processo metabólico) estão no topo da hierarquia, e termos mais específicos (ex: biossíntese de ergosterol) estão na base. Um gene pode ser anotado em múltiplos termos e em múltiplas ontologias simultaneamente.

7.1.2 KEGG Pathways

O KEGG (Kyoto Encyclopedia of Genes and Genomes) é um banco de dados que organiza o conhecimento biológico em mapas de vias metabólicas e de sinalização. Cada via é um diagrama que conecta enzimas, metabólitos e reações em sequência, representando processos como a glicólise, o ciclo do TCA ou a biossíntese de aminoácidos.

Diferente do GO, o KEGG tem uma estrutura plana (não hierárquica): cada via é independente e autocontida. Além disso, o KEGG inclui informações sobre reações bioquímicas e compostos químicos, o que o torna especialmente útil para estudar metabolismo.

7.1.3 Principais diferenças

Característica Gene Ontology (GO) KEGG Pathways
Estrutura Hierárquica (DAG) Plana (mapas independentes)
Foco Funções, processos e localização Vias metabólicas e de sinalização
Granularidade Alta (termos muito específicos a gerais) Moderada (vias completas)
Curadoria Comunidade científica global Equipe centralizada (Kanehisa Lab)
Organismo Ampla cobertura (via OrgDb ou GAF) Código por organismo (ex: cal)

7.2 A Importância dos Bancos de Dados Curados

Antes de começar, é fundamental entender que o enriquecimento funcional depende inteiramente da qualidade e completude das anotações disponíveis para o organismo estudado. Para organismos-modelo como Homo sapiens ou Mus musculus, bancos como KEGG, Gene Ontology e Reactome possuem anotações extensas, curadas por décadas de pesquisa. O resultado é que milhares de genes estão anotados em centenas de vias biológicas, e o teste estatístico tem alta chance de encontrar padrões significativos.

Para organismos não-modelo como Candida albicans, a situação é diferente:

  • Menos genes anotados: uma fração menor do genoma possui anotação funcional.
  • Menos vias catalogadas: o número de vias biológicas mapeadas é mais restrito.
  • Anotações menos curadas: muitas anotações são inferidas computacionalmente (IEA - Inferred from Electronic Annotation), e não por evidência experimental direta.

Isso significa que, mesmo que nossos genes estejam participando de processos biológicos relevantes, se esses processos não estiverem adequadamente anotados no banco de dados, o enriquecimento simplesmente não os detectará. Por isso, para C. albicans, utilizaremos tanto o KEGG (que suporta o organismo nativamente) quanto as anotações curadas do Candida Genome Database (CGD), o banco de referência para genômica de Candida, que possui mais de 33 mil anotações GO específicas.

WarningNota Didática — Thresholds Relaxados

Em publicações científicas, os cortes padrão para enriquecimento são p.adjust ≤ 0.05 e qvalue ≤ 0.05. Neste minicurso, utilizamos thresholds mais permissivos (p.adjust ≤ 0.5 e qvalue ≤ 0.5) para fins didáticos, de modo que seja possível visualizar e interpretar os gráficos de enriquecimento mesmo com um número reduzido de DEGs. Em um cenário de pesquisa real, recomendamos utilizar os cortes padrão.

7.3 1. Carregando os Pacotes

7.4 2. Carregando os Resultados da Expressão Diferencial

Vamos recuperar a tabela de resultados que salvamos no módulo anterior:

res_dge <- readRDS(here::here("data/DGE/res_dge.rds"))

head(res_dge)
              baseMean log2FoldChange     lfcSE        stat    pvalue      padj
C1_00010W_A 0.13073479      0.2439030 2.5493706  0.09567186 0.9237812 1.0000000
C1_00020C_A 1.35871278     -0.7858614 0.5442897 -1.44382928 0.1487870 0.5357854
C1_00030C_A 0.02292633      0.2485875 2.9448570  0.08441413 0.9327272 1.0000000
C1_00040W_A 0.03442856      0.3688108 2.9448570  0.12523896 0.9003344 1.0000000
C1_00050C_A 0.03644129     -0.1120823 2.9448570 -0.03806036 0.9696396 1.0000000
C1_00060W_A 0.36778762      0.4121354 1.3458047  0.30623715 0.7594241 1.0000000
                     signif
C1_00010W_A Not significant
C1_00020C_A Not significant
C1_00030C_A Not significant
C1_00040W_A Not significant
C1_00050C_A Not significant
C1_00060W_A Not significant

7.5 3. Preparando os Conjuntos de Genes

Vamos preparar três conjuntos de genes para o enriquecimento: os upregulados, os downregulados e todos os DEGs juntos.

genes_up <- rownames(subset(res_dge, signif == "Upregulated"))
genes_down <- rownames(subset(res_dge, signif == "Downregulated"))
genes_all <- rownames(subset(res_dge, signif != "Not significant"))

cat("Genes upregulados:", length(genes_up), "\n")
Genes upregulados: 40 
cat("Genes downregulados:", length(genes_down), "\n")
Genes downregulados: 35 
cat("Total DEGs:", length(genes_all), "\n")
Total DEGs: 75 

7.6 4. Conversão de Identificadores para o KEGG

Os nossos identificadores de genes seguem o padrão do Ensembl Fungi (ex: C1_00010W_A), mas o KEGG utiliza um formato próprio para C. albicans (ex: CAALFM_C100010WA). Precisamos construir o dicionário para conversão:

# Baixar os nomes dos genes de C. albicans (cal) do KEGG
kegg_genes <- keggList("cal")

# Criar tabela de mapeamento
kegg_mapping <- data.frame(
  kegg_id = sub("cal:", "", names(kegg_genes)),
  description = as.character(kegg_genes),
  stringsAsFactors = FALSE
)

# Gerar o ID Ensembl Fungi equivalente a partir do KEGG ID
kegg_mapping <- kegg_mapping %>%
  mutate(
    core = sub("^CAALFM_", "", kegg_id),
    ensembl_id = gsub(
      "^(C[0-9R]+)(\\d{5})(\\w)(\\w)$",
      "\\1_\\2\\3_\\4",
      core
    )
  )

# Verificar quantos dos nossos genes têm correspondência
genes_com_kegg <- sum(rownames(res_dge) %in% kegg_mapping$ensembl_id)
cat("Genes mapeados para KEGG:", genes_com_kegg, "de", nrow(res_dge), "\n")
Genes mapeados para KEGG: 6117 de 6468 
# Criar dicionário: Ensembl → KEGG
ensembl_to_kegg <- setNames(kegg_mapping$kegg_id, kegg_mapping$ensembl_id)

Esse dicionário será usado adiante tanto para o enriquecimento KEGG via GSEA (seção 5) quanto para a construção da rede PPI com o STRING (seção 11).


7.7 5. Enriquecimento KEGG — GSEA

Diferente do ORA, que parte de uma lista discreta de DEGs (filtrada por um cutoff de significância), o GSEA (Gene Set Enrichment Analysis) considera todos os genes ranqueados por uma métrica contínua — tipicamente o log2FoldChange — e verifica se os membros de cada via biológica se concentram nas extremidades do ranking. A pergunta muda:

  • ORA: “Há mais DEGs nesta via do que o esperado ao acaso?”
  • GSEA: “Os genes desta via estão sistematicamente entre os mais up- ou down-regulados?”

A vantagem é que o GSEA não precisa de cutoff: ele aproveita o sinal coordenado de muitos genes com mudanças pequenas, que o ORA descartaria. O resultado vem na forma de um Normalized Enrichment Score (NES): positivo indica que a via está enriquecida na ponta UP do ranking, negativo na ponta DOWN.

7.7.1 Construindo o vetor ranqueado

Para o GSEA, precisamos transformar a tabela res_dge em um vetor numérico nomeado com todos os genes ranqueados por log2FC e identificados pelo formato KEGG (CAALFM_). Vamos usar o dicionário ensembl_to_kegg que construímos na seção 4:

suppressPackageStartupMessages(library(stringr))

# Vetor: log2FC com nome = Ensembl ID
ranks <- res_dge$log2FoldChange
names(ranks) <- rownames(res_dge)
ranks <- ranks[!is.na(ranks)]

# Converter os nomes para IDs KEGG e ordenar decrescentemente
ranks_kegg <- ranks[names(ranks) %in% names(ensembl_to_kegg)]
names(ranks_kegg) <- ensembl_to_kegg[names(ranks_kegg)]
ranks_kegg <- sort(ranks_kegg[!duplicated(names(ranks_kegg))], decreasing = TRUE)

cat("Genes no ranking (KEGG IDs únicos):", length(ranks_kegg), "\n")
Genes no ranking (KEGG IDs únicos): 6023 
cat("log2FC — máximo (topo do ranking) :", round(max(ranks_kegg), 2), "\n")
log2FC — máximo (topo do ranking) : 2.82 
cat("log2FC — mínimo (base do ranking) :", round(min(ranks_kegg), 2), "\n")
log2FC — mínimo (base do ranking) : -2.62 

7.7.2 Rodando gseKEGG()

A função gseKEGG() consulta a API REST do KEGG em tempo real para obter as anotações de vias da C. albicans (organism = "cal") e roda o algoritmo de permutação. Como o GSEA é estocástico, fixamos uma semente com set.seed() para garantir que duas execuções da mesma versão do banco produzam exatamente o mesmo resultado:

set.seed(42)  # GSEA é estocástica (usa permutações); set.seed garante reprodutibilidade
gsea_kegg <- gseKEGG(
  geneList      = ranks_kegg,
  organism      = "cal",
  keyType       = "kegg",
  minGSSize     = 5,
  maxGSSize     = 500,
  pvalueCutoff  = 0.5,
  pAdjustMethod = "BH",
  eps           = 0,
  verbose       = FALSE
)

cat("Vias enriquecidas (GSEA):", nrow(as.data.frame(gsea_kegg)), "\n")
Vias enriquecidas (GSEA): 62 

7.7.3 Dotplot — GSEA KEGG (UP vs. DOWN)

Diferente do dotplot do ORA, o do GSEA já vem separado por direção: cada via aparece no painel activated (NES > 0, regulada para cima) ou suppressed (NES < 0, regulada para baixo).

if (!is.null(gsea_kegg) && nrow(as.data.frame(gsea_kegg)) > 0) {
  dotplot(gsea_kegg,
          showCategory = 15,
          split = ".sign",
          title = "GSEA-KEGG — Vias enriquecidas") +
    facet_grid(. ~ .sign) +
    scale_color_gradient(low = "#E31A1C", high = "#FCBBA1", name = "p-valor\najustado")
} else {
  cat("Nenhuma via KEGG enriquecida.\n")
}

7.7.4 Ridgeplot — distribuição de log2FC por via

O ridgeplot mostra, para cada via enriquecida, a distribuição dos log2FC dos seus genes membros. Vias “puxadas” para a direita estão dominantes em UP; para a esquerda, em DOWN.

if (!is.null(gsea_kegg) && nrow(as.data.frame(gsea_kegg)) > 0) {
  ridgeplot(gsea_kegg, showCategory = 15) +
    labs(title = "GSEA-KEGG — Distribuição de log2FC por via",
         x = "log2 Fold Change") +
    theme(plot.title = element_text(face = "bold", hjust = 0.5))
} else {
  cat("Nenhuma via KEGG enriquecida.\n")
}

7.7.5 Curva de Running Enrichment Score — Top 3 vias

A curva clássica do GSEA mostra como o Enrichment Score “cresce” ou “decresce” à medida que percorremos o ranking do topo (mais UP) até a base (mais DOWN). Picos positivos no início indicam vias UP; vales negativos no final indicam vias DOWN. As marcas verticais (|) abaixo da curva indicam onde caem, no ranking, os genes da via:

if (!is.null(gsea_kegg) && nrow(as.data.frame(gsea_kegg)) > 0) {
  top3 <- as.data.frame(gsea_kegg) %>%
    arrange(p.adjust) %>%
    head(3) %>%
    pull(ID)
  gseaplot2(gsea_kegg, geneSetID = top3,
            title = "GSEA-KEGG — Top 3 vias (running ES)")
} else {
  cat("Nenhuma via KEGG enriquecida.\n")
}

7.7.6 Barplot Bidirecional — NES por via

O análogo direto do barplot bidirecional do ORA é usar o NES como métrica: barras vermelhas (UP, NES > 0) crescem para a direita; azuis (DOWN, NES < 0) crescem para a esquerda, compartilhando o mesmo eixo Y:

if (!is.null(gsea_kegg) && nrow(as.data.frame(gsea_kegg)) > 0) {
  df_gsea <- as.data.frame(gsea_kegg) %>%
    mutate(Direction = ifelse(NES > 0, "Upregulated", "Downregulated"),
           term_lbl  = str_wrap(Description, 45)) %>%
    arrange(desc(abs(NES))) %>%
    head(15)

  ggplot(df_gsea, aes(x = NES, y = reorder(term_lbl, NES), fill = Direction)) +
    geom_col(width = 0.7) +
    scale_fill_manual(values = c("Upregulated"   = "#E31A1C",
                                 "Downregulated" = "#1F78B4")) +
    geom_vline(xintercept = 0, linewidth = 0.5) +
    labs(title = "GSEA-KEGG — NES por via",
         x = "Normalized Enrichment Score (NES)",
         y = NULL, fill = "Direção") +
    theme_minimal(base_size = 13) +
    theme(plot.title  = element_text(hjust = 0.5, face = "bold"),
          axis.text.y = element_text(size = 9),
          legend.position = "bottom")
} else {
  cat("Nenhuma via KEGG enriquecida para gerar o barplot.\n")
}

7.7.7 Como interpretar os gráficos:

  • Dotplot: cada ponto é uma via enriquecida, separada nos painéis activated (NES > 0) e suppressed (NES < 0). O tamanho indica o número de genes da via (setSize); a cor indica o p-valor ajustado.
  • Ridgeplot: a curva de densidade mostra como os log2FC dos genes daquela via estão distribuídos — concentração à direita = UP, à esquerda = DOWN.
  • Running ES: a curva clássica do GSEA. Picos no topo do ranking (esquerda) indicam vias UP; vales na base (direita), vias DOWN. As marcas (|) mostram onde os genes da via caem no ranking.
  • Barplot bidirecional (NES): análogo direto do barplot do ORA, mas usando NES como métrica de direção e magnitude — barras vermelhas (UP) à direita, azuis (DOWN) à esquerda.

7.8 6. Enriquecimento GO com g:Profiler

Vamos rodar a ORA com Gene Ontology (Biological Process) separadamente para cada conjunto de genes (upregulados, downregulados e todos os DEGs), usando o corte didático de p-valor ajustado ≤ 0.5 com correção FDR (Benjamini-Hochberg). Observe que diferentemente do enrichKEGG(), aqui passamos os identificadores no formato original do Ensembl Fungi (ex: C1_00210C_A) — o g:Profiler reconhece e mapeia internamente, sem precisar de dicionário de conversão.

7.9 7. Sobre o pacote g:Profiler

Uma alternativa para o enriquecimento usando o Gene Ontology é o g:Profiler, uma ferramenta mantida pela Universidade de Tartu que indexa anotações funcionais do Ensembl e do Ensembl Genomes (incluindo o Ensembl Fungi) e oferece uma API REST consultável a partir do R via o pacote gprofiler2. A função central do pacote é gost(), que envia uma lista de genes ao servidor, executa o teste hipergeométrico contra cada termo da ontologia e retorna uma tabela com os resultados.

A principal vantagem do g:Profiler é a reprodutibilidade por release: cada versão do banco recebe um identificador citável (ex: e114_eg62_p19) e fica permanentemente arquivada, o que permite que análises sejam refeitas exatamente como na data original.

NotePonto Importante — g:Profiler e Organismos Não-Modelo

O g:Profiler suporta mais de 200 organismos via Ensembl e Ensembl Genomes, incluindo C. albicans SC5314 (código de organismo: calbicans). No entanto, a integração com KEGG, Reactome e WikiPathways está disponível apenas para organismos-modelo (humano, camundongo, rato, levedura, etc.). Para C. albicans, somente as três sub-ontologias do Gene Ontology (Biological Process, Molecular Function e Cellular Component) estão indexadas. Por isso, mantemos a análise KEGG da seção anterior e usamos o g:Profiler aqui apenas para GO (Biological Process).

7.9.1 Carregando o pacote gprofiler2

gost_up <- gost(
  query             = genes_up,
  organism          = "calbicans",
  sources           = "GO:BP",
  user_threshold    = 0.5,
  correction_method = "fdr"
)

gost_down <- gost(
  query             = genes_down,
  organism          = "calbicans",
  sources           = "GO:BP",
  user_threshold    = 0.5,
  correction_method = "fdr"
)

gost_all <- gost(
  query             = genes_all,
  organism          = "calbicans",
  sources           = "GO:BP",
  user_threshold    = 0.5,
  correction_method = "fdr"
)

# Versão do banco consultado (citável nos métodos)
cat("Versão do g:Profiler:", gost_all$meta$version, "\n\n")
Versão do g:Profiler: e114_eg62_p19_27110d83 
cat("Termos GO BP enriquecidos:\n")
Termos GO BP enriquecidos:
cat("  UP   :", if (is.null(gost_up))   0 else nrow(gost_up$result),   "\n")
  UP   : 149 
cat("  DOWN :", if (is.null(gost_down)) 0 else nrow(gost_down$result), "\n")
  DOWN : 143 
cat("  ALL  :", if (is.null(gost_all))  0 else nrow(gost_all$result),  "\n")
  ALL  : 240 

Como o objeto retornado pelo gost() é uma lista (não um enrichResult do clusterProfiler), as funções dotplot() e barplot() do enrichplot não funcionam diretamente. Definimos abaixo uma pequena função auxiliar que monta dotplots no mesmo estilo dos da seção KEGG: o eixo X traz o GeneRatio (proporção de DEGs caindo no termo), a cor representa o p-valor ajustado e o tamanho do ponto indica quantos DEGs pertencem ao termo (Count).

# Função para criar o gráfico
dotplot_gost <- function(gost_res, titulo, cor_low, cor_high) {
  df <- gost_res$result %>%
    mutate(
      GeneRatio = intersection_size / query_size,
      term_lbl  = str_wrap(term_name, 45)
    ) %>%
    arrange(p_value) %>%
    head(15) %>%
    mutate(term_lbl = factor(term_lbl,
                             levels = unique(term_lbl[order(GeneRatio)])))

  ggplot(df, aes(x = GeneRatio, y = term_lbl,
                 color = p_value, size = intersection_size)) +
    geom_point() +
    scale_color_gradient(low = cor_low, high = cor_high,
                         name = "p-valor\najustado") +
    scale_size_continuous(name = "Count", range = c(3, 8)) +
    labs(title = titulo, x = "GeneRatio", y = NULL) +
    theme_minimal(base_size = 11) +
    theme(plot.title  = element_text(face = "bold", hjust = 0.5),
          axis.text.y = element_text(size = 9))
}

7.9.2 Dotplot — GO BP Upregulados

if (!is.null(gost_up) && nrow(gost_up$result) > 0) {
  dotplot_gost(gost_up,
               "GO Biological Process — Genes Upregulados (g:Profiler)",
               cor_low = "#E31A1C", cor_high = "#FCBBA1")
} else {
  cat("Nenhum processo biológico enriquecido para genes upregulados.\n")
}

7.9.3 Dotplot — GO BP Downregulados

if (!is.null(gost_down) && nrow(gost_down$result) > 0) {
  dotplot_gost(gost_down,
               "GO Biological Process — Genes Downregulados (g:Profiler)",
               cor_low = "#1F78B4", cor_high = "#C6DBEF")
} else {
  cat("Nenhum processo biológico enriquecido para genes downregulados.\n")
}

7.9.4 Dotplot — GO BP Todos os DEGs

if (!is.null(gost_all) && nrow(gost_all$result) > 0) {
  dotplot_gost(gost_all,
               "GO Biological Process — Todos os DEGs (g:Profiler)",
               cor_low = "pink", cor_high = "purple")
} else {
  cat("Nenhum processo biológico enriquecido para todos os DEGs.\n")
}

7.9.5 Barplot Bidirecional — GO BP UP vs. DOWN

Como fizemos na seção KEGG, podemos comparar diretamente os termos enriquecidos em UP e DOWN num único gráfico bidirecional — as barras vermelhas (UP) crescem para a direita e as azuis (DOWN) para a esquerda, compartilhando o mesmo eixo Y:

df_gost_up <- if (!is.null(gost_up) && nrow(gost_up$result) > 0) {
  gost_up$result %>%
    mutate(Direction = "Upregulated", Count_dir = intersection_size)
} else { data.frame() }

df_gost_down <- if (!is.null(gost_down) && nrow(gost_down$result) > 0) {
  gost_down$result %>%
    mutate(Direction = "Downregulated", Count_dir = -intersection_size)
} else { data.frame() }

if (nrow(df_gost_up) > 0 || nrow(df_gost_down) > 0) {
  df_gost_bidir <- bind_rows(
    df_gost_up   %>% arrange(p_value) %>% head(10),
    df_gost_down %>% arrange(p_value) %>% head(10)
  ) %>%
    mutate(term_lbl = str_wrap(term_name, 45))

  ggplot(df_gost_bidir,
         aes(x = Count_dir, y = reorder(term_lbl, Count_dir), fill = Direction)) +
    geom_col(width = 0.7) +
    scale_fill_manual(values = c("Upregulated"   = "#E31A1C",
                                 "Downregulated" = "#1F78B4")) +
    geom_vline(xintercept = 0, linewidth = 0.5) +
    labs(title = "GO Biological Process — UP vs. DOWN (g:Profiler)",
         x = "Número de Genes", y = NULL, fill = "Regulação") +
    theme_minimal(base_size = 13) +
    theme(plot.title  = element_text(hjust = 0.5, face = "bold"),
          axis.text.y = element_text(size = 9),
          legend.position = "bottom")
} else {
  cat("Nenhum processo biológico enriquecido para gerar o barplot bidirecional.\n")
}

7.9.6 Barplot — GO BP Todos os DEGs

Finalmente, o barplot considerando todos os DEGs juntos, em tom roxo para diferenciá-lo do par UP/DOWN:

if (!is.null(gost_all) && nrow(gost_all$result) > 0) {
  gost_all$result %>%
    arrange(p_value) %>%
    head(15) %>%
    mutate(term_lbl = str_wrap(term_name, 45)) %>%
    ggplot(aes(x = intersection_size,
               y = reorder(term_lbl, intersection_size))) +
    geom_col(fill = "#6A3D9A", width = 0.7) +
    labs(title = "GO Biological Process — Todos os DEGs (g:Profiler)",
         x = "Número de Genes", y = NULL) +
    theme_minimal(base_size = 13) +
    theme(plot.title  = element_text(hjust = 0.5, face = "bold"),
          axis.text.y = element_text(size = 9))
} else {
  cat("Nenhum processo biológico enriquecido para todos os DEGs.\n")
}

7.9.7 Como interpretar os gráficos:

  • Dotplot: cada ponto representa um termo GO BP enriquecido. O tamanho indica quantos DEGs pertencem ao termo (Count); a cor indica o p-valor ajustado (FDR).
  • Barplot bidirecional: permite comparar diretamente os termos UP (vermelho, à direita) e DOWN (azul, à esquerda) em um único gráfico, com o eixo Y compartilhado.
  • Barplot (ALL): mostra os termos GO BP enriquecidos considerando todos os DEGs juntos, ordenados pelo número de genes que pertencem a cada termo.

7.10 8. Resumo e Interpretação

cat("=== Resumo do Enriquecimento Funcional ===\n\n")
=== Resumo do Enriquecimento Funcional ===
cat("Genes UP:", length(genes_up), "| DOWN:", length(genes_down),
    "| Total:", length(genes_all), "\n\n")
Genes UP: 40 | DOWN: 35 | Total: 75 
cat("--- KEGG (GSEA) ---\n")
--- KEGG (GSEA) ---
if (!is.null(gsea_kegg) && nrow(as.data.frame(gsea_kegg)) > 0) {
  df_gsea_sum <- as.data.frame(gsea_kegg)
  cat("  Activated (NES > 0):", sum(df_gsea_sum$NES > 0), "vias\n")
  cat("  Suppressed (NES < 0):", sum(df_gsea_sum$NES < 0), "vias\n")
  cat("  Total              :", nrow(df_gsea_sum), "vias\n")
} else {
  cat("  nenhuma via enriquecida\n")
}
  Activated (NES > 0): 26 vias
  Suppressed (NES < 0): 36 vias
  Total              : 62 vias
cat("\n--- GO Biological Process (g:Profiler) ---\n")

--- GO Biological Process (g:Profiler) ---
cat("  UP:   ")
  UP:   
if (!is.null(gost_up) && nrow(gost_up$result) > 0) {
  cat(nrow(gost_up$result), "termos\n")
} else { cat("nenhum termo\n") }
149 termos
cat("  DOWN: ")
  DOWN: 
if (!is.null(gost_down) && nrow(gost_down$result) > 0) {
  cat(nrow(gost_down$result), "termos\n")
} else { cat("nenhum termo\n") }
143 termos
cat("  ALL:  ")
  ALL:  
if (!is.null(gost_all) && nrow(gost_all$result) > 0) {
  cat(nrow(gost_all$result), "termos\n")
} else { cat("nenhum termo\n") }
240 termos

Identificamos quais vias biológicas estão enriquecidas entre os genes diferencialmente expressos da Candida albicans. Mas como esses genes se relacionam entre si? Será que as proteínas codificadas por eles interagem fisicamente ou funcionam nos mesmos processos? Para responder a essas perguntas, vamos construir uma rede de interação proteína-proteína (PPI).


8 Redes de Interação Proteína-Proteína (PPI)

Uma lista de genes diferencialmente expressos nos diz quais genes mudaram. O enriquecimento funcional nos diz em quais vias esses genes participam. Mas nenhuma dessas análises nos mostra como os produtos desses genes se relacionam entre si. É exatamente essa lacuna que as redes de interação proteína-proteína (PPI) preenchem.

Uma rede PPI é um grafo onde cada representa uma proteína e cada aresta conecta duas proteínas que possuem alguma relação funcional ou física documentada. Ao mapear nossos genes diferencialmente expressos nessa rede.

8.1 9. O Banco de Dados STRING

O STRING (Search Tool for the Retrieval of Interacting Genes/Proteins) é o maior banco de dados público de interações proteicas, cobrindo mais de 14.000 organismos. Um ponto crucial é entender que o STRING não armazena apenas interações físicas diretas (como ligações proteína-proteína demonstradas por co-imunoprecipitação). Ele integra múltiplas fontes de evidência para inferir associações funcionais entre proteínas:

Canal de Evidência Coluna O que mede
Vizinhança genômica nscore Genes fisicamente próximos no genoma tendem a ser funcionalmente relacionados
Fusão gênica fscore Genes que aparecem fundidos em outros organismos provavelmente interagem
Co-ocorrência filogenética pscore Proteínas que co-evoluem (presentes/ausentes juntas em espécies)
Co-expressão ascore Genes com padrões de expressão similares em múltiplos experimentos
Evidência experimental escore Interações demonstradas por ensaios bioquímicos (Y2H, AP-MS, etc.)
Bancos de dados curados dscore Interações registradas em bancos curados (KEGG, Reactome, BioCyc, etc.)
Text mining tscore Co-menção de proteínas na literatura científica

Cada canal produz um escore entre 0 e 1 que indica a confiança daquela evidência para um dado par de proteínas. O STRING combina esses escores em um escore combinado usando uma fórmula probabilística que evita dupla contagem.

ImportantInterações ≠ Interações Físicas

É fundamental entender que o termo “interação” no STRING é mais amplo do que interação física direta. Quando dizemos que duas proteínas “interagem” no STRING, estamos dizendo que há evidência de associação funcional, elas podem participar do mesmo complexo, da mesma via metabólica, ou serem co-reguladas, sem necessariamente se tocarem fisicamente. Por isso, o termo mais preciso é associação funcional.

NotePonto Importante — Bancos de Dados Curados e Organismos Não-Modelo

Assim como vimos na análise de enriquecimento funcional, a qualidade dos resultados de redes PPI depende diretamente da completude do banco de dados para o organismo estudado. Para organismos-modelo como Homo sapiens e Saccharomyces cerevisiae, o STRING possui milhares de interações experimentalmente validadas. Para Candida albicans, a cobertura é menor, muitas interações são inferidas computacionalmente a partir de ortologia com S. cerevisiae ou por text mining. Mesmo assim, o STRING suporta C. albicans SC5314 nativamente (identificador taxonômico 237561), o que nos permite consultar a base diretamente.

8.2 10. Carregando os Pacotes

8.3 11. Preparando a Lista de Genes

Vamos recuperar nossos resultados de expressão diferencial e preparar a lista de genes que será submetida ao STRING. O STRING não reconhece diretamente os identificadores do Ensembl Fungi (ex: C1_00210C_A). Para C. albicans, o STRING utiliza os identificadores no formato CAALFM_ (ex: CAALFM_C100210CA), que são os mesmos usados pelo CGD e pelo KEGG. Felizmente, já construímos o dicionário de conversão (ensembl_to_kegg) na seção anterior:

# Carregar resultados da expressão diferencial
res_dge <- readRDS(here::here("data/DGE/res_dge.rds"))

# Selecionar apenas os genes significativos (DEGs)
res_dge_signif <- res_dge %>%
  filter(signif != "Not significant")

genes_deg <- rownames(res_dge_signif)

# Converter Ensembl IDs para o formato CAALFM_ usando o dicionário da seção 4
genes_deg_caalfm <- unname(ensembl_to_kegg[genes_deg[genes_deg %in% names(ensembl_to_kegg)]])

# Criar dicionário reverso para mapear de volta
caalfm_to_ensembl <- setNames(names(ensembl_to_kegg), unname(ensembl_to_kegg))

cat("Total de DEGs:", length(genes_deg), "\n")
Total de DEGs: 75 
cat("DEGs com ID CAALFM_ mapeado:", length(genes_deg_caalfm), "\n")
DEGs com ID CAALFM_ mapeado: 75 
NotePonto Importante — Conversão de Identificadores

A conversão entre Ensembl Fungi e CAALFM_ segue uma regra simples: o ID C1_00010W_A corresponde a CAALFM_C100010WA (remoção dos underscores e adição do prefixo). Essa é a mesma conversão que fizemos para o KEGG na seção 4. Se um gene não possui correspondência no dicionário, ele simplesmente não será incluído na rede.

8.4 12. Consultando o STRING via API

O STRING disponibiliza uma API pública que permite realizar consultas programáticas. Vamos utilizá-la para: (1) mapear nossos identificadores CAALFM_ para os identificadores internos do STRING, e (2) obter a rede de interações entre eles.

O identificador taxonômico da cepa SC5314 de C. albicans no STRING é 237561.

8.4.1 Mapeando os identificadores

# Concatenar os genes no formato CAALFM_ para a requisição à API
genes_concatenado <- paste0(genes_deg_caalfm, collapse = "%0d")

# Requisição para mapear nossos IDs para os IDs do STRING
req_map <- RCurl::postForm(
  "https://string-db.org/api/tsv/get_string_ids",
  identifiers = genes_concatenado,
  echo_query = "1",
  species = "237561"  # C. albicans SC5314
)

map_ids <- read.table(text = req_map, sep = "\t", header = TRUE, quote = "")

cat("Genes mapeados no STRING:", nrow(map_ids), "de", length(genes_deg_caalfm), "\n")
Genes mapeados no STRING: 70 de 75 

8.4.2 Obtendo a rede de interações

# Concatenar os identificadores do STRING
genes_string_concatenado <- paste0(unique(map_ids$stringId), collapse = "%0d")

# Requisição para o método 'network'
req_net <- RCurl::postForm(
  "https://string-db.org/api/tsv/network",
  identifiers = genes_string_concatenado,
  required_score = "0",   # Sem filtro mínimo (filtraremos depois)
  species = "237561"
)

int_network <- read.table(text = req_net, sep = "\t", header = TRUE)
int_network <- unique(int_network)

cat("Interações brutas obtidas:", nrow(int_network), "\n")
Interações brutas obtidas: 237 

8.5 13. Filtrando as Interações com combinescores

As interações brutas incluem todas as fontes de evidência e todos os níveis de confiança. Para obtermos uma rede de alta qualidade, precisamos:

  1. Selecionar os canais de evidência mais confiáveis para o nosso contexto.
  2. Combinar os escores desses canais usando a fórmula probabilística do STRING.
  3. Filtrar pelo escore combinado mínimo.

A função combinescores abaixo implementa exatamente esse procedimento. Ela recebe a tabela de interações, seleciona os canais desejados e calcula o escore combinado usando a fórmula: \(1 - \prod_{i}(1 - S_i)\), onde \(S_i\) é o escore de cada canal. Essa abordagem probabilística garante que múltiplas fontes fracas de evidência possam se somar para produzir uma interação de alta confiança.

# Função para combinar escores de acordo com o algoritmo do STRING
combinescores <- function(dat, evidences = "all", confLevel = 0.4) {
  if (evidences[1] == "all") {
    edat <- dat[, -c(1, 2, ncol(dat))]
  } else {
    if (!all(evidences %in% colnames(dat))) {
      stop("ERRO: uma ou mais evidências não encontradas nas colunas dos dados!")
    }
    edat <- dat[, evidences]
  }
  # Os escores do STRING vêm em escala 0-1000; converter para 0-1
  if (any(edat > 1)) {
    edat <- edat / 1000
  }
  # Fórmula probabilística: 1 - prod(1 - Si)
  edat <- 1 - edat
  sc <- apply(X = edat, MARGIN = 1, FUN = function(x) 1 - prod(x))
  dat <- cbind(dat[, c(1, 2)], combined_score = sc)
  idx <- dat$combined_score >= confLevel
  dat <- dat[idx, ]
  return(dat)
}

Vamos aplicar o filtro. Utilizaremos os canais de co-expressão, evidência experimental e bancos de dados curados. Aqui, por motivos didáticos, vamos estabelecer um escore combinado mínimo de 0.15 (baixa confiança):

# Aplicar o filtro por canais e confiança mínima
int_network_filtered <- combinescores(
  int_network,
  evidences = c("ascore", "escore", "dscore"),
  confLevel = 0.15
)

cat("Interações após filtro (confiança ≥ 0.15):", nrow(int_network_filtered), "\n")
Interações após filtro (confiança ≥ 0.15): 157 
NotePonto Importante — Escolha do Threshold

O STRING utiliza uma classificação de confiança para seus escores:

  • Low confidence: > 0.15
  • Medium confidence: > 0.4
  • High confidence: > 0.7
  • Highest confidence: > 0.9

Para C. albicans, um organismo com menos interações experimentalmente validadas, utilizamos o threshold de baixa confiança (0.15) de forma didática para obter uma rede com conexões suficientes a fim de explorar e interpretar grafos durante a aula. Diferentes escores devem ser empregados (como > 0.4 ou 0.7) a depender do contexto e perguntas biológicas com intenção em dados com nível maior de acurácia.

8.6 14. Construindo o Grafo com igraph

Agora vamos transformar a tabela de interações filtrada em um objeto igraph e anotar cada nó com as informações de expressão diferencial (log2FC e significância):

# Criar dataframe de anotação mapeando stringId → nomes preferenciais e IDs originais
ann <- data.frame(
  stringId = map_ids$stringId,
  gene_name = map_ids$preferredName,
  query_caalfm = map_ids$queryItem,
  stringsAsFactors = FALSE
) %>%
  distinct(stringId, .keep_all = TRUE) %>%
  mutate(
    # Mapear CAALFM_ de volta para Ensembl ID
    ensembl_id = caalfm_to_ensembl[query_caalfm]
  )

# Substituir os stringIds pelos nomes dos genes na tabela de interações
int_network_named <- int_network_filtered %>%
  mutate(
    gene_A = ann$gene_name[match(stringId_A, ann$stringId)],
    gene_B = ann$gene_name[match(stringId_B, ann$stringId)]
  ) %>%
  filter(!is.na(gene_A), !is.na(gene_B)) %>%
  dplyr::select(gene_A, gene_B, combined_score)

# Criar objeto igraph (rede não direcionada)
g <- graph_from_data_frame(int_network_named, directed = FALSE)

# Remover arestas duplicadas e auto-loops
g <- igraph::simplify(g)

cat("Rede construída:", vcount(g), "nós,", ecount(g), "arestas\n")
Rede construída: 64 nós, 157 arestas

8.6.1 Anotando os nós com informações de expressão

# Criar dicionário gene_name → ensembl_id
name_to_ensembl <- setNames(ann$ensembl_id, ann$gene_name)

# Para cada nó do grafo, buscar o Ensembl ID e depois as informações do DEG
node_names <- V(g)$name
node_ensembl <- name_to_ensembl[node_names]

# Extrair log2FC e padj
node_log2fc <- res_dge[node_ensembl, "log2FoldChange"]
node_padj <- res_dge[node_ensembl, "padj"]
node_signif <- res_dge[node_ensembl, "signif"]

# Atribuir como atributos do grafo
V(g)$log2FC <- ifelse(is.na(node_log2fc), 0, node_log2fc)
V(g)$padj <- ifelse(is.na(node_padj), 1, node_padj)
V(g)$signif <- ifelse(is.na(node_signif), "Not significant", node_signif)
V(g)$ensembl_id <- ifelse(is.na(node_ensembl), "", node_ensembl)

# Calcular -log10(padj) como atributo (usado na visualização)
V(g)$neg_log10_padj <- -log10(ifelse(V(g)$padj == 0, .Machine$double.xps, V(g)$padj))

8.7 15. Layout Interativo com easylayout

Para posicionar os nós da rede de forma visualmente clara, utilizaremos o pacote easylayout, desenvolvido pelo nosso grupo. O easylayout é um pacote R que utiliza simulações de forças interativas diretamente dentro do RStudio, permitindo ajustar o layout em tempo real antes de exportar o resultado para plotagem com ggraph ou ggplot2.

O easylayout não é uma biblioteca de visualização, ele é uma ferramenta de posicionamento que se integra com bibliotecas existentes. Ele recebe um objeto igraph, abre uma aplicação web interativa dentro do IDE, e retorna uma matriz de coordenadas XY que pode ser usada por qualquer pacote de plotagem.

TipUsando o easylayout na prática
  1. Execute layout <- easylayout(g) — uma janela interativa abrirá no RStudio.
  2. Ajuste as forças de atração/repulsão em tempo real até que o layout fique satisfatório.
  3. Use o modo de edição para mover nós manualmente, se necessário.
  4. Clique em “Export” — as coordenadas serão enviadas de volta ao R.
  5. Plote com ggraph(g, layout = layout) + ...
library(easylayout)

# Layout interativo(Descomente a linha para executar) — uma janela abrirá no RStudio
# layout_ppi <- easylayout(g)

# Salvar o layout para reprodutibilidade
# saveRDS(layout_ppi, file = here::here("data/DGE/layout_ppi.rds"))

8.8 16. Visualização da Rede PPI com ggraph

Vamos plotar a rede utilizando ggraph, colorindo os nós de acordo com o log2 Fold Change (escala contínua: azul para downregulados, vermelho para upregulados) e ajustando o tamanho conforme a significância estatística (-log10 do p-valor ajustado):

# Carregar layout

layout_ppi <- readRDS(here::here("data/DGE/layout_ppi.rds"))

ggraph(g, layout = layout_ppi) +
  geom_edge_link(alpha = 0.3, color = "grey60", width = 0.3) +
  geom_node_point(
    aes(color = log2FC, size = neg_log10_padj),
    alpha = 0.85
  ) +
  geom_node_text(
    aes(label = name),
    size = 2.8,
    repel = TRUE,
    max.overlaps = 20,
    color = "black",
    segment.color = "grey50",
    segment.size = 0.3
  ) +
  scale_color_gradient2(
    low = "#1F78B4",
    mid = "grey90",
    high = "#E31A1C",
    midpoint = 0,
    name = expression(log[2] ~ "FC"),
    limits = c(
      -max(abs(V(g)$log2FC), na.rm = TRUE),
       max(abs(V(g)$log2FC), na.rm = TRUE)
    )
  ) +
  scale_size_continuous(
    name = expression("-" ~ log[10] ~ "padj"),
    range = c(2, 8)
  ) +
  labs(
    title = "Rede PPI — Genes Diferencialmente Expressos de C. albicans",
    subtitle = "Interações STRING (co-expressão + experimental + banco de dados, score ≥ 0.4)",
    caption = "Fonte: STRING v12 | Organismo: C. albicans SC5314 (taxid: 237561)"
  ) +
  coord_fixed() +
  theme_void(base_size = 13) +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
    plot.subtitle = element_text(hjust = 0.5, color = "grey40", size = 10),
    plot.caption = element_text(hjust = 1, color = "grey50", size = 8),
    legend.position = "right",
    plot.margin = margin(10, 10, 10, 10)
  )

8.8.1 Como interpretar a rede PPI:

  • Cor dos nós: segue uma escala contínua do azul (downregulado sob vancomicina) ao vermelho (upregulado), passando por cinza (log2FC próximo de zero). Isso permite identificar rapidamente se regiões da rede são dominadas por up ou downregulação.
  • Tamanho dos nós: proporcional a \(-\log_{10}(\text{padj})\) quanto maior o ponto, mais significativa é a mudança de expressão daquele gene.
  • Arestas: representam associações funcionais documentadas no STRING (co-expressão, evidência experimental ou bancos de dados curados). Arestas mais densas indicam sub-redes de proteínas que atuam de forma coordenada.
  • Rótulos: aparecem apenas para os 10% dos genes mais significativos, destacando os candidatos de maior interesse biológico.
  • Clusters visuais: grupos de nós densamente conectados sugerem módulos funcionais — conjuntos de proteínas que trabalham juntas em um processo biológico. Identifique se esses módulos correspondem às vias enriquecidas que encontramos anteriormente.

Concluímos a análise completa de RNA-seq da Candida albicans* em resposta à vancomicina. Desde a descontaminação das leituras do hospedeiro com o nf-core/detaxizer, passando pela quantificação com o nf-core/rnaseq, a análise de expressão diferencial com DESeq2, o enriquecimento funcional com KEGG e GO, até a construção e visualização de redes de interação proteína-proteína com o STRING — percorremos um pipeline completo e reprodutível para transcriptômica de patógenos, enfrentando os desafios específicos de um organismo não-modelo.*