A.1Bloco 8 - Projeto final (opcional, pós-aula): um objeto desconhecido e quebrado
Objetivo: Aplicar os hábitos de diagnóstico dos blocos anteriores a um único objeto que contém quatro problemas independentes. Para cada cenário, execute o código de diagnóstico, escreva sua hipótese como um comentário no bloco abaixo, e então execute a correção para confirmá-la.
Este é o projeto final: o restante do curso ensinou você a identificar falhas específicas, uma de cada vez. Aqui todas chegam no mesmo objeto, sem uma ordem particular, e sem rótulos.
Este bloco vem depois do Encerramento de propósito. A sessão ao vivo termina no Bloco 7; isto é trabalho para casa para quem quiser mais prática antes da próxima sessão, não algo para ser feito às pressas na sala. Execute-o no seu próprio tempo, idealmente um ou dois dias depois do curso, uma vez que os hábitos de diagnóstico dos Blocos 1-6 tenham tido a chance de se consolidar.
O instrutor disponibilizará o sobj_broken.rds separadamente. Se o arquivo não estiver presente, este bloco é pulado (have_broken = FALSE) e o resto do script continua rodando normalmente.
Tenta primeiro o objeto injetado (necessário para o Cenário 4), depois o base. Se nenhum dos dois existir, o Bloco 8 é pulado de forma controlada (have_broken = FALSE) para que o restante do relatório ainda seja renderizado.
Código
broken_candidates <-c("checkpoints/sobj_broken_errors.rds", "checkpoints/sobj_broken.rds")broken_path <- broken_candidates[file.exists(broken_candidates)][1]have_broken <-!is.na(broken_path)if (have_broken) { sobj_broken <-readRDS(broken_path)cat("Loaded broken object from:", broken_path, "\n")print(sobj_broken)head(sobj_broken@meta.data, 3)} else {cat("Broken object not found in data/. Block 8 will be skipped.\n")cat("To enable Block 8, place one of these files (paths relative to this .R script):\n")cat(" ", paste(broken_candidates, collapse =" or "), "\n")cat("Scenario 4 (ADT batch / isotype) requires the *_errors.rds from\n")cat("inject_course_errors.R.\n")}
Loaded broken object from: checkpoints/sobj_broken_errors.rds
An object of class Seurat
526 features across 3000 samples within 2 assays
Active assay: ADT (26 features, 0 variable features)
1 layer present: counts
1 other assay present: RNA
2 dimensional reductions calculated: pca, umap
Contexto: Você recebeu este objeto de um colaborador. Você executa o fluxo de trabalho padrão de pré-processamento de RNA. Tudo é executado sem erros, mas os resultados estão completamente errados: FindVariableFeatures retorna menos features do que o esperado, o PCA explica quase toda a variância em PC1, e o UMAP é uma única mancha (blob).
Código
if (have_broken) {cat("Default assay :", DefaultAssay(sobj_broken), "\n")cat("Features active assay:", nrow(sobj_broken), "\n")cat("All assays :", paste(SafeAssays(sobj_broken), collapse =", "), "\n")}
Default assay : ADT
Features active assay: 26
All assays : RNA, ADT
Código
if (have_broken) {# What does FindVariableFeatures return on this object? test_hvg <-FindVariableFeatures(sobj_broken, nfeatures =2000, verbose =FALSE)cat("Variable features found:", length(VariableFeatures(test_hvg)), "\n")cat("Feature names:\n")print(head(VariableFeatures(test_hvg), 15))}
if (have_broken) {# Compare RNA and ADT feature countscat("Features in RNA assay:", nrow(sobj_broken[["RNA"]]), "\n")cat("Features in ADT assay:", nrow(sobj_broken[["ADT"]]), "\n")}
Features in RNA assay: 500
Features in ADT assay: 26
📝 Escreva sua hipótese como um comentário abaixo, depois execute a correção para confirmar. Qual é o DefaultAssay? Por que o FindVariableFeatures retorna tão poucas features? O que aconteceria se você executasse RunPCA e RunUMAP neste objeto sem corrigi-lo? Como você detectaria esse problema em um objeto Seurat publicado que você baixou?
Código
if (have_broken) {cat("Before fix:", DefaultAssay(sobj_broken), "\n")# The DefaultAssay is set to "ADT" (24 proteins).# All RNA functions are running silently on 24 protein features instead# of 500 RNA genes. FindVariableFeatures returns 24 features because that# is all that exists in the active assay.DefaultAssay(sobj_broken) <-"RNA"cat("After fix :", DefaultAssay(sobj_broken), "\n")cat("Features :", nrow(sobj_broken), "\n") test_fixed <-FindVariableFeatures(sobj_broken, nfeatures =2000, verbose =FALSE)cat("Variable features now:", length(VariableFeatures(test_fixed)), "\n")}
Before fix: ADT
After fix : RNA
Features : 500
Variable features now: 500
Lições aprendidas:
DefaultAssay() é a primeira linha a ser executada em qualquer objeto recebido.
Este erro é completamente silencioso. Sem aviso. Sem erro. Apenas resultados errados.
FindVariableFeatures, ScaleData, RunPCA, FindMarkers operam todos sobre o assay ativo. Um PCA executado em 24 features de ADT não é um PCA transcriptômico.
Sempre verifique DefaultAssay() após qualquer troca de assay para confirmar que ele foi redefinido.
A.1.2 Cenário 2 - Inconsistências nos metadados
Contexto: Este objeto foi montado a partir de amostras processadas em múltiplos locais. Agrupar por condition e donor_id produz resultados inesperados. Algumas amostras estão faltando nos gráficos e certos doadores aparecem duplicados.
Código
if (have_broken) {cat("Metadata columns:\n")print(colnames(sobj_broken@meta.data))cat("\nNAs per column:\n")print(colSums(is.na(sobj_broken@meta.data)))}
if (have_broken) {# What does a DimPlot by condition look like?DimPlot(sobj_broken, group.by ="condition") +ggtitle("Condition plot: how many groups appear?")}
📝 Escreva sua hipótese como um comentário abaixo, depois execute a correção para confirmar. Quantos problemas distintos você encontrou? Qual seria a consequência biológica se cada um deles não fosse corrigido? Por exemplo: se os rótulos de condição são inconsistentes, o que acontece com uma comparação entre Saudável e COVID-19?
Código
if (have_broken) { sobj_meta <- sobj_broken# PROBLEM 1: donor_id has three different formats for the same donors# e.g. "Donor01", "donor1", "DONOR01" all mean the same donorcat("=== FIX 1: donor_id ===\n") sobj_meta$donor_id <-toupper(gsub("[^A-Z0-9a-z]", "", sobj_meta$donor_id))print(table(sobj_meta$donor_id))# PROBLEM 2: condition has 4 variants of 2 values# "COVID19", "covid19", "Covid-19", "HC", NAcat("\n=== FIX 2: condition ===\n") sobj_meta$condition_clean <- dplyr::case_when(grepl("covid", sobj_meta$condition, ignore.case =TRUE) ~"COVID-19",grepl("healthy|HC|control", sobj_meta$condition,ignore.case =TRUE) ~"Healthy",TRUE~NA_character_ )print(table(sobj_meta$condition_clean, useNA ="always"))# PROBLEM 3: 200 cells have NA in cell_typecat("\n=== FIX 3: cell_type NAs ===\n")cat("NAs before:", sum(is.na(sobj_meta$cell_type)), "\n") sobj_meta$cell_type[is.na(sobj_meta$cell_type)] <-"Unknown"cat("NAs after :", sum(is.na(sobj_meta$cell_type)), "\n")}
table(col, useNA = "always") e str(meta.data) antes de cada análise.
Dados de múltiplos locais quase sempre têm inconsistências de formatação.
NA em condition: essa célula é excluída de qualquer comparação entre Saudável e COVID-19. Com 30 NAs, você está descartando silenciosamente 1% das células de todas as comparações de grupo.
Diferenciação entre maiúsculas e minúsculas importa: "COVID19" == "covid19" é FALSE em R.
A.1.3 Cenário 3 - Falha de normalização e efeito de lote
Contexto: O UMAP mostra uma separação clara por doador em vez de por tipo celular. Os dados de ADT também parecem distorcidos. Identifique ambos os problemas e corrija-os.
Código
if (have_broken) {DimPlot(sobj_broken, group.by ="orig.ident") +ggtitle("Donors separate in UMAP: batch effect or biology?")}
Código
if (have_broken) {DefaultAssay(sobj_broken) <-"ADT" adt_data <-LayerData(sobj_broken, layer ="data")cat("Row means (per protein); near 0 means margin=1 was used (wrong):\n")print(round(rowMeans(adt_data), 4))cat("\nColumn means (per cell, first 10); near 0 means margin=2 (correct):\n")print(round(colMeans(adt_data)[1:10], 4))DefaultAssay(sobj_broken) <-"RNA"}
Warning: Layer 'data' is empty
Row means (per protein); near 0 means margin=1 was used (wrong):
numeric(0)
Column means (per cell, first 10); near 0 means margin=2 (correct):
[1] NA NA NA NA NA NA NA NA NA NA
Código
if (have_broken) { rna_data <-LayerData(sobj_broken, assay ="RNA", layer ="data")cat("Any negative values in RNA data layer?",any(rna_data <0), "\n")cat("(TRUE would indicate SCTransform residuals stored as 'data')\n")cat("\nRange of non-zero values in RNA data:\n")print(summary(rna_data@x))}
Any negative values in RNA data layer? FALSE
(TRUE would indicate SCTransform residuals stored as 'data')
Range of non-zero values in RNA data:
Min. 1st Qu. Median Mean 3rd Qu. Max.
2.553 3.392 4.530 4.620 5.715 8.034
📝 Escreva sua hipótese como um comentário abaixo, depois execute a correção para confirmar. Como você determinou qual margin foi usado? Qual é a consequência do efeito de lote para qualquer comparação em nível de condição? Se você publicasse o UMAP original como uma figura, qual afirmação científica seria invalidada?
RNA re-normalized from counts.
ADT re-normalized (CLR, margin=2).
Warning: The default method for RunUMAP has changed from calling Python UMAP via reticulate to the R-native UWOT using the cosine metric
To use Python UMAP via reticulate, set umap.method to 'umap-learn' and metric to 'correlation'
This message will be shown once per session
Médias de linha ~0 nos dados de ADT é a assinatura diagnóstica do margin=1. Errado.
Médias de coluna ~0 nos dados de ADT é a assinatura do margin=2. Correto.
A camada de contagens (counts) é a verdade fundamental (ground truth). Renormalize a partir dela; nunca a sobrescreva.
Separação por doador no UMAP = efeito de lote. Execute DimPlot(group.by="orig.ident") antes de interpretar qualquer UMAP biologicamente.
O Harmony corrige efeitos de lote em nível de doador enquanto preserva a variação biológica. Requer pelo menos 3 doadores por grupo para ser confiável.
A.1.4 Cenário 4 | Lote de coloração de ADT e fundo de isotipo (CITE-seq, 25 min)
Contexto: Os dados de proteína parecem tecnicamente corretos: normalizam, plotam, o gating funciona. Mas o PCA de ADT separa as células por lote de processamento, não por tipo celular, e um doador mostra sinal inflado em todas as proteínas. Dois problemas distintos de CITE-seq estão presentes: um efeito de lote de coloração e um alto fundo inespecífico. Nenhum dos dois é um problema de RNA e nenhum gera um erro.
Código
if (have_broken) {if (!"adt_batch"%in%colnames(sobj_broken@meta.data)) {cat("This broken object has no 'adt_batch' column.\n")cat("Scenario 4 needs the injected object (data/sobj_broken_errors.rds\n")cat("from inject_course_errors.R). Skipping Scenario 4 diagnostics.\n") } else {DefaultAssay(sobj_broken) <-"ADT" adt_feats <-rownames(sobj_broken[["ADT"]]) sobj_broken <-ScaleData(sobj_broken, features = adt_feats, verbose =FALSE) sobj_broken <-RunPCA(sobj_broken, features = adt_feats,npcs =15, reduction.name ="pca.adt.broken",reduction.key ="pcaADTb_", verbose =FALSE)print(DimPlot(sobj_broken, reduction ="pca.adt.broken", group.by ="adt_batch") +ggtitle("ADT PCA colored by staining batch: should NOT separate") )DefaultAssay(sobj_broken) <-"RNA" }}
Warning: No layers found matching search pattern provided
Error in `ScaleData()`:
! No layer matching pattern 'data' found. Please run NormalizeData and retry
Código
if (have_broken) {# Isotype antibodies bind nothing specific. High isotype = high background# (sticky/dying cells, over-staining). Per-cell total ADT correlated with# isotype signal is the fingerprint.DefaultAssay(sobj_broken) <-"ADT" adt_counts <-LayerData(sobj_broken, layer ="counts") isotypes <-grep("[Ii]sotype|IgG", rownames(adt_counts), value =TRUE)cat("Isotype control channels found:", paste(isotypes, collapse =", "), "\n")if (length(isotypes) >0) { iso_total <- Matrix::colSums(adt_counts[isotypes, , drop =FALSE]) adt_total <- Matrix::colSums(adt_counts)cat("Spearman(total ADT, isotype signal):",round(cor(adt_total, iso_total, method ="spearman"), 3), "\n")# donor_id is inconsistent in this object; use a clean donor number dn <- sobj_broken$donor_numif (is.null(dn)) dn <-suppressWarnings(as.integer(gsub("\\D", "",as.character(sobj_broken$donor_id))))cat("Median isotype signal by donor number:\n")print(round(tapply(iso_total, dn, median), 2)) }DefaultAssay(sobj_broken) <-"RNA"}
Isotype control channels found: IgG1-isotype, IgG2a-isotype
Spearman(total ADT, isotype signal): 0.215
Median isotype signal by donor number:
1 2 3 4 5 6 7
16.5 0.0 0.0 0.0 0.0 0.0 0.0
📝 Escreva sua hipótese como um comentário abaixo, depois execute a correção para confirmar. Qual doador tem o maior fundo? O CLR sozinho removeria um efeito de lote de coloração? Se você fizesse o gating de células CD4+ com um único limiar fixo através de ambos os lotes, o que aconteceria com as contagens por lote?
Código
if (have_broken) {if (!"adt_batch"%in%colnames(sobj_broken@meta.data)) {cat("No 'adt_batch' column; Scenario 4 solution requires the injected object.\n") } else {# STEP 1: Flag and optionally remove high-background cells using isotypes.DefaultAssay(sobj_broken) <-"ADT" adt_counts <-LayerData(sobj_broken, layer ="counts") isotypes <-grep("[Ii]sotype|IgG", rownames(adt_counts), value =TRUE) sobj_clean <- sobj_brokenif (length(isotypes) >0) { iso_total <- Matrix::colSums(adt_counts[isotypes, , drop =FALSE]) cut_hi <-quantile(iso_total, 0.95) keep <- iso_total <= cut_hicat("High-background cells removed (top 5% isotype):", sum(!keep), "\n") sobj_clean <-subset(sobj_clean, cells =colnames(sobj_clean)[keep]) }# STEP 2: Re-normalize ADT (CLR margin=2) from counts. sobj_clean <-NormalizeData(sobj_clean, normalization.method ="CLR",margin =2, verbose =FALSE)# STEP 3: Correct the ADT staining batch with Harmony in protein space. adt_feats <-rownames(sobj_clean[["ADT"]]) sobj_clean <-ScaleData(sobj_clean, features = adt_feats, verbose =FALSE) sobj_clean <-RunPCA(sobj_clean, features = adt_feats, npcs =15,reduction.name ="pca.adt", reduction.key ="pcaADT_",verbose =FALSE) sobj_clean <-RunHarmony(sobj_clean, group.by.vars ="adt_batch",reduction ="pca.adt",reduction.save ="harmony.adt", verbose =FALSE)cat("ADT batch corrected in protein space (harmony.adt).\n")DefaultAssay(sobj_clean) <-"RNA" }}
Warning in svd.function(A = t(x = object), nv = npcs, ...): You're computing
too large a percentage of total singular values, use a standard svd instead.
ADT batch corrected in protein space (harmony.adt).
Lições aprendidas:
Uma camada de ADT que parece limpa ainda pode estar dominada por estrutura técnica. Sempre execute um PCA de ADT e colora-o por lote antes de confiar no espaço de proteínas.
Os controles de isotipo são a verdade fundamental (ground truth) para o fundo. Se o ADT total acompanha o sinal de isotipo, as células com valores altos são fundo, não biologia.
O CLR por célula não remove um efeito de coloração entre lotes. É necessária uma correção de lote (Harmony no PCA de ADT), separada do lote de RNA.
Um único limiar de gating através dos lotes classifica erroneamente as células. Faça o gating por lote ou corrija o lote primeiro.