# Manipulação e Visualização
library(dplyr)
library(tidyr)
library(ggplot2)
library(tibble)
library(RColorBrewer)
library(cowplot)
# Transcriptômica e Estatística
library(DESeq2)
library(matrixStats)
# Heatmaps
library(pheatmap)
library(ComplexHeatmap)
library(circlize)
# Enriquecimento e Redes
library(clusterProfiler)
library(org.Dr.eg.db)
library(STRINGdb)
library(igraph)
library(tidygraph)
library(ggraph)
# Semente global para reprodutibilidade
set.seed(123)Projeto de Transcriptômica: FASD e Dieta Hiperlipídica em Zebrafish
Definição da Pergunta Biológica
Objetivo Central: Avaliar como a Exposição Alcoólica Fetal (FAE) altera o transcriptoma basal de embriões de Danio rerio aos 13 dpf e investigar como essas perturbações se refletem na topologia de redes de interação gênica em condições de dieta normal (ND).
O fluxo de análise buscará responder: 1. Quais genes são diferencialmente expressos pela FAE sob dieta normal? 2. Quais processos biológicos estão enriquecidos nessa assinatura transcricional? 3. Como a conectividade das redes de coexpressão (LIONESS) e de interação proteína-proteína (PPI) é remodelada pela exposição ao álcool?
1. Setup e Preparação dos Dados
2. Importação e alinhamento
# 1. Metadados
meta <- read.table("metadata_fasd.csv", sep = ",", fill = TRUE, header = TRUE, stringsAsFactors = FALSE) %>%
transmute(
sample = Run,
age = Developmental_Stage,
treatment = treatment,
diet = Diet
) %>%
dplyr::filter(age == "13 dpf")
# Limpeza dos fatores para o DESeq2
meta <- meta %>%
mutate(
treatment = factor(ifelse(treatment == "0% EtOH", "Control", "EtOH"), levels = c("Control", "EtOH")),
diet = factor(ifelse(diet == "normal", "ND", "HFHC"), levels = c("ND", "HFHC"))
)
# 2. Contagens
counts <- read.csv("GSE142311_20180702_OGWFAE_raw.data.csv", sep = ";", header = TRUE, check.names = FALSE)
if (!is.numeric(counts[[1]])) {
rownames(counts) <- counts[[1]]
counts <- counts[, -1]
}
if (nrow(meta) == ncol(counts)) {
meta$sample <- colnames(counts)
rownames(meta) <- meta$sample
} else {
stop("Erro: O número de amostras no metadado (", nrow(meta),
") não bate com as colunas do count matrix (", ncol(counts), ").")
}
# 3. Alinhamento Estrito
common_samples <- intersect(colnames(counts), meta$sample)
counts <- counts[, common_samples, drop = FALSE]
meta <- meta %>%
filter(sample %in% common_samples) %>%
arrange(match(sample, colnames(counts)))
rownames(meta) <- meta$sample
stopifnot(all(meta$sample == colnames(counts)))3. Controle de Qualidade (QC)
lib_size <- colSums(counts)
qc_tbl <- meta %>% mutate(lib_size = lib_size, genes_detected = colSums(counts > 0))
p_lib <- ggplot(qc_tbl, aes(x = sample, y = lib_size, fill = treatment)) +
geom_col() + theme_bw() + theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(title = "Tamanho de Biblioteca", y = "Reads", x = "", fill = "Tratamento")
p_genes <- ggplot(qc_tbl, aes(x = sample, y = genes_detected, fill = treatment)) +
geom_col() + theme_bw() + theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(title = "Genes Detectados", y = "Genes", x = "", fill = "Tratamento")
plot_grid(p_lib, p_genes)# Transformação VST
dds_tmp <- DESeqDataSetFromMatrix(countData = counts, colData = meta, design = ~1)
vsd <- vst(dds_tmp, blind = TRUE)
vst_mat <- assay(vsd)
# PCA
pca <- prcomp(t(vst_mat))
pca_tbl <- as.data.frame(pca$x) %>% mutate(sample = rownames(pca$x)) %>% left_join(meta, by = "sample")
p_pca <- ggplot(pca_tbl, aes(x = PC1, y = PC2, color = diet, shape = treatment)) +
geom_point(size = 4, alpha = 0.8) +
scale_color_manual(values = c("ND" = "#FC8D62", "HFHC" = "#66C2A5")) +
theme_bw() +
labs(title = "PCA (VST) - Foco no Agrupamento Global")
p_pca4. Expressão Diferencial (DESeq2)
# Carregue este pacote junto com os outros no início do script
library(ggrepel)
# Isolando a análise para a Dieta Normal (ND)
idx_nd <- meta$diet == "ND"
dds_nd <- DESeqDataSetFromMatrix(
countData = counts[, idx_nd, drop = FALSE],
colData = meta[idx_nd, ],
design = ~ treatment
)
# Filtro de baixa contagem
dds_nd <- dds_nd[rowSums(counts(dds_nd)) >= 10, ]
dds_nd <- DESeq(dds_nd)
# Resultados com Shrinkage
res_nd <- lfcShrink(dds_nd, coef = "treatment_EtOH_vs_Control", type = "apeglm")
res_nd_tbl <- as.data.frame(res_nd) %>%
rownames_to_column("gene") %>%
# Remove NAs para não distorcer os limites do gráfico
filter(!is.na(padj)) %>%
mutate(sig = case_when(
padj < 0.05 & log2FoldChange > 0 ~ "Up-regulado",
padj < 0.05 & log2FoldChange < 0 ~ "Down-regulado",
TRUE ~ "NS"
))
# 1. Separando os 10 genes mais significativos
top_10 <- res_nd_tbl %>%
arrange(padj) %>%
slice_head(n = 10)
# 2. Encontrando os limites reais do eixo X para não sobrar espaço vazio
limites_x <- range(res_nd_tbl$log2FoldChange, na.rm = TRUE)
# Volcano Plot
ggplot(res_nd_tbl, aes(x = log2FoldChange, y = -log10(padj))) +
# Camada base: todos os genes
geom_point(aes(color = sig), alpha = 0.6, size = 2) +
# Camada de destaque: apenas os top 10, usando shape 21 (preenchimento + borda preta)
geom_point(data = top_10, aes(fill = sig), shape = 21, color = "black", size = 2.5, stroke = 0.8) +
# Camada de texto: labels apenas para os top 10
geom_text_repel(data = top_10, aes(label = gene),
size = 3.5,
fontface = "bold",
box.padding = 0.5,
max.overlaps = Inf) +
# Definindo cores
scale_color_manual(values = c("Down-regulado" = "#2166AC", "NS" = "grey80", "Up-regulado" = "#B2182B")) +
scale_fill_manual(values = c("Down-regulado" = "#2166AC", "NS" = "grey80", "Up-regulado" = "#B2182B"), guide = "none") +
# Cortando o excesso de espaço nos eixos
coord_cartesian(xlim = limites_x) +
theme_bw() +
labs(title = "Volcano plot: FAE vs Control (Dieta Normal)", color = "Significância")5. Enriquecimento Funcional e Redes
# -------------------------------------------------------------------------
# 1. ORA (Over-Representation Analysis)
# -------------------------------------------------------------------------
sig_genes <- res_nd_tbl %>% filter(sig != "NS") %>% pull(gene)
entrez <- bitr(sig_genes, fromType = "SYMBOL", toType = "ENTREZID", OrgDb = org.Dr.eg.db)
if(nrow(entrez) > 0) {
ego <- enrichGO(
gene = entrez$ENTREZID, OrgDb = org.Dr.eg.db, ont = "BP",
pAdjustMethod = "BH", pvalueCutoff = 0.05, readable = TRUE
)
if(!is.null(ego) && nrow(as.data.frame(ego)) > 0) {
p_ora <- dotplot(ego, showCategory = 15) +
labs(title = "ORA - Processos Biológicos (FAE vs Control)")
print(p_ora)
} else {
cat("Nenhum termo significativo encontrado no ORA.\n")
}
}# -------------------------------------------------------------------------
# 2. GSEA (Gene Set Enrichment Analysis)
# -------------------------------------------------------------------------
# Ranking pelo log2FoldChange (usando todos os genes testados, sem filtro de p-valor)
rank_tbl <- res_nd_tbl %>%
filter(!is.na(log2FoldChange)) %>%
arrange(desc(log2FoldChange))
rank_vec <- rank_tbl$log2FoldChange
names(rank_vec) <- rank_tbl$gene
# Conversão de SYMBOL para ENTREZID para o GSEA
rank_entrez <- bitr(names(rank_vec), fromType = "SYMBOL", toType = "ENTREZID", OrgDb = org.Dr.eg.db)
# Filtragem e substituição de nomes no vetor
rank_vec <- rank_vec[names(rank_vec) %in% rank_entrez$SYMBOL]
names(rank_vec) <- rank_entrez$ENTREZID[match(names(rank_vec), rank_entrez$SYMBOL)]
# O vetor obrigatoriamente precisa estar ordenado do maior LFC para o menor
rank_vec <- sort(rank_vec, decreasing = TRUE)
# Execução do GSEA
gsea <- gseGO(
geneList = rank_vec,
OrgDb = org.Dr.eg.db,
ont = "BP",
pAdjustMethod = "BH",
verbose = FALSE
)
# Separação dos top pathways (Up e Down) e plot customizado
if(!is.null(gsea) && nrow(as.data.frame(gsea)) > 0) {
gsea_tbl <- as.data.frame(gsea)
top_up <- gsea_tbl %>% filter(NES > 0) %>% arrange(desc(NES)) %>% slice_head(n = 10)
top_down <- gsea_tbl %>% filter(NES < 0) %>% arrange(NES) %>% slice_head(n = 10)
gsea_top <- bind_rows(top_up, top_down) %>%
mutate(Description = factor(Description, levels = Description[order(NES)]))
p_gsea <- ggplot(gsea_top, aes(x = NES, y = Description, color = p.adjust, size = setSize)) +
geom_point() +
scale_color_viridis_c(option = "C") +
theme_bw() +
labs(
title = "GSEA - Top Pathways Ativados e Reprimidos",
x = "Normalized Enrichment Score (NES)",
y = NULL
)
print(p_gsea)
} else {
cat("Nenhum termo significativo encontrado no GSEA.\n")
}5.1. Rede de Interação Proteína-Proteína (PPI)
string_db <- STRINGdb$new(version = "11.5", species = 7955, score_threshold = 400, input_directory = "")
# 1. Mapeamento
mapped <- string_db$map(data.frame(gene = sig_genes), "gene", removeUnmappedRows = TRUE) %>%
dplyr::distinct(STRING_id, .keep_all = TRUE)Warning: we couldn't map to STRING 4% of your identifiers
ppi <- string_db$get_interactions(mapped$STRING_id)
# 2. Preparar os nós trazendo o log2FoldChange do DESeq2 (res_nd_tbl)
nodes_tbl <- data.frame(STRING_id = unique(c(ppi$from, ppi$to))) %>%
dplyr::left_join(mapped, by = "STRING_id") %>%
# Trazendo os resultados de expressão (certifique-se de que res_nd_tbl está carregado)
dplyr::left_join(res_nd_tbl %>% dplyr::select(gene, log2FoldChange), by = "gene") %>%
dplyr::mutate(label = ifelse(is.na(gene), STRING_id, gene))
# 3. Construir o grafo base
g <- graph_from_data_frame(d = ppi, vertices = nodes_tbl, directed = FALSE)
g <- igraph::simplify(g)
# 4. Calcular métricas e filtrar rótulos com tidygraph
tg <- as_tbl_graph(g) %>%
activate(nodes) %>%
mutate(
degree = centrality_degree(),
# Define quem terá o nome plotado. Aqui, filtramos os top 15 genes com maior grau (hubs)
is_hub = rank(-degree) <= 15,
plot_label = ifelse(is_hub, label, NA)
)
# 5. Plotagem Estética
set.seed(123) # Fixa o layout para não mudar a cada execução
ggraph(tg, layout = "fr") +
# Arestas mais transparentes para não roubar a atenção
geom_edge_link(alpha = 0.15, color = "gray60") +
# Nós usando shape 21 (permite preenchimento interno com cor + borda branca)
geom_node_point(aes(size = degree, fill = log2FoldChange), shape = 21, color = "white", stroke = 0.5) +
# Escala divergente: Down = Azul, Neutro = Branco, Up = Vermelho
scale_fill_gradient2(
low = "#2166AC", mid = "white", high = "#B2182B", midpoint = 0,
name = "Log2 Fold Change"
) +
# Escala de tamanho ajustada para não engolir o plot
scale_size_continuous(range = c(2, 9), name = "Grau (Degree)") +
# Textos apenas nos Hubs, com sombra branca (bg.color) para destacar por cima das linhas
geom_node_text(
aes(label = plot_label),
repel = TRUE,
size = 4,
fontface = "bold",
color = "black",
bg.color = "white",
bg.r = 0.15,
max.overlaps = Inf
) +
theme_void() +
labs(
title = "Rede PPI - Genes Significativos na Dieta Normal",
subtitle = "Top 15 Hubs mapeados pela direção da regulação (Up/Down)"
) +
theme(
plot.title = element_text(face = "bold", size = 16),
legend.position = "right"
)5.2 Redes Específicas por Amostra (LIONESS)
# Função otimizada LIONESS (Pearson)
lioness_cor_optimized <- function(se) {
X <- assay(se); n <- ncol(X)
S <- rowSums(X); SS <- rowSums(X^2); SP <- X %*% t(X)
num_full <- SP - outer(S, S)/n
den_full <- sqrt((SS - S^2/n) %o% (SS - S^2/n))
C_full <- num_full / den_full
ut_idx <- which(upper.tri(C_full), arr.ind = TRUE)
genes <- rownames(X)
reg_names <- genes[ut_idx[, 1]]; tar_names <- genes[ut_idx[, 2]]
v_full <- C_full[ut_idx]
out <- matrix(NA_real_, nrow = length(reg_names), ncol = n)
colnames(out) <- colnames(X)
for (q in seq_len(n)) {
xq <- X[, q]; S_q <- S - xq; SS_q <- SS - xq^2; SP_q <- SP - xq %*% t(xq)
num_q <- SP_q - outer(S_q, S_q)/(n - 1)
den_q <- sqrt((SS_q - S_q^2/(n - 1)) %o% (SS_q - S_q^2/(n - 1)))
C_q <- num_q / den_q
out[, q] <- n * (v_full - C_q[ut_idx]) + C_q[ut_idx]
}
list(W = out, reg = reg_names, tar = tar_names)
}
# Preparando os dados (Usaremos os top 1000 genes mais variáveis da Dieta ND para performance)
vst_nd <- vst_mat[, idx_nd]
gene_var <- rowVars(vst_nd)
top_idx <- order(gene_var, decreasing = TRUE)[1:1000]
expr_top <- vst_nd[top_idx, , drop = FALSE]
se_filtered <- SummarizedExperiment(
assays = list(counts = expr_top),
colData = DataFrame(sample = colnames(expr_top), row.names = colnames(expr_top)),
rowData = DataFrame(gene = rownames(expr_top), row.names = rownames(expr_top))
)
# Rodando LIONESS
lion_res <- lioness_cor_optimized(se_filtered)
# Calculando o Grau (Degree) por amostra
W <- lion_res$W
all_genes <- unique(c(lion_res$reg, lion_res$tar))
deg_list <- lapply(seq_len(ncol(W)), function(i) {
col_w <- abs(W[, i])
thr <- mean(col_w, na.rm = TRUE) + (1.5 * sd(col_w, na.rm = TRUE)) # Threshold adaptativo
keep <- !is.na(col_w) & col_w > thr
active_genes <- c(lion_res$reg[keep], lion_res$tar[keep])
gene_counts <- table(active_genes)
sample_deg <- setNames(integer(length(all_genes)), all_genes)
sample_deg[names(gene_counts)] <- as.integer(gene_counts)
sample_deg
})
deg_mat <- as.data.frame(do.call(cbind, deg_list))
colnames(deg_mat) <- colnames(W)
rownames(deg_mat) <- all_genes# Extraindo Differentially Interacting Genes (DIGs)
meta_nd <- meta[idx_nd, ]
sf_idx <- which(meta_nd$treatment == "EtOH")
gc_idx <- which(meta_nd$treatment == "Control")
dig_res <- lapply(seq_len(nrow(deg_mat)), function(i) {
gene_name <- rownames(deg_mat)[i]
group_sf <- deg_mat[i, sf_idx]; group_gc <- deg_mat[i, gc_idx]
if (sd(group_sf) == 0 && sd(group_gc) == 0) return(NULL)
tryCatch({
t_test <- t.test(group_sf, group_gc)
data.frame(gene = gene_name, p_val = t_test$p.value, diff_deg = mean(group_sf) - mean(group_gc))
}, error = function(e) NULL)
}) %>% bind_rows() %>% mutate(padj = p.adjust(p_val, method = "BH")) %>% arrange(p_val)
# 1. Checa se algum gene passou no padj rigoroso. Se não, avisa no console.
qtd_sig <- dig_res %>% filter(padj < 0.05) %>% nrow()
if (qtd_sig == 0) {
message("Aviso: Nenhum gene atingiu padj < 0.05. Plotando os top 6 pelo p-valor nominal (p_val).")
}
# 2. Pega os top 6 genes pelo ranking de significância bruta (mesmo que padj > 0.05)
top_digs <- dig_res %>%
arrange(p_val) %>%
slice_head(n = 6) %>%
pull(gene)
# Plot dos Top DIGs
if (length(top_digs) > 0) {
plot_tbl <- as.data.frame(t(deg_mat[top_digs, , drop = FALSE])) %>%
rownames_to_column("sample") %>%
pivot_longer(cols = -sample, names_to = "gene", values_to = "degree") %>%
left_join(meta_nd %>% dplyr::select(sample, treatment), by = "sample")
p_lioness <- ggplot(plot_tbl, aes(x = treatment, y = degree, fill = treatment)) +
geom_boxplot(outlier.size = 0.6) +
geom_jitter(width = 0.1, alpha = 0.6) +
facet_wrap(~ gene, scales = "free_y") +
scale_fill_manual(values = c("Control" = "#4575B4", "EtOH" = "#D73027")) +
theme_bw() +
labs(
title = "Top DIGs: Alteração na Conectividade da Rede (LIONESS)",
y = "Grau (Degree)",
x = ""
)
# Força a exibição do gráfico
print(p_lioness)
}