Antes de analizar nada hay que saber qué representa cada fila, qué
variables hay y cómo se relacionan las tablas. En este informe se
trabaja con tres fuentes: iris (150 flores), dos extractos
históricos ZA8630 (familias e hijos) y business_staff (1000
personas, corte transversal). Los datos se leen desde
../data, por lo que el .Rmd debe estar en la
carpeta rmarkdown/.
library(data.table)
library(dplyr)
library(ggplot2)
library(lubridate)
library(modelsummary)
library(knitr)
iris_data <- fread(file.path(data_path, "iris_data.txt"))
business_staff <- fread(file.path(data_path, "business_staff_cross_section_original_names.txt"))
families_raw <- fread(file.path(data_path, "ZA8630_families_teaching_extract_original_names.txt"),
na.strings = c("", "NA"))
children_raw <- fread(file.path(data_path, "ZA8630_children_teaching_extract_original_names.txt"),
na.strings = c("", "NA"))
Qué representa cada fila en cada tabla:
| Tabla | Unidad de observación (una fila = …) | Filas |
|---|---|---|
iris |
una flor | 150 |
families (ZA8630) |
una familia (matrimonio) | 34.708 |
children (ZA8630) |
un hijo | 80.486 |
business_staff |
una persona (corte transversal) | 1.000 |
Los archivos originales no se modifican: todos los cambios se hacen en R, sobre copias en memoria, de modo que los datos originales y los cálculos quedan separados (Broman y Woo, 2018).
# Nombres de trabajo (snake_case, en inglés)
setnames(business_staff,
old = c("obs", "EDU", "inc", "zufr", "trip"),
new = c("wave", "education", "income", "happiness", "trip_abroad"))
setnames(families_raw,
old = c("doc", "dat4", "cort", "sozial", "grasen",
"idm", "gebm4", "todm4", "idf", "gebf4", "todf4"),
new = c("family_id", "marriage_date", "marriage_place", "social_group",
"landholding_grasen", "husband_id", "husband_birth_date",
"husband_death_date", "wife_id", "wife_birth_date", "wife_death_date"))
setnames(children_raw,
old = c("doc", "idk", "sex", "gebk4", "todk4", "gebortk", "todortk", "berufek"),
new = c("family_id", "child_id", "gender", "child_birth_date",
"child_death_date", "child_birth_place", "child_death_place",
"child_occupation"))
# Recodificación de sexo: M/W -> male/female
children_raw[gender == "M", gender := "male"]
children_raw[gender == "W", gender := "female"]
# Fechas como Date
date_variables_families <- c("marriage_date", "husband_birth_date", "husband_death_date", "wife_birth_date", "wife_death_date")
families_raw[, (date_variables_families) := lapply(.SD, as.Date), .SDcols = date_variables_families]
children_raw[, child_birth_date := as.Date(child_birth_date)]
children_raw[, child_death_date := as.Date(child_death_date)]
# Factores en business_staff (low es la categoría de referencia)
business_staff[, education := factor(education, levels = c("low", "middle", "high"))]
business_staff[, gender := factor(gender)]
Las decisiones principales y su justificación son:
doc, gebk4, zufr,
inc…) se sustituyen por nombres en inglés con un formato
único (family_id, child_birth_date,
happiness, income…). Broman y Woo (2018)
recomiendan nombres consistentes, sin ambigüedad, y documentar la
equivalencia en un diccionario de datos (ver Ejercicio 3).Date (ISO 8601). Las
fechas se leen como Date para poder restarlas y calcular
edades; esto sigue la recomendación de Broman y Woo sobre fechas sin
ambigüedad.NA (na.strings = c("", "NA")), de
modo que cada celda contiene un único valor y los ausentes se codifican
de una sola manera.gender.
M/W pasan a
male/female, la misma convención que
business_staff, lo que evita mezclar códigos en distintos
archivos.education como factor ordenado por
niveles. Los niveles low < middle < high
hacen que las tablas y gráficos respeten el orden natural. Consecuencia:
low es la categoría de referencia en las regresiones, y
todos los coeficientes de education se leen como
diferencias respecto a la educación baja.families (una fila por familia) y children
(una fila por hijo) se mantienen separadas y solo se combinan para el
análisis, según el principio de Wickham (2014).razon, diferencia y las edades parentales se
calculan en R, sin sobrescribir las columnas de partida.families_raw contiene una fila por familia (matrimonio)
y children_raw una fila por hijo. El script base ya
renombró las variables; la equivalencia queda documentada así (un
pequeño data dictionary, como recomiendan Broman y Woo,
2018).
dic_families <- data.frame(
original = c("doc", "dat4", "cort", "sozial", "grasen", "idm", "gebm4",
"todm4", "idf", "gebf4", "todf4"),
trabajo = c("family_id", "marriage_date", "marriage_place", "social_group",
"landholding_grasen", "husband_id", "husband_birth_date",
"husband_death_date", "wife_id", "wife_birth_date", "wife_death_date"),
significado = c("Identificador de la familia", "Fecha del matrimonio",
"Lugar del matrimonio", "Grupo social",
"Tenencia de tierras (Grasen)", "Identificador del marido",
"Nacimiento del marido", "Fallecimiento del marido",
"Identificador de la esposa", "Nacimiento de la esposa",
"Fallecimiento de la esposa"))
kable(dic_families, caption = "Correspondencia de nombres: families")
| original | trabajo | significado |
|---|---|---|
| doc | family_id | Identificador de la familia |
| dat4 | marriage_date | Fecha del matrimonio |
| cort | marriage_place | Lugar del matrimonio |
| sozial | social_group | Grupo social |
| grasen | landholding_grasen | Tenencia de tierras (Grasen) |
| idm | husband_id | Identificador del marido |
| gebm4 | husband_birth_date | Nacimiento del marido |
| todm4 | husband_death_date | Fallecimiento del marido |
| idf | wife_id | Identificador de la esposa |
| gebf4 | wife_birth_date | Nacimiento de la esposa |
| todf4 | wife_death_date | Fallecimiento de la esposa |
dic_children <- data.frame(
original = c("doc", "idk", "sex", "gebk4", "todk4", "gebortk", "todortk", "berufek"),
trabajo = c("family_id", "child_id", "gender", "child_birth_date",
"child_death_date", "child_birth_place", "child_death_place",
"child_occupation"),
significado = c("Familia a la que pertenece el hijo", "Identificador del hijo",
"Sexo (M = male, W = female)", "Nacimiento del hijo",
"Fallecimiento del hijo", "Lugar de nacimiento",
"Lugar de fallecimiento", "Ocupación"))
kable(dic_children, caption = "Correspondencia de nombres: children")
| original | trabajo | significado |
|---|---|---|
| doc | family_id | Familia a la que pertenece el hijo |
| idk | child_id | Identificador del hijo |
| sex | gender | Sexo (M = male, W = female) |
| gebk4 | child_birth_date | Nacimiento del hijo |
| todk4 | child_death_date | Fallecimiento del hijo |
| gebortk | child_birth_place | Lugar de nacimiento |
| todortk | child_death_place | Lugar de fallecimiento |
| berufek | child_occupation | Ocupación |
Además, sex se recodificó de
M/W a male/female.
La clave que une ambas tablas es family_id (antes
doc en los dos archivos).
anyDuplicated(families_raw$family_id) # 0: clave única en families
## [1] 0
all(children_raw$family_id %in% families_raw$family_id) # todo hijo tiene familia
## [1] TRUE
hijos_por_familia <- children_raw[, .(n_children = .N), by = family_id]
sum(hijos_por_familia$n_children > 1)
## [1] 14756
family_id no se repite en families_raw,
pero sí en children_raw: una familia puede tener varios
hijos y cada hijo pertenece a una sola familia. Por eso la relación es
uno-a-muchos. De hecho, 14.756 familias tienen más de
un hijo.
Se parte de children_raw con all.x = TRUE
para conservar todos los hijos. Se descartan za_nr,
version y doi (metadatos repetidos en ambas
tablas) para evitar columnas duplicadas con sufijos
.x/.y.
kid_fam <- merge(
children_raw[, .(family_id, child_id, gender, child_birth_date)],
families_raw[, .(family_id, marriage_date, husband_id, husband_birth_date,
wife_id, wife_birth_date)],
by = "family_id", all.x = TRUE)
c(hijos_originales = nrow(children_raw), filas_tras_fusion = nrow(kid_fam))
## hijos_originales filas_tras_fusion
## 80486 80486
La fusión no pierde ni duplica hijos. Ahora se calculan las edades en años (diferencia en días entre fechas dividida por 365,25):
kid_fam[, `:=`(
mother_age_at_birth = as.numeric(child_birth_date - wife_birth_date) / 365.25,
father_age_at_birth = as.numeric(child_birth_date - husband_birth_date) / 365.25)]
edad_media <- kid_fam[, .(
n_madre_valida = sum(!is.na(mother_age_at_birth)),
edad_media_madres = mean(mother_age_at_birth, na.rm = TRUE),
n_padre_valida = sum(!is.na(father_age_at_birth)),
edad_media_padres = mean(father_age_at_birth, na.rm = TRUE))]
kable(edad_media, digits = 2, caption = "Edad media al nacimiento del hijo")
| n_madre_valida | edad_media_madres | n_padre_valida | edad_media_padres |
|---|---|---|---|
| 52820 | 32.02 | 52348 | 35.72 |
kid_fam[, .(min_madre = min(mother_age_at_birth, na.rm = TRUE),
max_madre = max(mother_age_at_birth, na.rm = TRUE),
min_padre = min(father_age_at_birth, na.rm = TRUE),
max_padre = max(father_age_at_birth, na.rm = TRUE),
edades_negativas = sum(mother_age_at_birth < 0 | father_age_at_birth < 0,
na.rm = TRUE))]
## min_madre max_madre min_padre max_padre edades_negativas
## <num> <num> <num> <num> <int>
## 1: 0.2354552 55.77276 16.07392 78.39014 0
La edad media de las madres al nacimiento es de 32.02 años y la de los padres de 35.72, es decir, unos 3.7 años más. Las medias solo usan los casos con ambas fechas conocidas (52.820 en madres y 52.348 en padres, de 80.486 hijos), así que describen a las familias con fechas registradas, que pueden no ser representativas del conjunto.
La unidad de observación de kid_fam es el
hijo: cada fila es un hijo, cada columna una variable y cada
celda un valor, de modo que cumple los criterios de tidy data de Wickham
(2014). Los datos de la familia (fechas de los padres) se repiten para
hermanos, pero esa redundancia no rompe el orden de la tabla; solo hay
que tenerla en cuenta, por ejemplo, al contar familias con una tabla
donde la unidad es el hijo.
familiesEsta operación no es directa: families
tiene una fila por familia, mientras que las edades se calculan por hijo
y pueden variar entre hermanos (los padres envejecen). Hay que decidir
cómo resumir varias edades en un único valor por familia. Aquí se usa la
media por familia, antes de unir; con otros objetivos podría ser más
razonable la edad al primer hijo.
media_o_na <- function(x) if (all(is.na(x))) NA_real_ else mean(x)
edades_por_familia <- kid_fam[, .(
n_children = .N,
mother_age_at_birth = media_o_na(mother_age_at_birth),
father_age_at_birth = media_o_na(father_age_at_birth)), by = family_id]
families_edades <- merge(families_raw, edades_por_familia, by = "family_id", all.x = TRUE)
c(familias = nrow(families_raw), filas_resultado = nrow(families_edades))
## familias filas_resultado
## 34708 34708
La tabla resultante vuelve a tener como unidad de observación la familia (una fila por familia).
El archivo usa los nombres SepalLengthCm,
SepalWidthCm, PetalLengthCm,
PetalWidthCm y Species, equivalentes a
Sepal.Length, Sepal.Width, Petal.Length, Petal.Width y Species. Se
conservan tal cual. El objeto se llama iris_df para no
enmascarar el iris que incluye R.
iris_df <- as.data.frame(iris_data)
str(iris_df)
## 'data.frame': 150 obs. of 6 variables:
## $ Id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ SepalLengthCm: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
## $ SepalWidthCm : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
## $ PetalLengthCm: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
## $ PetalWidthCm : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
## $ Species : chr "setosa" "setosa" "setosa" "setosa" ...
Guardar, limpiar y volver a cargar permite comprobar que el objeto se recupera intacto:
rda_file <- tempfile(fileext = ".rda")
save(iris_df, file = rda_file)
rm(iris_df)
load(rda_file)
dim(iris_df)
## [1] 150 6
iris_dt <- as.data.table(iris_df)
class(iris_dt)
## [1] "data.table" "data.frame"
El conjunto tiene 150 observaciones (una flor por fila) y 6 columnas: un identificador, cuatro medidas en cm y la especie.
# Solo tres variables
iris_seleccion <- iris_df[, c("SepalLengthCm", "SepalWidthCm", "PetalLengthCm")]
dim(iris_seleccion)
## [1] 150 3
# Todas las variables, solo flores con Sepal.Length >= 5,0
iris_filtrado <- iris_df[iris_df$SepalLengthCm >= 5.0, ]
dim(iris_filtrado)
## [1] 128 6
El filtro conserva 128 de las 150 flores. A continuación se crean las
dos variables derivadas con data.table y con
dplyr, y se comprueba que dan lo mismo.
# data.table (copy() evita modificar iris_dt por referencia)
iris_dt2 <- copy(iris_dt)
iris_dt2[, `:=`(razon = SepalLengthCm / SepalWidthCm,
diferencia = SepalLengthCm - PetalLengthCm)]
# dplyr
iris_dplyr <- iris_df %>%
mutate(razon = SepalLengthCm / SepalWidthCm,
diferencia = SepalLengthCm - PetalLengthCm)
all.equal(as.data.frame(iris_dt2), iris_dplyr)
## [1] TRUE
Estadísticos descriptivos para el conjunto completo y por especie:
resumen <- function(x) {
c(N = sum(!is.na(x)), media = mean(x, na.rm = TRUE),
mediana = median(x, na.rm = TRUE), sd = sd(x, na.rm = TRUE))
}
vars_iris <- c("SepalLengthCm", "SepalWidthCm", "PetalLengthCm", "PetalWidthCm",
"razon", "diferencia")
desc_total <- iris_dt2[, rbindlist(lapply(.SD, function(x) as.list(resumen(x))),
idcol = "variable"), .SDcols = vars_iris]
kable(desc_total, digits = 2, caption = "Descriptivos de iris: conjunto completo")
| variable | N | media | mediana | sd |
|---|---|---|---|---|
| SepalLengthCm | 150 | 5.84 | 5.80 | 0.83 |
| SepalWidthCm | 150 | 3.06 | 3.00 | 0.44 |
| PetalLengthCm | 150 | 3.76 | 4.35 | 1.77 |
| PetalWidthCm | 150 | 1.20 | 1.30 | 0.76 |
| razon | 150 | 1.95 | 2.03 | 0.40 |
| diferencia | 150 | 2.09 | 1.70 | 1.12 |
desc_species <- iris_dt2[, rbindlist(lapply(.SD, function(x) as.list(resumen(x))),
idcol = "variable"),
by = Species, .SDcols = vars_iris]
kable(desc_species, digits = 2, caption = "Descriptivos de iris por especie")
| Species | variable | N | media | mediana | sd |
|---|---|---|---|---|---|
| setosa | SepalLengthCm | 50 | 5.01 | 5.00 | 0.35 |
| setosa | SepalWidthCm | 50 | 3.43 | 3.40 | 0.38 |
| setosa | PetalLengthCm | 50 | 1.46 | 1.50 | 0.17 |
| setosa | PetalWidthCm | 50 | 0.25 | 0.20 | 0.11 |
| setosa | razon | 50 | 1.47 | 1.46 | 0.12 |
| setosa | diferencia | 50 | 3.54 | 3.50 | 0.35 |
| versicolor | SepalLengthCm | 50 | 5.94 | 5.90 | 0.52 |
| versicolor | SepalWidthCm | 50 | 2.77 | 2.80 | 0.31 |
| versicolor | PetalLengthCm | 50 | 4.26 | 4.35 | 0.47 |
| versicolor | PetalWidthCm | 50 | 1.33 | 1.30 | 0.20 |
| versicolor | razon | 50 | 2.16 | 2.16 | 0.23 |
| versicolor | diferencia | 50 | 1.68 | 1.70 | 0.35 |
| virginica | SepalLengthCm | 50 | 6.59 | 6.50 | 0.64 |
| virginica | SepalWidthCm | 50 | 2.97 | 3.00 | 0.32 |
| virginica | PetalLengthCm | 50 | 5.55 | 5.55 | 0.55 |
| virginica | PetalWidthCm | 50 | 2.03 | 2.00 | 0.27 |
| virginica | razon | 50 | 2.23 | 2.17 | 0.25 |
| virginica | diferencia | 50 | 1.04 | 1.00 | 0.32 |
Los promedios globales ocultan diferencias claras entre especies: por ejemplo, el largo del pétalo es muy distinto entre grupos, y por eso la desviación típica del conjunto completo es mayor que la de cada especie por separado. Mirar solo el total llevaría a describir una “flor media” que no se parece a ninguna especie concreta.
str(business_staff)
## Classes 'data.table' and 'data.frame': 1000 obs. of 7 variables:
## $ id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ wave : int 5 5 5 5 5 5 5 5 5 5 ...
## $ gender : Factor w/ 2 levels "female","male": 2 1 2 1 1 2 2 2 1 2 ...
## $ education : Factor w/ 3 levels "low","middle",..: 2 2 2 1 1 2 2 3 2 1 ...
## $ income : int 3249 3669 4088 1260 1809 791 2114 5456 2627 1153 ...
## $ happiness : int 0 7 10 5 3 5 5 3 9 6 ...
## $ trip_abroad: int 0 1 1 0 0 0 0 1 0 0 ...
## - attr(*, ".internal.selfref")=<pointer: 0x000001e79624dd90>
summary(business_staff[, .(education, income, happiness, gender, trip_abroad)])
## education income happiness gender trip_abroad
## low :221 Min. : 0 Min. : 0.000 female:488 Min. :0.000
## middle:582 1st Qu.:2019 1st Qu.: 4.000 male :512 1st Qu.:0.000
## high :197 Median :3142 Median : 5.000 Median :0.000
## Mean :3230 Mean : 5.482 Mean :0.251
## 3rd Qu.:4240 3rd Qu.: 7.000 3rd Qu.:1.000
## Max. :8731 Max. :10.000 Max. :1.000
education: nivel educativo (categórica ordinal:
low, middle, high).income: ingresos mensuales en euros (numérica,
métrica).happiness: felicidad autodeclarada en escala de 0 a 10
(entera, ordinal en origen, usada aquí como cuasi-métrica).gender: female o male
(categórica).trip_abroad: indicador 0/1 de si la persona ha
realizado un viaje al extranjero.desc_edu <- business_staff[, .(
N = .N,
income_media = mean(income), income_sd = sd(income),
happiness_media = mean(happiness), happiness_sd = sd(happiness),
pct_viaje = 100 * mean(trip_abroad)), by = education][order(education)]
kable(desc_edu, digits = 2, caption = "Descriptivos por nivel educativo")
| education | N | income_media | income_sd | happiness_media | happiness_sd | pct_viaje |
|---|---|---|---|---|---|---|
| low | 221 | 1620.55 | 497.80 | 4.52 | 2.11 | 23.53 |
| middle | 582 | 3145.97 | 1006.94 | 5.61 | 2.24 | 24.91 |
| high | 197 | 5285.62 | 986.58 | 6.18 | 2.21 | 27.41 |
kable(as.data.frame(table(gender = business_staff$gender)), caption = "Distribución por género")
| gender | Freq |
|---|---|
| female | 488 |
| male | 512 |
¿Cómo se relaciona la felicidad con los ingresos y cambia esa relación según el nivel educativo?
ggplot(business_staff, aes(x = income, y = happiness)) +
geom_jitter(alpha = 0.35, height = 0.15) +
geom_smooth(method = "lm", se = TRUE) +
labs(title = "Felicidad e ingresos", x = "Ingresos mensuales (euros)",
y = "Felicidad (0-10)") +
theme_minimal()
ggplot(business_staff, aes(x = education, y = happiness)) +
geom_boxplot() +
labs(title = "Felicidad según nivel educativo", x = "Nivel educativo",
y = "Felicidad (0-10)") +
theme_minimal()
Los ingresos medios aumentan claramente con la educación (1621 <
3146 < 5286 euros en low, middle y
high), y la felicidad media también (4.52 < 5.61 <
6.18). El primer gráfico muestra una relación positiva con mucha
dispersión. El segundo muestra que, sin controlar por ingresos, la
felicidad aumenta con el nivel educativo. Más adelante se verá que esta
lectura cambia cuando se comparan personas con el mismo ingreso.
model1 <- lm(happiness ~ income, data = business_staff)
model2 <- lm(happiness ~ income + education, data = business_staff)
modelA <- lm(happiness ~ education, data = business_staff)
modelB <- lm(happiness ~ education + gender, data = business_staff)
modelC <- lm(happiness ~ education + gender + income, data = business_staff)
modelD <- lm(happiness ~ income * education, data = business_staff)
modelsummary(list("Model 1" = model1, "Model 2" = model2, "Model A" = modelA,
"Model B" = modelB, "Model C" = modelC, "Model D" = modelD),
estimate = "{estimate}{stars}",
statistic = "({std.error})",
stars = c("*" = 0.05, "**" = 0.01, "***" = 0.001),
gof_map = c("nobs", "r.squared", "adj.r.squared", "AIC", "BIC", "rmse"),
title = "Comparación de los modelos de regresión lineal")
| Model 1 | Model 2 | Model A | Model B | Model C | Model D | |
|---|---|---|---|---|---|---|
| (Intercept) | 3.501*** | 3.052*** | 4.520*** | 4.612*** | 3.112*** | 1.639*** |
| (0.156) | (0.179) | (0.148) | (0.163) | (0.192) | (0.467) | |
| income | 0.001*** | 0.001*** | 0.001*** | 0.002*** | ||
| (0.000) | (0.000) | (0.000) | (0.000) | |||
| educationmiddle | -0.293 | 1.090*** | 1.099*** | -0.282 | 1.260* | |
| (0.194) | (0.174) | (0.174) | (0.195) | (0.543) | ||
| educationhigh | -1.658*** | 1.662*** | 1.660*** | -1.649*** | 0.347 | |
| (0.328) | (0.216) | (0.216) | (0.328) | (0.920) | ||
| gendermale | -0.189 | -0.114 | ||||
| (0.140) | (0.130) | |||||
| income × educationmiddle | -0.001** | |||||
| (0.000) | ||||||
| income × educationhigh | -0.001** | |||||
| (0.000) | ||||||
| Num.Obs. | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 |
| R2 | 0.164 | 0.193 | 0.060 | 0.062 | 0.194 | 0.202 |
| R2 Adj. | 0.163 | 0.191 | 0.058 | 0.059 | 0.190 | 0.198 |
| RMSE | 2.08 | 2.04 | 2.20 | 2.20 | 2.04 | 2.03 |
| Modelo | Variables independientes | Característica principal |
|---|---|---|
| Modelo 1 | income |
Solo ingresos |
| Modelo 2 | income + education |
Ingresos y educación |
| Modelo A | education |
Solo educación |
| Modelo B | education + gender |
Educación y género |
| Modelo C | education + gender + income |
Educación, género e ingresos |
| Modelo D | income * education |
Ingresos, educación e interacción |
Cada euro adicional de ingreso mensual se asocia con 6.1^{-4} puntos más de felicidad, es decir, unos 0.61 puntos por cada 1.000 euros. El modelo explica el 16.4 % de la variabilidad de la felicidad, por lo que los ingresos importan pero dejan sin explicar la mayor parte.
Hay varios problemas al usar esta escala en una regresión lineal. La felicidad está acotada entre 0 y 10, pero el modelo puede predecir fuera de ese rango:
predict(model1, newdata = data.frame(income = 1000000))
## 1
## 616.6562
Con un millón de euros al mes, el modelo predice 617 puntos, un valor imposible. Además, la escala es subjetiva y discreta, y no tiene por qué haber la misma distancia entre un 4 y un 5 que entre un 8 y un 9. Por último, una asociación no demuestra que los ingresos causen mayor felicidad.
Con low como referencia, el Modelo A estima que los
niveles middle y high difieren de
low en 1.09 y 1.66 puntos respectivamente.
El género (Modelo B) apenas cambia estos coeficientes y su efecto es
pequeño e incierto (coeficiente de gendermale = -0.19).
Al añadir los ingresos (Modelo C), el R² pasa de 0.062 a 0.194 y el R² ajustado de 0.059 a 0.19; el AIC y el BIC también mejoran, por lo que los ingresos aportan mucha más información que el género.
Un matiz importante: la asociación de la educación con la felicidad
cambia de signo al controlar por ingresos. Sin
controlarlos (Modelo A), mayor educación va con mayor felicidad (1.66
puntos de high respecto a low). Con los
ingresos en el modelo (Modelo C), esa diferencia pasa a -1.65: a igual
ingreso, las personas con más educación declaran menos felicidad. Esto
es coherente con que la educación alta va asociada a ingresos mucho más
altos (ver tabla descriptiva): buena parte de la ventaja bruta en
felicidad de los más educados se debe a sus ingresos. Los coeficientes
de educación en el Modelo C son diferencias condicionadas al
ingreso y al género, y no deben leerse como el efecto total de la
educación.
¿Debe el modelo final incluir las tres variables? Atendiendo a los
criterios, el género no mejora el ajuste ni es relevante, mientras que
educación e ingresos sí; el modelo preferible entre A, B y C en términos
parsimoniosos sería el que contiene education e
income (Modelo 2). El Modelo C solo se justificaría si el
género interesara por razones teóricas.
Qué pasaría si considerasemos que los ingresos estan influidos por la educación, y afectan a la felicidad.
b <- coef(modelD)
pendientes <- c(low = unname(b["income"]),
middle = unname(b["income"] + b["income:educationmiddle"]),
high = unname(b["income"] + b["income:educationhigh"]))
kable(data.frame(educacion = names(pendientes),
pendiente_por_1000_euros = round(pendientes * 1000, 2)),
caption = "Pendiente estimada de la felicidad por cada 1.000 euros (Modelo D)")
| educacion | pendiente_por_1000_euros | |
|---|---|---|
| low | low | 1.78 |
| middle | middle | 0.86 |
| high | high | 0.79 |
anova(model2, modelD)
## Analysis of Variance Table
##
## Model 1: happiness ~ income + education
## Model 2: happiness ~ income * education
## Res.Df RSS Df Sum of Sq F Pr(>F)
## 1 996 4162.1
## 2 994 4117.1 2 45.008 5.4332 0.0045 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
El efecto de los ingresos es distinto según el nivel educativo: la
pendiente es mayor en low (1.78 puntos por 1.000 euros) que
en middle (0.86) y high (0.79). El contraste
ANOVA entre el Modelo 2 y el Modelo D da F = 5.43 con p = 0.0045, de
modo que la interacción mejora significativamente el ajuste. El R² sube
de 0.193 a 0.202, una mejora real pero modesta.
rejilla <- expand.grid(
income = seq(min(business_staff$income), max(business_staff$income), length.out = 100),
education = levels(business_staff$education))
pred_graf <- rbind(
transform(rejilla, modelo = "Modelo 2 (sin interacción)", happiness = predict(model2, rejilla)),
transform(rejilla, modelo = "Modelo D (con interacción)", happiness = predict(modelD, rejilla)))
ggplot(pred_graf, aes(x = income, y = happiness, colour = education)) +
geom_line(linewidth = 1.1) +
facet_wrap(~ modelo) +
labs(title = "Felicidad estimada según ingresos y educación",
x = "Ingresos mensuales (euros)", y = "Felicidad estimada",
colour = "Nivel educativo") +
theme_minimal()
Sin interacción las rectas son paralelas; con interacción la recta de
low es más empinada. Modelo elegido: el
Modelo D, porque el contraste formal respalda la interacción y mejora el
ajuste. Aun así, la mejora es moderada y el modelo es más complejo (y el
BIC, más exigente con la complejidad, penaliza su mayor número de
parámetros), así que el Modelo 2 sigue siendo una opción más sencilla
razonable si interesa la parsimonia.
cor_test <- cor.test(business_staff$income, business_staff$happiness, method = "pearson")
cor_test
##
## Pearson's product-moment correlation
##
## data: business_staff$income and business_staff$happiness
## t = 13.968, df = 998, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.3511973 0.4549734
## sample estimates:
## cor
## 0.4043861
La correlación es positiva (r = 0.404) y de magnitud moderada: las personas con más ingresos tienden a declarar mayor felicidad. El intervalo de confianza al 95 % va de 0.35 a 0.45, lo que indica que la asociación es claramente positiva y no pequeña. Su cuadrado, r² = 0.164, indica que ingresos y felicidad comparten aproximadamente el 16 % de su varianza, y el resto queda sin explicar.
El p-valor (<2e-16) indica que sería extremadamente raro observar una correlación así si en la población no hubiera relación lineal. Pero eso solo habla de la incertidumbre muestral, no de la importancia práctica ni de la causalidad: lo informativo es el tamaño de r y su intervalo, no que p cruce un umbral. Además, Pearson mide solo la relación lineal y es sensible a valores extremos.
t_test <- t.test(income ~ gender, data = business_staff)
t_test
##
## Welch Two Sample t-test
##
## data: income by gender
## t = 1.1798, df = 988.29, p-value = 0.2384
## alternative hypothesis: true difference in means between group female and group male is not equal to 0
## 95 percent confidence interval:
## -74.26356 298.19958
## sample estimates:
## mean in group female mean in group male
## 3287.693 3175.725
R aplica por defecto la prueba t de Welch, que no asume varianzas iguales. Las mujeres ganan de media 3288 euros y los hombres 3176, una diferencia observada de 112 euros a favor de las mujeres. Sin embargo, el intervalo de confianza al 95 % de la diferencia (-74 a 298 euros) incluye el cero, y el p-valor es 0.238. La diferencia es compatible tanto con una brecha en un sentido, en el otro, o con ninguna: la muestra no permite concluir una diferencia clara, pero tampoco demuestra que no exista.
tabla_edu_gen <- table(education = business_staff$education, gender = business_staff$gender)
kable(tabla_edu_gen, caption = "Frecuencias observadas")
| female | male | |
|---|---|---|
| low | 114 | 107 |
| middle | 270 | 312 |
| high | 104 | 93 |
chi <- chisq.test(tabla_edu_gen)
chi
##
## Pearson's Chi-squared test
##
## data: tabla_edu_gen
## X-squared = 3.2928, df = 2, p-value = 0.1927
kable(round(chi$expected, 1), caption = "Frecuencias esperadas bajo independencia")
| female | male | |
|---|---|---|
| low | 107.8 | 113.2 |
| middle | 284.0 | 298.0 |
| high | 96.1 | 100.9 |
kable(round(chi$stdres, 2), caption = "Residuos estandarizados")
| female | male | |
|---|---|---|
| low | 0.94 | -0.94 |
| middle | -1.80 | 1.80 |
| high | 1.25 | -1.25 |
v_cramer <- sqrt(unname(chi$statistic) / (sum(tabla_edu_gen) * (min(dim(tabla_edu_gen)) - 1)))
Las frecuencias observadas se parecen a las esperadas si no hubiera asociación; los residuos estandarizados son pequeños (ninguno supera claramente ±2). El estadístico es X² = 3.29 con 2 grados de libertad y p = 0.193, y la V de Cramér es de solo 0.057. La asociación observada entre educación y género es, por tanto, muy débil y compatible con el azar muestral. Con estos datos no hay evidencia de que la distribución educativa difiera entre hombres y mujeres, aunque eso no excluye diferencias pequeñas.
Estructura. Lo más importante fue decidir qué es una
observación en cada tabla (Wickham, 2014). children y
kid_fam tienen como unidad al hijo; families a
la familia. Al intentar llevar las edades de los hijos a la tabla de
familias hubo que agregar antes (media por familia),
una decisión que afecta al resultado: la edad media por familia no es lo
mismo que la edad media por hijo, porque las familias numerosas pesan
menos. También fue clave comprobar que family_id es único
en families y que la fusión conserva los 80.486 hijos.
Mantener los datos originales intactos, con diccionario y nombres
consistentes, sigue las recomendaciones de Broman y Woo (2018).
Error de medida (Innes et al., 2021). Un ejemplo concreto está en las fechas históricas:
fechas_nac <- c(families_raw$husband_birth_date, families_raw$wife_birth_date,
children_raw$child_birth_date)
fechas_nac <- fechas_nac[!is.na(fechas_nac)]
prop_enero <- mean(month(fechas_nac) == 1 & mday(fechas_nac) == 1)
c(observada = prop_enero, esperada_si_aleatoria = 1 / 365.25)
## observada esperada_si_aleatoria
## 0.141253309 0.002737851
El 14.1 % de las fechas de nacimiento caen el 1 de enero, frente al
~0,27 % esperable si los días se repartieran al azar. Esto sugiere que,
cuando solo se conocía el año, se imputó el 1 de enero. Es un error de
medida en las fechas de nacimiento que añade ruido a
mother_age_at_birth y father_age_at_birth:
cada edad puede desviarse hasta casi un año, y como no está distribuido
por igual entre familias (los registros más antiguos probablemente
tienen más imputaciones), puede afectar a comparaciones entre épocas.
Las medias globales son poco sensibles, pero la dispersión y las
comparaciones finas sí lo son.
En business_staff, la variable happiness es
autodeclarada y se mide con una escala discreta de 0 a 10; las personas
pueden usar la escala de forma distinta. Un error de medida no
sistemático en una variable resultado aumenta el ruido y reduce la
capacidad explicativa (parte del bajo R²), mientras que un error en
income (la variable explicativa) tendería a
atenuar su coeficiente hacia cero. Por eso, las
magnitudes estimadas (por ejemplo, el efecto por cada 1.000 euros) deben
leerse como aproximaciones y no como efectos exactos.
sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: Europe/Madrid
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] knitr_1.52 modelsummary_2.6.0 lubridate_1.9.5
## [4] ggplot2_4.0.3 dplyr_1.2.1 data.table_1.18.6.1
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 lattice_0.22-9 digest_0.6.39
## [5] magrittr_2.0.5 evaluate_1.0.5 grid_4.6.1 timechange_0.4.0
## [9] RColorBrewer_1.1-3 fastmap_1.2.0 jsonlite_2.0.0 Matrix_1.7-5
## [13] backports_1.5.1 mgcv_1.9-4 scales_1.4.0 jquerylib_0.1.4
## [17] cli_3.6.6 rlang_1.3.0 performance_0.19.0 splines_4.6.1
## [21] tinytable_0.19.0 withr_3.0.3 cachem_1.1.0 yaml_2.3.12
## [25] otel_0.2.0 datawizard_1.4.1 tools_4.6.1 checkmate_2.3.4
## [29] bayestestR_0.20.0 vctrs_0.7.3 R6_2.6.1 zoo_1.9-1
## [33] lifecycle_1.0.5 insight_1.7.0 pkgconfig_2.0.3 pillar_1.11.1
## [37] bslib_0.12.0 gtable_0.3.6 glue_1.8.1 xfun_0.61
## [41] tibble_3.3.1 lmtest_0.9-40 tidyselect_1.2.1 rstudioapi_0.19.0
## [45] parameters_0.30.0 farver_2.1.2 htmltools_0.5.9 nlme_3.1-169
## [49] tables_0.9.35 rmarkdown_2.32 labeling_0.4.3 compiler_4.6.1
## [53] S7_0.2.2