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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import streamlit as st
|
|
3
|
+
|
|
4
|
+
import dane_data as dd
|
|
5
|
+
from dane_data.app._ui import (fmt_num, MAX_SERIES, args_py, barras, botones_descarga, codigo, fmt_grande, fmt_pct,
|
|
6
|
+
fmt_pesos, grafico, linea, rutas_actuales, seleccion_geih, serie_geih)
|
|
7
|
+
|
|
8
|
+
st.title("👥 Indicadores del mercado laboral")
|
|
9
|
+
sel = seleccion_geih(clave="geih")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def calcular(por, anual):
|
|
13
|
+
"""Indicadores año por año, guardados en disco (con los filtros y meses de la barra lateral)."""
|
|
14
|
+
return serie_geih(tuple(sel["anios"]), por, anual, sel["empalme"], sel["filtros"],
|
|
15
|
+
tuple(sel["meses"]) if sel["meses"] else None, sel["meses_disponibles"], rutas_actuales())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
total = calcular(None, True)
|
|
19
|
+
if total.empty or not total["n_muestra"].sum():
|
|
20
|
+
st.warning("Ninguna persona cumple los filtros elegidos.")
|
|
21
|
+
st.stop()
|
|
22
|
+
mensual = calcular(None, False)
|
|
23
|
+
meses_ult = int((mensual["anio"] == total["anio"].iloc[-1]).sum())
|
|
24
|
+
st.caption(f"**Datos:** {sel['descripcion']} · {fmt_num(total['n_muestra'].sum())} personas en la muestra")
|
|
25
|
+
|
|
26
|
+
INDICADORES = {
|
|
27
|
+
"Tasa de desocupación (TD)": ("td", True),
|
|
28
|
+
"Tasa global de participación (TGP)": ("tgp", True),
|
|
29
|
+
"Tasa de ocupación (TO)": ("to", True),
|
|
30
|
+
"Tasa de informalidad": ("tasa_informalidad", True),
|
|
31
|
+
"Ingreso laboral mediano": ("ingreso_mediano", False),
|
|
32
|
+
"Ingreso laboral medio": ("ingreso_medio", False),
|
|
33
|
+
"Ocupados (personas)": ("ocupados", False),
|
|
34
|
+
"Desocupados (personas)": ("desocupados", False),
|
|
35
|
+
"Fuerza de trabajo (personas)": ("fuerza_trabajo", False),
|
|
36
|
+
}
|
|
37
|
+
DESAGREGAR = {
|
|
38
|
+
"Total nacional": None,
|
|
39
|
+
"Sexo": "sexo",
|
|
40
|
+
"Zona (urbano/rural)": "zona",
|
|
41
|
+
"Grupo de edad": "grupo_edad",
|
|
42
|
+
"Nivel educativo": "nivel_educativo",
|
|
43
|
+
"Departamento": "departamento",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------- KPIs
|
|
47
|
+
ult = total.iloc[-1]
|
|
48
|
+
prev = total.iloc[-2] if len(total) > 1 else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def delta(col, pct=True):
|
|
52
|
+
if prev is None or pd.isna(prev[col]):
|
|
53
|
+
return None
|
|
54
|
+
d = ult[col] - prev[col]
|
|
55
|
+
return f"{d:+.1f} p.p. vs {int(prev['anio'])}" if pct else f"{100 * d / prev[col]:+.1f} % vs {int(prev['anio'])}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
st.markdown(f"##### Resumen {int(ult['anio'])}")
|
|
59
|
+
k = st.columns(5)
|
|
60
|
+
k[0].metric("Desocupación", fmt_pct(ult.td), delta("td"), delta_color="inverse",
|
|
61
|
+
help=dd.geih.GLOSARIO["TD"])
|
|
62
|
+
k[1].metric("Participación (TGP)", fmt_pct(ult.tgp), delta("tgp"), help=dd.geih.GLOSARIO["TGP"])
|
|
63
|
+
k[2].metric("Ocupación (TO)", fmt_pct(ult.to), delta("to"), help=dd.geih.GLOSARIO["TO"])
|
|
64
|
+
k[3].metric("Informalidad", fmt_pct(ult.tasa_informalidad), delta("tasa_informalidad"), delta_color="inverse",
|
|
65
|
+
help=dd.geih.GLOSARIO["Informalidad"])
|
|
66
|
+
k[4].metric("Ingreso laboral mediano", fmt_pesos(ult.ingreso_mediano), delta("ingreso_mediano", pct=False),
|
|
67
|
+
help=dd.geih.GLOSARIO["Ingreso laboral"] + " Pesos corrientes (sin ajustar por inflación).")
|
|
68
|
+
if meses_ult < 12:
|
|
69
|
+
st.info(f"ℹ️ {int(ult['anio'])} tiene solo {meses_ult} mes(es) en la selección. La comparación con el año "
|
|
70
|
+
"anterior mezcla temporadas distintas (el desempleo es estacional); para comparar bien elija los "
|
|
71
|
+
"mismos meses en ambos años (desactive «Todos los meses»).")
|
|
72
|
+
st.caption(f"Ocupados: **{fmt_grande(ult.ocupados)}** · Desocupados: **{fmt_grande(ult.desocupados)}** · "
|
|
73
|
+
f"Población en edad de trabajar: **{fmt_grande(ult.pet)}** (promedio mensual).")
|
|
74
|
+
|
|
75
|
+
st.divider()
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------- Gráfico
|
|
78
|
+
c1, c2, c3 = st.columns([2, 2, 1])
|
|
79
|
+
nombre_ind = c1.selectbox("Indicador", list(INDICADORES))
|
|
80
|
+
nombre_por = c2.selectbox("Desagregar por", list(DESAGREGAR))
|
|
81
|
+
frecuencia = c3.radio("Frecuencia", ["Anual", "Mensual"], horizontal=True)
|
|
82
|
+
col, es_pct = INDICADORES[nombre_ind]
|
|
83
|
+
por = DESAGREGAR[nombre_por]
|
|
84
|
+
anual = frecuencia == "Anual"
|
|
85
|
+
|
|
86
|
+
res = calcular(por, anual)
|
|
87
|
+
if por:
|
|
88
|
+
res = res[res[por].notna()]
|
|
89
|
+
if por: # grupos sin dato (p. ej. 0–14 años no está en la PET)
|
|
90
|
+
res = res[res.groupby(por, observed=True)[col].transform("count") > 0]
|
|
91
|
+
if not anual:
|
|
92
|
+
res["periodo"] = pd.to_datetime(dict(year=res["anio"], month=res["mes"], day=1))
|
|
93
|
+
x = "anio" if anual else "periodo"
|
|
94
|
+
|
|
95
|
+
grupos = list(res[por].dropna().unique()) if por else []
|
|
96
|
+
if por and len(grupos) > MAX_SERIES and res[x].nunique() > 1:
|
|
97
|
+
orden = res.groupby(por, observed=True)["ocupados"].sum().sort_values(ascending=False).index.tolist()
|
|
98
|
+
elegidos = st.multiselect(f"Hay {len(grupos)} grupos; elija hasta {MAX_SERIES} para comparar en el tiempo",
|
|
99
|
+
orden, default=orden[:5], max_selections=MAX_SERIES)
|
|
100
|
+
res_graf = res[res[por].isin(elegidos)]
|
|
101
|
+
else:
|
|
102
|
+
res_graf = res
|
|
103
|
+
|
|
104
|
+
titulo = nombre_ind + (" (%)" if es_pct else "")
|
|
105
|
+
if res_graf[x].nunique() > 1:
|
|
106
|
+
fig = linea(res_graf, x=x, y=col, color=por, titulo_y=titulo, pct=es_pct)
|
|
107
|
+
else:
|
|
108
|
+
fig = barras(res_graf, x=por or x, y=col, titulo_y=titulo, pct=es_pct,
|
|
109
|
+
horizontal=bool(por) and len(grupos) > 6)
|
|
110
|
+
grafico(fig)
|
|
111
|
+
|
|
112
|
+
pocos = res[res["n_muestra"] < 100]
|
|
113
|
+
if len(pocos):
|
|
114
|
+
st.warning(f"⚠️ {len(pocos)} celda(s) tienen menos de 100 personas en la muestra: esas cifras son "
|
|
115
|
+
"poco confiables (mucho error de muestreo). Considere agrupar más meses o menos categorías.")
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------- Tabla
|
|
118
|
+
with st.expander("📋 Tabla completa", expanded=False):
|
|
119
|
+
tabla = res.drop(columns=["periodo"], errors="ignore")
|
|
120
|
+
st.dataframe(
|
|
121
|
+
tabla, hide_index=True, width="stretch",
|
|
122
|
+
column_config={
|
|
123
|
+
"anio": st.column_config.NumberColumn("Año", format="%d"),
|
|
124
|
+
"mes": st.column_config.NumberColumn("Mes", format="%d"),
|
|
125
|
+
**{c: st.column_config.NumberColumn(c.upper(), format="%.1f %%") for c in ("tgp", "to", "td")},
|
|
126
|
+
"tasa_informalidad": st.column_config.NumberColumn("Informalidad", format="%.1f %%"),
|
|
127
|
+
**{c: st.column_config.NumberColumn(c.replace("_", " ").capitalize(), format="localized")
|
|
128
|
+
for c in ("pet", "fuerza_trabajo", "ocupados", "desocupados", "inactivos", "ingreso_medio",
|
|
129
|
+
"ingreso_mediano", "n_muestra")},
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
botones_descarga(tabla, f"indicadores_geih_{'_'.join(map(str, sel['anios']))}", "ind")
|
|
133
|
+
|
|
134
|
+
with st.expander("📖 ¿Qué significa cada indicador?"):
|
|
135
|
+
for k_, v in dd.geih.GLOSARIO.items():
|
|
136
|
+
st.markdown(f"**{k_}.** {v}")
|
|
137
|
+
st.markdown(
|
|
138
|
+
"**Promedio mensual.** La GEIH es mensual: al juntar varios meses, los niveles (personas) son el "
|
|
139
|
+
"promedio de los meses y las tasas se calculan sobre esos promedios, como hace el DANE.\n\n"
|
|
140
|
+
"**Muestra vs población.** `n_muestra` es cuántas personas se encuestaron; los niveles usan el "
|
|
141
|
+
"factor de expansión para representar a todo el país."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
argumentos = f'{f", por={por!r}" if por else ""}{", anual=False" if not anual else ""}'
|
|
145
|
+
codigo(f'''
|
|
146
|
+
import dane_data as dd
|
|
147
|
+
|
|
148
|
+
# Igual a dd.geih.indicadores(...) sobre los microdatos filtrados, pero año por año y
|
|
149
|
+
# guardado en caché: la segunda vez es instantáneo.
|
|
150
|
+
ind = dd.geih.serie_indicadores({sel["anios"]!r}{argumentos}{args_py(empalme=sel["empalme"], filtros=sel["filtros"], meses=sel["meses"])})
|
|
151
|
+
ind
|
|
152
|
+
''')
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
|
|
3
|
+
import dane_data as dd
|
|
4
|
+
from dane_data.app._ui import disponibles_eam, disponibles_geih, rutas_actuales
|
|
5
|
+
|
|
6
|
+
st.title("📊 DANE-data")
|
|
7
|
+
st.markdown(
|
|
8
|
+
"#### Microdatos del DANE listos para analizar\n"
|
|
9
|
+
"Esta aplicación descarga, organiza y resume dos encuestas oficiales de Colombia publicadas por el DANE:"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
c1, c2 = st.columns(2)
|
|
13
|
+
with c1, st.container(border=True):
|
|
14
|
+
st.markdown(
|
|
15
|
+
"##### 👥 GEIH — Gran Encuesta Integrada de Hogares\n"
|
|
16
|
+
"Encuesta **mensual** a ~70.000 personas. Es la fuente oficial del **desempleo**, la "
|
|
17
|
+
"**informalidad** y los **ingresos laborales**. Cada fila es una persona; el *factor de "
|
|
18
|
+
"expansión* dice a cuántas personas del país representa.\n\n"
|
|
19
|
+
"**Úsela para:** brechas de género, desempleo juvenil, informalidad por región, "
|
|
20
|
+
"ingresos frente al salario mínimo…"
|
|
21
|
+
)
|
|
22
|
+
with c2, st.container(border=True):
|
|
23
|
+
st.markdown(
|
|
24
|
+
"##### 🏭 EAM — Encuesta Anual Manufacturera\n"
|
|
25
|
+
"Censo **anual** de ~7.000 establecimientos industriales (1992–2024). Cada fila es una "
|
|
26
|
+
"planta: producción, valor agregado, empleo, salarios, activos y energía.\n\n"
|
|
27
|
+
"**Úsela para:** productividad por sector, participación laboral, concentración "
|
|
28
|
+
"regional, funciones de producción y costos…"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
st.subheader("¿Cómo empiezo?")
|
|
32
|
+
st.markdown(
|
|
33
|
+
"1. **Datos:** si aún no los tiene, vaya a **⬇️ Descargar datos** (menú izquierdo). "
|
|
34
|
+
"Si ya los descargó, indique la carpeta abajo.\n"
|
|
35
|
+
"2. **Explore:** abra **👥 Indicadores laborales** o **🏭 Indicadores industriales**, elija años y filtros.\n"
|
|
36
|
+
"3. **Exporte:** cada tabla se descarga en CSV, Excel o Stata. Cada página muestra el "
|
|
37
|
+
"**código Python** equivalente para pasar a un notebook cuando esté listo."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------- Estado de datos
|
|
41
|
+
st.subheader("Datos disponibles en este computador")
|
|
42
|
+
rutas = rutas_actuales()
|
|
43
|
+
g, e, ge = disponibles_geih(False, rutas), disponibles_eam(rutas), disponibles_geih(True, rutas)
|
|
44
|
+
|
|
45
|
+
m1, m2, m3 = st.columns(3)
|
|
46
|
+
m1.metric("GEIH (serie principal)", f"{g.anio.nunique()} años" if len(g) else "Sin datos",
|
|
47
|
+
f"{g.anio.min()}–{g.anio.max()} · {len(g)} meses" if len(g) else None, delta_color="off")
|
|
48
|
+
m2.metric("GEIH (serie de empalme)", f"{ge.anio.nunique()} años" if len(ge) else "Sin datos",
|
|
49
|
+
f"{ge.anio.min()}–{ge.anio.max()}" if len(ge) else None, delta_color="off")
|
|
50
|
+
m3.metric("EAM", f"{e.anio.nunique()} años" if len(e) else "Sin datos",
|
|
51
|
+
f"{e.anio.min()}–{e.anio.max()}" if len(e) else None, delta_color="off")
|
|
52
|
+
|
|
53
|
+
incompletos = g[~g.completo] if len(g) else g
|
|
54
|
+
if len(incompletos):
|
|
55
|
+
with st.expander(f"⚠️ {len(incompletos)} mes(es) de la GEIH están incompletos y se omitirán"):
|
|
56
|
+
st.dataframe(incompletos[["anio", "nombre_mes", "carpeta", "faltantes"]], hide_index=True)
|
|
57
|
+
st.caption("Vuelva a descargarlos en ⬇️ Descargar datos (borre antes la carpeta de ese mes).")
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------- Configuración
|
|
60
|
+
with st.expander("⚙️ Carpetas de datos", expanded=not (len(g) or len(e))):
|
|
61
|
+
st.caption(
|
|
62
|
+
"Indique dónde están (o dónde quiere guardar) los datos. Para la GEIH, la carpeta que "
|
|
63
|
+
"contiene `datos_csv/`; para la EAM, la que contiene `datos/`. Se recuerda para la próxima vez."
|
|
64
|
+
)
|
|
65
|
+
with st.form("rutas"):
|
|
66
|
+
p_geih = st.text_input("GEIH serie principal", str(dd.get_path("geih")))
|
|
67
|
+
p_emp = st.text_input("GEIH serie de empalme", str(dd.get_path("geih_empalme")))
|
|
68
|
+
p_eam = st.text_input("EAM", str(dd.get_path("eam")))
|
|
69
|
+
if st.form_submit_button("Guardar", type="primary"):
|
|
70
|
+
dd.set_paths(geih=p_geih, geih_empalme=p_emp, eam=p_eam, save=True)
|
|
71
|
+
st.cache_data.clear()
|
|
72
|
+
st.cache_resource.clear()
|
|
73
|
+
st.rerun()
|
|
74
|
+
|
|
75
|
+
st.divider()
|
|
76
|
+
st.caption(
|
|
77
|
+
"Fuente: DANE — Archivo Nacional de Datos (ANDA), microdatos anonimizados de uso público. "
|
|
78
|
+
"Cite: *DANE, Gran Encuesta Integrada de Hogares (GEIH)* / *Encuesta Anual Manufacturera (EAM)*."
|
|
79
|
+
)
|
dane_data/cli.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Línea de comandos de dane_data.
|
|
3
|
+
|
|
4
|
+
dane-data app # abre la aplicación gráfica
|
|
5
|
+
dane-data estado # qué datos hay descargados
|
|
6
|
+
dane-data config --geih RUTA --eam RUTA # dónde están/estarán los datos
|
|
7
|
+
dane-data descargar geih --anios 2024 2025
|
|
8
|
+
dane-data descargar geih --anios 2016 --empalme
|
|
9
|
+
dane-data descargar eam --anios 2022 2023 2024
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import logging
|
|
16
|
+
import socket
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
import urllib.request
|
|
22
|
+
import webbrowser
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from . import config, eam, geih
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _puerto_libre(preferido: int) -> int:
|
|
29
|
+
"""El puerto pedido si está libre; si no, el siguiente libre."""
|
|
30
|
+
for puerto in range(preferido, preferido + 50):
|
|
31
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
32
|
+
if s.connect_ex(("127.0.0.1", puerto)) != 0:
|
|
33
|
+
return puerto
|
|
34
|
+
return preferido
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _abrir_navegador_cuando_lista(url: str, proceso: subprocess.Popen, espera_max: float = 120) -> None:
|
|
38
|
+
"""Espera a que Streamlit responda y abre el navegador (una sola vez)."""
|
|
39
|
+
salud = url.rstrip("/") + "/_stcore/health"
|
|
40
|
+
limite = time.time() + espera_max
|
|
41
|
+
while time.time() < limite and proceso.poll() is None:
|
|
42
|
+
try:
|
|
43
|
+
with urllib.request.urlopen(salud, timeout=1) as r:
|
|
44
|
+
if r.status == 200:
|
|
45
|
+
webbrowser.open(url)
|
|
46
|
+
return
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|
|
49
|
+
time.sleep(0.5)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cmd_app(args) -> int:
|
|
53
|
+
app = Path(__file__).parent / "app" / "Inicio.py"
|
|
54
|
+
puerto = _puerto_libre(args.port)
|
|
55
|
+
url = f"http://localhost:{puerto}"
|
|
56
|
+
# headless: Streamlit no abre el navegador ni pide correo la primera vez; lo abrimos nosotros.
|
|
57
|
+
cmd = [sys.executable, "-m", "streamlit", "run", str(app), "--server.port", str(puerto),
|
|
58
|
+
"--server.address", args.address, "--server.headless", "true",
|
|
59
|
+
"--browser.gatherUsageStats", "false"]
|
|
60
|
+
print(f"\n dane-data se está abriendo en {url}")
|
|
61
|
+
print(" Para cerrarla, cierre esta ventana o presione Ctrl+C.\n", flush=True)
|
|
62
|
+
proceso = subprocess.Popen(cmd, cwd=app.parent) # cwd: para que lea app/.streamlit/config.toml
|
|
63
|
+
if not args.no_browser:
|
|
64
|
+
threading.Thread(target=_abrir_navegador_cuando_lista, args=(url, proceso), daemon=True).start()
|
|
65
|
+
try:
|
|
66
|
+
return proceso.wait()
|
|
67
|
+
except KeyboardInterrupt:
|
|
68
|
+
proceso.terminate()
|
|
69
|
+
try:
|
|
70
|
+
proceso.wait(timeout=10)
|
|
71
|
+
except subprocess.TimeoutExpired:
|
|
72
|
+
proceso.kill()
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cmd_estado(args) -> int:
|
|
77
|
+
for key, label in (("geih", "GEIH principal"), ("geih_empalme", "GEIH empalme"), ("eam", "EAM")):
|
|
78
|
+
print(f"{label:15s} {config.get_path(key)}")
|
|
79
|
+
for emp in (False, True):
|
|
80
|
+
av = geih.available(empalme=emp)
|
|
81
|
+
nombre = "GEIH empalme" if emp else "GEIH principal"
|
|
82
|
+
if av.empty:
|
|
83
|
+
print(f"\n{nombre}: sin datos")
|
|
84
|
+
continue
|
|
85
|
+
resumen = av.groupby("anio").agg(meses=("mes", "size"), incompletos=("completo", lambda s: (~s).sum()))
|
|
86
|
+
print(f"\n{nombre}:")
|
|
87
|
+
print(resumen.to_string())
|
|
88
|
+
av = eam.available()
|
|
89
|
+
print("\nEAM:", "sin datos" if av.empty else f"{len(av)} años ({av.anio.min()}–{av.anio.max()})")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cmd_config(args) -> int:
|
|
94
|
+
config.set_paths(geih=args.geih, geih_empalme=args.empalme, eam=args.eam, save=True)
|
|
95
|
+
print(f"Guardado en {config.CONFIG_FILE}")
|
|
96
|
+
for k, v in config.show_paths().items():
|
|
97
|
+
print(f" {k:13s} {v}")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cmd_descargar(args) -> int:
|
|
102
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
103
|
+
if args.encuesta == "geih":
|
|
104
|
+
geih.download(years=args.anios, modules=args.modulos, empalme=args.empalme,
|
|
105
|
+
delete_zips=args.borrar_zips, limit_files=args.limite)
|
|
106
|
+
else:
|
|
107
|
+
eam.download(years=args.anios, include_tic=args.tic, delete_zips=args.borrar_zips)
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main(argv=None) -> int:
|
|
112
|
+
p = argparse.ArgumentParser(prog="dane-data", description="Microdatos DANE (GEIH y EAM) para economistas.")
|
|
113
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
114
|
+
|
|
115
|
+
a = sub.add_parser("app", help="Abrir la aplicación gráfica en el navegador")
|
|
116
|
+
a.add_argument("--port", type=int, default=8501, help="Puerto preferido (si está ocupado se usa el siguiente)")
|
|
117
|
+
a.add_argument("--no-browser", action="store_true", help="No abrir el navegador automáticamente")
|
|
118
|
+
a.add_argument("--address", default="localhost", help="Use 0.0.0.0 para compartir en la red local (aula)")
|
|
119
|
+
a.set_defaults(fn=_cmd_app)
|
|
120
|
+
|
|
121
|
+
e = sub.add_parser("estado", help="Mostrar rutas y datos disponibles")
|
|
122
|
+
e.set_defaults(fn=_cmd_estado)
|
|
123
|
+
|
|
124
|
+
c = sub.add_parser("config", help="Configurar carpetas de datos (se guarda para siempre)")
|
|
125
|
+
c.add_argument("--geih", help="Carpeta de la GEIH principal (la que contiene datos_csv/)")
|
|
126
|
+
c.add_argument("--empalme", help="Carpeta de la GEIH empalme (la que contiene datos_csv/)")
|
|
127
|
+
c.add_argument("--eam", help="Carpeta de la EAM (la que contiene datos/)")
|
|
128
|
+
c.set_defaults(fn=_cmd_config)
|
|
129
|
+
|
|
130
|
+
d = sub.add_parser("descargar", help="Descargar microdatos del DANE")
|
|
131
|
+
d.add_argument("encuesta", choices=["geih", "eam"])
|
|
132
|
+
d.add_argument("--anios", nargs="+", type=int, help="Años (por defecto todos)")
|
|
133
|
+
d.add_argument("--modulos", nargs="+", choices=list(geih.MODULE_PATTERNS), help="Solo GEIH")
|
|
134
|
+
d.add_argument("--empalme", action="store_true", help="GEIH: serie de empalme 2010–2020")
|
|
135
|
+
d.add_argument("--limite", type=int, help="GEIH: máximo de archivos mensuales por año (pruebas)")
|
|
136
|
+
d.add_argument("--tic", action="store_true", help="EAM: incluir módulo TIC 2008–2018")
|
|
137
|
+
d.add_argument("--borrar-zips", action="store_true", help="Borrar los ZIP tras extraer")
|
|
138
|
+
d.set_defaults(fn=_cmd_descargar)
|
|
139
|
+
|
|
140
|
+
args = p.parse_args(argv)
|
|
141
|
+
try:
|
|
142
|
+
return args.fn(args)
|
|
143
|
+
except KeyboardInterrupt:
|
|
144
|
+
print("\nInterrumpido.")
|
|
145
|
+
return 130
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
sys.exit(main())
|
dane_data/config.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuración de rutas de datos.
|
|
3
|
+
|
|
4
|
+
Cada encuesta vive en su propia carpeta raíz, con la misma estructura que
|
|
5
|
+
generan los descargadores originales:
|
|
6
|
+
|
|
7
|
+
GEIH (serie principal) : <raiz>/datos_csv/<año>/<mes>/*.csv
|
|
8
|
+
GEIH (serie empalme) : <raiz>/datos_csv/<año>/<mes>/*.csv
|
|
9
|
+
EAM : <raiz>/datos/<año>/*.csv|*.txt
|
|
10
|
+
|
|
11
|
+
Prioridad para resolver cada ruta:
|
|
12
|
+
1. ``set_paths(...)`` en la sesión actual.
|
|
13
|
+
2. Variables de entorno ``DANE_DATA_GEIH``, ``DANE_DATA_GEIH_EMPALME``, ``DANE_DATA_EAM``.
|
|
14
|
+
3. Archivo ``~/.config/dane_data/config.json`` (lo escribe ``save()`` y la app).
|
|
15
|
+
4. Valor por defecto bajo ``~/dane_data_datos/``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
CONFIG_FILE = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "dane_data" / "config.json"
|
|
25
|
+
CACHE_DIR = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "dane_data"
|
|
26
|
+
DEFAULT_HOME = Path.home() / "dane_data_datos"
|
|
27
|
+
|
|
28
|
+
_KEYS = {
|
|
29
|
+
"geih": ("DANE_DATA_GEIH", DEFAULT_HOME / "geih"),
|
|
30
|
+
"geih_empalme": ("DANE_DATA_GEIH_EMPALME", DEFAULT_HOME / "geih" / "empalme"),
|
|
31
|
+
"eam": ("DANE_DATA_EAM", DEFAULT_HOME / "eam"),
|
|
32
|
+
}
|
|
33
|
+
_session: dict[str, Path] = {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _read_file() -> dict:
|
|
37
|
+
try:
|
|
38
|
+
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
39
|
+
except (OSError, ValueError):
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_path(key: str) -> Path:
|
|
44
|
+
"""Devuelve la carpeta raíz de ``key`` ('geih', 'geih_empalme' o 'eam')."""
|
|
45
|
+
if key not in _KEYS:
|
|
46
|
+
raise KeyError(f"Clave desconocida {key!r}. Opciones: {list(_KEYS)}")
|
|
47
|
+
if key in _session:
|
|
48
|
+
return _session[key]
|
|
49
|
+
env, default = _KEYS[key]
|
|
50
|
+
if os.environ.get(env):
|
|
51
|
+
return Path(os.environ[env]).expanduser()
|
|
52
|
+
stored = _read_file().get(key)
|
|
53
|
+
if stored:
|
|
54
|
+
return Path(stored).expanduser()
|
|
55
|
+
return default
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def set_paths(geih=None, geih_empalme=None, eam=None, save: bool = False) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Fija las carpetas raíz para esta sesión.
|
|
61
|
+
|
|
62
|
+
>>> import dane_data as dd
|
|
63
|
+
>>> dd.set_paths(geih="~/datos/GEIH/data", eam="~/datos/EAM/data", save=True)
|
|
64
|
+
|
|
65
|
+
Con ``save=True`` se guardan en ``~/.config/dane_data/config.json`` para
|
|
66
|
+
futuras sesiones (y para la app).
|
|
67
|
+
"""
|
|
68
|
+
for key, val in (("geih", geih), ("geih_empalme", geih_empalme), ("eam", eam)):
|
|
69
|
+
if val is not None:
|
|
70
|
+
_session[key] = Path(val).expanduser()
|
|
71
|
+
if save:
|
|
72
|
+
data = _read_file()
|
|
73
|
+
data.update({k: str(v) for k, v in _session.items()})
|
|
74
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
CONFIG_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def show_paths() -> dict[str, str]:
|
|
79
|
+
"""Rutas efectivas actuales (útil para depurar)."""
|
|
80
|
+
return {k: str(get_path(k)) for k in _KEYS}
|
|
File without changes
|