Pregunta. A medida que una empresa usa Mi Diario Financiero (MDF) quincena tras quincena, ¿registra mejor? Es decir, ¿bajan los errores de registro contable — tipo de movimiento invertido, campos sin diligenciar, montos imposibles, registro tardío — y, cuando una empresa comete un error, deja de repetirlo en las 2 o 4 quincenas siguientes?

Prerrequisitos. Este cuaderno lee la base cruda Base_Anonimizada_de_Ingresos_Gastos.xlsx (no ig_limpia.csv, porque la limpieza ya corrigió algunos de los errores que aquí se quieren medir, por ejemplo Ventas registradas como Salida) y la caracterización limpia car_limpia.csv (producida por limpieza_caracterizacion.Rmd), solo para el sector y las variables de heterogeneidad.


0. Librerías, parámetros y funciones

library(tidyverse)    # manipulación y visualización (incluye lubridate)
library(readxl)       # lectura de Excel
library(scales)       # formato de ejes
library(knitr)        # tablas
library(kableExtra)   # formato de tablas
library(patchwork)    # combinar gráficas
library(sandwich)     # errores agrupados por empresa (vcovCL)
library(lmtest)       # coeftest

# Paleta institucional (la misma de analisis_MDF_v2_1)
COL_POS  <- "#1baf7a"
COL_NEG  <- "#e34948"
COL_AZUL <- "#1A5276"
COL_GRIS <- "#898781"
# ── Parámetros del análisis (cambiar aquí, no dentro del código) ─────────────
FECHA_CORTE   <- as.Date("2026-08-13")  # igual que limpieza_MDF.Rmd
DIAS_TARDIO   <- 30     # registro tardío: cargado > 30 días después de la transacción
K_TOPE_CURVA  <- 8      # en las curvas, k >= K_TOPE_CURVA se agrupa
K_TOPE_DUMMY  <- 6      # en el modelo con dummies, k >= K_TOPE_DUMMY se agrupa
MIN_Q_FIJO    <- 6      # panel fijo: empresas con al menos 6 quincenas de uso
HORIZONTES    <- c(2, 4) # ventanas de no repetición (quincenas de uso siguientes)

# ── Formato colombiano: miles con punto, decimales con coma ─────────────────
options(knitr.kable.NA = "—")
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_pp  <- function(x, d = 1) ifelse(is.na(x), "—", paste0(ifelse(x > 0, "+", ""), fmt(100 * x, d), " pp"))
fmt_p   <- function(p) case_when(is.na(p) ~ "—", p < 0.001 ~ "< 0,001", TRUE ~ fmt(p, 3))
fmt_ic  <- function(lo, hi, d = 1) ifelse(is.na(lo) | is.na(hi), "—",
                                          paste0("[", fmt(100 * lo, d), "; ", fmt(100 * hi, d), "]"))
estilo  <- function(k, ...) kable_styling(k, bootstrap_options = c("striped", "hover"), ...)
es_sig  <- function(p) !is.na(p) & p < 0.05

# ── Modelos lineales con errores agrupados por empresa ───────────────────────
# Modelo de probabilidad lineal con efectos fijos de empresa (dummies) y,
# opcionalmente, de mes calendario. Devuelve una fila por término pedido.
ajustar <- function(formula, datos, terminos, nombre) {
  m  <- lm(formula, data = datos)
  usadas <- datos[as.integer(rownames(model.frame(m))), ]   # filas sin NA que entran al modelo
  ct <- coeftest(m, vcov. = vcovCL(m, cluster = usadas$ID, type = "HC1"))
  map_dfr(terminos, function(t) {
    if (!t %in% rownames(ct)) {
      return(tibble(Modelo = nombre, termino = t, coef = NA_real_, ee = NA_real_, p = NA_real_))
    }
    tibble(Modelo = nombre, termino = t, coef = ct[t, 1], ee = ct[t, 2], p = ct[t, 4])
  }) %>%
    mutate(ic_lo = coef - 1.96 * ee, ic_hi = coef + 1.96 * ee,
           obs = nobs(m), empresas = n_distinct(datos$ID))
}

# Índice calendario de quincena (igual que analisis_MDF_v2_1): 1–15 y 16–fin de mes
q_index <- function(d) (year(d) - 2024) * 24 + (month(d) - 1) * 2 + if_else(day(d) <= 15, 1, 2)

1. Estrategia en una página

  1. Base sin corregir. Se reconstruye la base de transacciones aplicando solo los pasos de limpieza_MDF.Rmd que no tocan el contenido del registro: duplicados exactos (Capa 1), re-ingestas de lotes (Capa 2), fechas futuras e IDs nulos. Se conservan el Tipo, la Categoría, el Método de Pago, la Cuenta y el Monto tal como los dejó la empresa.
  2. Errores observables. Se define un catálogo de errores que se pueden leer en la base y que dependen de la empresa (no del sistema). Antes, se descartan los campos que llena la aplicación (sección 3.1).
  3. Experiencia. Para cada empresa, k = número de orden de la quincena de uso: k = 1 es la primera quincena con transacciones, k = 2 la segunda, etc. Cuenta quincenas usadas, no quincenas de calendario (una empresa que deja de registrar un mes no gana experiencia ese mes).
  4. Tres lecturas complementarias:
    • Curva descriptiva: tasa de error promedio por k, con todas las empresas y con un panel fijo (empresas con ≥ 6 quincenas), para separar aprendizaje de composición.
    • Modelo dentro de empresa: tasa de error de la empresa i en su quincena k contra log(k), con efectos fijos de empresa y de mes calendario. El efecto fijo de mes absorbe cambios de la aplicación que afectan a todos a la vez. Como k cuenta quincenas usadas, el efecto se identifica con las empresas que usan la app de forma intermitente (la experiencia acumulada difiere del tiempo transcurrido).
    • No repetición: de las empresas que cometen un error, ¿qué proporción no lo vuelve a cometer en sus 2 y 4 quincenas de uso siguientes? Se compara con la probabilidad de cometerlo en ese mismo horizonte cuando no se cometió antes.

2. Base de trabajo

ig_raw <- read_excel("Base_Anonimizada_de_Ingresos_Gastos.xlsx", sheet = "Ingresos_Gastos")
car    <- read_csv("car_limpia.csv", show_col_types = FALSE)

# ── Replica de los pasos de limpieza_MDF.Rmd que no alteran el contenido ─────
n0 <- nrow(ig_raw)
ig <- ig_raw %>% distinct()                                       # Capa 1
n1 <- nrow(ig)
LLAVE <- c("ID", "Fecha de la transacción", "Tipo", "Monto", "Categoría", "Método de Pago")
ig <- ig %>%
  arrange(`Fecha de cargue de información al sistema`) %>%       # conservar el más antiguo
  distinct(across(all_of(LLAVE)), .keep_all = TRUE)               # Capa 2
n2 <- nrow(ig)
ig <- ig %>%
  filter(as.Date(`Fecha de la transacción`) <= FECHA_CORTE, !is.na(ID))

# ── Fechas ───────────────────────────────────────────────────────────────────
parse_mdf <- function(x) {
  x %>% str_replace_all("p\\. m\\.", "PM") %>% str_replace_all("a\\. m\\.", "AM") %>%
    str_trim() %>% parse_date_time(orders = "d/m/y, I:M p", quiet = TRUE)
}
ig <- ig %>%
  mutate(
    fecha      = as.Date(`Fecha de la transacción`),
    cargue_dt  = parse_mdf(`Fecha de cargue de información al sistema`),
    q_idx      = q_index(fecha),
    mes        = (q_idx - 1) %/% 2,                        # mes calendario de la transacción
    q_cargue   = if_else(is.na(cargue_dt), NA_real_, q_index(as.Date(cargue_dt))),
    mes_cargue = floor_date(as.Date(cargue_dt), "month"),
    dias_cargue = as.numeric(difftime(cargue_dt, as.POSIXct(fecha, tz = "UTC"), units = "days"))
  ) %>%
  left_join(car %>% select(ID, Sector, Educación, `Registro de Facturas`, Presupuesto,
                           Perfil_Digital),
            by = "ID")

tibble(
  Paso  = c("Base cruda", "Capa 1: filas idénticas", "Capa 2: re-ingestas de lotes",
            "Fechas futuras e IDs nulos", "Base de trabajo (sin correcciones de contenido)"),
  Filas = c(n0, n1, n2, NA, nrow(ig)),
  Nota  = c("", "", "", "", "Debe coincidir con las filas de ig_limpia.csv")
) %>%
  mutate(Filas = fmt(Filas, 0)) %>%
  kable(caption = "Tabla 1. Construcción de la base de trabajo") %>%
  estilo(full_width = FALSE)
Tabla 1. Construcción de la base de trabajo
Paso Filas Nota
Base cruda 19.309
Capa 1: filas idénticas 16.721
Capa 2: re-ingestas de lotes 13.179
Fechas futuras e IDs nulos —
Base de trabajo (sin correcciones de contenido) 13.113 Debe coincidir con las filas de ig_limpia.csv

La base de trabajo tiene 13.113 transacciones de 564 empresas: las mismas filas de ig_limpia.csv, pero con el contenido original.


3. ¿Qué cuenta como un error de registro?

3.1 Lo que no se puede usar: campos que llena la aplicación

Antes de medir aprendizaje hay que separar los errores de la empresa de los “errores” del sistema. Si la aplicación cambia, la tasa de error cambia para todos al tiempo y eso se confunde con aprendizaje.

Fijo / Variable (Tipo de Gasto). La idea de evaluar si un costo está bien clasificado como fijo o variable no se puede aplicar: cada categoría tiene un único tipo de gasto en toda la base (Tabla 2a). Es decir, el campo lo asigna la aplicación a partir de la categoría, no la empresa. Por ejemplo, Servicios públicos aparece siempre como Variable y Arriendo siempre como Fijo.

tg_cat <- ig %>%
  filter(Tipo == "Salida", !is.na(`Tipo de Gasto`), !is.na(Categoría)) %>%
  group_by(Categoría) %>%
  summarise(Transacciones = n(),
            `Valores distintos de Tipo de Gasto` = n_distinct(`Tipo de Gasto`),
            `Tipo de Gasto` = paste(sort(unique(`Tipo de Gasto`)), collapse = " / "),
            .groups = "drop") %>%
  arrange(desc(Transacciones))

tg_cat %>%
  kable(caption = "Tabla 2a. Salidas: Tipo de Gasto según la categoría — un solo valor por categoría",
        format.args = list(big.mark = ".")) %>%
  estilo(full_width = FALSE) %>%
  scroll_box(height = "350px")
Tabla 2a. Salidas: Tipo de Gasto según la categoría — un solo valor por categoría
Categoría Transacciones Valores distintos de Tipo de Gasto Tipo de Gasto
Materia prima 1.309 1 Variable
Gastos personales 1.072 1 Variable
Mercancías 617 1 Variable
Suministros 452 1 Variable
Gastos Administrativos 403 1 Variable
Servicios tercerizados 394 1 Variable
Servicios públicos 251 1 Variable
Ahorro 212 1 Variable
Pago a empleados 159 1 Variable
Cuota de préstamo informal 130 1 Variable
Arriendo 121 1 Fijo
Mobiliario y maquinaria 77 1 Variable
Cuota de préstamo formal 70 1 Variable
Marketing y publicidad 51 1 Variable
Transporte 38 1 Variable
Impuestos 20 1 Variable
Gastos de software 16 1 Fijo
En seguros 12 1 Fijo
Ventas 3 1 Variable
Comisiones pagadas 1 1 Variable
Ventas de acciones 1 1 Variable

0 de 21 categorías tienen más de un tipo de gasto.

Tipo de Gasto en entradas. Las entradas no deberían tener tipo de gasto, pero muchas lo tienen. Su frecuencia depende del mes en que se cargó la información (Tabla 2b), no de la empresa: es un valor por defecto del sistema que cambió con el tiempo. Tampoco sirve como error de la empresa.

ig %>%
  filter(Tipo == "Entrada", !is.na(mes_cargue)) %>%
  group_by(`Mes de cargue` = format(mes_cargue, "%Y-%m")) %>%
  summarise(Entradas = n(),
            `% con Tipo de Gasto` = fmt_pct(mean(!is.na(`Tipo de Gasto`))),
            .groups = "drop") %>%
  kable(caption = "Tabla 2b. Entradas con Tipo de Gasto asignado, por mes de cargue",
        format.args = list(big.mark = ".")) %>%
  estilo(full_width = FALSE)
Tabla 2b. Entradas con Tipo de Gasto asignado, por mes de cargue
Mes de cargue Entradas % con Tipo de Gasto
2025-08 32 56,2%
2025-09 49 100,0%
2025-10 35 100,0%
2025-11 43 53,5%
2025-12 37 24,3%
2026-01 269 94,8%
2026-02 1.016 13,8%
2026-03 917 47,4%
2026-04 1.407 25,2%
2026-05 1.469 10,0%
2026-06 1.394 2,9%
2026-07 578 0,0%

Cuenta. Es texto libre con muchas variantes de escritura (“Negui”, “Dauplata”, “Billetera Moul”), que parecen errores de transcripción más que de contabilidad, y el campo deja de diligenciarse con el tiempo (Tabla 2c). Se usa solo de forma descriptiva (error E5 más abajo).

ig %>%
  filter(!is.na(mes_cargue)) %>%
  group_by(`Mes de cargue` = format(mes_cargue, "%Y-%m")) %>%
  summarise(Transacciones = n(), `% con Cuenta diligenciada` = fmt_pct(mean(!is.na(Cuenta))),
            .groups = "drop") %>%
  kable(caption = "Tabla 2c. Diligenciamiento del campo Cuenta por mes de cargue",
        format.args = list(big.mark = ".")) %>%
  estilo(full_width = FALSE)
Tabla 2c. Diligenciamiento del campo Cuenta por mes de cargue
Mes de cargue Transacciones % con Cuenta diligenciada
2025-08 77 77,9%
2025-09 111 99,1%
2025-10 81 98,8%
2025-11 87 97,7%
2025-12 79 92,4%
2026-01 484 91,5%
2026-02 1.970 71,1%
2026-03 1.559 39,6%
2026-04 2.406 31,6%
2026-05 2.507 19,0%
2026-06 2.595 21,6%
2026-07 980 9,7%

3.2 Catálogo de errores

Código Error Definición operativa Lectura contable
E1 Tipo de movimiento inconsistente con la categoría Entrada con categoría de gasto (Mercancías, Materia prima, Suministros, Gastos personales, Pago a empleados…); Salida con categoría de ingreso (Ventas, Préstamos formales, Premios, Cuentas por cobrar…); Arriendo como Entrada en empresas de Comercio, Industria o Agro El movimiento está al revés (un gasto anotado como ingreso) o la categoría está mal elegida (una venta de mercancía anotada en Mercancías y no en Ventas). En ambos casos el estado de resultados queda mal
E2 Categoría vacía o irreconocible Categoría NA o texto que no corresponde a ninguna cuenta (“CONACIÓN”, “Aperio”…) No clasifica el movimiento
E3 Método de pago vacío Método de Pago = NA No identifica por dónde entra o sale la plata
E4 Monto imposible Monto = $0 o menor a $1.000 Probable omisión de los miles (“50” por “50.000”)
E5 Método y cuenta contradictorios (descriptivo) Pago en efectivo registrado en una cuenta digital o bancaria, o pago digital registrado en caja Registra la plata en la cuenta equivocada
E6 Registro tardío Cargado más de 30 días después de la fecha de la transacción Registro no oportuno: se reconstruye de memoria
Núcleo Al menos un error E1–E4 Indicador resumen de calidad del registro

Quedan fuera, por ambiguas: Cuota de préstamo informal como entrada (puede ser un préstamo recibido), Donaciones, Comisiones y Premios como entrada (pueden ser ingresos reales), Servicios como salida y Arriendo como entrada en empresas de Servicios u Otros (pueden subarrendar). E6 no es un error de clasificación sino de oportunidad; se reporta aparte y no entra en el núcleo.

CAT_GASTO <- c("Materia prima", "Mercancías", "Suministros", "Servicios tercerizados",
               "Gastos Administrativos", "Gastos personales", "Pago a empleados", "Compras",
               "Proveedores", "Servicios públicos", "Transporte", "Impuestos", "En seguros",
               "Gastos de software", "Marketing y publicidad", "Mobiliario y maquinaria",
               "Gasto de Papelería")
CAT_INGRESO       <- c("Ventas", "Ventas de acciones", "Préstamos formales", "Premios",
                       "Cuentas por cobrar")
CAT_IRRECONOCIBLE <- c("CONACIÓN", "Aperio", "Transferencia desde Naoqui")
SECT_SIN_ARRIENDO <- c("Comercio", "Industria", "Agro")
MET_DIGITAL <- c("Transferencias", "Bre-B", "Billetera digital", "QR", "Link de pagos",
                 "Consignación", "Tarjeta de débito", "Tarjeta de crédito")

cta <- str_to_lower(coalesce(ig$Cuenta, ""))
ig <- ig %>%
  mutate(
    # E1 y sus tres componentes
    e1_gasto_como_entrada   = Tipo == "Entrada" & Categoría %in% CAT_GASTO,
    e1_ingreso_como_salida  = Tipo == "Salida"  & Categoría %in% CAT_INGRESO,
    e1_arriendo_como_ingreso = Tipo == "Entrada" & Categoría %in% "Arriendo" &
                               Sector %in% SECT_SIN_ARRIENDO,
    E1 = e1_gasto_como_entrada | e1_ingreso_como_salida | e1_arriendo_como_ingreso,
    E2 = is.na(Categoría) | Categoría %in% CAT_IRRECONOCIBLE,
    E3 = is.na(`Método de Pago`),
    E4 = Monto < 1000,
    # E5: solo donde la cuenta se identifica como caja o como digital/banco
    cta_caja    = str_detect(cta, "efecti|caja|cash"),
    cta_digital = str_detect(cta, paste0("nequi|negui|negri|negú|negüi|daviplata|davipl|dauplata|",
                                         "billetera|b digital|b\\. digital|bre-b|brc-b|bold|ahorro|",
                                         "corriente|bancolombia|davivienda|banco|moul|houll")),
    op_E5 = xor(cta_caja, cta_digital) &
            (`Método de Pago` %in% c("Efectivo", MET_DIGITAL)),
    E5 = op_E5 & ((`Método de Pago` == "Efectivo" & cta_digital) |
                  (`Método de Pago` %in% MET_DIGITAL & cta_caja)),
    E6 = !is.na(dias_cargue) & dias_cargue > DIAS_TARDIO,
    E_nucleo = E1 | E2 | E3 | E4
  ) %>%
  select(-cta_caja, -cta_digital)

ERRORES <- c(E1 = "E1 Tipo inconsistente con la categoría",
             E2 = "E2 Categoría vacía o irreconocible",
             E3 = "E3 Método de pago vacío",
             E4 = "E4 Monto imposible (< $1.000)",
             E6 = "E6 Registro tardío (> 30 días)",
             E_nucleo = "Núcleo: algún error E1–E4")

resumen_err <- function(v, etiqueta, datos = ig) {
  x <- datos[[v]]
  tibble(Error = etiqueta, Transacciones = sum(x), `% de transacciones` = mean(x),
         Empresas = n_distinct(datos$ID[x]),
         `% de empresas` = n_distinct(datos$ID[x]) / n_distinct(datos$ID))
}

tab_err <- bind_rows(
  resumen_err("E1", "E1 Tipo inconsistente con la categoría"),
  resumen_err("e1_gasto_como_entrada",    "   · gasto registrado como entrada"),
  resumen_err("e1_ingreso_como_salida",   "   · ingreso registrado como salida"),
  resumen_err("e1_arriendo_como_ingreso", "   · arriendo como entrada (Comercio/Industria/Agro)"),
  resumen_err("E2", ERRORES["E2"]),
  resumen_err("E3", ERRORES["E3"]),
  resumen_err("E4", ERRORES["E4"]),
  resumen_err("E5", "E5 Método y cuenta contradictorios*", filter(ig, op_E5)),
  resumen_err("E6", ERRORES["E6"]),
  resumen_err("E_nucleo", ERRORES["E_nucleo"])
)

tab_err %>%
  mutate(Transacciones = fmt(Transacciones, 0), `% de transacciones` = fmt_pct(`% de transacciones`),
         `% de empresas` = fmt_pct(`% de empresas`)) %>%
  kable(caption = "Tabla 3. Frecuencia de cada error en la base de trabajo") %>%
  estilo(full_width = FALSE) %>%
  footnote(general = paste0("* E5 se calcula solo sobre las ", fmt(sum(ig$op_E5), 0),
                            " transacciones con cuenta identificable como caja o como digital/banco."))
Tabla 3. Frecuencia de cada error en la base de trabajo
Error Transacciones % de transacciones Empresas % de empresas
E1 Tipo inconsistente con la categoría 195 1,5% 61 10,8%
· gasto registrado como entrada 87 0,7% 50 8,9%
· ingreso registrado como salida 4 0,0% 4 0,7%
· arriendo como entrada (Comercio/Industria/Agro) 104 0,8% 8 1,4%
E2 Categoría vacía o irreconocible 369 2,8% 17 3,0%
E3 Método de pago vacío 1.579 12,0% 178 31,6%
E4 Monto imposible (< $1.000) 728 5,6% 70 12,4%
E5 Método y cuenta contradictorios* 75 1,8% 37 9,7%
E6 Registro tardío (> 30 días) 4.635 35,3% 261 46,3%
Núcleo: algún error E1–E4 2.422 18,5% 228 40,4%
Note:
* E5 se calcula solo sobre las 4.189 transacciones con cuenta identificable como caja o como digital/banco.

3.3 El caso del arriendo como ingreso

arr <- ig %>%
  filter(Tipo == "Entrada", Categoría == "Arriendo") %>%
  group_by(Sector = coalesce(Sector, "(sin caracterización)")) %>%
  summarise(Transacciones = n(), Empresas = n_distinct(ID),
            `Transacciones de la empresa con más casos` = max(table(ID)),
            `Monto mediano` = median(Monto), .groups = "drop") %>%
  arrange(desc(Transacciones))

arr %>%
  mutate(`Monto mediano` = paste0("$", fmt(`Monto mediano`, 0))) %>%
  kable(caption = "Tabla 4. Arriendo registrado como entrada, por sector",
        format.args = list(big.mark = ".")) %>%
  estilo(full_width = FALSE)
Tabla 4. Arriendo registrado como entrada, por sector
Sector Transacciones Empresas Transacciones de la empresa con más casos Monto mediano
Comercio 104 8 73 $58.200
Servicios 7 6 2 $40.000
Otros 6 1 6 $49.500
ids_arr_com <- ig %>% filter(e1_arriendo_como_ingreso) %>% distinct(ID) %>% pull()
n_arr_tambien_sal <- ig %>%
  filter(ID %in% ids_arr_com, Tipo == "Salida", Categoría == "Arriendo") %>%
  distinct(ID) %>% nrow()

El arriendo como ingreso en empresas de comercio, industria o agro está muy concentrado: 8 empresas, de las cuales 2 también registran arriendo como salida (pagan arriendo y además lo anotan como entrada). Por eso pesa poco en las tasas promedio por empresa, aunque es el ejemplo más claro de confusión ingreso–gasto.

3.4 Transacciones sin fecha de cargue

sin_cargue <- ig %>% filter(is.na(cargue_dt))

sin_cargue %>%
  count(`Fecha de la transacción` = fecha, name = "Transacciones") %>%
  arrange(desc(Transacciones)) %>%
  slice_head(n = 8) %>%
  kable(caption = "Tabla 4b. Transacciones sin fecha de cargue: días más frecuentes") %>%
  estilo(full_width = FALSE)
Tabla 4b. Transacciones sin fecha de cargue: días más frecuentes
Fecha de la transacción Transacciones
2025-07-24 35
2025-09-02 20
2025-07-29 16
2026-02-17 16
2026-02-18 10
2025-07-30 9
2025-09-24 6
2026-02-16 6
ig %>%
  mutate(Grupo = if_else(is.na(cargue_dt), "Sin fecha de cargue", "Con fecha de cargue")) %>%
  group_by(Grupo) %>%
  summarise(Transacciones = n(), Empresas = n_distinct(ID),
            across(c(E1, E2, E3, E4, E_nucleo), mean), .groups = "drop") %>%
  mutate(across(c(E1, E2, E3, E4, E_nucleo), fmt_pct)) %>%
  rename(`Núcleo (E1–E4)` = E_nucleo) %>%
  kable(caption = "Tabla 4c. Tasa de error según si la transacción tiene fecha de cargue",
        format.args = list(big.mark = ".")) %>%
  estilo(full_width = FALSE)
Tabla 4c. Tasa de error según si la transacción tiene fecha de cargue
Grupo Transacciones Empresas E1 E2 E3 E4 Núcleo (E1–E4)
Con fecha de cargue 12.936 548 1,2% 2,8% 11,8% 5,5% 18,0%
Sin fecha de cargue 177 61 23,7% 1,1% 33,3% 7,3% 54,8%
top_dias   <- sin_cargue %>% count(fecha, sort = TRUE) %>% slice_head(n = 8)
share_top  <- sum(top_dias$n) / nrow(sin_cargue)
razon_err  <- mean(sin_cargue$E_nucleo) / mean(ig$E_nucleo[!is.na(ig$cargue_dt)])
n_sc       <- nrow(sin_cargue); n_sc_emp <- n_distinct(sin_cargue$ID)

# Se excluyen del análisis de experiencia
ig <- ig %>% filter(!is.na(cargue_dt))

177 transacciones (61 empresas) no tienen fecha de cargue. El 66,7% se concentra en ocho días (Tabla 4b), lo que sugiere jornadas de registro asistido o de prueba más que uso cotidiano, y su tasa de error núcleo es 3,0 veces la del resto (Tabla 4c). Como sin fecha de cargue no se puede ubicar el momento de uso, se excluyen del análisis de experiencia. Si se dejaran, inflarían el error de la primera quincena y fabricarían una curva de aprendizaje. Quedan 12.936 transacciones de 548 empresas.


4. La experiencia: quincenas de uso

ig <- ig %>%
  group_by(ID) %>%
  mutate(k  = dense_rank(q_idx),          # 1 = primera quincena con transacciones
         nq = n_distinct(q_idx)) %>%
  ungroup()

ig %>%
  distinct(ID, nq) %>%
  mutate(`Quincenas de uso` = case_when(nq == 1 ~ "1", nq == 2 ~ "2", nq == 3 ~ "3",
                                        nq <= 5 ~ "4–5", nq <= 9 ~ "6–9", TRUE ~ "10 o más"),
         `Quincenas de uso` = factor(`Quincenas de uso`,
                                     levels = c("1", "2", "3", "4–5", "6–9", "10 o más"))) %>%
  count(`Quincenas de uso`, name = "Empresas") %>%
  arrange(`Quincenas de uso`) %>%
  mutate(`% de empresas` = fmt_pct(Empresas / sum(Empresas))) %>%
  kable(caption = "Tabla 5. Empresas según el número de quincenas en que usaron MDF") %>%
  estilo(full_width = FALSE)
Tabla 5. Empresas según el número de quincenas en que usaron MDF
Quincenas de uso Empresas % de empresas
1 270 49,3%
2 102 18,6%
3 51 9,3%
4–5 60 10,9%
6–9 44 8,0%
10 o más 21 3,8%

270 empresas (49,3%) usan MDF en una sola quincena: no aportan a la pregunta de aprendizaje dentro de la empresa. El análisis dentro de empresa usa las 278 empresas con 2 o más quincenas.

# Panel empresa × quincena de uso: tasa de error = % de transacciones de la quincena con error
construir_panel <- function(datos, kvar = "k", calvar = "mes") {
  datos %>%
    group_by(ID, k = .data[[kvar]], cal = .data[[calvar]]) %>%
    summarise(n = n(),
              across(c(E1, E2, E3, E4, E6, E_nucleo), mean),
              .groups = "drop") %>%
    group_by(ID) %>%
    mutate(nq = n()) %>%
    ungroup() %>%
    mutate(lk = log(k), ln = log(n))
}
# Nota: una quincena de uso cae en un solo mes calendario (q_idx → mes es función).
panel <- construir_panel(ig)
stopifnot(nrow(panel) == nrow(distinct(ig, ID, q_idx)))

5. Curva de aprendizaje descriptiva

Cada punto es la tasa de error promedio de las empresas en su k-ésima quincena de uso (cada empresa-quincena pesa lo mismo, sin importar cuántas transacciones registró). Los intervalos son al 95% con errores agrupados por empresa.

curva <- function(p, v, tope) {
  d <- p %>% mutate(kc = factor(pmin(k, tope)))
  m <- lm(reformulate("0 + kc", v), data = d)
  V <- vcovCL(m, cluster = d$ID, type = "HC1")
  tibble(k = as.integer(levels(d$kc)), tasa = coef(m), ee = sqrt(diag(V)),
         empresas = as.integer(table(d$kc)), error = v)
}

curvas <- bind_rows(
  map_dfr(names(ERRORES), ~ curva(panel, .x, K_TOPE_CURVA)) %>% mutate(Muestra = "Todas las empresas"),
  map_dfr(names(ERRORES), ~ curva(filter(panel, nq >= MIN_Q_FIJO), .x, MIN_Q_FIJO)) %>%
    mutate(Muestra = paste0("Panel fijo (≥ ", MIN_Q_FIJO, " quincenas)"))
) %>%
  mutate(error = factor(ERRORES[error], levels = ERRORES),
         lo = pmax(tasa - 1.96 * ee, 0), hi = tasa + 1.96 * ee)

ggplot(curvas, aes(k, tasa, colour = Muestra, fill = Muestra)) +
  geom_ribbon(aes(ymin = lo, ymax = hi), alpha = 0.15, colour = NA) +
  geom_line(linewidth = 0.9) + geom_point(size = 1.8) +
  facet_wrap(~ error, scales = "free_y", ncol = 3) +
  scale_x_continuous(breaks = 1:K_TOPE_CURVA,
                     labels = c(1:(K_TOPE_CURVA - 1), paste0(K_TOPE_CURVA, "+"))) +
  scale_y_continuous(labels = label_percent(decimal.mark = ",")) +
  scale_colour_manual(values = c(COL_AZUL, "#e67e22")) +
  scale_fill_manual(values = c(COL_AZUL, "#e67e22")) +
  labs(x = "Quincena de uso (k)", y = "% de transacciones con error",
       title = "Figura 1. Tasa de error según la quincena de uso de MDF",
       subtitle = "En el panel fijo, la última categoría agrupa k ≥ 6",
       colour = NULL, fill = NULL) +
  theme_minimal(base_size = 11) + theme(legend.position = "bottom")

curvas %>%
  filter(Muestra == "Todas las empresas") %>%
  transmute(error, k = if_else(k == K_TOPE_CURVA, paste0(k, "+"), as.character(k)),
            valor = paste0(fmt_pct(tasa), " (", empresas, ")")) %>%
  pivot_wider(names_from = k, values_from = valor) %>%
  rename(Error = error) %>%
  kable(caption = "Tabla 6. % de transacciones con error por quincena de uso — todas las empresas (entre paréntesis: empresas)") %>%
  estilo(full_width = TRUE)
Tabla 6. % de transacciones con error por quincena de uso — todas las empresas (entre paréntesis: empresas)
Error 1 2 3 4 5 6 7 8+
E1 Tipo inconsistente con la categoría 0,6% (548) 0,2% (278) 0,5% (176) 0,4% (125) 1,6% (94) 0,7% (65) 0,2% (50) 1,1% (178)
E2 Categoría vacía o irreconocible 0,9% (548) 0,6% (278) 0,2% (176) 0,8% (125) 0,3% (94) 0,8% (65) 3,7% (50) 8,6% (178)
E3 Método de pago vacío 15,6% (548) 17,6% (278) 17,1% (176) 16,1% (125) 7,6% (94) 9,5% (65) 9,6% (50) 7,4% (178)
E4 Monto imposible (< $1.000) 4,6% (548) 8,4% (278) 7,9% (176) 4,2% (125) 2,8% (94) 0,5% (65) 1,2% (50) 1,9% (178)
E6 Registro tardío (> 30 días) 37,2% (548) 36,2% (278) 31,2% (176) 30,1% (125) 27,3% (94) 27,0% (65) 36,6% (50) 48,1% (178)
Núcleo: algún error E1–E4 18,5% (548) 19,8% (278) 19,7% (176) 19,6% (125) 10,6% (94) 11,5% (65) 12,6% (50) 16,9% (178)

Cómo leerla. La curva de “todas las empresas” mezcla dos cosas: aprendizaje y composición (quiénes siguen usando la app). El panel fijo sigue a las mismas empresas en todas sus quincenas, pero es pequeño. Por eso la evidencia principal es el modelo dentro de empresa (sección 6).


6. Modelo dentro de empresa

\[\text{error}_{ik} = \alpha_i + \beta \log(k) + \gamma_{\text{mes}} + \delta \log(n_{ik}) + \varepsilon_{ik}\]

  • \(\alpha_i\): efecto fijo de empresa (compara a cada empresa consigo misma).
  • \(\gamma_{\text{mes}}\): efecto fijo de mes calendario (absorbe cambios de la aplicación y estacionalidad comunes a todas).
  • \(n_{ik}\): número de transacciones de la quincena (más transacciones = más oportunidades de error).
  • \(\beta < 0\) indica aprendizaje. Con log(k), \(\beta \times \log 2\) es el cambio en la tasa de error al duplicar la experiencia (p. ej. pasar de la 2.ª a la 4.ª quincena).
pm <- filter(panel, nq >= 2)

especificaciones <- list(
  "EF empresa"                  = "~ lk + factor(ID)",
  "EF empresa + mes"            = "~ lk + factor(ID) + factor(cal)",
  "EF empresa + mes + log(n)"   = "~ lk + ln + factor(ID) + factor(cal)"
)

res_fe <- map_dfr(c("E1", "E2", "E3", "E4", "E6", "E_nucleo"), function(v) {
  imap_dfr(especificaciones, function(rhs, nom) {
    ajustar(as.formula(paste(v, rhs)), pm, "lk", nom) %>% mutate(error = v)
  })
})

res_fe %>%
  mutate(Error = ERRORES[error],
         `Cambio al duplicar k` = fmt_pp(coef * log(2)),
         `IC 95% (pp)` = fmt_ic(ic_lo * log(2), ic_hi * log(2)),
         `p-valor` = fmt_p(p)) %>%
  select(Error, Modelo, `Cambio al duplicar k`, `IC 95% (pp)`, `p-valor`, Obs. = obs, Empresas = empresas) %>%
  kable(caption = "Tabla 7. Efecto de la experiencia sobre la tasa de error (modelo de probabilidad lineal, errores agrupados por empresa)",
        format.args = list(big.mark = ".")) %>%
  estilo(full_width = TRUE) %>%
  collapse_rows(columns = 1, valign = "top")
Tabla 7. Efecto de la experiencia sobre la tasa de error (modelo de probabilidad lineal, errores agrupados por empresa)
Error Modelo Cambio al duplicar k IC 95% (pp) p-valor Obs. Empresas
E1 Tipo inconsistente con la categoría EF empresa -0,0 pp [-0,6; 0,6] 0,943 1.244 278
EF empresa + mes -0,1 pp [-0,8; 0,7] 0,868 1.244 278
EF empresa + mes + log(n) -0,1 pp [-0,8; 0,6] 0,771 1.244 278
E2 Categoría vacía o irreconocible EF empresa +1,6 pp [-0,3; 3,6] 0,097 1.244 278
EF empresa + mes +0,6 pp [-0,4; 1,5] 0,234 1.244 278
EF empresa + mes + log(n) +0,7 pp [-0,3; 1,6] 0,173 1.244 278
E3 Método de pago vacío EF empresa +1,2 pp [-0,3; 2,7] 0,125 1.244 278
EF empresa + mes +2,1 pp [-0,2; 4,4] 0,071 1.244 278
EF empresa + mes + log(n) +2,2 pp [-0,1; 4,4] 0,063 1.244 278
E4 Monto imposible (< $1.000) EF empresa -0,9 pp [-2,8; 0,9] 0,325 1.244 278
EF empresa + mes -0,5 pp [-3,0; 1,9] 0,678 1.244 278
EF empresa + mes + log(n) -0,7 pp [-3,1; 1,7] 0,577 1.244 278
E6 Registro tardío (> 30 días) EF empresa -14,5 pp [-18,0; -10,9] < 0,001 1.244 278
EF empresa + mes -5,6 pp [-10,9; -0,3] 0,039 1.244 278
EF empresa + mes + log(n) -5,6 pp [-10,9; -0,4] 0,036 1.244 278
Núcleo: algún error E1–E4 EF empresa +1,1 pp [-2,0; 4,1] 0,493 1.244 278
EF empresa + mes +0,8 pp [-2,4; 4,1] 0,613 1.244 278
EF empresa + mes + log(n) +0,9 pp [-2,3; 4,2] 0,575 1.244 278
# Sin forma funcional: dummies de k (k = 1 es la base), con EF de empresa y de mes
pm_d <- pm %>% mutate(kd = factor(pmin(k, K_TOPE_DUMMY)))
terms_d <- paste0("kd", 2:K_TOPE_DUMMY)
res_d <- map_dfr(c("E1", "E3", "E4", "E6", "E_nucleo"), function(v) {
  ajustar(as.formula(paste(v, "~ kd + factor(ID) + factor(cal)")), pm_d, terms_d, v) %>%
    mutate(error = v)
})

res_d %>%
  mutate(k = str_remove(termino, "kd"),
         k = if_else(k == as.character(K_TOPE_DUMMY), paste0(k, "+"), k),
         valor = paste0(fmt_pp(coef), if_else(es_sig(p), " *", ""))) %>%
  select(error, k, valor) %>%
  pivot_wider(names_from = k, values_from = valor) %>%
  mutate(error = ERRORES[error]) %>%
  rename(`Error (diferencia vs. k = 1)` = error) %>%
  kable(caption = "Tabla 8. Diferencia en la tasa de error frente a la primera quincena de uso, dentro de empresa y controlando por mes (* p < 0,05)") %>%
  estilo(full_width = TRUE)
Tabla 8. Diferencia en la tasa de error frente a la primera quincena de uso, dentro de empresa y controlando por mes (* p < 0,05)
Error (diferencia vs. k = 1) 2 3 4 5 6+
E1 Tipo inconsistente con la categoría -0,2 pp -0,1 pp -0,5 pp +0,5 pp -0,7 pp
E3 Método de pago vacío +2,0 pp +3,9 pp +5,5 pp +4,5 pp +7,9 pp *
E4 Monto imposible (< $1.000) +1,3 pp +1,4 pp -2,4 pp -1,4 pp -2,0 pp
E6 Registro tardío (> 30 días) -7,8 pp * -13,1 pp * -14,1 pp * -18,1 pp * -11,1 pp
Núcleo: algún error E1–E4 -0,5 pp +0,0 pp +0,8 pp -1,7 pp +1,4 pp

Lectura. Con efectos fijos de empresa y de mes, los errores que bajan de forma estadísticamente significativa con el uso son: E6 Registro tardío (> 30 días). Para el indicador núcleo (E1–E4) el efecto es +0,9 pp por cada duplicación de la experiencia (p = 0,575). El registro tardío (E6) cambia -5,6 pp por cada duplicación de la experiencia (p = 0,036), aunque este resultado no es robusto (sección 9).

El tipo de movimiento inconsistente (E1) no cambia con la experiencia: -0,1 pp por cada duplicación de la experiencia (p = 0,771).


7. ¿Se corrige el error? No repetición a 2 y 4 quincenas

Dos medidas complementarias, ambas sobre la secuencia de quincenas de uso de cada empresa:

  • Primer error (Tabla 9). Para cada empresa y tipo de error se toma la primera quincena en que lo comete. Entre las que tienen al menos h quincenas de uso posteriores, ¿qué % no lo repite en las h siguientes? Y, entre las que tienen al menos 4 quincenas posteriores, ¿qué % no lo vuelve a cometer nunca en lo que resta del período?
  • Transiciones (Tabla 10). En cualquier quincena k, compara la probabilidad de cometer el error en las h quincenas siguientes si lo cometió en k frente a si no lo cometió. Si la primera es mucho mayor, el error es un hábito persistente de la empresa; si fueran parecidas, el error sería ocasional.
# Máximo de b en las h posiciones siguientes; NA si no hay h quincenas posteriores
fut_max <- function(b, h) {
  n <- length(b); out <- rep(NA_real_, n)
  if (n > h) for (i in seq_len(n - h)) out[i] <- max(b[(i + 1):(i + h)])
  out
}

panel_b <- panel %>%
  arrange(ID, k) %>%
  mutate(across(c(E1, E2, E3, E4, E6, E_nucleo), ~ as.integer(.x > 0), .names = "b_{.col}"))

# ── Tabla 9: primer error ────────────────────────────────────────────────────
primer_error <- map_dfr(c("E1", "E2", "E3", "E4", "E6", "E_nucleo"), function(v) {
  bv <- paste0("b_", v)
  panel_b %>%
    group_by(ID) %>%
    summarise(
      k0      = if (any(.data[[bv]] == 1)) which(.data[[bv]] == 1)[1] else NA_integer_,
      despues = if (is.na(k0)) NA_integer_ else n() - k0,
      no_rep2 = if (!is.na(k0) && despues >= 2) max(.data[[bv]][(k0 + 1):(k0 + 2)]) == 0 else NA,
      no_rep4 = if (!is.na(k0) && despues >= 4) max(.data[[bv]][(k0 + 1):(k0 + 4)]) == 0 else NA,
      nunca   = if (!is.na(k0) && despues >= 4) max(.data[[bv]][(k0 + 1):n()]) == 0 else NA,
      .groups = "drop") %>%
    summarise(error = v,
              `Empresas que lo cometen` = sum(!is.na(k0)),
              n2 = sum(!is.na(no_rep2)), r2 = mean(no_rep2, na.rm = TRUE),
              n4 = sum(!is.na(no_rep4)), r4 = mean(no_rep4, na.rm = TRUE),
              rnunca = mean(nunca, na.rm = TRUE))
})

primer_error %>%
  transmute(Error = ERRORES[error], `Empresas que lo cometen`,
            `No lo repite en 2 q.` = paste0(fmt_pct(r2), " (n = ", n2, ")"),
            `No lo repite en 4 q.` = paste0(fmt_pct(r4), " (n = ", n4, ")"),
            `No lo repite nunca (≥ 4 q. de seguimiento)` = fmt_pct(rnunca)) %>%
  kable(caption = "Tabla 9. ¿Qué % de empresas no repite el error tras cometerlo por primera vez?") %>%
  estilo(full_width = TRUE)
Tabla 9. ¿Qué % de empresas no repite el error tras cometerlo por primera vez?
Error Empresas que lo cometen No lo repite en 2 q. No lo repite en 4 q. No lo repite nunca (≥ 4 q. de seguimiento)
E1 Tipo inconsistente con la categoría 22 69,2% (n = 13) 55,6% (n = 9) 55,6%
E2 Categoría vacía o irreconocible 15 25,0% (n = 4) 0,0% (n = 2) 0,0%
E3 Método de pago vacío 169 29,2% (n = 65) 25,9% (n = 27) 22,2%
E4 Monto imposible (< $1.000) 64 32,4% (n = 37) 35,0% (n = 20) 35,0%
E6 Registro tardío (> 30 días) 261 26,6% (n = 128) 11,6% (n = 69) 10,1%
Núcleo: algún error E1–E4 196 25,3% (n = 83) 25,6% (n = 39) 17,9%
# ── Tabla 10: transiciones ───────────────────────────────────────────────────
transiciones <- map_dfr(c("E1", "E2", "E3", "E4", "E6", "E_nucleo"), function(v) {
  bv <- paste0("b_", v)
  map_dfr(HORIZONTES, function(h) {
    panel_b %>%
      group_by(ID) %>%
      mutate(fut = fut_max(.data[[bv]], h)) %>%
      ungroup() %>%
      filter(!is.na(fut)) %>%
      summarise(error = v, h = h,
                p_con = mean(fut[.data[[bv]] == 1]), n_con = sum(.data[[bv]] == 1),
                p_sin = mean(fut[.data[[bv]] == 0]), n_sin = sum(.data[[bv]] == 0))
  })
})

transiciones %>%
  transmute(Error = ERRORES[error], `Horizonte (quincenas)` = h,
            `P(error en las h siguientes | error en k)` = paste0(fmt_pct(p_con), " (", n_con, ")"),
            `P(error en las h siguientes | sin error en k)` = paste0(fmt_pct(p_sin), " (", n_sin, ")"),
            `Razón` = fmt(p_con / p_sin, 1)) %>%
  kable(caption = "Tabla 10. Persistencia del error: probabilidad de cometerlo en las h quincenas siguientes (entre paréntesis: empresa-quincenas)") %>%
  estilo(full_width = TRUE) %>%
  collapse_rows(columns = 1, valign = "top")
Tabla 10. Persistencia del error: probabilidad de cometerlo en las h quincenas siguientes (entre paréntesis: empresa-quincenas)
Error Horizonte (quincenas) P(error en las h siguientes &#124; error en k) P(error en las h siguientes &#124; sin error en k) Razón
E1 Tipo inconsistente con la categoría 2 58,1% (31) 3,8% (657) 15,3
4 68,0% (25) 9,4% (362) 7,2
E2 Categoría vacía o irreconocible 2 95,8% (24) 1,5% (664) 63,6
4 100,0% (18) 3,8% (369) 26,4
E3 Método de pago vacío 2 73,4% (143) 18,7% (545) 3,9
4 78,1% (73) 31,8% (314) 2,5
E4 Monto imposible (< $1.000) 2 67,1% (70) 8,6% (618) 7,8
4 72,2% (36) 13,7% (351) 5,3
E6 Registro tardío (> 30 días) 2 77,3% (406) 28,7% (282) 2,7
4 91,7% (252) 53,3% (135) 1,7
Núcleo: algún error E1–E4 2 77,6% (201) 24,0% (487) 3,2
4 81,1% (111) 40,6% (276) 2,0

Lectura. El error de tipo de movimiento (E1) es el que más se corrige: 69,2% de las empresas que lo cometen no lo repiten en las 2 quincenas siguientes y 55,6% en las 4 siguientes (n = 13 y 9 empresas, respectivamente). En cambio, el método de pago vacío (E3) y el monto imposible (E4) se comportan como hábitos: solo 29,2% y 32,4% no los repiten en 2 quincenas, y quien los comete en una quincena tiene 3,9 y 7,8 veces más probabilidad de cometerlos en las 2 siguientes que quien no.

Cuidado con la interpretación. La Tabla 10 no separa “el error genera más error” de “hay empresas que siempre registran así”. Lo segundo es lo más probable: son rasgos estables de la forma de registrar de cada empresa, que el uso por sí solo no cambia.


8. ¿Se van las empresas que se equivocan?

Si las empresas con más errores abandonan la app, la curva de “todas las empresas” bajaría sin que nadie aprendiera. Se compara la probabilidad de seguir usando MDF después de la primera quincena según si la empresa cometió o no el error en esa primera quincena.

k1 <- panel %>% filter(k == 1)
map_dfr(c("E1", "E3", "E4", "E6", "E_nucleo"), function(v) {
  tibble(Error = ERRORES[v],
         `Siguen (con error en k = 1)` = mean(k1$nq[k1[[v]] > 0] >= 2),
         `Siguen (sin error en k = 1)` = mean(k1$nq[k1[[v]] == 0] >= 2),
         `Empresas con error en k = 1` = sum(k1[[v]] > 0),
         p = suppressWarnings(prop.test(
           c(sum(k1$nq[k1[[v]] > 0] >= 2), sum(k1$nq[k1[[v]] == 0] >= 2)),
           c(sum(k1[[v]] > 0), sum(k1[[v]] == 0)))$p.value))
}) %>%
  mutate(across(starts_with("Siguen"), fmt_pct), p = fmt_p(p)) %>%
  rename(`p-valor (diferencia)` = p) %>%
  kable(caption = "Tabla 11. % de empresas que usan MDF en una segunda quincena, según su desempeño en la primera") %>%
  estilo(full_width = FALSE)
Tabla 11. % de empresas que usan MDF en una segunda quincena, según su desempeño en la primera
Error Siguen (con error en k = 1) Siguen (sin error en k = 1) Empresas con error en k = 1 p-valor (diferencia)
E1 Tipo inconsistente con la categoría 66,7% 50,6% 6 0,708
E3 Método de pago vacío 53,6% 50,0% 112 0,570
E4 Monto imposible (< $1.000) 80,0% 48,7% 35 < 0,001
E6 Registro tardío (> 30 días) 68,5% 38,7% 222 < 0,001
Núcleo: algún error E1–E4 56,0% 49,0% 134 0,195

Las empresas que se equivocan en su primera quincena no abandonan más que las demás; si acaso, siguen un poco más. La atrición no fabrica una curva de aprendizaje.


9. Robustez

Cuatro variaciones del modelo principal (EF empresa + mes + log(n)):

  1. Experiencia por fecha de cargue. k = orden de la quincena en que la empresa subió información (en lugar de la fecha de la transacción), con efecto fijo del mes de cargue. Es la medida más cercana a “veces que la empresa usó la plataforma”, pero muchas empresas suben todo en uno o dos lotes.
  2. Sin registros tardíos. Se excluyen las transacciones cargadas con más de 30 días de retraso (información histórica), que no reflejan uso en tiempo real.
  3. Solo desde el primer cargue. Se conservan únicamente las transacciones ocurridas desde la fecha del primer cargue de la empresa. Al vincularse, muchas empresas suben de una vez el historial de meses anteriores: esas transacciones quedan con k bajo y con registro tardío por construcción, lo que puede fabricar una caída de E6 sin aprendizaje. Esta muestra elimina ese efecto mecánico.
  4. Sin la empresa con más transacciones. Para verificar que una sola empresa no arrastre el resultado.
ig_c <- ig %>%
  filter(!is.na(q_cargue)) %>%
  group_by(ID) %>% mutate(k_cargue = dense_rank(q_cargue)) %>% ungroup() %>%
  mutate(mes_c = as.character(mes_cargue))
p_cargue <- construir_panel(ig_c, kvar = "k_cargue", calvar = "mes_c") %>% filter(nq >= 2)

ig_rt <- ig %>%
  filter(!E6) %>%
  group_by(ID) %>% mutate(k_rt = dense_rank(q_idx)) %>% ungroup()
p_rt <- construir_panel(ig_rt, kvar = "k_rt") %>% filter(nq >= 2)

id_grande <- ig %>% count(ID, sort = TRUE) %>% slice(1) %>% pull(ID)
p_sg <- pm %>% filter(ID != id_grande)

# Solo transacciones ocurridas desde el primer cargue de la empresa (uso "en tiempo real")
ig_post <- ig %>%
  group_by(ID) %>%
  filter(fecha >= as.Date(min(cargue_dt, na.rm = TRUE))) %>%
  mutate(k_post = dense_rank(q_idx)) %>%
  ungroup()
p_post <- construir_panel(ig_post, kvar = "k_post") %>% filter(nq >= 2)

muestras <- list("Principal (k por fecha de transacción)" = pm,
                 "k por fecha de cargue"                  = p_cargue,
                 "Sin registros tardíos"                  = p_rt,
                 "Solo desde el primer cargue"            = p_post,
                 "Sin la empresa más grande"              = p_sg)

res_rob <- map_dfr(c("E1", "E3", "E4", "E_nucleo", "E6"), function(v) {
  imap_dfr(muestras, function(d, nom) {
    if (v == "E6" && nom == "Sin registros tardíos") return(NULL)
    ajustar(as.formula(paste(v, "~ lk + ln + factor(ID) + factor(cal)")), d, "lk", nom) %>%
      mutate(error = v)
  })
})

res_rob %>%
  mutate(Error = ERRORES[error],
         `Cambio al duplicar k` = paste0(fmt_pp(coef * log(2)), if_else(es_sig(p), " *", "")),
         `IC 95% (pp)` = fmt_ic(ic_lo * log(2), ic_hi * log(2))) %>%
  select(Error, Muestra = Modelo, `Cambio al duplicar k`, `IC 95% (pp)`, Obs. = obs, Empresas = empresas) %>%
  kable(caption = "Tabla 12. Robustez del efecto de la experiencia (* p < 0,05)",
        format.args = list(big.mark = ".")) %>%
  estilo(full_width = TRUE) %>%
  collapse_rows(columns = 1, valign = "top")
Tabla 12. Robustez del efecto de la experiencia (* p < 0,05)
Error Muestra Cambio al duplicar k IC 95% (pp) Obs. Empresas
E1 Tipo inconsistente con la categoría Principal (k por fecha de transacción) -0,1 pp [-0,8; 0,6] 1.244 278
k por fecha de cargue -0,7 pp [-2,2; 0,9] 772 241
Sin registros tardíos -0,2 pp [-1,6; 1,2] 783 205
Solo desde el primer cargue -0,6 pp [-2,1; 0,9] 689 159
Sin la empresa más grande -0,1 pp [-0,8; 0,6] 1.233 277
E3 Método de pago vacío Principal (k por fecha de transacción) +2,2 pp [-0,1; 4,4] 1.244 278
k por fecha de cargue +4,4 pp * [0,1; 8,7] 772 241
Sin registros tardíos +4,0 pp [-0,1; 8,0] 783 205
Solo desde el primer cargue +3,3 pp [-2,0; 8,5] 689 159
Sin la empresa más grande +2,1 pp [-0,2; 4,4] 1.233 277
E4 Monto imposible (< $1.000) Principal (k por fecha de transacción) -0,7 pp [-3,1; 1,7] 1.244 278
k por fecha de cargue -3,5 pp [-7,0; 0,1] 772 241
Sin registros tardíos -1,0 pp [-3,9; 1,8] 783 205
Solo desde el primer cargue -1,5 pp [-4,8; 1,7] 689 159
Sin la empresa más grande -0,7 pp [-3,1; 1,7] 1.233 277
Núcleo: algún error E1–E4 Principal (k por fecha de transacción) +0,9 pp [-2,3; 4,2] 1.244 278
k por fecha de cargue +0,5 pp [-5,0; 6,1] 772 241
Sin registros tardíos +1,5 pp [-3,3; 6,4] 783 205
Solo desde el primer cargue +1,6 pp [-3,9; 7,0] 689 159
Sin la empresa más grande +0,9 pp [-2,4; 4,1] 1.233 277
E6 Registro tardío (> 30 días) Principal (k por fecha de transacción) -5,6 pp * [-10,9; -0,4] 1.244 278
k por fecha de cargue +1,5 pp [-6,9; 9,9] 772 241
Solo desde el primer cargue -4,8 pp [-12,0; 2,4] 689 159
Sin la empresa más grande -5,6 pp * [-10,8; -0,3] 1.233 277

Lectura. Para el indicador núcleo (E1–E4), ninguna variación muestra un efecto significativo. El método de pago vacío (E3) tiene coeficiente positivo en 5 de 5 variaciones (significativo en 1): si algo, se diligencia menos con el uso, un patrón compatible con fatiga de registro.

El registro tardío (E6) es el resultado más sensible. En la especificación principal baja -5,6 pp (p = 0,036) por cada duplicación de la experiencia, pero con k por fecha de cargue el cambio es +1,5 pp (p = 0,721) y solo con transacciones desde el primer cargue es -4,8 pp (p = 0,190). La mejora de la especificación principal parece venir sobre todo del cargue del historial al vincularse (transacciones antiguas, con k bajo y tardías por construcción), no de que las empresas aprendan a registrar a tiempo.


10. Heterogeneidad (exploratorio)

¿Aprenden más rápido quienes ya llevaban registros en Excel o software, quienes tienen educación técnica o superior, o quienes hacen presupuesto? Se agrega la interacción log(k) × grupo al modelo principal. Con estas muestras, las interacciones tienen poca potencia: un resultado no significativo no prueba que no haya diferencias.

grupos <- ig %>%
  distinct(ID, Educación, `Registro de Facturas`, Presupuesto) %>%
  mutate(
    herramienta_previa = str_detect(coalesce(`Registro de Facturas`, ""),
                                    "Excel|aplicaciones|POS|software"),
    educ_superior = Educación %in% c("Técnico/tecnólogo completo", "Universitario grado completo",
                                     "Universitario grado incompleto",
                                     "Postgrado completo (Maestría/Doctorado)",
                                     "Postgrado incompleto (Maestría/Doctorado)"),
    hace_presupuesto = Presupuesto %in% "Sí"
  ) %>%
  select(ID, herramienta_previa, educ_superior, hace_presupuesto)

pm_h <- pm %>% left_join(grupos, by = "ID")

ETQ_GRUPO <- c(herramienta_previa = "Ya usaba Excel, app o software",
               educ_superior      = "Educación técnica o superior",
               hace_presupuesto   = "Hace presupuesto")

res_het <- map_dfr(c("E_nucleo", "E6"), function(v) {
  imap_dfr(ETQ_GRUPO, function(etq, g) {
    f <- as.formula(paste0(v, " ~ lk + lk:", g, " + ln + factor(ID) + factor(cal)"))
    ajustar(f, pm_h, c("lk", paste0("lk:", g, "TRUE")), etq) %>%
      mutate(error = v, n_grupo = n_distinct(pm_h$ID[pm_h[[g]]]))
  })
})

res_het %>%
  mutate(Término = if_else(termino == "lk", "Pendiente del grupo base",
                           "Diferencia de pendiente del grupo"),
         `Cambio al duplicar k` = paste0(fmt_pp(coef * log(2)), if_else(es_sig(p), " *", "")),
         `p-valor` = fmt_p(p),
         Error = ERRORES[error]) %>%
  select(Error, Grupo = Modelo, `Empresas en el grupo` = n_grupo, Término, `Cambio al duplicar k`, `p-valor`) %>%
  kable(caption = "Tabla 13. ¿Cambia el aprendizaje según el perfil de la empresa? (* p < 0,05)") %>%
  estilo(full_width = TRUE) %>%
  collapse_rows(columns = 1:3, valign = "top")
Tabla 13. ¿Cambia el aprendizaje según el perfil de la empresa? (* p < 0,05)
Error Grupo Empresas en el grupo Término Cambio al duplicar k p-valor
Núcleo: algún error E1–E4 Ya usaba Excel, app o software 43 Pendiente del grupo base -0,4 pp 0,816
Diferencia de pendiente del grupo +6,5 pp 0,177
Educación técnica o superior 151 Pendiente del grupo base -0,1 pp 0,963
Diferencia de pendiente del grupo +1,6 pp 0,562
Hace presupuesto 112 Pendiente del grupo base +0,0 pp 0,978
Diferencia de pendiente del grupo +2,3 pp 0,454
E6 Registro tardío (> 30 días) Ya usaba Excel, app o software 43 Pendiente del grupo base -7,1 pp * 0,020
Diferencia de pendiente del grupo +7,2 pp 0,188
Educación técnica o superior 151 Pendiente del grupo base -9,9 pp * 0,005
Diferencia de pendiente del grupo +7,0 pp 0,072
Hace presupuesto 112 Pendiente del grupo base -5,6 pp 0,051
Diferencia de pendiente del grupo -0,1 pp 0,981
het_sig <- res_het %>% filter(termino != "lk", es_sig(p))

Ninguna diferencia de pendiente es significativa: no hay evidencia de que algún perfil aprenda más rápido.


11. Señal exploratoria: ¿materia prima o mercancía en empresas de comercio?

Un comercio, en sentido estricto, compra mercancía para revender; la materia prima se transforma. Si las empresas de comercio aprendieran a clasificar sus costos, la proporción de compras anotadas como Materia prima debería bajar con el uso. No se trata como error, porque muchos negocios clasificados como comercio transforman lo que venden (por ejemplo, venta de comida preparada).

mp <- ig %>%
  filter(Sector == "Comercio", Tipo == "Salida", Categoría %in% c("Materia prima", "Mercancías")) %>%
  mutate(es_mp = Categoría == "Materia prima") %>%
  group_by(ID, k, cal = mes) %>%
  summarise(n = n(), es_mp = mean(es_mp), .groups = "drop") %>%
  group_by(ID) %>% mutate(nq = n()) %>% ungroup() %>%
  mutate(lk = log(k), ln = log(n))

mp_firma <- mp %>% group_by(ID) %>% summarise(mp = weighted.mean(es_mp, n))
n_solo_mp  <- sum(mp_firma$mp == 1)
n_solo_mer <- sum(mp_firma$mp == 0)
n_mixtas   <- sum(mp_firma$mp > 0 & mp_firma$mp < 1)

r_mp <- ajustar(es_mp ~ lk + ln + factor(ID) + factor(cal), filter(mp, nq >= 2), "lk",
                "k por fecha de transacción (principal)")

# Robustez: experiencia por fecha de cargue
mp_c <- ig_c %>%
  filter(Sector == "Comercio", Tipo == "Salida", Categoría %in% c("Materia prima", "Mercancías")) %>%
  mutate(es_mp = Categoría == "Materia prima") %>%
  group_by(ID, k = k_cargue, cal = mes_c) %>%
  summarise(n = n(), es_mp = mean(es_mp), .groups = "drop") %>%
  group_by(ID) %>% mutate(nq = n()) %>% ungroup() %>%
  mutate(lk = log(k), ln = log(n))
r_mp_c <- ajustar(es_mp ~ lk + ln + factor(ID) + factor(cal), filter(mp_c, nq >= 2), "lk",
                  "k por fecha de cargue")

bind_rows(r_mp, r_mp_c) %>%
  transmute(Modelo, `Cambio al duplicar k` = fmt_pp(coef * log(2)),
            `IC 95% (pp)` = fmt_ic(ic_lo * log(2), ic_hi * log(2)), `p-valor` = fmt_p(p),
            Obs. = obs, Empresas = empresas) %>%
  kable(caption = "Tabla 14. Comercio: % de compras anotadas como materia prima y experiencia (EF empresa + mes + log(n))") %>%
  estilo(full_width = FALSE)
Tabla 14. Comercio: % de compras anotadas como materia prima y experiencia (EF empresa + mes + log(n))
Modelo Cambio al duplicar k IC 95% (pp) p-valor Obs. Empresas
k por fecha de transacción (principal) -5,9 pp [-11,1; -0,8] 0,025 325 77
k por fecha de cargue -4,7 pp [-14,9; 5,4] 0,362 219 68

De las 217 empresas de comercio que registran compras, 147 las anotan siempre como materia prima, 33 siempre como mercancía y 37 usan ambas. La mayoría elige una categoría y la mantiene. Dentro de las empresas con 2 o más quincenas, la proporción anotada como materia prima cambia -5,9 pp al duplicar la experiencia (p = 0,025): un desplazamiento estadísticamente significativo hacia Mercancías. Con la experiencia medida por fecha de cargue el cambio es -4,7 pp (p = 0,362; 68 empresas). Para saber si es aprendizaje habría que confirmar con ANIF qué venden estas empresas.


12. Conclusiones

  1. Calidad de la clasificación contable: no hay evidencia de aprendizaje con el uso. En la primera quincena, en promedio 18,5% de las transacciones de una empresa tiene al menos un error E1–E4. Dentro de una misma empresa, y descontando lo que cambia para todos en el mismo mes, la tasa de error núcleo cambia +0,9 pp por cada duplicación de la experiencia (p = 0,575). Las caídas que se ven en algunas quincenas de las curvas descriptivas reflejan quiénes siguen usando la app (composición): no aparecen al comparar a cada empresa consigo misma.
  2. Hay errores que se corrigen y errores que son hábitos. El tipo de movimiento inconsistente con la categoría (E1) es poco frecuente y, en la mayoría de las empresas, puntual: 69,2% no lo repite en las 2 quincenas siguientes y 55,6% en las 4 siguientes (con pocas empresas: n = 13 y 9). Dejar el método de pago vacío (E3) o anotar montos sin los miles (E4) se repite: solo 29,2% y 32,4% no los repiten en 2 quincenas. Son la forma de registrar de cada empresa, y el uso por sí solo no la cambia.
  3. Oportunidad del registro: la mejora aparente no es robusta. En la especificación principal el registro tardío (E6) baja -5,6 pp por cada duplicación de la experiencia (p = 0,036), pero el efecto desaparece al medir la experiencia por fecha de cargue o al usar solo las transacciones posteriores al primer cargue (sección 9). Lo más probable es que refleje el cargue del historial al vincularse, no aprendizaje.
  4. Clasificación de costos en comercio: señal débil, no robusta. Entre las empresas de comercio, la proporción de compras anotadas como Materia prima (en vez de Mercancías) cambia -5,9 pp al duplicar la experiencia (p = 0,025; 77 empresas), y -4,7 pp (p = 0,362) con la experiencia medida por fecha de cargue. Va en la dirección de aprendizaje (de materia prima hacia mercancía) en ambas mediciones, pero solo es significativa en una. Además es exploratoria: no sabemos cuáles de estos comercios transforman lo que venden.
  5. No se puede evaluar la clasificación fijo/variable: el campo lo asigna la aplicación según la categoría. Tampoco el tipo de gasto en las entradas, que depende del mes de cargue.
  6. Alcance. 49,3% de las empresas usó MDF una sola quincena, y solo 65 la usaron 6 quincenas o más. Los resultados describen a las empresas que siguieron usando la herramienta y tienen poca potencia para detectar mejoras pequeñas. La atrición no parece sesgar las conclusiones (sección 8).

Respuesta corta a la pregunta. Con la información disponible no hay evidencia de apropiación contable con el uso de MDF: los errores de clasificación no bajan a medida que las empresas acumulan quincenas. El único error que la mayoría corrige es el puntual de tipo de movimiento; los errores de diligenciamiento (método de pago, montos) son persistentes. La única señal en dirección de aprendizaje, débil y no robusta, es la clasificación de compras en comercio, que tiende a desplazarse de materia prima hacia mercancía.

Implicación práctica. Si el objetivo es mejorar la educación contable, el uso de la herramienta no basta. Los errores persistentes (método de pago, montos) son candidatos naturales para validaciones en la aplicación (campo obligatorio, alerta de monto menor a $1.000) o para retroalimentación personalizada a cada empresa.


13. Exportar

write_csv(ig %>% select(ID, `Fecha de la transacción`, q_idx, k, nq, Tipo, Categoría, Monto,
                        `Método de Pago`, Cuenta, Sector, dias_cargue,
                        e1_gasto_como_entrada, e1_ingreso_como_salida, e1_arriendo_como_ingreso,
                        E1, E2, E3, E4, op_E5, E5, E6, E_nucleo),
          "errores_registro_transaccion.csv")
write_csv(panel, "errores_registro_panel_quincenal.csv")
cat("Exportados: errores_registro_transaccion.csv y errores_registro_panel_quincenal.csv\n")
## Exportados: errores_registro_transaccion.csv y errores_registro_panel_quincenal.csv

Granados Rodríguez, H. (2026). Aprendizaje contable con el uso de Mi Diario Financiero. SDDE / ODEB. Resultados exploratorios sobre usuarios activos de MDF.