enlace web: https://rpubs.com/daniloceschin/Rbioinfo-modulo3
Asignatura: Bioinformática
Carrera: Licenciatura en Genética
Institución: IUCBC – CIMETSA, Córdoba, Argentina
Docente: Dr. Danilo G. Ceschin
Año: 2026
Objetivo del módulo: aprender a generar y personalizar gráficos en R, primero con las herramientas de R base y luego con ggplot2. Al final del módulo vas a poder elegir el tipo de gráfico adecuado para cada tipo de dato y producir figuras con contexto biológico real.
R tiene un sistema gráfico integrado que no requiere ningún paquete adicional. Es menos elegante que ggplot2, pero es inmediato, flexible y suficiente para exploración y análisis rápido.
Vamos a usar un conjunto de datos simulados que representan genes con distintos atributos. Lo creamos una sola vez y lo usamos en toda la Parte 1.
set.seed(42)
n_genes <- 80
genes_df <- data.frame(
gen = paste0("Gen_", sprintf("%02d", 1:n_genes)),
longitud = round(rnorm(n_genes, mean = 2500, sd = 900)), # pb
gc_content = round(runif(n_genes, min = 0.35, max = 0.70), 3),
expresion = round(2^rnorm(n_genes, mean = 7, sd = 2), 1), # CPM
condicion = factor(rep(c("Control","KO"), each = n_genes/2),
levels = c("Control","KO")),
cromosoma = factor(sample(paste0("chr", c(1:5,"X")),
n_genes, replace = TRUE)),
stringsAsFactors = FALSE
)
# Vista rápida
head(genes_df)
str(genes_df)
plot() — el punto de entradaplot() es la función gráfica más general de R. Detecta
automáticamente el tipo de dato que recibe y elige el gráfico más
apropiado.
# El gráfico más simple posible: x vs y
plot(genes_df$longitud, genes_df$gc_content)
Funciona, pero es bastante austero. Los argumentos principales para mejorar la presentación son:
| Argumento | Qué controla | Ejemplo |
|---|---|---|
main |
Título | main = "Mi gráfico" |
xlab, ylab |
Etiquetas de ejes | xlab = "Longitud (pb)" |
col |
Color de los puntos | col = "steelblue" |
pch |
Forma de los puntos | pch = 16 (círculo sólido) |
cex |
Tamaño de los puntos | cex = 0.8 |
xlim, ylim |
Límites de los ejes | xlim = c(0, 5000) |
type |
Tipo de gráfico | type = "p" (puntos), "l" (líneas),
"b" (ambos) |
plot(
x = genes_df$longitud,
y = genes_df$gc_content,
main = "Longitud génica vs Contenido GC",
xlab = "Longitud del gen (pb)",
ylab = "Contenido GC",
col = "steelblue",
pch = 16, # círculo sólido
cex = 0.8 # puntos un poco más pequeños
)
Una técnica muy útil: asignar colores distintos según una variable
categórica. Usamos el factor condicion para ello.
# Definir paleta de colores por condición
colores <- c(Control = "steelblue", KO = "firebrick")
plot(
x = genes_df$longitud,
y = genes_df$gc_content,
main = "Longitud génica vs Contenido GC\npor condición experimental",
xlab = "Longitud del gen (pb)",
ylab = "Contenido GC",
col = colores[genes_df$condicion], # asigna color según el factor
pch = 16,
cex = 0.9
)
# Agregar leyenda
legend(
x = "topright",
legend = names(colores),
col = colores,
pch = 16,
title = "Condición"
)
💡 Nota:
colores[genes_df$condicion]usa el vector de condiciones como índice del vector de colores. Cuando el valor es"Control"toma"steelblue", cuando es"KO"toma"firebrick". Es el mismo mecanismo de subsetting por nombre que vimos en el Módulo 2.
plot(
x = genes_df$longitud,
y = genes_df$gc_content,
main = "Longitud génica vs Contenido GC",
xlab = "Longitud del gen (pb)",
ylab = "Contenido GC",
col = "steelblue",
pch = 16, cex = 0.8
)
# Línea de regresión
abline(lm(gc_content ~ longitud, data = genes_df),
col = "firebrick", lwd = 2, lty = 2) # lty=2: línea punteada
💡 Nota:
abline()agrega una línea sobre un gráfico existente.lm()ajusta un modelo lineal yabline()extrae automáticamente la pendiente e intercepto para dibujar la recta.
hist()El histograma muestra la distribución de una variable continua. Ideal para explorar si los datos siguen una distribución normal, tienen sesgo, o presentan valores atípicos.
hist(
x = genes_df$longitud,
main = "Distribución de longitudes génicas",
xlab = "Longitud (pb)",
ylab = "Frecuencia",
col = "steelblue",
border = "white", # color del borde de las barras
breaks = 15 # número aproximado de barras
)
# freq=FALSE: eje Y como densidad (en lugar de frecuencia absoluta)
hist(
genes_df$longitud,
main = "Distribución de longitudes génicas",
xlab = "Longitud (pb)",
ylab = "Densidad",
col = "lightsteelblue",
border = "white",
breaks = 15,
freq = FALSE # necesario para superponer la curva de densidad
)
# Superponer curva de densidad suavizada
lines(density(genes_df$longitud), col = "firebrick", lwd = 2)
# par(mfrow) divide la ventana gráfica en filas × columnas
par(mfrow = c(1, 2)) # 1 fila, 2 columnas
hist(genes_df$longitud[genes_df$condicion == "Control"],
main = "Control",
xlab = "Longitud (pb)",
col = "steelblue",
border = "white",
breaks = 10,
xlim = c(0, 6000)) # mismo eje x en ambos para comparar
hist(genes_df$longitud[genes_df$condicion == "KO"],
main = "KO",
xlab = "Longitud (pb)",
col = "firebrick",
border = "white",
breaks = 10,
xlim = c(0, 6000))
par(mfrow = c(1, 1)) # restablecer la ventana gráfica a 1 panel
⚠️ Importante: siempre restablecer
par(mfrow = c(1,1))al terminar. Si no lo hacés, el siguiente gráfico que generes seguirá dividido en paneles.
boxplot()El boxplot (diagrama de caja) es ideal para comparar distribuciones entre grupos. Muestra mediana, cuartiles y valores atípicos de un vistazo.
Anatomía de un boxplot:
─── línea superior del bigote: máximo (sin outliers)
┐ borde superior de la caja: Q3 (tercer cuartil, 75%)
│ línea dentro de la caja: mediana (Q2, 50%)
┘ borde inferior de la caja: Q1 (primer cuartil, 25%)
─── línea inferior del bigote: mínimo (sin outliers)
○ puntos fuera de los bigotes: valores atípicos (outliers)
boxplot(
expresion ~ condicion,
data = genes_df,
main = "Expresión génica por condición",
xlab = "Condición",
ylab = "Expresión (CPM)",
col = c("steelblue", "firebrick"),
border = "gray30",
notch = FALSE # notch=TRUE agrega muesca en la mediana
)
💡 Nota: la fórmula
expresion ~ condicionse lee: “expresión en función de condición”. Es la misma notación que se usa en modelos estadísticos comolm()ot.test(). El~separa variable respuesta (izquierda) de variable explicativa (derecha).
boxplot(
longitud ~ cromosoma,
data = genes_df,
main = "Longitud génica por cromosoma",
xlab = "Cromosoma",
ylab = "Longitud (pb)",
col = c("steelblue","firebrick","forestgreen",
"darkorange","mediumpurple","gold3"),
border = "gray30",
las = 1 # las=1: etiquetas del eje X horizontales
)
barplot()Ideales para mostrar valores resumidos (medias, conteos, proporciones) por categoría. No son adecuados para mostrar distribuciones — para eso son los boxplots o histogramas.
# Conteo de genes por cromosoma
conteo_crom <- table(genes_df$cromosoma)
conteo_crom
barplot(
height = conteo_crom,
main = "Número de genes por cromosoma",
xlab = "Cromosoma",
ylab = "Número de genes",
col = "steelblue",
border = "white"
)
# Tabla de frecuencias: cromosoma × condición
tabla_crom_cond <- table(genes_df$cromosoma, genes_df$condicion)
barplot(
height = t(tabla_crom_cond), # transponer: condición en filas
beside = TRUE, # beside=TRUE: barras una al lado de otra
main = "Genes por cromosoma y condición",
xlab = "Cromosoma",
ylab = "Número de genes",
col = c("steelblue","firebrick"),
border = "white",
legend.text = colnames(tabla_crom_cond),
args.legend = list(x = "topright", title = "Condición")
)
par(mfrow)par(mfrow = c(filas, columnas)) divide la ventana
gráfica en una cuadrícula. Muy útil para comparar varios gráficos a la
vez.
par(mfrow = c(2, 2)) # cuadrícula 2×2
# Gráfico 1: distribución de longitud
hist(genes_df$longitud, main = "Distribución de longitud",
xlab = "Longitud (pb)", col = "steelblue", border = "white")
# Gráfico 2: distribución de GC content
hist(genes_df$gc_content, main = "Distribución GC content",
xlab = "GC content", col = "forestgreen", border = "white")
# Gráfico 3: dispersión longitud vs GC
plot(genes_df$longitud, genes_df$gc_content,
main = "Longitud vs GC content",
xlab = "Longitud (pb)", ylab = "GC content",
col = "steelblue", pch = 16, cex = 0.8)
# Gráfico 4: boxplot expresión por condición
boxplot(expresion ~ condicion, data = genes_df,
main = "Expresión por condición",
col = c("steelblue","firebrick"), border = "gray30")
par(mfrow = c(1, 1)) # restablecer
Para guardar un gráfico, abrís un “dispositivo” gráfico antes de
generarlo y lo cerrás con dev.off() al terminar.
# ── PNG — para presentaciones y web ─────────────────────────────────────────
png(
filename = "longitud_vs_gc.png",
width = 1800, # píxeles de ancho
height = 1400, # píxeles de alto
res = 300 # resolución (dpi) — 300 es estándar para publicación
)
plot(
genes_df$longitud, genes_df$gc_content,
main = "Longitud génica vs Contenido GC",
xlab = "Longitud (pb)", ylab = "GC content",
col = "steelblue", pch = 16, cex = 0.8
)
dev.off() # CRÍTICO: cerrar el dispositivo para que el archivo se escriba
# ── PDF — para publicaciones científicas ─────────────────────────────────────
pdf(
file = "longitud_vs_gc.pdf",
width = 7, # pulgadas
height = 5
)
plot(
genes_df$longitud, genes_df$gc_content,
main = "Longitud génica vs Contenido GC",
xlab = "Longitud (pb)", ylab = "GC content",
col = "steelblue", pch = 16
)
dev.off()
⚠️ Importante: si olvidás llamar a
dev.off(), el archivo queda abierto e incompleto, y los siguientes gráficos que generes se seguirán escribiendo en ese archivo en lugar de mostrarse en RStudio. Si eso ocurre, ejecutádev.off()varias veces hasta que la consola diga"null device"o1.
ggplot2 funciona de forma completamente diferente a R base. En lugar
de una función única que hace todo, construís un gráfico por
capas, combinando componentes con el operador
+.
La idea central es la gramática de gráficos (Grammar of Graphics): todo gráfico se puede describir como la combinación de:
| Componente | Función | Qué hace |
|---|---|---|
| Datos | ggplot(data) |
Define el data frame de origen |
| Aesthetics | aes() |
Mapea variables a propiedades visuales (x, y, color, tamaño…) |
| Geometría | geom_*() |
Define el tipo de gráfico (puntos, barras, cajas…) |
| Escalas | scale_*() |
Controla ejes, colores, tamaños |
| Tema | theme_*() |
Controla la apariencia general |
| Facetas | facet_*() |
Divide el gráfico en paneles por grupo |
# Instalar ggplot2 (solo la primera vez)
install.packages("ggplot2")
# Cargar en cada sesión
library(ggplot2)
geom_point()Rehacemos el primer gráfico de R base y comparamos:
# Versión mínima
ggplot(data = genes_df, aes(x = longitud, y = gc_content)) +
geom_point()
Inmediatamente más prolijo que R base, sin configurar nada. Ahora lo personalizamos:
ggplot(genes_df, aes(x = longitud, y = gc_content, color = condicion)) +
geom_point(size = 2, alpha = 0.7) + # alpha: transparencia (0-1)
scale_color_manual(values = c(Control = "steelblue", KO = "firebrick")) +
labs(
title = "Longitud génica vs Contenido GC",
x = "Longitud del gen (pb)",
y = "Contenido GC",
color = "Condición"
) +
theme_bw() # tema con fondo blanco y grilla gris
💡 Diferencia clave con R base: en ggplot2 el color por grupo se define dentro de
aes()comocolor = condicion. ggplot2 se encarga automáticamente de asignar colores y generar la leyenda. En R base había que hacerlo manualmente concolores[genes_df$condicion]y luego agregar la leyenda conlegend().
ggplot(genes_df, aes(x = longitud, y = gc_content)) +
geom_point(color = "steelblue", size = 2, alpha = 0.7) +
geom_smooth(method = "lm", formula = y ~ x,
color = "firebrick", se = TRUE) + # se=TRUE: banda de confianza
labs(
title = "Longitud génica vs Contenido GC",
x = "Longitud del gen (pb)",
y = "Contenido GC"
) +
theme_bw()
geom_histogram()ggplot(genes_df, aes(x = longitud)) +
geom_histogram(bins = 15, fill = "steelblue", color = "white") +
labs(
title = "Distribución de longitudes génicas",
x = "Longitud (pb)",
y = "Frecuencia"
) +
theme_bw()
ggplot(genes_df, aes(x = longitud)) +
geom_histogram(aes(y = after_stat(density)), # eje Y como densidad
bins = 15, fill = "lightsteelblue", color = "white") +
geom_density(color = "firebrick", linewidth = 1) +
labs(
title = "Distribución de longitudes génicas",
x = "Longitud (pb)",
y = "Densidad"
) +
theme_bw()
facet_wrap()facet_wrap() es una de las características más potentes
de ggplot2: divide automáticamente el gráfico en paneles según una
variable categórica.
ggplot(genes_df, aes(x = longitud, fill = condicion)) +
geom_histogram(bins = 12, color = "white") +
scale_fill_manual(values = c(Control = "steelblue", KO = "firebrick")) +
facet_wrap(~ condicion) + # un panel por condición
labs(
title = "Distribución de longitudes por condición",
x = "Longitud (pb)",
y = "Frecuencia",
fill = "Condición"
) +
theme_bw() +
theme(legend.position = "none") # leyenda redundante con las etiquetas
geom_boxplot()ggplot(genes_df, aes(x = condicion, y = expresion, fill = condicion)) +
geom_boxplot(outlier.color = "gray40", outlier.size = 1.5) +
scale_fill_manual(values = c(Control = "steelblue", KO = "firebrick")) +
labs(
title = "Expresión génica por condición",
x = "Condición",
y = "Expresión (CPM)"
) +
theme_bw() +
theme(legend.position = "none")
En ggplot2 es trivial agregar una capa de puntos sobre el boxplot — algo muy tedioso en R base. Esto es buena práctica cuando el número de observaciones es pequeño.
ggplot(genes_df, aes(x = condicion, y = expresion, fill = condicion)) +
geom_boxplot(outlier.shape = NA, # ocultar outliers del boxplot...
alpha = 0.6) + # porque los mostraremos como puntos
geom_jitter(width = 0.15, size = 1.5, alpha = 0.5) +
scale_fill_manual(values = c(Control = "steelblue", KO = "firebrick")) +
labs(
title = "Expresión génica por condición",
x = "Condición",
y = "Expresión (CPM)"
) +
theme_bw() +
theme(legend.position = "none")
💡 Nota:
geom_jitter()agrega un pequeño desplazamiento aleatorio horizontal a los puntos para que no se solapen.width = 0.15controla cuánto se dispersan lateralmente.
geom_bar() y
geom_col()# geom_bar() cuenta automáticamente las observaciones por categoría
ggplot(genes_df, aes(x = cromosoma, fill = condicion)) +
geom_bar(position = "dodge") + # "dodge": barras una al lado de otra
scale_fill_manual(values = c(Control = "steelblue", KO = "firebrick")) +
labs(
title = "Número de genes por cromosoma y condición",
x = "Cromosoma",
y = "Número de genes",
fill = "Condición"
) +
theme_bw()
ggplot2 incluye varios temas para cambiar la apariencia global del gráfico:
# Gráfico base para comparar temas
g_base <- ggplot(genes_df, aes(x = longitud, y = gc_content, color = condicion)) +
geom_point(size = 2, alpha = 0.7) +
scale_color_manual(values = c(Control = "steelblue", KO = "firebrick")) +
labs(x = "Longitud (pb)", y = "GC content", color = "Condición")
g_base + theme_bw() # fondo blanco con grilla — muy usado en publicaciones
g_base + theme_classic() # ejes clásicos sin grilla — muy usado en publicaciones
g_base + theme_minimal() # minimalista — bueno para presentaciones
g_base + theme_dark() # fondo oscuro — bueno para pantallas
💡 Para publicaciones científicas:
theme_classic()ytheme_bw()son los más utilizados porque tienen fondos blancos sin ruido visual.theme_minimal()es una buena opción para presentaciones.
ggsave()En ggplot2 se usa ggsave() en lugar de
png() / dev.off(). Es más simple: guardás el
último gráfico generado, o uno específico:
# Guardar el último gráfico generado
ggsave(
filename = "expresion_por_condicion.png",
width = 8, # pulgadas
height = 6,
dpi = 300 # resolución para publicación
)
# Guardar un gráfico guardado en un objeto
mi_grafico <- ggplot(genes_df, aes(x = condicion, y = expresion, fill = condicion)) +
geom_boxplot() +
theme_bw()
ggsave("boxplot_expresion.pdf", plot = mi_grafico, width = 6, height = 5)
En esta sección aplicamos lo aprendido a situaciones más cercanas a lo que encontrarás en bioinformática real.
set.seed(7)
n <- 120
# Datos de expresión génica con 3 condiciones y 2 réplicas cada una
datos_exp <- data.frame(
gen = rep(paste0("Gen_", sprintf("%02d", 1:20)), times = 6),
condicion = factor(rep(rep(c("WT","KO_A","KO_B"), each = 20), times = 2),
levels = c("WT","KO_A","KO_B")),
replica = factor(rep(c("Rep1","Rep2"), each = 60)),
expresion = c(
rnorm(20, mean = 8.0, sd = 1.2), # WT Rep1
rnorm(20, mean = 8.0, sd = 1.2), # WT Rep2
rnorm(20, mean = 5.5, sd = 1.5), # KO_A Rep1
rnorm(20, mean = 5.5, sd = 1.5), # KO_A Rep2
rnorm(20, mean = 9.2, sd = 1.0), # KO_B Rep1
rnorm(20, mean = 9.2, sd = 1.0) # KO_B Rep2
),
longitud_kb = round(runif(n, 0.5, 8.0), 2),
gc_content = round(runif(n, 0.38, 0.68), 3)
)
# Datos de frecuencias alélicas en 4 poblaciones
snps <- c("rs334","rs1799945","rs4988235","rs7903146","rs1426654")
poblaciones <- c("AFR","EUR","EAS","AMR")
freq_df <- data.frame(
snp = rep(snps, times = length(poblaciones)),
poblacion = factor(rep(poblaciones, each = length(snps))),
frecuencia = c(
0.19, 0.00, 0.07, 0.18, 0.09, # AFR
0.00, 0.01, 0.68, 0.30, 0.85, # EUR
0.00, 0.00, 0.01, 0.26, 0.71, # EAS
0.02, 0.00, 0.45, 0.28, 0.76 # AMR
)
)
ggplot(datos_exp[datos_exp$replica == "Rep1", ],
aes(x = longitud_kb)) +
geom_histogram(bins = 12, fill = "steelblue", color = "white", alpha = 0.85) +
geom_density(aes(y = after_stat(count) * 0.6),
color = "firebrick", linewidth = 1) +
labs(
title = "Distribución de longitudes génicas",
x = "Longitud del gen (kb)",
y = "Frecuencia"
) +
theme_classic()
ggplot(datos_exp, aes(x = condicion, y = expresion, fill = condicion)) +
geom_boxplot(outlier.shape = NA, alpha = 0.7) +
geom_jitter(width = 0.15, size = 1.2, alpha = 0.4) +
scale_fill_manual(values = c(WT = "steelblue",
KO_A = "firebrick",
KO_B = "forestgreen")) +
labs(
title = "Expresión génica por condición experimental",
x = "Condición",
y = "Expresión (log2 CPM)"
) +
theme_classic() +
theme(legend.position = "none")
ggplot(datos_exp[datos_exp$replica == "Rep1", ],
aes(x = longitud_kb, y = gc_content, color = condicion)) +
geom_point(size = 2.5, alpha = 0.8) +
geom_smooth(method = "lm", formula = y ~ x,
se = FALSE, linewidth = 0.8) +
scale_color_manual(values = c(WT = "steelblue",
KO_A = "firebrick",
KO_B = "forestgreen")) +
labs(
title = "Contenido GC en función de la longitud génica",
x = "Longitud del gen (kb)",
y = "Contenido GC",
color = "Condición"
) +
theme_bw()
ggplot(freq_df, aes(x = snp, y = frecuencia, fill = poblacion)) +
geom_col(position = "dodge", color = "white", linewidth = 0.3) +
scale_fill_manual(values = c(AFR = "#E05A4B",
EUR = "steelblue",
EAS = "forestgreen",
AMR = "darkorange")) +
scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
labs(
title = "Frecuencias alélicas por SNP y población",
x = "SNP",
y = "Frecuencia del alelo alternativo",
fill = "Población"
) +
theme_bw() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
facet_wrap()ggplot(datos_exp, aes(x = condicion, y = expresion, fill = condicion)) +
geom_boxplot(outlier.shape = NA, alpha = 0.7) +
geom_jitter(width = 0.15, size = 1, alpha = 0.4) +
scale_fill_manual(values = c(WT = "steelblue",
KO_A = "firebrick",
KO_B = "forestgreen")) +
facet_wrap(~ replica, labeller = label_both) +
labs(
title = "Expresión génica por condición — comparación entre réplicas",
x = "Condición",
y = "Expresión (log2 CPM)"
) +
theme_bw() +
theme(legend.position = "none")
💡 Nota: este gráfico con
facet_wrap()permite verificar de un vistazo si las dos réplicas biológicas son consistentes entre sí — un control de calidad estándar antes de cualquier análisis de expresión diferencial.
Usá el data frame genes_df creado al inicio del
módulo.
a) Generá un histograma del gc_content
con 12 barras, color "forestgreen" y un título
descriptivo.
b) Generá un boxplot que compare la expresión
(expresion) entre cromosomas (cromosoma).
Asigná un color diferente a cada cromosoma. ¿Hay algún cromosoma con una
distribución notablemente diferente?
c) Generá una figura con par(mfrow) que
muestre en una cuadrícula 2×2 los siguientes gráficos: - Histograma de
longitud - Histograma de expresion - Boxplot
de expresion ~ condicion - Dispersión de
longitud vs expresion
d) Guardá la figura del inciso c) como archivo PNG de 1800×1400 píxeles a 300 dpi.
Usá los data frames datos_exp y freq_df
creados en la Parte 3.
a) Rehacé el histograma del Ejercicio 1a usando
ggplot2. Agregá facet_wrap(~ condicion) para comparar la
distribución de gc_content entre condiciones.
b) Generá un gráfico de dispersión de
longitud_kb vs expresion coloreado por
condicion. Agregá una línea de tendencia con
geom_smooth(method = "lm"). Usá
theme_classic().
c) Generá un boxplot de expresion por
condicion que muestre también los puntos individuales con
geom_jitter(). Diferenciá las réplicas usando
shape = replica dentro de aes().
d) Con freq_df, generá un gráfico de
barras apiladas (position = "stack") que muestre la
frecuencia alélica de cada SNP dividida por población. ¿En qué SNP hay
mayor diferencia entre poblaciones?
e) Guardá cualquiera de los gráficos anteriores como
PDF de 8×6 pulgadas usando ggsave().
Módulo 3 elaborado para Bioinformática — Licenciatura en
Genética
IUCBC – CIMETSA · Córdoba, Argentina · 2026
Dr. Danilo G. Ceschin