library(ggplot2); library(readxl); library(moments)
R Studio Basics
R Studio fácil y sencillo para principiantes
Básicos de R
Todo lo de las ayudantías parte de vectores, factores y data frames; con esta tabla se resuelve casi cualquier ítem de la Ayudantía 1.
R como calculadora, objetos y ayuda
| Qué quiero | Código | Resultado |
|---|---|---|
| Sumar, restar, multiplicar, dividir | 8 - 2, (9 + 3) * 7, 4 / 25 |
6, 84, 0.16 |
| Potencia | (4 - 7)^3 |
-27 |
| Raíz cuadrada | sqrt(81) |
9 |
| Raíz cúbica (también de negativos) | 27^(1/3), -8^(1/3) |
3, -2 |
| Raíz de un negativo | sqrt(-8), (-8)^(1/3) |
NaN (no existe en los reales) |
| e y potencias de e | exp(1), exp(2) |
2.718, 7.389 |
| Logaritmo natural (ln) | log(3) |
1.099 |
| Logaritmo en base 10 | log10(1000) o log(1000, base = 10) |
3 |
| Redondear a la décima | round(5.67, 1) |
5.7 |
| Redondear hacia arriba / abajo | ceiling(10.2), floor(10.8) |
11, 10 |
| Valor absoluto | abs(-8) |
8 |
| Suma y producto de un vector | sum(x), prod(x) |
|
| Guardar en un objeto | x <- 1 + 1 (guarda lo de la derecha en lo de la izquierda) |
|
| Ver los objetos creados | ls() u objects() |
|
| Borrar un objeto / todos | rm(x) / rm(list = ls()) |
|
| Ayuda de una función | help(sum) o ?sum |
|
| Versión de R | R.version.string |
|
| Carpeta de trabajo | getwd(); cambiarla con setwd("C:/Users/.../ExploratorioII") |
|
| Paquetes | install.packages("ggplot2") una vez; library(ggplot2) en cada sesión |
Ojo con -8^(1/3): R calcula primero la potencia y luego el signo, por eso da -2. (-8)^(1/3) da NaN. R distingue mayúsculas: Edad y edad son objetos distintos.
Vectores e indexación
| Qué quiero | Código | Ejemplo |
|---|---|---|
| Crear vector | c() |
edades <- c(21, 19, 23, 20) |
| Vector con nombres | c(nombre = valor) |
notas <- c(Ana = 5.2, Pedro = 6.1) |
| Cantidad de elementos | length(x) |
length(edades) |
| Tipo del objeto | class(x) |
"numeric", "character", "factor" |
| Elemento en posición i | x[i] |
edades[1] |
| Varias posiciones | x[c(...)] |
edades[c(1, 3, 5)] |
| Rango de posiciones | x[a:b] |
edades[2:5] |
| Todo menos | x[-i] |
edades[-c(1, 4)] |
| Por nombre | x["nombre"] |
notas[c("Ana", "Diego")] |
| Nombres | names(x) |
names(notas) |
| Ordenar valores | sort(x) |
sort(x, decreasing = TRUE) |
| Posiciones para ordenar | order(x) |
nombres[order(edad, decreasing = TRUE)] |
Condiciones y filtros
- Comparadores:
>,>=,<,<=,==,!=; devuelvenTRUE/FALSEpor elemento. - Combinar:
&(y),|(o). Se repite la variable:notas[notas >= 4 & notas <= 6]. - Filtrar otro vector con la condición:
nombres[notas >= 5]. - Contar cuántos cumplen:
sum(notas >= 4)(cadaTRUEvale 1). - Valores únicos:
unique(x); cuántos distintos:length(unique(x)); repetidos:duplicated(x).
Estadísticos básicos
| Función | Calcula |
|---|---|
mean(x) |
Promedio |
median(x) |
Mediana |
min(x), max(x), range(x) |
Mínimo, máximo, ambos |
sd(x), var(x) |
Desviación estándar, varianza |
sum(x), prod(x) |
Suma, producto |
quantile(x, probs = c(0.1, 0.9)) |
Percentiles (probs entre 0 y 1) |
IQR(x) |
Rango intercuartil Q3 − Q1 |
summary(x) |
Mín, Q1, mediana, media, Q3, máx |
skewness(x), kurtosis(x) |
Asimetría y curtosis (paquete moments) |
Con datos faltantes se agrega na.rm = TRUE: mean(x, na.rm = TRUE).
Moda: library(modeest); mfv(x) (datos sin agrupar o cualitativos; puede devolver varias modas). Sin paquete: names(which.max(table(x))). Con datos agrupados se informa la clase modal, el intervalo con mayor fᵢ: names(which.max(table(intervalos))). Se dice "el intervalo [32, 39) concentra más observaciones", nunca "la moda es 35".
Coeficiente de variación: 100 * sd(x) / mean(x) (no hay función propia). Varianza en unidades al cuadrado; la DE en la unidad de la variable.
Fórmulas: centro y dispersión
\bar{x} = \frac{x_1 + x_2 + \cdots + x_n}{n} = \frac{1}{n}\sum_{i=1}^{n} x_iMe = x_{\left(\frac{n+1}{2}\right)} \;\; (n \text{ impar}) \qquad Me = \frac{x_{\left(\frac{n}{2}\right)} + x_{\left(\frac{n}{2}+1\right)}}{2} \;\; (n \text{ par, datos ordenados})s^2 = \frac{\sum_{i=1}^{n} (x_i - \bar{x})^2}{n - 1} \qquad s = \sqrt{s^2}- La media usa todas las observaciones, por eso un dato extremo la mueve: agregar un paciente con IMC 50 kg/m² la sube. La mediana es más robusta.
- Mismo centro no es mismo comportamiento: A = {9, 10, 10, 10, 11} y B = {1, 5, 10, 15, 19} tienen x̄ = 10, pero B es mucho más disperso.
- Se eleva al cuadrado porque las desviaciones se cancelan: (−2) + (−1) + 0 + 1 + 2 = 0, en cambio 4 + 1 + 0 + 1 + 4 = 10.
var()divide por n − 1 (varianza muestral). - La DE no significa que todos los datos estén exactamente a ±s de la media: es una distancia promedio.
- Moda: un conjunto puede ser unimodal, bimodal, multimodal o no tener moda. 5, 6, 7, 8, 8, 9, 10, 10, 11 tiene dos modas (8 y 10).
- Clase modal: con intervalos de igual amplitud es la barra más alta del histograma. Si las amplitudes son distintas se compara la densidad de frecuencia (fᵢ / amplitud), no la altura.
Factores, matrices, data frames y listas
- Factor nominal:
factor(x, levels = c("Soltero", "Casado", "Viudo")). - Factor ordinal:
factor(x, levels = c("basica", "media", "superior"), ordered = TRUE). - Categorías del factor:
levels(f); cuántas:nlevels(f).c("Mujer", "Hombre")es texto (character);factor(c("Mujer", "Hombre"))es factor y tiene levels. - Matriz desde cero:
matrix(1:6, nrow = 2)(se llena por columnas;byrow = TRUEpara llenar por filas). - Matriz pegando vectores:
cbind(edad, notas)(como columnas) orbind(edad, notas)(como filas). - Dimensiones:
dim(m),nrow(m),ncol(m); nombres:colnames(m) <- c("Edad", "Nota"),rownames(m) <- nombres. - Acceder:
m[fila, columna],m[, "Edad"](columna entera),m[c("Pedro", "Luis"), "Nota"]. - Data frame:
datos <- data.frame(ID, Edad, Sexo, Peso); columna condatos$Edad; fila (un sujeto) condatos[2, ]; celda condatos[3, 4]; variables connames(datos); estructura y tipo de cada columna constr(datos). - Filtrar filas:
iris[iris$Species == "setosa", 1:4]. - Lista:
persona <- list(nombre = "Ana", edad = 25, notas = c(5.5, 6.0)); acceder conpersona$notasopersona[["nombre"]].
| Objeto | Qué guarda | Ejemplo |
|---|---|---|
| Vector | Valores del mismo tipo | c(10, 20, 30) |
| Factor | Categorías (variable cualitativa) | sexo, estado civil |
| Matriz | Filas × columnas, todo del mismo tipo | cbind(edad, notas) |
| data.frame | Filas = sujetos, columnas = variables de distinto tipo | una base de datos |
| Lista | Objetos de distinto tipo y largo | la información de una persona |
Importar datos
library(readxl)
getwd() # ¿en qué carpeta está trabajando R?
data <- read_excel("colesterol.xlsx") # Excel (.xls o .xlsx)
data <- read.csv("Libro5.csv") # CSV
View(data); head(data); str(data) # ver la base y el tipo de cada variable
dim(data); nrow(data); ncol(data) # sujetos y variables
names(data); summary(data) # nombres y resumen inicial de cada variable
data("iris") # base que viene con R
data$Unit_price <- as.numeric(data$Unit_price) # si quedó como texto
data$sexo <- factor(data$sexo) # texto -> factorTabular: frecuencias y tablas de contingencia
| Quiero | Código |
|---|---|
| Frecuencia absoluta fᵢ | table(data$sexo) |
| Frecuencia relativa rᵢ = fᵢ / n | prop.table(table(data$sexo)) |
| Porcentaje con 1 decimal | round(100 * prop.table(table(data$sexo)), 1) |
| Frecuencia acumulada Fᵢ (ordinales o intervalos) | cumsum(table(data$actividad)) |
| Categoría más frecuente | names(which.max(table(data$sexo))) |
| Tabla de contingencia (dos variables) | t1 <- table(data$sexo, data$actividad) |
| Agregar totales de fila y columna | addmargins(t1) |
| % dentro de cada fila (filas suman 100) | round(100 * prop.table(t1, margin = 1), 1) |
| % dentro de cada columna | round(100 * prop.table(t1, margin = 2), 1) |
| % sobre el total | round(100 * prop.table(t1), 1) |
| Tabla como data frame (para ggplot o torta) | tabla <- as.data.frame(table(data$sexo)) (columnas Var1, Freq) |
table() sobre una variable cuantitativa con muchos valores da una tabla inútil: hay que agruparla en intervalos con cut() (ver Histogramas). Para que una ordinal salga en su orden, convertirla antes: factor(data$actividad, levels = c("Baja", "Moderada", "Alta"), ordered = TRUE).
Medidas por variable y por grupo (sapply y familia)
Para sacar medidas de muchas variables o de muchos grupos sin repetir código: sapply recorre columnas, tapply/aggregate recorren grupos, y una función propia medidas() junta todo en una tabla.
| Quiero | Función | Ejemplo |
|---|---|---|
| Una medida para cada columna | sapply(df, f) |
sapply(setosa, mean) |
| Solo columnas numéricas | df[sapply(df, is.numeric)] |
sapply(iris[sapply(iris, is.numeric)], mean) |
| Por columnas (2) o filas (1) de una matriz | apply(m, 2, f) |
apply(iris[, 1:4], 2, max) |
| Una variable según grupo | tapply(x, grupo, f) |
tapply(iris$Petal.Length, iris$Species, mean) |
| Igual, pero como data frame | aggregate(y ~ g, data, FUN) |
aggregate(Petal.Length ~ Species, data = iris, FUN = mean) |
| Todas las variables según grupo | aggregate(. ~ g, data, FUN) |
aggregate(. ~ Species, data = iris, FUN = median) |
| Separar una variable por grupo | split(x, grupo) |
split(iris$Petal.Length, iris$Species) |
| Con datos faltantes | argumento extra | sapply(df, mean, na.rm = TRUE) |
La función medidas(): todas las medidas de una vez
Se define una vez y se usa con cualquier variable, base o grupo.
library(moments)
medidas <- function(x) c(
Media = mean(x), Mediana = median(x), Min = min(x), Max = max(x),
DE = sd(x), CV = 100 * sd(x) / mean(x),
Asimetria = skewness(x), Curtosis = kurtosis(x))
medidas(iris$Petal.Length) # una variableCaso 1 — varias variables (como la A6): cada fila es una variable.
setosa <- iris[iris$Species == "setosa", 1:4]
res <- round(t(sapply(setosa, medidas)), 2) # t() pone variables en filas
res
# Media Mediana Min Max DE CV Asimetria Curtosis
# Sepal.Length 5.01 5.0 4.3 5.8 0.35 7.04 0.12 2.65
# Petal.Length 1.46 1.5 1.0 1.9 0.17 11.88 0.10 3.80 ...Caso 2 — una variable según grupo (como la Tarea 1: cuantitativa vs cualitativa): cada fila es un grupo.
res <- round(t(sapply(split(iris$Petal.Length, iris$Species), medidas)), 2)
res
# Media Mediana Min Max DE CV Asimetria Curtosis
# setosa 1.46 1.50 1.0 1.9 0.17 11.88 0.10 3.80
# versicolor 4.26 4.35 3.0 5.1 0.47 11.03 -0.59 2.93
# virginica 5.55 5.55 4.5 6.9 0.55 9.94 0.53 2.74
# Tarea 1
t(sapply(split(BASE_TAREA_1$horas_estudio, BASE_TAREA_1$carrera), medidas))Caso 3 — una sola medida por grupo: tapply(x, grupo, sd) o aggregate(x ~ grupo, data, FUN = sd).
Para llevar cualquiera de estas tablas al informe: xtable::xtable(res) (LaTeX) o knitr::kable(res) (R Markdown).
Cómo leer las medidas
- CV < 15 % datos homogéneos; > 30 % muy dispersos (Tarea 1: horas de estudio CV = 31.4 %).
- Asimetría ≈ 0 simétrica; > 0 cola a la derecha (media > mediana); < 0 cola a la izquierda.
- Curtosis (
moments) ≈ 3 mesocúrtica; > 3 leptocúrtica (colas más pesadas que la normal); < 3 platicúrtica (colas menos pesadas). No se interpreta como "qué tan puntiaguda".
Soluciones
Cada ayudantía agrega una pieza: vectores (A1), barras (A2–A3), tabla de frecuencias + histograma (A4), interpretación de medidas con airquality (A5) y boxplot + medidas + reporte en LaTeX (A6).
| Ayudantía | Fecha | Base | Tema | Funciones clave |
|---|---|---|---|---|
| A1 | 19 ago | vectores a mano | Vectores, filtros, matrices | c, [ ], sort, order, cbind, rownames |
| A2 | 26 ago | penguins.xlsx | Frecuencias y gráfico de barras | table, barplot, geom_bar, labs |
| A3 | 2 sep | SuperMarket.xlsx | Barras, %, torta, dos variables | prop.table, after_stat, coord_polar, position = "fill" |
| A4 | 9 sep | SuperMarket.xlsx | Tabla de frecuencias e histograma | cut, table, cumsum, hist, geom_histogram |
| A5 | 23 sep | airquality | Interpretar medidas y gráficos | na.omit, quantile, sd, boxplot.stats |
| A6 | 30 sep | iris | Descriptivo, boxplot, medidas por grupo | summary, geom_boxplot, sapply, skewness |
A1 — Vectores y matrices (19 ago)
Ejercicio 1 — edades <- c(21, 19, 23, 20, 22, 18, 25, 21)
length(edades) # a) 8 estudiantes
class(edades) # b) "numeric"
edades[1] # c) primera edad: 21
edades[c(1, 3, 5)] # posiciones 1, 3, 5: 21 23 22
edades[2:5] # de la 2 a la 5: 19 23 20 22
edades[-2] # d) todas menos la segunda
edades[-c(1, 4)] # todas menos la primera y la cuarta
sort(edades) # e) creciente: 18 19 20 21 21 22 23 25
sort(edades, decreasing = TRUE) # decreciente: 25 23 22 21 21 20 19 18Ejercicio 2 — notas <- c(Ana = 5.2, Pedro = 6.1, Camila = 4.8, Diego = 5.7)
names(notas) # a) "Ana" "Pedro" "Camila" "Diego"
class(notas) # b) "numeric" (los nombres no cambian la clase)
notas[2] # c) por posición: Pedro 6.1
notas["Camila"] # por nombre: 4.8
notas[c("Ana", "Diego")] # dos a la vez: 5.2 5.7
sort(notas) # d) menor a mayor: Camila 4.8, Ana 5.2, Diego 5.7, Pedro 6.1
order(notas) # e) 3 1 4 2
notas[order(notas, decreasing = TRUE)] # f) mayor a menor: Pedro, Diego, Ana, Camilae) order() no devuelve notas, devuelve posiciones: "la menor está en la posición 3 (Camila), luego la 1 (Ana), la 4 (Diego) y la 2 (Pedro)".
Ejercicio 3
nombres <- c("Ana", "Pedro", "Camila", "Diego", "Sofía", "Esteban", "Luis")
edad <- c(23, 19, 25, 21, 22, 24, 17)
notas <- c(5.5, 4.2, 6.1, 3.8, 5.9, 6.5, 4.2)
length(nombres) # a) 7 estudiantes
nombres[order(edad)] # b) por edad, menor a mayor:
# Luis Pedro Diego Sofía Ana Esteban Camila
sort(edad) # c) 17 19 21 22 23 24 25
notas > 4 # d) TRUE/FALSE por cada nota
notas >= 5
notas == 6.1
notas != 4.2
notas[notas >= 5] # e) 5.5 6.1 5.9 6.5
notas[notas < 4] # 3.8
notas[notas >= 4 & notas <= 6] # entre 4 y 6 incluidos: 5.5 4.2 5.9 4.2
sum(notas >= 4) # f) 6 estudiantes con nota >= 4
sum(notas < 4) # 1 estudiante con nota < 4
nombres[notas >= 5] # g) Ana Camila Sofía Esteban
nombres[notas < 4] # h) Diego
summary(notas); sd(notas) # i) ver abajo
unique(notas) # j) 5.5 4.2 6.1 3.8 5.9 6.5
length(unique(notas)) # k) 6 notas distintas
duplicated(notas) # l) TRUE solo en la posición 7 (el segundo 4.2)
nombres[order(notas, decreasing = TRUE)] # m) Esteban Camila Sofía Ana Pedro Luis Diegoi) Respuesta modelo: "Las 7 notas van de 3,8 a 6,5. El promedio es 5,17 y la mediana 5,5: la mitad del grupo tiene 5,5 o más. La desviación estándar es 1,08 puntos (CV ≈ 21 %, variabilidad moderada). Solo Diego reprueba (3,8)."
Ejercicio 4
Informacion <- cbind(edad, notas) # a) columnas = variables
dim(Informacion); nrow(Informacion); ncol(Informacion) # b) 7 filas, 2 columnas
colnames(Informacion) <- c("Edad", "Nota") # c)
rownames(Informacion) <- nombres # d)
Informacion["Sofía", "Edad"] # e) 22
Informacion["Sofía", "Nota"] # 5.9
Informacion["Diego", ] # toda la fila: Edad 21, Nota 3.8
Informacion[, "Edad"] # todas las edades
Informacion[, "Nota"] # todas las notas
Informacion[c("Pedro", "Camila", "Luis"), "Edad"] # 19 25 17
Informacion[c("Pedro", "Camila", "Luis"), "Nota"] # 4.2 6.1 4.2Regla de la matriz: [fila, columna]; dejar un lado vacío significa "todas". rbind() pondría las variables como filas.
A2 — Pingüinos: frecuencias y barras (26 ago)
1. Importar
library(readxl); library(ggplot2)
penguins <- read_excel("penguins.xlsx") # 333 filas, 4 columnas
str(penguins); head(penguins)2. Clasificar variables
| Variable | Tipo | Nota |
|---|---|---|
| Especies | Cualitativa nominal | Adelie, Chinstrap, Gentoo |
| Isla | Cualitativa nominal | Biscoe, Dream, Torgersen |
| Sexo | Cualitativa nominal (dicotómica) | female, male |
| Crias | Cuantitativa discreta | 0 a 3; "No_aplica" en machos, por eso R la lee como texto |
penguins$Crias_num <- as.numeric(ifelse(penguins$Crias == "No_aplica", NA, penguins$Crias))
summary(penguins$Crias_num) # solo hembras: media 1,35 crías3a. Pingüinos por especie
table(penguins$Especies) # Adelie 146, Chinstrap 68, Gentoo 119
round(100 * prop.table(table(penguins$Especies)), 1) # 43.8 % 20.4 % 35.7 %
barplot(table(penguins$Especies), main = "Pingüinos por especie",
xlab = "Especie", ylab = "Frecuencia", col = c("orange", "purple", "cyan4"))Respuesta: Adelie es la especie con más observaciones (146, 43,8 %); Chinstrap la con menos (68). Gráfico adecuado: barras, porque es cualitativa.
3b. Pingüinos por isla
table(penguins$Isla) # Biscoe 163, Dream 123, Torgersen 47
names(which.max(table(penguins$Isla))) # "Biscoe"
names(which.min(table(penguins$Isla))) # "Torgersen"Respuesta: la mayor cantidad se registró en Biscoe (163) y la menor en Torgersen (47).
3c. Sexo
table(penguins$Sexo) # female 165, male 168
prop.table(table(penguins$Sexo)) # 49.5 % y 50.5 %
barplot(table(penguins$Sexo), col = c("pink", "skyblue"), main = "Sexo")Respuesta: sí, es prácticamente igual (165 hembras y 168 machos, ≈ 50 % cada uno).
3d. ggplot2 paso a paso
ggplot(penguins, aes(x = Especies)) # 1) base + eje X (lienzo vacío)
ggplot(penguins, aes(x = Especies)) +
geom_bar() # 2) barras
ggplot(penguins, aes(x = Especies)) +
geom_bar(fill = "steelblue") + # 5) relleno de un color
labs(title = "Cantidad de pingüinos por especie", # 3) título
x = "Especie", y = "N° de pingüinos") # 4) ejes
# relleno distinto por especie
ggplot(penguins, aes(x = Especies, fill = Especies)) + geom_bar() +
scale_fill_manual(values = c("darkorange", "purple", "cyan4"))La altura de cada barra es la frecuencia absoluta: el número de pingüinos de esa especie.
Extra (barras con dos variables): ggplot(penguins, aes(x = Isla, fill = Especies)) + geom_bar() muestra que Gentoo solo vive en Biscoe y Chinstrap solo en Dream.
A3 — SuperMarket: variables cualitativas (2 sep)
library(readxl); library(ggplot2); library(paletteer)
SuperMarket <- read_excel("SuperMarket.xlsx") # 1000 transacciones
paleta <- paletteer_d("dichromat::Categorical_12")Regla para dos variables: la de x es el grupo; la de fill es la que se distribuye dentro de cada grupo.
1. Transacciones por tipo de cliente
table(SuperMarket$Customer_type) # Member 565, Normal 435
ggplot(SuperMarket, aes(x = Customer_type, fill = Customer_type)) +
geom_bar() + scale_fill_manual(values = paleta) +
geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) +
labs(x = "Tipo de cliente", y = "N° de transacciones",
title = "Transacciones por tipo de cliente") +
theme(legend.position = "none")Respuesta: 565 transacciones de clientes Member y 435 de Normal.
2. Porcentaje por tipo de cliente
round(100 * prop.table(table(SuperMarket$Customer_type)), 1) # 56.5 % y 43.5 %
ggplot(SuperMarket, aes(x = Customer_type, fill = Customer_type)) +
geom_bar(aes(y = after_stat(count / sum(count)))) +
geom_text(stat = "count",
aes(y = after_stat(count / sum(count)),
label = scales::percent(after_stat(count / sum(count)), accuracy = 0.1)),
vjust = -0.5) +
scale_fill_manual(values = paleta) +
scale_y_continuous(labels = scales::percent, expand = expansion(mult = c(0, 0.1))) +
labs(x = "Tipo de cliente", y = "Porcentaje de transacciones")Respuesta: el 56,5 % de las transacciones son de clientes Member y el 43,5 % de Normal.
3. Medios de pago (torta)
table(SuperMarket$Payment) # Cash 344, Credit card 311, Ewallet 345
tabla1 <- as.data.frame(table(SuperMarket$Payment))
tabla1$porcentaje <- tabla1$Freq / sum(tabla1$Freq)
ggplot(tabla1, aes(x = "", y = Freq, fill = Var1)) +
geom_bar(stat = "identity") + coord_polar(theta = "y") +
geom_text(aes(label = scales::percent(porcentaje, accuracy = 0.1)),
position = position_stack(vjust = 0.5)) +
scale_fill_manual(values = paleta) +
labs(title = "Transacciones por medio de pago", fill = "Medio de pago") +
theme_void()Respuesta: se usan 3 medios. Distribución pareja: Ewallet 34,5 %, Cash 34,4 %, Credit card 31,1 %.
4. Combinación género × medio de pago
tabla2 <- table(SuperMarket$Gender, SuperMarket$Payment)
addmargins(tabla2)
# Cash Credit card Ewallet Sum
# Female 205 183 183 571
# Male 139 128 162 4295. Comparar directamente hombres y mujeres: barras lado a lado
ggplot(SuperMarket, aes(x = Payment, fill = Gender)) +
geom_bar(position = "dodge") +
geom_text(stat = "count", aes(label = after_stat(count)),
position = position_dodge(width = 0.9), vjust = -0.5) +
labs(x = "Medio de pago", y = "N° de transacciones", fill = "Género")6. Barras apiladas (medio de pago dentro de cada género)
ggplot(SuperMarket, aes(x = Gender, fill = Payment)) +
geom_bar() + scale_fill_manual(values = paleta) +
geom_text(stat = "count", aes(label = after_stat(count)),
position = position_stack(vjust = 0.5)) +
labs(x = "Género", y = "N° de transacciones", fill = "Medio de pago")7. % de cada medio de pago dentro de cada género (filas suman 100)
round(100 * prop.table(tabla2, margin = 1), 1)
# Cash Credit card Ewallet
# Female 35.9 32.0 32.0
# Male 32.4 29.8 37.8
ggplot(SuperMarket, aes(x = Gender, fill = Payment)) +
geom_bar(position = "fill") + scale_y_continuous(labels = scales::percent) +
labs(x = "Género", y = "Porcentaje", fill = "Medio de pago")Respuesta: las mujeres usan más efectivo (35,9 %); los hombres más Ewallet (37,8 %).
8. Distribución del género dentro de cada medio de pago (columnas suman 100)
round(100 * prop.table(tabla2, margin = 2), 1)
# Cash Credit card Ewallet
# Female 59.6 58.8 53.0
# Male 40.4 41.2 47.0
ggplot(SuperMarket, aes(x = Payment, fill = Gender)) +
geom_bar(position = "fill") + scale_y_continuous(labels = scales::percent)Respuesta: en los 3 medios predominan las mujeres; Ewallet es el más equilibrado (53 % vs 47 %).
9. (Tarea) Categoría de producto según tipo de cliente
round(100 * prop.table(table(SuperMarket$Customer_type, SuperMarket$Product_line), 1), 1)
ggplot(SuperMarket, aes(x = Customer_type, fill = Product_line)) +
geom_bar(position = "fill") + scale_y_continuous(labels = scales::percent) +
scale_fill_manual(values = paleta) +
labs(x = "Tipo de cliente", y = "Porcentaje", fill = "Categoría de producto")Respuesta: se distribuye Product_line respecto del grupo Customer_type. Las 6 categorías están entre 15 % y 19 % en ambos tipos; la distribución es similar. Diferencia mayor: Food and beverages (Member 18,4 % vs Normal 16,1 %).
| Pregunta tipo | Gráfico | position |
|---|---|---|
| Cuántos de cada combinación | Apiladas | "stack" (por defecto) |
| Comparar grupos directamente | Lado a lado | "dodge" |
| Qué % dentro de cada grupo | Apiladas al 100 % | "fill" |
A4 — Tabla de frecuencias e histograma (9 sep)
Ojo: en el Excel Unit_price viene como texto (chr). Hay que convertirla antes de cualquier cálculo.
library(readxl); library(ggplot2)
SuperMarket <- read_excel("SuperMarket.xlsx")
SuperMarket$Unit_price <- as.numeric(SuperMarket$Unit_price)
x <- SuperMarket$Unit_price1. Descripción inicial del precio unitario
n <- length(x) # 1000 observaciones
sort(x) # ordenar
minimo <- min(x); maximo <- max(x) # 10.08 y 99.96
rango <- maximo - minimo # 89.88
mean(x) # 55.67
median(x) # 55.23
names(which.max(table(x))) # moda: "83.77" (aparece 3 veces)Respuesta: media 55,67 ≈ mediana 55,23 → distribución aproximadamente simétrica. La moda (83,77) aparece solo 3 veces en 1000 datos: en una variable continua la moda casi no informa.
2. Número de intervalos (k)
k <- round(1 + 3.22 * log10(n)) # Sturges: 1 + 3.22·3 = 10.66 → k = 11
sqrt(n) # alternativa raíz: 31.6 → 32 (demasiados)k es la cantidad de intervalos (clases) en que se agrupan los datos para la tabla y el histograma.
3. Amplitud
A <- round(rango / k, 2) + 0.01 # 89.88 / 11 = 8.17 → A = 8.18Se suma 0.01 para que el último intervalo alcance a cubrir el máximo.
4. Límites de los intervalos
limites <- round(minimo + (0:k) * A, 2)
# 10.08 18.26 26.44 34.62 42.80 50.98 59.16 67.34 75.52 83.70 91.88 100.06A mano: el primer límite es el mínimo y se suma A cada vez: 10,08 → 18,26 → 26,44 → …
5. Etiquetas
etiquetas <- paste0(format(limites[-length(limites)], nsmall = 2), " - ",
format(limites[-1], nsmall = 2))
# "10.08 - 18.26" "18.26 - 26.44" ...limites[-length(limites)] = todos menos el último (límites inferiores); limites[-1] = todos menos el primero (superiores).
6. Clasificar y tabla de frecuencias
intervalos <- cut(x, breaks = limites, labels = etiquetas,
right = FALSE, include.lowest = TRUE)
f <- table(intervalos); fi <- as.vector(f)
ri <- fi / n; Fi <- cumsum(fi); Ri <- cumsum(ri)
tabla_frecuencia <- data.frame(intervalo = names(f), fi, ri = round(ri, 3),
Fi, Ri = round(Ri, 3))
sum(fi) # 1000 = n ✓| Intervalo | fi | Intervalo | fi |
|---|---|---|---|
| [10.08, 18.26) | 90 | [59.16, 67.34) | 79 |
| [18.26, 26.44) | 101 | [67.34, 75.52) | 98 |
| [26.44, 34.62) | 82 | [75.52, 83.70) | 89 |
| [34.62, 42.80) | 83 | [83.70, 91.88) | 81 |
| [42.80, 50.98) | 89 | [91.88, 100.06] | 110 |
| [50.98, 59.16) | 98 | Total | 1000 |
7. Histograma con hist() y con ggplot2
hist(x, breaks = limites, right = FALSE,
main = "Distribución del precio unitario",
xlab = "Precio unitario", ylab = "Frecuencia", col = "gray")
ggplot(SuperMarket, aes(x = Unit_price)) +
geom_histogram(breaks = limites, closed = "left", fill = "gray", color = "black") +
labs(title = "Distribución del precio unitario", x = "Precio unitario", y = "Frecuencia") +
theme_minimal()Respuesta: todas las barras tienen alturas parecidas (entre 79 y 110): la distribución es aproximadamente uniforme, los precios están repartidos de forma pareja entre 10 y 100, sin una zona que concentre los datos.
8. Repetir con Quantity
x <- SuperMarket$Quantity # ya es numérica
# mismo código: n = 1000, min 1, max 10, rango 9, k = 11, A = 0.83
mean(x); median(x) # 5.51 y 5
names(which.max(table(x))) # moda: 10 (119 veces)Cuidado: Quantity es discreta (solo enteros 1 a 10). Con A = 0,83 queda un intervalo vacío ([5.15, 5.98) no contiene ningún entero) y el histograma muestra un hueco falso. Mejor usar un intervalo por valor:
table(x) # frecuencia de cada cantidad
ggplot(SuperMarket, aes(x = Quantity)) +
geom_histogram(breaks = seq(0.5, 10.5, 1), fill = "gray", color = "black") +
scale_x_continuous(breaks = 1:10)Respuesta: cada cantidad de 1 a 10 aparece entre 85 y 119 veces; también es aproximadamente uniforme. Media 5,51 ≈ mediana 5.
A5 — airquality: interpretar medidas y gráficos (23 sep)
Nueva York, mayo a septiembre de 1973. Los cuadros del PDF salen de na.omit(airquality): 111 días sin datos faltantes. Con la base completa los números no coinciden. Las respuestas siguen el orden del Word de preguntas.
| Variable | Descripción | Unidad | Tipo |
|---|---|---|---|
| Ozone | Concentración de ozono | ppb | Cuantitativa continua |
| Solar.R | Radiación solar | Langley (Ly) | Cuantitativa continua |
| Wind | Velocidad promedio del viento | mph | Cuantitativa continua |
| Temp | Temperatura máxima diaria | °F | Cuantitativa continua |
| Month | Mes (5 a 9) | — | Cualitativa ordinal (como grupo) |
| Day | Día del mes | — | Cuantitativa discreta |
library(ggplot2); library(moments)
data("airquality")
aq <- na.omit(airquality) # n = 111
aq$Mes <- factor(aq$Month, levels = 5:9,
labels = c("Mayo", "Junio", "Julio", "Agosto", "Septiembre"))
# Cuadros 2, 3 y 5: cambiar x por aq$Temp, aq$Wind o aq$Ozone
x <- aq$Temp
round(c(Media = mean(x), Mediana = median(x),
Moda = as.numeric(names(which.max(table(x)))),
Min = min(x), Max = max(x), Rango = max(x) - min(x),
Varianza = var(x), DE = sd(x), CV = 100 * sd(x) / mean(x),
Q1 = quantile(x, 0.25, names = FALSE), Q3 = quantile(x, 0.75, names = FALSE),
RIC = IQR(x), Asimetria = skewness(x), Curtosis = kurtosis(x)), 2)
# Cuadro 4: medidas por mes
t(sapply(split(aq$Wind, aq$Mes), function(x) round(c(
n = length(x), Media = mean(x), Mediana = median(x), DE = sd(x), Var = var(x),
Min = min(x), Q1 = quantile(x, 0.25, names = FALSE),
Q3 = quantile(x, 0.75, names = FALSE), Max = max(x)), 2)))
# Figuras
ggplot(aq, aes(x = Temp)) + geom_histogram(binwidth = 5, fill = "gray", color = "black")
ggplot(aq, aes(x = Mes, y = Temp, fill = Mes)) + geom_boxplot() +
theme(legend.position = "none")
ggplot(aq, aes(x = Ozone, y = "")) + geom_boxplot() +
stat_summary(fun = mean, geom = "point", shape = 8)
boxplot.stats(aq$Ozone)$out # atípicosSituación 1 — Temperatura máxima diaria (°F)
Datos: media 77,79 · mediana 79 · moda 81 · mín 57 · máx 97 · rango 40 · varianza 90,82 · DE 9,53 · CV 12,3 % · Q1 71 · Q3 84,5 · asimetría −0,23.
- Promedio: la temperatura máxima diaria promedio fue 77,79 °F.
- Posición central: la mediana es 79 °F; la mitad de los días tuvo máximas de 79 °F o menos y la otra mitad de 79 °F o más.
- Media, mediana y moda: media 77,79 < mediana 79 < moda 81. Son cercanas, así que la distribución es casi simétrica, con leve asimetría negativa (asimetría −0,23): algunos días fríos, sobre todo en mayo, bajan la media.
- Dispersión: DE 9,53 °F; en promedio las máximas se alejan unos 9,5 °F de la media. CV 12,3 % < 15 %: temperaturas homogéneas, la media es representativa.
- Rango: 97 − 57 = 40 °F. Es la diferencia entre el día más caluroso y el más frío del período. Solo depende de dos días extremos.
- 50 % central: entre Q1 = 71 °F y Q3 = 84,5 °F (RIC 13,5 °F).
- Histograma: unimodal, con la mayor concentración entre 80 y 85 °F y la mayoría de los días entre 70 y 90 °F. La cola izquierda (días fríos de 57–65 °F) es algo más larga, lo que confirma la leve asimetría negativa. No hay atípicos.
- Entre meses: mayo es el más frío (media 66,5 °F); julio (83,9) y agosto (83,7) son los más calurosos; junio (78,2) y septiembre (76,9) son intermedios. La temperatura sube de mayo a julio–agosto y baja en septiembre. Julio es el mes menos variable (DE 4,4) y septiembre el más variable (DE 8,5). Julio tiene un atípico bajo (73 °F).
Situación 2 — Velocidad promedio del viento (mph)
Datos: media 9,94 · mediana 9,70 · mín 2,3 · máx 20,7 · rango 18,4 · varianza 12,66 · DE 3,56 · CV 35,79 % · Q1 7,4 · Q3 11,5.
| Mes | n | Media | Mediana | DE |
|---|---|---|---|---|
| Mayo | 24 | 11,50 | 11,50 | 3,46 |
| Junio | 9 | 12,20 | 11,50 | 3,87 |
| Julio | 26 | 8,52 | 7,70 | 3,00 |
| Agosto | 23 | 8,86 | 9,70 | 3,37 |
| Septiembre | 29 | 10,10 | 10,30 | 3,47 |
- Promedio: la velocidad promedio del viento fue 9,94 mph.
- Media y mediana: 9,94 vs 9,70, casi iguales. La distribución es aproximadamente simétrica, con leve asimetría positiva: algunos días de viento fuerte suben un poco la media.
- DE = 3,56 mph: en promedio, la velocidad diaria del viento se aleja unas 3,56 mph de la media de 9,94 mph.
- CV = 35,79 %: la DE equivale a casi el 36 % de la media. Es una variabilidad relativa alta (> 30 %): las velocidades son heterogéneas y la media no representa bien a todos los días.
- 50 % central: entre 7,4 y 11,5 mph (RIC 4,1 mph).
- Mes con mayor promedio: junio (12,20 mph), seguido de mayo (11,50). Junio tiene solo 9 días, así que su resultado es menos confiable.
- Mes más variable según DE: junio (3,87 mph). El menos variable es julio (3,00).
- Boxplots: mayo y junio tienen las cajas más altas (más viento); julio y agosto las más bajas (meses de verano con menos viento); septiembre es intermedio. Las cajas tienen alturas parecidas, así que la dispersión es similar entre meses. Hay atípicos altos en mayo (18,4 y 20,1 mph) y junio (20,7 mph).
Situación 3 — Concentración de ozono (ppb)
Datos: media 42,10 · mediana 31 · mín 1 · máx 168 · rango 167 · varianza 1107,29 ppb² · DE 33,28 · CV 79,04 % · Q1 18 · Q3 62 · asimetría 1,25.
- Promedio: la concentración promedio de ozono fue 42,10 ppb.
- Posición central: la mediana es 31 ppb; la mitad de los días tuvo 31 ppb o menos.
- Media y mediana: la media (42,10) supera claramente a la mediana (31). Hay asimetría positiva: pocos días con ozono muy alto inflan la media. La mediana representa mejor el día típico.
- 50 % central: entre 18 y 62 ppb (RIC 44 ppb).
- Dispersión: muy alta. La DE es 33,28 ppb (varianza 1107,29 ppb²) y el rango 167 ppb, de 1 a 168.
- Coeficiente de variación: 79,04 %, muy superior al 30 %. Los datos son muy heterogéneos y la media es poco representativa.
- Histograma: sesgado a la derecha. La mayoría de los días se concentra bajo 40 ppb (68 de 111) y las barras bajan a medida que sube el ozono, con una cola larga hasta 168 ppb.
- Valores extremos: sí. LS = Q3 + 1,5·RIC = 62 + 1,5·44 = 128 ppb, y los días con 135 y 168 ppb lo superan. Aparecen como puntos sobre el bigote superior.
- Descripción general: distribución asimétrica positiva, muy dispersa (CV 79 %) y con atípicos altos. El día típico tiene alrededor de 31 ppb, pero hay episodios puntuales de contaminación elevada. Conviene resumirla con mediana y RIC, no con media y DE.
Comparar dispersión entre variables con distinta unidad: por CV. Temperatura (12,3 %) < viento (35,8 %) < ozono (79,0 %).
A6 — iris: descriptivo, boxplot y medidas de Setosa (30 sep)
1. Cargar la base (viene con R, no se importa)
data("iris")
head(iris); str(iris) # 150 flores × 5 variables, 50 por especie2. Análisis descriptivo
summary(iris)
round(sapply(iris[, 1:4], sd), 2) # DE de cada variable
round(sapply(iris[, 1:4], function(x) 100 * sd(x) / mean(x)), 1) # CV| Variable | Mín | Q1 | Mediana | Media | Q3 | Máx | DE | CV |
|---|---|---|---|---|---|---|---|---|
| Sepal.Length (cm) | 4,3 | 5,1 | 5,80 | 5,84 | 6,4 | 7,9 | 0,83 | 14,2 % |
| Sepal.Width (cm) | 2,0 | 2,8 | 3,00 | 3,06 | 3,3 | 4,4 | 0,44 | 14,3 % |
| Petal.Length (cm) | 1,0 | 1,6 | 4,35 | 3,76 | 5,1 | 6,9 | 1,77 | 47,0 % |
| Petal.Width (cm) | 0,1 | 0,3 | 1,30 | 1,20 | 1,8 | 2,5 | 0,76 | 63,6 % |
Respuesta: hay 4 variables cuantitativas continuas (cm) y una cualitativa nominal (Species, 50 flores por especie). Las medidas del sépalo son homogéneas (CV ≈ 14 %). Las del pétalo son muy heterogéneas (CV 47 % y 64 %) y su media es menor que la mediana. El salto entre Q1 (1,6) y la mediana (4,35) en Petal.Length sugiere grupos de flores muy distintos: las especies.
3. Boxplot de la longitud del pétalo según especie
library(ggplot2)
ggplot(iris, aes(x = Species, y = Petal.Length, fill = Species)) +
geom_boxplot() +
labs(title = "Longitud del pétalo según especie", x = "Especie", y = "Longitud (cm)") +
theme(legend.position = "none")
ggsave("boxplot_petalo.png", width = 7, height = 5)
aggregate(Petal.Length ~ Species, data = iris, FUN = median)Respuesta: las tres cajas no se solapan, así que la longitud del pétalo separa claramente las especies. Setosa tiene los pétalos más cortos (mediana 1,5 cm) y menos variables (DE 0,17). Versicolor es intermedia (mediana 4,35 cm) y Virginica tiene los más largos (mediana 5,55 cm) y es la más dispersa (DE 0,55). Hay atípicos en Setosa (1,0; 1,1; 1,9 cm) y en Versicolor (3,0 cm).
4. Medidas para Setosa
library(moments)
setosa <- iris[iris$Species == "setosa", 1:4]
resultados <- data.frame(
Media = sapply(setosa, mean), Mediana = sapply(setosa, median),
Minimo = sapply(setosa, min), Maximo = sapply(setosa, max),
DE = sapply(setosa, sd),
Asimetria = sapply(setosa, skewness), Curtosis = sapply(setosa, kurtosis))
round(resultados, 2)
xtable::xtable(resultados, digits = 2) # misma tabla en formato LaTeX| Variable (cm) | Media | Mediana | Mín | Máx | DE | Asimetría | Curtosis |
|---|---|---|---|---|---|---|---|
| Sepal.Length | 5,01 | 5,0 | 4,3 | 5,8 | 0,35 | 0,12 | 2,65 |
| Sepal.Width | 3,43 | 3,4 | 2,3 | 4,4 | 0,38 | 0,04 | 3,74 |
| Petal.Length | 1,46 | 1,5 | 1,0 | 1,9 | 0,17 | 0,10 | 3,80 |
| Petal.Width | 0,25 | 0,2 | 0,1 | 0,6 | 0,11 | 1,22 | 4,43 |
Respuesta: en Setosa media ≈ mediana en las 4 variables. Sepal.Length, Sepal.Width y Petal.Length son casi simétricas (asimetría cercana a 0). Petal.Width tiene asimetría positiva (1,22): la mayoría de los pétalos mide 0,2 cm y unos pocos llegan a 0,6 cm. Sepal.Length es platicúrtica (2,65 < 3). Las otras tres son leptocúrticas (> 3): datos concentrados cerca del centro con colas más pesadas; Petal.Width es la más marcada (4,43).
Tarea 1 — Cuantitativa vs cualitativa
# Clasificar y convertir a factor
BASE_TAREA_1$carrera <- factor(BASE_TAREA_1$carrera,
levels = c("Ciencias", "Ciencias Sociales", "Ingeniería", "Salud"))
BASE_TAREA_1$carga_academica <- factor(BASE_TAREA_1$carga_academica,
levels = c("Baja", "Moderada", "Alta"), ordered = TRUE)
summary(BASE_TAREA_1[, c("carrera", "carga_academica", "horas_estudio")])
round(100 * prop.table(table(BASE_TAREA_1$carrera)), 1) # % por carrera
# Boxplot horizontal por grupo con la media marcada
ggplot(BASE_TAREA_1, aes(x = horas_estudio, y = carrera, fill = carrera)) +
geom_boxplot() +
stat_summary(fun = mean, geom = "point", shape = 16, size = 2) +
theme(legend.position = "none")
# Medidas por carrera: ver "Medidas por variable y por grupo", caso 2Ojo: en tu script se creó carga_academic (sin la "a" final), así que quedó una columna nueva en vez de convertir la original.
Gráficos: R base y ggplot2
Antes de graficar, pregúntate qué variable se distribuye y respecto de qué grupo. R base sirve para mirar rápido; ggplot2 queda mejor para el informe.
R base
t <- table(data$sexo)
barplot(t, main = "Pacientes según sexo", xlab = "Sexo", ylab = "N° de pacientes",
col = c("pink", "skyblue"), ylim = c(0, 150)) # ylim cambia la escala del eje Y
barplot(t, horiz = TRUE, xlim = c(0, 150)) # barras horizontales
barplot(table(data$sexo, data$actividad), beside = TRUE, legend.text = TRUE) # agrupadas
pie(t) # torta
pie(t, labels = paste0(names(t), " ", round(100 * prop.table(t), 1), " %"))
hist(data$imc, breaks = limites, right = FALSE) # ver Histogramas
boxplot(data$edad, horizontal = TRUE) # boxplot
boxplot(edad ~ actividad, data = data) # boxplot por grupoggplot2: la estructura
Todo gráfico es ggplot(datos, aes(...)) + geom_...() + labs() + scale_...() + theme_...(). El + va al final de cada línea.
| Capa | Qué hace | Ejemplo |
|---|---|---|
| Datos | La base que se usa | ggplot(data, ...) |
aes() |
Qué variable va en x, y, fill | aes(x = sexo, fill = actividad) |
| Geometría | Tipo de gráfico | geom_bar(), geom_col(), geom_histogram(), geom_boxplot() |
after_stat() |
Usa lo que calculó el geom | after_stat(count), after_stat(count / sum(count)) |
| Escalas | Ejes y colores | scale_y_continuous(labels = scales::percent), scale_fill_manual() |
| Etiquetas | Título y nombres | labs(title = "...", x = "...", y = "...", fill = "...") |
| Coordenadas | Girar o hacer torta | coord_flip(), coord_polar(theta = "y") |
| Tema | Apariencia | theme_minimal(), theme_classic(), theme_void() |
Recetario ggplot2
| Quiero | Código |
|---|---|
| Barras de frecuencias | ggplot(data, aes(x = sexo)) + geom_bar() |
| Frecuencia sobre cada barra | + geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) |
| Barras de proporciones | geom_bar(aes(y = after_stat(count / sum(count)))) |
| Eje Y en porcentaje | + scale_y_continuous(labels = scales::percent) |
| Todas las barras de un color | geom_bar(fill = "steelblue") (fuera de aes) |
| Un color por categoría | aes(x = sexo, fill = sexo) (dentro de aes) |
| Elegir los colores | + scale_fill_manual(values = c("Mujer" = "#E78AC3", "Hombre" = "#66C2A5")) |
| Sacar la leyenda | + theme(legend.position = "none") |
| Barras horizontales | + coord_flip() |
| Dos variables, apiladas (conteos) | ggplot(data, aes(x = sexo, fill = actividad)) + geom_bar() |
| Dos variables, lado a lado | geom_bar(position = "dodge") |
| Dos variables, apiladas al 100 % | geom_bar(position = "fill") + eje en % |
| Torta | ggplot(data, aes(x = "", fill = sexo)) + geom_bar() + coord_polar(theta = "y") + theme_void() |
| Histograma con tus límites | geom_histogram(breaks = limites, closed = "left", color = "black") |
| Boxplot | ggplot(data, aes(x = edad)) + geom_boxplot() |
| Boxplot por grupo | ggplot(data, aes(x = actividad, y = edad)) + geom_boxplot() |
| Marcar la media en el boxplot | + stat_summary(fun = mean, geom = "point", shape = 16, size = 3) |
| Guardar | ggsave("grafico.png", plot = g, width = 10, height = 6, units = "in", dpi = 300) (también .pdf) |
Con position = "fill", la variable en x es el grupo. x = sexo, fill = actividad muestra la actividad según sexo; al revés, el sexo según actividad.
Torta con porcentajes
tabla <- as.data.frame(table(data$sexo)) # columnas Var1 y Freq
tabla$porcentaje <- tabla$Freq / sum(tabla$Freq) * 100
g <- ggplot(tabla, aes(x = "", y = Freq, fill = Var1)) +
geom_bar(stat = "identity") + # la altura es Freq, no un conteo
coord_polar(theta = "y") +
geom_text(aes(label = paste0(round(porcentaje, 1), " %")),
position = position_stack(vjust = 0.5)) + # etiqueta al centro del sector
scale_fill_manual(values = c("Mujer" = "#E78AC3", "Hombre" = "#66C2A5")) +
labs(x = NULL, y = NULL, fill = "Sexo", title = "Distribución por sexo") +
theme_void()
ggsave("grafico_sexo.png", plot = g, width = 10, height = 6, units = "in", dpi = 300)Usa la torta con pocas categorías y proporciones claramente distintas. Para comparar categorías con precisión son mejores las barras.
- Evitar la torta cuando hay muchas categorías, cuando las proporciones son muy parecidas o cuando hay que comparar con precisión.
- Las categorías deben ser mutuamente excluyentes y sumar el total.
coord_polar(theta = "y")transforma la altura de cada barra en un ángulo; el total de las frecuencias es el círculo completo.scale_y_continuous(labels = scales::percent)solo cambia cómo se muestra el eje; la proporción la calculaafter_stat(). Conlibrary(scales)se puede escribirpercentsin el prefijo.
Boxplot
ggplot(data, aes(x = edad, y = "")) +
geom_boxplot() +
stat_summary(fun = mean, geom = "point", shape = 16, size = 3) + # media
labs(title = "Distribución de la edad", x = "Edad (años)", y = NULL) +
theme_classic() +
theme(axis.text.y = element_blank(), axis.ticks.y = element_blank()) # sin eje Y
ggplot(data, aes(x = actividad, y = edad, fill = actividad)) + # por grupo
geom_boxplot() + theme(legend.position = "none")
boxplot.stats(data$edad)$out # atípicosCómo leerlo: ver "Leer un boxplot" en Interpretación de resultados.
Histogramas
Un histograma se arma en 6 pasos: n, rango, k (Sturges), amplitud, límites e intervalos con cut(); después se grafica con hist() o geom_histogram() usando esos mismos límites.
Fórmulas
R = x_{\max} - x_{\min}k = 1 + 3.22 \log_{10}(n) \quad \text{(Sturges)} \qquad k \approx \sqrt{n} \quad \text{(raíz)}A = \frac{R}{k} \quad (\text{redondear y sumar } 0.01 \text{ para cubrir el máximo})f_i = \text{frecuencia}, \quad r_i = \frac{f_i}{n}, \quad F_i = \sum_{j \le i} f_j, \quad R_i = \sum_{j \le i} r_jPasos en R
x <- colesterol$imc # o SuperMarket$Unit_price
n <- length(x)
minimo <- min(x); maximo <- max(x)
R <- maximo - minimo
k <- round(1 + 3.22 * log10(n)) # Sturges
# k <- ceiling(sqrt(n)) # alternativa raíz de n
A <- round(R / k, 2) + 0.01
limites <- round(minimo + (0:k) * A, 2)
etiquetas <- paste0(format(limites[-length(limites)], nsmall = 2), " - ",
format(limites[-1], nsmall = 2))
intervalos <- cut(x, breaks = limites, labels = etiquetas,
right = FALSE, include.lowest = TRUE) # [a, b)Al redondear A, revisar que el último límite supere el máximo y que el primer y el último intervalo tengan fᵢ ≠ 0. Si no, sumar una unidad del último decimal (0.01 con dos decimales). Ejemplo IMC (n = 250, k = 9): con A = 2.48 el último límite es 36.79 y deja fuera el máximo 36.83; con A = 2.49 llega a 36.88. Sturges y √n no coinciden: para n = 250 dan k ≈ 9 y k ≈ 16.
Tabla de frecuencias
f <- table(intervalos)
fi <- as.vector(f)
ri <- fi / n
Fi <- cumsum(fi)
Ri <- cumsum(ri)
tabla_frecuencia <- data.frame(intervalo = names(f), fi = fi,
ri = round(ri, 3), Fi = Fi, Ri = round(Ri, 3))
sum(fi) == n # chequeo: debe dar TRUE
xtable::xtable(tabla_frecuencia) # versión LaTeXAtajos: prop.table(tabla) * 100 da porcentajes; cumsum(tabla) da la acumulada.
Chequeos de la tabla: Σfᵢ = n, Σrᵢ = 1 y la última acumulada Fₖ = n (sum(fi), sum(ri), Fi[k]).
Graficar
# R base
hist(x, breaks = limites, right = FALSE,
main = "Distribución del IMC", xlab = "IMC (kg/m^2)", ylab = "N° de pacientes")
# ggplot2
ggplot(colesterol, aes(x = imc)) +
geom_histogram(breaks = limites, closed = "left",
fill = "steelblue", color = "black") +
labs(title = "Distribución del IMC", x = "IMC (kg/m^2)", y = "N° de pacientes") +
theme_minimal()
# Desde la tabla, con colores por intervalo
ggplot(tabla_frecuencia, aes(x = intervalo, y = fi, fill = intervalo)) +
geom_col(width = 1, color = "black") +
theme_minimal() + theme(legend.position = "none")| Argumento | Para qué |
|---|---|
breaks = limites |
Usa los límites calculados en vez de los automáticos |
right = FALSE / closed = "left" |
Intervalos [a, b): incluye el límite izquierdo |
include.lowest = TRUE |
Incluye el valor extremo en el primer/último intervalo |
fill, color |
Relleno y borde de las barras |
width = 1 en geom_col |
Barras pegadas, como histograma |
hist(x) sin breaks deja que R elija los intervalos; para usar los de Sturges hay que pasarle breaks = limites. En el gráfico desde la tabla se ajustan los ejes con scale_x_discrete(labels = etiquetas) y scale_y_continuous(breaks = seq(0, 50, by = 5)).
Si el histograma tiene cola larga a la derecha, la media queda sobre la mediana (asimetría positiva); compararlas con mean() y median().
Histograma vs gráfico de barras
| Histograma | Gráfico de barras | |
|---|---|---|
| Variable | Cuantitativa | Cualitativa, o cuantitativa con pocas categorías |
| Eje X | Valores agrupados en intervalos | Categorías |
| Barras | Adyacentes (pegadas) | Separadas |
| Orden | Importa | Puede no importar (nominales) |
La tabla de frecuencias y el histograma muestran la misma información; el número de clases cambia la forma que se ve.
Interpretación de resultados
Una buena respuesta tiene 3 partes: el valor con su unidad, qué significa en el contexto de la variable, y qué se concluye (comparando con otra medida o con el gráfico). Nunca solo el número.
Checklist para describir una variable cuantitativa
- Explorar los datos:
str(),head(),length(), NA. - Mirar un gráfico: histograma y boxplot.
- Centro:
mean(),median(), moda o clase modal. - Dispersión: rango,
var(),sd(), CV. - Posición:
quantile(x, probs = c(.10, .25, .50, .75, .90)),IQR(). - Valores extremos: regla 1,5·RIC,
boxplot.stats(x)$out. - Forma: media vs mediana,
skewness(),kurtosis(). - Interpretar todo en el contexto de la variable, con su unidad.
Percentil en palabras: "P90 = 20 min: aproximadamente el 90 % de los tiempos es menor o igual a 20 minutos y el 10 % restante lo supera". El sesgo depende del estimador y de n: se informa junto con el gráfico y la comparación media vs mediana, nunca solo el número.
Qué dice cada medida
| Medida | Qué significa | Frase modelo |
|---|---|---|
| Media | Valor promedio; sensible a extremos | "En promedio, la temperatura máxima fue 77,79 °F." |
| Mediana (Q2) | Valor central: 50 % bajo y 50 % sobre; resistente a extremos | "La mitad de los días tuvo a lo más 79 °F." |
| Moda | Valor más frecuente | "La temperatura más repetida fue 81 °F." |
| Rango | Máx − mín; amplitud total, depende solo de 2 datos | "Las temperaturas abarcan 40 °F, de 57 a 97 °F." |
| Varianza | Promedio de desviaciones al cuadrado; unidad al cuadrado, difícil de interpretar | "Varianza 90,82 °F²" (mejor interpretar la DE) |
| Desviación estándar | Cuánto se alejan los datos de la media, en la misma unidad | "En promedio, los valores se alejan 9,53 °F de la media." |
| CV | DE como % de la media; compara dispersión entre variables con distinta unidad | "CV 35,8 %: variabilidad relativa alta." |
| Q1 / Q3 | 25 % de los datos bajo Q1; 75 % bajo Q3 | "El 25 % de los días tuvo menos de 71 °F." |
| RIC = Q3 − Q1 | Ancho del 50 % central; resistente a extremos | "El 50 % central está entre 71 y 84,5 °F (RIC 13,5 °F)." |
| Percentil p | p % de los datos bajo ese valor | "El 90 % de los días tuvo menos de X." |
| Asimetría | Hacia dónde va la cola | "Asimetría 1,25 > 0: cola hacia valores altos." |
| Curtosis | Peso de las colas comparado con la normal (3); no es "qué tan puntiaguda" | "Curtosis 4,2 > 3: leptocúrtica, valores concentrados con colas pesadas." |
Cuartiles y percentiles: Q1 = P25, Q2 = P50 = mediana, Q3 = P75. quantile(x) sin probs entrega mín, Q1, Q2, Q3 y máx; summary(x) agrega la media.
Mapa: centro, dispersión, posición y forma
| Pregunta | Medidas | En R |
|---|---|---|
| ¿Dónde está el centro? | Media, mediana, moda | mean(), median(), mfv() |
| ¿Cuánto varían? | Rango, varianza, desviación estándar, CV | max(x) - min(x), var(), sd(), 100 * sd(x) / mean(x) |
| ¿Dónde se ubican? | Cuantiles, RIC | quantile(), IQR() |
| ¿Qué forma tienen? | Sesgo, curtosis | skewness(), kurtosis() |
CV: cómo clasificar
| CV | Interpretación |
|---|---|
| < 15 % | Datos homogéneos, poca variabilidad; la media es representativa |
| 15 % – 30 % | Variabilidad moderada |
| > 30 % | Datos heterogéneos, alta variabilidad; la media es poco representativa (usar mediana) |
CV = \frac{s}{\bar{x}} \times 100\%Forma: media vs mediana vs moda
| Relación | Forma | Histograma | Boxplot |
|---|---|---|---|
| Media ≈ mediana ≈ moda | Simétrica | Campana centrada | Mediana al centro de la caja, bigotes parecidos |
| Media > mediana (> moda) | Asimetría positiva (sesgo a la derecha) | Cola larga hacia la derecha | Mediana cerca de Q1, bigote superior más largo, atípicos arriba |
| Media < mediana (< moda) | Asimetría negativa (sesgo a la izquierda) | Cola larga hacia la izquierda | Mediana cerca de Q3, bigote inferior más largo |
Con asimetría o atípicos se describe con mediana y RIC; si es simétrica, con media y DE.
Ejemplos típicos de sesgo positivo: ingresos, tiempos de espera, precios de viviendas.
Valores atípicos (regla 1,5·RIC)
LI = Q_1 - 1.5 \cdot RIC \qquad LS = Q_3 + 1.5 \cdot RICUn dato fuera de [LI, LS] es atípico y aparece como punto en el boxplot. En R: boxplot.stats(x)$out.
Ejemplo ozono: RIC = 62 − 18 = 44; LS = 62 + 66 = 128 → 135 y 168 ppb son atípicos.
Leer un boxplot
- Línea dentro de la caja = mediana.
- Bordes de la caja = Q1 y Q3; altura de la caja = RIC (50 % central).
- Bigotes = hasta el dato más extremo que no es atípico.
- Puntos sueltos = atípicos.
- Caja larga = más dispersión; mediana descentrada = asimetría.
Comparar grupos (boxplots o tabla por grupo)
- Centro: ¿qué grupo tiene mayor mediana/media? ("Julio y agosto son los meses más calurosos").
- Dispersión: ¿qué caja es más larga o qué DE es mayor? ("Septiembre es el más variable").
- Forma y atípicos: ¿algún grupo asimétrico o con puntos extremos?
- Tamaño: mencionar si un grupo tiene n chico ("junio con solo 9 días, resultado menos confiable").
- Superposición: si las cajas se solapan mucho, la diferencia entre grupos es débil.
Leer un histograma
- Centro: dónde está la barra más alta (clase modal).
- Forma: simétrico, sesgado a la derecha o a la izquierda; unimodal o bimodal.
- Dispersión: qué tan ancho es el rango de barras.
- Atípicos: barras aisladas lejos del resto.
Qué NO concluir
- Descriptivo ≠ causal: "en julio hay menos viento" no dice que el calor cause menos viento.
- Los resultados valen para esta muestra (NY, mayo–sep 1973), no para cualquier lugar o año.
- Si se eliminaron NA (
na.omit), mencionarlo: cambia n y puede sesgar.
R Markdown
Un .Rmd tiene tres partes: encabezado YAML, texto en Markdown y chunks de código; el botón Knit (Ctrl+Shift+K) lo convierte en HTML, PDF o Word.
Plantilla base
---
title: "Análisis Descriptivo"
author: "Nombre Autor"
date: "29 de septiembre de 2026"
output: html_document # pdf_document, word_document
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
library(readxl)
library(ggplot2)
```
# Introducción
En este informe se analiza la variable ...
# Datos
```{r datos}
carga2 <- read_excel("BASE_TAREA_1.xlsx")
names(carga2)
```
# Medidas
```{r}
media <- mean(carga2$horas_estudio, na.rm = TRUE)
mediana <- median(carga2$horas_estudio, na.rm = TRUE)
```
El promedio fue de `r round(media, 2)` horas, con mediana `r round(mediana, 2)`.Opciones de chunk
| Opción | Efecto |
|---|---|
echo = FALSE |
Muestra el resultado pero no el código |
include = FALSE |
Ejecuta pero no muestra nada (ideal para setup) |
eval = FALSE |
Muestra el código sin ejecutarlo |
message = FALSE, warning = FALSE |
Oculta mensajes y advertencias de paquetes |
results = "hide" |
Oculta la salida impresa |
fig.width = 6, fig.height = 4 |
Tamaño del gráfico en pulgadas |
fig.align = "center" |
Centra el gráfico |
fig.cap = "Título" |
Pie de figura |
Las opciones van en la llave del chunk: ```{r grafico, echo=FALSE, fig.align="center"}. Con knitr::opts_chunk$set() quedan por defecto para todo el documento.
Sintaxis Markdown
| Escribo | Obtengo |
|---|---|
# Título, ## Subtítulo |
Encabezados nivel 1 y 2 |
**negrita**, *cursiva* |
negrita, cursiva |
`codigo` |
Código en línea |
- item / 1. item |
Lista con viñetas / numerada |
`r expresion` |
Resultado de R dentro del texto |
$\bar{x}$ / $$ ... $$ |
Fórmula en línea / centrada |
 |
Imagen |
[texto](url) |
Enlace |
Tablas bonitas
knitr::kable(tabla_frecuencia, caption = "Tabla de frecuencias del IMC")Errores típicos
- Cada chunk corre en un entorno limpio al hacer Knit: cargar paquetes y datos dentro del
.Rmd, no basta con tenerlos en la consola. View()einstall.packages()dentro del.Rmdrompen o enlentecen el Knit; déjalos en la consola.- La ruta del Excel es relativa a la carpeta del
.Rmd; si el archivo está al lado, bastaread_excel("BASE_TAREA_1.xlsx"). - Los chunks se escriben en el
.Rmd, no en un script.R.
Errores encontrados en tus scripts
| Archivo | Dice | Debe decir |
|---|---|---|
| t.Rmd | nar.rm=TRUE |
na.rm = TRUE |
| Ayudantia_1.R | notas[notas >=4 & <=6] |
notas[notas >= 4 & notas <= 6] |
| Ayudantia_1.R | length(unique(notas) |
length(unique(notas)) |
| Ayudantia_1.R | nombres[order(edad)] para "mayor a menor" |
nombres[order(edad, decreasing = TRUE)] |
| Ayudantia_3.R | data <- read_excel(...) y luego usa SuperMarket |
SuperMarket <- read_excel("SuperMarket.xlsx") |
| Introduccion.R | Data.frame(...) |
data.frame(...) (minúscula) |
Qué gráfico y qué medidas según el tipo de variable
Primero clasificar la variable; eso decide la tabla, el gráfico y las medidas.
| Tipo | Ejemplos | Tabla / medidas | Gráfico | En R |
|---|---|---|---|---|
| Cualitativa nominal | sexo, carrera, medio de pago | Frecuencia y %, moda | Barras, torta | table, prop.table, geom_bar |
| Cualitativa ordinal | carga académica (Baja < Moderada < Alta) | Frecuencia, %, acumulada, moda, mediana | Barras en orden | factor(..., ordered = TRUE) |
| Cuantitativa discreta | n° de crías, Quantity | Todas las medidas | Barras o histograma | table, summary |
| Cuantitativa continua | temperatura, IMC, precio | Todas las medidas, tabla por intervalos | Histograma, boxplot | cut, geom_histogram, geom_boxplot |
| Cuali vs cuali | género × medio de pago | Tabla de contingencia, % por fila/columna | Barras apiladas, lado a lado, 100 % | table(a, b), position = "fill" |
| Cuanti vs cuali | horas estudio por carrera | Medidas por grupo | Boxplots por grupo | tapply, aggregate, geom_boxplot |
Recordatorio: class(x) dice cómo lo guardó R (numeric, character, factor); no siempre coincide con el tipo estadístico. Un código numérico (1 = Ciencias) es cualitativo aunque R diga numeric.
Errores de R frecuentes
Casi todos los errores son de nombre, de paquete sin cargar, de tipo de dato o de NA.
| Mensaje | Causa | Solución |
|---|---|---|
object 'X' not found |
Variable mal escrita o no creada/cargada | Revisar mayúsculas; names(df); volver a correr read_excel |
could not find function "ggplot" |
Paquete sin cargar | library(ggplot2) (y install.packages("ggplot2") si no está) |
there is no package called 'X' |
Paquete no instalado | install.packages("X") |
non-numeric argument to binary operator / argument is not numeric or logical |
La columna es texto | df$x <- as.numeric(df$x) |
Resultado NA en mean, sd, max... |
Hay datos faltantes | mean(x, na.rm = TRUE) o na.omit(df) |
unexpected symbol / unexpected ')' |
Falta coma, paréntesis o comilla | Contar paréntesis; mirar la línea anterior |
$ operator is invalid for atomic vectors |
Se usó $ en un vector |
Usar x["nombre"] o verificar que sea data frame |
undefined columns selected |
Nombre de columna mal escrito | names(df); recordar la coma: df[filas, ] |
cannot open file / path does not exist |
Ruta mal o archivo en otra carpeta | getwd(), setwd() o Session → Set Working Directory |
Removed N rows containing non-finite values (warning) |
ggplot ignoró NA | Normal; o filtrar con na.omit |
+ al inicio de la consola |
Comando incompleto | Terminar el comando o apretar Esc |
| ggplot no muestra nada | + al inicio de la línea en vez del final |
El + va al final de cada línea |
factor con NA tras convertir |
levels no coinciden con los datos (tildes, mayúsculas) |
unique(df$x) y copiar los niveles exactos |
Atajos de RStudio: Ctrl+Enter corre la línea; Ctrl+Shift+M escribe %>%; Alt+− escribe <-; ?funcion abre la ayuda.