Objetivo: Trazer a camada de proteína para a análise. O fluxo de trabalho de RNA até o Bloco 5 ignorou completamente o ADT. O valor agregado do CITE-seq é a capacidade de resolver o dropout, validar a anotação e ponderar as modalidades por célula.
Ordem das operações: inspecionar a camada de ADT e contrastá-la com o RNA, normalizar, fazer o controle de qualidade (QC) do painel, contrastar RNA vs ADT para marcadores correspondentes, fazer o gating, verificar canais trocados, construir o PCA de ADT, e então integrar com WNN.
Carregar o arquivo no R:
Código
library(here) # install.packages("here")
here() starts at /private/var/folders/zz/cdcfnwts145c0xjqs_c0y6mm0000gn/T/RtmppqY2dt/file177bf72942b49
Código
sobj_filt <-readRDS(here("checkpoints", "outputs", "sobj_preprocessed.rds"))# top markerstop_markers <-readRDS(here("checkpoints", "outputs", "top_markers.rds"))
Uma proteína de ADT com mediana ~0 mas máximo nas centenas altas é uma bimodalidade típica (células de fundo vs células coradas). Uma proteína com média abaixo de 1 em todas as células é outra coisa. Qual é qual na tabela acima?
DicaRESPOSTA
Bimodal saudável: mediana próxima de 0, média algumas unidades mais alta, máximo nas centenas. A proteína não está corada na maioria das células e é fortemente positiva em seu subconjunto alvo. Padrão de anticorpo morto: média abaixo de 1, máximo também baixo (abaixo de 10), pct_zero muito alto. Nada sobe acima do fundo porque o anticorpo não é funcional. No conjunto de dados injetado, CD56 mostra o padrão morto; CD3, CD4, CD8a, CD14, CD19 mostram o padrão bimodal. Examine a tabela ordenada por média para detectar a anomalia.
5.1.1 Etapa 6.0b - Esparsidade de RNA vs esparsidade de ADT
O Bloco 1 (Seção 4.4) mediu a esparsidade (sparsity) do RNA isoladamente. Agora que a camada de ADT também foi inspecionada, a comparação é o ponto principal: as mesmas células, dois assays, taxas de zero muito diferentes.
Por que a esparsidade de ADT é tão menor do que a de RNA? O que cada zero significa biologicamente nas duas modalidades?
DicaRESPOSTA
A esparsidade de RNA neste subconjunto de 500+9 genes ainda é substancial; a esparsidade de ADT tipicamente fica abaixo de 5%. Uma única molécula de mRNA precisa ser capturada por um primer oligo-dT, transcrita reversamente, amplificada e sequenciada. As etapas de captura e RT falham a uma taxa substancial por molécula, produzindo um zero onde o gene era expresso. Isso é dropout. Um zero de ADT vem da coloração com anticorpos: as células são incubadas com centenas a milhares de moléculas de anticorpo por proteína, e a profundidade de sequenciamento para tags de proteína também é maior por célula. A única forma de ler zero é a proteína estar ausente ou abaixo do fundo de detecção. Zeros de RNA misturam ausência verdadeira com falha de captura; zeros de ADT são majoritariamente ausência verdadeira.
Neste ponto esperamos: apenas counts. Sem data, sem scale.data. A normalização é a próxima etapa.
DicaENIGMA 6.1/A (ADT)
Sem normalizar ainda, qual proteína tem a maior variância bruta entre células? É um marcador de linhagem (CD3, CD4, CD8a, CD14, CD19, CD56) ou um marcador de ativação (HLADR, CD69, CD25, PD1)?
Os marcadores de linhagem geralmente vencem: CD4, CD8a, CD14, CD19, CD3 têm a maior variância bruta porque vão de baixo fundo em células não-alvo a muito alto em células-alvo. Marcadores de ativação como CD69, HLADR, CD25 têm menor variância porque são expressos em níveis moderados em muitas células. Se um marcador de ativação lidera a lista, o conjunto de dados pode estar enriquecido em células ativadas, ou um anticorpo está se comportando de forma incomum.
Quantas células são positivas para AMBOS CD4 e CD8a nas contagens brutas (> 5 contagens cada)? Em PBMC saudáveis, isso deveria ficar próximo de zero; qualquer coisa substancial é um doublet ou um sinal de contaminação.
O duplo-positivo CD4+/CD8a+ em ADT bruto deveria ficar próximo de zero em PBMC. Mais de 1-2% das células: suspeita de doublets que o scDblFinder deixou passar, ou de spillover de anticorpos. Investigue via nCount_RNA e ADT total para as células suspeitas.
A contagem de duplo-positivos corresponde ao que você espera biologicamente, ou indica doublets que sobreviveram ao scDblFinder? O que você faria a respeito antes de anotar os subtipos de células T?
DicaRESPOSTA
Esperado próximo de zero (abaixo de 1% das células). Células verdadeiramente duplo-positivas CD4+/CD8+ são raras em PBMC (algumas células T MAIT e gamma-delta). Mais de 1-2% e as células provavelmente são doublets que o scDblFinder deixou passar (doublets homotípicos de células T). Verifique cruzadamente com a classe de doublet do scDblFinder; se muitos duplo-positivos não estiverem marcados como doublets, aumente o limiar do scDblFinder ou remova os duplo-positivos manualmente antes de anotar os subtipos de células T.
Normalização CLR (Centered Log-Ratio, log-razão centralizada). O argumento margin define a direção: + margin = 1: normaliza cada proteína em todas as células (médias de linha ~0) + margin = 2: normaliza cada célula em todas as proteínas (médias de coluna ~0)
margin = 2 é o correto para CITE-seq. Ele remove a variação por célula na captura total de anticorpos, análogo à normalização por tamanho de biblioteca em RNA. margin = 1 roda sem erro, mas destrói o sinal biológico. Você vai diagnosticar um objeto real normalizado da forma errada no Bloco 8, Cenário 3.
Código
DefaultAssay(sobj_filt) <-"ADT"sobj_filt <-NormalizeData( sobj_filt,normalization.method ="CLR",margin =2# CORRECT: normalizes each cell across proteins)
Normalizing layer: counts
Normalizing across cells
Código
adt_m2 <-LayerData(sobj_filt, layer ="data")cat("Column means after margin=2 (per cell), expected ~0:\n")
Column means after margin=2 (per cell), expected ~0:
Por que a média de linha é um diagnóstico útil? Como as médias de linha ficariam se margin tivesse sido definido como 1?
DicaRESPOSTA
Depois do CLR com margin=2 (por célula entre proteínas), as médias de coluna ficam ~0 por construção (os valores de proteína de cada célula foram centralizados). As médias de linha (por proteína entre células) carregam o sinal biológico de quais proteínas são abundantes e NÃO deveriam ficar próximas de zero. Elas refletem diferenças de abundância de proteína em todo o conjunto de dados. Se margin=1 tivesse sido usado em vez disso (por proteína entre células), as médias de linha ficariam próximas de zero. Essa é a impressão digital do margin errado. O Cenário 3 do Bloco 7 contém exatamente essa falha.
GRÁFICO / SAÍDA: Saída de console; médias de linha != 0, médias de coluna ~ 0.
5.1.4 Etapa 6.3 - QC do painel de ADT: detectar anticorpos mortos / falhos
Uma conjugação falha ou um anticorpo degradado produz um “canal morto”: a proteína lê perto de zero em todas as células, com quase nenhuma variância. Não gera erro. Se você fizer gating ou anotar sobre um canal morto, perde silenciosamente uma população inteira.
ADT panel QC (sorted by mean; suspicious = bottom rows):
Código
print(head(panel_qc, 6), row.names =FALSE)
protein median mean pct_zero max
PD1 1 0.79 46.0 5
CD25 1 0.80 45.8 5
CD45RO 1 0.80 44.0 5
LAG3 1 0.80 43.7 6
CD69 1 0.81 44.1 6
CD197 1 0.81 44.1 5
Código
DefaultAssay(sobj_filt) <-"RNA"
DicaPERGUNTA 6.3
Uma proteína tem contagens próximas de zero em essencialmente todas as células, enquanto sua contraparte de RNA é claramente expressa em um cluster definido. Qual proteína, e qual tipo celular ela marca?
DicaRESPOSTA
No conjunto de dados injetado: CD56 (codificado por NCAM1). CD56 marca as células NK. Com CD56 ADT morto, qualquer anotação de células NK baseada apenas em ADT falha. O bloco de diagnóstico compara CD56 ADT (quase zero) com NCAM1 RNA (sinal claro no cluster de NK) e expõe a inconsistência. Hábito: sempre verifique cruzadamente as proteínas de ADT com menor média contra suas contrapartes de RNA.
Warning: Could not find CD56 in the default search locations, found in 'ADT'
assay instead
GRÁFICO / SAÍDA: Painel FeaturePlot: CD56 ADT (plano) ao lado de NCAM1 RNA (positivo em um cluster)
Compare a proteína suspeita (ADT) com seu gene de RNA lado a lado.
Edite suspect_adt / suspect_rna para a proteína sinalizada acima.
Código
suspect_adt <-"CD56"# protein flagged as near-zero in panel QCsuspect_rna <-"NCAM1"# its RNA counterpart (NK marker)
Decida o título a partir dos números reais de panel_qc em vez de afirmar uma afirmação fixa. No objeto limpo, esta proteína está saudável; no objeto injetado, está morta. O título deve dizer o que for verdadeiro para o objeto que está realmente carregado.
Código
suspect_row <- panel_qc[panel_qc$protein == suspect_adt, ]is_dead <-nrow(suspect_row) ==1&& suspect_row$pct_zero >90&& suspect_row$mean <1adt_title <-if (is_dead) {paste0(suspect_adt, " ADT\n(flat: likely failed antibody)")} else {paste0(suspect_adt, " ADT\n(signal present: not a dead channel here)")}DefaultAssay(sobj_filt) <-"ADT"p_dead <-FeaturePlot(sobj_filt, suspect_adt, min.cutoff ="q05") +ggtitle(adt_title)DefaultAssay(sobj_filt) <-"RNA"p_alive <-FeaturePlot(sobj_filt, suspect_rna, min.cutoff ="q05") +ggtitle(paste0(suspect_rna, " RNA\n(clear NK signal: protein should exist)"))p_dead | p_alive
Código
DefaultAssay(sobj_filt) <-"RNA"
5.1.5 Etapa 6.4 - RNA vs ADT para o mesmo marcador (dropout)
Comparação direta. O mRNA de CD4 lê como zero em muitas células T CD4+ (dropout), enquanto a proteína CD4 do ADT é bimodal nas mesmas células.
Código
DefaultAssay(sobj_filt) <-"RNA"p_rna <-FeaturePlot(sobj_filt, "CD4", min.cutoff ="q05") +ggtitle("CD4 via RNA\n(dropout: many CD4+ cells read as zero)") +scale_color_gradient(low ="lightgrey", high ="#991b1b")
Scale for colour is already present.
Adding another scale for colour, which will replace the existing scale.
Código
DefaultAssay(sobj_filt) <-"ADT"p_adt <-FeaturePlot(sobj_filt, "CD4", min.cutoff ="q05") +ggtitle("CD4 via ADT protein\n(bimodal, reliable)") +scale_color_gradient(low ="lightgrey", high ="#0d7377")
Scale for colour is already present.
Adding another scale for colour, which will replace the existing scale.
Código
p_rna | p_adt
Código
DefaultAssay(sobj_filt) <-"RNA"
5.1.6 Etapa 6.5 - Correlação RNA-proteína entre todos os marcadores correspondentes
Para cada proteína de ADT com uma contraparte de RNA, calcule a correlação de Spearman entre todas as células. Correlação baixa = dropout alto em RNA.
5.1.7 Etapa 6.6 - Quantificar o dropout: células ADT-positivas mas RNA-zero
Para cada marcador, encontre as células que são claramente positivas para a proteína (ADT > limiar) mas têm zero contagens de RNA. Essas são as células que seriam mal-anotadas por uma análise baseada apenas em RNA.
Código
DefaultAssay(sobj_filt) <-"ADT"adt_data <-LayerData(sobj_filt, layer ="data")DefaultAssay(sobj_filt) <-"RNA"rna_counts <-LayerData(sobj_filt, layer ="counts")cat("Dropout analysis: cells positive by ADT but zero by RNA\n")
Dropout analysis: cells positive by ADT but zero by RNA
cat(" Marker panel of N independent markers (each with 80% dropout):\n")
Marker panel of N independent markers (each with 80% dropout):
Código
cat(" P(all N drop)= 0.8^N\n")
P(all N drop)= 0.8^N
Código
for (n inc(1, 2, 3, 5)) {cat(sprintf(" N = %d markers -> %.1f%% of cells still misannotated\n", n, 0.8^n *100))}
N = 1 markers -> 80.0% of cells still misannotated
N = 2 markers -> 64.0% of cells still misannotated
N = 3 markers -> 51.2% of cells still misannotated
N = 5 markers -> 32.8% of cells still misannotated
Código
cat("Rule of thumb: with 80% per-marker dropout, you need >= 3 independent\n")
Rule of thumb: with 80% per-marker dropout, you need >= 3 independent
Código
cat("markers in the panel to reduce misannotation below 50%.\n")
markers in the panel to reduce misannotation below 50%.
DicaRESPOSTA
Decisões de marcador único: até 80% mal-anotadas para células cujo RNA sofreu dropout. O script imprime a aritmética para 1, 2, 3, 5 marcadores independentes (cada um com 80% de dropout): regra P(todos falham) = 0.8^N. Então 64%, 51%, 33% com 2, 3, 5 marcadores respectivamente. A anotação nunca deveria depender de um único marcador para um gene com alto dropout. O ADT resgata porque o dropout de proteína é próximo de zero.
DEMONSTRAÇÃO AO VIVO:
Código
for (n in1:5) cat(sprintf("N=%d -> %.1f%% still miss\n", n, 0.8^n*100))
N=1 -> 80.0% still miss
N=2 -> 64.0% still miss
N=3 -> 51.2% still miss
N=4 -> 41.0% still miss
N=5 -> 32.8% still miss
GRÁFICO / SAÍDA: Saída de console
5.1.8 Etapa 6.7 - Gating digital
O gating digital replica os gráficos de dispersão biaxiais da citometria de fluxo usando dados de ADT. Ele permite a classificação computacional de células com a mesma lógica que os imunologistas usam na bancada.
Código
DefaultAssay(sobj_filt) <-"ADT"color_by <-if ("singler_label_top"%in%colnames(sobj_filt@meta.data)) {"singler_label_top"} else {"cell_type"}gate_data <-FetchData(sobj_filt, vars =c("CD4", "CD8a", color_by))names(gate_data)[3] <-"label"ggplot(gate_data, aes(CD4, CD8a, color = label)) +geom_point(alpha =0.4, size =0.8) +geom_density_2d(color ="grey50", linewidth =0.3) +geom_vline(xintercept =1.5, linetype ="dashed", color ="#991b1b") +geom_hline(yintercept =1.5, linetype ="dashed", color ="#991b1b") +annotate("text", x =3.5, y =0.4, label ="CD4+ T cells", size =3.5) +annotate("text", x =0.4, y =3.5, label ="CD8+ T cells", size =3.5) +labs(title ="Digital gating: CD4 vs CD8a (ADT protein)",subtitle ="Only possible with ADT; RNA dropout makes this scatter uninformative",x ="CD4 (CLR normalized)", y ="CD8a (CLR normalized)",color =NULL) +theme_classic(base_size =13) +theme(legend.text =element_text(size =7))
Código
DefaultAssay(sobj_filt) <-"RNA"
5.1.9 Etapa 6.7b - Falha silenciosa: DefaultAssay esquecido entre chamadas
Uma chamada de função que “funciona” no assay errado produz um erro silencioso.
FeaturePlot("CD4") com DefaultAssay = "RNA" plota o gene CD4 de RNA.
FeaturePlot("CD4") com DefaultAssay = "ADT" plota a proteína CD4.
O gráfico é RENDERIZADO em ambos os casos, e os dois gráficos podem parecer enganosamente semelhantes: min.cutoff = "q05" reescala o gradiente de cor de cada painel para seu próprio intervalo de valores, então um sinal de RNA esparso e um sinal de ADT denso acabam ambos esticados por uma escala de cor de aparência similar. A falha é silenciosa precisamente porque nada no gráfico renderizado avisa qual assay o produziu.
Quantifique a diferença que o gráfico por si só oculta: qual fração de células lê exatamente zero em cada modalidade para este marcador.
Código
DefaultAssay(sobj_filt) <-"RNA"cd4_rna_zero_pct <-mean(LayerData(sobj_filt, layer ="counts")["CD4", ] ==0) *100DefaultAssay(sobj_filt) <-"ADT"cd4_adt_zero_pct <-mean(LayerData(sobj_filt, layer ="counts")["CD4", ] ==0) *100DefaultAssay(sobj_filt) <-"RNA"cat(sprintf("CD4 zero rate: %.1f%% of cells in RNA, %.1f%% of cells in ADT\n", cd4_rna_zero_pct, cd4_adt_zero_pct))
CD4 zero rate: 67.3% of cells in RNA, 35.6% of cells in ADT
Código
cat("The plots below can look similar at a glance; the zero rates above are\n")
The plots below can look similar at a glance; the zero rates above are
Código
cat("the actual difference the two assays are reporting for the same gene.\n")
the actual difference the two assays are reporting for the same gene.
Demonstre desenhando a mesma chamada sob ambos os assays. Os títulos são mantidos curtos e o tamanho da fonte é restrito, já que os dois gráficos são renderizados lado a lado com metade da largura cada.
Ambos os gráficos foram renderizados, e à primeira vista podem parecer quase iguais. Os números de taxa de zero impressos acima contam uma história diferente. Se você salvasse um desses gráficos como uma figura para um artigo sem verificar DefaultAssay primeiro, qual você teria, e um leitor conseguiria perceber apenas pela figura que ‘CD4’ significava algo diferente em cada painel?
DicaRESPOSTA
Você teria qualquer assay que estivesse ativo no momento da chamada do gráfico, e um leitor não conseguiria distinguir qual apenas pela figura: o título do gráfico diz apenas ‘CD4’, os eixos são coordenadas UMAP idênticas, e min.cutoff='q05' reescala o gradiente de cor próprio de cada painel de forma independente, então um sinal de RNA esparso e um sinal de ADT mais denso acabam esticados por escalas de aparência visualmente similar. É exatamente por isso que os números de taxa de zero importam: eles mostram a diferença real em qual fração de células lê como positiva em cada modalidade, uma diferença que o gradiente de cor reescalado oculta. A figura publicada poderia mostrar silenciosamente CD4 RNA quando CD4 proteína era pretendida, ou vice-versa, e nada na própria imagem sinalizaria a discrepância. Hábito: defina DefaultAssay explicitamente no topo de qualquer bloco de plotagem, E/OU passe o assay dentro da chamada; melhor ainda, edite o título do gráfico para incluir o nome do assay e considere relatar a taxa de zero junto com a figura para que a diferença de modalidade fique documentada, não apenas visível para quem já sabe procurar por isso.
DEMONSTRAÇÃO AO VIVO:
Código
DefaultAssay(sobj_filt) <-"RNA"mean(LayerData(sobj_filt, layer="counts")["CD4", ] ==0) *100# RNA zero rate
[1] 67.34007
Código
DefaultAssay(sobj_filt) <-"ADT"mean(LayerData(sobj_filt, layer="counts")["CD4", ] ==0) *100# ADT zero rate
[1] 35.61541
Código
DefaultAssay(sobj_filt) <-"RNA"FeaturePlot(sobj_filt, "CD4") +ggtitle("CD4 protein (ADT)")
GRÁFICO / SAÍDA: Dois FeaturePlots lado a lado, um por assay, com títulos distintos; os números de taxa de zero explicam o que os gráficos sozinhos não mostram.
5.1.10 Etapa 6.7c - Encontre o erro
Leia o código abaixo. Ele parece correto. O que está errado?
For a (much!) faster implementation of the Wilcoxon Rank Sum Test,
(default method for FindMarkers) please install the presto package
--------------------------------------------
install.packages('devtools')
devtools::install_github('immunogenomics/presto')
--------------------------------------------
After installation of presto, Seurat will automatically use the more
efficient implementation (no further action necessary).
This message will be shown once per session
Sua resposta:
REVELAR: a chamada pede marcadores de células T CD4, mas DefaultAssay é RNA. Ela retorna marcadores de RNA, não marcadores de ADT. A saída está correta para RNA, mas não responde à pergunta “quais proteínas distinguem as células T CD4”. Ou defina DefaultAssay = "ADT" primeiro, ou passe assay = "ADT" dentro da chamada FindMarkers. O min.pct = 0.25 também está errado para ADT (projetado para RNA esparso); para ADT use um valor como 0.5 ou 0.0 com logfc.threshold.
5.1.11 Etapa 6.8 - Quando o gating não faz sentido biológico: um canal trocado
Um canal trocado ou mal rotulado roda perfeitamente. O CLR o normaliza, o FeaturePlot o desenha, o gating o agrupa. Nada dá erro. O único sinal é que a biologia é impossível. A defesa é verificar cruzadamente cada proteína contra um rótulo independente (seu marcador de RNA).
Cada proteína deveria se correlacionar com seu próprio gene de RNA entre as células.
Um marcador canônico com rho próximo de 0 é um canal mal rotulado.
for (a innames(check_pairs)) { g <- check_pairs[[a]]if (a %in%colnames(adt_d) && g %in%colnames(rna_d)) { rho <-round(cor(adt_d[, a], rna_d[, g], method ="spearman"), 3) verdict <-if (rho <0.05) "SUSPECT"else"ok"cat(sprintf(" %-6s %-12s %-10s\n", a, rho, verdict)) }}
CD4 0.585 ok
CD8a 0.225 ok
CD19 0.502 ok
CD14 0.727 ok
CD3 0.753 ok
DicaPERGUNTA 6.8:
Dois canais de ADT mostram rho próximo de 0 contra seu próprio gene de RNA, enquanto o par cruzado é alto. Confirme a troca com a visualização abaixo.
Código
DefaultAssay(sobj_filt) <-"ADT"g <-FetchData(sobj_filt, vars =c("CD14", "CD19"))DefaultAssay(sobj_filt) <-"RNA"g$CD19_rna <-FetchData(sobj_filt, vars ="CD19")[, 1] # B cell RNA markerggplot(g, aes(CD14, CD19, color = CD19_rna)) +geom_point(alpha =0.5, size =0.8) +scale_color_gradient(low ="lightgrey", high ="#991b1b") +labs(title ="CD14 vs CD19 (ADT), colored by CD19 RNA",subtitle ="If CD19 RNA piles up on the CD14 axis, the labels are swapped",x ="CD14 (ADT)", y ="CD19 (ADT)", color ="CD19 RNA") +theme_classic(base_size =12)
Código
DefaultAssay(sobj_filt) <-"RNA"
DicaRESPOSTA
Rho de Spearman entre cada proteína de ADT e sua contraparte de RNA entre as células. Marcadores saudáveis mostram rho de 0.3-0.7. Impressão digital da troca: ADT_X vs RNA_X próximo de zero E ADT_X vs RNA_Y alto; o mesmo com X,Y invertidos. No conjunto de dados injetado, os rownames de CD14 e CD19 em ADT estão trocados. O bloco de correção reconstrói o assay ADT com os rownames re-trocados e reexecuta o CLR. É condicional, então em dados limpos não faz nada.
DEMONSTRAÇÃO AO VIVO:
Código
DefaultAssay(sobj_filt) <-"ADT"FeatureScatter(sobj_filt, "CD14", "CD19") +ggtitle("ADT CD14 vs ADT CD19")
Código
DefaultAssay(sobj_filt) <-"RNA"FeatureScatter(sobj_filt, "CD14", "CD19") +ggtitle("RNA CD14 vs RNA CD19")
GRÁFICO / SAÍDA: Dois gráficos de dispersão lado a lado. Saudável: os dispersogramas de ADT e RNA coincidem. Com uma troca: eles divergem
Seguro para v5: reconstrói o assay ADT a partir de sua matriz de contagens com os nomes corrigidos.
Condicional: só age se a troca for de fato detectada (seguro em dados limpos).
Código
DefaultAssay(sobj_filt) <-"ADT"adt_d <-t(LayerData(sobj_filt, layer ="data"))rna_d <-t(LayerData(sobj_filt, assay ="RNA", layer ="data"))swapped <-FALSEif (all(c("CD14", "CD19") %in%colnames(adt_d))) { rho14 <-cor(adt_d[, "CD14"], rna_d[, "CD14"], method ="spearman") swapped <-is.na(rho14) || rho14 <0.05}if (swapped) { adt_counts <-LayerData(sobj_filt, assay ="ADT", layer ="counts") rn <-rownames(adt_counts) i14 <-which(rn =="CD14"); i19 <-which(rn =="CD19") rn[c(i14, i19)] <- rn[c(i19, i14)]rownames(adt_counts) <- rn# CreateAssay5Object() builds a fresh assay with only the counts layer, so# the [[<- replacement method warns that it differs in structure (no# data/scale.data layers yet) from the ADT assay it replaces. Expected and# harmless here; suppressWarnings() wraps the assignment itself, since the# notice comes from the replacement method, not from CreateAssay5Object(). new_adt_assay <-CreateAssay5Object(counts = adt_counts)suppressWarnings(sobj_filt[["ADT"]] <- new_adt_assay) sobj_filt <-NormalizeData(sobj_filt, normalization.method ="CLR",margin =2, verbose =FALSE)cat("ADT labels corrected (CD14 <-> CD19) and re-normalized.\n")} else {cat("No swap detected; ADT labels left unchanged.\n")}
5.1.12 Etapa 6.9 - WNN: calcular o PCA de ADT, depois integrar
A integração WNN (Weighted Nearest Neighbor, vizinho mais próximo ponderado) aprende a contribuição relativa de cada modalidade por célula. Células onde o RNA é informativo recebem mais peso de RNA; células onde a proteína é mais discriminante recebem mais peso de ADT. O WNN requer um PCA por modalidade. O PCA de RNA existe desde o Bloco 4; agora construímos o PCA de ADT.
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.
Código
cat("Reductions now available:", paste(SafeReductions(sobj_filt), collapse =", "), "\n")
Reductions now available: pca, umap, harmony, umap.harmony, pca.adt
O Seurat espera um nome de peso por modalidade. Passar apenas “RNA.weight” dispara um aviso de que ADT.weight foi auto-atribuído. Passe ambos explicitamente para silenciar o aviso e documentar os nomes de coluna pretendidos.
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
Código
DefaultAssay(sobj_filt) <-"RNA"cat("WNN complete. New reduction: wnn.umap\n")
WNN complete. New reduction: wnn.umap
5.1.13 Etapa 6.10 - UMAP apenas de RNA vs UMAP de WNN
O WNN não necessariamente move todos os tipos celulares para uma região visualmente diferente; os layouts de UMAP podem rotacionar ou espelhar entre execuções mesmo quando as vizinhanças subjacentes mal mudam. Os dois painéis abaixo podem parecer semelhantes à primeira vista. O que realmente importa é se o conjunto de células agrupadas mudou, não se a imagem parece diferente.
A Etapa 6.11c (mais adiante) verifica isso diretamente por tipo celular através de uma tabela cruzada; aqui, uma comparação rápida de clustering dá uma primeira leitura antes disso.
Uma pontuação de concordância simples: para cada cluster apenas-RNA, qual fração de suas células cai no único cluster de WNN que captura a maioria delas.
Se a concordância mediana acima é alta (as células permanecem majoritariamente agrupadas da mesma forma) mas os dois layouts de UMAP ainda parecem visualmente diferentes (rotação diferente, posições relativas diferentes), o que isso diz sobre ler gráficos de UMAP lado a lado versus comparar diretamente a composição dos clusters?
DicaRESPOSTA
Significa que a comparação visual e a comparação de composição de clusters estão respondendo perguntas diferentes, e apenas uma delas realmente trata se o WNN mudou o resultado. O UMAP é uma projeção 2D escolhida de forma independente cada vez que é executado; rotação, espelhamento e espaçamento relativo entre blobs podem diferir entre duas execuções de UMAP mesmo sobre o mesmo grafo de vizinhança subjacente exato, porque a otimização do layout do UMAP não tem uma orientação fixa à qual se ancorar. Alta concordância na tabela cruzada de clusters significa que as células que estavam agrupadas juntas sob apenas-RNA ainda estão agrupadas juntas sob WNN, que é a afirmação substantiva. Uma comparação de UMAP lado a lado é útil para uma primeira impressão visual e para detectar diferenças grosseiras (uma população que se divide ou funde), mas não é uma forma confiável de julgar mudanças sutis, e dois UMAPs que parecem diferentes não são, por si só, evidência de que o WNN mudou algo sobre quais células pertencem juntas.
median(best_match_frac) # fraction of cells staying grouped together
[1] 1
GRÁFICO / SAÍDA: A tabela cruzada agreement_tab e o número de concordância mediano, junto aos dois painéis de UMAP
5.1.14 Etapa 6.11 - Pesos de modalidade por célula
RNA.weight próximo de 1 = a célula é melhor caracterizada por RNA
RNA.weight próximo de 0 = a célula é melhor caracterizada por ADT
RNA.weight e ADT.weight somam 1 para cada célula (FindMultiModalNeighbors os normaliza dessa forma), então 0.5 é o ponto de referência natural: abaixo dele, o ADT está contribuindo mais do que o RNA para a colocação daquela célula no grafo WNN, não apenas “alguma quantidade” de informação de proteína.
Código
pct_below_half <-round(mean(sobj_filt$RNA.weight <0.5) *100, 1)cat(sprintf("Cells where ADT contributes more than RNA (RNA.weight < 0.5): %.1f%%\n", pct_below_half))
Cells where ADT contributes more than RNA (RNA.weight < 0.5): 74.8%
Código
ggplot(sobj_filt@meta.data, aes(x = RNA.weight, fill = singler_label_top)) +geom_histogram(bins =40, alpha =0.8) +geom_vline(xintercept =0.5, linetype ="dashed", color ="#2c3142") +facet_wrap(~singler_label_top, scales ="free_y") +labs(title ="Per-cell RNA modality weight from WNN",subtitle ="Dashed line at 0.5: left of it, ADT outweighs RNA for that cell",x ="RNA weight (1 = fully RNA, 0 = fully ADT)") +theme_classic(base_size =10) +theme(legend.position ="none",strip.text =element_text(size =7))
DicaPERGUNTA 6.11
Quais tipos celulares neste conjunto de dados dependem mais do ADT (RNA.weight baixo)? Por que isso corresponde ao que você sabe sobre o dropout de RNA para seus marcadores definidores?
DicaRESPOSTA
Células T CD4 e células T CD8, por uma ampla margem. Seus marcadores definidores (CD4, CD8A) têm o pior dropout de RNA. Células B (MS4A1, CD79A) e monócitos (CD14, LYZ) têm sinal de RNA mais forte e dependem menos do ADT. Células NK (NKG7, GNLY, NCAM1) são intermediárias. O histograma facetado de RNA.weight por tipo celular torna isso concreto, e a linha tracejada em RNA.weight = 0.5 marca o ponto em que o ADT supera o RNA para aquela célula, não apenas contribui com alguma quantidade de informação.
DEMONSTRAÇÃO AO VIVO:
Código
mean(sobj_filt$RNA.weight <0.5) *100# cells where ADT outweighs RNA
GRÁFICO / SAÍDA: Histograma de RNA.weight facetado por tipo celular, com uma linha tracejada em 0.5. Células T atingem o pico à esquerda da linha, monócitos à direita.
5.1.15 Etapa 6.11b - Extremos de RNA.weight por cluster: existem clusters impulsionados por proteína?
Um cluster onde a maioria das células tem RNA.weight próximo de 0 é identificado quase inteiramente por proteína. Isso pode ser biologia (subtipos de células T, onde o dropout de RNA é severo) ou um artefato (um efeito de lote específico de proteína concentrado em um cluster).
Marque os clusters onde a mediana de RNA.weight < 0.3 (a proteína domina)
Código
protein_driven <- rna_w_by_cluster$seurat_clusters[rna_w_by_cluster$median_rna_w <0.3]if (length(protein_driven) >0) {cat("\nClusters where protein dominates (median RNA.weight < 0.3):\n")cat(" ", paste(protein_driven, collapse =", "), "\n")cat("Inspect these clusters: are they T-cell subsets (expected) or","something else?\n")}
Clusters where protein dominates (median RNA.weight < 0.3):
4
Inspect these clusters: are they T-cell subsets (expected) or something else?
DicaPERGUNTA 6.11b
Para o cluster mais impulsionado por proteína, qual fração de suas células foi atribuída a um rótulo de célula T pelo SingleR? O bloco abaixo calcula a resposta e aplica a regra de decisão.
DicaRESPOSTA
Calculado no script. Limiares de decisão: >=70% células T -> comportamento esperado de dropout de RNA, sem ação; 30-70% -> misto, inspecionar as proteínas de ADT que impulsionam a dominância; <30% -> artefato técnico, verificar lote de ADT, margin do CLR, fundo de isótipo. O script aplica a regra automaticamente e imprime o veredito.
DEMONSTRAÇÃO AO VIVO:
Código
md <- sobj_filt@meta.datacs <-aggregate(RNA.weight ~ seurat_clusters, data = md, FUN = median)cs[order(cs$RNA.weight), ]
Para a linha da tabela cruzada com a maior discordância, o rótulo SingleR-RNA e o cluster WNN divergem. Em qual você confia? O código abaixo identifica essa linha automaticamente e executa a verificação decisiva.
Código
if (n_low >=20) { ctab <-table(SingleR_label = sobj_filt$singler_label[low_rna_mask],WNN_cluster = sobj_filt$wnn_clusters[low_rna_mask] )# Largest off-diagonal cell = biggest disagreementif (length(ctab) >1) { max_cell <-arrayInd(which.max(ctab), dim(ctab)) src_lbl <-rownames(ctab)[max_cell[1]] src_clu <-colnames(ctab)[max_cell[2]] n_conf <- ctab[max_cell[1], max_cell[2]]cat(sprintf("\nLargest disagreement: %d cells labeled '%s' by SingleR-RNA\n", n_conf, src_lbl))cat(sprintf("but assigned to WNN cluster %s.\n", src_clu))# Deciding check: marker expression in those cells.# T-cell markers (RNA + ADT), B-cell markers, Mono markers decider_markers <-list(T_cell =list(rna =c("CD3E", "CD3D"), adt =c("CD3")),B_cell =list(rna =c("MS4A1", "CD79A"), adt =c("CD19", "CD20")),Monocyte =list(rna =c("CD14", "LYZ"), adt =c("CD14")),NK_cell =list(rna =c("NKG7", "GNLY"), adt =c("CD56")) ) conf_cells <-which(low_rna_mask & sobj_filt$singler_label == src_lbl & sobj_filt$wnn_clusters == src_clu)cat(sprintf("Marker detection in %d disputed cells:\n", length(conf_cells)))for (ct innames(decider_markers)) {for (mk in decider_markers[[ct]]$rna) {if (mk %in%rownames(sobj_filt[["RNA"]])) { v <-FetchData(sobj_filt, vars = mk)[conf_cells, 1]cat(sprintf(" %s (RNA %s): %.1f%% > 0\n", ct, mk, mean(v >0) *100)) } }for (mk in decider_markers[[ct]]$adt) {if (mk %in%rownames(sobj_filt[["ADT"]])) {DefaultAssay(sobj_filt) <-"ADT" v <-FetchData(sobj_filt, vars = mk)[conf_cells, 1]DefaultAssay(sobj_filt) <-"RNA"cat(sprintf(" %s (ADT %s): %.1f%% > 1.0 (CLR units)\n", ct, mk, mean(v >1.0) *100)) } } }cat("\nDecision rule:\n")cat(" The cell type whose markers fire in >= 50% of cells (RNA + ADT\n")cat(" combined) wins. If two cell types tie, mark these cells as\n")cat(" ambiguous (likely doublets that scDblFinder missed) and remove\n")cat(" before downstream group comparison.\n") }}
Largest disagreement: 53 cells labeled 'T_cells' by SingleR-RNA
but assigned to WNN cluster 4.
Marker detection in 53 disputed cells:
T_cell (RNA CD3E): 100.0% > 0
T_cell (RNA CD3D): 100.0% > 0
T_cell (ADT CD3): 100.0% > 1.0 (CLR units)
B_cell (RNA MS4A1): 5.7% > 0
B_cell (RNA CD79A): 1.9% > 0
B_cell (ADT CD19): 0.0% > 1.0 (CLR units)
B_cell (ADT CD20): 0.0% > 1.0 (CLR units)
Monocyte (RNA CD14): 5.7% > 0
Monocyte (RNA LYZ): 3.8% > 0
Monocyte (ADT CD14): 0.0% > 1.0 (CLR units)
NK_cell (RNA NKG7): 3.8% > 0
NK_cell (RNA GNLY): 5.7% > 0
NK_cell (ADT CD56): 0.0% > 1.0 (CLR units)
Decision rule:
The cell type whose markers fire in >= 50% of cells (RNA + ADT
combined) wins. If two cell types tie, mark these cells as
ambiguous (likely doublets that scDblFinder missed) and remove
before downstream group comparison.
DicaRESPOSTA
Regra de decisão do script: verificar cruzadamente a expressão de marcadores nas células disputadas. O tipo celular cujos marcadores (RNA + ADT) disparam em >=50% das células vence. Se dois tipos celulares empatarem, marque as células como ambíguas (provavelmente doublets não detectados) e exclua-as das comparações de grupo posteriores. NÃO escolha o rótulo WNN por padrão; NÃO escolha o rótulo SingleR por padrão. Deixe os marcadores decidirem.
5.1.17 Etapa 6.12 - Comparação de condições: Saudável vs COVID-19
Este gráfico parece informativo. Mas com 3 doadores saudáveis e 4 com COVID-19, ele pode ser completamente impulsionado por um doador atípico.
Código
group_props <- sobj_filt@meta.data %>%filter(!is.na(singler_label_top), !is.na(condition)) %>%group_by(condition, singler_label_top) %>%summarise(n =n(), .groups ="drop") %>%group_by(condition) %>%mutate(prop = n /sum(n))ggplot(group_props, aes(x = condition, y = prop, fill = singler_label_top)) +geom_bar(stat ="identity", width =0.6) +scale_y_continuous(labels =percent_format()) +labs(title ="Cell type proportions by condition",subtitle ="Looks clear at the group level; check whether one donor drives it",x ="Condition", y ="Proportion", fill ="Cell type") +theme_classic(base_size =12) +theme(legend.text =element_text(size =8))
Código
donor_props <- sobj_filt@meta.data %>%filter(!is.na(singler_label_top)) %>%group_by(donor_id, condition, singler_label_top) %>%summarise(n =n(), .groups ="drop") %>%group_by(donor_id) %>%mutate(prop = n /sum(n))ggplot(donor_props, aes(x = donor_id, y = prop, fill = singler_label_top)) +geom_bar(stat ="identity", width =0.7) +scale_y_continuous(labels =percent_format()) +facet_wrap(~condition, scales ="free_x") +labs(title ="Cell type proportions per donor",subtitle ="Consistent pattern across donors within each group; not driven by one outlier",x =NULL, y ="Proportion (%)", fill ="Cell type") +theme_classic(base_size =11) +theme(axis.text.x =element_text(angle =45, hjust =1),legend.text =element_text(size =7),strip.text =element_text(face ="bold"))
5.1.18 Etapa 6.12b - Verificação estatística de realidade: teste em nível de doador com nota de poder
Proporções por doador para UM tipo celular de exemplo, testadas entre condições. Execute isso para o rótulo que sua anotação produziu como o maior em COVID-19; o script tenta escolher automaticamente.
Proporções por doador para UM tipo celular de exemplo, testadas entre condições. Execute isso para o rótulo que sua anotação produziu como o maior em COVID-19; o script tenta escolher automaticamente. Restrito aos rótulos top-N: um rótulo sustentado por 1-2 células poderia mostrar um delta espúrio de 100% entre condições apenas por acaso, o que faria um “achado” sem sentido parecer o sinal mais forte do conjunto de dados.
subset_df <- per_donor %>%filter(singler_label_top == target_lbl)n_h <-sum(subset_df$condition =="Healthy")n_c <-sum(subset_df$condition =="COVID19")if (n_h >=2&& n_c >=2) { wt <-wilcox.test(prop ~ condition, data = subset_df, exact =FALSE)cat(sprintf("\nWilcoxon rank-sum (n=%d Healthy vs n=%d COVID-19):\n", n_h, n_c))cat(sprintf(" W = %g, p = %.3f\n", wt$statistic, wt$p.value))}
Wilcoxon rank-sum (n=3 Healthy vs n=4 COVID-19):
W = 0, p = 0.052
Piso de poder: com n=3 vs n=4 e Wilcoxon em alpha=0.05, o menor tamanho de efeito padronizado detectável (d de Cohen) é aproximadamente 1.5 para 80% de poder.
Em termos simples: apenas diferenças de proporção maiores que ~1.5 desvios-padrão da distribuição em nível de doador são detectáveis. Qualquer valor-p aqui é exploratório, não confirmatório.
n=3 vs n=4 donors. Minimum detectable effect size (Cohen's d) ~1.5 for
Código
cat("80% power at alpha=0.05. Treat any p-value here as exploratory only.\n")
80% power at alpha=0.05. Treat any p-value here as exploratory only.
DicaPERGUNTA 6.12b
Supondo que o valor-p impresso esteja na faixa de 0.05 - 0.10, que conclusão você pode TIRAR deste conjunto de dados? O bloco abaixo aplica a regra de decisão explícita para comparação de grupos com amostra pequena.
Código
cat("\nDecision matrix for the observed p-value:\n")
Decision matrix for the observed p-value:
Código
if (exists("wt")) { pv <- wt$p.valuecat(sprintf(" Observed p-value: %.3f\n", pv))cat("\n p < 0.01 with n=7 donors total: STRONG signal, replicate in a\n")cat(" larger cohort before claiming.\n")cat(" 0.01 <= p < 0.05 : Suggestive. Report effect size +\n")cat(" per-donor plot. Not confirmatory.\n")cat(" 0.05 <= p < 0.10 : Exploratory. Cannot claim difference.\n")cat(" State the n, the direction, and the\n")cat(" effect size; flag as hypothesis-\n")cat(" generating only.\n")cat(" p >= 0.10 : No evidence of difference at this n.\n")cat(" Power analysis says n>=10 per group\n")cat(" needed for the observed effect size.\n")# Auto-apply verdict <-if (pv <0.01) {"STRONG (replicate in larger cohort)" } elseif (pv <0.05) {"suggestive (report with caveats)" } elseif (pv <0.10) {"exploratory (hypothesis-generating only)" } else {"no evidence of difference at this n" }cat(sprintf("\nAutomatic verdict at p = %.3f: %s\n", pv, verdict))# Minimum n needed for the observed effect (Cohen's d -> n via power calc) pooled_sd <-sd(subset_df$prop) obs_d <- delta_by_lbl$delta[1] / pooled_sdcat(sprintf("\nObserved Cohen's d (donor-level): %.2f\n", obs_d))cat("Approximate n per group needed for 80%% power at alpha=0.05:\n")# Rough rule: n ~= 16 / d^2 for two-sample t-test, similar for Wilcoxonif (!is.na(obs_d) && obs_d >0) { n_needed <-ceiling(16/ obs_d^2)cat(sprintf(" ~%d donors per group (using rule n ~ 16/d^2)\n", n_needed)) }}
Observed p-value: 0.052
p < 0.01 with n=7 donors total: STRONG signal, replicate in a
larger cohort before claiming.
0.01 <= p < 0.05 : Suggestive. Report effect size +
per-donor plot. Not confirmatory.
0.05 <= p < 0.10 : Exploratory. Cannot claim difference.
State the n, the direction, and the
effect size; flag as hypothesis-
generating only.
p >= 0.10 : No evidence of difference at this n.
Power analysis says n>=10 per group
needed for the observed effect size.
Automatic verdict at p = 0.052: exploratory (hypothesis-generating only)
Observed Cohen's d (donor-level): 1.80
Approximate n per group needed for 80%% power at alpha=0.05:
~5 donors per group (using rule n ~ 16/d^2)
Apenas exploratório. Com n=7 doadores no total, um valor-p nessa faixa é gerador de hipótese e não pode sustentar uma afirmação publicada de diferença. Relate: n por grupo, direção da diferença, tamanho de efeito, declaração de que a análise confirmatória requer uma coorte maior. O script estima o n necessário para 80% de poder no tamanho de efeito observado (regra prática n ~ 16 / d^2).
DEMONSTRAÇÃO AO VIVO: Veja a Etapa 6.12b (Seção 5.1.18) no script: ela aplica automaticamente a matriz de decisão.
GRÁFICO / SAÍDA: Saída de console com veredito automático.
DicaPERGUNTA 6.12
Como você testaria formalmente a diferença em nível de grupo dados apenas 3 doadores saudáveis vs 4 com COVID-19? Qual é o padrão mínimo de relato que você aceitaria em um artigo?
DicaRESPOSTA
O doador como unidade de análise. Calcule as proporções de tipo celular por doador (um número por doador por tipo celular), Wilcoxon de soma de postos entre doadores. Com n=3 vs n=4, o poder é mínimo: apenas tamanhos de efeito próximos de d de Cohen=1.5 são detectáveis em alfa=0.05. Padrão mínimo de relato: plotar as proporções por doador (a Etapa 6.12 faz isso), declarar o n, e marcar explicitamente a análise como exploratória a menos que os tamanhos de efeito sejam muito grandes. Gráficos de barras em nível de grupo sem respaldo por doador não deveriam aparecer em um artigo.
Error in `filter()`:
ℹ In argument: `singler_label == target_lbl`.
Caused by error:
! object 'singler_label' not found
Código
wilcox.test(prop ~ condition, data = subset_df, exact =FALSE)
Wilcoxon rank sum test with continuity correction
data: prop by condition
W = 0, p-value = 0.05183
alternative hypothesis: true location shift is not equal to 0