dane-data 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dane_data/__init__.py +26 -0
- dane_data/__main__.py +7 -0
- dane_data/_hooks.py +24 -0
- dane_data/_io.py +128 -0
- dane_data/app/.streamlit/config.toml +8 -0
- dane_data/app/Inicio.py +22 -0
- dane_data/app/_ui.py +361 -0
- dane_data/app/paginas/descargar.py +101 -0
- dane_data/app/paginas/diccionario.py +62 -0
- dane_data/app/paginas/eam_industria.py +129 -0
- dane_data/app/paginas/eam_microdatos.py +56 -0
- dane_data/app/paginas/geih_explorar.py +219 -0
- dane_data/app/paginas/geih_mercado.py +152 -0
- dane_data/app/paginas/inicio.py +79 -0
- dane_data/cli.py +149 -0
- dane_data/config.py +80 -0
- dane_data/data/__init__.py +0 -0
- dane_data/data/eam_diccionario.json +3235 -0
- dane_data/data/geih_diccionario.json +8159 -0
- dane_data/eam/__init__.py +49 -0
- dane_data/eam/downloader.py +402 -0
- dane_data/eam/indicadores.py +135 -0
- dane_data/eam/loader.py +219 -0
- dane_data/geih/__init__.py +62 -0
- dane_data/geih/agregados.py +358 -0
- dane_data/geih/downloader.py +676 -0
- dane_data/geih/indicadores.py +82 -0
- dane_data/geih/loader.py +600 -0
- dane_data/referencias.py +79 -0
- dane_data/stats.py +109 -0
- dane_data-0.2.1.dist-info/METADATA +265 -0
- dane_data-0.2.1.dist-info/RECORD +36 -0
- dane_data-0.2.1.dist-info/WHEEL +5 -0
- dane_data-0.2.1.dist-info/entry_points.txt +2 -0
- dane_data-0.2.1.dist-info/licenses/LICENSE +21 -0
- dane_data-0.2.1.dist-info/top_level.txt +1 -0
dane_data/eam/loader.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Carga y armonización de microdatos EAM (un establecimiento por fila).
|
|
3
|
+
|
|
4
|
+
Entre 1992 y 2024 cambian: el separador (``;``, ``,``, tabulador), las
|
|
5
|
+
mayúsculas de los nombres y el nombre de la variable CIIU (``CIIU2N4``,
|
|
6
|
+
``ciiu2``, ``CIIU``, ``ciiu3``, ``ciiu4``, ``ciiu_4``). ``load`` deja todos los
|
|
7
|
+
nombres en minúscula y agrega:
|
|
8
|
+
|
|
9
|
+
* ``anio`` año de referencia.
|
|
10
|
+
* ``ciiu`` código CIIU (texto; normalmente 4 dígitos, pero en algunos años
|
|
11
|
+
se publica a 2–3 dígitos por anonimización).
|
|
12
|
+
* ``ciiu_rev`` revisión de la CIIU (2, 3 o 4) según la variable de origen.
|
|
13
|
+
* ``division`` primeros 2 dígitos del CIIU y ``sector`` su nombre.
|
|
14
|
+
* ``departamento`` nombre del departamento.
|
|
15
|
+
|
|
16
|
+
Valores monetarios en **miles de pesos corrientes** (p. ej. el valor agregado
|
|
17
|
+
total de 2024 suma ~1,48e11, es decir ~148 billones de pesos). Para comparar
|
|
18
|
+
entre años hay que deflactar (``dd.eam.deflactar``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import re
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Iterable
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import pandas as pd
|
|
30
|
+
|
|
31
|
+
from .. import config
|
|
32
|
+
from .._io import read_table
|
|
33
|
+
from ..referencias import DEPARTAMENTOS
|
|
34
|
+
|
|
35
|
+
log = logging.getLogger("dane_data.eam")
|
|
36
|
+
|
|
37
|
+
# Variables presentes en TODOS los años 1992–2024 (más algunas desde 2000).
|
|
38
|
+
VARIABLES_COMUNES = [
|
|
39
|
+
"nordemp", "nordest", "dpto", "periodo",
|
|
40
|
+
"prodbr2", "prodbind", "consin2", "valagri", "valorven", "valorcom",
|
|
41
|
+
"pertotal", "persocu", "persoesc", "pertem3", "pperytem",
|
|
42
|
+
"salarper", "pressper", "salpeyte", "prespyte",
|
|
43
|
+
"activfi", "invebrta", "eelec",
|
|
44
|
+
]
|
|
45
|
+
VARIABLES_DESDE_2000 = ["totalv", "deprecia"]
|
|
46
|
+
CORE = VARIABLES_COMUNES + VARIABLES_DESDE_2000
|
|
47
|
+
|
|
48
|
+
ETIQUETAS = {
|
|
49
|
+
"nordemp": "Identificador de la empresa",
|
|
50
|
+
"nordest": "Identificador del establecimiento (planta)",
|
|
51
|
+
"dpto": "Departamento",
|
|
52
|
+
"periodo": "Año de referencia",
|
|
53
|
+
"prodbr2": "Producción bruta",
|
|
54
|
+
"prodbind": "Producción industrial",
|
|
55
|
+
"consin2": "Consumo intermedio",
|
|
56
|
+
"valagri": "Valor agregado",
|
|
57
|
+
"valorven": "Ventas totales",
|
|
58
|
+
"valorcom": "Compras de materias primas, materiales y empaques",
|
|
59
|
+
"pertotal": "Personal total (permanente + propietarios + temporal directo + agencias)",
|
|
60
|
+
"persocu": "Empleados permanentes",
|
|
61
|
+
"persoesc": "Empleados permanentes + propietarios",
|
|
62
|
+
"pertem3": "Empleados temporales directos",
|
|
63
|
+
"pperytem": "Empleados permanentes y temporales directos",
|
|
64
|
+
"salarper": "Salarios de empleados permanentes",
|
|
65
|
+
"pressper": "Prestaciones de empleados permanentes",
|
|
66
|
+
"salpeyte": "Salarios de empleados permanentes y temporales",
|
|
67
|
+
"prespyte": "Prestaciones de empleados permanentes y temporales",
|
|
68
|
+
"activfi": "Activos fijos",
|
|
69
|
+
"invebrta": "Inversión bruta",
|
|
70
|
+
"eelec": "Energía eléctrica consumida (kWh)",
|
|
71
|
+
"totalv": "Valor de otros energéticos consumidos",
|
|
72
|
+
"deprecia": "Depreciación",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Nombres de división (2 dígitos) por revisión de la CIIU.
|
|
76
|
+
SECTORES = {
|
|
77
|
+
4: {
|
|
78
|
+
"10": "Alimentos", "11": "Bebidas", "12": "Tabaco", "13": "Textiles",
|
|
79
|
+
"14": "Prendas de vestir", "15": "Cuero y calzado", "16": "Madera", "17": "Papel y cartón",
|
|
80
|
+
"18": "Impresión", "19": "Refinación de petróleo", "20": "Químicos", "21": "Farmacéuticos",
|
|
81
|
+
"22": "Caucho y plástico", "23": "Minerales no metálicos", "24": "Metalurgia básica",
|
|
82
|
+
"25": "Productos de metal", "26": "Electrónicos y ópticos", "27": "Equipo eléctrico",
|
|
83
|
+
"28": "Maquinaria y equipo", "29": "Vehículos automotores", "30": "Otro equipo de transporte",
|
|
84
|
+
"31": "Muebles", "32": "Otras manufacturas", "33": "Reparación e instalación de maquinaria",
|
|
85
|
+
"58": "Edición (publicaciones)", "59": "Cine, video y música", "73": "Publicidad",
|
|
86
|
+
},
|
|
87
|
+
3: {
|
|
88
|
+
"15": "Alimentos y bebidas", "16": "Tabaco", "17": "Textiles", "18": "Prendas de vestir",
|
|
89
|
+
"19": "Cuero y calzado", "20": "Madera", "21": "Papel y cartón", "22": "Edición e impresión",
|
|
90
|
+
"23": "Refinación de petróleo", "24": "Químicos", "25": "Caucho y plástico",
|
|
91
|
+
"26": "Minerales no metálicos", "27": "Metalurgia básica", "28": "Productos de metal",
|
|
92
|
+
"29": "Maquinaria y equipo", "30": "Maquinaria de oficina", "31": "Aparatos eléctricos",
|
|
93
|
+
"32": "Radio, TV y comunicaciones", "33": "Instrumentos médicos y de precisión",
|
|
94
|
+
"34": "Vehículos automotores", "35": "Otro equipo de transporte", "36": "Muebles y otras manufacturas",
|
|
95
|
+
"37": "Reciclaje",
|
|
96
|
+
},
|
|
97
|
+
2: {
|
|
98
|
+
"31": "Alimentos, bebidas y tabaco", "32": "Textiles, confecciones y cuero",
|
|
99
|
+
"33": "Madera y muebles", "34": "Papel e imprentas", "35": "Químicos, petróleo, caucho y plástico",
|
|
100
|
+
"36": "Minerales no metálicos", "37": "Metálicas básicas", "38": "Productos metálicos y maquinaria",
|
|
101
|
+
"39": "Otras manufacturas",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_CIIU_COL = re.compile(r"^ciiu(_?\d)?(n\d)?$")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _root(root) -> Path:
|
|
109
|
+
return Path(root).expanduser() if root is not None else config.get_path("eam")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _data_dir(root: Path) -> Path:
|
|
113
|
+
return root / "datos" if (root / "datos").is_dir() else root
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def find_files(years: Iterable[int] | int | None = None, root=None) -> dict[int, Path]:
|
|
117
|
+
"""``{año: archivo}`` de los datos EAM descargados (ignora el módulo TIC)."""
|
|
118
|
+
base = _data_dir(_root(root))
|
|
119
|
+
if not base.is_dir():
|
|
120
|
+
return {}
|
|
121
|
+
if years is not None:
|
|
122
|
+
years = {int(y) for y in ([years] if isinstance(years, (int, str)) else years)}
|
|
123
|
+
out = {}
|
|
124
|
+
for ydir in sorted(p for p in base.iterdir() if p.is_dir() and p.name.isdigit()):
|
|
125
|
+
anio = int(ydir.name)
|
|
126
|
+
if years is not None and anio not in years:
|
|
127
|
+
continue
|
|
128
|
+
files = sorted(f for f in ydir.iterdir() if f.is_file() and f.suffix.lower() in (".csv", ".txt"))
|
|
129
|
+
if files:
|
|
130
|
+
out[anio] = max(files, key=lambda f: f.stat().st_size)
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def available(root=None) -> pd.DataFrame:
|
|
135
|
+
rows = [{"anio": y, "archivo": f.name, "tamano_mb": round(f.stat().st_size / 1e6, 1)}
|
|
136
|
+
for y, f in find_files(root=root).items()]
|
|
137
|
+
return pd.DataFrame(rows, columns=["anio", "archivo", "tamano_mb"])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _ciiu_rev(colname: str, anio: int) -> int:
|
|
141
|
+
m = re.search(r"ciiu_?(\d)", colname)
|
|
142
|
+
if m: # ciiu2, ciiu3, ciiu4, ciiu_4, ciiu2n4 -> el dígito tras 'ciiu'
|
|
143
|
+
return int(m.group(1))
|
|
144
|
+
# Cortes según los códigos de los archivos, no según el ANDA: su documentación dice
|
|
145
|
+
# Rev. 3 desde 2000 y Rev. 4 desde las cifras de 2013, pero el archivo de 2000 trae
|
|
146
|
+
# códigos Rev. 2 y el de 2012 ya trae Rev. 4 (ver README).
|
|
147
|
+
return 2 if anio <= 2000 else 3 if anio <= 2011 else 4
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def harmonize(df: pd.DataFrame, anio: int) -> pd.DataFrame:
|
|
151
|
+
df = df.copy()
|
|
152
|
+
ciiu_cols = [c for c in df.columns if _CIIU_COL.match(c)]
|
|
153
|
+
if ciiu_cols:
|
|
154
|
+
col = ciiu_cols[0]
|
|
155
|
+
codes = pd.to_numeric(df[col], errors="coerce")
|
|
156
|
+
rev = _ciiu_rev(col, anio)
|
|
157
|
+
# Los nombres originales cambian cada época; quedan unificados en `ciiu`.
|
|
158
|
+
df = df.drop(columns=ciiu_cols)
|
|
159
|
+
# Sin ceros a la izquierda: por anonimización algunos años publican el
|
|
160
|
+
# código a 2 o 3 dígitos (p. ej. "20", "110"); los 2 primeros siempre son la división.
|
|
161
|
+
df["ciiu"] = codes.astype("Int64").astype("string")
|
|
162
|
+
df["ciiu_rev"] = rev
|
|
163
|
+
else:
|
|
164
|
+
df["ciiu"] = pd.NA
|
|
165
|
+
df["ciiu_rev"] = pd.NA
|
|
166
|
+
df["division"] = df["ciiu"].str[:2]
|
|
167
|
+
rev = df["ciiu_rev"].iloc[0] if len(df) else None
|
|
168
|
+
df["sector"] = df["division"].map(SECTORES.get(rev, {}))
|
|
169
|
+
dpto = pd.to_numeric(df.get("dpto"), errors="coerce")
|
|
170
|
+
df["departamento"] = dpto.map(DEPARTAMENTOS) if dpto is not None else pd.NA
|
|
171
|
+
df.insert(0, "anio", anio)
|
|
172
|
+
return df
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def load(
|
|
176
|
+
years: Iterable[int] | int | None = None,
|
|
177
|
+
columns="core",
|
|
178
|
+
harmonize_vars: bool = True,
|
|
179
|
+
root=None,
|
|
180
|
+
progress=None,
|
|
181
|
+
) -> pd.DataFrame:
|
|
182
|
+
"""
|
|
183
|
+
Carga la EAM (un establecimiento-año por fila).
|
|
184
|
+
|
|
185
|
+
Parámetros
|
|
186
|
+
----------
|
|
187
|
+
years : año, lista/rango de años o None (todos los disponibles).
|
|
188
|
+
columns : ``"core"`` (variables comparables entre años, por defecto),
|
|
189
|
+
``"all"`` o lista de variables extra (minúscula o mayúscula),
|
|
190
|
+
p. ej. ``["c3r10c1", "c4r5c1"]``.
|
|
191
|
+
|
|
192
|
+
>>> eam = dd.eam.load(range(2014, 2025))
|
|
193
|
+
>>> dd.eam.resumen(eam, por=["anio", "sector"])
|
|
194
|
+
"""
|
|
195
|
+
files = find_files(years, root=root)
|
|
196
|
+
if not files:
|
|
197
|
+
raise FileNotFoundError(
|
|
198
|
+
f"No hay datos EAM para años={years} en {_root(root)}. "
|
|
199
|
+
"Descárguelos con dane_data.eam.download(...) o revise dane_data.set_paths(...)."
|
|
200
|
+
)
|
|
201
|
+
if columns == "all":
|
|
202
|
+
selector = None
|
|
203
|
+
else:
|
|
204
|
+
wanted = set(CORE) | ({c.lower() for c in columns} if columns not in (None, "core") else set())
|
|
205
|
+
selector = lambda c: c in wanted or bool(_CIIU_COL.match(c)) # noqa: E731
|
|
206
|
+
|
|
207
|
+
frames = []
|
|
208
|
+
for i, (anio, path) in enumerate(sorted(files.items())):
|
|
209
|
+
if progress:
|
|
210
|
+
progress(i / len(files), f"Leyendo EAM {anio}")
|
|
211
|
+
df = read_table(path, columns=selector, case="lower")
|
|
212
|
+
frames.append(harmonize(df, anio) if harmonize_vars else df.assign(anio=anio))
|
|
213
|
+
if progress:
|
|
214
|
+
progress(1.0, "Listo")
|
|
215
|
+
out = pd.concat(frames, ignore_index=True, sort=False)
|
|
216
|
+
for c in ("nordemp", "nordest"):
|
|
217
|
+
if c in out.columns:
|
|
218
|
+
out[c] = pd.to_numeric(out[c], errors="coerce").astype("Int64")
|
|
219
|
+
return out
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Gran Encuesta Integrada de Hogares (GEIH) — mercado laboral, microdatos de personas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from .. import config
|
|
8
|
+
from .downloader import (DEFAULT_MODULES, EMPALME_CATALOGS, KNOWN_CATALOGS, MODULE_PATTERNS,
|
|
9
|
+
GEIHDownloader)
|
|
10
|
+
from .agregados import (POBLACIONES, POR_PRECALCULADOS, VARIABLES_INDICADORES, exportar, filtrar, serie_frecuencias,
|
|
11
|
+
serie_indicadores, serie_tabla, tabla_de_frecuencias)
|
|
12
|
+
from .indicadores import GLOSARIO, indicadores
|
|
13
|
+
from .loader import (CATEGORIAS, CONDICIONES, CORE_RAW, GRUPOS_EDAD, MARCOS, MODULOS, NIVELES_EDUC, available,
|
|
14
|
+
clear_cache, find_months, harmonize, load, load_month, marco_muestral)
|
|
15
|
+
|
|
16
|
+
ANIOS_DISPONIBLES = sorted(int(y) for y in KNOWN_CATALOGS)
|
|
17
|
+
ANIOS_EMPALME = sorted(int(y) for y in EMPALME_CATALOGS)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def download(
|
|
21
|
+
years: Iterable[int] | int | None = None,
|
|
22
|
+
modules: Iterable[str] | None = None,
|
|
23
|
+
empalme: bool = False,
|
|
24
|
+
root=None,
|
|
25
|
+
delete_zips: bool = False,
|
|
26
|
+
limit_files: int | None = None,
|
|
27
|
+
progress=None,
|
|
28
|
+
should_stop=None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Descarga microdatos GEIH del ANDA (DANE) a la carpeta configurada.
|
|
32
|
+
|
|
33
|
+
>>> dd.geih.download([2024, 2025]) # serie principal
|
|
34
|
+
>>> dd.geih.download(2016, empalme=True) # serie de empalme
|
|
35
|
+
>>> dd.geih.download(2025, limit_files=1) # solo enero (prueba)
|
|
36
|
+
|
|
37
|
+
``modules`` acepta: vivienda, caracteristicas, fuerza_trabajo, ocupados,
|
|
38
|
+
desocupados, inactivos, otras_formas_trabajo (por defecto todos).
|
|
39
|
+
Es incremental: los meses ya extraídos se saltan.
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(years, (int, str)):
|
|
42
|
+
years = [years]
|
|
43
|
+
root = root or config.get_path("geih_empalme" if empalme else "geih")
|
|
44
|
+
dl = GEIHDownloader(
|
|
45
|
+
base_dir=root,
|
|
46
|
+
keep_zips=not delete_zips,
|
|
47
|
+
extract_csv=True,
|
|
48
|
+
is_empalme=empalme,
|
|
49
|
+
modules=modules,
|
|
50
|
+
progress=progress,
|
|
51
|
+
should_stop=should_stop,
|
|
52
|
+
)
|
|
53
|
+
dl.run_pipeline(target_years=years, limit_files=limit_files)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"download", "load", "load_month", "available", "find_months", "harmonize", "indicadores",
|
|
58
|
+
"serie_indicadores", "serie_tabla", "serie_frecuencias", "tabla_de_frecuencias", "exportar", "filtrar",
|
|
59
|
+
"POBLACIONES", "marco_muestral", "MARCOS", "clear_cache", "GLOSARIO", "MODULOS", "CORE_RAW", "CONDICIONES", "NIVELES_EDUC",
|
|
60
|
+
"GRUPOS_EDAD", "CATEGORIAS", "VARIABLES_INDICADORES", "POR_PRECALCULADOS",
|
|
61
|
+
"ANIOS_DISPONIBLES", "ANIOS_EMPALME", "MODULE_PATTERNS", "DEFAULT_MODULES",
|
|
62
|
+
]
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resultados de la GEIH calculados año por año y guardados en disco.
|
|
3
|
+
|
|
4
|
+
Las funciones ``serie_*`` dan lo mismo que cargar todos los años con ``load`` y calcular
|
|
5
|
+
encima, pero leen un año a la vez: la memoria no depende de cuántos años se pidan. Cada
|
|
6
|
+
año queda en ``~/.cache/dane_data/geih/<tipo>/`` y se recalcula solo si cambian sus
|
|
7
|
+
archivos, la solicitud (filtros, meses, variables) o el código de dane-data. ``exportar``
|
|
8
|
+
escribe los microdatos armonizados mes a mes, por la misma razón.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable, Iterable
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from .. import config
|
|
25
|
+
from ..referencias import MESES
|
|
26
|
+
from ..stats import _as_list, peso_mensual_promedio, weighted_quantile
|
|
27
|
+
from .indicadores import _COLUMNAS, indicadores
|
|
28
|
+
from .loader import _HARMONIZE_VERSION, CATEGORIAS, MesGEIH, _firma_archivos, _root, find_months, load
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger("dane_data.geih")
|
|
31
|
+
|
|
32
|
+
# Lo que lee `indicadores` más las desagregaciones y filtros de la app.
|
|
33
|
+
VARIABLES_INDICADORES = ["anio", "mes", "fex", "pet", "fuerza_trabajo", "ocupado", "desocupado", "inactivo",
|
|
34
|
+
"formal", "ingreso_laboral", "sexo", "zona", "edad", "grupo_edad", "nivel_educativo",
|
|
35
|
+
"departamento"]
|
|
36
|
+
POR_PRECALCULADOS = (None, "sexo", "zona", "grupo_edad", "nivel_educativo", "departamento")
|
|
37
|
+
POBLACIONES = ("pet", "fuerza_trabajo", "ocupado", "desocupado", "inactivo")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def filtrar(df: pd.DataFrame, filtros: dict | None = None, poblacion: str | None = None) -> pd.DataFrame:
|
|
41
|
+
"""
|
|
42
|
+
Filas que cumplen ``filtros`` y pertenecen a ``poblacion``.
|
|
43
|
+
|
|
44
|
+
``filtros``: ``{columna: [valores]}``, o ``{columna: (mínimo, máximo)}`` para un rango
|
|
45
|
+
(con los extremos), p. ej. ``{"sexo": ["Mujer"], "edad": (15, 28)}``. Si la columna no
|
|
46
|
+
existe (una variable que ese año no trae), nadie cumple el filtro.
|
|
47
|
+
``poblacion``: una de ``POBLACIONES`` (``"ocupado"``, ``"pet"``...).
|
|
48
|
+
"""
|
|
49
|
+
mask = pd.Series(True, index=df.index)
|
|
50
|
+
for col, cond in (filtros or {}).items():
|
|
51
|
+
if col not in df.columns:
|
|
52
|
+
mask &= False
|
|
53
|
+
elif isinstance(cond, tuple):
|
|
54
|
+
mask &= df[col].between(*cond)
|
|
55
|
+
else:
|
|
56
|
+
mask &= df[col].isin(list(cond))
|
|
57
|
+
if poblacion:
|
|
58
|
+
mask &= df[poblacion].eq(1).fillna(False).astype(bool)
|
|
59
|
+
return df if mask.all() else df[mask]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# --------------------------------------------------------------------------- #
|
|
63
|
+
# Caché por año
|
|
64
|
+
# --------------------------------------------------------------------------- #
|
|
65
|
+
@lru_cache(maxsize=1)
|
|
66
|
+
def _firma_codigo() -> str:
|
|
67
|
+
"""Hash del código que produce los resultados: si cambia, la caché se recalcula sola."""
|
|
68
|
+
from .. import __version__
|
|
69
|
+
|
|
70
|
+
base = Path(__file__).resolve().parent.parent
|
|
71
|
+
h = hashlib.sha1(__version__.encode())
|
|
72
|
+
for rel in ("geih/loader.py", "geih/indicadores.py", "geih/agregados.py", "stats.py", "referencias.py",
|
|
73
|
+
"_io.py"):
|
|
74
|
+
try:
|
|
75
|
+
h.update((base / rel).read_bytes())
|
|
76
|
+
except OSError:
|
|
77
|
+
pass
|
|
78
|
+
return h.hexdigest()[:16]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _meses(meses) -> list[int] | None:
|
|
82
|
+
if meses is None:
|
|
83
|
+
return None
|
|
84
|
+
return sorted({int(m) for m in ([meses] if isinstance(meses, (int, str)) else meses)})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _meses_por_anio(anios, meses, empalme: bool, root) -> dict[int, list[MesGEIH]]:
|
|
88
|
+
"""Meses completos en disco (los que usa ``load``), agrupados por año."""
|
|
89
|
+
meses = _meses(meses)
|
|
90
|
+
por_anio: dict[int, list[MesGEIH]] = {}
|
|
91
|
+
for m in find_months(anios, empalme=empalme, root=root):
|
|
92
|
+
if m.completo and (meses is None or m.mes in meses):
|
|
93
|
+
por_anio.setdefault(m.anio, []).append(m)
|
|
94
|
+
if not por_anio:
|
|
95
|
+
raise FileNotFoundError(
|
|
96
|
+
f"No hay datos GEIH para años={anios}, meses={meses} en {_root(empalme, root)}. "
|
|
97
|
+
"Descárguelos con dane_data.geih.download(...) o revise dane_data.set_paths(...)."
|
|
98
|
+
)
|
|
99
|
+
return por_anio
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _por_anio(tipo: str, anios, solicitud: dict, calcular: Callable[[int], pd.DataFrame], *, meses=None,
|
|
103
|
+
empalme: bool = False, root=None, progress=None) -> list[pd.DataFrame]:
|
|
104
|
+
"""``calcular(anio)`` para cada año, leído de la caché si ya se hizo con los mismos archivos y solicitud."""
|
|
105
|
+
por_anio = _meses_por_anio(anios, meses, empalme, root)
|
|
106
|
+
texto = json.dumps(solicitud, sort_keys=True, default=str, ensure_ascii=False)
|
|
107
|
+
serie = "empalme" if empalme else "principal"
|
|
108
|
+
partes = []
|
|
109
|
+
for i, (anio, ms) in enumerate(sorted(por_anio.items())):
|
|
110
|
+
firma = [_HARMONIZE_VERSION, _firma_codigo(), texto, *(s for m in ms for s in _firma_archivos(m))]
|
|
111
|
+
digest = hashlib.sha1("|".join(firma).encode()).hexdigest()[:16]
|
|
112
|
+
ruta = config.CACHE_DIR / "geih" / tipo / f"{serie}_{anio}_{digest}.parquet"
|
|
113
|
+
tabla = None
|
|
114
|
+
if ruta.exists():
|
|
115
|
+
try:
|
|
116
|
+
tabla = pd.read_parquet(ruta)
|
|
117
|
+
except Exception: # caché corrupta: se regenera
|
|
118
|
+
tabla = None
|
|
119
|
+
if tabla is None:
|
|
120
|
+
if progress:
|
|
121
|
+
progress(i / len(por_anio), f"Calculando {anio} (solo la primera vez)")
|
|
122
|
+
tabla = calcular(anio)
|
|
123
|
+
try:
|
|
124
|
+
ruta.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
tmp = ruta.with_name(f"{ruta.name}.{os.getpid()}.tmp") # varias sesiones de la app a la vez
|
|
126
|
+
tabla.to_parquet(tmp, index=False)
|
|
127
|
+
tmp.replace(ruta)
|
|
128
|
+
except Exception as e: # disco lleno, sin permisos, etc.
|
|
129
|
+
log.debug(f"No se pudo guardar caché {ruta}: {e}")
|
|
130
|
+
partes.append(tabla)
|
|
131
|
+
if progress:
|
|
132
|
+
progress(1.0, "Listo")
|
|
133
|
+
return partes
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --------------------------------------------------------------------------- #
|
|
137
|
+
# Indicadores laborales
|
|
138
|
+
# --------------------------------------------------------------------------- #
|
|
139
|
+
def serie_indicadores(
|
|
140
|
+
anios: Iterable[int] | int,
|
|
141
|
+
por: str | None = None,
|
|
142
|
+
anual: bool = True,
|
|
143
|
+
empalme: bool = False,
|
|
144
|
+
root=None,
|
|
145
|
+
progress=None,
|
|
146
|
+
filtros: dict | None = None,
|
|
147
|
+
meses: Iterable[int] | int | None = None,
|
|
148
|
+
) -> pd.DataFrame:
|
|
149
|
+
"""
|
|
150
|
+
Lo mismo que ``indicadores(filtrar(load(anios, months=meses), filtros), por=por, anual=anual)``,
|
|
151
|
+
pero sin juntar los microdatos de todos los años en memoria.
|
|
152
|
+
|
|
153
|
+
Cada año se calcula por separado (con todas las desagregaciones de
|
|
154
|
+
``POR_PRECALCULADOS``, anual y mensual) y se guarda. La primera vez tarda unos
|
|
155
|
+
segundos por año; después es instantáneo, también al cambiar ``por`` o ``anual``.
|
|
156
|
+
Para desagregar por otras variables use ``indicadores`` sobre
|
|
157
|
+
``load(..., variables=VARIABLES_INDICADORES)``.
|
|
158
|
+
|
|
159
|
+
>>> dd.geih.serie_indicadores(range(2007, 2026)) # TD, TGP... por año
|
|
160
|
+
>>> dd.geih.serie_indicadores(range(2021, 2026), por="sexo", anual=False)
|
|
161
|
+
>>> dd.geih.serie_indicadores(range(2015, 2026), filtros={"edad": (15, 28)}) # jóvenes
|
|
162
|
+
"""
|
|
163
|
+
if por not in POR_PRECALCULADOS:
|
|
164
|
+
raise ValueError(f"por={por!r} no está precalculado (opciones: {list(POR_PRECALCULADOS)}). Use "
|
|
165
|
+
"dd.geih.indicadores(dd.geih.load(..., variables=dd.geih.VARIABLES_INDICADORES), por=...).")
|
|
166
|
+
variables = list(dict.fromkeys([*VARIABLES_INDICADORES, *(filtros or {})]))
|
|
167
|
+
|
|
168
|
+
def calcular(anio):
|
|
169
|
+
df = filtrar(load(anio, months=meses, empalme=empalme, root=root, variables=variables), filtros)
|
|
170
|
+
partes = []
|
|
171
|
+
for p in POR_PRECALCULADOS:
|
|
172
|
+
for a in (True, False):
|
|
173
|
+
r = indicadores(df, por=p, anual=a)
|
|
174
|
+
if p:
|
|
175
|
+
r = r.rename(columns={p: "grupo"}).astype({"grupo": object})
|
|
176
|
+
partes.append(r.assign(desagregacion=p or "total", anual=a))
|
|
177
|
+
return pd.concat(partes, ignore_index=True)
|
|
178
|
+
|
|
179
|
+
tablas = _por_anio("indicadores", anios, {"filtros": filtros or {}, "meses": _meses(meses)}, calcular,
|
|
180
|
+
meses=meses, empalme=empalme, root=root, progress=progress)
|
|
181
|
+
res = pd.concat([t[(t["desagregacion"] == (por or "total")) & (t["anual"] == anual)] for t in tablas],
|
|
182
|
+
ignore_index=True)
|
|
183
|
+
keys = ["anio"] + ([] if anual else ["mes"])
|
|
184
|
+
if not anual:
|
|
185
|
+
res["mes"] = res["mes"].astype("int64")
|
|
186
|
+
if por:
|
|
187
|
+
res[por] = res["grupo"].astype(CATEGORIAS[por])
|
|
188
|
+
keys.append(por)
|
|
189
|
+
return res[keys + _COLUMNAS]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# --------------------------------------------------------------------------- #
|
|
193
|
+
# Tablas ponderadas
|
|
194
|
+
# --------------------------------------------------------------------------- #
|
|
195
|
+
def serie_frecuencias(
|
|
196
|
+
anios: Iterable[int] | int,
|
|
197
|
+
variable: str | None = None,
|
|
198
|
+
por: str | Iterable[str] | None = None,
|
|
199
|
+
poblacion: str | None = None,
|
|
200
|
+
filtros: dict | None = None,
|
|
201
|
+
meses: Iterable[int] | int | None = None,
|
|
202
|
+
columns="core",
|
|
203
|
+
empalme: bool = False,
|
|
204
|
+
root=None,
|
|
205
|
+
progress=None,
|
|
206
|
+
) -> pd.DataFrame:
|
|
207
|
+
"""
|
|
208
|
+
Distribución ponderada: una fila por año, combinación de ``por`` y valor de ``variable``,
|
|
209
|
+
con ``peso`` (personas, promedio mensual de cada año) y ``n_muestra``.
|
|
210
|
+
|
|
211
|
+
Es la base de ``serie_tabla``: de ella salen totales, porcentajes, medias y medianas
|
|
212
|
+
exactas aunque se junten varios años. ``columns`` son las variables DANE extra que
|
|
213
|
+
hay que leer (como en ``load``).
|
|
214
|
+
|
|
215
|
+
>>> f = dd.geih.serie_frecuencias(range(2015, 2026), "ingreso_laboral", poblacion="ocupado")
|
|
216
|
+
>>> dd.weighted_quantile(f["ingreso_laboral"], f["peso"], 0.5) # mediana de todos esos años
|
|
217
|
+
"""
|
|
218
|
+
por = [p for p in _as_list(por) if p != "anio"]
|
|
219
|
+
claves = ["anio", *por] + ([variable] if variable else [])
|
|
220
|
+
necesarias = list(dict.fromkeys(["fex", *claves, *(filtros or {}), *([poblacion] if poblacion else [])]))
|
|
221
|
+
|
|
222
|
+
def calcular(anio):
|
|
223
|
+
df = load(anio, months=meses, columns=columns, empalme=empalme, root=root, variables=necesarias)
|
|
224
|
+
for c in claves: # una variable DANE que ese año no existe
|
|
225
|
+
if c not in df.columns:
|
|
226
|
+
df[c] = np.nan
|
|
227
|
+
d = filtrar(df, filtros, poblacion)
|
|
228
|
+
x = d[claves].assign(peso=peso_mensual_promedio(d).to_numpy(), n_muestra=1)
|
|
229
|
+
if variable:
|
|
230
|
+
x = x[x[variable].notna()]
|
|
231
|
+
return x.groupby(claves, observed=True, dropna=False).agg(
|
|
232
|
+
peso=("peso", "sum"), n_muestra=("n_muestra", "sum")).reset_index()
|
|
233
|
+
|
|
234
|
+
solicitud = {"variable": variable, "por": sorted(por), "poblacion": poblacion, "filtros": filtros or {},
|
|
235
|
+
"meses": _meses(meses), "columns": columns}
|
|
236
|
+
tablas = _por_anio("frecuencias", anios, solicitud, calcular, meses=meses, empalme=empalme, root=root,
|
|
237
|
+
progress=progress)
|
|
238
|
+
return pd.concat(tablas, ignore_index=True)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def serie_tabla(
|
|
242
|
+
anios: Iterable[int] | int,
|
|
243
|
+
por: str | Iterable[str] | None = None,
|
|
244
|
+
variable: str | None = None,
|
|
245
|
+
estadistico: str = "total",
|
|
246
|
+
poblacion: str | None = None,
|
|
247
|
+
filtros: dict | None = None,
|
|
248
|
+
meses: Iterable[int] | int | None = None,
|
|
249
|
+
dropna: bool = False,
|
|
250
|
+
columns="core",
|
|
251
|
+
empalme: bool = False,
|
|
252
|
+
root=None,
|
|
253
|
+
progress=None,
|
|
254
|
+
) -> pd.DataFrame:
|
|
255
|
+
"""
|
|
256
|
+
Lo mismo que ``dd.tabla(filtrar(load(anios, months=meses), filtros, poblacion), por, variable,
|
|
257
|
+
estadistico)``, calculado año por año y guardado en disco.
|
|
258
|
+
|
|
259
|
+
``dropna=True`` deja fuera a las personas con valor vacío en alguna variable de ``por``
|
|
260
|
+
(p. ej. la rama de actividad de quien no trabaja).
|
|
261
|
+
|
|
262
|
+
>>> dd.geih.serie_tabla(range(2015, 2026), por=["anio", "nivel_educativo"], estadistico="porcentaje",
|
|
263
|
+
... poblacion="ocupado")
|
|
264
|
+
>>> dd.geih.serie_tabla([2024, 2025], por="sexo", variable="ingreso_laboral", estadistico="mediana",
|
|
265
|
+
... filtros={"zona": ["Urbano (cabecera)"]})
|
|
266
|
+
"""
|
|
267
|
+
por = _as_list(por)
|
|
268
|
+
if estadistico not in ("total", "porcentaje", "media", "mediana"):
|
|
269
|
+
raise ValueError("estadistico debe ser 'total', 'porcentaje', 'media' o 'mediana'.")
|
|
270
|
+
if estadistico in ("media", "mediana") and variable is None:
|
|
271
|
+
raise ValueError("Indique `variable` para media/mediana.")
|
|
272
|
+
f = serie_frecuencias(anios, variable, por, poblacion, filtros, meses, columns, empalme, root, progress)
|
|
273
|
+
if dropna and por:
|
|
274
|
+
f = f.dropna(subset=por)
|
|
275
|
+
return tabla_de_frecuencias(f, por, variable, estadistico)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def tabla_de_frecuencias(f: pd.DataFrame, por: str | Iterable[str] | None = None, variable: str | None = None,
|
|
279
|
+
estadistico: str = "total") -> pd.DataFrame:
|
|
280
|
+
"""Tabla de ``dd.tabla`` a partir de las frecuencias ponderadas de ``serie_frecuencias``."""
|
|
281
|
+
por = _as_list(por)
|
|
282
|
+
if estadistico in ("total", "porcentaje"):
|
|
283
|
+
if not por:
|
|
284
|
+
res = pd.DataFrame({"total": [f["peso"].sum()], "n_muestra": [int(f["n_muestra"].sum())]})
|
|
285
|
+
else:
|
|
286
|
+
res = f.groupby(por, observed=True, dropna=False).agg(
|
|
287
|
+
total=("peso", "sum"), n_muestra=("n_muestra", "sum")).reset_index()
|
|
288
|
+
if estadistico == "porcentaje":
|
|
289
|
+
outer = por[:-1]
|
|
290
|
+
denom = res.groupby(outer, observed=True)["total"].transform("sum") if outer else res["total"].sum()
|
|
291
|
+
res["porcentaje"] = 100 * res["total"] / denom
|
|
292
|
+
return res
|
|
293
|
+
|
|
294
|
+
if estadistico == "media":
|
|
295
|
+
def fn(x):
|
|
296
|
+
return float(np.average(x[variable], weights=x["peso"])) if x["peso"].sum() > 0 else np.nan
|
|
297
|
+
else:
|
|
298
|
+
def fn(x):
|
|
299
|
+
return weighted_quantile(x[variable], x["peso"], 0.5)
|
|
300
|
+
if not por:
|
|
301
|
+
return pd.DataFrame({estadistico: [fn(f)], "n_muestra": [int(f["n_muestra"].sum())]})
|
|
302
|
+
g = f.groupby(por, observed=True, dropna=False)
|
|
303
|
+
stat = g[[variable, "peso"]].apply(fn).rename(estadistico)
|
|
304
|
+
n = g["n_muestra"].sum().rename("n_muestra")
|
|
305
|
+
return pd.concat([stat, n], axis=1).reset_index()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# --------------------------------------------------------------------------- #
|
|
309
|
+
# Exportar microdatos
|
|
310
|
+
# --------------------------------------------------------------------------- #
|
|
311
|
+
def exportar(
|
|
312
|
+
anios: Iterable[int] | int,
|
|
313
|
+
ruta,
|
|
314
|
+
variables: Iterable[str] | None = None,
|
|
315
|
+
poblacion: str | None = None,
|
|
316
|
+
filtros: dict | None = None,
|
|
317
|
+
meses: Iterable[int] | int | None = None,
|
|
318
|
+
columns="core",
|
|
319
|
+
empalme: bool = False,
|
|
320
|
+
root=None,
|
|
321
|
+
progress=None,
|
|
322
|
+
) -> int:
|
|
323
|
+
"""
|
|
324
|
+
Escribe en ``ruta`` (CSV) los microdatos armonizados, una persona por fila, un mes a la vez:
|
|
325
|
+
sirve para muchos años aunque no quepan en memoria. Devuelve el número de filas.
|
|
326
|
+
|
|
327
|
+
``variables``: columnas a escribir (por defecto, las armonizadas). Las variables DANE
|
|
328
|
+
que no están en ``CORE_RAW`` deben pedirse también en ``columns``.
|
|
329
|
+
|
|
330
|
+
>>> dd.geih.exportar(range(2015, 2026), "ocupados.csv", poblacion="ocupado")
|
|
331
|
+
"""
|
|
332
|
+
ruta = Path(ruta)
|
|
333
|
+
por_anio = _meses_por_anio(anios, meses, empalme, root)
|
|
334
|
+
lista = [m for _, ms in sorted(por_anio.items()) for m in ms]
|
|
335
|
+
necesarias = None
|
|
336
|
+
if variables is not None:
|
|
337
|
+
necesarias = list(dict.fromkeys(["anio", "mes", *variables]))
|
|
338
|
+
leer = list(dict.fromkeys([*(necesarias or []), *(filtros or {}), *([poblacion] if poblacion else [])])) or None
|
|
339
|
+
ruta.parent.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
tmp = ruta.with_name(f"{ruta.name}.{os.getpid()}.tmp")
|
|
341
|
+
filas = 0
|
|
342
|
+
try:
|
|
343
|
+
for i, m in enumerate(lista):
|
|
344
|
+
if progress:
|
|
345
|
+
progress(i / len(lista), f"Exportando {MESES[m.mes]} {m.anio}")
|
|
346
|
+
df = filtrar(load(m.anio, months=m.mes, columns=columns, empalme=empalme, root=root, variables=leer),
|
|
347
|
+
filtros, poblacion)
|
|
348
|
+
if necesarias is None: # las armonizadas (minúscula), iguales en todos los meses
|
|
349
|
+
necesarias = [c for c in df.columns if c == c.lower()]
|
|
350
|
+
df.reindex(columns=necesarias).to_csv(tmp, mode="w" if i == 0 else "a", header=i == 0, index=False,
|
|
351
|
+
encoding="utf-8-sig" if i == 0 else "utf-8")
|
|
352
|
+
filas += len(df)
|
|
353
|
+
tmp.replace(ruta)
|
|
354
|
+
finally:
|
|
355
|
+
tmp.unlink(missing_ok=True)
|
|
356
|
+
if progress:
|
|
357
|
+
progress(1.0, "Listo")
|
|
358
|
+
return filas
|