Análisis de expresión diferencial

Última actualización:

17 de mayo de 2026

PRÁCTICA 12

¡OJO! Es importante actualizar y descargar el paquete DESeq2, ya que lo utilizaremos en esta práctica.

Datos

Utilizaremos las medidas de expresión génica del archivo salmon.merged.gene_counts.ts (descargado desde el aula virtual). Los datos proceden de 12 genotecas de RNA-seq de lecturas emparejadas, de muestras de Drosophila melanogaster de dos orígenes geográficos (Panamá y Maine) y dos tratamientos de temperatura (low y high) (Zhao 2015). Hay 3 réplicas biológicas para cada combinación de región y temperatura. Además, necesitáis el archivo dme_elev_samples.tsv, una tabla que indica a qué región y tratamiento pertenece cada muestra (descargado desde el aula virtual). Los dos archivos deben estar guardados en la carpeta de datos.

Exploración de los datos

muestras <- read.table('../../data/dme_elev_samples.tsv', header = TRUE)
genex <- read.table('../../data/salmon.merged.gene_counts.tsv',
                    header = TRUE)
muestras
       sample population temp
1  SRR1576457      maine  low
2  SRR1576458      maine  low
3  SRR1576459      maine  low
4  SRR1576460      maine high
5  SRR1576461      maine high
6  SRR1576462      maine high
7  SRR1576463     panama  low
8  SRR1576464     panama  low
9  SRR1576465     panama  low
10 SRR1576514     panama high
11 SRR1576515     panama high
12 SRR1576516     panama high

Ejercicio 1

Recuerda dejar constancia en un bloque de código de cómo obtienes las respuestas.

A) Usa la función paste() para añadir una columna a muestras que contenga la población y la temperatura unidas por _.

muestras$nueva_columna <- paste(muestras$population, muestras$temp, sep = "_")
muestras$nueva_columna
 [1] "maine_low"   "maine_low"   "maine_low"   "maine_high"  "maine_high" 
 [6] "maine_high"  "panama_low"  "panama_low"  "panama_low"  "panama_high"
[11] "panama_high" "panama_high"

B) ¿Cuál es el número total de lecturas por muestra?

El dataframe genex tiene el dataframe del gen y una columna por cada muestra, los números que vemos son “counts”. Es un recuento del número de lecturas que mapean en cada uno de los genes/muestras proveniendo del RNASeq. Si pone un punto en el número es porque el programa no está “seguro” acerca del recuento y pone un decimal. No lo tenemos que aplicar a toda la sumas de columnas, ya que las dos primeras no son números, entonces no deben ser sumadas. Vemos que hay entre 20-30 millones de lecturas por muestra. Si hay 20.000 genes y entre 20-30 millones de lecturas por muestra nos hacemos la idea de lecturas por gen.

total_lecturas <- colSums(genex[ , -c (1,2)])
total_lecturas
SRR1576457 SRR1576458 SRR1576459 SRR1576460 SRR1576461 SRR1576462 SRR1576463 
  28252778   21565448   29471559   27692134   26953900   29368135   29706071 
SRR1576464 SRR1576465 SRR1576514 SRR1576515 SRR1576516 
  26757043   26596118   30950499   25563787   24401671 

C) ¿Los recuentos tienen distribuciones parecidas entre muestras?

Para separar las muestras metemos las columnas una bajo de otra, para ello usamos pivot_longer. Enviamos los nombres a la variable muestra y los valores a la de expresion. Aparecen algunos errores porque son valores (de expresión) con cero, es decir, no se expresan los genes (y nosotros estamos usando logaritmos). A continuación vemos un violin plot.

Nota: he añadido en el siguiente código warning:false y message:false, ya que salía un error a la hora de representar.

library(tidyr)
library(ggplot2)
genex_long <- pivot_longer(genex, 3:14, names_to = 'muestra', values_to = 'expression')
ggplot(data = genex_long, mapping = aes(x = muestra, y = expression)) + geom_violin() + scale_y_continuous(trans = 'log')

Otra opción sería la función pairs acepta una matriz/dataframe de datos numéricos.

pairs(genex[, -c(1,2)])

La función pairs muestra la nube de puntos de cada pareja. La mayor parte de muestras tienen una expresión baja, se concentran todos en la esquina inferior izquierda. Tenemos muchos paneles, podríamos haberlo hecho condición por condición.

D) ¿Cuántos genes no se expresan en absoluto en ninguna de las muestras?

Igualamos a cero para crear un vector de verdadero y falso, así nos dicen en cuántos genes la expresión es cero. En este caso, hay 4722 genes sin expresión.

genes_sin_expresion <- sum(rowSums(genex[, -c(1,2)]) ==0)
genes_sin_expresion
[1] 4722

Crear el objeto de clase DESeqDataSet

Para crear un objeto de clase DESeqDataSet necesitamos que las filas de la tabla muestras estén en el mismo orden que las columnas en genex. La función DESeqDataSetFromMatrix() necesita que los recuentos sean una matriz, no un marco de datos. Y en muestras debe haber factores.

suppressMessages(library('DESeq2'))
all(names(genex)[3:14] == muestras$sample)
[1] TRUE
# Pasamos la columna "sample" a nombres de fila:
row.names(muestras) <- muestras$sample
# Eliminamos la columna "sample":
muestras$sample <- NULL
# Y convertimos "population" y "temp" en factores:
muestras$population <- factor(muestras$population,
                              levels = c('maine', 'panama'))
muestras$temp       <- factor(muestras$temp,
                              levels = c('low', 'high'))
muestras
           population temp nueva_columna
SRR1576457      maine  low     maine_low
SRR1576458      maine  low     maine_low
SRR1576459      maine  low     maine_low
SRR1576460      maine high    maine_high
SRR1576461      maine high    maine_high
SRR1576462      maine high    maine_high
SRR1576463     panama  low    panama_low
SRR1576464     panama  low    panama_low
SRR1576465     panama  low    panama_low
SRR1576514     panama high   panama_high
SRR1576515     panama high   panama_high
SRR1576516     panama high   panama_high

Aunque no hacía falta definir los niveles de los factores, este es un buen momento para definir manualmente su orden (por defecto se ordenan alfabéticamente). El orden de los niveles determina qué nivel actúa como referencia en los contrastes estadísticos (el primero). Esto resultaría importante, por ejemplo, al comparar tratamientos respecto de un control.

Los datos de recuento deben ser números enteros y en forma de matriz.

row.names(genex) <- genex$gene_name
genex$gene_name <- NULL
genex$gene_id   <- NULL
genex <- round(as.matrix(genex))
dds1 <- DESeqDataSetFromMatrix(
   countData = genex,
   colData = muestras,
   design = ~ population + temp
)
converting counts to integer mode
dds1
class: DESeqDataSet 
dim: 24254 12 
metadata(1): version
assays(1): counts
rownames(24254): 7SLRNA:CR32864 a ... RR51477 Bari1{}RR51478
rowData names(0):
colnames(12): SRR1576457 SRR1576458 ... SRR1576515 SRR1576516
colData names(3): population temp nueva_columna

Nota: los objetos DESeqDataSet pueden contener información sobre las coordenadas cromosómicas de los genes o tránscritos, si se construyen a partir de otro tipo de datos.

Pre-filtrado

Una fracción importante de genes no se están expresando en ningúna muestra. Aunque el filtrado no es necesario, ayuda a reducir la memoria ocupada por dds1:

keep <- rowSums(counts(dds1)) > 0
dds1 <- dds1[keep, ]

Expresión diferencial

Observa que la función DESeq() produce un objeto también de clase DESeqDataSet, pero con resultados añadidos:

dds1 <- DESeq(dds1)
estimating size factors
estimating dispersions
gene-wise dispersion estimates
mean-dispersion relationship
final dispersion estimates
fitting model and testing

Vemos que nos produce los resultados entre el contraste del último nivel y del factor en la tabla.

res <- results(dds1)
res
log2 fold change (MLE): temp high vs low 
Wald test p-value: temp high vs low 
DataFrame with 19508 rows and 6 columns
                baseMean log2FoldChange     lfcSE       stat      pvalue
               <numeric>      <numeric> <numeric>  <numeric>   <numeric>
7SLRNA:CR32864   346.333      0.5918974 1.3301901   0.444972 0.656339979
a                555.596     -0.2730041 0.0718487  -3.799710 0.000144866
abd-A            531.996     -0.0286785 0.0789218  -0.363379 0.716321732
Abd-B            634.760     -0.1083918 0.0771241  -1.405421 0.159896272
Abl             1654.842     -0.1328055 0.0746580  -1.778851 0.075264257
...                  ...            ...       ...        ...         ...
RR51007          4.90906      3.8596869  2.972471  1.2984773 1.94123e-01
RR51048         16.60710     -0.8973071  0.341603 -2.6267541 8.62036e-03
RR51093         36.93279      0.0660523  1.685323  0.0391927 9.68737e-01
RR51475        159.62678     -2.6058194  1.875172 -1.3896429 1.64637e-01
RR51477         70.83394      1.6927986  0.192437  8.7966393 1.40975e-18
                      padj
                 <numeric>
7SLRNA:CR32864  0.81170166
a               0.00154573
abd-A           0.85146289
Abd-B           0.33849217
Abl             0.20046220
...                    ...
RR51007        3.82417e-01
RR51048        4.01745e-02
RR51093        9.84464e-01
RR51475        3.44815e-01
RR51477        2.06427e-16

Por defecto, la función results() produce la tabla de resultados para el contraste entre el nivel último y el de referencia del último factor en la tabla que describe las muestras. En este caso, el factor es temp, el nivel de referencia es low y el otro nivel es high. Por tanto, los valores de log2FoldChange representan el logaritmo en base 2 de la relación entre niveles de expresión en muestras high respecto de muestras low.

En referencia a estos resultados: log2foldchange - logaritmo en base 2 de la relación de cambio. Valores positivos son genes que a temperatura alta se expresan más que en baja. El nivel de expresión es mayor a temperatura baja para los valores negativos. Tenemos un dataframe con 19.508 filas (esto es porque hemos prefiltrado y hemos eliminado los que tienen una expresión muy baja).

Ejercicio 3

A) Mira la ayuda de results() y genera también la tabla de resultados para el contraste entre las dos regiones de procedencia de las moscas.

Repetimos lo que hemos realizado anteriormente:

dds1_2 <- DESeqDataSetFromMatrix(
   countData = genex,
   colData = muestras,
   design = ~ population
)
converting counts to integer mode
dds1_2
class: DESeqDataSet 
dim: 24254 12 
metadata(1): version
assays(1): counts
rownames(24254): 7SLRNA:CR32864 a ... RR51477 Bari1{}RR51478
rowData names(0):
colnames(12): SRR1576457 SRR1576458 ... SRR1576515 SRR1576516
colData names(3): population temp nueva_columna
keep <- rowSums(counts(dds1_2)) > 0
dds1_2 <- dds1_2[keep, ]
dds1_2 <- DESeq(dds1_2)
estimating size factors
estimating dispersions
gene-wise dispersion estimates
mean-dispersion relationship
final dispersion estimates
fitting model and testing
res2 <- results(dds1_2)
res2
log2 fold change (MLE): population panama vs maine 
Wald test p-value: population panama vs maine 
DataFrame with 19508 rows and 6 columns
                baseMean log2FoldChange     lfcSE       stat     pvalue
               <numeric>      <numeric> <numeric>  <numeric>  <numeric>
7SLRNA:CR32864   346.333     -0.3405225 1.2828671  -0.265439  0.7906716
a                555.596     -0.1061901 0.1079132  -0.984033  0.3250994
abd-A            531.996      0.0468148 0.0780163   0.600065  0.5484628
Abd-B            634.760     -0.0386942 0.0827328  -0.467701  0.6399985
Abl             1654.842     -0.1435019 0.0846880  -1.694478  0.0901746
...                  ...            ...       ...        ...        ...
RR51007          4.90906      1.0441599  2.913048  0.3584424 0.72001228
RR51048         16.60710     -1.2841124  0.397258 -3.2324379 0.00122739
RR51093         36.93279     -0.8907347  1.591194 -0.5597901 0.57562259
RR51475        159.62678     -0.0858284  1.937892 -0.0442896 0.96467360
RR51477         70.83394     -0.2603940  0.461897 -0.5637486 0.57292520
                    padj
               <numeric>
7SLRNA:CR32864  0.966622
a               0.790762
abd-A           0.907772
Abd-B           0.928931
Abl             0.523390
...                  ...
RR51007        0.9498358
RR51048        0.0472064
RR51093        0.9181683
RR51475        0.9967946
RR51477        0.9169334

También lo podemos hacer así (no lo pongo en código de bloque de R para evitar sobreescribir los datos de antes): res2 <- results(dds1, contrast = c(‘population’, ‘panama’, ‘maine’))

Otro: p-valor ajustado para resolver el problema de los tests múltiples.

B) Usa summary() sobre los resultados para ver el número de genes diferencialmente expresados entre cada par de condiciones.

summary(res)

out of 19508 with nonzero total read count
adjusted p-value < 0.1
LFC > 0 (up)       : 2418, 12%
LFC < 0 (down)     : 2336, 12%
outliers [1]       : 90, 0.46%
low counts [2]     : 3018, 15%
(mean count < 2)
[1] see 'cooksCutoff' argument of ?results
[2] see 'independentFiltering' argument of ?results

Tenemos un 12% de genes up-regulated o sobreexpresados ene temperaturas altas sobre bajas, también un 12% de subexpresados. Outliers tenemos 90 y low counts 3018.

summary(res2)

out of 19508 with nonzero total read count
adjusted p-value < 0.1
LFC > 0 (up)       : 350, 1.8%
LFC < 0 (down)     : 351, 1.8%
outliers [1]       : 46, 0.24%
low counts [2]     : 2270, 12%
(mean count < 1)
[1] see 'cooksCutoff' argument of ?results
[2] see 'independentFiltering' argument of ?results

Tenemos porcentaje bastante equilibrado entre main y panama. La temperatura causa la sobreexpresión. Nota: En clase hemos probado este bloque de código pero me da distinto. Debería dar LFC > 0 de 623 y LFC < 0 de 634. Los outliers deberían ser de 90 y low count de 3018.

C) ¿Cuál es la tasa de falsos positivos (FDR)?

En summary podemos apreciar que el FDR es menor de 0,1 en ambos casos. Esto podemos conocerlo de la siguiente forma: en la función results tenemos un parametro alfa que es por defecto 0,1. Por su definición alfa es el umbral de significancia utilziado para filtrar el filtrado independiente. Si somos más exigentes y no lo toleramos y lo queremos hacer con 0,5 lo cambiamos. Este no ha sido el caso de esta práctica.

Gráficas

plotMA(res)

Este plot es simétrico y centrado en cero. Vemos el logFC según el nivel de expresión. Vemos que hay una dispersión grande.

Los genes con niveles de expresión más bajos producen resultados más ruidosos (la varianza es mayor). La función lfcShrink() contrae (shrink) las estimaciones de los efectos (log fold change) para reducir el ruido y hacer el resultado más realista: Aplicamos empirical bayes que es una forma de descomprimir. Lo comprime asumiendo que el logFC lo calcula con error. Vemos que ha disminuido la escala del logFC. Es una forma de ser más conservadores, estimamos la variación pero con muy pocas réplicas. Si asumimos que la tasas de cambio está entre 0,5 y -0,5, vemos que hay dispersión pero la media está muy concentrada. Suponemos que los valores elevados o bajos con pocas muestra se parecen más a la muestra, lo que hace es comprimirlo.

resShrink <- lfcShrink(dds1, coef = 'temp_high_vs_low',
                       type = 'normal')
using 'normal' for LFC shrinkage, the Normal prior from Love et al (2014).

Note that type='apeglm' and type='ashr' have shown to have less bias than type='normal'.
See ?lfcShrink for more details on shrinkage type, and the DESeq2 vignette.
Reference: https://doi.org/10.1093/bioinformatics/bty895
plotMA(resShrink)

Ejercicio 4

¿Sabrías hacer una gráfica volcán?

res_df <- as.data.frame(res)

volcano <- ggplot(res_df, aes(x = log2FoldChange, y = -log10(padj))) +
  geom_point(alpha = 0.5) +
  labs(title = "Volcano Plot", x = "resShrink", y = "-log10(P-value)") +
  theme_minimal()

print(volcano)

Extraer datos transformados

Aunque los datos de entrada no deben estar normalizados y la función DESeq() ya tiene en cuenta las diferencias de tamaño de genoteca entre muestras y la relación entre media y varianza, puede ser útil disponer de los datos normalizados para representarlos gráficamente. Para ello podemos usar las funciones vst() y assay():

dds2 <- vst(dds1)
NormCounts <- assay(dds2)
res <- res[! is.na(res$pvalue), ]
res <- res[order(res$log2FoldChange),]
heatmap(NormCounts[c(head(row.names(res), 100),
                     tail(row.names(res), 100)), ],
        Rowv = NA, Colv = NA)

Ejercicio 5

¿Podrías explicar qué hace el bloque de código anterior?

  • vst tiene la función de estimar la tendencia de la dispersió (Quickly estimate dispersion trend and apply a variance stabilizing transformation).

  • assay contiene una matriz donde las filas representan objetos de interés y las columnas representan las muestras.

Nota: He consultado help(vst) y help(assay) respectivamente.

Ejercicio 6

A) En este enlace analizan exactamente los mismos datos con otro paquete de R: limma. Una de sus conclusiones es se sobreexpresan más genes en temperatura baja que en temperatura alta. ¿Confirma tu análisis esta conclusión?

Recapitulando con la primera parte de la práctica, yo he visualizado estos valores mediante summary(res). De esta forma, llegué a la conclusión de se están expresando más genes a temperatura alta: 2418 se sobreexpresan en temperatura alta 2336 se sobreexpresan en temperatura baja No obstante, el enlace no me redirige, así que no puedo comparar los resultados. Mi análisis se confirmaría en el caso de que en el otro análisis también se llegue a la conclusión de que hay más genes sobreexpresados en temperatura alta.

B) Estos son los 10 genes más diferencialmente expresados según aquel análisis entre las condiciones de baja y alta temperatura:

He elaborado la tabla a partir de los resultados que se ven en el guion del Aula Virtual
logFC AveExpr t P.Value adj.P.Val B
Cpr92F 2.473527 6.201774 24.28881 1.306910e-13 1.739105e-09 21.29258
Vajk3 1.675184 4.029281 21.90298 6.069753e-13 3.524528e-09 19.66813
lncRNA:CR46017 -3.974938 1.044005 -21.50807 7.945880e-13 3.524528e-09 16.80878
CG7214 2.198970 7.945664 20.75054 1.349821e-12 4.490516e-09 19.14847
CG3739 -2.869172 6.514652 -20.49734 4.414988e-12 1.175005e-08 18.01975
Acp1 2.231834 7.638125 17.79441 2.089853e-11 3.997808e-08 16.54909

¿Cómo se comparan estos resultados con los tuyos?

Mediante el siguiente código voy a comprobar mis valores para estos genes:

res[c("Cpr92F", "Vajk3", "Acp1","CG3739", "CG7214"), ]
log2 fold change (MLE): temp high vs low 
Wald test p-value: temp high vs low 
DataFrame with 5 rows and 6 columns
        baseMean log2FoldChange     lfcSE      stat      pvalue        padj
       <numeric>      <numeric> <numeric> <numeric>   <numeric>   <numeric>
Cpr92F  2718.979       -2.39126 0.1172256  -20.3988 1.71374e-92 1.40527e-88
Vajk3    513.411       -1.65423 0.0886464  -18.6610 1.02701e-77 3.36859e-74
Acp1    7042.192       -2.17394 0.1217046  -17.8624 2.31289e-71 4.21460e-68
CG3739  3912.044        2.93354 0.1497441   19.5903 1.86963e-85 7.66550e-82
CG7214  8615.934       -2.14594 0.1178994  -18.2015 5.02408e-74 1.17707e-70

A través de esto, y retomando el apartado A, veo que los resultados están “invertidos”. Esto lo observo mediante el FC, ya que cuando me da positivo a mi, en el análisis por limma da negativo. Esto quiere decir que yo para calcular el FC lo he hecho temp alta/temp bajo, mientras que en el analisis por limma se ha hecho temp baja/temp alta. Para poder comprobar los resultados probaríamos a calcular el FC de la misma forma que en el otro estudio.