library(httr)
library(jsonlite)
library(leaflet)
library(dplyr)
library(stringr)
library(tibble)
library(readr)
library(ggplot2)
library(sf)
library(scales)
library(tidyr)
library(geodata)
library(giscoR)
library(car)
library(RColorBrewer)
library(FNN)
library(readxl)
library(stringi)
# API endpoint
api_url <- "https://data.culture.gouv.fr/api/records/1.0/search/?dataset=liste-et-localisation-des-musees-de-france&rows=10000"
response <- GET(api_url)
data <- fromJSON(content(response, "text"))
museums <- data$records$fields
museums <- museums %>%
mutate(
latitude = as.numeric(latitude),
longitude = as.numeric(longitude)
)
leaflet(museums) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitude,
lat = ~latitude,
radius = 5,
color = "#2E7D32",
fillOpacity = 0.7,
popup = ~paste0("<b>", nom_officiel_du_musee, "</b><br>",
adresse, "<br>",
code_postal, " ", commune, "<br>",
departement),
clusterOptions = markerClusterOptions()
) %>%
addLegend(
position = "bottomright",
colors = "#2E7D32",
labels = "Musées de France",
title = "Répertoire des Musées de France"
)
enef24 <- read_delim("C:/Users/gari4/OneDrive/Escritorio/UC3M clases/2cuatri UC3M/geografia electoral/data-raw/fr_cdsp_ddi_enef2024_v7.csv", delim = ";")
museums <- museums %>%
mutate(
code_postal = as.character(code_postal),
dep_code = case_when(
str_detect(code_postal, "^(97|98)") ~ str_sub(code_postal, 1, 3),
TRUE ~ str_sub(code_postal, 1, 2)
)
)
museums_dep <- museums %>%
filter(!is.na(dep_code), dep_code != "") %>%
count(dep_code, name = "n_museums")
enef22 <- enef24 %>%
transmute(
dep_code = as.character(YYREG13),
pres22b = suppressWarnings(as.integer(Y7PRES22B)),
w_pub = suppressWarnings(as.numeric(Y7PPUB))
) %>%
mutate(
dep_code = case_when(
nchar(dep_code) == 1 ~ str_pad(dep_code, 2, pad = "0"),
TRUE ~ dep_code
)
) %>%
filter(!is.na(dep_code), dep_code != "", !is.na(pres22b))
enef22_expr_dep <- enef22 %>%
filter(pres22b %in% c(1, 2)) %>%
group_by(dep_code) %>%
summarise(
n = n(),
macron_share = mean(pres22b == 1),
lepen_share = mean(pres22b == 2),
macron_share_w = if (all(is.na(w_pub))) NA_real_ else
weighted.mean(pres22b == 1, w = w_pub, na.rm = TRUE),
.groups = "drop"
)
fr_adm2 <- geodata::gadm(country = "FRA", level = 2, path = tempdir())
fr_adm2_sf <- st_as_sf(fr_adm2) %>% st_transform(4326)
dep_code_field <- intersect(c("CC_2","GID_2","HASC_2"), names(fr_adm2_sf))[1]
fr_deps <- fr_adm2_sf %>%
mutate(
dep_code = case_when(
dep_code_field == "CC_2" ~ as.character(.data[["CC_2"]]),
dep_code_field == "HASC_2" ~ str_extract(.data[["HASC_2"]], "[0-9]{2,3}$"),
dep_code_field == "GID_2" ~ str_extract(.data[["GID_2"]], "[0-9]{2,3}$"),
TRUE ~ NA_character_
),
dep_code = case_when(
!is.na(dep_code) & nchar(dep_code) == 1 ~ str_pad(dep_code, 2, pad = "0"),
TRUE ~ dep_code
)
)
deps_join <- fr_deps %>%
left_join(museums_dep, by = "dep_code") %>%
left_join(enef22_expr_dep, by = "dep_code") %>%
mutate(n_museums = replace_na(n_museums, 0))
domain_ok <- deps_join$macron_share[is.finite(deps_join$macron_share)]
pal_macron <- colorNumeric("Blues", domain = domain_ok, na.color = "#f0f0f0")
leaflet(deps_join) %>%
addTiles() %>%
addPolygons(
fillColor = ~pal_macron(macron_share),
fillOpacity = 0.75,
color = "white", weight = 1, opacity = 1,
label = ~paste0(
"dep_code: ", dep_code, "\n",
"Macron share: ", ifelse(is.na(macron_share), "NA", percent(macron_share, 0.1)), "\n",
"Le Pen share: ", ifelse(is.na(lepen_share), "NA", percent(lepen_share, 0.1)), "\n",
"Museos: ", n_museums, "\n",
"n (expresada): ", ifelse(is.na(n), "NA", n)
)
) %>%
addLegend(
position = "bottomright",
pal = pal_macron,
values = domain_ok,
title = "Macron share (ENEF, base expresada)"
)
regions_sf <- gisco_get_nuts(
country = "FR",
nuts_level = 2,
year = 2021,
resolution = "20"
) |>
st_transform(4326) |>
transmute(nuts2 = NUTS_ID, region_name = NAME_LATN, geometry)
museums_sf <- museums |>
mutate(
latitude = as.numeric(latitude),
longitude = as.numeric(longitude)
) |>
filter(is.finite(latitude), is.finite(longitude)) |>
st_as_sf(coords = c("longitude", "latitude"), crs = 4326)
museums_region <- st_join(museums_sf, regions_sf) |>
st_drop_geometry() |>
count(nuts2, name = "n_museums")
regions_data <- regions_sf |>
left_join(museums_region, by = "nuts2") |>
mutate(n_museums = replace_na(n_museums, 0))
pal <- colorNumeric("Blues", domain = regions_data$n_museums)
leaflet(regions_data) |>
addTiles() |>
addPolygons(
fillColor = ~pal(n_museums),
fillOpacity = 0.75,
color = "white", weight = 1,
label = ~paste0(region_name, "\nMuseos: ", n_museums)
) |>
addLegend(
position = "bottomright",
pal = pal,
values = ~n_museums,
title = "Entorno cultural (conteo de museos)"
)
reg_map <- tibble::tribble(
~YYREG13, ~nuts2,
11, "FR10",
24, "FRB0",
27, "FRC1",
28, "FRD0",
32, "FRE1",
44, "FRF1",
52, "FRG0",
53, "FRH0",
75, "FRI1",
76, "FRJ1",
84, "FRK2",
93, "FRL0",
94, "FRM0"
)
model_data <- enef24 |>
transmute(
YYREG13 = as.integer(YYREG13),
vote22 = as.integer(Y7PRES22B),
agglom = YYAGGLO9,
eco = YYECO2,
intpol = Y7INTPOL,
age = YYAGE_r,
edu = YYEDU,
sex = YYSEXE
) |>
left_join(reg_map, by = "YYREG13") |>
left_join(museums_region, by = "nuts2") |>
mutate(
macron_vote = ifelse(vote22 == 1, 1, ifelse(vote22 == 2, 0, NA)),
n_museums = replace_na(n_museums, 0),
agglom = as.factor(agglom),
eco = as.factor(eco),
intpol = as.factor(intpol),
edu = as.factor(edu),
sex = as.factor(sex)
) |>
filter(!is.na(macron_vote))
m3 <- glm(
macron_vote ~ n_museums + age + sex + edu + eco + agglom + intpol,
data = model_data,
family = binomial(link = "logit")
)
summary(m3)
##
## Call:
## glm(formula = macron_vote ~ n_museums + age + sex + edu + eco +
## agglom + intpol, family = binomial(link = "logit"), data = model_data)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.3671340 0.3590383 -3.808 0.000140 ***
## n_museums -0.0019136 0.0008184 -2.338 0.019367 *
## age 0.1778501 0.0168725 10.541 < 2e-16 ***
## sex2 0.1067150 0.0494826 2.157 0.031036 *
## edu2 -0.1320192 0.3089280 -0.427 0.669127
## edu3 -0.1945825 0.2819765 -0.690 0.490152
## edu4 0.1267062 0.2818434 0.450 0.653026
## edu5 0.4205219 0.2827466 1.487 0.136942
## edu6 0.9368921 0.2856933 3.279 0.001040 **
## edu7 1.3471304 0.2894595 4.654 3.26e-06 ***
## eco2 -0.4818937 0.2599621 -1.854 0.063781 .
## eco3 -0.1127928 0.2354577 -0.479 0.631913
## eco4 -0.2672069 0.2239658 -1.193 0.232842
## eco5 -0.2913097 0.2165207 -1.345 0.178492
## eco6 0.0022640 0.2137105 0.011 0.991547
## eco7 -0.0911995 0.2108982 -0.432 0.665426
## eco8 0.0590126 0.2151556 0.274 0.783871
## eco9 -0.0891769 0.2059946 -0.433 0.665081
## eco10 -0.0433975 0.2053369 -0.211 0.832616
## eco11 0.1173713 0.2083558 0.563 0.573216
## eco12 0.1173558 0.2066252 0.568 0.570059
## eco13 0.3616001 0.2228239 1.623 0.104631
## eco14 0.2492867 0.2382810 1.046 0.295474
## eco15 -0.1841820 0.3541327 -0.520 0.602999
## eco16 -0.2519481 0.2120590 -1.188 0.234793
## agglom1 -0.1049662 0.1059186 -0.991 0.321682
## agglom2 0.1065141 0.1053999 1.011 0.312222
## agglom3 0.0194403 0.1122343 0.173 0.862485
## agglom4 0.2389318 0.1053622 2.268 0.023346 *
## agglom5 0.2241072 0.0945952 2.369 0.017831 *
## agglom6 0.4468343 0.1175661 3.801 0.000144 ***
## agglom7 0.3924590 0.0702272 5.588 2.29e-08 ***
## agglom8 0.6337420 0.1045685 6.061 1.36e-09 ***
## intpol2 0.3327460 0.0557361 5.970 2.37e-09 ***
## intpol3 0.3139898 0.0739971 4.243 2.20e-05 ***
## intpol4 -0.0112910 0.1140196 -0.099 0.921117
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 10892 on 8060 degrees of freedom
## Residual deviance: 10164 on 8025 degrees of freedom
## AIC: 10236
##
## Number of Fisher Scoring iterations: 4
exp(coef(m3)) # Odds ratios
## (Intercept) n_museums age sex2 edu2 edu3
## 0.2548363 0.9980882 1.1946462 1.1126171 0.8763241 0.8231782
## edu4 edu5 edu6 edu7 eco2 eco3
## 1.1350835 1.5227560 2.5520377 3.8463722 0.6176127 0.8933358
## eco4 eco5 eco6 eco7 eco8 eco9
## 0.7655147 0.7472842 1.0022666 0.9128355 1.0607886 0.9146837
## eco10 eco11 eco12 eco13 eco14 eco15
## 0.9575307 1.1245369 1.1245195 1.4356248 1.2831098 0.8317844
## eco16 agglom1 agglom2 agglom3 agglom4 agglom5
## 0.7772851 0.9003549 1.1123936 1.0196305 1.2698919 1.2512052
## agglom6 agglom7 agglom8 intpol2 intpol3 intpol4
## 1.5633552 1.4806172 1.8846497 1.3947930 1.3688758 0.9887725
exp(confint(m3)) # Intervalos de confianza
## 2.5 % 97.5 %
## (Intercept) 0.1261696 0.517122
## n_museums 0.9964878 0.999690
## age 1.1558643 1.234906
## sex2 1.0098080 1.225981
## edu2 0.4758097 1.604443
## edu3 0.4706890 1.429930
## edu4 0.6492011 1.971227
## edu5 0.8694391 2.649176
## edu6 1.4490187 4.465680
## edu7 2.1683919 6.780671
## eco2 0.3700046 1.026302
## eco3 0.5621211 1.416366
## eco4 0.4925110 1.186418
## eco5 0.4877816 1.141342
## eco6 0.6578827 1.522567
## eco7 0.6024148 1.378973
## eco8 0.6943787 1.616123
## eco9 0.6093633 1.368452
## eco10 0.6387167 1.430697
## eco11 0.7457872 1.690315
## eco12 0.7482252 1.684427
## eco13 0.9262218 2.221315
## eco14 0.8039782 2.048436
## eco15 0.4185812 1.685206
## eco16 0.5117703 1.176796
## agglom1 0.7314482 1.108060
## agglom2 0.9050159 1.368208
## agglom3 0.8184166 1.270940
## agglom4 1.0334577 1.562160
## agglom5 1.0398041 1.506710
## agglom6 1.2431586 1.971417
## agglom7 1.2903289 1.699291
## agglom8 1.5358698 2.314170
## intpol2 1.2505485 1.555942
## intpol3 1.1843865 1.583004
## intpol4 0.7905612 1.236339
vif(m3) # Diagnóstico de multicolinealidad
## GVIF Df GVIF^(1/(2*Df))
## n_museums 1.647424 1 1.283520
## age 1.211880 1 1.100854
## sex 1.087224 1 1.042700
## edu 1.369701 6 1.026563
## eco 1.238125 15 1.007145
## agglom 1.735060 8 1.035040
## intpol 1.120765 3 1.019184
En el modelo logístico, un menor número de museos en la región está asociado con una menor probabilidad de votar por Macron, y esta asociación es estadísticamente significativa, incluso controlando por edad, sexo, educación, situación económica, entorno urbano-rural e interés político.
El modelo logístico está bien especificado y no presenta problemas técnicos relevantes.
La reducción de la deviance (de 10 892 a 10 164) y un AIC de 10 236 indican una mejora sustantiva frente al modelo nulo. No es un modelo “perfecto” (no lo debe ser), pero sí informativo.
El diagnóstico de multicolinealidad es muy limpio. Todos los GVIF ajustados están muy por debajo de 2, lo que indica que los coeficientes son estables y no están inflados por colinealidad entre covariables. En particular, n_museums tiene un GVIF ajustado de 1.28, completamente aceptable.
Conclusión: el modelo es estadísticamente sano y las interpretaciones son fiables.
Este es el coeficiente clave:
coeficiente logit de n_museums: −0.0019
p-valor: 0.019
odds ratio: 0.998
IC 95 % (odds ratio): [0.9965, 0.9997]
interpretación correcta
Manteniendo constantes edad, sexo, educación, situación económica, entorno urbano–rural e interés por la política, un mayor número de museos en la región de residencia se asocia con una ligera pero estadísticamente significativa reducción en la probabilidad de votar por Macron en la segunda vuelta de 2022.
En términos sustantivos:
cada museo adicional reduce las odds de votar por Macron en aproximadamente 0.2 %;
una diferencia de 100 museos entre dos regiones se asociaría con una reducción aproximada del 18–20 % en las odds de votar por Macron.
Este efecto es pequeño en magnitud, pero:
robusto a controles,
estadísticamente significativo,
y contrario a la expectativa inicial de que un entorno culturalmente activo favorecería el voto por Macron.
Este es un hallazgo interesante, no un problema: obliga a reinterpretar el concepto de “actividad cultural” como algo distinto de “capital cultural pro-Macron”.
El efecto de la edad es fuerte, positivo y altamente significativo.
OR ≈ 1.19 por unidad de edad recodificada
p < 0.001
Interpretación: a mayor edad, mayor probabilidad de votar por Macron frente a Le Pen. Este es un resultado esperado y consistente con la literatura sobre voto generacional en Francia.
sexo
sex2 (probablemente mujeres, según codificación ENEF) presenta:
OR ≈ 1.11
p ≈ 0.03
Las mujeres muestran una probabilidad ligeramente mayor de votar por Macron que los hombres, controlando por el resto de variables.
educación
Aquí el patrón es muy claro y sustantivo:
niveles educativos altos (edu6, edu7) tienen efectos muy fuertes y significativos;
OR ≈ 2.55 y 3.85 respectivamente;
intervalos de confianza amplios pero claramente por encima de 1.
Interpretación: la educación es uno de los principales determinantes estructurales del voto por Macron. Este resultado domina claramente al efecto de los museos y refuerza la idea de que el voto macronista está más asociado a capital educativo que a mera exposición cultural territorial.
situación económica
La situación económica (eco) no muestra un patrón claro ni sistemático:
la mayoría de los coeficientes no son significativos;
algunos efectos negativos débiles en categorías bajas, pero sin robustez estadística fuerte.
Esto sugiere que, una vez controlados educación y edad, la situación económica declarada no es un predictor central del voto Macron–Le Pen en esta especificación.
entorno urbano–rural (aglomeración)
Aquí hay un resultado muy importante:
categorías urbanas más grandes (agglom4 a agglom8) tienen efectos positivos, crecientes y altamente significativos;
OR que van de ~1.25 hasta ~1.88.
Interpretación: vivir en entornos urbanos densos aumenta de manera sustantiva la probabilidad de votar por Macron, independientemente del número de museos, la educación o la edad.
Este resultado es clave para reinterpretar el efecto negativo de los museos.
interés en la política
El interés político (intpol2 y intpol3) tiene efectos positivos claros:
OR ≈ 1.37–1.39
p < 0.001
Esto confirma que Macron atrae con mayor probabilidad a votantes políticamente más interesados, lo cual es consistente con su perfil tecnocrático y proinstitucional.
Este es el punto analítico más importante.
El efecto negativo de n_museums no significa que la cultura “vaya contra Macron”. Lo que sugiere, más bien, es lo siguiente:
El número de museos no mide capital cultural individual, sino densidad institucional cultural.
Regiones con muchos museos suelen ser:
grandes, históricamente centrales, culturalmente consolidadas, y, a menudo, políticamente más diversas o más polarizadas.
Una vez que controlas:
urbanización, educación, edad, interés político, el efecto “residual” de vivir en una región con muchos museos puede capturar:
saturación cultural, turistificación, o heterogeneidad social interna, más que una socialización cultural pro-Macron.
Dicho de forma clara para un trabajo académico:
The findings suggest that the association between cultural institutional density and voting behaviour is not straightforward. Once individual socio-demographic and attitudinal factors are accounted for, regions with a higher concentration of museums do not exhibit higher support for Macron; on the contrary, a small but significant negative association emerges.
Puedes cerrar el apartado de resultados con algo como:
Contrary to initial expectations, the analysis does not provide evidence that residing in a culturally active regional environment, proxied by the density of museums, increases the likelihood of voting for Emmanuel Macron in the second round of the 2022 presidential election. While education, age, urban residence and political interest strongly and positively predict Macron support, the contextual cultural indicator displays a small but statistically significant negative association. This finding suggests that cultural institutional presence at the regional level captures structural territorial characteristics rather than individual cultural capital.
El primer mapa sirve de muy poco porque no ilumina realmente como afectó (o no) la presencia de museos en el voto de las presidenciales de 2022. Por ello se realizaron otros dos mapas que sí sirven para medir el efecto. Aquí veremos ya la mezcla entre el mapa de museos y los datos del ENEF.
Un mapa bivariado cruza:
eje 1: densidad de museos (alto/bajo)
eje 2: porcentaje de voto Macron (alto/bajo)
Esto NO es causal, pero permite ver si hay patrones espaciales coherentes o contradictorios.
Este mapa responde visualmente:
Eso ya conecta directamente con el resultado negativo.
¿Cuándo usarlo?
sección descriptiva
antes del modelo
para motivar el resultado “inesperado”
## ===============================
## MAPA BIVARIADO (MACRON + LEYENDA EN ESPAÑOL)
## ===============================
## 1) Agregado regional del voto Macron (base expresada)
vote_region <- model_data |>
group_by(nuts2) |>
summarise(macron_share = mean(macron_vote), .groups = "drop")
## 2) Dataset regional final (solo 13 regiones ENEF)
bivar_data <- regions_sf |>
filter(nuts2 %in% reg_map$nuts2) |>
left_join(museums_region, by = "nuts2") |>
left_join(vote_region, by = "nuts2") |>
mutate(n_museums = replace_na(n_museums, 0))
## 3) Cortes alto/bajo (medianas)
m_cut <- median(bivar_data$n_museums, na.rm = TRUE)
v_cut <- median(bivar_data$macron_share, na.rm = TRUE)
bivar_data <- bivar_data |>
mutate(
museos_cat = ifelse(n_museums >= m_cut, "muchos museos", "pocos museos"),
macron_cat = ifelse(macron_share >= v_cut, "macron fuerte", "macron débil"),
clase_es = paste(museos_cat, "•", macron_cat)
)
## 4) Paleta bivariada con "sabor Macron" (azules)
bivar_pal_es <- c(
"pocos museos • macron débil" = "#deebf7",
"muchos museos • macron débil" = "#9ecae1",
"pocos museos • macron fuerte" = "#3182bd",
"muchos museos • macron fuerte"= "#08519c"
)
bivar_data <- bivar_data |>
mutate(fill_col = unname(bivar_pal_es[clase_es]))
## 5) Mapa
leaflet(bivar_data) |>
addTiles() |>
addPolygons(
fillColor = ~fill_col,
fillOpacity = 0.85,
color = "white",
weight = 1,
label = ~paste0(
"región: ", nuts2, "\n",
"voto macron: ", percent(macron_share, 0.1), "\n",
"museos: ", n_museums, "\n",
"lectura: ", clase_es
)
) |>
addLegend(
position = "bottomright",
colors = unname(bivar_pal_es),
labels = names(bivar_pal_es),
title = "museos y voto macron"
)
El mapa 2 es un mapa de residuos.
Este es el mapa correcto si quieres que el lector entienda el resultado del logit.
Qué haces:
Predices la probabilidad de votar Macron sin museos
Predices la probabilidad con museos
Calculas el residuo regional
Mapeas ese residuo
Eso muestra:
Regiones donde Macron rinde más o menos de lo esperado dado su contexto social, y cómo eso se relaciona con museos.
Este mapa sí representa el efecto neto.
## ===============================
## MAPA RESIDUOS (PERIODÍSTICO + MUSEOS EN TOOLTIP)
## ===============================
## 1) Modelo SIN museos (baseline)
m0 <- glm(
macron_vote ~ age + sex + edu + eco + agglom + intpol,
data = model_data,
family = binomial(link = "logit")
)
## 2) Residuos individuales
model_data_res <- model_data |>
mutate(
pred_macron = predict(m0, type = "response"),
resid = macron_vote - pred_macron
)
## 3) Residuos promedio por región + n
resid_region <- model_data_res |>
group_by(nuts2) |>
summarise(
mean_resid = mean(resid),
n = n(),
.groups = "drop"
)
## 4) Junta espacial (solo 13 regiones ENEF) + museos
resid_map <- regions_sf |>
filter(nuts2 %in% reg_map$nuts2) |>
left_join(resid_region, by = "nuts2") |>
left_join(museums_region, by = "nuts2") |>
mutate(
n_museums = replace_na(n_museums, 0),
lectura = case_when(
mean_resid > 0 ~ "más apoyo a macron de lo esperado",
mean_resid < 0 ~ "menos apoyo a macron de lo esperado",
TRUE ~ "igual a lo esperado"
)
)
## 5) Paleta divergente, centrada en 0 y simétrica
max_abs <- max(abs(resid_map$mean_resid), na.rm = TRUE)
pal_resid <- colorNumeric(
palette = brewer.pal(11, "RdBu"), # rojo = menos, azul = más
domain = c(-max_abs, max_abs),
na.color = "#d9d9d9"
)
## 6) Etiquetas de leyenda (texto + escala numérica)
legend_vals <- c(-max_abs, 0, max_abs)
legend_labs <- c(
paste0("Menos de lo esperado (", round(-max_abs, 2), ")"),
"Como se esperaba (0.00)",
paste0("Más de lo esperado (+", round(max_abs, 2), ")")
)
## 7) Mapa
leaflet(resid_map) |>
addTiles() |>
addPolygons(
fillColor = ~pal_resid(mean_resid),
fillOpacity = 0.88,
color = "#ffffff",
weight = 1.5,
opacity = 1,
label = ~paste0(
"región: ", nuts2, "\n",
"lectura: ", lectura, "\n",
"diferencia vs esperado: ", round(mean_resid, 3), "\n",
"museos en la región: ", n_museums, "\n",
"casos (base expresada): ", n
)
) |>
addLegend(
position = "bottomright",
colors = pal_resid(legend_vals),
labels = legend_labs,
title = "Apoyo a Macron vs esperado\n(residuo del modelo)"
)
Interpretación de Chat: El mapa muestra en qué regiones Emmanuel Macron obtuvo más o menos apoyo del que cabría esperar una vez considerados factores como la edad, el nivel educativo, la situación económica, el entorno urbano o rural y el interés por la política. Las zonas en azul indican regiones donde Macron superó esas expectativas, mientras que las zonas en rojo señalan territorios donde su apoyo fue inferior al previsto. El patrón sugiere que el desempeño electoral de Macron no sigue de manera directa la distribución de las instituciones culturales: varias regiones con una fuerte presencia de museos registran un apoyo menor al esperado, lo que indica que la densidad cultural regional, por sí sola, no se traduce automáticamente en un mayor respaldo electoral.
Los mapas, como vimos, no incluyen información de todo Francia. Lo anterior es porque los datos del ENEF no contienen datos de todos los estados de Francia, sólo incluyen de 13, lo cual limita la capacidad de observar esto en todo el país. No obstante se realizó un mapa más con la ayuda de los datos electorales de las elecciones presidenciales de 2022 publicados por el gobierno de Francia.
ruta_excel <- "C:/Users/gari4/OneDrive/Escritorio/UC3M clases/2cuatri UC3M/geografia electoral/resultats-par-niveau-subcom-t2-france-entiere.xlsx"
normalizar_txt <- function(x) {
x |>
stri_trans_general("Latin-ASCII") |>
str_to_lower() |>
str_replace_all("[^a-z0-9]+", " ") |>
str_squish()
}
raw <- read_excel(ruta_excel, col_names = FALSE)
names(raw) <- paste0("V", seq_len(ncol(raw)))
elec_com <- raw |>
transmute(
code_dep = as.character(V1),
dep_label = as.character(V2),
code_com = as.character(V3),
com_label = as.character(V4),
inscrits = as.numeric(V6),
exprimes = as.numeric(V17),
nom1 = as.character(V22),
prenom1 = as.character(V23),
voix1 = as.numeric(V24),
pctexp1 = as.numeric(V26),
nom2 = as.character(V29),
prenom2 = as.character(V30),
voix2 = as.numeric(V31),
pctexp2 = as.numeric(V33)
) |>
mutate(
code_dep = str_trim(code_dep),
dep_label = str_trim(dep_label),
macron_voix = case_when(
nom1 == "MACRON" ~ voix1,
nom2 == "MACRON" ~ voix2,
TRUE ~ NA_real_
),
lepen_voix = case_when(
nom1 == "LE PEN" ~ voix1,
nom2 == "LE PEN" ~ voix2,
TRUE ~ NA_real_
)
) |>
filter(
str_detect(code_dep, "^(0[1-9]|[1-8][0-9]|9[0-5]|2A|2B|97[1-6])$")
)
## Agregar resultados por departamento
result_dep <- elec_com |>
group_by(code_dep, dep_label) |>
summarise(
inscrits = sum(inscrits, na.rm = TRUE),
exprimes = sum(exprimes, na.rm = TRUE),
macron_voix = sum(macron_voix, na.rm = TRUE),
lepen_voix = sum(lepen_voix, na.rm = TRUE),
.groups = "drop"
) |>
mutate(
macron_pctexp = 100 * macron_voix / exprimes,
lepen_pctexp = 100 * lepen_voix / exprimes,
macron_margen = macron_pctexp - lepen_pctexp,
dep_label_norm = normalizar_txt(dep_label)
)
## Descargar shapefile de Francia a nivel departamental
deps_sf <- geodata::gadm(country = "FRA", level = 2, path = tempdir()) |>
st_as_sf() |>
mutate(dep_label_norm = normalizar_txt(NAME_2))
## Unir resultados electorales con shapefile
map_dep <- deps_sf |>
left_join(result_dep, by = "dep_label_norm")
## Agregar museos por departamento
museums_dep_gov <- museums |>
mutate(
dep_label = as.character(departement),
dep_label = str_trim(dep_label),
dep_label_norm = normalizar_txt(dep_label)
) |>
filter(!is.na(dep_label_norm), dep_label_norm != "") |>
count(dep_label_norm, name = "n_museums")
## Unir museos al mapa electoral departamental
map_dep_museums <- map_dep |>
mutate(dep_label_norm = normalizar_txt(dep_label)) |>
left_join(museums_dep_gov, by = "dep_label_norm") |>
mutate(n_museums = replace_na(n_museums, 0))
## MAPA COMBINADO: margen electoral + museos en tooltip
pal_margen <- colorNumeric(
palette = colorRampPalette(c("gold", "#f7f7f7", "#2166ac"))(11),
domain = map_dep_museums$macron_margen,
na.color = "#d9d9d9"
)
leaflet(map_dep_museums) |>
addTiles() |>
addPolygons(
fillColor = ~pal_margen(macron_margen),
fillOpacity = 0.88,
color = "white",
weight = 0.8,
label = ~paste0(
"departamento: ", dep_label, "\n",
"ventaja de Macron sobre Le Pen: ", round(macron_margen, 1), " puntos\n",
"Macron: ", round(macron_pctexp, 1), "%\n",
"Le Pen: ", round(lepen_pctexp, 1), "%\n",
"museos en el departamento: ", n_museums
)
) |>
addLegend(
position = "bottomright",
pal = pal_margen,
values = ~macron_margen,
title = "Ventaja de Macron sobre Le Pen"
)
## 1 convertir museos a objeto espacial
museums_sf <- museums |>
mutate(
latitude = as.numeric(latitude),
longitude = as.numeric(longitude)
) |>
filter(is.finite(latitude), is.finite(longitude)) |>
st_as_sf(coords = c("longitude","latitude"), crs = 4326)
## 2 centroides de departamentos
deps_centroid <- map_dep_museums |>
st_centroid() |>
select(code_dep, dep_label, geometry)
## 3 extraer coordenadas
coords_dep <- st_coordinates(deps_centroid)
coords_museums <- st_coordinates(museums_sf)
## 4 distancia al museo más cercano
nearest <- FNN::get.knnx(
coords_museums,
coords_dep,
k = 1
)
deps_centroid$dist_museo <- nearest$nn.dist[,1]
## 5 unir distancia al mapa departamental
map_dep_dist <- map_dep_museums |>
left_join(
deps_centroid |>
st_drop_geometry() |>
select(code_dep, dist_museo),
by = "code_dep"
)
## 6 mapa de distancia a museos
pal_dist <- colorNumeric(
palette = "viridis",
domain = map_dep_dist$dist_museo,
na.color = "#d9d9d9"
)
leaflet(map_dep_dist) |>
addTiles() |>
addPolygons(
fillColor = ~pal_dist(dist_museo),
fillOpacity = 0.85,
color = "white",
weight = 0.8,
label = ~paste0(
"departamento: ", dep_label, "\n",
"distancia al museo más cercano: ", round(dist_museo,2), "\n",
"Macron (% votos válidos): ", round(macron_pctexp,1), "%\n",
"Le Pen (% votos válidos): ", round(lepen_pctexp,1), "%\n",
"museos en el departamento: ", n_museums
)
) |>
addLegend(
position = "bottomright",
pal = pal_dist,
values = ~dist_museo,
title = "Distancia al museo más cercano"
)
## 1) Museos como puntos sf
museums_sf <- museums |>
mutate(
latitude = as.numeric(latitude),
longitude = as.numeric(longitude)
) |>
filter(is.finite(latitude), is.finite(longitude)) |>
st_as_sf(coords = c("longitude", "latitude"), crs = 4326)
## 2) Centroides de departamentos
deps_centroid <- map_dep_museums |>
st_transform(4326) |>
st_centroid()
## 3) Coordenadas
coords_dep <- st_coordinates(deps_centroid)
coords_museums <- st_coordinates(museums_sf)
## 4) Distancia al museo más cercano
nearest <- FNN::get.knnx(
data = coords_museums,
query = coords_dep,
k = 1
)
deps_centroid <- deps_centroid |>
mutate(dist_museo = nearest$nn.dist[, 1])
## 5) Pegar distancia a la base departamental
map_dep_museums <- map_dep_museums |>
mutate(row_id = row_number()) |>
left_join(
deps_centroid |>
st_drop_geometry() |>
mutate(row_id = row_number()) |>
select(row_id, dist_museo),
by = "row_id"
) |>
select(-row_id)
## 6) Comprobar
summary(map_dep_museums$dist_museo)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.001576 0.066331 0.118203 0.116128 0.166749 0.261543
sum(is.na(map_dep_museums$dist_museo))
## [1] 0
## 7) Regresión
model_dist <- lm(
macron_margen ~ dist_museo,
data = map_dep_museums
)
summary(model_dist)
##
## Call:
## lm(formula = macron_margen ~ dist_museo, data = map_dep_museums)
##
## Residuals:
## Min 1Q Median 3Q Max
## -34.133 -10.644 -2.848 8.358 51.938
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 18.501 3.422 5.406 4.88e-07 ***
## dist_museo -61.008 25.639 -2.379 0.0194 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 16.54 on 94 degrees of freedom
## Multiple R-squared: 0.05681, Adjusted R-squared: 0.04678
## F-statistic: 5.662 on 1 and 94 DF, p-value: 0.01936
## margen esperado según distancia
map_dep_museums <- map_dep_museums |>
mutate(
margen_esperado = predict(model_dist),
residuo_margen = macron_margen - margen_esperado
)
## paleta centrada en 0
max_abs <- max(abs(map_dep_museums$residuo_margen), na.rm = TRUE)
pal_resid <- colorNumeric(
palette = colorRampPalette(c("#b2182b","#f7f7f7","#2166ac"))(11),
domain = c(-max_abs, max_abs),
na.color = "#d9d9d9"
)
## mapa interpretativo
leaflet(map_dep_museums) |>
addTiles() |>
addPolygons(
fillColor = ~pal_resid(residuo_margen),
fillOpacity = 0.9,
color = "white",
weight = 0.8,
label = ~paste0(
"departamento: ", dep_label, "\n",
"margen Macron-Le Pen: ", round(macron_margen,1), " pts\n",
"distancia a museo: ", round(dist_museo,3), "\n",
"desviación respecto a lo esperado: ", round(residuo_margen,2)
)
) |>
addLegend(
position = "bottomright",
pal = pal_resid,
values = c(-max_abs,0,max_abs),
title = "Macron vs esperado\nsegún cercanía a museos",
labFormat = labelFormat(
transform = function(x) round(x,2)
)
)
Interpretación de Chat:
El mapa muestra dónde el resultado de Macron fue mayor o menor de lo que se esperaría únicamente por la cercanía a museos. Los colores azules indican departamentos donde Macron obtuvo más ventaja sobre Le Pen de la que el modelo predice a partir de la distancia a museos, mientras que los rojos muestran lugares donde obtuvo menos ventaja de la esperada.
Al observar el patrón espacial, se ve que Macron tiende a rendir relativamente mejor de lo esperado en varias zonas del oeste y del centro de Francia, donde aparecen tonos azules. En estos territorios, aunque la cercanía a museos no necesariamente sea muy alta, el apoyo a Macron resulta más fuerte de lo que el modelo basado en proximidad cultural anticiparía. Esto sugiere que otros factores —como características socioeconómicas, urbanización o cultura política regional— pueden estar reforzando su desempeño electoral.
En cambio, Macron rinde peor de lo esperado en varias áreas del norte, noreste y partes del sureste, donde predominan los tonos rojos. En esos departamentos, el margen real entre Macron y Le Pen es menor que el que se esperaría si la cercanía a museos fuera el principal determinante cultural. Es decir, incluso en zonas donde la presencia o accesibilidad a instituciones culturales podría sugerir un mayor apoyo a Macron, el resultado electoral termina siendo relativamente más favorable a Le Pen.
En conjunto, el mapa indica que la cercanía a museos no explica por sí sola el patrón territorial del voto. Aunque podría existir una relación general entre entornos culturalmente activos y mayor apoyo a Macron, el desempeño electoral muestra desviaciones importantes en distintas regiones del país. Por tanto, la variable cultural capturada por la proximidad a museos parece influir solo parcialmente en la geografía del voto, mientras que otros factores territoriales y sociales siguen siendo determinantes.
En general, no se observa un patrón claro en el que las zonas más cercanas a museos presenten sistemáticamente una mayor ventaja para Macron. En algunas regiones donde la distancia a museos es menor —es decir, territorios potencialmente más “culturalmente activos”— Macron obtiene una ventaja mayor de la esperada, lo que podría sugerir cierta asociación positiva entre proximidad cultural y apoyo al candidato centrista. Sin embargo, el mapa también muestra varios departamentos cercanos a museos donde el desempeño de Macron es menor de lo esperado o incluso relativamente más favorable a Le Pen.
Por tanto, incluso si supusiéramos que la cercanía a museos fuera la única variable relevante, el patrón espacial indica que la proximidad a instituciones culturales no parece traducirse de forma consistente en un mayor voto para Macron. En otras palabras, hay territorios cercanos a museos donde Macron rinde bien, pero también otros donde no, lo que sugiere que la relación entre entorno cultural y voto no es uniforme en todo el país.
Me quedé pensando en el mapa que nos enseñó Fernando así que este es un intento de hacer algo similar al de Madrid y las embajadas
## centroides de departamentos
dep_points <- map_dep_museums |>
st_centroid()
## paleta para margen electoral
pal_vote <- colorNumeric(
palette = colorRampPalette(c("red","#f7f7f7","#2166ac"))(11),
domain = dep_points$macron_margen
)
leaflet() |>
addTiles() |>
## departamentos coloreados por resultado electoral
addCircleMarkers(
data = dep_points,
radius = 7,
stroke = FALSE,
fillOpacity = 0.85,
fillColor = ~pal_vote(macron_margen),
label = ~paste0(
"Departamento: ", dep_label, "\n",
"Ventaja Macron-Le Pen: ", round(macron_margen,1), " pts"
)
) |>
## museos
addCircleMarkers(
data = museums_sf,
radius = 3,
color = "violet",
stroke = FALSE,
fillOpacity = 0.9,
group = "Museos"
) |>
addLegend(
position = "bottomright",
pal = pal_vote,
values = dep_points$macron_margen,
title = "Ventaja Macron − Le Pen"
)