Diego Sebastián Luyo León · Estefanía Diana Carolina Apaza Díaz — Estadística para el Análisis Político II, PUCP
Desde 1980 hasta mediados de 2025, el Perú registró 25,026 eventos de protesta (Aragón et al., 2025), un canal recurrente de comunicación entre la ciudadanía y el Estado. En un contexto de capacidades estatales desiguales, el escalamiento hacia la violencia puede leerse como una estrategia para elevar los costos de la inacción estatal (Arce, 2014; Dargent et al., 2017). En ese sentido, nos planteamos la siguiente pregunta:
Cinco variables construidas y operacionalizadas a partir de la Base de Eventos de Protesta del Perú.
violencia_yEs una variable dicotómica: toma el valor 1 si el evento presentó una acción colectiva violenta (enfrentamiento, saqueo, quema, agresión física, toma de local, entre otras) o si hubo alguna víctima —herido o muerto—, según el Libro de Códigos de la base original (Aragón et al., 2024).
Lima concentra el mayor número de eventos, pero regiones como Loreto tienen tasas de violencia mucho más altas.
Los picos de eventos coinciden con momentos de crisis institucional (1983-1984, hiperinflación; 2022-2023, crisis política); la tasa de violencia (línea) no siempre sube junto con el volumen.
Esta sección se centra únicamente en periodo_politico, nuestra variable independiente. Se construyó a partir del año de cada evento, dividiendo la historia reciente del Perú en cuatro periodos según la literatura sobre estructura de oportunidad política (McAdam et al., 1996; S. G. Tarrow, 2011). H1: la probabilidad de escalamiento a la violencia responde a las condiciones estructurales de cada periodo político y sus respectivos ejecutivos.
25,026 eventos en 45 años: el riesgo de escalamiento no es constante, cambia con cada estructura de oportunidad política.
Cruce de tasa de violencia por tipo de actor y periodo político — cada celda es un subgrupo real de la base, no un promedio agregado. Este cruce es clave para leer si el efecto del periodo es homogéneo entre actores.
El tipo de actor (categoria_actor) es una variable explicativa secundaria, no la variable independiente principal. La incorporamos como control para aislar si el escalamiento responde a quién protesta, y así contrastarlo con el efecto del periodo político.
La identidad del actor movilizado también predice el escalamiento — casi tanto como el periodo político.
El actor laboral tiene la mayor cantidad de eventos registrados, pero es el menos propenso a la violencia.
Cada burbuja es un tipo de actor. Eje X: cuántos eventos protagoniza. Eje Y: qué tan violentos suelen ser. Tamaño: volumen total. Arriba a la izquierda = poco frecuente pero muy peligroso cuando ocurre (estudiantil); abajo a la derecha = frecuente pero mayormente pacífico (laboral).
Recordatorio — Variable dependiente: violencia_y (¿escaló a violencia?). Variable independiente: periodo_politico.
Esta sección cubre las variables de control: la magnitud de la protesta (protesta_masiva) y la inercia temporal del conflicto (numero_eventos_previos), ambas construidas a partir de la base original para aislar el efecto del periodo político.
protesta_masiva = 1 si el número de participantes
supera el percentil 75 de la muestra (3,500 asistentes). Distingue
movilizaciones convencionales de eventos de gran alcance.
Porcentaje de eventos que superaron el umbral de 3,500 participantes, año a año.
numero_eventos_previos = eventos ocurridos en la
misma provincia durante el mes calendario anterior. Captura si la
movilización se retroalimenta a sí misma.
Solo el 15.5% de los eventos originales escaló a violencia (N = 3,882 de 25,026). Sin corrección, el modelo tiende a sobreajustarse a la clase mayoritaria, por eso comparamos dos estrategias de remuestreo antes de estimar el modelo final.
Antes de cualquier remuestreo: así de desbalanceada está la base real. Ambas estrategias de corrección (down y up-sampling) llevan esta distribución a un 50/50 exacto por diseño; lo que realmente las diferencia es cuánta información original se conserva en el proceso (ver la pestaña de rendimiento abajo).
Por qué up-sampling: el down-sampling elimina ~69% de las protestas pacíficas originales, perdiendo heterogeneidad; el up-sampling conserva el 100% de la información con métricas igualmente estables.
Estimamos un modelo logístico binomial sobre la base up-sampled (N = 42,288): violencia_y ~ periodo + actor + protesta_masiva + eventos_previos. Las siguientes pestañas muestran los resultados desde distintos ángulos — Odds Ratios, efectos marginales, calidad de la predicción y diagnóstico de multicolinealidad.
Un OR mayor a 1 (rojo) aumenta el riesgo de violencia frente a la categoría de referencia; menor a 1 (verde) lo reduce.
Traduce el OR a un número directo: cuántos puntos porcentuales cambia la probabilidad de violencia.
Diagnóstico de multicolinealidad: valores cercanos a ±1 (rojo/verde intenso) indicarían variables redundantes. Aquí no hay ninguna preocupante.
H1 — "La probabilidad de que una protesta social escale hacia la violencia en el Perú responde a las condiciones estructurales y los incentivos institucionales determinados por cada periodo político y sus respectivos ejecutivos." Validada — los nueve predictores del modelo son estadísticamente significativos (p < 0.001, salvo eventos previos p < 0.01).
La violencia en las protestas no es un desvío irracional de la movilización: es un mecanismo relacional y estratégico que emerge cuando las ventanas de negociación política se cierran y cuando la identidad del actor movilizado carece de canales formales de intermediación con el poder central. La respuesta estatal más efectiva frente a la conflictividad social no es únicamente el control punitivo, sino la apertura sostenida de espacios institucionales de diálogo.
---
title: "Estructuras de oportunidad y acción colectiva"
output:
flexdashboard::flex_dashboard:
orientation: rows
vertical_layout: scroll
theme:
version: 4
bootswatch: journal
source_code: embed
---
```{r setup, include=FALSE}
# ============================================================
# Si es la primera vez que usas estos paquetes en Posit Cloud,
# corre esto UNA VEZ en la Consola (no en un chunk):
#
# install.packages(c("flexdashboard", "tidyverse", "plotly", "DT", "broom",
# "scales", "sf", "leaflet", "stringi"))
# ============================================================
library(flexdashboard)
library(tidyverse)
library(plotly)
library(DT)
library(broom)
library(scales)
library(sf)
library(leaflet)
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
# Paleta del proyecto (misma lógica que el panel HTML: verde-azulado = baja
# violencia, óxido = alta violencia)
color_ink <- "#1c2b3a"
color_oxide <- "#a23e28"
color_slate <- "#2f6a63"
color_paper <- "#e7e4da"
# Barra de herramientas de plotly simplificada: solo zoom (recuadro) y
# descarga de PNG, sin el resto de botones (pan, lasso, autoscale, etc.)
clean_plotly <- function(p){
config(
p,
displaylogo = FALSE,
modeBarButtonsToRemove = c(
"pan2d", "select2d", "lasso2d", "autoScale2d", "resetScale2d",
"hoverClosestCartesian", "hoverCompareCartesian", "toggleSpikelines",
"zoomIn2d", "zoomOut2d"
)
)
}
```
```{css}
/* Subpestañas (tabset): antes se veían en blanco/sin contraste */
.nav-tabs > li > a {
color: #4a4842 !important;
font-weight: 500;
font-size: 14px;
background-color: #efece3;
border: 1px solid #c7c2b1;
border-bottom: none;
margin-right: 3px;
}
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:focus,
.nav-tabs > li.active > a:hover {
color: #ffffff !important;
background-color: #1c2b3a !important;
border-color: #1c2b3a;
font-weight: 600;
}
.nav-tabs > li > a:hover {
background-color: #dcd8cc;
border-color: #c7c2b1;
}
/* Tamaño de letra general un poco más grande en toda la app */
.chart-title {
font-size: 15px !important;
font-weight: 600;
}
table.dataTable {
font-size: 13.5px !important;
}
.value-box .value {
font-size: 30px !important;
}
.value-box .caption {
font-size: 13.5px !important;
}
```
```{r carga-datos}
# Misma fuente que el .Rnw original (Base de Eventos de Protesta del Perú)
link_egpp <- "https://docs.google.com/spreadsheets/d/e/2PACX-1vR9hUSJf257WLVR3UUb50fPIXjLx_HlsNcp6rth5i5_fST9KYV1uPkfoDbU0MnfTrIIAs_SWYF_5TCB/pub?output=csv"
data <- read.csv(link_egpp)
# Dummies para tipo de actor
data$actor_laboral <- ifelse(data$categoria_actor == "actor_laboral", 1, 0)
data$actor_territorial_social <- ifelse(data$categoria_actor == "actor_territorial_social", 1, 0)
data$actor_economico <- ifelse(data$categoria_actor == "actor_economico", 1, 0)
data$actor_estudiantil <- ifelse(data$categoria_actor == "actor_estudiantil", 1, 0)
data$actor_politico_ciudadano <- ifelse(data$categoria_actor == "actor_politico_ciudadano", 1, 0)
# Dummies para periodos políticos
data$periodo_pre90 <- ifelse(data$ano < 1990, 1, 0)
data$periodo_90_00 <- ifelse(data$ano >= 1990 & data$ano <= 2000, 1, 0)
data$periodo_01_16 <- ifelse(data$ano >= 2001 & data$ano <= 2016, 1, 0)
data$periodo_actual <- ifelse(data$ano > 2016, 1, 0)
# Etiqueta legible de periodo, para los gráficos descriptivos
data <- data %>%
mutate(periodo_label = case_when(
ano <= 1989 ~ "Previo a 1990",
ano >= 1990 & ano <= 2000 ~ "Fujimori (1990-2000)",
ano >= 2001 & ano <= 2016 ~ "Regreso a la democracia (2001-2016)",
ano >= 2017 ~ "Autoritarismo parlamentario (2017-)"
)) %>%
mutate(periodo_label = factor(periodo_label, levels = c(
"Previo a 1990", "Fujimori (1990-2000)",
"Regreso a la democracia (2001-2016)", "Autoritarismo parlamentario (2017-)"
)))
# Dataframe para el modelo
Y <- data$violencia_y
X_model <- data %>%
select(periodo_pre90, periodo_90_00, periodo_01_16,
actor_laboral, actor_territorial_social, actor_economico, actor_estudiantil,
protesta_masiva, numero_eventos_previos)
data_modelo <- cbind(protesta_violenta = Y, X_model)
```
```{r datos-mapa}
# Polígonos de departamentos del Perú (fuente pública: juaneladio/peru-geojson)
peru_geo <- st_read(
"https://raw.githubusercontent.com/juaneladio/peru-geojson/master/peru_departamental_simple.geojson",
quiet = TRUE
)
normalize_name <- function(x) {
x <- toupper(trimws(x))
stringi::stri_trans_general(x, "Latin-ASCII")
}
peru_geo$region_norm <- normalize_name(peru_geo$NOMBDEP)
region_map_data <- data %>%
group_by(region) %>%
summarise(n = n(), tasa = mean(violencia_y) * 100) %>%
mutate(region_norm = normalize_name(region))
peru_geo <- peru_geo %>%
left_join(region_map_data, by = "region_norm")
```
```{r resampleo-y-modelo}
# ---- Up-sampling (método elegido en el trabajo final) ----
pacificas <- data_modelo[data_modelo$protesta_violenta == 0, ]
violentas <- data_modelo[data_modelo$protesta_violenta == 1, ]
set.seed(123)
replicated_violentas <- violentas[sample(1:nrow(violentas), nrow(pacificas), replace = TRUE), ]
balanced_data <- rbind(pacificas, replicated_violentas)
modelo_up <- glm(
protesta_violenta ~ periodo_pre90 + periodo_90_00 + periodo_01_16 +
actor_laboral + actor_territorial_social + actor_economico + actor_estudiantil +
protesta_masiva + numero_eventos_previos,
data = balanced_data, family = binomial()
)
pred_prob_up <- predict(modelo_up, type = "response")
pred_class_up <- ifelse(pred_prob_up > 0.5, 1, 0)
actual_up <- balanced_data$protesta_violenta
accuracy_up <- mean(pred_class_up == actual_up) * 100
sensitivity_up <- sum(pred_class_up == 1 & actual_up == 1) / sum(actual_up == 1) * 100
specificity_up <- sum(pred_class_up == 0 & actual_up == 0) / sum(actual_up == 0) * 100
# ---- Down-sampling (comparación) ----
wins_down <- data_modelo[data_modelo$protesta_violenta == 1, ]
losses_down <- data_modelo[data_modelo$protesta_violenta == 0, ]
set.seed(123)
replicated_losses <- losses_down[sample(1:nrow(losses_down), nrow(wins_down), replace = FALSE), ]
balanced_data_down <- rbind(replicated_losses, wins_down)
modelo_down <- glm(
protesta_violenta ~ periodo_pre90 + periodo_90_00 + periodo_01_16 +
actor_laboral + actor_territorial_social + actor_economico + actor_estudiantil +
protesta_masiva + numero_eventos_previos,
data = balanced_data_down, family = binomial()
)
pred_prob_down <- predict(modelo_down, type = "response")
pred_class_down <- ifelse(pred_prob_down > 0.5, 1, 0)
actual_down <- balanced_data_down$protesta_violenta
accuracy_down <- mean(pred_class_down == actual_down) * 100
sensitivity_down <- sum(pred_class_down == 1 & actual_down == 1) / sum(actual_down == 1) * 100
specificity_down <- sum(pred_class_down == 0 & actual_down == 0) / sum(actual_down == 0) * 100
# ---- Odds Ratios + IC 95% (modelo up-sampled) ----
or_table <- tidy(modelo_up, conf.int = TRUE, exponentiate = TRUE) %>%
filter(term != "(Intercept)")
# ---- Efectos Marginales Promedio (AME), calculados a mano ----
ame_dummy <- function(var){
d1 <- balanced_data; d1[[var]] <- 1
d0 <- balanced_data; d0[[var]] <- 0
mean(predict(modelo_up, newdata = d1, type = "response") -
predict(modelo_up, newdata = d0, type = "response")) * 100
}
ame_continua <- function(var){
d1 <- balanced_data; d1[[var]] <- balanced_data[[var]] + 1
mean(predict(modelo_up, newdata = d1, type = "response") -
predict(modelo_up, newdata = balanced_data, type = "response")) * 100
}
dummy_vars <- c("periodo_pre90", "periodo_90_00", "periodo_01_16",
"actor_laboral", "actor_territorial_social", "actor_economico", "actor_estudiantil",
"protesta_masiva")
ame_values <- sapply(dummy_vars, ame_dummy)
ame_values["numero_eventos_previos"] <- ame_continua("numero_eventos_previos")
ame_df <- tibble(term = names(ame_values), ame = as.numeric(ame_values))
# Etiquetas legibles para el modelo
label_map <- c(
periodo_pre90 = "Periodo: Pre-1990",
periodo_90_00 = "Periodo: 1990-2000 (Fujimori)",
periodo_01_16 = "Periodo: 2001-2016",
actor_laboral = "Actor: Laboral",
actor_territorial_social = "Actor: Territorial/social",
actor_economico = "Actor: Económico",
actor_estudiantil = "Actor: Estudiantil",
protesta_masiva = "Control: Protesta masiva",
numero_eventos_previos = "Control: Eventos previos (+1)"
)
modelo_final <- or_table %>%
left_join(ame_df, by = "term") %>%
mutate(label = label_map[term]) %>%
arrange(estimate)
```
1. Introducción
=======================================================================
Row {data-height=40}
-----------------------------------------------------------------------
###
```{r}
htmltools::tags$p("Diego Sebastián Luyo León · Estefanía Diana Carolina Apaza Díaz — Estadística para el Análisis Político II, PUCP",
style = "color:#6b675c; font-size:13.5px; margin:0;")
```
Row {data-height=130}
-----------------------------------------------------------------------
### Eventos registrados
```{r}
valueBox(comma(nrow(data)), icon = "fa-list", color = color_ink)
```
### % que escaló a violencia
```{r}
pct_viol <- round(mean(data$violencia_y) * 100, 2)
valueBox(paste0(pct_viol, "%"), icon = "fa-fire", color = color_oxide)
```
### Años cubiertos
```{r}
valueBox(paste0(min(data$ano), "–", max(data$ano)), icon = "fa-calendar", color = color_ink)
```
### Periodo de mayor riesgo
```{r}
riskiest <- data %>% group_by(periodo_label) %>%
summarise(rate = mean(violencia_y) * 100) %>%
arrange(desc(rate)) %>% slice(1)
valueBox(paste0(round(riskiest$rate,1), "%"), icon = "fa-exclamation-triangle", color = color_oxide)
```
Row
-----------------------------------------------------------------------
### Presentación del problema
Desde 1980 hasta mediados de 2025, el Perú registró **25,026 eventos de protesta** (Aragón et al., 2025), un canal recurrente de comunicación entre la ciudadanía y el Estado. En un contexto de capacidades estatales desiguales, el escalamiento hacia la violencia puede leerse como una estrategia para elevar los costos de la inacción estatal (Arce, 2014; Dargent et al., 2017). En ese sentido, nos planteamos la siguiente pregunta:
```{r}
htmltools::tags$h4(
"¿De qué manera las condiciones estructurales de los distintos periodos políticos influyen en la probabilidad de que una protesta escale hacia la violencia?",
style = "color:#1c2b3a; font-weight:600; font-size:19px; line-height:1.45; border-left:4px solid #a23e28; padding-left:14px; margin:14px 0 0 0;"
)
```
Row
-----------------------------------------------------------------------
### Variables del estudio
*Cinco variables construidas y operacionalizadas a partir de la Base de Eventos de Protesta del Perú.*
```{r}
tibble::tribble(
~Variable, ~Tipo, ~Descripción,
"violencia_y", "Dependiente (dicotómica)", "1 si el evento presentó acción violenta o víctimas; 0 si fue pacífico.",
"periodo_politico", "Independiente (categórica, 4 niveles)", "Periodo político del evento, construido a partir del año (pre-1990, Fujimori, democracia, actual).",
"categoria_actor", "Explicativa secundaria (categórica, 5 niveles)", "Tipo de actor que lidera la movilización.",
"protesta_masiva", "Control (dicotómica)", "1 si el N° de participantes supera el percentil 75 (≥3,500).",
"numero_eventos_previos", "Control (continua)", "Eventos en la misma provincia durante el mes anterior."
) %>%
datatable(options = list(dom = "t", paging = FALSE, scrollY = FALSE, scrollX = FALSE),
rownames = FALSE, fillContainer = FALSE)
```
Row
-----------------------------------------------------------------------
### Variable dependiente: `violencia_y`
Es una variable **dicotómica**: toma el valor 1 si el evento presentó una acción colectiva violenta (enfrentamiento, saqueo, quema, agresión física, toma de local, entre otras) o si hubo alguna víctima —herido o muerto—, según el Libro de Códigos de la base original (Aragón et al., 2024).
```{r}
dv_summary <- data %>%
mutate(tipo = ifelse(violencia_y == 1, "Violenta", "Pacífica")) %>%
count(tipo) %>%
mutate(pct = round(n / sum(n) * 100, 2))
plot_ly(dv_summary, labels = ~tipo, values = ~n, type = "pie", hole = 0.55,
sort = FALSE,
marker = list(colors = c(color_slate, color_oxide), line = list(color = "white", width = 2)),
textinfo = "percent", textposition = "inside",
textfont = list(color = "white", size = 15),
hovertemplate = "%{label}: %{value} eventos (%{percent})<extra></extra>") %>%
layout(showlegend = TRUE,
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.05,
font = list(size = 13)),
margin = list(t = 20, b = 40, l = 20, r = 20)) %>%
clean_plotly()
```
### Mapa: tasa de violencia por región
*Lima concentra el mayor número de eventos, pero regiones como Loreto tienen tasas de violencia mucho más altas.*
```{r}
pal <- colorNumeric(
palette = c(color_slate, "#f2f0e7", color_oxide),
domain = peru_geo$tasa,
na.color = "#eeeeee"
)
leaflet(peru_geo, options = leafletOptions(zoomControl = TRUE, minZoom = 4.5, maxZoom = 8)) %>%
addProviderTiles("CartoDB.Positron") %>%
setView(lng = -75.5, lat = -9.5, zoom = 5) %>%
addPolygons(
fillColor = ~pal(tasa),
fillOpacity = 0.87,
color = "white",
weight = 1.2,
label = ~paste0(coalesce(region, NOMBDEP), ": ", round(tasa,1), "% violencia (N=", comma(n), ")"),
labelOptions = labelOptions(style = list("font-family" = "sans-serif", "font-size" = "12px")),
highlightOptions = highlightOptions(weight = 3, color = color_ink, bringToFront = TRUE)
) %>%
addLegend(pal = pal, values = ~tasa, title = "% violencia", position = "bottomright")
```
Row {data-height=470}
-----------------------------------------------------------------------
### ¿Cuándo ocurrieron los picos de conflictividad y de violencia?
*Los picos de eventos coinciden con momentos de crisis institucional (1983-1984, hiperinflación; 2022-2023, crisis política); la tasa de violencia (línea) no siempre sube junto con el volumen.*
```{r}
year_summary <- data %>%
group_by(ano) %>%
summarise(n = n(), viol = sum(violencia_y), tasa = mean(violencia_y) * 100)
plot_ly(year_summary, x = ~ano) %>%
add_bars(y = ~n, name = "Eventos", marker = list(color = color_ink, opacity = 0.75),
hovertemplate = "<b>Año %{x}</b><br>Eventos: %{y}<extra></extra>") %>%
add_trace(y = ~tasa, name = "Tasa de violencia (%)", yaxis = "y2",
type = "scatter", mode = "lines+markers",
line = list(color = color_oxide, width = 2.5),
marker = list(color = color_oxide, size = 5),
hovertemplate = "<b>Año %{x}</b><br>Tasa: %{y:.1f}%<extra></extra>") %>%
layout(
hovermode = "x unified",
yaxis = list(title = "N° de eventos"),
yaxis2 = list(overlaying = "y", side = "right", title = "% violencia", rangemode = "tozero"),
xaxis = list(title = NULL),
legend = list(orientation = "h", y = 1.14),
margin = list(t = 40, b = 50),
shapes = list(
list(type = "line", x0 = 1990, x1 = 1990, y0 = 0, y1 = 1, yref = "paper",
line = list(color = "#a49f8c", dash = "dot", width = 1)),
list(type = "line", x0 = 2001, x1 = 2001, y0 = 0, y1 = 1, yref = "paper",
line = list(color = "#a49f8c", dash = "dot", width = 1)),
list(type = "line", x0 = 2017, x1 = 2017, y0 = 0, y1 = 1, yref = "paper",
line = list(color = "#a49f8c", dash = "dot", width = 1))
),
annotations = list(
list(x = 1990, y = 1.06, yref = "paper", text = "Fujimori →", showarrow = FALSE, font = list(size = 10.5, color = "#6b675c")),
list(x = 2001, y = 1.06, yref = "paper", text = "Democracia →", showarrow = FALSE, font = list(size = 10.5, color = "#6b675c")),
list(x = 2017, y = 1.06, yref = "paper", text = "Actual →", showarrow = FALSE, font = list(size = 10.5, color = "#6b675c"))
)
) %>% clean_plotly()
```
2. Periodo político
=======================================================================
Row {data-height=100}
-----------------------------------------------------------------------
###
```{r}
htmltools::tagList(
htmltools::tags$h3("Variable independiente: el periodo político",
style = "color:#1c2b3a; font-weight:600; margin:0 0 6px 0;"),
htmltools::tags$p(
htmltools::HTML("Esta sección se centra únicamente en <code>periodo_politico</code>, nuestra <b>variable independiente</b>. Se construyó a partir del año de cada evento, dividiendo la historia reciente del Perú en cuatro periodos según la literatura sobre estructura de oportunidad política (McAdam et al., 1996; S. G. Tarrow, 2011). <b>H1:</b> la probabilidad de escalamiento a la violencia responde a las condiciones estructurales de cada periodo político y sus respectivos ejecutivos."),
style = "color:#4a4842; font-size:14.5px; margin:0; line-height:1.55;"
)
)
```
Row {data-height=470}
-----------------------------------------------------------------------
### Eventos y tasa de violencia por periodo político
*25,026 eventos en 45 años: el riesgo de escalamiento no es constante, cambia con cada estructura de oportunidad política.*
```{r}
periodo_summary <- data %>%
group_by(periodo_label) %>%
summarise(n = n(), tasa = mean(violencia_y) * 100)
p <- ggplot(periodo_summary, aes(x = periodo_label, y = n, fill = tasa,
text = paste0(periodo_label, "<br>N = ", comma(n), "<br>Tasa violencia = ", round(tasa,1), "%"))) +
geom_col() +
geom_text(aes(label = comma(n)), vjust = -0.6, size = 4, color = color_ink) +
scale_fill_gradient(low = color_slate, high = color_oxide, name = "% violencia") +
scale_y_continuous(expand = expansion(mult = c(0, 0.18))) +
labs(x = NULL, y = "N° de eventos") +
theme_minimal(base_size = 13) +
theme(axis.text.x = element_text(angle = 15, hjust = 1))
ggplotly(p, tooltip = "text") %>% layout(margin = list(t = 30)) %>% clean_plotly()
```
Row {data-height=470}
-----------------------------------------------------------------------
### ¿Los mismos actores se comportan igual en todos los periodos?
*Cruce de tasa de violencia por tipo de actor y periodo político — cada celda es un subgrupo real de la base, no un promedio agregado. Este cruce es clave para leer si el efecto del periodo es homogéneo entre actores.*
```{r}
actor_labels <- c(
actor_laboral = "Actor laboral",
actor_territorial_social = "Actor territorial/social",
actor_economico = "Actor económico",
actor_estudiantil = "Actor estudiantil",
actor_politico_ciudadano = "Actor político-ciudadano"
)
heat_data <- data %>%
filter(categoria_actor %in% names(actor_labels)) %>%
group_by(periodo_label, categoria_actor) %>%
summarise(n = n(), tasa = mean(violencia_y) * 100, .groups = "drop") %>%
mutate(actor = actor_labels[categoria_actor])
p7 <- ggplot(heat_data, aes(x = periodo_label, y = actor, fill = tasa,
text = paste0(actor, " · ", periodo_label, "<br>N = ", comma(n), "<br>Tasa = ", round(tasa,1), "%"))) +
geom_tile(color = "white", linewidth = 0.8) +
geom_text(aes(label = paste0(round(tasa,0), "%")), color = "white", size = 4, fontface = "bold") +
scale_fill_gradient(low = color_slate, high = color_oxide, guide = "none") +
labs(x = NULL, y = NULL) +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 20, hjust = 1), panel.grid = element_blank())
ggplotly(p7, tooltip = "text") %>% layout(margin = list(b = 110, l = 130, t = 10)) %>% clean_plotly()
```
3. Actores
=======================================================================
Row {data-height=110}
-----------------------------------------------------------------------
###
```{r}
htmltools::tagList(
htmltools::tags$h3("Variable explicativa secundaria: el tipo de actor",
style = "color:#1c2b3a; font-weight:600; margin:0 0 6px 0;"),
htmltools::tags$p(
htmltools::HTML("El <b>tipo de actor</b> (<code>categoria_actor</code>) es una variable explicativa secundaria, no la variable independiente principal. La incorporamos como control para aislar si el escalamiento responde a <i>quién</i> protesta, y así contrastarlo con el efecto del periodo político."),
style = "color:#4a4842; font-size:14.5px; margin:0; line-height:1.55;"
)
)
```
Row {.tabset data-height=600}
-----------------------------------------------------------------------
### Tasa de violencia
```{r}
actor_summary <- data %>%
filter(categoria_actor %in% names(actor_labels)) %>%
group_by(categoria_actor) %>%
summarise(n = n(), tasa = mean(violencia_y) * 100) %>%
mutate(actor = actor_labels[categoria_actor]) %>%
arrange(tasa)
```
```{r}
htmltools::tags$h4("Tasa de violencia según tipo de actor",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*La identidad del actor movilizado también predice el escalamiento — casi tanto como el periodo político.*
```{r}
p2 <- ggplot(actor_summary, aes(x = reorder(actor, tasa), y = tasa, fill = tasa,
text = paste0(actor, "<br>N = ", comma(n), "<br>Tasa = ", round(tasa,1), "%"))) +
geom_col() +
geom_text(aes(label = paste0(round(tasa,1), "%")), hjust = -0.15, size = 4, color = color_ink) +
coord_flip() +
scale_y_continuous(expand = expansion(mult = c(0, 0.3))) +
scale_fill_gradient(low = color_slate, high = color_oxide, guide = "none") +
labs(x = NULL, y = "% de eventos violentos") +
theme_minimal(base_size = 13)
ggplotly(p2, tooltip = "text") %>% layout(margin = list(r = 65, l = 10, t = 10)) %>% clean_plotly()
```
### N° de eventos
```{r}
htmltools::tags$h4("N° de eventos por tipo de actor",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*El actor laboral tiene la mayor cantidad de eventos registrados, pero es el menos propenso a la violencia.*
```{r}
p2b <- ggplot(actor_summary, aes(x = reorder(actor, n), y = n, fill = tasa,
text = paste0(actor, "<br>N = ", comma(n), "<br>Tasa = ", round(tasa,1), "%"))) +
geom_col() +
geom_text(aes(label = comma(n)), hjust = -0.15, size = 4, color = color_ink) +
coord_flip() +
scale_y_continuous(expand = expansion(mult = c(0, 0.3))) +
scale_fill_gradient(low = color_slate, high = color_oxide, guide = "none") +
labs(x = NULL, y = "N° de eventos") +
theme_minimal(base_size = 13)
ggplotly(p2b, tooltip = "text") %>% layout(margin = list(r = 65, l = 10, t = 10)) %>% clean_plotly()
```
### Panorama comparado
```{r}
htmltools::tags$h4("Volumen vs. peligrosidad: los dos ejes a la vez",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*Cada burbuja es un tipo de actor. Eje X: cuántos eventos protagoniza. Eje Y: qué tan violentos suelen ser. Tamaño: volumen total. Arriba a la izquierda = poco frecuente pero muy peligroso cuando ocurre (estudiantil); abajo a la derecha = frecuente pero mayormente pacífico (laboral).*
```{r}
p2c <- ggplot(actor_summary, aes(x = n, y = tasa, size = n, color = tasa,
text = paste0(actor, "<br>N = ", comma(n), "<br>Tasa violencia = ", round(tasa,1), "%"))) +
geom_point(alpha = 0.85) +
geom_text(aes(label = actor), vjust = -1.3, size = 3.3, color = color_ink, show.legend = FALSE) +
scale_size_continuous(range = c(8, 26), guide = "none") +
scale_color_gradient(low = color_slate, high = color_oxide, guide = "none") +
scale_x_continuous(expand = expansion(mult = c(0.18, 0.18))) +
scale_y_continuous(expand = expansion(mult = c(0.2, 0.35))) +
labs(x = "N° de eventos", y = "% de eventos violentos") +
theme_minimal(base_size = 12)
ggplotly(p2c, tooltip = "text") %>% layout(margin = list(t = 30, b = 20)) %>% clean_plotly()
```
4. Variables de control
=======================================================================
Row {data-height=140}
-----------------------------------------------------------------------
###
```{r}
htmltools::tagList(
htmltools::tags$h3("Variables de control: magnitud e inercia temporal",
style = "color:#1c2b3a; font-weight:600; margin:0 0 8px 0;"),
htmltools::tags$div(
htmltools::HTML("<p style='margin:0 0 6px 0; font-size:13px; color:#6b675c;'><b>Recordatorio</b> — Variable dependiente: <code>violencia_y</code> (¿escaló a violencia?). Variable independiente: <code>periodo_politico</code>.</p><p style='margin:0; font-size:14.5px; color:#4a4842; line-height:1.55;'>Esta sección cubre las <b>variables de control</b>: la <b>magnitud</b> de la protesta (<code>protesta_masiva</code>) y la <b>inercia temporal</b> del conflicto (<code>numero_eventos_previos</code>), ambas construidas a partir de la base original para aislar el efecto del periodo político.</p>"),
style = "padding:2px 0;"
)
)
```
Row {data-height=130}
-----------------------------------------------------------------------
### % de protestas masivas
```{r}
pct_masiva <- round(mean(data$protesta_masiva) * 100, 2)
valueBox(paste0(pct_masiva, "%"), icon = "fa-users", color = color_ink)
```
### Umbral de corte
```{r}
valueBox("3,500", icon = "fa-arrows-alt-h", color = color_ink)
```
### Media de eventos previos
```{r}
valueBox(round(mean(data$numero_eventos_previos),1), icon = "fa-history", color = color_ink)
```
### Máx. eventos previos (mes/provincia)
```{r}
valueBox(comma(max(data$numero_eventos_previos)), icon = "fa-chart-line", color = color_ink)
```
Row {.tabset data-height=540}
-----------------------------------------------------------------------
### Protesta masiva
```{r}
htmltools::tags$h4("Protesta masiva y violencia",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*`protesta_masiva` = 1 si el número de participantes supera el percentil 75 de la muestra (3,500 asistentes). Distingue movilizaciones convencionales de eventos de gran alcance.*
```{r}
masiva_summary <- data %>%
mutate(tipo = ifelse(protesta_masiva == 1, "Masiva (≥3,500)", "No masiva (<3,500)")) %>%
group_by(tipo) %>%
summarise(n = n(), tasa = mean(violencia_y) * 100)
p9 <- ggplot(masiva_summary, aes(x = reorder(tipo, n), y = n, fill = tasa,
text = paste0(tipo, "<br>N = ", comma(n), "<br>Tasa violencia = ", round(tasa,1), "%"))) +
geom_col(width = 0.55) +
geom_text(aes(label = comma(n)), hjust = -0.15, color = color_ink, size = 4) +
coord_flip() +
scale_y_continuous(expand = expansion(mult = c(0, 0.25))) +
scale_fill_gradient(low = color_slate, high = color_oxide, guide = "none") +
labs(x = NULL, y = "N° de eventos") +
theme_minimal(base_size = 13)
ggplotly(p9, tooltip = "text") %>% layout(margin = list(r = 60, l = 10, t = 10)) %>% clean_plotly()
```
### Masiva por año
```{r}
htmltools::tags$h4("¿En qué años hubo más protestas masivas?",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*Porcentaje de eventos que superaron el umbral de 3,500 participantes, año a año.*
```{r}
masiva_year <- data %>%
group_by(ano) %>%
summarise(n = n(), pct_masiva = mean(protesta_masiva) * 100)
plot_ly(masiva_year, x = ~ano, y = ~pct_masiva, type = "scatter", mode = "lines+markers",
line = list(color = color_oxide, width = 2.2), marker = list(color = color_oxide, size = 5),
hovertemplate = "<b>Año %{x}</b><br>%{y:.1f}% de eventos masivos<extra></extra>") %>%
layout(yaxis = list(title = "% de eventos masivos", rangemode = "tozero"),
xaxis = list(title = NULL), margin = list(t = 10)) %>%
clean_plotly()
```
### Inercia temporal
```{r}
htmltools::tags$h4("Eventos previos por provincia (rezago mensual)",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*`numero_eventos_previos` = eventos ocurridos en la misma provincia durante el mes calendario anterior. Captura si la movilización se retroalimenta a sí misma.*
```{r}
p10 <- ggplot(data, aes(x = numero_eventos_previos)) +
geom_histogram(bins = 30, fill = color_ink, alpha = 0.8) +
labs(x = "N° de eventos previos (mes/provincia)", y = "N° de eventos") +
theme_minimal(base_size = 13)
ggplotly(p10, tooltip = c("x","y")) %>% layout(margin = list(t = 10, b = 90)) %>% clean_plotly()
```
5. Remuestreo
=======================================================================
Row {data-height=110}
-----------------------------------------------------------------------
###
```{r}
htmltools::tagList(
htmltools::tags$h3("Corrigiendo el desbalance de clases",
style = "color:#1c2b3a; font-weight:600; margin:0 0 6px 0;"),
htmltools::tags$p(
htmltools::HTML("Solo el <b>15.5%</b> de los eventos originales escaló a violencia (N = 3,882 de 25,026). Sin corrección, el modelo tiende a sobreajustarse a la clase mayoritaria, por eso comparamos dos estrategias de remuestreo antes de estimar el modelo final."),
style = "color:#4a4842; font-size:14.5px; margin:0; line-height:1.55;"
)
)
```
Row {data-height=120}
-----------------------------------------------------------------------
### N final — Down-sampling
```{r}
valueBox(comma(nrow(balanced_data_down)), icon = "fa-database", color = color_slate)
```
### N final — Up-sampling (elegido)
```{r}
valueBox(comma(nrow(balanced_data)), icon = "fa-database", color = color_oxide)
```
Row {data-height=460}
-----------------------------------------------------------------------
### Distribución original de la variable dependiente
*Antes de cualquier remuestreo: así de desbalanceada está la base real. Ambas estrategias de corrección (down y up-sampling) llevan esta distribución a un 50/50 exacto por diseño; lo que realmente las diferencia es cuánta información original se conserva en el proceso (ver la pestaña de rendimiento abajo).*
```{r}
plot_ly(dv_summary, labels = ~tipo, values = ~n, type = "pie", hole = 0.55,
sort = FALSE,
marker = list(colors = c(color_slate, color_oxide), line = list(color = "white", width = 2)),
textinfo = "percent", textposition = "inside",
textfont = list(color = "white", size = 15),
hovertemplate = "%{label}: %{value} eventos (%{percent})<extra></extra>") %>%
layout(showlegend = TRUE,
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.05,
font = list(size = 13)),
margin = list(t = 20, b = 40, l = 20, r = 20)) %>%
clean_plotly()
```
Row {.tabset data-height=420}
-----------------------------------------------------------------------
### Accuracy
```{r}
htmltools::tags$h4("Precisión global (accuracy): down vs. up-sampling",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
```{r}
acc_df <- tibble(Metodo = c("Down-sampling", "Up-sampling"), Valor = c(accuracy_down, accuracy_up))
p_acc <- ggplot(acc_df, aes(x = Metodo, y = Valor, fill = Metodo,
text = paste0(Metodo, "<br>Accuracy = ", round(Valor,2), "%"))) +
geom_col(width = 0.5) +
geom_text(aes(label = paste0(round(Valor,1), "%")), vjust = -0.6, size = 5) +
scale_fill_manual(values = c(`Down-sampling` = color_slate, `Up-sampling` = color_oxide), guide = "none") +
scale_y_continuous(expand = expansion(mult = c(0, 0.2))) +
labs(x = NULL, y = "%") +
theme_minimal(base_size = 14)
ggplotly(p_acc, tooltip = "text") %>% layout(margin = list(t = 10)) %>% clean_plotly()
```
### Sensibilidad
```{r}
htmltools::tags$h4("Sensibilidad: down vs. up-sampling",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
```{r}
sen_df <- tibble(Metodo = c("Down-sampling", "Up-sampling"), Valor = c(sensitivity_down, sensitivity_up))
p_sen <- ggplot(sen_df, aes(x = Metodo, y = Valor, fill = Metodo,
text = paste0(Metodo, "<br>Sensibilidad = ", round(Valor,2), "%"))) +
geom_col(width = 0.5) +
geom_text(aes(label = paste0(round(Valor,1), "%")), vjust = -0.6, size = 5) +
scale_fill_manual(values = c(`Down-sampling` = color_slate, `Up-sampling` = color_oxide), guide = "none") +
scale_y_continuous(expand = expansion(mult = c(0, 0.2))) +
labs(x = NULL, y = "%") +
theme_minimal(base_size = 14)
ggplotly(p_sen, tooltip = "text") %>% layout(margin = list(t = 10)) %>% clean_plotly()
```
### Especificidad
```{r}
htmltools::tags$h4("Especificidad: down vs. up-sampling",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
```{r}
esp_df <- tibble(Metodo = c("Down-sampling", "Up-sampling"), Valor = c(specificity_down, specificity_up))
p_esp <- ggplot(esp_df, aes(x = Metodo, y = Valor, fill = Metodo,
text = paste0(Metodo, "<br>Especificidad = ", round(Valor,2), "%"))) +
geom_col(width = 0.5) +
geom_text(aes(label = paste0(round(Valor,1), "%")), vjust = -0.6, size = 5) +
scale_fill_manual(values = c(`Down-sampling` = color_slate, `Up-sampling` = color_oxide), guide = "none") +
scale_y_continuous(expand = expansion(mult = c(0, 0.2))) +
labs(x = NULL, y = "%") +
theme_minimal(base_size = 14)
ggplotly(p_esp, tooltip = "text") %>% layout(margin = list(t = 10)) %>% clean_plotly()
```
> **Por qué up-sampling:** el down-sampling elimina ~69% de las protestas pacíficas originales, perdiendo heterogeneidad; el up-sampling conserva el 100% de la información con métricas igualmente estables.
6. Modelo
=======================================================================
Row {data-height=100}
-----------------------------------------------------------------------
###
```{r}
htmltools::tagList(
htmltools::tags$h3("Aplicación de regresión logística", style = "color:#1c2b3a; font-weight:600; margin:0 0 6px 0;"),
htmltools::tags$p(
htmltools::HTML("Estimamos un modelo logístico binomial sobre la base <b>up-sampled</b> (N = 42,288): <code>violencia_y ~ periodo + actor + protesta_masiva + eventos_previos</code>. Las siguientes pestañas muestran los resultados desde distintos ángulos — Odds Ratios, efectos marginales, calidad de la predicción y diagnóstico de multicolinealidad."),
style = "color:#4a4842; font-size:14.5px; margin:0; line-height:1.55;"
)
)
```
Row {.tabset data-height=620}
-----------------------------------------------------------------------
### Odds Ratios
```{r}
htmltools::tags$h4("Odds Ratios (IC 95%) — regresión logística, up-sampling",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*Un OR mayor a 1 (rojo) aumenta el riesgo de violencia frente a la categoría de referencia; menor a 1 (verde) lo reduce.*
```{r}
p4 <- ggplot(modelo_final, aes(x = reorder(label, estimate), y = estimate,
ymin = conf.low, ymax = conf.high, color = estimate > 1,
text = paste0(label, "<br>OR = ", round(estimate,2),
"<br>IC 95%: [", round(conf.low,2), ", ", round(conf.high,2), "]"))) +
geom_hline(yintercept = 1, linetype = "dashed", color = "grey60") +
geom_pointrange() +
coord_flip() +
scale_y_log10() +
scale_color_manual(values = c(`TRUE` = color_oxide, `FALSE` = color_slate), guide = "none") +
labs(x = NULL, y = "Odds Ratio (escala log)") +
theme_minimal(base_size = 13)
ggplotly(p4, tooltip = "text") %>% layout(margin = list(t = 10)) %>% clean_plotly()
```
### Efecto Marginal (AME)
```{r}
htmltools::tags$h4("Efecto Marginal Promedio (puntos porcentuales)",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*Traduce el OR a un número directo: cuántos puntos porcentuales cambia la probabilidad de violencia.*
```{r}
p5 <- ggplot(modelo_final, aes(x = reorder(label, ame), y = ame, fill = ame > 0,
text = paste0(label, "<br>AME = ", round(ame,2), " pp"))) +
geom_col() +
geom_hline(yintercept = 0, color = "grey40") +
coord_flip() +
scale_fill_manual(values = c(`TRUE` = color_oxide, `FALSE` = color_slate), guide = "none") +
labs(x = NULL, y = "AME (puntos porcentuales)") +
theme_minimal(base_size = 13)
ggplotly(p5, tooltip = "text") %>% layout(margin = list(t = 10)) %>% clean_plotly()
```
### Matriz de confusión
```{r}
htmltools::tags$h4("Matriz de confusión (umbral = 0.5)",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
```{r}
conf_df <- as.data.frame(table(Real = actual_up, Predicho = pred_class_up)) %>%
mutate(Real = ifelse(Real == 1, "Violenta", "Pacífica"),
Predicho = ifelse(Predicho == 1, "Violenta", "Pacífica"))
p6 <- ggplot(conf_df, aes(x = Real, y = Predicho, fill = Freq,
text = paste0("Real: ", Real, "<br>Predicho: ", Predicho, "<br>N = ", comma(Freq)))) +
geom_tile(color = "white", linewidth = 1) +
geom_text(aes(label = comma(Freq)), color = "white", size = 6, fontface = "bold") +
scale_fill_gradient(low = color_slate, high = color_oxide, guide = "none") +
labs(x = "Valor real", y = "Valor predicho") +
theme_minimal(base_size = 13) +
theme(panel.grid = element_blank())
ggplotly(p6, tooltip = "text") %>% layout(margin = list(t = 10)) %>% clean_plotly()
```
### Correlación
```{r}
htmltools::tags$h4("Correlación entre variables del modelo",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
*Diagnóstico de multicolinealidad: valores cercanos a ±1 (rojo/verde intenso) indicarían variables redundantes. Aquí no hay ninguna preocupante.*
```{r}
cor_mat <- balanced_data %>%
select(protesta_violenta, periodo_pre90, periodo_90_00, periodo_01_16,
actor_laboral, actor_territorial_social, actor_economico, actor_estudiantil,
protesta_masiva, numero_eventos_previos) %>%
cor() %>%
as.data.frame() %>%
rownames_to_column("Var1") %>%
pivot_longer(-Var1, names_to = "Var2", values_to = "r")
p8 <- ggplot(cor_mat, aes(x = Var1, y = Var2, fill = r, text = paste0(Var1, " × ", Var2, "<br>r = ", round(r,2)))) +
geom_tile(color = "white") +
scale_fill_gradient2(low = color_slate, mid = "#f2f0e7", high = color_oxide, midpoint = 0, guide = "none") +
labs(x = NULL, y = NULL) +
theme_minimal(base_size = 11) +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
ggplotly(p8, tooltip = "text") %>% layout(margin = list(b = 130, l = 130, t = 10)) %>% clean_plotly()
```
### Tabla completa
```{r}
htmltools::tags$h4("Tabla completa del modelo (up-sampling)",
style = "color:#1c2b3a; font-weight:600; font-size:16.5px; margin:4px 0 4px 0;")
```
```{r}
modelo_final %>%
transmute(Variable = label, OR = round(estimate, 3),
`IC 2.5%` = round(conf.low, 3), `IC 97.5%` = round(conf.high, 3),
`AME (pp)` = round(ame, 3), `p-valor` = signif(p.value, 3)) %>%
datatable(options = list(dom = "t", pageLength = 10), rownames = FALSE)
```
7. Conclusiones
=======================================================================
Row {data-height=110}
-----------------------------------------------------------------------
### Eventos analizados
```{r}
valueBox(comma(nrow(data)), icon = "fa-list", color = color_ink)
```
### Tasa de violencia
```{r}
valueBox(paste0(pct_viol, "%"), icon = "fa-fire", color = color_oxide)
```
### Predictor de mayor riesgo
```{r}
top_or <- modelo_final %>% arrange(desc(estimate)) %>% slice(1)
valueBox(paste0("OR ", round(top_or$estimate,2)), icon = "fa-arrow-up", color = color_oxide)
```
### Accuracy del modelo final
```{r}
valueBox(paste0(round(accuracy_up,2), "%"), icon = "fa-check-circle", color = color_ink)
```
Row {data-height=90}
-----------------------------------------------------------------------
###
```{r}
htmltools::tags$blockquote(
htmltools::HTML("<b>H1 —</b> \"La probabilidad de que una protesta social escale hacia la violencia en el Perú responde a las condiciones estructurales y los incentivos institucionales determinados por cada periodo político y sus respectivos ejecutivos.\" <span style='color:#2f6a63; font-weight:600;'>Validada</span> — los nueve predictores del modelo son estadísticamente significativos (p < 0.001, salvo eventos previos p < 0.01)."),
style = "border-left:4px solid #a49f8c; margin:0; padding:6px 0 6px 14px; color:#4a4842; font-size:14px; font-style:italic; line-height:1.55;"
)
```
Row {data-height=300}
-----------------------------------------------------------------------
### Hallazgos principales
```{r}
htmltools::tags$div(
htmltools::HTML("
El <b>periodo previo a 1990</b> es el de mayor riesgo estructural frente al periodo actual (2016 en adelante) — la militarización y la crisis hiperinflacionaria dejan huella estadística clara. En contraste, <b>el fujimorismo (1990–2000)</b> es el único periodo que reduce el riesgo base, consistente con un aparato autocrático que desmoviliza mediante control y cooptación en vez de negociación. En cuanto a los actores, <b>los territoriales/sociales</b> tienen el mayor impacto agregado por su alta representatividad en el registro histórico, mientras que <b>los estudiantes</b> muestran la mayor volatilidad relativa (OR = 3.37) — su confrontación con la autoridad tiende a ser más intensa cuando ocurre. Por el contrario, <b>los actores laborales y económicos</b>, con mayor capacidad organizativa formal, actúan sistemáticamente como factores de mitigación de la violencia.
"),
style = "background-color:#f7ece8; border-left:5px solid #a23e28; border-radius:4px; padding:18px 22px; font-size:18px; line-height:1.8; color:#1c2b3a;"
)
```
Row
-----------------------------------------------------------------------
### Implicancias
La violencia en las protestas no es un desvío irracional de la movilización: es un **mecanismo relacional y estratégico** que emerge cuando las ventanas de negociación política se cierran y cuando la identidad del actor movilizado carece de canales formales de intermediación con el poder central. La respuesta estatal más efectiva frente a la conflictividad social no es únicamente el control punitivo, sino la apertura sostenida de espacios institucionales de diálogo.
### Limitaciones y próximos pasos
- La cobertura de prensa nacional subrepresenta eventos en la periferia rural.
- La agregación provincial oculta dinámicas de control social a escala distrital.
- El up-sampling duplica la varianza del grupo minoritario — cautela al extrapolar en tiempo real.
- Próximo paso: un término de interacción explícito periodo × actor, como sugiere la sección 2.