Registro de cambios

v2 → v2.1 (tras correr la v2 con los datos reales)

# Cambio Por qué
1 La lectura de la descomposición (2.6) se apoya solo en los componentes cuyo IC 95% excluye el cero, y reporta montos negativos correctamente. En v2 el texto decía “$-11” y “101,9% es costo operativo”: sumaba un operativo fijo no significativo y un financiero negativo.
2 La lectura del control de registro (2.4) considera la elasticidad y la PMG en niveles. En v2 solo miraba la elasticidad (−16%) e ignoraba que la PMG en niveles caía a la mitad.
3 La heterogeneidad del total de salidas (2.9) se estima en log-log, con covariables centradas en la empresa promedio. En niveles el modelo era numéricamente inestable (base −1,22; Servicios +1,52), y la “base” era un grupo atípico (hombre, Comercio, tradicional).
4 La prueba de fungibilidad se repite solo con empresas que registran gasto personal, y se muestra cuántas lo registran según separen o no las finanzas. Quien separa las finanzas puede simplemente no anotar el gasto del hogar en el diario del negocio.
5 La asimetría (2.7) se re-estima con control de días con registro, y se prueba si los días con registro caen cuando el ingreso está por debajo de lo habitual. β+ > β− puede venir de sub-registro de ingresos en las malas quincenas.
6 Nueva sensibilidad (2.11): entradas registradas con categoría de gasto (Mercancías, Materia prima…) excluidas o reclasificadas como salidas; arriendo como entrada por separado. Un gasto anotado como ingreso infla Y y G a la vez: justo la correlación que se mide.
7 Nueva sensibilidad (2.11): valores extremos en los modelos en niveles (winsorización p1–p99 y recorte del 1% superior; la descomposición se re-estima en la muestra recortada). La PMG agrupada en niveles tenía un IC de [0,03; 0,95].

v1 → v2

# Cambio Por qué
1 β se llama propensión marginal a gastar (PMG), no “elasticidad”. Se agrega la elasticidad log-log por separado. lm(G ~ Y) en niveles da pesos de gasto por peso de ingreso; una elasticidad sale de log(G) ~ log(Y).
2 La conclusión pasa de “no suavizan” a “suavización parcial”, y se genera desde los resultados. En v1 la Tabla 6 rechazaba β = 1, lo que contradice “gastan lo que entra”.
3 Nueva clasificación de las salidas por función económica (operativo variable, operativo fijo, inversión, personal/hogar, financiero) y de las entradas (operativas vs. financiamiento). Una “Salida” no es consumo: incluye mercancía e insumos, que se mueven con las ventas, y el ahorro, que es justamente suavizar.
4 Nuevo modelo de panel con efectos fijos de empresa y de quincena, con errores agrupados por empresa (fixest). Los β individuales salen de 4 a 11 puntos y son muy ruidosos; el panel usa a todas las empresas con ≥ 2 quincenas bilaterales y controla estacionalidad.
5 Nuevo control de intensidad de registro (días con registro en la quincena). Las quincenas en que se usa más la app tienen más entradas y más salidas a la vez: eso genera β > 0 sin ningún comportamiento económico detrás.
6 Nueva descomposición de la PMG por componente del gasto. Permite decir cuántos de los pesos que responden al ingreso son inventario y cuántos son gasto del hogar.
7 Nuevas pruebas de asimetría (subidas vs. caídas de ingreso) y de rezagos/adelantos. La asimetría es la señal clásica de restricciones de liquidez; los adelantos detectan compra de inventario antes de vender.
8 Nueva prueba del sesgo del filtro bilateral. Quedarse solo con quincenas con entradas y salidas bota las quincenas donde se vería suavización.
9 Se elimina el hallazgo “el gasto es más volátil que el ingreso”; se reemplaza por la comparación en pesos (desviación estándar). El CV divide por la media, y la media del gasto es menor que la del ingreso: el CV del gasto queda inflado mecánicamente.
10 Agro, Industria y Otros se agrupan en “Otros sectores” para tablas y regresiones. No se borra ninguna observación. En la Submuestra B son 2, 3 y 2 empresas: las medianas por celda no son interpretables.
11 La robustez entre submuestras usa IC bootstrap de la mediana en lugar del umbral |Δ| < 0,10. El umbral era arbitrario y D (n = 11) quedaba justo en el borde.
12 La regresión de predictores en dos etapas usa errores robustos HC3 de verdad (sandwich + lmtest). En v1 el comentario decía HC3 pero tidy(lm) reportaba errores clásicos.
13 Decimales con coma y miles con punto en todas las tablas; “1 de cada N” calculado. “822.123.3” era ambiguo; 27% es 1 de cada 4, no 1 de cada 3.
14 Se agrega una sección sobre a quién representa la muestra. La submuestra B es una fracción pequeña de los usuarios: los resultados hablan de usuarios activos de la app, no de los micronegocios bogotanos en general.

Nota metodológica sobre calidad de datos. La base cruda presenta limitaciones estructurales documentadas en el diagnóstico de calidad (González Lozano & Granados Rodríguez, agosto 2026): (1) una proporción alta de filas potencialmente duplicadas por re-ingesta de lotes del sistema de cargue, problema que no puede resolverse completamente con la información disponible; (2) 58 transacciones con fechas futuras (eliminadas en limpieza_MDF.Rmd); (3) 8 valores imposibles de año de inicio (convertidos a NA en este cuaderno). Todo el análisis aplica los filtros de limpieza disponibles y declara los supuestos adoptados. Los resultados son exploratorios y describen a usuarios activos de Mi Diario Financiero, no a los micronegocios bogotanos en general.


0. Librerías, parámetros y carga de datos

library(tidyverse)    # manipulación y visualización (incluye lubridate)
library(readxl)       # lectura de Excel
library(scales)       # formato de ejes
library(ggridges)     # density ridges
library(broom)        # tidying de modelos
library(knitr)        # tablas
library(kableExtra)   # formato de tablas
library(patchwork)    # combinar gráficas
library(writexl)      # exportar Excel
library(fixest)       # panel con efectos fijos   — NUEVO en v2
library(sandwich)     # errores robustos HC3     — NUEVO en v2
library(lmtest)       # coeftest                 — NUEVO en v2

# Paleta institucional
COL_POS  <- "#1baf7a"   # flujo positivo
COL_NEG  <- "#e34948"   # flujo negativo
COL_AZUL <- "#1A5276"   # color principal
COL_GRIS <- "#898781"   # secundario
COL_SECT <- c(Comercio = COL_AZUL, Servicios = "#e67e22", `Otros sectores` = COL_GRIS)
# ── Parámetros del análisis (cambiar aquí, no dentro del código) ─────────────
SEED        <- 20261005   # semilla para el bootstrap
N_BOOT      <- 2000       # réplicas bootstrap
MIN_Q_BETA  <- 4          # quincenas bilaterales mínimas para estimar β por empresa
MIN_Q_PANEL <- 2          # quincenas bilaterales mínimas para entrar al panel con EF de empresa
set.seed(SEED)

# ── Formato colombiano: miles con punto, decimales con coma ─────────────────
options(knitr.kable.NA = "—")
FMT_TABLA <- list(big.mark = ".", decimal.mark = ",")

fmt <- function(x, d = 2) {
  out <- formatC(round(x, d), format = "f", digits = d,
                 big.mark = ".", decimal.mark = ",")
  out[is.na(x)] <- "—"
  out
}
fmt_pct <- function(x, d = 1) ifelse(is.na(x), "—", paste0(fmt(100 * x, d), "%"))
fmt_p   <- function(p) case_when(is.na(p) ~ "—", p < 0.001 ~ "< 0,001", TRUE ~ fmt(p, 3))
txt_p   <- function(p) if_else(!is.na(p) & p < 0.001, "p < 0,001", paste0("p = ", fmt(p, 3)))
fmt_ic  <- function(lo, hi, d = 2) {
  ifelse(is.na(lo) | is.na(hi), "—", paste0("[", fmt(lo, d), "; ", fmt(hi, d), "]"))
}
etiqueta_millones <- function(x) paste0(fmt(x / 1e6, 1), " M")
fmt_pesos <- function(x, d = 0) ifelse(is.na(x), "—", paste0(ifelse(x < 0, "−$", "$"), fmt(abs(x), d)))
es_sig    <- function(lo, hi) !is.na(lo) & !is.na(hi) & (lo > 0 | hi < 0)   # IC 95% excluye el cero
winsor    <- function(x, p = c(0.01, 0.99)) {
  q <- quantile(x, p, na.rm = TRUE); pmin(pmax(x, q[1]), q[2])
}
eje_coma <- label_number(decimal.mark = ",", big.mark = ".")
estilo  <- function(k, ...) kable_styling(k, bootstrap_options = c("striped", "hover"), ...)

# ── Funciones estadísticas ───────────────────────────────────────────────────
# IC bootstrap percentil de la mediana
boot_mediana <- function(x, B = N_BOOT) {
  x <- x[!is.na(x)]
  if (length(x) < 5) return(c(NA_real_, NA_real_))
  meds <- replicate(B, median(sample(x, replace = TRUE)))
  unname(quantile(meds, c(0.025, 0.975)))
}

# Prueba de signo: H0 mediana = mu (a diferencia de Wilcoxon, no supone simetría)
p_signo <- function(x, mu) {
  x <- x[!is.na(x) & x != mu]
  if (length(x) < 1) return(NA_real_)
  binom.test(sum(x > mu), length(x))$p.value
}

# Una fila resumen de un coeficiente de un modelo fixest
fila_fx <- function(m, var, nombre) {
  vacio <- tibble(Modelo = nombre, coef = NA_real_, ee = NA_real_, ic_lo = NA_real_,
                  ic_hi = NA_real_, p = NA_real_, obs = NA_real_, empresas = NA_real_)
  if (is.null(m) || length(var) != 1 || !var %in% names(coef(m))) return(vacio)
  ct <- coeftable(m)
  ci <- confint(m)
  tibble(
    Modelo   = nombre,
    coef     = ct[var, 1],
    ee       = ct[var, 2],
    ic_lo    = ci[var, 1],
    ic_hi    = ci[var, 2],
    p        = ct[var, 4],
    obs      = nobs(m),
    empresas = if ("ID" %in% names(m$fixef_sizes)) unname(m$fixef_sizes["ID"]) else NA_real_
  )
}

# Todos los coeficientes de un modelo fixest
coefs_fx <- function(m, nombre) {
  ct <- coeftable(m)
  ci <- confint(m)
  tibble(Modelo = nombre, termino = rownames(ct), coef = ct[, 1], ee = ct[, 2],
         p = ct[, 4], ic_lo = ci[, 1], ic_hi = ci[, 2])
}

# Tabla estándar para filas de fila_fx()
kable_fx <- function(df, caption, d = 3) {
  df %>%
    transmute(
      Modelo,
      `Coef.`        = fmt(coef, d),
      `EE (cluster)` = fmt(ee, d),
      `IC 95%`       = fmt_ic(ic_lo, ic_hi, d),
      `p-valor`      = fmt_p(p),
      Obs.           = fmt(obs, 0),
      Empresas       = fmt(empresas, 0)
    ) %>%
    kable(caption = caption) %>%
    estilo(full_width = TRUE)
}
# ── PREREQUISITO ─────────────────────────────────────────────────────────────
# Este cuaderno asume que ya corriste limpieza_MDF.Rmd, que produce:
#   ig_limpia.csv — base de ingresos y gastos limpia y estandarizada
# La Caracterización se limpia aquí directamente (910 filas).
# Si aún no has corrido limpieza_MDF.Rmd, hazlo primero, por favor.

ig_limpia <- read_csv("ig_limpia.csv", show_col_types = FALSE) %>%
  mutate(`Fecha de la transacción` = as.Date(`Fecha de la transacción`))

car <- read_excel("Base_Anonimizada_Caracterizacion.xlsx",
                  sheet = "Caracterizacion")

car_clean <- car %>%
  mutate(
    `Año de Inicio` = if_else(!between(`Año de Inicio`, 1950, 2026),
                               NA_real_, `Año de Inicio`),
    across(c("RUT", "Presupuesto", "División de Finanzas"),
           ~ str_to_title(str_trim(.))),
    `Tipo de Registro` = str_to_title(str_trim(`Tipo de Registro`)),
    `Tipo de Registro` = if_else(`Tipo de Registro` == "No",
                                  NA_character_, `Tipo de Registro`)
  )

cat("Base I&G limpia: ", nrow(ig_limpia), "filas |", ncol(ig_limpia), "columnas\n")
## Base I&G limpia:  13113 filas | 25 columnas
cat("Base Car:        ", nrow(car_clean), "filas |", ncol(car_clean), "columnas\n")
## Base Car:         910 filas | 37 columnas

ETAPA 1 — Construcción y justificación de submuestras

1.1 Resumen de la base limpia recibida

La base ig_limpia.csv fue producida por limpieza_MDF.Rmd, que aplicó:

  • Capa 1 — 2.588 filas 100% idénticas eliminadas con distinct().
  • Capa 2 — 3.542 re-ingestas de lotes eliminadas con llave de unicidad.
  • Capa 3 — transacciones cargadas con más de 30 días de retraso, marcadas con es_retroactivo = TRUE (no eliminadas; se controlan con la Submuestra D). Dentro de ellas, el diagnóstico estimó ~1.977 duplicados irresolubles; el número total de filas marcadas aparece en la Tabla 1.
  • Atípicos — excluidos vía Monto_Atipico == TRUE.
  • Fechas futuras — 58 transacciones eliminadas.
  • IDs nulos — 16 filas eliminadas.
  • Categorías — estandarizadas al catálogo de 29 valores.
  • Cuenta — estandarizada en Cuenta_limpia (12 categorías).
# Base de análisis: transacciones sin atípicos de monto.
# q_idx = índice calendario de quincena (1–15 y 16–fin de mes), contado desde ene-2024.
# Se calcula aquí para tener un orden temporal explícito (necesario para rezagos).
ig_clean <- ig_limpia %>%
  filter(Monto_Atipico == FALSE) %>%
  mutate(
    q_idx = (year(`Fecha de la transacción`) - 2024) * 24 +
            (month(`Fecha de la transacción`) - 1) * 2 +
            if_else(day(`Fecha de la transacción`) <= 15, 1, 2)
  )

# Verificar que la quincena de limpieza_MDF coincide 1 a 1 con q_idx
chequeo_q <- ig_clean %>%
  distinct(quincena, q_idx) %>%
  summarise(etiquetas = n_distinct(quincena), indices = n_distinct(q_idx), pares = n())

if (chequeo_q$pares == chequeo_q$etiquetas && chequeo_q$pares == chequeo_q$indices) {
  cat("OK: `quincena` de limpieza_MDF y q_idx coinciden 1 a 1 (",
      chequeo_q$indices, "quincenas ).\n")
} else {
  cat("ATENCIÓN: `quincena` no coincide 1 a 1 con la definición calendario.",
      "Este cuaderno agrupa por q_idx; revisar la definición en limpieza_MDF.\n")
}
## OK: `quincena` de limpieza_MDF y q_idx coinciden 1 a 1 ( 60 quincenas ).
tibble(
  Concepto = c(
    "Filas en ig_limpia.csv",
    "Sin atípicos de monto (base de análisis)",
    "Empresas únicas",
    "Con flag es_retroactivo = TRUE (Capa 3)",
    "Período"
  ),
  Valor = c(
    fmt(nrow(ig_limpia), 0),
    fmt(nrow(ig_clean), 0),
    fmt(n_distinct(ig_clean$ID), 0),
    fmt(sum(ig_clean$es_retroactivo, na.rm = TRUE), 0),
    paste(min(ig_clean$`Fecha de la transacción`),
          "→", max(ig_clean$`Fecha de la transacción`))
  )
) %>%
  kable(caption = "Tabla 1. Resumen de la base limpia recibida") %>%
  estilo(full_width = FALSE)
Tabla 1. Resumen de la base limpia recibida
Concepto Valor
Filas en ig_limpia.csv 13.113
Sin atípicos de monto (base de análisis) 12.339
Empresas únicas 557
Con flag es_retroactivo = TRUE (Capa 3) 4.455
Período 2024-01-15 → 2026-08-08

1.2 Clasificación de entradas y salidas por función económica — NUEVO

La hipótesis de ingresos permanentes (HIP) habla del consumo. Las salidas de un micronegocio incluyen mercancía, insumos y arriendo (costos que se mueven con las ventas), pagos de deuda y ahorro. Si una empresa ahorra más en sus buenas quincenas — que es exactamente suavizar —, en la base eso aparece como más “gasto”. Del lado de las entradas pasa algo parecido: un desembolso de un préstamo entra como ingreso y luego sale como gasto, lo que produce un β alto sin miopía de por medio.

Por eso se clasifican las transacciones según su función. Esta asignación es una propuesta y debe revisarse con el equipo: cambiarla aquí cambia todo el análisis por componentes (sección 2.6).

stopifnot("Categoría" %in% names(ig_clean))

# ── Salidas ──────────────────────────────────────────────────────────────────
# Lo que no aparece aquí (incluida Categoría = NA) queda como "Otros / sin clasificar".
grupos_salida <- tribble(
  ~Categoría,                    ~grupo_salida,
  "Materia prima",               "Operativo variable",
  "Mercancías",                  "Operativo variable",
  "Suministros",                 "Operativo variable",
  "Servicios tercerizados",      "Operativo variable",
  "Comisiones",                  "Operativo variable",
  "Transporte",                  "Operativo variable",
  "Gasto de Papelería",          "Operativo variable",
  "Arriendo",                    "Operativo fijo",
  "Servicios públicos",          "Operativo fijo",
  "Pago a empleados",            "Operativo fijo",     # REVISAR: en micronegocios puede ser jornal (variable)
  "Gastos Administrativos",      "Operativo fijo",
  "Gastos de software",          "Operativo fijo",
  "Marketing y publicidad",      "Operativo fijo",
  "Impuestos",                   "Operativo fijo",
  "En seguros",                  "Operativo fijo",
  "Mobiliario y maquinaria",     "Inversión",
  "Gastos personales",           "Personal / hogar",
  "Ahorro",                      "Financiero (ahorro y deuda)",
  "Cuota de préstamo formal",    "Financiero (ahorro y deuda)",
  "Cuota de préstamo informal",  "Financiero (ahorro y deuda)",
  "Financieros",                 "Financiero (ahorro y deuda)"
)

# ── Entradas ─────────────────────────────────────────────────────────────────
grupos_entrada <- tribble(
  ~Categoría,                    ~grupo_entrada,
  "Ventas",                      "Operativo",
  "Servicios",                   "Operativo",
  "Cuentas por cobrar",          "Operativo",
  "Comisiones",                  "Operativo",
  "Préstamos formales",          "Financiamiento",
  "Cuota de préstamo informal",  "Financiamiento",     # REVISAR: como entrada, ¿préstamo informal recibido?
  "Cuota de préstamo formal",    "Financiamiento",
  "Ahorro",                      "Financiamiento",     # retiro de ahorros
  "Financieros",                 "Financiamiento"
)

ig_clean <- ig_clean %>%
  left_join(grupos_salida,  by = "Categoría") %>%
  left_join(grupos_entrada, by = "Categoría") %>%
  mutate(
    grupo_salida = case_when(
      Tipo != "Salida"    ~ NA_character_,
      is.na(grupo_salida) ~ "Otros / sin clasificar",
      TRUE                ~ grupo_salida
    ),
    grupo_entrada = case_when(
      Tipo != "Entrada"    ~ NA_character_,
      is.na(grupo_entrada) ~ "Otros / sin clasificar",
      TRUE                 ~ grupo_entrada
    )
  )

resumen_grupo <- function(df, var, tipo) {
  df %>%
    filter(Tipo == tipo) %>%
    group_by(Grupo = {{ var }}) %>%
    summarise(
      Transacciones = n(),
      Empresas      = n_distinct(ID),
      `Monto total` = sum(Monto, na.rm = TRUE),
      .groups = "drop"
    ) %>%
    mutate(`% del monto` = 100 * `Monto total` / sum(`Monto total`)) %>%
    arrange(desc(`Monto total`))
}

resumen_grupo(ig_clean, grupo_salida, "Salida") %>%
  kable(caption = "Tabla 2a. Salidas por función económica (base sin atípicos)",
        format.args = FMT_TABLA, digits = c(0, 0, 0, 0, 1)) %>%
  estilo(full_width = FALSE)
Tabla 2a. Salidas por función económica (base sin atípicos)
Grupo Transacciones Empresas Monto total % del monto
Operativo variable 2.750 312 1.150.046.172 53,6
Operativo fijo 986 195 475.466.088 22,2
Personal / hogar 1.058 131 331.324.280 15,5
Financiero (ahorro y deuda) 405 56 122.663.100 5,7
Inversión 79 40 45.885.597 2,1
Otros / sin clasificar 154 13 18.602.831 0,9
resumen_grupo(ig_clean, grupo_entrada, "Entrada") %>%
  kable(caption = "Tabla 2b. Entradas por función económica (base sin atípicos)",
        format.args = FMT_TABLA, digits = c(0, 0, 0, 0, 1)) %>%
  estilo(full_width = FALSE)
Tabla 2b. Entradas por función económica (base sin atípicos)
Grupo Transacciones Empresas Monto total % del monto
Operativo 6.422 483 2.974.123.126 95,4
Otros / sin clasificar 431 80 106.767.758 3,4
Financiamiento 54 23 37.061.773 1,2
# Qué quedó sin clasificar — para revisar con el equipo
ig_clean %>%
  filter(grupo_salida == "Otros / sin clasificar" | grupo_entrada == "Otros / sin clasificar") %>%
  mutate(Categoría = replace_na(Categoría, "(sin categoría)")) %>%
  count(Tipo, Categoría, sort = TRUE, name = "Transacciones") %>%
  kable(caption = "Tabla 2c. Categorías que quedaron en 'Otros / sin clasificar' — revisar",
        format.args = FMT_TABLA) %>%
  estilo(full_width = FALSE)
Tabla 2c. Categorías que quedaron en ‘Otros / sin clasificar’ — revisar
Tipo Categoría Transacciones
Entrada (sin categoría) 204
Salida (sin categoría) 152
Entrada Arriendo 113
Entrada Mercancías 42
Entrada Materia prima 32
Entrada Donaciones 25
Entrada Premios 5
Entrada Ventas de acciones 5
Entrada Servicios tercerizados 2
Entrada Gastos Administrativos 1
Entrada Pago a empleados 1
Entrada Suministros 1
Salida Servicios 1
Salida Ventas de acciones 1

Entradas registradas con una categoría de gasto — NUEVO v2.1

Una entrada con categoría “Mercancías” o “Materia prima” casi seguro es una salida mal tipada. Si se deja como ingreso, infla las entradas y — si el usuario además registró la salida — también las salidas de la misma quincena, lo que fabrica correlación. “Arriendo” como entrada puede ser ingreso real por alquiler, así que se trata aparte. Aquí solo se marcan; la sección 2.11 re-estima los modelos sin ellas.

CATS_GASTO_ENT <- setdiff(grupos_salida$Categoría, c(grupos_entrada$Categoría, "Arriendo"))

ig_clean <- ig_clean %>%
  mutate(
    entrada_cat_gasto = Tipo == "Entrada" & Categoría %in% CATS_GASTO_ENT,
    entrada_arriendo  = Tipo == "Entrada" & Categoría %in% "Arriendo"
  )

total_entradas <- sum(ig_clean$Monto[ig_clean$Tipo == "Entrada"], na.rm = TRUE)
ids_afectadas  <- ig_clean %>% filter(entrada_cat_gasto) %>% distinct(ID) %>% pull(ID)
ids_arriendo   <- ig_clean %>% filter(entrada_arriendo)  %>% distinct(ID) %>% pull(ID)

tab_ent_gasto <- ig_clean %>%
  filter(entrada_cat_gasto | entrada_arriendo) %>%
  mutate(Tratamiento = if_else(entrada_arriendo, "Arriendo (¿ingreso por alquiler?)",
                               "Categoría de gasto (¿mal tipada?)")) %>%
  group_by(Tratamiento, Categoría) %>%
  summarise(Transacciones = n(), Empresas = n_distinct(ID),
            Monto = sum(Monto, na.rm = TRUE), .groups = "drop") %>%
  mutate(`% del monto de entradas` = 100 * Monto / total_entradas) %>%
  arrange(Tratamiento, desc(Monto))

if (nrow(tab_ent_gasto) > 0) {
  tab_ent_gasto %>%
    kable(caption = "Tabla 2d. Entradas registradas con una categoría de gasto",
          format.args = FMT_TABLA, digits = c(0, 0, 0, 0, 0, 2)) %>%
    estilo(full_width = FALSE) %>%
    collapse_rows(columns = 1, valign = "top")
}
Tabla 2d. Entradas registradas con una categoría de gasto
Tratamiento Categoría Transacciones Empresas Monto % del monto de entradas
Arriendo (¿ingreso por alquiler?) Arriendo 113 15 21.789.700 0,70
Categoría de gasto (¿mal tipada?) Mercancías 42 41 38.150.000 1,22
Materia prima 32 4 2.550.800 0,08
Pago a empleados 1 1 600.000 0,02
Servicios tercerizados 2 2 455.000 0,01
Suministros 1 1 350.000 0,01
Gastos Administrativos 1 1 73.808 0,00

48 empresas tienen al menos una entrada con categoría de gasto (sin contar arriendo), y 15 registran arriendo como entrada.

1.3 Panel quincenal por empresa

COMPONENTES <- c(G_var = "Operativo variable", G_fijo = "Operativo fijo",
                 G_inv = "Inversión", G_pers = "Personal / hogar",
                 G_fin = "Financiero (ahorro y deuda)", G_otros = "Otros / sin clasificar")

suma_si <- function(monto, cond) sum(monto[cond], na.rm = TRUE)

panel_q <- ig_clean %>%
  group_by(ID, q_idx) %>%
  summarise(
    quincena = first(quincena),
    Y_qt     = suma_si(Monto, Tipo == "Entrada"),
    Y_op     = suma_si(Monto, Tipo == "Entrada" & grupo_entrada == "Operativo"),
    Y_catg   = suma_si(Monto, entrada_cat_gasto),   # entradas con categoría de gasto (v2.1)
    Y_arr    = suma_si(Monto, entrada_arriendo),    # arriendo como entrada (v2.1)
    G_qt     = suma_si(Monto, Tipo == "Salida"),
    G_var    = suma_si(Monto, Tipo == "Salida" & grupo_salida == "Operativo variable"),
    G_fijo   = suma_si(Monto, Tipo == "Salida" & grupo_salida == "Operativo fijo"),
    G_inv    = suma_si(Monto, Tipo == "Salida" & grupo_salida == "Inversión"),
    G_pers   = suma_si(Monto, Tipo == "Salida" & grupo_salida == "Personal / hogar"),
    G_fin    = suma_si(Monto, Tipo == "Salida" & grupo_salida == "Financiero (ahorro y deuda)"),
    G_otros  = suma_si(Monto, Tipo == "Salida" & grupo_salida == "Otros / sin clasificar"),
    F_qt     = sum(flujo, na.rm = TRUE),
    n_trans  = n(),
    n_dias   = n_distinct(`Fecha de la transacción`),   # días con algún registro
    .groups  = "drop"
  )

# Verificaciones de consistencia
stopifnot(isTRUE(all.equal(panel_q$G_qt,
  with(panel_q, G_var + G_fijo + G_inv + G_pers + G_fin + G_otros))))
cat("Quincenas donde flujo = entradas − salidas:",
    fmt_pct(mean(abs(panel_q$F_qt - (panel_q$Y_qt - panel_q$G_qt)) < 1)), "\n")
## Quincenas donde flujo = entradas − salidas: 100,0%
# Solo quincenas con AMBOS ingresos y gastos (definición de v1; ver sección 2.5)
panel_bilateral <- panel_q %>%
  filter(Y_qt > 0, G_qt > 0)

cat("Empresas con al menos una quincena bilateral:", n_distinct(panel_bilateral$ID), "\n")
## Empresas con al menos una quincena bilateral: 314
cat("Total quincenas bilaterales:", nrow(panel_bilateral), "\n")
## Total quincenas bilaterales: 816

1.4 Indicadores por empresa y criterios de submuestras

El análisis enfrenta una tensión: más criterios de calidad → muestra más pequeña → menos poder estadístico. Se construyen cuatro submuestras con criterios progresivamente más exigentes para evaluar la robustez (siguiendo la sugerencia de G).

# Ajuste por empresa: PMG en niveles, su error estándar y la elasticidad log-log
ajuste_emp <- function(Y, G) {
  m  <- lm(G ~ Y)
  ml <- lm(log(G) ~ log(Y))
  s  <- summary(m)$coefficients
  tibble(
    corr_ig   = suppressWarnings(cor(Y, G)),
    beta      = unname(coef(m)["Y"]),          # PMG: pesos de salida por peso de entrada
    beta_se   = if ("Y" %in% rownames(s)) s["Y", "Std. Error"] else NA_real_,
    elast_log = unname(coef(ml)["log(Y)"])     # elasticidad: % de salida por 1% de entrada
  )
}

indicadores_emp <- panel_bilateral %>%
  group_by(ID) %>%
  summarise(
    n_quincenas_bilat  = n(),
    ing_medio          = mean(Y_qt),
    gas_medio          = mean(G_qt),
    flujo_medio        = mean(F_qt),
    sd_ingreso         = sd(Y_qt),
    sd_gasto           = sd(G_qt),
    cv_ingreso         = sd_ingreso / ing_medio,
    cv_gasto           = sd_gasto / gas_medio,
    ratio_sd           = sd_gasto / sd_ingreso,   # volatilidad relativa EN PESOS
    pct_quin_negativas = mean(F_qt < 0),
    ratio_gi           = sum(G_qt) / sum(Y_qt),
    .groups = "drop"
  ) %>%
  # Quincenas con cualquier registro (no solo bilaterales)
  left_join(panel_q %>% count(ID, name = "n_quincenas_total"), by = "ID") %>%
  # PMG, elasticidad y correlación (solo con ≥ MIN_Q_BETA quincenas bilaterales)
  left_join(
    panel_bilateral %>%
      group_by(ID) %>%
      filter(n() >= MIN_Q_BETA) %>%
      summarise(ajuste_emp(Y_qt, G_qt), .groups = "drop"),
    by = "ID"
  ) %>%
  # Perfil de la empresa
  left_join(
    car_clean %>%
      select(ID, Sector, Genero, Perfil_Digital,
             `División de Finanzas`, `Año de Inicio`,
             `Acceso a Financiamiento`, Localidad),
    by = "ID"
  ) %>%
  mutate(
    antiguedad = 2026 - `Año de Inicio`,
    es_mujer   = if_else(Genero == "Mujer", 1L, 0L),
    es_digital = if_else(Perfil_Digital == "En transformación digital", 1L, 0L),
    separa_fin = if_else(`División de Finanzas` == "Sí", 1L, 0L),
    # Agro, Industria y Otros se agrupan: con < 5 empresas por celda no hay inferencia posible
    sector_g   = case_when(
      is.na(Sector)                          ~ NA_character_,
      Sector %in% c("Comercio", "Servicios") ~ Sector,
      TRUE                                   ~ "Otros sectores"
    )
  )

# ── SUBMUESTRA A: criterio mínimo ────────────────────────────────────────────
# ≥ 3 quincenas bilaterales. Máxima cobertura.
muestra_A <- indicadores_emp %>%
  filter(n_quincenas_bilat >= 3)

# ── SUBMUESTRA B: criterio intermedio (PRINCIPAL) ────────────────────────────
# ≥ 5 quincenas bilaterales + ratio gasto/ingreso < 3 + β calculable
muestra_B <- indicadores_emp %>%
  filter(n_quincenas_bilat >= 5, ratio_gi < 3, !is.na(beta))

# ── SUBMUESTRA C: criterio estricto ──────────────────────────────────────────
# ≥ 8 quincenas bilaterales + ratio < 2 + CV ingreso < 3
muestra_C <- indicadores_emp %>%
  filter(n_quincenas_bilat >= 8, ratio_gi < 2, cv_ingreso < 3, !is.na(beta))

# ── SUBMUESTRA D: confiabilidad de cargue ────────────────────────────────────
# Empresas sin ninguna transacción cargada con > 30 días de retraso y sin
# fechas de cargue faltantes. Minimiza el riesgo de duplicados irresolubles
# (Capa 3) a costa de una muestra muy pequeña.
riesgo_cargue <- ig_clean %>%
  group_by(ID) %>%
  summarise(
    pct_retroactivo  = mean(es_retroactivo, na.rm = TRUE),
    tiene_sin_cargue = any(is.na(diff_dias_cargue)),
    .groups = "drop"
  )

muestra_D <- indicadores_emp %>%
  left_join(riesgo_cargue, by = "ID") %>%
  filter(n_quincenas_bilat >= 3, pct_retroactivo == 0, !tiene_sin_cargue, !is.na(beta))

SUBMUESTRAS <- list(
  "A — mínimo"        = muestra_A,
  "B — intermedio"    = muestra_B,
  "C — estricto"      = muestra_C,
  "D — confiabilidad" = muestra_D
)

n_emp_trans <- n_distinct(ig_clean$ID)

tibble(
  Submuestra = c(
    "A — mínimo (≥3 q bilaterales)",
    "B — intermedio (≥5 q, ratio<3)",
    "C — estricto (≥8 q, ratio<2, CV<3)",
    "D — confiabilidad de cargue (0% retroactivo)"
  ),
  N = map_int(SUBMUESTRAS, nrow),
  `% de empresas con transacciones` = 100 * N / n_emp_trans,
  `Criterio duplicados` = c(rep("Capas 1 y 2 eliminadas. Capa 3: no controlada.", 3),
                            "Capas 1, 2 y 3 controladas — sin cargue retroactivo"),
  `Mediana quincenas` = map_dbl(SUBMUESTRAS, ~ median(.x$n_quincenas_bilat)),
  `% con β calculado` = map_dbl(SUBMUESTRAS, ~ 100 * mean(!is.na(.x$beta)))
) %>%
  kable(caption = "Tabla 3. Criterios y características de las cuatro submuestras",
        format.args = FMT_TABLA, digits = 1) %>%
  estilo(full_width = TRUE) %>%
  row_spec(2, bold = TRUE, background = "#EBF5FB") %>%
  row_spec(4, bold = TRUE, background = "#D5F5E3")
Tabla 3. Criterios y características de las cuatro submuestras
Submuestra N % de empresas con transacciones Criterio duplicados Mediana quincenas % con β calculado
A — mínimo (≥3 q bilaterales) 99 17,8 Capas 1 y 2 eliminadas. Capa 3: no controlada. 5 67,7
B — intermedio (≥5 q, ratio<3) 51 9,2 Capas 1 y 2 eliminadas. Capa 3: no controlada. 7 100,0
C — estricto (≥8 q, ratio<2, CV<3) 17 3,1 Capas 1 y 2 eliminadas. Capa 3: no controlada. 11 100,0
D — confiabilidad de cargue (0% retroactivo) 11 2,0 Capas 1, 2 y 3 controladas — sin cargue retroactivo 5 100,0
indicadores_emp %>%
  mutate(
    corte = case_when(
      n_quincenas_bilat >= 8 ~ "≥ 8 quincenas",
      n_quincenas_bilat >= 5 ~ "5 a 7 quincenas",
      n_quincenas_bilat >= 3 ~ "3 a 4 quincenas",
      TRUE                   ~ "Menos de 3 (excluida)"
    ),
    corte = factor(corte, levels = c("≥ 8 quincenas", "5 a 7 quincenas",
                                     "3 a 4 quincenas", "Menos de 3 (excluida)"))
  ) %>%
  ggplot(aes(x = n_quincenas_bilat, fill = corte)) +
  geom_histogram(binwidth = 1, color = "white", linewidth = 0.3) +
  scale_fill_manual(values = c("≥ 8 quincenas" = COL_AZUL, "5 a 7 quincenas" = "#2e86c1",
                               "3 a 4 quincenas" = "#85c1e9", "Menos de 3 (excluida)" = "#d5d8dc"),
                    name = NULL) +
  geom_vline(xintercept = c(3, 5, 8) - 0.5, linetype = "dashed",
             color = "gray40", linewidth = 0.7) +
  annotate("text", x = c(3.2, 5.2, 8.2), y = Inf, label = c("Corte A", "Corte B", "Corte C"),
           vjust = 2, hjust = 0, size = 3, color = "gray40") +
  labs(title = "Figura 1. Distribución de quincenas bilaterales por empresa",
       subtitle = "Los cortes son por número de quincenas; B y C aplican además filtros de ratio gasto/ingreso y CV.",
       x = "Número de quincenas con ingresos Y gastos", y = "Número de empresas") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom", plot.title = element_text(face = "bold"))

1.5 Perfil de cada submuestra

perfil_tabla <- function(df, nombre) {
  df %>%
    summarise(
      N                            = n(),
      `Quincenas (mediana)`        = median(n_quincenas_bilat),
      `Ing. mediano/quincena`      = median(ing_medio),
      `Gas. mediano/quincena`      = median(gas_medio),
      `% quinc. negativas (med.)`  = 100 * median(pct_quin_negativas),
      `% Comercio`                 = 100 * mean(Sector == "Comercio", na.rm = TRUE),
      `% Servicios`                = 100 * mean(Sector == "Servicios", na.rm = TRUE),
      `% Mujer`                    = 100 * mean(es_mujer, na.rm = TRUE),
      `% Separa finanzas`          = 100 * mean(separa_fin, na.rm = TRUE)
    ) %>%
    mutate(Submuestra = nombre, .before = 1)
}

imap_dfr(SUBMUESTRAS, perfil_tabla) %>%
  kable(caption = "Tabla 4. Perfil comparativo de las cuatro submuestras (montos en pesos)",
        format.args = FMT_TABLA, digits = c(0, 0, 0, 0, 0, 1, 1, 1, 1, 1)) %>%
  estilo(full_width = TRUE, font_size = 11)
Tabla 4. Perfil comparativo de las cuatro submuestras (montos en pesos)
Submuestra N Quincenas (mediana) Ing. mediano/quincena Gas. mediano/quincena % quinc. negativas (med.) % Comercio % Servicios % Mujer % Separa finanzas
A — mínimo 99 5 1.009.275 759.928 33,3 58,6 25,3 79,2 60,6
B — intermedio 51 7 1.361.135 822.123 27,3 60,8 25,5 81,6 58,8
C — estricto 17 11 1.717.035 902.818 27,3 52,9 29,4 87,5 58,8
D — confiabilidad 11 5 1.666.469 1.165.029 25,0 45,5 45,5 81,8 54,5

1.6 Transacciones por empresa en cada submuestra

A mayor número de transacciones por empresa, más rica es la serie. Esta tabla muestra si los criterios progresivos seleccionan, como efecto indirecto, a las empresas que más usan la app.

dist_trans <- function(df, nombre) {
  ig_clean %>%
    filter(ID %in% df$ID) %>%
    count(ID, name = "n_trans") %>%
    summarise(
      Submuestra               = nombre,
      Empresas                 = n(),
      `Trans. totales`         = sum(n_trans),
      `Media trans./empresa`   = mean(n_trans),
      `Mediana trans./empresa` = median(n_trans),
      P25                      = quantile(n_trans, 0.25),
      P75                      = quantile(n_trans, 0.75),
      `Mín.`                   = min(n_trans),
      `Máx.`                   = max(n_trans)
    )
}

imap_dfr(SUBMUESTRAS, dist_trans) %>%
  kable(caption = "Tabla 5. Distribución de transacciones por empresa en cada submuestra",
        format.args = FMT_TABLA, digits = c(0, 0, 0, 1, 0, 0, 0, 0, 0)) %>%
  estilo(full_width = FALSE) %>%
  row_spec(2, bold = TRUE, background = "#EBF5FB") %>%
  row_spec(4, bold = TRUE, background = "#D5F5E3")
Tabla 5. Distribución de transacciones por empresa en cada submuestra
Submuestra Empresas Trans. totales Media trans./empresa Mediana trans./empresa P25 P75 Mín. Máx.
A — mínimo 99 9.377 94,7 71 29 126 8 757
B — intermedio 51 7.087 139,0 110 71 162 22 757
C — estricto 17 3.665 215,6 152 106 192 56 757
D — confiabilidad 11 807 73,4 59 32 111 16 156

1.7 ¿A quién representan las submuestras? — NUEVO

rep_tabla <- car_clean %>%
  mutate(
    grupo = case_when(
      ID %in% muestra_B$ID ~ "Submuestra B",
      ID %in% ig_clean$ID  ~ "Resto con transacciones",
      TRUE                 ~ "Sin transacciones"
    ),
    grupo = factor(grupo, levels = c("Submuestra B", "Resto con transacciones", "Sin transacciones"))
  ) %>%
  group_by(Grupo = grupo) %>%
  summarise(
    Empresas              = n(),
    `% Mujer`             = 100 * mean(Genero == "Mujer", na.rm = TRUE),
    `% Comercio`          = 100 * mean(Sector == "Comercio", na.rm = TRUE),
    `% Separa finanzas`   = 100 * mean(`División de Finanzas` == "Sí", na.rm = TRUE),
    `% Perfil digital`    = 100 * mean(Perfil_Digital == "En transformación digital", na.rm = TRUE),
    `Antigüedad mediana`  = median(2026 - `Año de Inicio`, na.rm = TRUE),
    .groups = "drop"
  )

rep_tabla %>%
  kable(caption = "Tabla 6. Perfil de la Submuestra B frente al resto de empresas registradas",
        format.args = FMT_TABLA, digits = c(0, 0, 1, 1, 1, 1, 0)) %>%
  estilo(full_width = FALSE)
Tabla 6. Perfil de la Submuestra B frente al resto de empresas registradas
Grupo Empresas % Mujer % Comercio % Separa finanzas % Perfil digital Antigüedad mediana
Submuestra B 51 81,6 60,8 58,8 31,4 4
Resto con transacciones 506 74,8 76,1 48,0 48,7 5
Sin transacciones 353 70,7 77,0 63,4 53,1 5
pct_B_univ <- nrow(muestra_B) / n_emp_trans

La Submuestra B reúne 51 de las 557 empresas con al menos una transacción (9,2%) y de las 910 registradas en la caracterización. Son las que más usan la app. Por eso los resultados que siguen se refieren a usuarios activos de Mi Diario Financiero: si estas empresas difieren del resto en la Tabla 6, esa diferencia es una razón más para no extrapolar.


ETAPA 2 — Análisis: ¿gastan lo que entra?

Qué mide β y qué no. β es la pendiente de la regresión de las salidas sobre las entradas de la misma quincena: cuántos pesos de salida registrada acompañan a cada peso adicional de entrada registrada. Es una propensión marginal a gastar (PMG), no una elasticidad (la elasticidad sale de la versión log-log, que también se reporta).

Para leerla como una prueba de la HIP (Hall, 1978) hacen falta dos supuestos que esta base no garantiza:

  1. Que las salidas sean consumo. Incluyen inventario, insumos, arriendo, deuda y ahorro. La sección 2.6 descompone la PMG por componente.
  2. Que lo registrado refleje la actividad real y no cuánto se usó la app esa quincena. Las secciones 2.4 y 2.5 lo ponen a prueba.

Mientras esos dos puntos no se resuelvan, β describe la sensibilidad del flujo de salidas al flujo de entradas, y así se reporta.

El análisis principal por empresa se hace sobre la Submuestra B (n = 51). Los modelos de panel usan a todas las empresas con al menos 2 quincenas bilaterales.

2.1 Irregularidad del flujo: déficit y volatilidad

Quincenas en déficit

deficit_med_B <- median(muestra_B$pct_quin_negativas)
uno_de_cada   <- if (deficit_med_B > 0) round(1 / deficit_med_B) else NA

En la empresa mediana de la Submuestra B, el 27,3% de las quincenas bilaterales cierra con más salidas que entradas: más o menos 1 de cada 4 quincenas.

muestra_B %>%
  filter(!is.na(sector_g)) %>%
  ggplot(aes(x = pct_quin_negativas, y = reorder(sector_g, pct_quin_negativas), fill = sector_g)) +
  geom_density_ridges(alpha = 0.7, scale = 1.2, bandwidth = 0.08) +
  scale_fill_manual(values = COL_SECT) +
  scale_x_continuous(labels = percent_format(decimal.mark = ",")) +
  labs(title = "Figura 2. Distribución del % de quincenas en déficit por sector",
       subtitle = paste0("Submuestra B. Un valor de 25% = 1 de cada 4 quincenas el gasto superó al ingreso.\n",
                         "'Otros sectores' agrupa Agro, Industria y Otros (n pequeño)."),
       x = "Proporción de quincenas con flujo neto negativo", y = NULL) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none", plot.title = element_text(face = "bold"))

Volatilidad: por qué el CV engaña

En v1 se reportó que “el gasto es más volátil que el ingreso” porque el CV del gasto superaba al del ingreso. Pero el CV divide por la media, y la media del gasto es menor que la del ingreso. Formalmente:

\[\frac{CV_G}{CV_Y} = \frac{\sigma_G}{\sigma_Y}\cdot\frac{\bar Y}{\bar G}\]

Si el gasto medio es, digamos, el 60% del ingreso medio, el CV del gasto queda inflado en un factor de 1/0,6 ≈ 1,7 aunque el gasto se mueva menos en pesos. La comparación relevante es la desviación estándar en pesos.

vol_tabla <- function(df, nombre) {
  tibble(
    Submuestra                          = nombre,
    N                                   = nrow(df),
    `% CV gasto > CV ingreso`           = 100 * mean(df$cv_gasto > df$cv_ingreso, na.rm = TRUE),
    `% DE gasto > DE ingreso (pesos)`   = 100 * mean(df$sd_gasto > df$sd_ingreso, na.rm = TRUE),
    `Mediana DE gasto / DE ingreso`     = median(df$ratio_sd, na.rm = TRUE),
    `Mediana gasto medio / ing. medio`  = median(df$gas_medio / df$ing_medio, na.rm = TRUE)
  )
}

tab_vol <- imap_dfr(SUBMUESTRAS, vol_tabla)

tab_vol %>%
  kable(caption = "Tabla 7. Volatilidad del gasto frente al ingreso: CV vs. desviación estándar en pesos",
        format.args = FMT_TABLA, digits = c(0, 0, 1, 1, 2, 2)) %>%
  estilo(full_width = TRUE)
Tabla 7. Volatilidad del gasto frente al ingreso: CV vs. desviación estándar en pesos
Submuestra N % CV gasto > CV ingreso % DE gasto > DE ingreso (pesos) Mediana DE gasto / DE ingreso Mediana gasto medio / ing. medio
A — mínimo 99 60,6 38,4 0,86 0,74
B — intermedio 51 60,8 29,4 0,83 0,70
C — estricto 17 58,8 11,8 0,59 0,70
D — confiabilidad 11 36,4 36,4 0,81 0,71
p1 <- muestra_B %>%
  filter(!is.na(cv_ingreso), !is.na(cv_gasto), cv_ingreso < 4, cv_gasto < 4) %>%
  ggplot(aes(x = cv_ingreso, y = cv_gasto, color = sector_g)) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "gray50") +
  geom_point(alpha = 0.7, size = 2) +
  scale_color_manual(values = COL_SECT, na.value = "gray70") +
  scale_x_continuous(labels = eje_coma) + scale_y_continuous(labels = eje_coma) +
  labs(title = "Con CV (v1)", x = "CV ingreso", y = "CV gasto", color = NULL) +
  theme_minimal(base_size = 11) +
  theme(legend.position = "bottom", plot.title = element_text(face = "bold"))

p2 <- muestra_B %>%
  filter(sd_ingreso > 0, sd_gasto > 0) %>%
  ggplot(aes(x = sd_ingreso, y = sd_gasto, color = sector_g)) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "gray50") +
  geom_point(alpha = 0.7, size = 2) +
  scale_x_log10(labels = etiqueta_millones) +
  scale_y_log10(labels = etiqueta_millones) +
  scale_color_manual(values = COL_SECT, na.value = "gray70") +
  labs(title = "En pesos (v2)", x = "DE ingreso quincenal (millones, escala log)",
       y = "DE gasto quincenal (millones, escala log)", color = NULL) +
  theme_minimal(base_size = 11) +
  theme(legend.position = "bottom", plot.title = element_text(face = "bold"))

(p1 + p2) +
  plot_annotation(
    title = "Figura 3. Volatilidad del gasto vs. del ingreso por empresa (Submuestra B)",
    subtitle = "Puntos sobre la diagonal = el gasto varía más que el ingreso.",
    theme = theme(plot.title = element_text(face = "bold"))
  ) +
  plot_layout(guides = "collect") & theme(legend.position = "bottom")

Indicadores por sector

n_por_sector <- muestra_B %>% filter(!is.na(Sector)) %>% count(Sector)

muestra_B %>%
  filter(!is.na(sector_g)) %>%
  group_by(Sector = sector_g) %>%
  summarise(
    N                          = n(),
    `% quinc. negativas (med.)` = 100 * median(pct_quin_negativas),
    `DE gasto / DE ing. (med.)` = median(ratio_sd, na.rm = TRUE),
    `Corr. ing-gas (med.)`      = median(corr_ig, na.rm = TRUE),
    `PMG β (med.)`              = median(beta, na.rm = TRUE),
    `Elasticidad log (med.)`    = median(elast_log, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(N)) %>%
  kable(caption = "Tabla 8. Indicadores de irregularidad y sensibilidad por sector (Submuestra B)",
        format.args = FMT_TABLA, digits = c(0, 0, 1, 2, 2, 2, 2)) %>%
  estilo(full_width = FALSE) %>%
  footnote(general_title = "Nota: ", footnote_as_chunk = TRUE,
           general = paste0("'Otros sectores' agrupa: ",
                            paste(n_por_sector$Sector[!n_por_sector$Sector %in% c("Comercio", "Servicios")],
                                  " (n = ", n_por_sector$n[!n_por_sector$Sector %in% c("Comercio", "Servicios")],
                                  ")", sep = "", collapse = ", "),
                            ". Con menos de 5 empresas por celda las medianas por sector no son interpretables."))
Tabla 8. Indicadores de irregularidad y sensibilidad por sector (Submuestra B)
Sector N % quinc. negativas (med.) DE gasto / DE ing. (med.) Corr. ing-gas (med.) PMG β (med.) Elasticidad log (med.)
Comercio 31 20,0 0,84 0,74 0,41 0,77
Servicios 13 28,6 0,91 0,86 0,57 0,83
Otros sectores 7 40,0 0,62 0,74 0,36 0,65
Nota: ‘Otros sectores’ agrupa: Agro (n = 2), Industria (n = 3), Otros (n = 2). Con menos de 5 empresas por celda las medianas por sector no son interpretables.

2.2 Propensión marginal a gastar por empresa

Distribución de la PMG (β)

CLASES_BETA <- c("Contracíclica (β < 0)", "Absorbe hasta la mitad (0 ≤ β ≤ 0,5)",
                 "Absorbe más de la mitad (0,5 < β ≤ 1)", "Sobre-reacciona (β > 1)")
COL_CLASES  <- setNames(c("#1a5276", COL_POS, "#f39c12", COL_NEG), CLASES_BETA)

clasificar_beta <- function(b) {
  factor(case_when(b < 0 ~ CLASES_BETA[1], b <= 0.5 ~ CLASES_BETA[2],
                   b <= 1 ~ CLASES_BETA[3], TRUE ~ CLASES_BETA[4]),
         levels = CLASES_BETA)
}

beta_clean <- muestra_B %>%
  filter(!is.na(beta)) %>%
  mutate(beta_win = pmin(pmax(beta, -2), 3),   # solo para visualizar
         clase_beta = clasificar_beta(beta))

med_beta <- median(beta_clean$beta)

beta_clean %>%
  ggplot(aes(x = beta_win)) +
  geom_histogram(aes(fill = clase_beta), binwidth = 0.15, color = "white", linewidth = 0.3) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray40", linewidth = 0.8) +
  geom_vline(xintercept = 1, linetype = "dashed", color = COL_NEG, linewidth = 0.8) +
  geom_vline(xintercept = med_beta, color = COL_POS, linewidth = 1) +
  annotate("text", x = 0.05, y = Inf, vjust = 2, hjust = 0, size = 2.8, color = "gray40",
           label = "β = 0\n(las salidas no\nresponden)") +
  annotate("text", x = 1.05, y = Inf, vjust = 2, hjust = 0, size = 2.8, color = COL_NEG,
           label = "β = 1\n(sale todo el\ningreso adicional)") +
  annotate("text", x = med_beta + 0.05, y = Inf, vjust = 5, hjust = 0, size = 2.8, color = COL_POS,
           label = paste0("Mediana\n= ", fmt(med_beta))) +
  scale_fill_manual(values = COL_CLASES, name = NULL, drop = FALSE) +
  scale_x_continuous(labels = eje_coma) +
  labs(title = "Figura 4. Distribución de la propensión marginal a gastar (β) por empresa",
       subtitle = paste0("Submuestra B, N = ", nrow(beta_clean),
                         " empresas. β winsorizado en [−2, 3] solo para visualizar."),
       x = "PMG β (pendiente de G_qt sobre Y_qt dentro de cada empresa)",
       y = "Número de empresas") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom", plot.title = element_text(face = "bold"),
        legend.text = element_text(size = 9)) +
  guides(fill = guide_legend(nrow = 2))

beta_clean %>%
  count(clase_beta, name = "N", .drop = FALSE) %>%
  mutate(`%` = 100 * N / sum(N)) %>%
  rename(Clase = clase_beta) %>%
  kable(caption = "Tabla 9. Clasificación de empresas por PMG (Submuestra B)",
        format.args = FMT_TABLA, digits = c(0, 0, 1)) %>%
  estilo(full_width = FALSE)
Tabla 9. Clasificación de empresas por PMG (Submuestra B)
Clase N %
Contracíclica (β < 0) 8 15,7
Absorbe hasta la mitad (0 ≤ β ≤ 0,5) 24 47,1
Absorbe más de la mitad (0,5 < β ≤ 1) 14 27,5
Sobre-reacciona (β > 1) 5 9,8

¿Qué tan precisos son los β individuales? — NUEVO

Cada β individual sale de pocas quincenas. Antes de tratar la mediana de 51 pendientes como un dato exacto, vale la pena ver cuánto ruido trae cada una.

precision <- muestra_B %>%
  filter(!is.na(beta_se), n_quincenas_bilat > 2) %>%
  mutate(
    tc    = qt(0.975, n_quincenas_bilat - 2),
    lo    = beta - tc * beta_se,
    hi    = beta + tc * beta_se,
    inc_0 = lo <= 0 & hi >= 0,
    inc_1 = lo <= 1 & hi >= 1
  )

tibble(
  Indicador = c("Empresas con error estándar calculable",
                "Mediana del error estándar de β individual",
                "Mediana del ancho del IC 95% individual",
                "% de empresas cuyo IC 95% incluye β = 0",
                "% de empresas cuyo IC 95% incluye β = 1",
                "% cuyo IC incluye 0 y 1 a la vez (β no informativo)"),
  Valor = c(fmt(nrow(precision), 0),
            fmt(median(precision$beta_se)),
            fmt(median(precision$hi - precision$lo)),
            fmt_pct(mean(precision$inc_0)),
            fmt_pct(mean(precision$inc_1)),
            fmt_pct(mean(precision$inc_0 & precision$inc_1)))
) %>%
  kable(caption = "Tabla 10. Precisión de las PMG estimadas empresa por empresa (Submuestra B)") %>%
  estilo(full_width = FALSE)
Tabla 10. Precisión de las PMG estimadas empresa por empresa (Submuestra B)
Indicador Valor
Empresas con error estándar calculable 51
Mediana del error estándar de β individual 0,19
Mediana del ancho del IC 95% individual 0,88
% de empresas cuyo IC 95% incluye β = 0 47,1%
% de empresas cuyo IC 95% incluye β = 1 49,0%
% cuyo IC incluye 0 y 1 a la vez (β no informativo) 27,5%

Si una fracción alta de empresas tiene un IC que contiene a la vez 0 y 1, la clasificación de la Tabla 9 es en buena parte ruido de estimación: una empresa puede caer en “sobre-reacciona” solo por tener 5 quincenas. Por eso la v2 trae el panel de la sección 2.3, que agrega la información de todas las empresas antes de estimar.

Mediana de la PMG: IC bootstrap y pruebas

La medida principal es el IC bootstrap de la mediana (2000 réplicas). Se reportan dos pruebas: la de signo, que contrasta la mediana sin supuestos, y la de Wilcoxon (la de v1), que contrasta la pseudo-mediana y supone simetría.

tabla_mediana <- function(df, nombre) {
  b  <- df$beta[!is.na(df$beta)]
  e  <- df$elast_log[!is.na(df$elast_log)]
  ic <- boot_mediana(b)
  tibble(
    Submuestra = nombre,
    N          = length(b),
    med        = median(b),
    lo         = ic[1],
    hi         = ic[2],
    med_e      = if (length(e) > 0) median(e) else NA_real_,
    p_sig0     = p_signo(b, 0),
    p_sig1     = p_signo(b, 1),
    p_wil0     = wilcox.test(b, mu = 0)$p.value,
    p_wil1     = wilcox.test(b, mu = 1)$p.value,
    pct_neg    = mean(b < 0),
    pct_gt1    = mean(b > 1)
  )
}

set.seed(SEED)
res_med <- imap_dfr(SUBMUESTRAS, tabla_mediana)

res_med %>%
  transmute(
    Submuestra, N,
    `PMG mediana`             = fmt(med),
    `IC 95% bootstrap`        = fmt_ic(lo, hi),
    `Elasticidad log mediana` = fmt(med_e),
    `p signo (β=0)`           = fmt_p(p_sig0),
    `p signo (β=1)`           = fmt_p(p_sig1),
    `p Wilcoxon (β=0)`        = fmt_p(p_wil0),
    `p Wilcoxon (β=1)`        = fmt_p(p_wil1),
    `% β < 0`                 = fmt_pct(pct_neg),
    `% β > 1`                 = fmt_pct(pct_gt1)
  ) %>%
  kable(caption = "Tabla 11. Mediana de la PMG por submuestra: IC bootstrap y pruebas contra 0 y 1") %>%
  estilo(full_width = TRUE, font_size = 11) %>%
  row_spec(2, bold = TRUE, background = "#EBF5FB") %>%
  row_spec(4, bold = TRUE, background = "#D5F5E3")
Tabla 11. Mediana de la PMG por submuestra: IC bootstrap y pruebas contra 0 y 1
Submuestra N PMG mediana IC 95% bootstrap Elasticidad log mediana p signo (β=0) p signo (β=1) p Wilcoxon (β=0) p Wilcoxon (β=1) % β < 0 % β > 1
A — mínimo 67 0,43 [0,30; 0,56] 0,63 < 0,001 < 0,001 < 0,001 < 0,001 16,4% 16,4%
B — intermedio 51 0,41 [0,30; 0,53] 0,77 < 0,001 < 0,001 < 0,001 < 0,001 15,7% 9,8%
C — estricto 17 0,36 [0,15; 0,57] 0,65 0,002 < 0,001 0,004 < 0,001 11,8% 5,9%
D — confiabilidad 11 0,50 [0,19; 0,79] 0,74 < 0,001 0,065 < 0,001 0,054 0,0% 18,2%
rb <- res_med %>% filter(Submuestra == "B — intermedio")
txt_B <- case_when(
  rb$lo > 0 & rb$hi < 1 ~ paste0(
    "la mediana es distinta tanto de 0 como de 1: hay **suavización parcial**. ",
    "De cada $100 adicionales que entran en una quincena, salen alrededor de $",
    round(100 * rb$med), " (IC 95%: $", round(100 * rb$lo), " a $", round(100 * rb$hi), ")"),
  rb$lo <= 0 ~ "no se puede descartar que la PMG mediana sea 0",
  TRUE       ~ "no se puede descartar que la PMG mediana sea 1 (gastar todo lo que entra)"
)

Lectura para la Submuestra B: la mediana es distinta tanto de 0 como de 1: hay suavización parcial. De cada $100 adicionales que entran en una quincena, salen alrededor de $41 (IC 95%: $30 a $53).

2.3 Modelo de panel con efectos fijos — NUEVO

En lugar de estimar una pendiente por empresa y luego resumirlas, el panel estima una pendiente común usando solo la variación dentro de cada empresa (efecto fijo de empresa) y quitando choques comunes a todas en una misma quincena (efecto fijo de quincena: temporada, prima, fin de año). Los errores se agrupan por empresa.

  • M1–M3 (log-log): el coeficiente es una elasticidad.
  • M4–M5 (niveles normalizados): ingreso y gasto se dividen por el ingreso medio de cada empresa, para que las empresas grandes no dominen. El coeficiente es una PMG agrupada, comparable con la mediana de la sección 2.2.
panel_fe <- panel_bilateral %>%
  group_by(ID) %>%
  filter(n() >= MIN_Q_PANEL) %>%
  mutate(
    media_Y   = mean(Y_qt),
    y_norm    = Y_qt / media_Y,
    g_norm    = G_qt / media_Y,
    across(all_of(names(COMPONENTES)), ~ .x / media_Y, .names = "{.col}_norm"),
    gcorr_norm = (G_qt - G_fin - G_inv) / media_Y   # salidas corrientes: sin ahorro/deuda ni inversión
  ) %>%
  ungroup() %>%
  mutate(
    log_Y     = log(Y_qt),
    log_G     = log(G_qt),
    log_dias  = log(n_dias),
    log_trans = log(n_trans),
    log_Yop   = if_else(Y_op > 0, log(Y_op), NA_real_)
  )

cat("Panel con EF:", n_distinct(panel_fe$ID), "empresas |", nrow(panel_fe), "quincenas bilaterales\n")
## Panel con EF: 144 empresas | 646 quincenas bilaterales
m1  <- feols(log_G ~ log_Y                  | ID,          panel_fe, cluster = ~ID)
m2  <- feols(log_G ~ log_Y                  | ID + q_idx,  panel_fe, cluster = ~ID)
m3a <- feols(log_G ~ log_Y + log_dias       | ID + q_idx,  panel_fe, cluster = ~ID)
m3b <- feols(log_G ~ log_Y + log_trans      | ID + q_idx,  panel_fe, cluster = ~ID)
m4  <- feols(g_norm ~ y_norm                | ID + q_idx,  panel_fe, cluster = ~ID)
m5  <- feols(g_norm ~ y_norm + log_dias     | ID + q_idx,  panel_fe, cluster = ~ID)
m6  <- feols(log_G ~ log_Yop                | ID + q_idx,  panel_fe, cluster = ~ID)

tab_panel <- bind_rows(
  fila_fx(m1,  "log_Y",   "M1. Elasticidad — EF empresa"),
  fila_fx(m2,  "log_Y",   "M2. Elasticidad — EF empresa + quincena"),
  fila_fx(m3a, "log_Y",   "M3a. M2 + control días con registro"),
  fila_fx(m3b, "log_Y",   "M3b. M2 + control n.º de transacciones"),
  fila_fx(m4,  "y_norm",  "M4. PMG agrupada — EF empresa + quincena"),
  fila_fx(m5,  "y_norm",  "M5. M4 + control días con registro"),
  fila_fx(m6,  "log_Yop", "M6. Elasticidad con ingreso SOLO operativo")
)

kable_fx(tab_panel, "Tabla 12. Modelos de panel con efectos fijos (errores agrupados por empresa)")
Tabla 12. Modelos de panel con efectos fijos (errores agrupados por empresa)
Modelo Coef. EE (cluster) IC 95% p-valor Obs. Empresas
M1. Elasticidad — EF empresa 0,634 0,052 [0,531; 0,737] < 0,001 646 144
M2. Elasticidad — EF empresa + quincena 0,629 0,056 [0,519; 0,739] < 0,001 628 142
M3a. M2 + control días con registro 0,529 0,070 [0,390; 0,667] < 0,001 628 142
M3b. M2 + control n.º de transacciones 0,461 0,081 [0,300; 0,621] < 0,001 628 142
M4. PMG agrupada — EF empresa + quincena 0,491 0,233 [0,030; 0,951] 0,037 628 142
M5. M4 + control días con registro 0,252 0,238 [-0,218; 0,722] 0,291 628 142
M6. Elasticidad con ingreso SOLO operativo 0,646 0,059 [0,530; 0,762] < 0,001 610 141

Con efectos fijos de empresa y de quincena, un aumento de 10% en las entradas de la quincena se asocia con un aumento de 6,3% en las salidas (M2). En niveles, la PMG agrupada es 0,49 (M4): de cada $100 adicionales salen unos $49. Compárese con la mediana empresa por empresa de la sección 2.2 (0,41).

2.4 ¿Comportamiento o uso de la app? Control de intensidad de registro — NUEVO

Si en las quincenas en que la persona usa más la app registra a la vez más entradas y más salidas, β sale positivo aunque el negocio no cambie su comportamiento. Para ponerlo a prueba se controla por la intensidad de registro de la quincena.

  • Control principal: días con al menos un registro.
  • Control más agresivo: número de transacciones. Este control también absorbe actividad real (más ventas implican más transacciones), así que la caída de β con él es una cota superior del confusor.
panel_fe %>%
  summarise(
    `Corr(log días, log ingreso)`        = cor(log_dias, log_Y),
    `Corr(log días, log gasto)`          = cor(log_dias, log_G),
    `Corr(log transacciones, log ingreso)` = cor(log_trans, log_Y),
    `Corr(log transacciones, log gasto)`   = cor(log_trans, log_G)
  ) %>%
  pivot_longer(everything(), names_to = "Correlación (todo el panel)", values_to = "Valor") %>%
  mutate(Valor = fmt(Valor)) %>%
  kable(caption = "Tabla 13. ¿Cuánto se asocia la intensidad de registro con los montos registrados?") %>%
  estilo(full_width = FALSE)
Tabla 13. ¿Cuánto se asocia la intensidad de registro con los montos registrados?
Correlación (todo el panel) Valor
Corr(log días, log ingreso) 0,39
Corr(log días, log gasto) 0,40
Corr(log transacciones, log ingreso) 0,51
Corr(log transacciones, log gasto) 0,49
cambio_max <- max(abs(c(cambio_reg, cambio_niv)), na.rm = TRUE)
m4_impreciso <- !es_sig(tab_panel$ic_lo[5], tab_panel$ic_hi[5]) ||
                (tab_panel$ic_hi[5] - tab_panel$ic_lo[5]) > 3 * (tab_panel$ic_hi[2] - tab_panel$ic_lo[2])

txt_registro <- paste0(
  "Al controlar por días con registro, la elasticidad pasa de ", fmt(e_m2), " a ", fmt(e_m3a),
  " (", fmt_pct(cambio_reg), ") y la PMG agrupada en niveles de ", fmt(pmg_m4), " a ", fmt(pmg_m5),
  " (", fmt_pct(cambio_niv), "). ",
  case_when(
    cambio_max < 0.10 ~ "En ambas métricas el cambio es pequeño: la co-variación entre entradas y salidas **no es solo** intensidad de uso de la app. ",
    cambio_max < 0.40 ~ "Una parte de la co-variación es intensidad de uso de la app, y las cifras deben leerse con cautela. ",
    TRUE ~ "En al menos una de las dos métricas la caída es grande: **una parte importante de lo que se mide es uso de la app**. "
  ),
  if (m4_impreciso) "La PMG en niveles es muy imprecisa (ver sección 2.11), así que su caída no debe sobre-interpretarse. " else "",
  "Como cifra de referencia conviene usar la elasticidad **con** control de días (", fmt(e_m3a),
  "); con el control más agresivo (número de transacciones) queda en ", fmt(e_m3b),
  if (e_m3b < e_m3a) ", que funciona como cota inferior." else "."
)

txt_operativo <- if (is.na(e_m6)) {
  "No fue posible estimar el modelo con ingreso solo operativo."
} else if (abs(e_m6 - e_m2) / abs(e_m2) < 0.25) {
  paste0("Con ingreso solo operativo (sin préstamos ni retiros de ahorro) la elasticidad es ", fmt(e_m6),
         ", similar a la de M2: el resultado no lo producen los desembolsos de crédito.")
} else {
  paste0("Con ingreso solo operativo (sin préstamos ni retiros de ahorro) la elasticidad pasa a ", fmt(e_m6),
         " (vs. ", fmt(e_m2), " en M2): los flujos de financiamiento sí pesan en la estimación.")
}

Al controlar por días con registro, la elasticidad pasa de 0,63 a 0,53 (-16,0%) y la PMG agrupada en niveles de 0,49 a 0,25 (-48,6%). En al menos una de las dos métricas la caída es grande: una parte importante de lo que se mide es uso de la app. La PMG en niveles es muy imprecisa (ver sección 2.11), así que su caída no debe sobre-interpretarse. Como cifra de referencia conviene usar la elasticidad con control de días (0,53); con el control más agresivo (número de transacciones) queda en 0,46, que funciona como cota inferior.

Con ingreso solo operativo (sin préstamos ni retiros de ahorro) la elasticidad es 0,65, similar a la de M2: el resultado no lo producen los desembolsos de crédito.

2.5 ¿Sesga el filtro bilateral? — NUEVO

Quedarse solo con quincenas con entradas y salidas bota justo las quincenas donde se vería suavización: gastar sin que entre plata, o que entre plata sin gastar. La PMG agrupada (M4) se re-estima incluyendo esas quincenas.

panel_q %>%
  mutate(tipo_q = case_when(
    Y_qt > 0 & G_qt > 0 ~ "Bilateral (entradas y salidas)",
    Y_qt > 0            ~ "Solo entradas",
    G_qt > 0            ~ "Solo salidas",
    TRUE                ~ "Sin montos"
  )) %>%
  count(`Tipo de quincena` = tipo_q, name = "Quincenas") %>%
  mutate(`%` = 100 * Quincenas / sum(Quincenas)) %>%
  kable(caption = "Tabla 14. Quincenas con registro según tipo", format.args = FMT_TABLA,
        digits = c(0, 0, 1)) %>%
  estilo(full_width = FALSE)
Tabla 14. Quincenas con registro según tipo
Tipo de quincena Quincenas %
Bilateral (entradas y salidas) 816 53,8
Solo entradas 462 30,4
Solo salidas 240 15,8
panel_todo <- panel_q %>%
  group_by(ID) %>%
  filter(n() >= MIN_Q_PANEL, mean(Y_qt) > 0) %>%
  mutate(media_Y = mean(Y_qt), y_norm = Y_qt / media_Y, g_norm = G_qt / media_Y) %>%
  ungroup()

m4_todo <- feols(g_norm ~ y_norm | ID + q_idx, panel_todo, cluster = ~ID)

ids_ambos <- intersect(unique(panel_fe$ID), unique(panel_todo$ID))
m4_misma  <- feols(g_norm ~ y_norm | ID + q_idx, panel_fe %>% filter(ID %in% ids_ambos), cluster = ~ID)
m4_todo_m <- feols(g_norm ~ y_norm | ID + q_idx, panel_todo %>% filter(ID %in% ids_ambos), cluster = ~ID)

bind_rows(
  fila_fx(m4,        "y_norm", "M4. Solo quincenas bilaterales (v1)"),
  fila_fx(m4_todo,   "y_norm", "M4'. Todas las quincenas con registro"),
  fila_fx(m4_misma,  "y_norm", "M4 — mismas empresas, solo bilaterales"),
  fila_fx(m4_todo_m, "y_norm", "M4' — mismas empresas, todas las quincenas")
) %>%
  kable_fx("Tabla 15. PMG agrupada con y sin el filtro bilateral")
Tabla 15. PMG agrupada con y sin el filtro bilateral
Modelo Coef. EE (cluster) IC 95% p-valor Obs. Empresas
M4. Solo quincenas bilaterales (v1) 0,491 0,233 [0,030; 0,951] 0,037 628 142
M4’. Todas las quincenas con registro 0,355 0,115 [0,128; 0,582] 0,002 1.216 275
M4 — mismas empresas, solo bilaterales 0,491 0,233 [0,030; 0,951] 0,037 628 142
M4’ — mismas empresas, todas las quincenas 0,470 0,121 [0,230; 0,711] < 0,001 881 144
pmg_todo <- coef(m4_todo_m)[["y_norm"]]
pmg_bil  <- coef(m4_misma)[["y_norm"]]

Para las mismas empresas, la PMG agrupada es 0,49 con el filtro bilateral y 0,47 sin él. El filtro bilateral no cambia de forma importante la PMG agrupada.

2.6 ¿Qué se mueve con el ingreso? Descomposición por componente — NUEVO

Como las regresiones en niveles normalizados son lineales y usan la misma muestra y los mismos efectos fijos, la PMG total es exactamente la suma de las PMG de cada componente. Eso permite decir de dónde salen los pesos que responden al ingreso.

vars_comp <- c("Total salidas" = "g_norm",
               setNames(paste0(names(COMPONENTES), "_norm"), COMPONENTES))

mods_comp <- map(vars_comp, ~ feols(as.formula(paste(.x, "~ y_norm | ID + q_idx")),
                                    panel_fe, cluster = ~ID))

tab_comp <- imap_dfr(mods_comp, ~ fila_fx(.x, "y_norm", .y)) %>%
  rename(Componente = Modelo) %>%
  mutate(`Pesos por cada $100` = 100 * coef,
         `% de la PMG total`   = if_else(Componente == "Total salidas", NA_real_,
                                         100 * coef / coef[Componente == "Total salidas"]))

suma_comp <- sum(tab_comp$coef[tab_comp$Componente != "Total salidas"])
cat("Chequeo de aditividad: suma de componentes =", fmt(suma_comp, 4),
    "| total =", fmt(tab_comp$coef[tab_comp$Componente == "Total salidas"], 4), "\n")
## Chequeo de aditividad: suma de componentes = 0,4905 | total = 0,4905
# Participación de cada componente en el monto de salidas (descriptivo)
part_monto <- panel_fe %>%
  summarise(across(all_of(names(COMPONENTES)), sum)) %>%
  pivot_longer(everything(), names_to = "var", values_to = "monto") %>%
  mutate(Componente = unname(COMPONENTES[var]), `% del monto de salidas` = 100 * monto / sum(monto)) %>%
  select(Componente, `% del monto de salidas`)

# Empresas que registran gasto personal alguna vez
emp_pers <- panel_fe %>%
  group_by(ID) %>%
  summarise(q_pers = sum(G_pers > 0), .groups = "drop")

tab_comp %>%
  left_join(part_monto, by = "Componente") %>%
  transmute(
    Componente,
    `PMG`                    = fmt(coef, 3),
    `IC 95%`                 = fmt_ic(ic_lo, ic_hi, 3),
    `p-valor`                = fmt_p(p),
    `Pesos por cada $100`    = fmt(`Pesos por cada $100`, 1),
    `% de la PMG total`      = fmt(`% de la PMG total`, 1),
    `% del monto de salidas` = fmt(`% del monto de salidas`, 1),
    `¿Distinto de cero?`     = if_else(es_sig(ic_lo, ic_hi), "Sí", "No")
  ) %>%
  kable(caption = paste0("Tabla 16. Descomposición de la PMG agrupada por componente del gasto (",
                         n_distinct(panel_fe$ID), " empresas, EF empresa + quincena)")) %>%
  estilo(full_width = TRUE) %>%
  row_spec(1, bold = TRUE)
Tabla 16. Descomposición de la PMG agrupada por componente del gasto (144 empresas, EF empresa + quincena)
Componente PMG IC 95% p-valor Pesos por cada $100 % de la PMG total % del monto de salidas ¿Distinto de cero?
Total salidas 0,491 [0,030; 0,951] 0,037 49,1 — — Sí
Operativo variable 0,213 [0,116; 0,309] < 0,001 21,3 43,4 49,8 Sí
Operativo fijo 0,287 [-0,076; 0,650] 0,120 28,7 58,5 22,9 No
Inversión -0,005 [-0,022; 0,012] 0,544 -0,5 -1,1 1,3 No
Personal / hogar 0,101 [0,013; 0,189] 0,024 10,1 20,6 18,3 Sí
Financiero (ahorro y deuda) -0,109 [-0,363; 0,145] 0,398 -10,9 -22,2 6,9 No
Otros / sin clasificar 0,004 [-0,007; 0,015] 0,464 0,4 0,9 0,8 No
tab_comp %>%
  filter(Componente != "Total salidas") %>%
  mutate(Componente = fct_reorder(Componente, coef)) %>%
  ggplot(aes(x = 100 * coef, y = Componente, xmin = 100 * ic_lo, xmax = 100 * ic_hi)) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray50") +
  geom_pointrange(color = COL_AZUL, linewidth = 0.8) +
  scale_x_continuous(labels = eje_coma) +
  labs(title = "Figura 5. ¿Cuántos pesos de cada componente salen por cada $100 adicionales de ingreso?",
       subtitle = "PMG agrupada por componente con IC 95% (errores agrupados por empresa). Los componentes suman la PMG total.",
       x = "Pesos de salida por cada $100 adicionales de entrada", y = NULL) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"))

# ── Gasto personal, solo empresas que lo registran (≥ 2 quincenas) ──────────
ids_pers <- emp_pers %>% filter(q_pers >= 2) %>% pull(ID)

m_pers_sub <- if (length(ids_pers) >= 5) {
  feols(G_pers_norm ~ y_norm | ID + q_idx, panel_fe %>% filter(ID %in% ids_pers), cluster = ~ID)
} else NULL

m_corr <- feols(gcorr_norm ~ y_norm | ID + q_idx, panel_fe, cluster = ~ID)

bind_rows(
  fila_fx(m_corr,     "y_norm", "Salidas corrientes (sin ahorro, deuda ni inversión) — todas"),
  fila_fx(m_pers_sub, "y_norm", "Personal / hogar — solo empresas que lo registran (≥ 2 quincenas)")
) %>%
  kable_fx("Tabla 17. PMG de las salidas corrientes y del gasto personal")
Tabla 17. PMG de las salidas corrientes y del gasto personal
Modelo Coef. EE (cluster) IC 95% p-valor Obs. Empresas
Salidas corrientes (sin ahorro, deuda ni inversión) — todas 0,605 0,187 [0,236; 0,974] 0,002 628 142
Personal / hogar — solo empresas que lo registran (≥ 2 quincenas) 0,137 0,064 [0,009; 0,265] 0,037 285 54
fila_comp <- function(nombre) tab_comp %>% filter(Componente == nombre)
c_tot  <- fila_comp("Total salidas")$coef
c_var  <- fila_comp("Operativo variable")$coef
c_oper <- sum(tab_comp$coef[tab_comp$Componente %in% c("Operativo variable", "Operativo fijo")])
c_pers <- fila_comp("Personal / hogar")$coef
c_fin  <- fila_comp("Financiero (ahorro y deuda)")$coef
var_sig <- es_sig(fila_comp("Operativo variable")$ic_lo, fila_comp("Operativo variable")$ic_hi)
fin_sig <- es_sig(fila_comp("Financiero (ahorro y deuda)")$ic_lo, fila_comp("Financiero (ahorro y deuda)")$ic_hi)
tot_sig <- es_sig(fila_comp("Total salidas")$ic_lo, fila_comp("Total salidas")$ic_hi)
sh_var  <- if (c_tot > 0) c_var / c_tot else NA_real_
pct_emp_pers <- mean(emp_pers$q_pers >= 1)
f_pers <- fila_fx(m_pers_sub, "y_norm", "pers")

comp_det <- tab_comp %>%
  filter(Componente != "Total salidas") %>%
  mutate(sig = es_sig(ic_lo, ic_hi), etiqueta = paste0(Componente, " (", fmt_pesos(100 * coef), ")"))
lista_sig   <- comp_det %>% filter(sig)  %>% arrange(desc(coef)) %>% pull(etiqueta)
lista_nosig <- comp_det %>% filter(!sig) %>% arrange(desc(abs(coef))) %>% pull(etiqueta)

txt_comp <- paste0(
  "De cada $100 adicionales de ingreso, las salidas suben unos ", fmt_pesos(100 * c_tot),
  if (!tot_sig) " (con un intervalo que incluye el cero)" else "",
  if (fila_comp("Total salidas")$ic_hi - fila_comp("Total salidas")$ic_lo > 0.5)
    ", aunque la PMG total en niveles es imprecisa (ver la sensibilidad a valores extremos en 2.11)" else "",
  ". ",
  if (length(lista_sig) > 0) paste0("Los componentes que se estiman con precisión (IC 95% sin el cero) son: ",
                                    paste(lista_sig, collapse = ", "), ". ")
  else "Ningún componente se estima con precisión suficiente. ",
  if (length(lista_nosig) > 0) paste0("Los demás — ", paste(lista_nosig, collapse = ", "),
                                      " — tienen intervalos que incluyen el cero y no se pueden interpretar uno por uno. ")
  else "",
  if (var_sig && !is.na(sh_var)) paste0(
    "La parte robusta de la respuesta es el **costo operativo variable** (mercancía e insumos): ",
    fmt_pesos(100 * c_var), " de cada $100, el ", fmt_pct(sh_var), " de la PMG total. ",
    "Esa parte refleja la estructura de costos del negocio — una tienda que vende más compra más —, no miopía financiera. ")
  else "",
  if (pct_emp_pers < 0.5) {
    paste0("Solo el ", fmt_pct(pct_emp_pers), " de las empresas del panel registra gasto personal alguna vez. ")
  } else {
    paste0("El ", fmt_pct(pct_emp_pers), " de las empresas del panel registra gasto personal al menos una vez, ",
           "aunque no hay forma de saber si todo el gasto del hogar pasa por la app. ")
  },
  if (!is.na(f_pers$coef)) {
    paste0("La prueba más cercana a la HIP es la del gasto personal entre las empresas que lo registran (Tabla 17): ",
           "PMG de ", fmt(f_pers$coef, 3), " ", fmt_ic(f_pers$ic_lo, f_pers$ic_hi, 3), ", es decir, unos ",
           fmt_pesos(100 * f_pers$coef, 1), " de gasto del hogar por cada $100 adicionales que entran al negocio. ",
           if (!es_sig(f_pers$ic_lo, f_pers$ic_hi)) "No se distingue de cero: es compatible con que el hogar suavice su gasto frente a la caja del negocio. "
           else if (f_pers$coef > 0) "Es distinta de cero: el gasto del hogar sí se mueve con la caja del negocio, aunque en una proporción pequeña. "
           else "Es negativa y distinta de cero, lo que no tiene una lectura económica directa; conviene revisar esos registros. ")
  } else {
    "No hay suficientes empresas que registren gasto personal para estimar su PMG por separado. "
  },
  if (fin_sig && c_fin > 0) {
    "Además, el componente financiero es positivo y distinto de cero: en las buenas quincenas se ahorra o se paga más deuda, lo cual es **suavización**, pero en v1 se contaba como \"gasto\"."
  } else ""
)

Lectura. De cada $100 adicionales de ingreso, las salidas suben unos $49, aunque la PMG total en niveles es imprecisa (ver la sensibilidad a valores extremos en 2.11). Los componentes que se estiman con precisión (IC 95% sin el cero) son: Operativo variable ($21), Personal / hogar ($10). Los demás — Operativo fijo ($29), Financiero (ahorro y deuda) (−$11), Inversión (−$1), Otros / sin clasificar ($0) — tienen intervalos que incluyen el cero y no se pueden interpretar uno por uno. La parte robusta de la respuesta es el costo operativo variable (mercancía e insumos): $21 de cada $100, el 43,4% de la PMG total. Esa parte refleja la estructura de costos del negocio — una tienda que vende más compra más —, no miopía financiera. El 56,2% de las empresas del panel registra gasto personal al menos una vez, aunque no hay forma de saber si todo el gasto del hogar pasa por la app. La prueba más cercana a la HIP es la del gasto personal entre las empresas que lo registran (Tabla 17): PMG de 0,137 [0,009; 0,265], es decir, unos $13,7 de gasto del hogar por cada $100 adicionales que entran al negocio. Es distinta de cero: el gasto del hogar sí se mueve con la caja del negocio, aunque en una proporción pequeña.

2.7 Asimetría: ¿responde igual el gasto cuando el ingreso sube que cuando cae? — NUEVO

Con restricciones de liquidez (Zeldes, 1989; Shea, 1995), el gasto debería caer cuando el ingreso cae por debajo de lo habitual, porque no hay colchón, y subir menos cuando el ingreso sube, porque se ahorra el excedente. Se separa la desviación del log-ingreso respecto a la media de la empresa en su parte positiva y su parte negativa.

panel_asim <- panel_fe %>%
  group_by(ID) %>%
  mutate(dev = log_Y - mean(log_Y), dev_pos = pmax(dev, 0), dev_neg = pmin(dev, 0)) %>%
  ungroup()

m_asim  <- feols(log_G ~ dev_pos + dev_neg | ID + q_idx, panel_asim, cluster = ~ID)
# Reparametrización: el coeficiente de dev_neg aquí es (β− − β+) → prueba directa de asimetría
m_asim2 <- feols(log_G ~ dev + dev_neg     | ID + q_idx, panel_asim, cluster = ~ID)

# v2.1: lo mismo con control de días con registro
m_asim_c  <- feols(log_G ~ dev_pos + dev_neg + log_dias | ID + q_idx, panel_asim, cluster = ~ID)
m_asim2_c <- feols(log_G ~ dev + dev_neg + log_dias     | ID + q_idx, panel_asim, cluster = ~ID)
# v2.1: ¿se registra menos cuando el ingreso cae por debajo de lo habitual? (sub-registro)
m_dias    <- feols(log_dias ~ dev_pos + dev_neg         | ID + q_idx, panel_asim, cluster = ~ID)

bind_rows(
  fila_fx(m_asim,    "dev_pos", "Sin control · β+ (ingreso por ENCIMA de lo habitual)"),
  fila_fx(m_asim,    "dev_neg", "Sin control · β− (ingreso por DEBAJO de lo habitual)"),
  fila_fx(m_asim2,   "dev_neg", "Sin control · diferencia β− − β+"),
  fila_fx(m_asim_c,  "dev_pos", "Con control de días · β+"),
  fila_fx(m_asim_c,  "dev_neg", "Con control de días · β−"),
  fila_fx(m_asim2_c, "dev_neg", "Con control de días · diferencia β− − β+"),
  fila_fx(m_dias,    "dev_pos", "Días con registro como resultado · ingreso por encima"),
  fila_fx(m_dias,    "dev_neg", "Días con registro como resultado · ingreso por debajo")
) %>%
  kable_fx("Tabla 18. Respuesta asimétrica del gasto (elasticidades, EF empresa + quincena)") %>%
  pack_rows("Sin control de registro", 1, 3) %>%
  pack_rows("Con control de días con registro (v2.1)", 4, 6) %>%
  pack_rows("Prueba de sub-registro (v2.1)", 7, 8)
Tabla 18. Respuesta asimétrica del gasto (elasticidades, EF empresa + quincena)
Modelo Coef. EE (cluster) IC 95% p-valor Obs. Empresas
Sin control de registro
Sin control · β+ (ingreso por ENCIMA de lo habitual) 0,879 0,094 [0,693; 1,064] < 0,001 628 142
Sin control · β− (ingreso por DEBAJO de lo habitual) 0,382 0,112 [0,160; 0,603] < 0,001 628 142
Sin control · diferencia β− − β+ -0,497 0,175 [-0,842; -0,152] 0,005 628 142
Con control de días con registro (v2.1)
Con control de días · β+ 0,815 0,103 [0,612; 1,018] < 0,001 628 142
Con control de días · β− 0,222 0,124 [-0,024; 0,467] 0,076 628 142
Con control de días · diferencia β− − β+ -0,594 0,185 [-0,959; -0,229] 0,002 628 142
Prueba de sub-registro (v2.1)
Días con registro como resultado · ingreso por encima 0,145 0,061 [0,025; 0,264] 0,018 628 142
Días con registro como resultado · ingreso por debajo 0,365 0,042 [0,283; 0,448] < 0,001 628 142
b_pos  <- coef(m_asim)[["dev_pos"]]
b_neg  <- coef(m_asim)[["dev_neg"]]
p_asim <- coeftable(m_asim2)["dev_neg", 4]

b_pos_c  <- coef(m_asim_c)[["dev_pos"]]
b_neg_c  <- coef(m_asim_c)[["dev_neg"]]
p_asim_c <- coeftable(m_asim2_c)["dev_neg", 4]
d_neg    <- coeftable(m_dias)["dev_neg", 1]
p_dneg   <- coeftable(m_dias)["dev_neg", 4]

asim_sobrevive <- p_asim < 0.10 && p_asim_c < 0.10 && sign(b_pos - b_neg) == sign(b_pos_c - b_neg_c)
subregistro    <- p_dneg < 0.10 && d_neg > 0

txt_asim_c <- paste0(
  if (p_asim >= 0.10) {
    paste0("Con control de días con registro: β+ = ", fmt(b_pos_c), ", β− = ", fmt(b_neg_c), " (", txt_p(p_asim_c), " para la diferencia). ")
  } else if (asim_sobrevive) {
    paste0("**La asimetría se mantiene** al controlar por días con registro (β+ = ", fmt(b_pos_c), ", β− = ", fmt(b_neg_c),
           "; ", txt_p(p_asim_c), " para la diferencia). ")
  } else {
    paste0("**Con control de días con registro, la asimetría deja de ser significativa** (β+ = ", fmt(b_pos_c), ", β− = ",
           fmt(b_neg_c), "; ", txt_p(p_asim_c), " para la diferencia): no se puede descartar que sea un efecto de registro. ")
  },
  if (subregistro) {
    paste0("Además, cuando el ingreso cae por debajo de lo habitual también caen los días con registro (",
           txt_p(p_dneg), "): es consistente con **sub-registro** en las malas quincenas — ingresos o gastos que no se anotan —, ",
           "un mecanismo que aplana β− sin que el negocio cambie su comportamiento.")
  } else {
    "Los días con registro no caen de forma significativa cuando el ingreso está por debajo de lo habitual, lo que debilita la explicación de sub-registro."
  }
)

txt_asim <- case_when(
  p_asim < 0.10 & b_neg > b_pos ~ paste0(
    "El gasto responde más cuando el ingreso cae por debajo de lo habitual (β− = ", fmt(b_neg),
    ") que cuando sube (β+ = ", fmt(b_pos), "; ", txt_p(p_asim), " para la diferencia",
    "). Es el patrón esperado con **restricciones de liquidez**: en las malas quincenas no hay colchón para sostener el gasto. ",
    "Esto conecta directamente con la pregunta de acceso a crédito."),
  p_asim < 0.10 & b_pos > b_neg ~ paste0(
    "El gasto responde más a las subidas del ingreso (β+ = ", fmt(b_pos), ") que a las caídas (β− = ",
    fmt(b_neg), "; ", txt_p(p_asim), " para la diferencia): en las malas quincenas el gasto se sostiene, ",
    "lo que sugiere algún mecanismo de amortiguación (ahorro previo, fiado o crédito informal)."),
  TRUE ~ paste0(
    "No hay evidencia estadística de asimetría (β+ = ", fmt(b_pos), ", β− = ", fmt(b_neg),
    "; ", txt_p(p_asim), " para la diferencia). Con este tamaño de muestra la prueba tiene poco poder: ",
    "la ausencia de evidencia no es evidencia de simetría.")
)

El gasto responde más a las subidas del ingreso (β+ = 0,88) que a las caídas (β− = 0,38; p = 0,005 para la diferencia): en las malas quincenas el gasto se sostiene, lo que sugiere algún mecanismo de amortiguación (ahorro previo, fiado o crédito informal).

La asimetría se mantiene al controlar por días con registro (β+ = 0,82, β− = 0,22; p = 0,002 para la diferencia). Además, cuando el ingreso cae por debajo de lo habitual también caen los días con registro (p < 0,001): es consistente con sub-registro en las malas quincenas — ingresos o gastos que no se anotan —, un mecanismo que aplana β− sin que el negocio cambie su comportamiento.

2.8 Rezagos y adelantos: ¿se gasta antes o después de que entre la plata? — NUEVO

Si las salidas de hoy predicen las entradas de la quincena siguiente (por ejemplo, se compra inventario y luego se vende), el β contemporáneo está mezclando tiempos. Este modelo solo usa empresas con quincenas bilaterales consecutivas, así que pierde muchas observaciones.

# Adelanto y rezago construidos a mano: solo cuentan quincenas CALENDARIO contiguas
panel_rez <- panel_fe %>%
  arrange(ID, q_idx) %>%
  group_by(ID) %>%
  mutate(
    log_Y_sig = if_else(lead(q_idx) == q_idx + 1, lead(log_Y), NA_real_),   # quincena siguiente
    log_Y_ant = if_else(lag(q_idx)  == q_idx - 1, lag(log_Y),  NA_real_)    # quincena anterior
  ) %>%
  ungroup() %>%
  filter(!is.na(log_Y_sig), !is.na(log_Y_ant))

m_rez <- if (nrow(panel_rez) >= 30 && n_distinct(panel_rez$ID) >= 5) {
  tryCatch(feols(log_G ~ log_Y_sig + log_Y + log_Y_ant | ID, panel_rez, cluster = ~ID),
           error = function(e) { cat("No se pudo estimar:", conditionMessage(e), "\n"); NULL })
} else NULL

if (!is.null(m_rez)) {
  tab_rez <- bind_rows(
    fila_fx(m_rez, "log_Y_sig", "Ingreso de la quincena SIGUIENTE (adelanto)"),
    fila_fx(m_rez, "log_Y",     "Ingreso de la MISMA quincena"),
    fila_fx(m_rez, "log_Y_ant", "Ingreso de la quincena ANTERIOR (rezago)")
  )

  ct_rez <- coeftable(m_rez)
  p_lead <- ct_rez["log_Y_sig", 4]; b_lead <- ct_rez["log_Y_sig", 1]
  p_lag  <- ct_rez["log_Y_ant", 4]; b_lag  <- ct_rez["log_Y_ant", 1]

  txt_rez <- paste(
    if (nobs(m_rez) < 100) paste0("**Advertencia:** solo ", nobs(m_rez),
                                  " observaciones con tres quincenas bilaterales seguidas; leer con mucha cautela.") else "",
    if (p_lead < 0.10 & b_lead > 0) {
      "Las salidas de hoy se asocian con las entradas de la quincena siguiente: es consistente con comprar inventario antes de vender, y quiere decir que parte del β contemporáneo es un efecto de calendario y no de miopía."
    } else {
      "No hay asociación significativa entre las salidas de hoy y las entradas de la quincena siguiente."
    },
    if (p_lag < 0.10 & b_lag > 0) {
      "Las salidas también responden al ingreso de la quincena anterior: el gasto se ajusta con rezago a la caja disponible."
    } else {
      "Las salidas no responden de forma significativa al ingreso de la quincena anterior."
    }
  )
} else {
  txt_rez <- paste0("Solo hay ", nrow(panel_rez), " observaciones con tres quincenas bilaterales seguidas: ",
                    "no alcanzan para estimar este modelo.")
}

# La tabla va al final para que knitr la imprima
if (!is.null(m_rez)) {
  kable_fx(tab_rez, "Tabla 19. Elasticidad del gasto a ingresos pasados, presentes y futuros (EF empresa)")
}
Tabla 19. Elasticidad del gasto a ingresos pasados, presentes y futuros (EF empresa)
Modelo Coef. EE (cluster) IC 95% p-valor Obs. Empresas
Ingreso de la quincena SIGUIENTE (adelanto) 0,009 0,046 [-0,084; 0,101] 0,853 195 44
Ingreso de la MISMA quincena 0,806 0,112 [0,580; 1,033] < 0,001 195 44
Ingreso de la quincena ANTERIOR (rezago) 0,056 0,065 [-0,076; 0,187] 0,397 195 44

No hay asociación significativa entre las salidas de hoy y las entradas de la quincena siguiente. Las salidas no responden de forma significativa al ingreso de la quincena anterior.

2.9 Heterogeneidad: ¿quién suaviza más?

Interacciones en el panel — NUEVO (corregido en v2.1)

En vez de regresar los β ruidosos de cada empresa sobre sus características (v1), la sensibilidad se estima en el panel permitiendo que cambie según el perfil. Las características se centran en la empresa promedio de cada modelo, así que el coeficiente principal es la sensibilidad de una empresa “típica” y cada interacción es la diferencia asociada a esa característica.

  • Total de salidas: modelo log-log (elasticidad). En niveles era numéricamente inestable.
  • Gasto personal: niveles normalizados (tiene muchos ceros, no admite logaritmos), en la muestra completa y solo con empresas que registran gasto personal.

La hipótesis con teoría detrás es la de fungibilidad: quien no separa las finanzas del negocio y las del hogar debería llevar al hogar una parte mayor de cada peso adicional que entra.

# Covariables por empresa, centradas en la media de las empresas de cada modelo
centrar_emp <- function(ids) {
  indicadores_emp %>%
    filter(ID %in% ids) %>%
    select(ID, separa_fin, es_mujer, es_digital, sector_g) %>%
    filter(if_all(everything(), ~ !is.na(.x))) %>%
    mutate(serv  = as.integer(sector_g == "Servicios"),
           otros = as.integer(sector_g == "Otros sectores"),
           across(c(separa_fin, es_mujer, es_digital, serv, otros), ~ .x - mean(.x), .names = "{.col}_c"))
}

COVS_C <- c("separa_fin_c", "es_mujer_c", "es_digital_c", "serv_c", "otros_c")
f_inter <- function(y, x) as.formula(paste0(y, " ~ ", x, " + ", paste0(x, ":", COVS_C, collapse = " + "),
                                            " | ID + q_idx"))

emp_todas <- centrar_emp(unique(panel_fe$ID))
emp_reg   <- centrar_emp(intersect(unique(panel_fe$ID), ids_pers))

panel_het     <- panel_fe %>% inner_join(emp_todas, by = "ID")
panel_het_reg <- panel_fe %>% inner_join(emp_reg,   by = "ID")

m_het          <- feols(f_inter("log_G", "log_Y"),        panel_het,     cluster = ~ID)
m_het_pers     <- feols(f_inter("G_pers_norm", "y_norm"), panel_het,     cluster = ~ID)
m_het_pers_reg <- if (n_distinct(panel_het_reg$ID) >= 10) {
  feols(f_inter("G_pers_norm", "y_norm"), panel_het_reg, cluster = ~ID)
} else NULL

etiqueta_het <- function(t) case_when(
  t == "log_Y"                 ~ "Elasticidad de la empresa promedio",
  t == "y_norm"                ~ "PMG de la empresa promedio",
  str_detect(t, "separa_fin")  ~ "× Separa finanzas",
  str_detect(t, "es_mujer")    ~ "× Propietaria mujer",
  str_detect(t, "es_digital")  ~ "× Perfil digital",
  str_detect(t, "serv_c")      ~ "× Servicios (vs. Comercio)",
  str_detect(t, "otros_c")     ~ "× Otros sectores (vs. Comercio)",
  TRUE ~ t
)

MODELOS_HET <- c("Total salidas (elasticidad)", "Gasto personal — todas las empresas",
                 "Gasto personal — solo las que lo registran")

tab_het <- bind_rows(
  coefs_fx(m_het, MODELOS_HET[1]),
  coefs_fx(m_het_pers, MODELOS_HET[2]),
  if (!is.null(m_het_pers_reg)) coefs_fx(m_het_pers_reg, MODELOS_HET[3])
) %>%
  mutate(Término = etiqueta_het(termino), Modelo = factor(Modelo, levels = MODELOS_HET))

n_emp_mod <- c(n_distinct(panel_het$ID), n_distinct(panel_het$ID), n_distinct(panel_het_reg$ID))

tab_het %>%
  mutate(Modelo = paste0(Modelo, " (", n_emp_mod[as.integer(Modelo)], " empresas)")) %>%
  transmute(Modelo, Término, `Coef.` = fmt(coef, 3), `IC 95%` = fmt_ic(ic_lo, ic_hi, 3),
            `p-valor` = fmt_p(p)) %>%
  kable(caption = "Tabla 20. Sensibilidad del gasto según perfil de la empresa (covariables centradas, EF empresa + quincena)") %>%
  estilo(full_width = TRUE) %>%
  collapse_rows(columns = 1, valign = "top")
Tabla 20. Sensibilidad del gasto según perfil de la empresa (covariables centradas, EF empresa + quincena)
Modelo Término Coef. IC 95% p-valor
Total salidas (elasticidad) (139 empresas) Elasticidad de la empresa promedio 0,595 [0,461; 0,729] < 0,001
× Separa finanzas 0,020 [-0,234; 0,275] 0,875
× Propietaria mujer 0,051 [-0,271; 0,373] 0,754
× Perfil digital 0,107 [-0,102; 0,316] 0,313
× Servicios (vs. Comercio) 0,239 [-0,069; 0,547] 0,127
× Otros sectores (vs. Comercio) 0,212 [-0,186; 0,610] 0,294
Gasto personal — todas las empresas (139 empresas) PMG de la empresa promedio 0,108 [0,049; 0,167] < 0,001
× Separa finanzas -0,212 [-0,364; -0,060] 0,007
× Propietaria mujer 0,001 [-0,153; 0,155] 0,990
× Perfil digital -0,020 [-0,140; 0,101] 0,750
× Servicios (vs. Comercio) 0,074 [-0,075; 0,223] 0,327
× Otros sectores (vs. Comercio) -0,057 [-0,248; 0,134] 0,554
Gasto personal — solo las que lo registran (52 empresas) PMG de la empresa promedio 0,127 [0,059; 0,196] < 0,001
× Separa finanzas -0,100 [-0,230; 0,031] 0,131
× Propietaria mujer 0,129 [-0,075; 0,332] 0,209
× Perfil digital 0,076 [-0,114; 0,266] 0,425
× Servicios (vs. Comercio) 0,286 [0,121; 0,450] < 0,001
× Otros sectores (vs. Comercio) 0,001 [-0,311; 0,313] 0,994
tab_het %>%
  filter(str_starts(Término, "×")) %>%
  ggplot(aes(x = coef, y = Término, xmin = ic_lo, xmax = ic_hi)) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray50") +
  geom_pointrange(aes(color = es_sig(ic_lo, ic_hi)), linewidth = 0.8, show.legend = FALSE) +
  scale_color_manual(values = c(`TRUE` = COL_NEG, `FALSE` = COL_AZUL)) +
  scale_x_continuous(labels = eje_coma) +
  facet_wrap(~ Modelo, ncol = 1, scales = "free_x") +
  labs(title = "Figura 6. Diferencias en la sensibilidad del gasto según perfil de la empresa",
       subtitle = "Coeficiente > 0 = el gasto responde más (menos suavización) en ese grupo. Barras = IC 95%; en rojo, IC sin el cero.",
       x = NULL, y = NULL) +
  theme_minimal(base_size = 11) +
  theme(plot.title = element_text(face = "bold"), strip.text = element_text(face = "bold"))

Prueba de fungibilidad — NUEVO v2.1

part_pers <- panel_fe %>%
  group_by(ID) %>%
  summarise(q_pers = sum(G_pers > 0), sh_pers = sum(G_pers) / sum(G_qt), .groups = "drop")

desc_fung <- emp_todas %>%
  left_join(indicadores_emp %>% select(ID, separa_orig = separa_fin), by = "ID") %>%
  left_join(part_pers, by = "ID") %>%
  group_by(`Separa finanzas` = if_else(separa_orig == 1, "Sí", "No")) %>%
  summarise(
    Empresas                                  = n(),
    `% registra gasto personal alguna vez`    = 100 * mean(q_pers >= 1),
    `% con ≥ 2 quincenas con gasto personal`  = 100 * mean(q_pers >= 2),
    `Gasto personal / salidas (mediana, %)`   = 100 * median(sh_pers),
    .groups = "drop"
  )

desc_fung %>%
  kable(caption = "Tabla 20b. ¿Registran gasto personal quienes separan finanzas?",
        format.args = FMT_TABLA, digits = c(0, 0, 1, 1, 1)) %>%
  estilo(full_width = FALSE)
Tabla 20b. ¿Registran gasto personal quienes separan finanzas?
Separa finanzas Empresas % registra gasto personal alguna vez % con ≥ 2 quincenas con gasto personal Gasto personal / salidas (mediana, %)
No 63 69,8 46,0 11,9
Sí 76 43,4 30,3 0,0
pct_reg_si <- desc_fung$`% registra gasto personal alguna vez`[desc_fung$`Separa finanzas` == "Sí"]
pct_reg_no <- desc_fung$`% registra gasto personal alguna vez`[desc_fung$`Separa finanzas` == "No"]
f_fung_todas <- fila_fx(m_het_pers,     "y_norm:separa_fin_c", "todas")
f_fung_reg   <- fila_fx(m_het_pers_reg, "y_norm:separa_fin_c", "registran")
n_inter <- sum(str_starts(tab_het$Término, "×"))

txt_fung <- paste0(
  "Entre las empresas que separan finanzas, el ", fmt(pct_reg_si, 1), "% registra gasto personal alguna vez, frente al ",
  fmt(pct_reg_no, 1), "% de las que no separan. ",
  if (length(pct_reg_si) == 1 && length(pct_reg_no) == 1 && abs(pct_reg_si - pct_reg_no) >= 15)
    "Esa diferencia en el registro, por sí sola, podría producir una PMG del gasto personal más baja entre quienes separan. " else "",
  "En la muestra completa, separar finanzas se asocia con una diferencia de ", fmt(f_fung_todas$coef, 3), " ",
  fmt_ic(f_fung_todas$ic_lo, f_fung_todas$ic_hi, 3), " en la PMG del gasto personal (", txt_p(f_fung_todas$p), "). ",
  if (is.na(f_fung_reg$coef)) {
    "No hay suficientes empresas que registren gasto personal para repetir la prueba solo con ellas."
  } else {
    paste0(
      "Solo entre las ", n_distinct(panel_het_reg$ID), " empresas que registran gasto personal en al menos 2 quincenas, la diferencia es ",
      fmt(f_fung_reg$coef, 3), " ", fmt_ic(f_fung_reg$ic_lo, f_fung_reg$ic_hi, 3), " (", txt_p(f_fung_reg$p), "). ",
      if (f_fung_reg$coef < 0 && f_fung_reg$p < 0.05) {
        paste0("**La diferencia se mantiene** entre quienes sí registran gasto personal: es evidencia a favor de la hipótesis de ",
               "fungibilidad — quien no separa las cuentas lleva al hogar parte de cada peso adicional que entra al negocio. ",
               "Como la Tabla 20 estima ", n_inter, " diferencias a la vez, conviene confirmarla con más datos antes de destacarla.")
      } else if (f_fung_reg$coef < 0 && f_fung_reg$p < 0.10) {
        "La diferencia va en la misma dirección pero es débil (p entre 0,05 y 0,10): sugerente, no concluyente."
      } else if (!is.na(f_fung_todas$p) && f_fung_todas$p < 0.05 && f_fung_todas$coef < 0) {
        paste0("La diferencia **no se sostiene** entre quienes sí registran gasto personal: el resultado de la muestra completa ",
               "probablemente reflejaba que quienes separan finanzas no anotan su gasto del hogar en el diario del negocio.")
      } else {
        "No hay evidencia de fungibilidad ni en la muestra completa ni entre quienes registran gasto personal."
      }
    )
  }
)

Entre las empresas que separan finanzas, el 43,4% registra gasto personal alguna vez, frente al 69,8% de las que no separan. Esa diferencia en el registro, por sí sola, podría producir una PMG del gasto personal más baja entre quienes separan. En la muestra completa, separar finanzas se asocia con una diferencia de -0,212 [-0,364; -0,060] en la PMG del gasto personal (p = 0,007). Solo entre las 52 empresas que registran gasto personal en al menos 2 quincenas, la diferencia es -0,100 [-0,230; 0,031] (p = 0,131). La diferencia no se sostiene entre quienes sí registran gasto personal: el resultado de la muestra completa probablemente reflejaba que quienes separan finanzas no anotan su gasto del hogar en el diario del negocio.

Regresión en dos etapas (v1, corregida)

Se conserva como referencia, ahora con errores HC3 de verdad y sectores agrupados. Con una empresa por observación y pocas empresas por categoría, su poder es bajo.

reg_data <- muestra_B %>%
  filter(!is.na(beta), !is.na(sector_g), !is.na(es_mujer),
         !is.na(es_digital), !is.na(separa_fin)) %>%
  mutate(beta_win = pmin(pmax(beta, -2), 3),
         sector_g = relevel(factor(sector_g), ref = "Comercio"))

modelo <- lm(beta_win ~ sector_g + es_mujer + es_digital + separa_fin + n_quincenas_bilat,
             data = reg_data)

ct_hc3 <- coeftest(modelo, vcov. = vcovHC(modelo, type = "HC3"))
tc     <- qt(0.975, df.residual(modelo))

tibble(term = rownames(ct_hc3), est = ct_hc3[, 1], se = ct_hc3[, 2], p = ct_hc3[, 4]) %>%
  mutate(
    Variable = recode(term,
      "(Intercept)"              = "Constante",
      "sector_gServicios"        = "Servicios (ref. Comercio)",
      "sector_gOtros sectores"   = "Otros sectores (ref. Comercio)",
      "es_mujer"                 = "Propietaria mujer",
      "es_digital"               = "Perfil digital",
      "separa_fin"               = "Separa finanzas",
      "n_quincenas_bilat"        = "N.º quincenas observadas (control)"),
    `Coef.`      = fmt(est, 3),
    `EE (HC3)`   = fmt(se, 3),
    `IC 95%`     = fmt_ic(est - tc * se, est + tc * se, 3),
    `p-valor`    = fmt_p(p)
  ) %>%
  select(Variable, `Coef.`, `EE (HC3)`, `IC 95%`, `p-valor`) %>%
  kable(caption = paste0("Tabla 21. Regresión en dos etapas: predictores de β (N = ",
                         nrow(reg_data), ", Submuestra B, errores HC3)")) %>%
  estilo(full_width = TRUE)
Tabla 21. Regresión en dos etapas: predictores de β (N = 49, Submuestra B, errores HC3)
Variable Coef. EE (HC3) IC 95% p-valor
Constante 0,327 0,533 [-0,750; 1,403] 0,544
Otros sectores (ref. Comercio) -0,029 0,228 [-0,490; 0,432] 0,899
Servicios (ref. Comercio) 0,278 0,291 [-0,309; 0,865] 0,344
Propietaria mujer -0,027 0,417 [-0,869; 0,815] 0,950
Perfil digital 0,202 0,216 [-0,234; 0,638] 0,356
Separa finanzas 0,141 0,230 [-0,323; 0,605] 0,543
N.º quincenas observadas (control) -0,015 0,022 [-0,060; 0,030] 0,498

2.10 Robustez entre submuestras

En lugar del umbral |Δ| < 0,10 de v1, se comparan tres estimadores con sus intervalos de confianza en cada submuestra: la mediana de β por empresa (IC bootstrap), la PMG agrupada del panel (M4) y la elasticidad del panel (M2). También se reporta B sin las empresas de Agro, como sensibilidad.

panel_sub <- function(ids, nombre) {
  d <- panel_fe %>% filter(ID %in% ids)
  m_e <- tryCatch(feols(log_G ~ log_Y | ID + q_idx, d, cluster = ~ID), error = function(e) NULL)
  m_p <- tryCatch(feols(g_norm ~ y_norm | ID + q_idx, d, cluster = ~ID), error = function(e) NULL)
  bind_rows(
    fila_fx(m_p, "y_norm", nombre) %>% mutate(Estimador = "PMG agrupada (panel M4)"),
    fila_fx(m_e, "log_Y",  nombre) %>% mutate(Estimador = "Elasticidad (panel M2)")
  )
}

ids_B_sin_agro <- muestra_B %>% filter(is.na(Sector) | Sector != "Agro") %>% pull(ID)

set.seed(SEED)
med_B_sin_agro <- tabla_mediana(muestra_B %>% filter(ID %in% ids_B_sin_agro), "B sin Agro")

rob <- bind_rows(
  imap_dfr(SUBMUESTRAS, ~ panel_sub(.x$ID, .y)),
  panel_sub(ids_B_sin_agro, "B sin Agro"),
  bind_rows(res_med, med_B_sin_agro) %>%
    transmute(Modelo = Submuestra, coef = med, ic_lo = lo, ic_hi = hi, empresas = N,
              Estimador = "Mediana de β por empresa (bootstrap)")
) %>%
  rename(Submuestra = Modelo) %>%
  mutate(Submuestra = factor(Submuestra, levels = rev(c(names(SUBMUESTRAS), "B sin Agro"))),
         Estimador  = factor(Estimador, levels = c("Mediana de β por empresa (bootstrap)",
                                                   "PMG agrupada (panel M4)",
                                                   "Elasticidad (panel M2)")))

rob %>%
  arrange(Estimador, desc(Submuestra)) %>%
  transmute(Estimador, Submuestra, Empresas = fmt(empresas, 0),
            Estimación = fmt(coef, 3), `IC 95%` = fmt_ic(ic_lo, ic_hi, 3)) %>%
  kable(caption = "Tabla 22. Robustez: tres estimadores en cada submuestra") %>%
  estilo(full_width = TRUE) %>%
  collapse_rows(columns = 1, valign = "top")
Tabla 22. Robustez: tres estimadores en cada submuestra
Estimador Submuestra Empresas Estimación IC 95%
Mediana de β por empresa (bootstrap) A — mínimo 67 0,429 [0,302; 0,560]
B — intermedio 51 0,409 [0,302; 0,530]
C — estricto 17 0,361 [0,154; 0,565]
D — confiabilidad 11 0,502 [0,191; 0,794]
B sin Agro 49 0,409 [0,275; 0,530]
PMG agrupada (panel M4) A — mínimo 98 0,563 [0,114; 1,011]
B — intermedio 51 0,335 [-0,046; 0,717]
C — estricto 17 0,538 [0,263; 0,814]
D — confiabilidad 11 0,762 [0,357; 1,167]
B sin Agro 49 0,331 [-0,134; 0,796]
Elasticidad (panel M2) A — mínimo 98 0,642 [0,531; 0,754]
B — intermedio 51 0,641 [0,499; 0,784]
C — estricto 17 0,653 [0,446; 0,861]
D — confiabilidad 11 0,654 [0,296; 1,012]
B sin Agro 49 0,634 [0,476; 0,792]
rob %>%
  ggplot(aes(x = coef, y = Submuestra, xmin = ic_lo, xmax = ic_hi)) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray50") +
  geom_vline(xintercept = 1, linetype = "dashed", color = COL_NEG) +
  geom_pointrange(aes(color = Submuestra == "B — intermedio"), linewidth = 0.8, show.legend = FALSE) +
  scale_color_manual(values = c(`TRUE` = COL_AZUL, `FALSE` = COL_GRIS)) +
  scale_x_continuous(labels = eje_coma) +
  facet_wrap(~ Estimador, ncol = 1, scales = "free_x") +
  labs(title = "Figura 7. Robustez de la sensibilidad del gasto al ingreso entre submuestras",
       subtitle = "Puntos = estimación; barras = IC 95%. Líneas punteadas en 0 y 1.",
       x = NULL, y = NULL) +
  theme_minimal(base_size = 11) +
  theme(plot.title = element_text(face = "bold"), strip.text = element_text(face = "bold"))

rD <- res_med %>% filter(Submuestra == "D — confiabilidad")
ancho_D <- rD$hi - rD$lo
ancho_B <- rb$hi - rb$lo
# D se considera imprecisa si su IC es más del doble de ancho que el de B
D_impreciso <- !is.na(ancho_D) && ancho_D > 2 * ancho_B
txt_D <- if (is.na(ancho_D)) {
  "La Submuestra D no tiene suficientes empresas para un IC bootstrap."
} else if (rb$med >= rD$lo & rb$med <= rD$hi & D_impreciso) {
  paste0("La mediana de D (", fmt(rD$med), ") es compatible con la de B, pero su IC (", fmt_ic(rD$lo, rD$hi),
         ") es ", fmt(ancho_D / ancho_B, 1), " veces más ancho que el de B: **D no permite confirmar ni descartar** ",
         "que los cargues retroactivos sesguen el resultado.")
} else if (rb$med >= rD$lo & rb$med <= rD$hi) {
  paste0("La mediana de D (", fmt(rD$med), ") es compatible con la de B y su IC es razonablemente estrecho (",
         fmt_ic(rD$lo, rD$hi), "): no hay señal de que los cargues retroactivos sesguen el resultado.")
} else {
  paste0("La mediana de B queda fuera del IC de D (", fmt_ic(rD$lo, rD$hi),
         "): hay señal de que los cargues retroactivos (Capa 3) afectan la estimación.")
}

La mediana de D (0,50) es compatible con la de B, pero su IC ([0,19; 0,79]) es 2,6 veces más ancho que el de B: D no permite confirmar ni descartar que los cargues retroactivos sesguen el resultado. Con solo 11 empresas, además, los errores agrupados del panel en D son poco confiables.

2.11 Sensibilidades — NUEVO v2.1

Entradas registradas con categoría de gasto

Se re-estiman los tres modelos de panel principales con cuatro tratamientos de las entradas de la Tabla 2d.

estimar_variante <- function(pq, nombre) {
  pf <- pq %>%
    filter(Y_qt > 0.5, G_qt > 0.5) %>%
    group_by(ID) %>%
    filter(n() >= MIN_Q_PANEL) %>%
    mutate(y_norm = Y_qt / mean(Y_qt), g_norm = G_qt / mean(Y_qt)) %>%
    ungroup() %>%
    mutate(log_Y = log(Y_qt), log_G = log(G_qt), log_dias = log(n_dias))
  m_e  <- tryCatch(feols(log_G ~ log_Y | ID + q_idx,            pf, cluster = ~ID), error = function(e) NULL)
  m_ec <- tryCatch(feols(log_G ~ log_Y + log_dias | ID + q_idx, pf, cluster = ~ID), error = function(e) NULL)
  m_p  <- tryCatch(feols(g_norm ~ y_norm | ID + q_idx,          pf, cluster = ~ID), error = function(e) NULL)
  bind_rows(
    fila_fx(m_e,  "log_Y",  nombre) %>% mutate(Estimador = "Elasticidad (M2)"),
    fila_fx(m_ec, "log_Y",  nombre) %>% mutate(Estimador = "Elasticidad con control de días (M3a)"),
    fila_fx(m_p,  "y_norm", nombre) %>% mutate(Estimador = "PMG agrupada en niveles (M4)")
  )
}

VARIANTES <- c("Base (como en v2)",
               "Sin entradas con categoría de gasto",
               "Entradas con categoría de gasto reclasificadas como salidas",
               "Sin entradas con categoría de gasto ni arriendo",
               "Sin las empresas que tienen entradas con categoría de gasto")

sens_ent <- bind_rows(
  estimar_variante(panel_q, VARIANTES[1]),
  estimar_variante(panel_q %>% mutate(Y_qt = Y_qt - Y_catg), VARIANTES[2]),
  estimar_variante(panel_q %>% mutate(Y_qt = Y_qt - Y_catg, G_qt = G_qt + Y_catg), VARIANTES[3]),
  estimar_variante(panel_q %>% mutate(Y_qt = Y_qt - Y_catg - Y_arr), VARIANTES[4]),
  estimar_variante(panel_q %>% filter(!ID %in% ids_afectadas), VARIANTES[5])
) %>%
  mutate(Estimador = factor(Estimador, levels = unique(Estimador)),
         Variante  = factor(Modelo, levels = VARIANTES))

sens_ent %>%
  arrange(Estimador, Variante) %>%
  transmute(Estimador, Variante, Estimación = fmt(coef, 3), `IC 95%` = fmt_ic(ic_lo, ic_hi, 3),
            Obs. = fmt(obs, 0), Empresas = fmt(empresas, 0)) %>%
  kable(caption = "Tabla 23. Sensibilidad a las entradas con categoría de gasto") %>%
  estilo(full_width = TRUE) %>%
  collapse_rows(columns = 1, valign = "top")
Tabla 23. Sensibilidad a las entradas con categoría de gasto
Estimador Variante Estimación IC 95% Obs. Empresas
Elasticidad (M2) Base (como en v2) 0,629 [0,519; 0,739] 628 142
Sin entradas con categoría de gasto 0,628 [0,517; 0,738] 628 142
Entradas con categoría de gasto reclasificadas como salidas 0,628 [0,518; 0,739] 628 142
Sin entradas con categoría de gasto ni arriendo 0,626 [0,514; 0,739] 626 142
Sin las empresas que tienen entradas con categoría de gasto 0,597 [0,463; 0,732] 543 127
Elasticidad con control de días (M3a) Base (como en v2) 0,529 [0,390; 0,667] 628 142
Sin entradas con categoría de gasto 0,526 [0,387; 0,665] 628 142
Entradas con categoría de gasto reclasificadas como salidas 0,525 [0,386; 0,665] 628 142
Sin entradas con categoría de gasto ni arriendo 0,523 [0,383; 0,663] 626 142
Sin las empresas que tienen entradas con categoría de gasto 0,484 [0,313; 0,654] 543 127
PMG agrupada en niveles (M4) Base (como en v2) 0,491 [0,030; 0,951] 628 142
Sin entradas con categoría de gasto 0,481 [0,037; 0,925] 628 142
Entradas con categoría de gasto reclasificadas como salidas 0,485 [0,040; 0,930] 628 142
Sin entradas con categoría de gasto ni arriendo 0,491 [0,043; 0,938] 626 142
Sin las empresas que tienen entradas con categoría de gasto 0,234 [-0,190; 0,658] 543 127
base_e <- sens_ent %>% filter(Estimador == "Elasticidad (M2)", Variante == VARIANTES[1]) %>% pull(coef)
cambio_ent <- sens_ent %>%
  filter(Estimador == "Elasticidad (M2)", Variante != VARIANTES[1]) %>%
  mutate(cambio = (coef - base_e) / base_e)
max_cambio_ent <- max(abs(cambio_ent$cambio), na.rm = TRUE)
n_B_afect <- sum(muestra_B$ID %in% ids_afectadas)

txt_sens_ent <- paste0(
  length(ids_afectadas), " empresas (", n_B_afect, " de la Submuestra B) tienen entradas con categoría de gasto. ",
  "Con los distintos tratamientos, la elasticidad se mueve como máximo un ", fmt_pct(max_cambio_ent),
  " respecto a la base (", fmt(base_e), "). ",
  if (max_cambio_ent < 0.10) "**Los resultados no dependen de estas entradas mal tipadas.**"
  else if (max_cambio_ent < 0.25) "El efecto es moderado: conviene reportar la versión sin estas entradas como principal."
  else "**El efecto es grande**: las entradas mal tipadas inflan la co-variación y deben corregirse en `limpieza_MDF.Rmd` antes de reportar."
)

48 empresas (6 de la Submuestra B) tienen entradas con categoría de gasto. Con los distintos tratamientos, la elasticidad se mueve como máximo un 5,1% respecto a la base (0,63). Los resultados no dependen de estas entradas mal tipadas.

Valores extremos en los modelos en niveles

La PMG en niveles normalizados es sensible a pocas quincenas extremas: una quincena en que la empresa registra salidas muchas veces mayores que su ingreso medio pesa muchísimo. Se re-estima de dos formas:

  • Winsorizando ingreso y gasto normalizados en los percentiles 1 y 99.
  • Recortando el 1% superior de cualquiera de los dos. El recorte conserva la aditividad entre componentes, así que la descomposición se re-estima en esa muestra.
panel_w <- panel_fe %>% mutate(y_norm = winsor(y_norm), g_norm = winsor(g_norm))

lim_y <- quantile(panel_fe$y_norm, 0.99)
lim_g <- quantile(panel_fe$g_norm, 0.99)
panel_rec <- panel_fe %>% filter(y_norm <= lim_y, g_norm <= lim_g)

m4_w <- feols(g_norm ~ y_norm            | ID + q_idx, panel_w,   cluster = ~ID)
m5_w <- feols(g_norm ~ y_norm + log_dias | ID + q_idx, panel_w,   cluster = ~ID)
m4_r <- feols(g_norm ~ y_norm            | ID + q_idx, panel_rec, cluster = ~ID)
m5_r <- feols(g_norm ~ y_norm + log_dias | ID + q_idx, panel_rec, cluster = ~ID)

bind_rows(
  fila_fx(m4,   "y_norm", "M4. Base"),
  fila_fx(m4_w, "y_norm", "M4. Winsorizado p1–p99"),
  fila_fx(m4_r, "y_norm", "M4. Sin el 1% superior"),
  fila_fx(m5,   "y_norm", "M5 (control de días). Base"),
  fila_fx(m5_w, "y_norm", "M5 (control de días). Winsorizado p1–p99"),
  fila_fx(m5_r, "y_norm", "M5 (control de días). Sin el 1% superior")
) %>%
  kable_fx("Tabla 24. PMG agrupada en niveles: sensibilidad a valores extremos")
Tabla 24. PMG agrupada en niveles: sensibilidad a valores extremos
Modelo Coef. EE (cluster) IC 95% p-valor Obs. Empresas
M4. Base 0,491 0,233 [0,030; 0,951] 0,037 628 142
M4. Winsorizado p1–p99 0,519 0,090 [0,340; 0,698] < 0,001 628 142
M4. Sin el 1% superior 0,420 0,066 [0,289; 0,551] < 0,001 614 140
M5 (control de días). Base 0,252 0,238 [-0,218; 0,722] 0,291 628 142
M5 (control de días). Winsorizado p1–p99 0,420 0,102 [0,217; 0,622] < 0,001 628 142
M5 (control de días). Sin el 1% superior 0,297 0,066 [0,167; 0,427] < 0,001 614 140
# Descomposición en la muestra recortada
mods_comp_rec <- map(vars_comp, ~ feols(as.formula(paste(.x, "~ y_norm | ID + q_idx")),
                                        panel_rec, cluster = ~ID))
tab_comp_rec <- imap_dfr(mods_comp_rec, ~ fila_fx(.x, "y_norm", .y)) %>% rename(Componente = Modelo)

tab_comp %>%
  select(Componente, coef, ic_lo, ic_hi) %>%
  left_join(tab_comp_rec %>% select(Componente, coef_r = coef, lo_r = ic_lo, hi_r = ic_hi), by = "Componente") %>%
  transmute(
    Componente,
    `Pesos por $100 — todas`         = fmt(100 * coef, 1),
    `IC 95% — todas`                 = fmt_ic(100 * ic_lo, 100 * ic_hi, 1),
    `Pesos por $100 — sin 1% sup.`   = fmt(100 * coef_r, 1),
    `IC 95% — sin 1% sup.`           = fmt_ic(100 * lo_r, 100 * hi_r, 1)
  ) %>%
  kable(caption = paste0("Tabla 25. Descomposición de la PMG con y sin el 1% superior (",
                         nrow(panel_fe) - nrow(panel_rec), " quincenas recortadas)")) %>%
  estilo(full_width = TRUE) %>%
  row_spec(1, bold = TRUE)
Tabla 25. Descomposición de la PMG con y sin el 1% superior (14 quincenas recortadas)
Componente Pesos por $100 — todas IC 95% — todas Pesos por $100 — sin 1% sup. IC 95% — sin 1% sup.
Total salidas 49,1 [3,0; 95,1] 42,0 [28,9; 55,1]
Operativo variable 21,3 [11,6; 30,9] 16,8 [7,7; 25,9]
Operativo fijo 28,7 [-7,6; 65,0] 12,9 [5,5; 20,4]
Inversión -0,5 [-2,2; 1,2] 0,7 [-0,5; 1,9]
Personal / hogar 10,1 [1,3; 18,9] 4,9 [2,4; 7,5]
Financiero (ahorro y deuda) -10,9 [-36,3; 14,5] 5,9 [2,0; 9,8]
Otros / sin clasificar 0,4 [-0,7; 1,5] 0,8 [-1,0; 2,5]
pmg_m4_r <- coef(m4_r)[["y_norm"]]
pmg_m5_r <- coef(m5_r)[["y_norm"]]
ci_m4   <- confint(m4)["y_norm", ];   ci_m4_r <- confint(m4_r)["y_norm", ]
ancho_m4 <- ci_m4[[2]] - ci_m4[[1]];  ancho_m4_r <- ci_m4_r[[2]] - ci_m4_r[[1]]
f_var_r <- tab_comp_rec %>% filter(Componente == "Operativo variable")
f_tot_r <- tab_comp_rec %>% filter(Componente == "Total salidas")

txt_extremos <- paste0(
  "Sin el 1% superior (", nrow(panel_fe) - nrow(panel_rec), " quincenas), la PMG agrupada pasa de ", fmt(pmg_m4),
  " a ", fmt(pmg_m4_r), " y su IC se ", if (ancho_m4_r < 0.7 * ancho_m4) "estrecha" else "mantiene parecido",
  " (ancho de ", fmt(ancho_m4), " a ", fmt(ancho_m4_r), "). Con control de días, queda en ", fmt(pmg_m5_r), ". ",
  if (ancho_m4_r < 0.7 * ancho_m4) "**La imprecisión de los modelos en niveles venía de pocas quincenas extremas.** "
  else if (m4_impreciso) "La imprecisión no se explica solo por valores extremos: con tan pocas quincenas por empresa, los modelos en niveles son intrínsecamente ruidosos y la elasticidad es la métrica de referencia. "
  else "Los modelos en niveles no dependen de forma importante de los valores extremos. ",
  "En la descomposición recortada, el costo operativo variable es ", fmt_pesos(100 * f_var_r$coef, 1), " de cada $100 ",
  fmt_ic(100 * f_var_r$ic_lo, 100 * f_var_r$ic_hi, 1), " de un total de ", fmt_pesos(100 * f_tot_r$coef, 1), "."
)

Sin el 1% superior (14 quincenas), la PMG agrupada pasa de 0,49 a 0,42 y su IC se estrecha (ancho de 0,92 a 0,26). Con control de días, queda en 0,30. La imprecisión de los modelos en niveles venía de pocas quincenas extremas. En la descomposición recortada, el costo operativo variable es $16,8 de cada $100 [7,7; 25,9] de un total de $42,0.


3. Síntesis de hallazgos

r_A <- res_med %>% filter(Submuestra == "A — mínimo")
r_C <- res_med %>% filter(Submuestra == "C — estricto")
vol_B <- tab_vol %>% filter(Submuestra == "B — intermedio")

estable_ABC <- (rb$med >= r_A$lo & rb$med <= r_A$hi) & (rb$med >= r_C$lo & rb$med <= r_C$hi)
D_informativa <- !is.na(ancho_D) && !D_impreciso
D_compatible  <- !is.na(ancho_D) && rb$med >= rD$lo && rb$med <= rD$hi

impl_rob <- paste0(
  if (estable_ABC) "La mediana de B cae dentro de los IC de A y de C: la sensibilidad es estable entre criterios de selección. "
  else "La mediana de B cae fuera del IC de A o de C: el resultado depende del criterio de selección. ",
  if (!D_informativa) "D (sin cargue retroactivo) es demasiado imprecisa para concluir sobre la Capa 3 (Tabla 22)."
  else if (D_compatible) "D (sin cargue retroactivo) es compatible con B: no hay señal de sesgo por la Capa 3."
  else "D (sin cargue retroactivo) difiere de B: hay señal de sesgo por la Capa 3."
)

impl_comp <- paste0(
  if (var_sig) "La parte robusta de la respuesta es inventario e insumos: estructura de costos, no miopía. "
  else "Ningún componente operativo se estima con precisión. ",
  if (!is.na(f_pers$coef) && es_sig(f_pers$ic_lo, f_pers$ic_hi) && f_pers$coef > 0)
    paste0("Entre quienes registran gasto personal, el hogar recibe unos ", fmt_pesos(100 * f_pers$coef),
           " de cada $100 adicionales: la suavización del consumo del hogar no es perfecta, pero la respuesta es pequeña.")
  else "El gasto del hogar no responde de forma distinguible de cero al ingreso del negocio."
)

impl_asim <- case_when(
  p_asim >= 0.10 ~ "Sin evidencia de asimetría; la prueba tiene poco poder.",
  !asim_sobrevive ~ "La asimetría desaparece con el control de registro: no reportarla como hallazgo de comportamiento.",
  b_neg > b_pos & subregistro ~ "Compatible con restricciones de liquidez, aunque hay señales de sub-registro en las malas quincenas.",
  b_neg > b_pos ~ "Compatible con restricciones de liquidez: sin colchón en las malas quincenas.",
  subregistro ~ "El gasto parece sostenerse en las malas quincenas, pero hay señales de sub-registro que pueden producir el patrón: no reportarla aún.",
  TRUE ~ "El gasto se sostiene en las malas quincenas incluso con control de registro: hay algún amortiguador (fiado, ahorro, crédito informal)."
)

impl_fung <- if (is.na(f_fung_reg$coef)) {
  "No hay suficientes empresas que registren gasto personal para probarla."
} else if (f_fung_reg$coef < 0 && f_fung_reg$p < 0.05) {
  "Evidencia a favor de la fungibilidad entre caja del negocio y del hogar; confirmar con más datos (pruebas múltiples)."
} else if (f_fung_reg$coef < 0 && f_fung_reg$p < 0.10) {
  "Sugerente pero no concluyente."
} else if (!is.na(f_fung_todas$p) && f_fung_todas$p < 0.05 && f_fung_todas$coef < 0) {
  "No se sostiene entre quienes registran gasto personal: era un efecto de quién anota qué."
} else {
  "Sin evidencia de fungibilidad."
}

impl_vol <- if (vol_B$`% DE gasto > DE ingreso (pesos)` < 50) {
  "La irregularidad del flujo viene más del ingreso que del gasto."
} else {
  "En la mayoría de empresas el gasto varía más que el ingreso incluso en pesos: la irregularidad también viene del lado del gasto."
}

hallazgos <- tribble(
  ~Hallazgo, ~Implicación,
  paste0("PMG mediana por empresa (Submuestra B): ", fmt(rb$med), " ", fmt_ic(rb$lo, rb$hi),
         ". De cada $100 adicionales que entran, salen alrededor de $", round(100 * rb$med), "."),
  if_else(rb$lo > 0 & rb$hi < 1,
          "Suavización parcial: se rechaza tanto 'no responden' (β = 0) como 'gastan todo lo que entra' (β = 1).",
          "La evidencia no permite distinguir con precisión el grado de suavización."),

  paste0("Elasticidad en el panel (", n_distinct(panel_fe$ID), " empresas, EF empresa + quincena): ", fmt(e_m2),
         "; con control de días con registro, ", fmt(e_m3a), "; con control de transacciones",
         if (e_m3b < e_m3a) " (cota inferior)" else "", ", ", fmt(e_m3b), "."),
  paste0("Cifra de referencia: ", fmt(e_m3a), ". Un aumento de 10% en el ingreso de la quincena se asocia con ~",
         fmt(10 * e_m3a, 1), "% más de salidas. Una parte de la co-variación es intensidad de uso de la app."),

  paste0("Descomposición (niveles): el componente estimado con más precisión es el operativo variable, ",
         fmt_pesos(100 * c_var), " de cada $100 ", fmt_ic(100 * fila_comp("Operativo variable")$ic_lo,
                                                          100 * fila_comp("Operativo variable")$ic_hi, 0),
         if (!is.na(f_pers$coef)) paste0("; gasto del hogar entre quienes lo registran: ", fmt_pesos(100 * f_pers$coef), " ",
                                         fmt_ic(100 * f_pers$ic_lo, 100 * f_pers$ic_hi, 0)) else "", "."),
  impl_comp,

  paste0("Asimetría: sin control, β+ = ", fmt(b_pos), " y β− = ", fmt(b_neg), " (", txt_p(p_asim),
         "); con control de días, β+ = ", fmt(b_pos_c), " y β− = ", fmt(b_neg_c), " (", txt_p(p_asim_c), ")."),
  impl_asim,

  paste0("Fungibilidad: separar finanzas se asocia con una PMG del gasto personal ",
         if (!is.na(f_fung_reg$coef)) paste0(fmt(f_fung_reg$coef, 3), " ", fmt_ic(f_fung_reg$ic_lo, f_fung_reg$ic_hi, 3),
                                             " entre quienes lo registran (", txt_p(f_fung_reg$p), ")") else "no estimable",
         "; en la muestra completa, ", fmt(f_fung_todas$coef, 3), " (", txt_p(f_fung_todas$p), ")."),
  impl_fung,

  paste0("Mediana de quincenas en déficit: ", fmt_pct(deficit_med_B), " — más o menos 1 de cada ", uno_de_cada, " quincenas."),
  "Exposición a choques negativos: una fracción no despreciable de quincenas cierra en rojo.",

  paste0("Con CV, el gasto parece más volátil que el ingreso en el ", fmt(vol_B$`% CV gasto > CV ingreso`, 1),
         "% de las empresas; en pesos, solo en el ", fmt(vol_B$`% DE gasto > DE ingreso (pesos)`, 1), "%."),
  impl_vol,

  paste0("Medianas de β entre submuestras: A = ", fmt(r_A$med), ", B = ", fmt(rb$med), ", C = ",
         fmt(r_C$med), ", D = ", fmt(rD$med), " (n = ", rD$N, ")."),
  impl_rob,

  paste0("Entradas con categoría de gasto: la elasticidad cambia como máximo ", fmt_pct(max_cambio_ent),
         " al excluirlas o reclasificarlas."),
  if (max_cambio_ent < 0.10) "Los resultados no dependen de estos registros mal tipados."
  else "Corregir estos registros en limpieza_MDF.Rmd antes de reportar.",

  paste0("La Submuestra B es el ", fmt_pct(pct_B_univ), " de las empresas con transacciones."),
  "Los resultados describen a usuarios activos de MDF, no a los micronegocios bogotanos en general.",

  "Limitación central: duplicados irresolubles, registro voluntario e incompleto, y salidas que mezclan costos del negocio con gasto del hogar.",
  "Publicar como documento de trabajo exploratorio, no como evidencia causal."
)

hallazgos %>%
  mutate(`#` = row_number(), .before = 1) %>%
  kable(caption = "Tabla 26. Síntesis de hallazgos e implicaciones") %>%
  estilo(full_width = TRUE, font_size = 11) %>%
  row_spec(nrow(hallazgos), italic = TRUE, color = COL_GRIS)
Tabla 26. Síntesis de hallazgos e implicaciones
# Hallazgo Implicación
1 PMG mediana por empresa (Submuestra B): 0,41 [0,30; 0,53]. De cada $100 adicionales que entran, salen alrededor de $41. Suavización parcial: se rechaza tanto ‘no responden’ (β = 0) como ‘gastan todo lo que entra’ (β = 1).
2 Elasticidad en el panel (144 empresas, EF empresa + quincena): 0,63; con control de días con registro, 0,53; con control de transacciones (cota inferior), 0,46. Cifra de referencia: 0,53. Un aumento de 10% en el ingreso de la quincena se asocia con ~5,3% más de salidas. Una parte de la co-variación es intensidad de uso de la app.
3 Descomposición (niveles): el componente estimado con más precisión es el operativo variable, $21 de cada $100 [12; 31]; gasto del hogar entre quienes lo registran: $14 [1; 26]. La parte robusta de la respuesta es inventario e insumos: estructura de costos, no miopía. Entre quienes registran gasto personal, el hogar recibe unos $14 de cada $100 adicionales: la suavización del consumo del hogar no es perfecta, pero la respuesta es pequeña.
4 Asimetría: sin control, β+ = 0,88 y β− = 0,38 (p = 0,005); con control de días, β+ = 0,82 y β− = 0,22 (p = 0,002). El gasto parece sostenerse en las malas quincenas, pero hay señales de sub-registro que pueden producir el patrón: no reportarla aún.
5 Fungibilidad: separar finanzas se asocia con una PMG del gasto personal -0,100 [-0,230; 0,031] entre quienes lo registran (p = 0,131); en la muestra completa, -0,212 (p = 0,007). No se sostiene entre quienes registran gasto personal: era un efecto de quién anota qué.
6 Mediana de quincenas en déficit: 27,3% — más o menos 1 de cada 4 quincenas. Exposición a choques negativos: una fracción no despreciable de quincenas cierra en rojo.
7 Con CV, el gasto parece más volátil que el ingreso en el 60,8% de las empresas; en pesos, solo en el 29,4%. La irregularidad del flujo viene más del ingreso que del gasto.
8 Medianas de β entre submuestras: A = 0,43, B = 0,41, C = 0,36, D = 0,50 (n = 11). La mediana de B cae dentro de los IC de A y de C: la sensibilidad es estable entre criterios de selección. D (sin cargue retroactivo) es demasiado imprecisa para concluir sobre la Capa 3 (Tabla 22).
9 Entradas con categoría de gasto: la elasticidad cambia como máximo 5,1% al excluirlas o reclasificarlas. Los resultados no dependen de estos registros mal tipados.
10 La Submuestra B es el 9,2% de las empresas con transacciones. Los resultados describen a usuarios activos de MDF, no a los micronegocios bogotanos en general.
11 Limitación central: duplicados irresolubles, registro voluntario e incompleto, y salidas que mezclan costos del negocio con gasto del hogar. Publicar como documento de trabajo exploratorio, no como evidencia causal.

Lo que este análisis no permite concluir

  • Que los micronegocios “no suavizan”. La evidencia es de suavización parcial, y una parte de la respuesta del gasto es costo operativo.
  • Que el producto adecuado sea un crédito con cuota fija. Nada en el análisis lleva a esa conclusión. Con una fracción relevante de quincenas en déficit, una cuota fija podría ser justamente el producto más riesgoso. Cualquier recomendación de producto financiero requiere evidencia que esta base no tiene: demanda, mora, uso del crédito.
  • Efectos causales de separar finanzas, del género de la propietaria o del perfil digital. Son asociaciones, en una muestra autoseleccionada de usuarios activos.
  • Resultados por sector fuera de Comercio y Servicios.

Pendientes

  1. Revisar con el equipo la clasificación de categorías de la sección 1.2, en especial “Pago a empleados”, “Comisiones” y “Cuota de préstamo informal” cuando aparece como entrada.
  2. Confirmar con ANIF qué significan las entradas con categoría de gasto (Tabla 2d) y si “Arriendo” como entrada es ingreso por alquiler; corregirlas en limpieza_MDF.Rmd.
  3. Si ANIF puede entregar una marca de duplicado confiable, re-estimar sin la Capa 3 en lugar de depender de la Submuestra D.
  4. Si la fungibilidad se sostiene (Tabla 20 y texto de 2.9), confirmarla con nuevas cargas de datos antes de destacarla.

4. Exportación de submuestras analíticas

Propósito: generar una base completa por cada submuestra para que los colaboradores puedan hacer sus propios análisis sin reproducir todo el pipeline. Cada base incluye las transacciones limpias (de ig_limpia.csv) + los indicadores calculados aquí (con prefijo ind_) + el perfil de la empresa (de car_limpia.csv, prefijo car_), unidos por ID. Nuevo en v2: también se exporta el panel quincenal con los componentes del gasto.

if (file.exists("car_limpia.csv")) {
  car_limpia <- read_csv("car_limpia.csv", show_col_types = FALSE)
  cat("car_limpia.csv cargado:", nrow(car_limpia), "empresas\n")
} else {
  cat("ADVERTENCIA: car_limpia.csv no encontrado.\n")
  cat("Ejecute limpieza_caracterizacion.Rmd primero.\n")
  cat("Las submuestras se exportarán sin el perfil de la empresa.\n")
  car_limpia <- tibble(ID = character())
}
## car_limpia.csv cargado: 910 empresas
construir_submuestra <- function(ids_submuestra, nombre_sub) {
  transacciones <- ig_clean %>% filter(ID %in% ids_submuestra)

  indicadores_sub <- indicadores_emp %>%
    filter(ID %in% ids_submuestra) %>%
    rename_with(~ paste0("ind_", .), -ID)

  perfil_sub <- if (nrow(car_limpia) > 0) {
    car_limpia %>% filter(ID %in% ids_submuestra) %>% rename_with(~ paste0("car_", .), -ID)
  } else {
    tibble(ID = ids_submuestra)
  }

  transacciones %>%
    left_join(indicadores_sub, by = "ID") %>%
    left_join(perfil_sub,      by = "ID") %>%
    mutate(submuestra = nombre_sub) %>%
    arrange(ID, `Fecha de la transacción`)
}

base_A <- construir_submuestra(muestra_A$ID, "A")
base_B <- construir_submuestra(muestra_B$ID, "B")
base_C <- construir_submuestra(muestra_C$ID, "C")
base_D <- construir_submuestra(muestra_D$ID, "D")

# Panel quincenal con componentes (una fila por empresa-quincena) — NUEVO
panel_export <- panel_q %>%
  mutate(
    bilateral = Y_qt > 0 & G_qt > 0,
    en_A = ID %in% muestra_A$ID, en_B = ID %in% muestra_B$ID,
    en_C = ID %in% muestra_C$ID, en_D = ID %in% muestra_D$ID
  ) %>%
  arrange(ID, q_idx)

tibble(
  Base        = c("A — mínimo", "B — intermedio", "C — estricto", "D — confiabilidad",
                  "Panel quincenal"),
  Criterio    = c("≥3 quincenas bilaterales", "≥5 q + ratio_gi<3", "≥8 q + ratio_gi<2 + CV<3",
                  "≥3 q + 0% retroactivo + sin cargues sin fecha",
                  "Todas las empresa-quincena con registro"),
  Empresas    = c(n_distinct(base_A$ID), n_distinct(base_B$ID), n_distinct(base_C$ID),
                  n_distinct(base_D$ID), n_distinct(panel_export$ID)),
  Filas       = c(nrow(base_A), nrow(base_B), nrow(base_C), nrow(base_D), nrow(panel_export)),
  Columnas    = c(ncol(base_A), ncol(base_B), ncol(base_C), ncol(base_D), ncol(panel_export))
) %>%
  kable(caption = "Tabla 27. Bases exportadas", format.args = FMT_TABLA) %>%
  estilo(full_width = FALSE) %>%
  row_spec(2, bold = TRUE, background = "#EBF5FB")
Tabla 27. Bases exportadas
Base Criterio Empresas Filas Columnas
A — mínimo ≥3 quincenas bilaterales 99 9.377 102
B — intermedio ≥5 q + ratio_gi<3 51 7.087 102
C — estricto ≥8 q + ratio_gi<2 + CV<3 17 3.665 102
D — confiabilidad ≥3 q + 0% retroactivo + sin cargues sin fecha 11 807 102
Panel quincenal Todas las empresa-quincena con registro 557 1.518 22
write_csv(base_A, "submuestra_A.csv")
write_csv(base_B, "submuestra_B.csv")
write_csv(base_C, "submuestra_C.csv")
write_csv(base_D, "submuestra_D.csv")
write_csv(panel_export,    "panel_quincenal_componentes.csv")
write_csv(indicadores_emp, "indicadores_por_empresa.csv")

write_xlsx(list(
  "Submuestra_A" = base_A,
  "Submuestra_B" = base_B,
  "Submuestra_C" = base_C,
  "Submuestra_D" = base_D
), "submuestras_analiticas.xlsx")

write_xlsx(base_A, "submuestra_A.xlsx")
write_xlsx(base_B, "submuestra_B.xlsx")
write_xlsx(base_C, "submuestra_C.xlsx")
write_xlsx(base_D, "submuestra_D.xlsx")

cat("Archivos exportados:\n")
## Archivos exportados:
cat("  submuestra_A/B/C/D.csv y .xlsx — transacciones + indicadores + perfil\n")
##   submuestra_A/B/C/D.csv y .xlsx — transacciones + indicadores + perfil
cat("  submuestras_analiticas.xlsx — las cuatro en un archivo con 4 hojas\n")
##   submuestras_analiticas.xlsx — las cuatro en un archivo con 4 hojas
cat("  panel_quincenal_componentes.csv —", nrow(panel_export), "empresa-quincenas (NUEVO)\n")
##   panel_quincenal_componentes.csv — 1518 empresa-quincenas (NUEVO)
cat("  indicadores_por_empresa.csv —", nrow(indicadores_emp), "empresas (NUEVO)\n")
##   indicadores_por_empresa.csv — 314 empresas (NUEVO)
cat("\nLlave de unión: campo 'ID' (UUID). Para recrear el join: left_join(ig_limpia, car_limpia, by = 'ID')\n")
## 
## Llave de unión: campo 'ID' (UUID). Para recrear el join: left_join(ig_limpia, car_limpia, by = 'ID')

5. Referencias

  • Hall, R. E. (1978). Stochastic implications of the life cycle–permanent income hypothesis: Theory and evidence. Journal of Political Economy, 86(6), 971–987.
  • Morduch, J. (1995). Income smoothing and consumption smoothing. Journal of Economic Perspectives, 9(3), 103–114.
  • Shea, J. (1995). Myopia, liquidity constraints, and aggregate consumption: A simple test. Journal of Money, Credit and Banking, 27(3), 798–805.
  • Townsend, R. M. (1994). Risk and insurance in village India. Econometrica, 62(3), 539–591.
  • Zeldes, S. P. (1989). Consumption and liquidity constraints: An empirical investigation. Journal of Political Economy, 97(2), 305–346.

6. Información de sesión

sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 10 x64 (build 19045)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8  LC_CTYPE=Spanish_Colombia.utf8   
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Spanish_Colombia.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] lmtest_0.9-40    zoo_1.8-14       sandwich_3.1-1   fixest_0.13.2   
##  [5] writexl_1.5.4    patchwork_1.3.2  kableExtra_1.4.1 knitr_1.51      
##  [9] broom_1.0.9      ggridges_0.5.7   scales_1.4.0     readxl_1.5.0    
## [13] lubridate_1.9.4  forcats_1.0.0    stringr_1.6.0    dplyr_1.2.1     
## [17] purrr_1.2.2      readr_2.1.5      tidyr_1.3.2      tibble_3.3.1    
## [21] ggplot2_4.0.3    tidyverse_2.0.0 
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6        xfun_0.52           bslib_0.9.0        
##  [4] lattice_0.22-7      tzdb_0.5.0          numDeriv_2016.8-1.1
##  [7] vctrs_0.7.3         tools_4.5.2         generics_0.1.4     
## [10] parallel_4.5.2      pkgconfig_2.0.3     RColorBrewer_1.1-3 
## [13] S7_0.2.0            stringmagic_1.2.0   lifecycle_1.0.5    
## [16] compiler_4.5.2      farver_2.1.2        textshaping_1.0.1  
## [19] htmltools_0.5.8.1   sass_0.4.10         yaml_2.3.10        
## [22] Formula_1.2-5       crayon_1.5.3        pillar_1.11.0      
## [25] jquerylib_0.1.4     cachem_1.1.0        nlme_3.1-168       
## [28] tidyselect_1.2.1    digest_0.6.37       mvtnorm_1.3-3      
## [31] stringi_1.8.7       labeling_0.4.3      fastmap_1.2.0      
## [34] grid_4.5.2          cli_3.6.5           magrittr_2.0.5     
## [37] withr_3.0.2         dreamerr_1.5.0      backports_1.5.0    
## [40] bit64_4.6.0-1       estimability_1.5.1  timechange_0.3.0   
## [43] rmarkdown_2.29      emmeans_1.11.2-8    bit_4.6.0          
## [46] cellranger_1.1.0    hms_1.1.3           coda_0.19-4.1      
## [49] evaluate_1.0.5      viridisLite_0.4.2   rlang_1.3.0        
## [52] Rcpp_1.1.0          xtable_1.8-4        glue_1.8.0         
## [55] xml2_1.4.0          vroom_1.6.5         svglite_2.2.1      
## [58] rstudioapi_0.17.1   jsonlite_2.0.0      R6_2.6.1           
## [61] systemfonts_1.2.3

González Lozano, L.E. & Granados Rodríguez, H. (2026). ¿Gastan lo que entra? Sensibilidad del gasto al ingreso en micronegocios usuarios de Mi Diario Financiero — SDDE. Documento de trabajo exploratorio, versión 2.1. Subdirección de Estudios Estratégicos / ODEB.