Código fuente para dane_data.eam.loader

"""
Carga y armonización de microdatos EAM (un establecimiento por fila).

Entre 1992 y 2024 cambian: el separador (``;``, ``,``, tabulador), las
mayúsculas de los nombres y el nombre de la variable CIIU (``CIIU2N4``,
``ciiu2``, ``CIIU``, ``ciiu3``, ``ciiu4``, ``ciiu_4``). ``load`` deja todos los
nombres en minúscula y agrega:

* ``anio``      año de referencia.
* ``ciiu``      código CIIU (texto; normalmente 4 dígitos, pero en algunos años
                se publica a 2–3 dígitos por anonimización).
* ``ciiu_rev``  revisión de la CIIU (2, 3 o 4) según la variable de origen.
* ``division``  primeros 2 dígitos del CIIU y ``sector`` su nombre.
* ``departamento`` nombre del departamento.

Valores monetarios en **miles de pesos corrientes** (p. ej. el valor agregado
total de 2024 suma ~1,48e11, es decir ~148 billones de pesos). Para comparar
entre años hay que deflactar (``dd.eam.deflactar``).
"""

from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import Iterable

import pandas as pd

from .. import config
from .._io import read_table
from ..referencias import DEPARTAMENTOS

log = logging.getLogger("dane_data.eam")

# Formatos de los archivos de datos: CSV, TXT delimitado (2023) o Stata (2024), todos leídos con _io.read_table
EXTENSIONES = (".csv", ".txt", ".dta")

# Variables presentes en TODOS los años 1992–2024 (más algunas desde 2000).
VARIABLES_COMUNES = [
    "nordemp", "nordest", "dpto", "periodo",
    "prodbr2", "prodbind", "consin2", "valagri", "valorven", "valorcom",
    "pertotal", "persocu", "persoesc", "pertem3", "pperytem",
    "salarper", "pressper", "salpeyte", "prespyte",
    "activfi", "invebrta", "eelec",
]
VARIABLES_DESDE_2000 = ["totalv", "deprecia"]
CORE = VARIABLES_COMUNES + VARIABLES_DESDE_2000

ETIQUETAS = {
    "nordemp": "Identificador de la empresa",
    "nordest": "Identificador del establecimiento (planta)",
    "dpto": "Departamento",
    "periodo": "Año de referencia",
    "prodbr2": "Producción bruta",
    "prodbind": "Producción industrial",
    "consin2": "Consumo intermedio",
    "valagri": "Valor agregado",
    "valorven": "Ventas totales",
    "valorcom": "Compras de materias primas, materiales y empaques",
    "pertotal": "Personal total (permanente + propietarios + temporal directo + agencias)",
    "persocu": "Empleados permanentes",
    "persoesc": "Empleados permanentes + propietarios",
    "pertem3": "Empleados temporales directos",
    "pperytem": "Empleados permanentes y temporales directos",
    "salarper": "Salarios de empleados permanentes",
    "pressper": "Prestaciones de empleados permanentes",
    "salpeyte": "Salarios de empleados permanentes y temporales",
    "prespyte": "Prestaciones de empleados permanentes y temporales",
    "activfi": "Activos fijos",
    "invebrta": "Inversión bruta",
    "eelec": "Energía eléctrica consumida (kWh)",
    "totalv": "Valor de otros energéticos consumidos",
    "deprecia": "Depreciación",
}

# Nombres de división (2 dígitos) por revisión de la CIIU.
SECTORES = {
    4: {
        "10": "Alimentos", "11": "Bebidas", "12": "Tabaco", "13": "Textiles",
        "14": "Prendas de vestir", "15": "Cuero y calzado", "16": "Madera", "17": "Papel y cartón",
        "18": "Impresión", "19": "Refinación de petróleo", "20": "Químicos", "21": "Farmacéuticos",
        "22": "Caucho y plástico", "23": "Minerales no metálicos", "24": "Metalurgia básica",
        "25": "Productos de metal", "26": "Electrónicos y ópticos", "27": "Equipo eléctrico",
        "28": "Maquinaria y equipo", "29": "Vehículos automotores", "30": "Otro equipo de transporte",
        "31": "Muebles", "32": "Otras manufacturas", "33": "Reparación e instalación de maquinaria",
        "58": "Edición (publicaciones)", "59": "Cine, video y música", "73": "Publicidad",
    },
    3: {
        "15": "Alimentos y bebidas", "16": "Tabaco", "17": "Textiles", "18": "Prendas de vestir",
        "19": "Cuero y calzado", "20": "Madera", "21": "Papel y cartón", "22": "Edición e impresión",
        "23": "Refinación de petróleo", "24": "Químicos", "25": "Caucho y plástico",
        "26": "Minerales no metálicos", "27": "Metalurgia básica", "28": "Productos de metal",
        "29": "Maquinaria y equipo", "30": "Maquinaria de oficina", "31": "Aparatos eléctricos",
        "32": "Radio, TV y comunicaciones", "33": "Instrumentos médicos y de precisión",
        "34": "Vehículos automotores", "35": "Otro equipo de transporte", "36": "Muebles y otras manufacturas",
        "37": "Reciclaje",
    },
    2: {
        "31": "Alimentos, bebidas y tabaco", "32": "Textiles, confecciones y cuero",
        "33": "Madera y muebles", "34": "Papel e imprentas", "35": "Químicos, petróleo, caucho y plástico",
        "36": "Minerales no metálicos", "37": "Metálicas básicas", "38": "Productos metálicos y maquinaria",
        "39": "Otras manufacturas",
    },
}

_CIIU_COL = re.compile(r"^ciiu(_?\d)?(n\d)?$")


def _root(root) -> Path:
    return Path(root).expanduser() if root is not None else config.get_path("eam")


def _data_dir(root: Path) -> Path:
    return root / "datos" if (root / "datos").is_dir() else root


[documentos] def find_files(years: Iterable[int] | int | None = None, root=None) -> dict[int, Path]: """ Archivo de datos de cada año de la EAM descargado. Parámetros ---------- years Año o lista de años. ``None`` para todos. root Carpeta raíz. Por defecto, la configurada (``dd.get_path("eam")``). Devuelve -------- dict ``{año: ruta}``. Si un año tiene varios archivos se usa el más grande, con CSV o TXT antes que Stata; el módulo TIC se ignora. Ejemplos -------- >>> import dane_data as dd >>> dd.eam.find_files(2024) # doctest: +SKIP {2024: PosixPath('/home/ana/datos/EAM/datos/2024/EAM_ANONIMIZADA_2024.txt')} """ base = _data_dir(_root(root)) if not base.is_dir(): return {} if years is not None: years = {int(y) for y in ([years] if isinstance(years, (int, str)) else years)} out = {} for ydir in sorted(p for p in base.iterdir() if p.is_dir() and p.name.isdigit()): anio = int(ydir.name) if years is not None and anio not in years: continue files = sorted(f for f in ydir.iterdir() if f.is_file() and f.suffix.lower() in EXTENSIONES) if files: # El texto primero (un .txt convertido por una versión anterior, junto al .dta); entre # archivos del mismo tipo, el más grande. out[anio] = max(files, key=lambda f: (f.suffix.lower() != ".dta", f.stat().st_size)) return out
[documentos] def available(root=None) -> pd.DataFrame: """ Tabla de los años de la EAM descargados. Parámetros ---------- root Carpeta raíz. Por defecto, la configurada. Devuelve -------- DataFrame Una fila por año con ``anio``, ``archivo`` y ``tamano_mb``. Véase también ------------- download : Descargar lo que falta. Ejemplos -------- >>> import dane_data as dd >>> dd.eam.available().tail(3) # doctest: +SKIP anio archivo tamano_mb 30 2022 EAM_ANONIMIZADA_2022.csv 9.1 31 2023 EAM_ANONIMIZADA_2023.txt 9.0 32 2024 EAM_ANONIMIZADA_2024.txt 14.0 """ rows =[{"anio": y, "archivo": f.name, "tamano_mb": round(f.stat().st_size / 1e6, 1)} for y, f in find_files(root=root).items()] return pd.DataFrame(rows, columns=["anio", "archivo", "tamano_mb"])
def _ciiu_rev(colname: str, anio: int) -> int: m = re.search(r"ciiu_?(\d)", colname) if m: # ciiu2, ciiu3, ciiu4, ciiu_4, ciiu2n4 -> el dígito tras 'ciiu' return int(m.group(1)) # Cortes según los códigos de los archivos, no según el ANDA: su documentación dice # Rev. 3 desde 2000 y Rev. 4 desde las cifras de 2013, pero el archivo de 2000 trae # códigos Rev. 2 y el de 2012 ya trae Rev. 4 (ver README). return 2 if anio <= 2000 else 3 if anio <= 2011 else 4
[documentos] def harmonize(df: pd.DataFrame, anio: int) -> pd.DataFrame: """ Agrega las variables armonizadas a los datos de un año. Parámetros ---------- df Datos de un año de la EAM, con los nombres en minúscula. anio Año de los datos. Devuelve -------- DataFrame Una copia de ``df`` con ``anio``, ``ciiu`` (texto), ``ciiu_rev`` (2, 3 o 4), ``division`` (los dos primeros dígitos), ``sector`` (nombre de la división, de ``SECTORES``) y ``departamento``. Las columnas CIIU originales (``ciiu2``, ``ciiu3``, ``ciiu4``...) se reemplazan por ``ciiu``. Véase también ------------- load : Carga y armoniza. Notas ----- La revisión se deduce de la variable de origen (``ciiu2`` → Rev. 2, ``ciiu3`` → Rev. 3, ``ciiu4`` → Rev. 4). Algunos años publican el código a 2 o 3 dígitos por anonimización; los dos primeros son siempre la división. """ df = df.copy() ciiu_cols = [c for c in df.columns if _CIIU_COL.match(c)] if ciiu_cols: col = ciiu_cols[0] codes = pd.to_numeric(df[col], errors="coerce") rev = _ciiu_rev(col, anio) # Los nombres originales cambian cada época; quedan unificados en `ciiu`. df = df.drop(columns=ciiu_cols) # Sin ceros a la izquierda: por anonimización algunos años publican el # código a 2 o 3 dígitos (p. ej. "20", "110"); los 2 primeros siempre son la división. df["ciiu"] = codes.astype("Int64").astype("string") df["ciiu_rev"] = rev else: df["ciiu"] = pd.NA df["ciiu_rev"] = pd.NA df["division"] = df["ciiu"].str[:2] rev = df["ciiu_rev"].iloc[0] if len(df) else None df["sector"] = df["division"].map(SECTORES.get(rev, {})) if "dpto" in df.columns: df["departamento"] = pd.to_numeric(df["dpto"], errors="coerce").map(DEPARTAMENTOS) else: df["departamento"] = pd.NA df.insert(0, "anio", anio) return df
[documentos] def load( years: Iterable[int] | int | None = None, columns="core", harmonize_vars: bool = True, root=None, progress=None, ) -> pd.DataFrame: """ Carga la EAM: un establecimiento-año por fila, con las variables armonizadas. Parámetros ---------- years Año, lista o rango de años. ``None`` para todos los descargados. columns ``"core"`` (las de ``CORE``, comparables entre años; por defecto), ``"all"`` o una lista de variables extra en minúscula o mayúscula, por ejemplo ``["c3r10c1", "c4r5c1"]``. harmonize_vars Agregar ``ciiu``, ``ciiu_rev``, ``division``, ``sector`` y ``departamento``. Si False, solo ``anio``. root Carpeta raíz. Por defecto, la configurada. progress Función ``progress(fraccion, mensaje)`` que recibe el avance. Devuelve -------- DataFrame Todas las variables en minúscula. Los montos están en miles de pesos corrientes. Lanza ----- FileNotFoundError Si no hay datos para esos años. Véase también ------------- resumen : Agregados por año, sector o departamento. panel : Panel de establecimientos. ETIQUETAS : Qué es cada variable de ``CORE``. Notas ----- La CIIU cambia de revisión: Rev. 2 hasta 2000, Rev. 3 en 2001–2011 y Rev. 4 desde 2012. Los ``sector`` de revisiones distintas no son comparables sin una correlativa. Ejemplos -------- >>> import dane_data as dd >>> eam = dd.eam.load(range(2022, 2025)) # doctest: +SKIP >>> eam[["anio", "ciiu", "ciiu_rev", "division", "sector", "departamento"]].head(3) # doctest: +SKIP anio ciiu ciiu_rev division sector departamento 0 2022 1812 4 18 Impresión Bogotá D.C. 1 2022 2392 4 23 Minerales no metálicos Cundinamarca 2 2022 2429 4 24 Metalurgia básica Antioquia """ files = find_files(years, root=root) if not files: raise FileNotFoundError( f"No hay datos EAM para años={years} en {_root(root)}. " "Descárguelos con dane_data.eam.download(...) o revise dane_data.set_paths(...)." ) if isinstance(columns, str) and columns not in ("core", "all"): columns = [columns] # un solo nombre: list("valagri") lo partiría en letras if columns == "all": selector = None else: wanted = set(CORE) | ({c.lower() for c in columns} if columns not in (None, "core") else set()) selector = lambda c: c in wanted or bool(_CIIU_COL.match(c)) # noqa: E731 frames = [] for i, (anio, path) in enumerate(sorted(files.items())): if progress: progress(i / len(files), f"Leyendo EAM {anio}") df = read_table(path, columns=selector, case="lower") frames.append(harmonize(df, anio) if harmonize_vars else df.assign(anio=anio)) if progress: progress(1.0, "Listo") out = pd.concat(frames, ignore_index=True, sort=False) for c in ("nordemp", "nordest"): if c in out.columns: out[c] = pd.to_numeric(out[c], errors="coerce").astype("Int64") return out