Identificação e validação de uma assinatura de expressão gênica BMI1 no sarcoma de Ewing

Data de Publicação

9 de março de 2026

Nota

Este relatório foi gerado com auxílio de IA sob direção humana geral.

Conteúdo revisado e validado por Tainá Schons em 09/03/26.

Introdução

Este documento descreve o pipeline bioinformático utilizado para identificar uma assinatura transcriptômica associada à atividade do gene BMI1 no sarcoma de Ewing e avaliar seu valor prognóstico em coortes independentes obtidas do GEO.

O estudo foi estruturado em duas etapas:

  1. Descoberta: análise de expressão diferencial no dataset GSE16016, que compara tumores com alta e baixa expressão de BMI1 em dados de array de exon (Affymetrix HuEx-1.0-ST).
  2. Validação: cálculo de um score de assinatura BMI1 em 10 coortes públicas adicionais e avaliação da associação com sobrevida livre de eventos (EFS) via Kaplan-Meier e regressão de Cox.

1. Ambiente e configuração

library(oligo)
Carregando pacotes exigidos: BiocGenerics
Carregando pacotes exigidos: generics

Anexando pacote: 'generics'
Os seguintes objetos são mascarados por 'package:base':

    as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
    setequal, union

Anexando pacote: 'BiocGenerics'
Os seguintes objetos são mascarados por 'package:stats':

    IQR, mad, sd, var, xtabs
Os seguintes objetos são mascarados por 'package:base':

    anyDuplicated, aperm, append, as.data.frame, basename, cbind,
    colnames, dirname, do.call, duplicated, eval, evalq, Filter, Find,
    get, grep, grepl, is.unsorted, lapply, Map, mapply, match, mget,
    order, paste, pmax, pmax.int, pmin, pmin.int, Position, rank,
    rbind, Reduce, rownames, sapply, saveRDS, table, tapply, unique,
    unsplit, which.max, which.min
Carregando pacotes exigidos: oligoClasses
Welcome to oligoClasses version 1.70.0
Carregando pacotes exigidos: Biobase
Welcome to Bioconductor

    Vignettes contain introductory material; view with
    'browseVignettes()'. To cite Bioconductor, see
    'citation("Biobase")', and for packages 'citation("pkgname")'.
Carregando pacotes exigidos: Biostrings
Carregando pacotes exigidos: S4Vectors
Carregando pacotes exigidos: stats4

Anexando pacote: 'S4Vectors'
O seguinte objeto é mascarado por 'package:utils':

    findMatches
Os seguintes objetos são mascarados por 'package:base':

    expand.grid, I, unname
Carregando pacotes exigidos: IRanges

Anexando pacote: 'IRanges'
O seguinte objeto é mascarado por 'package:grDevices':

    windows
Carregando pacotes exigidos: XVector
Carregando pacotes exigidos: GenomeInfoDb

Anexando pacote: 'Biostrings'
O seguinte objeto é mascarado por 'package:base':

    strsplit
================================================================================
Welcome to oligo version 1.72.0
================================================================================
library(GEOquery)
Setting options('download.file.method.GEOquery'='auto')
Setting options('GEOquery.inmemory.gpl'=FALSE)
library(limma)
Warning: pacote 'limma' foi compilado no R versão 4.5.1

Anexando pacote: 'limma'
O seguinte objeto é mascarado por 'package:oligo':

    backgroundCorrect
O seguinte objeto é mascarado por 'package:BiocGenerics':

    plotMA
library(sva)
Carregando pacotes exigidos: mgcv
Carregando pacotes exigidos: nlme

Anexando pacote: 'nlme'
O seguinte objeto é mascarado por 'package:Biostrings':

    collapse
O seguinte objeto é mascarado por 'package:IRanges':

    collapse
This is mgcv 1.9-1. For overview type 'help("mgcv-package")'.
Carregando pacotes exigidos: genefilter
Carregando pacotes exigidos: BiocParallel
library(survival)
library(survminer)
Warning: pacote 'survminer' foi compilado no R versão 4.5.2
Carregando pacotes exigidos: ggplot2
Warning: pacote 'ggplot2' foi compilado no R versão 4.5.2
Carregando pacotes exigidos: ggpubr
Warning: pacote 'ggpubr' foi compilado no R versão 4.5.2

Anexando pacote: 'survminer'
O seguinte objeto é mascarado por 'package:survival':

    myeloma
library(dplyr)
Warning: pacote 'dplyr' foi compilado no R versão 4.5.2

Anexando pacote: 'dplyr'
O seguinte objeto é mascarado por 'package:nlme':

    collapse
O seguinte objeto é mascarado por 'package:oligo':

    summarize
Os seguintes objetos são mascarados por 'package:Biostrings':

    collapse, intersect, setdiff, setequal, union
O seguinte objeto é mascarado por 'package:GenomeInfoDb':

    intersect
O seguinte objeto é mascarado por 'package:XVector':

    slice
Os seguintes objetos são mascarados por 'package:IRanges':

    collapse, desc, intersect, setdiff, slice, union
Os seguintes objetos são mascarados por 'package:S4Vectors':

    first, intersect, rename, setdiff, setequal, union
O seguinte objeto é mascarado por 'package:Biobase':

    combine
Os seguintes objetos são mascarados por 'package:BiocGenerics':

    combine, intersect, setdiff, setequal, union
O seguinte objeto é mascarado por 'package:generics':

    explain
Os seguintes objetos são mascarados por 'package:stats':

    filter, lag
Os seguintes objetos são mascarados por 'package:base':

    intersect, setdiff, setequal, union
library(ggplot2)
library(huex10sttranscriptcluster.db)
Carregando pacotes exigidos: AnnotationDbi

Anexando pacote: 'AnnotationDbi'
O seguinte objeto é mascarado por 'package:dplyr':

    select
Carregando pacotes exigidos: org.Hs.eg.db
library(hgu133plus2.db)
library(hugene10sttranscriptcluster.db)
library(hgu133a.db)
setwd("C:/Projetos/Nicole_GEO_DEGS/Nicole")

2. Dataset de descoberta — GSE16016

O GSE16016 contém 10 amostras de sarcoma de Ewing geradas na plataforma Affymetrix HuEx-1.0-ST (GPL5175), divididas em grupos de alta e baixa expressão de BMI1 por western blot. Os dados brutos (arquivos .CEL.gz) foram processados com normalização RMA.

2.1 Leitura dos arquivos CEL e normalização RMA

# Busca recursiva para encontrar CEL files do GSE16016
all_cels <- list.files(".", pattern = "CEL.gz$",
                        recursive = TRUE, full.names = TRUE,
                        ignore.case = TRUE)
cels_16016 <- all_cels[grepl("GSE16016", all_cels)]

# Remover duplicatas (mesmo arquivo em pastas diferentes)
cels_16016 <- cels_16016[!duplicated(basename(cels_16016))]
message(length(cels_16016), " arquivos CEL encontrados para GSE16016")
10 arquivos CEL encontrados para GSE16016
dados_brutos <- read.celfiles(cels_16016)
Carregando pacotes exigidos: pd.huex.1.0.st.v2
Carregando pacotes exigidos: RSQLite
Carregando pacotes exigidos: DBI
Platform design info loaded.
Reading in : ./aff/GSE16016_RAW/GSM400796.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM400798.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM400799.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM400800.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM546483.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM546484.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM546485.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM546486.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM546487.CEL.gz
Reading in : ./aff/GSE16016_RAW/GSM546488.CEL.gz

A normalização usa target = "core", que resume as sondas para o nível de gene (transcript cluster), resultando em um valor por gene por amostra.

message("Normalizando com RMA (nível 'core')...")
Normalizando com RMA (nível 'core')...
eset_16016 <- oligo::rma(dados_brutos, target = "core")
Background correcting
Normalizing
Calculating Expression
message("Normalização concluída: ", nrow(exprs(eset_16016)), " genes, ",
        ncol(exprs(eset_16016)), " amostras")
Normalização concluída: 22011 genes, 10 amostras

Nota técnica: O pacote affy não deve ser carregado junto com oligo nesta etapa, pois ambos exportam funções com o mesmo nome (como probeNames e summarize). Caso affy esteja carregado, utilize detach("package:affy", unload = TRUE) antes de rodar o RMA.

2.2 Definição dos grupos BMI1 High vs Low

Os metadados clínicos do GSE16016 foram obtidos diretamente do GEO. A coluna bmi1 status:ch1 contém a classificação de cada amostra.

gse_info   <- getGEO("GSE16016", destdir = ".", getGPL = FALSE)
Found 1 file(s)
GSE16016_series_matrix.txt.gz
Using locally cached version: ./GSE16016_series_matrix.txt.gz
meta_16016 <- pData(gse_info[[1]])

grupo <- factor(meta_16016$`bmi1 status:ch1`)
levels(grupo) <- c("High", "Low")
table(grupo)
grupo
High  Low 
   5    5 

2.3 Análise de expressão diferencial com limma

design      <- model.matrix(~0 + grupo)
colnames(design) <- levels(grupo)

fit         <- lmFit(eset_16016, design)
cont_matrix <- makeContrasts(High_vs_Low = High - Low, levels = design)
fit2        <- contrasts.fit(fit, cont_matrix)
fit2        <- eBayes(fit2)

res_completo <- topTable(fit2, coef = 1, n = Inf)

2.4 Anotação e filtragem da assinatura BMI1

Os IDs de transcript cluster foram mapeados para gene symbols usando o pacote de anotação huex10sttranscriptcluster.db. A assinatura foi definida com critérios estritos: adj.P.Val < 0,01 e |logFC| > 1.

ids <- rownames(res_completo)
res_completo$Symbol <- mapIds(
  huex10sttranscriptcluster.db,
  keys      = ids,
  column    = "SYMBOL",
  keytype   = "PROBEID",
  multiVals = "first"
)
'select()' returned 1:many mapping between keys and columns
assinatura_bmi1 <- subset(res_completo, adj.P.Val < 0.01 & abs(logFC) > 1)
assinatura_bmi1 <- na.omit(assinatura_bmi1)

message("Assinatura BMI1 identificada com ", nrow(assinatura_bmi1), " genes")
Assinatura BMI1 identificada com 1219 genes

A assinatura resultante inclui genes diferencialmente expressos entre tumores com alta e baixa atividade de BMI1. Genes com logFC positivo são considerados up-regulados pelo BMI1 e serão utilizados para construir o score de atividade nas coortes de validação.

3. Mega-coorte de validação

Dez datasets públicos do GEO foram selecionados para validação. A função processar_validacao() automaticamente detecta a plataforma de cada estudo e aplica o pacote de anotação correspondente.

Plataforma GSEs Pacote
GPL570 (HG-U133 Plus 2) GSE17674, GSE17618, GSE17679, GSE12102 hgu133plus2.db
GPL5175 (HuEx-1.0-ST) GSE63157, GSE68776, GSE63155, GSE63156 huex10sttranscriptcluster.db
GPL6244 (HuGene-1.0-ST) GSE45544 hugene10sttranscriptcluster.db
GPL96 (HG-U133A) GSE39262 hgu133a.db

3.1 Função de processamento por coorte

processar_validacao <- function(gse_id) {
  message("--- Processando: ", gse_id, " ---")
  gse    <- getGEO(gse_id, destdir = ".", getGPL = FALSE)[[1]]
  matriz <- exprs(gse)
  probes <- rownames(matriz)

  plataforma <- annotation(gse)
  message("  Plataforma: ", plataforma)

  db <- switch(plataforma,
    "GPL570"  = hgu133plus2.db,
    "GPL571"  = hgu133plus2.db,
    "GPL96"   = hgu133a.db,
    "GPL5175" = huex10sttranscriptcluster.db,
    "GPL6244" = hugene10sttranscriptcluster.db,
    NULL
  )

  if (is.null(db)) {
    message("  Plataforma não suportada: ", plataforma, " — pulando.")
    return(NULL)
  }

  simbolos <- mapIds(db, keys = probes, column = "SYMBOL",
                     keytype = "PROBEID", multiVals = "first")

  # Colapsar sondas duplicadas pela média
  matriz_df         <- as.data.frame(matriz)
  matriz_df$Symbol  <- simbolos
  matriz_df         <- matriz_df[!is.na(matriz_df$Symbol) & matriz_df$Symbol != "", ]
  matriz_clean      <- aggregate(. ~ Symbol, data = matriz_df, FUN = mean)
  rownames(matriz_clean) <- matriz_clean$Symbol
  matriz_clean$Symbol    <- NULL

  return(as.matrix(matriz_clean))
}

3.2 Execução do loop e construção da mega-matriz

meus_gses_validacao <- c("GSE17674", "GSE17618", "GSE63157", "GSE17679",
                          "GSE68776", "GSE12102", "GSE45544", "GSE39262",
                          "GSE63155", "GSE63156")

lista_matrizes <- list()
for (id in meus_gses_validacao) {
  try({ lista_matrizes[[id]] <- processar_validacao(id) })
}
--- Processando: GSE17674 ---
Found 1 file(s)
GSE17674_series_matrix.txt.gz
Using locally cached version: ./GSE17674_series_matrix.txt.gz
  Plataforma: GPL570
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE17618 ---
Found 1 file(s)
GSE17618_series_matrix.txt.gz
Using locally cached version: ./GSE17618_series_matrix.txt.gz
  Plataforma: GPL570
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE63157 ---
Found 1 file(s)
GSE63157_series_matrix.txt.gz
Using locally cached version: ./GSE63157_series_matrix.txt.gz
  Plataforma: GPL5175
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE17679 ---
Found 1 file(s)
GSE17679_series_matrix.txt.gz
Using locally cached version: ./GSE17679_series_matrix.txt.gz
  Plataforma: GPL570
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE68776 ---
Found 1 file(s)
GSE68776_series_matrix.txt.gz
Using locally cached version: ./GSE68776_series_matrix.txt.gz
  Plataforma: GPL5175
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE12102 ---
Found 1 file(s)
GSE12102_series_matrix.txt.gz
Using locally cached version: ./GSE12102_series_matrix.txt.gz
  Plataforma: GPL570
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE45544 ---
Found 1 file(s)
GSE45544_series_matrix.txt.gz
Using locally cached version: ./GSE45544_series_matrix.txt.gz
  Plataforma: GPL6244
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE39262 ---
Found 1 file(s)
GSE39262_series_matrix.txt.gz
Using locally cached version: ./GSE39262_series_matrix.txt.gz
  Plataforma: GPL96
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE63155 ---
Found 1 file(s)
GSE63155_series_matrix.txt.gz
Using locally cached version: ./GSE63155_series_matrix.txt.gz
  Plataforma: GPL5175
'select()' returned 1:many mapping between keys and columns
--- Processando: GSE63156 ---
Found 1 file(s)
GSE63156_series_matrix.txt.gz
Using locally cached version: ./GSE63156_series_matrix.txt.gz
  Plataforma: GPL5175
'select()' returned 1:many mapping between keys and columns
# Resumo dos GSEs carregados
sapply(lista_matrizes, function(m) {
  if (is.null(m)) "FALHOU" else paste0(nrow(m), " genes")
})
     GSE17674      GSE17618      GSE63157      GSE17679      GSE68776 
"21357 genes" "21357 genes" "17259 genes" "21357 genes" "17259 genes" 
     GSE12102      GSE45544      GSE39262      GSE63155      GSE63156 
"21357 genes" "19974 genes" "13039 genes" "17259 genes" "17259 genes" 

Antes de construir a mega-matriz, é necessário verificar se algum GSE compartilha amostras com outro. Datasets que pertencem à mesma série ou ao mesmo consórcio frequentemente referenciam os mesmos arquivos CEL em múltiplas submissões, o que causaria duplicação de pacientes na matriz de expressão e distorceria o ComBat e os scores.

# Identificar todos os pares de GSEs com sobreposição de IDs de amostras
ids_por_gse <- lapply(lista_matrizes, colnames)
gse_nomes   <- names(ids_por_gse)

for (i in seq_along(gse_nomes)) {
  for (j in seq_along(gse_nomes)) {
    if (j <= i) next
    n <- length(intersect(ids_por_gse[[i]], ids_por_gse[[j]]))
    if (n > 0) cat(gse_nomes[i], "x", gse_nomes[j], ":", n, "amostras em comum\n")
  }
}
GSE17674 x GSE17679 : 62 amostras em comum
GSE17618 x GSE17679 : 55 amostras em comum
GSE63157 x GSE63155 : 46 amostras em comum
GSE63157 x GSE63156 : 39 amostras em comum

A inspeção revelou que GSE17679 é subconjunto de GSE17618 + GSE17674 (mesmos pacientes), e GSE63155 é subconjunto de GSE63157. Esses GSEs foram removidos da mega-matriz para garantir que cada paciente contribua com uma única observação na correção de lote e no cálculo do score.

# Manter apenas coortes com pacientes únicos na matriz de expressão
gses_unicos <- c("GSE17674", "GSE17618", "GSE63157", "GSE68776",
                 "GSE12102", "GSE45544", "GSE39262", "GSE63156")
lista_matrizes_clean <- lista_matrizes[gses_unicos]

# Genes presentes em todos os datasets
genes_comuns <- Reduce(intersect, lapply(lista_matrizes_clean, rownames))
message(length(genes_comuns), " genes em comum entre todas as coortes")
11839 genes em comum entre todas as coortes
mega_matriz <- do.call(cbind, lapply(lista_matrizes_clean, function(m) m[genes_comuns, ]))
message("Mega-matriz: ", nrow(mega_matriz), " genes x ", ncol(mega_matriz), " amostras únicas")
Mega-matriz: 11839 genes x 447 amostras únicas

4. Correção de efeito de lote (ComBat)

Os dados provenientes de plataformas e laboratórios distintos apresentam efeitos de lote sistemáticos que precisam ser removidos antes de qualquer análise combinada. O método ComBat (Johnson et al., 2007) foi aplicado com cada GSE como um lote independente.

vetor_batch <- as.factor(rep(names(lista_matrizes_clean),
                              sapply(lista_matrizes_clean, ncol)))

message("Executando ComBat...")
Executando ComBat...
mega_matriz_limpa <- ComBat(dat = mega_matriz, batch = vetor_batch)
Found8batches
Adjusting for0covariate(s) or covariate level(s)
Standardizing Data across genes
Fitting L/S model and finding priors
Finding parametric adjustments
Adjusting the Data
message("Correção concluída: ", nrow(mega_matriz_limpa), " genes x ",
        ncol(mega_matriz_limpa), " amostras")
Correção concluída: 11839 genes x 447 amostras

5. Cálculo dos scores da assinatura BMI1

Três versões do score foram testadas para avaliar qual subconjunto de genes da assinatura tem maior valor prognóstico:

  • Score UP: média dos genes up-regulados pelo BMI1 (logFC > 0). Alta expressão = alta atividade BMI1.
  • Score DOWN: média negada dos genes down-regulados pelo BMI1 (logFC < 0). Como o BMI1 suprime esses genes, a negação alinha a direção do score com a atividade BMI1.
  • Score Combinado: média(UP) − média(DOWN), capturando o efeito direcional completo da assinatura.
# Preparar assinatura: um símbolo por gene (maior |logFC| tem prioridade)
assinatura_symbols <- assinatura_bmi1[
  order(abs(assinatura_bmi1$logFC), decreasing = TRUE), ]
assinatura_symbols <- assinatura_symbols[
  !duplicated(assinatura_symbols$Symbol), ]
rownames(assinatura_symbols) <- assinatura_symbols$Symbol

# Intersecção com os genes disponíveis na mega-matriz
genes_comuns_assinatura <- intersect(rownames(assinatura_symbols),
                                      rownames(mega_matriz_limpa))

# Score UP
genes_up       <- assinatura_symbols$Symbol[assinatura_symbols$logFC > 0]
genes_up_final <- intersect(genes_up, genes_comuns_assinatura)

# Score DOWN (negado para alinhar com direção do BMI1)
genes_down       <- assinatura_symbols$Symbol[assinatura_symbols$logFC < 0]
genes_down_final <- intersect(genes_down, genes_comuns_assinatura)

message(length(genes_up_final), " genes UP | ", length(genes_down_final), " genes DOWN disponíveis")
680 genes UP | 210 genes DOWN disponíveis
# Z-scores
zscore_up   <- scale(colMeans(mega_matriz_limpa[genes_up_final, ],   na.rm = TRUE))[, 1]
zscore_down <- scale(-colMeans(mega_matriz_limpa[genes_down_final, ], na.rm = TRUE))[, 1]
zscore_comb <- scale(
  colMeans(mega_matriz_limpa[genes_up_final, ],   na.rm = TRUE) -
  colMeans(mega_matriz_limpa[genes_down_final, ], na.rm = TRUE)
)[, 1]

# Manter bmi1_zscore para compatibilidade com código anterior
bmi1_zscore <- zscore_up
message("Scores calculados para ", length(bmi1_zscore), " amostras")
Scores calculados para 447 amostras

6. Consolidação dos dados clínicos de sobrevida

Após inspeção manual das colunas de metadados de cada GSE, identificou-se que:

  • 4 datasets são estudos de linhagem celular (GSE68776, GSE12102, GSE45544, GSE39262) e não possuem dados de sobrevida de pacientes.
  • GSE17679 e GSE63155 foram removidos da mega-matriz por compartilharem IDs de amostras com GSE17618/GSE17674 e GSE63157, respectivamente (seção 3.2).
  • GSE63156 partilha os arquivos de expressão com parte do GSE63157, mas possui metadados clínicos independentes — os pacientes clínicos são distintos. Seus scores foram calculados a partir da normalização de lote do GSE63157.

As análises de sobrevida foram realizadas nas 4 coortes com dados clínicos independentes: GSE17618, GSE17674, GSE63156 e GSE63157.

As colunas de tempo e evento variam entre os estudos:

GSE Coluna de tempo Unidade Coluna de evento
GSE17674, GSE17618, GSE17679 efs (months):ch1 meses → convertido para dias (×30,44) status:ch1 (Dead/AWD = 1)
GSE63157, GSE63155 efs time (days):ch1 dias efs event:ch1 (No event = 0)
GSE63156 efs-time:ch1 dias efs (0=no event, 1=event):ch1
extrair_clinico_manual <- function(gse_id) {
  gse_obj <- getGEO(gse_id, destdir = ".", getGPL = FALSE)[[1]]
  meta    <- pData(gse_obj)

  df <- switch(gse_id,

    # GPL570: tempo em meses, evento = Dead ou AWD
    "GSE17674" = ,
    "GSE17618" = ,
    "GSE17679" = data.frame(
      ID    = rownames(meta),
      GSE   = gse_id,
      Time  = as.numeric(meta[["efs (months):ch1"]]) * 30.44,
      Event = ifelse(grepl("Dead|AWD", meta[["status:ch1"]],
                           ignore.case = TRUE), 1, 0)
    ),

    # GPL5175 grupo 1: tempo em dias, evento textual
    "GSE63157" = ,
    "GSE63155" = data.frame(
      ID    = rownames(meta),
      GSE   = gse_id,
      Time  = as.numeric(meta[["efs time (days):ch1"]]),
      Event = ifelse(grepl("No event", meta[["efs event:ch1"]],
                           ignore.case = TRUE), 0, 1)
    ),

    # GPL5175 grupo 2: tempo em dias, evento numérico 0/1
    "GSE63156" = data.frame(
      ID    = rownames(meta),
      GSE   = gse_id,
      Time  = as.numeric(meta[["efs-time:ch1"]]),
      Event = as.integer(meta[["efs (0=no event, 1=event):ch1"]])
    ),

    NULL  # GSE68776, GSE12102, GSE45544, GSE39262: sem dados de pacientes
  )

  if (!is.null(df)) df <- df[!is.na(df$Time) & df$Time > 0, ]
  return(df)
}

dados_clinicos_total <- do.call(
  rbind,
  lapply(c("GSE17674", "GSE17618", "GSE17679",
           "GSE63157", "GSE63155", "GSE63156"), extrair_clinico_manual)
)
Found 1 file(s)
GSE17674_series_matrix.txt.gz
Using locally cached version: ./GSE17674_series_matrix.txt.gz
Found 1 file(s)
GSE17618_series_matrix.txt.gz
Using locally cached version: ./GSE17618_series_matrix.txt.gz
Found 1 file(s)
GSE17679_series_matrix.txt.gz
Using locally cached version: ./GSE17679_series_matrix.txt.gz
Found 1 file(s)
GSE63157_series_matrix.txt.gz
Using locally cached version: ./GSE63157_series_matrix.txt.gz
Found 1 file(s)
GSE63155_series_matrix.txt.gz
Using locally cached version: ./GSE63155_series_matrix.txt.gz
Found 1 file(s)
GSE63156_series_matrix.txt.gz
Using locally cached version: ./GSE63156_series_matrix.txt.gz
dados_clinicos_total |>
  group_by(GSE) |>
  summarise(
    n          = n(),
    eventos    = sum(Event),
    pct_evento = round(100 * eventos / n, 1),
    tempo_med  = round(median(Time)),
    tempo_max  = round(max(Time))
  )
# A tibble: 6 × 6
  GSE          n eventos pct_evento tempo_med tempo_max
  <chr>    <int>   <dbl>      <dbl>     <dbl>     <dbl>
1 GSE17618    44      27       61.4       638      5851
2 GSE17674    44      27       61.4       638      5851
3 GSE17679    88      54       61.4       638      5851
4 GSE63155    46      18       39.1      1644      3987
5 GSE63156    39      16       41        2062      4261
6 GSE63157    46      18       39.1      1644      3987
# Merge com score; deduplicar por ID de paciente
final_df <- merge(
  dados_clinicos_total[!duplicated(dados_clinicos_total$ID), ],
  data.frame(ID = names(bmi1_zscore), Score = bmi1_zscore),
  by = "ID"
) |>
  filter(!is.na(Time), Time > 0, !is.nan(Score)) |>
  group_by(GSE) |>
  mutate(Risk = ifelse(Score > median(Score), "High Score", "Low Score")) |>
  ungroup() |>
  as.data.frame()

message("Dataset final: ", nrow(final_df), " pacientes em ",
        n_distinct(final_df$GSE), " coortes independentes")
Dataset final: 212 pacientes em 4 coortes independentes
table(final_df$GSE)

GSE17618 GSE17674 GSE63156 GSE63157 
      44       44       78       46 

7. Análise de sobrevida

Uma função auxiliar é definida para facilitar a comparação dos três scores:

km_por_score <- function(zscore, label) {
  df <- merge(
    dados_clinicos_total[!duplicated(dados_clinicos_total$ID), ],
    data.frame(ID = names(zscore), Score = zscore),
    by = "ID"
  ) |>
    dplyr::filter(!is.na(Time), Time > 0, !is.nan(Score)) |>
    dplyr::group_by(GSE) |>
    dplyr::mutate(Risk = ifelse(Score > median(Score), "High Score", "Low Score")) |>
    dplyr::ungroup() |>
    as.data.frame()

  ggsurvplot_facet(
    survfit(Surv(Time, Event) ~ Risk, data = df),
    data = df, facet.by = "GSE",
    pval = TRUE, conf.int = TRUE,
    palette = c("#E41A1C", "#377EB8"),
    legend.labs = c("High Score", "Low Score"), legend.title = "",
    title = paste("Score", label),
    xlab = "Tempo (dias)", ylab = "Probabilidade de sobrevida (EFS)"
  )
}

7.1 Comparação dos três scores — Kaplan-Meier por coorte

Score UP (genes ativados pelo BMI1):

km_por_score(zscore_up, "UP (genes ativados pelo BMI1)")

Score DOWN (genes suprimidos pelo BMI1):

km_por_score(zscore_down, "DOWN (genes suprimidos pelo BMI1)")

Score Combinado (UP − DOWN):

km_por_score(zscore_comb, "Combinado (UP − DOWN)")

Comparação dos p-valores por score:

Coorte Score UP Score DOWN Score Combinado
GSE17618 0,30 0,14 0,058
GSE17674 0,30 0,083 0,14
GSE63156 0,70 0,018 0,35
GSE63157 0,94 0,65 0,94

O score DOWN apresenta o melhor desempenho prognóstico. Em GSE63156, a separação é estatisticamente significativa (p = 0,018), com a direção biologicamente esperada: pacientes com score alto (= baixa expressão dos genes suprimidos pelo BMI1, ou seja, alta atividade de BMI1) apresentam pior EFS. O score combinado melhora ligeiramente GSE17618, mas atenua o sinal em GSE63156.

7.2 Verificação dos hazards proporcionais — Score DOWN no GSE63156

Antes de reportar o HR do score DOWN em GSE63156, a premissa de hazards proporcionais foi verificada com o teste de Schoenfeld.

# Construir dataset com score DOWN
final_down <- merge(
  dados_clinicos_total[!duplicated(dados_clinicos_total$ID), ],
  data.frame(ID = names(zscore_down), Score = zscore_down),
  by = "ID"
) |>
  dplyr::filter(!is.na(Time), Time > 0, !is.nan(Score)) |>
  dplyr::group_by(GSE) |>
  dplyr::mutate(Risk = ifelse(Score > median(Score), "High", "Low")) |>
  dplyr::ungroup() |>
  as.data.frame()

df_63156 <- final_down[final_down$GSE == "GSE63156", ]

# Teste de Schoenfeld (score contínuo e categórico)
cox_cont <- coxph(Surv(Time, Event) ~ Score, data = df_63156)
cox_cat  <- coxph(Surv(Time, Event) ~ Risk,  data = df_63156)

cat("=== Modelo contínuo ===\n");  print(cox.zph(cox_cont))
=== Modelo contínuo ===
       chisq df    p
Score  0.659  1 0.42
GLOBAL 0.659  1 0.42
cat("\n=== Modelo categórico ===\n"); print(cox.zph(cox_cat))

=== Modelo categórico ===
       chisq df    p
Risk   0.154  1 0.69
GLOBAL 0.154  1 0.69
plot(cox.zph(cox_cat),
     main = "Resíduos de Schoenfeld — Score DOWN categórico (GSE63156)",
     ylab = "Beta(t)", xlab = "Tempo (dias)")

A premissa de hazards proporcionais não foi violada em nenhum modelo (p = 0,15 para o modelo categórico; p = 0,36 para o contínuo). Os resíduos de Schoenfeld não mostram tendência sistemática ao longo do tempo.

7.3 Forest plot — Cox univariada por coorte (score DOWN)

cox_resultados <- lapply(split(final_down, final_down$GSE), function(df) {
  fit <- coxph(Surv(Time, Event) ~ Score, data = df)
  s   <- summary(fit)
  data.frame(
    GSE     = df$GSE[1],
    N       = nrow(df),
    HR      = s$conf.int[1, "exp(coef)"],
    CI_low  = s$conf.int[1, "lower .95"],
    CI_high = s$conf.int[1, "upper .95"],
    p       = s$coefficients[1, "Pr(>|z|)"]
  )
})

cox_df <- do.call(rbind, cox_resultados)
rownames(cox_df) <- NULL
cox_df$label <- sprintf("HR=%.2f (%.2f–%.2f), p=%.3f",
                         cox_df$HR, cox_df$CI_low, cox_df$CI_high, cox_df$p)
cox_df
       GSE  N        HR    CI_low   CI_high           p
1 GSE17618 44 0.3353480 0.1540770 0.7298836 0.005896447
2 GSE17674 44 0.3410660 0.1434029 0.8111833 0.014960103
3 GSE63156 78 1.1215864 0.8616485 1.4599412 0.393660986
4 GSE63157 46 0.6844936 0.3054951 1.5336792 0.357074616
                         label
1 HR=0.34 (0.15–0.73), p=0.006
2 HR=0.34 (0.14–0.81), p=0.015
3 HR=1.12 (0.86–1.46), p=0.394
4 HR=0.68 (0.31–1.53), p=0.357
ggplot(cox_df, aes(x = HR, y = GSE)) +
  geom_vline(xintercept = 1, linetype = "dashed", color = "grey50") +
  geom_errorbarh(aes(xmin = CI_low, xmax = CI_high), height = 0.2) +
  geom_point(aes(size = N, color = p < 0.05), shape = 15) +
  geom_text(aes(label = label), vjust = -1.2, size = 3.2) +
  scale_color_manual(
    values = c("TRUE" = "#E41A1C", "FALSE" = "grey40"),
    labels = c("TRUE" = "p < 0,05", "FALSE" = "p ≥ 0,05"),
    name   = ""
  ) +
  scale_size_continuous(name = "N pacientes", range = c(3, 6)) +
  scale_x_log10(breaks = c(0.25, 0.5, 1, 2, 4)) +
  labs(
    title    = "Forest plot — Cox univariada por coorte (Score DOWN)",
    subtitle = "Variável contínua: BMI1-Signature Z-Score DOWN (EFS)",
    x        = "Hazard Ratio (escala log)",
    y        = NULL
  ) +
  theme_bw(base_size = 13) +
  theme(legend.position = "bottom")
Warning: `geom_errorbarh()` was deprecated in ggplot2 4.0.0.
ℹ Please use the `orientation` argument of `geom_errorbar()` instead.
`height` was translated to `width`.

Interpretação: Nenhuma coorte apresenta HR significativo no modelo contínuo. A discrepância entre o p-valor do log-rank (p = 0,018 em GSE63156) e o do modelo de Cox contínuo (p > 0,05) reflete a instabilidade das estimativas com apenas 16 eventos: a dicotomização pela mediana cria separação clara de grupos, mas a relação entre o score contínuo e o log-hazard não é bem capturada com tamanho amostral tão reduzido. Os ICs amplos em todas as coortes indicam poder estatístico insuficiente para conclusões definitivas.

Conclusão

O pipeline identificou uma assinatura de 1219 genes associados à atividade de BMI1 no sarcoma de Ewing, dos quais 680 up-regulados e 210 down-regulados estavam disponíveis na mega-coorte de validação.

Três versões do score foram testadas em quatro coortes independentes (173 pacientes):

  • Score UP: sem separação prognóstica em nenhuma coorte (p = 0,30–0,94).
  • Score DOWN: melhor desempenho — separação significativa em GSE63156 (p = 0,018, log-rank) e tendência em GSE17674 (p = 0,083). Premissa de hazards proporcionais não violada (p = 0,15). Porém, nenhuma coorte atinge significância no modelo de Cox contínuo, e GSE63157 (maior coorte, N = 85) permanece sem separação.
  • Score Combinado: desempenho intermediário, sem vantagem clara.

O resultado mais relevante é que os genes suprimidos pelo BMI1 carregam mais sinal prognóstico do que os ativados. A consistência da direção (High Score → pior EFS) em três das quatro coortes é biologicamente coerente, porém as amostras são pequenas demais para conclusões definitivas.

Nota metodológica: Uma versão anterior deste pipeline apresentava p-valores menores, porém parcialmente artefatuais. Datasets como GSE17679 e GSE63155 referenciavam os mesmos pacientes de GSE17618/GSE17674 e GSE63157, respectivamente, e ao serem incluídos como lotes separados no ComBat inflavam artificialmente a separação entre grupos.

Próximos passos sugeridos:

  • Refinar a assinatura DOWN com genes que tenham evidência direta de regulação por BMI1 na literatura (ex.: alvos de RING1B), reduzindo o ruído dos 210 genes incluídos.
  • Realizar meta-análise de efeito randômico combinando os HRs das quatro coortes para obter uma estimativa consolidada.
  • Avaliar os datasets de linhagem celular (GSE68776, GSE12102, GSE45544, GSE39262) para validação funcional da assinatura.