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,49 @@
|
|
|
1
|
+
"""Encuesta Anual Manufacturera (EAM) — industria, microdatos de establecimientos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from .. import config
|
|
8
|
+
from .downloader import EAM_KNOWN_CATALOGS, EAM_TIC_CATALOGS, EAMDownloader
|
|
9
|
+
from .indicadores import GLOSARIO, crecimiento, deflactar, panel, participacion, resumen, supervivencia
|
|
10
|
+
from .loader import (CORE, ETIQUETAS, SECTORES, VARIABLES_COMUNES, available, find_files, harmonize,
|
|
11
|
+
load)
|
|
12
|
+
|
|
13
|
+
ANIOS_DISPONIBLES = sorted(int(y) for y in EAM_KNOWN_CATALOGS)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def download(
|
|
17
|
+
years: Iterable[int] | int | None = None,
|
|
18
|
+
root=None,
|
|
19
|
+
include_tic: bool = False,
|
|
20
|
+
delete_zips: bool = False,
|
|
21
|
+
progress=None,
|
|
22
|
+
should_stop=None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Descarga microdatos EAM (1992–2024) del ANDA (DANE).
|
|
26
|
+
|
|
27
|
+
>>> dd.eam.download([2022, 2023, 2024])
|
|
28
|
+
>>> dd.eam.download() # serie completa (~330 MB)
|
|
29
|
+
|
|
30
|
+
Es incremental: los años ya extraídos se saltan.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(years, (int, str)):
|
|
33
|
+
years = [years]
|
|
34
|
+
dl = EAMDownloader(
|
|
35
|
+
base_dir=root or config.get_path("eam"),
|
|
36
|
+
keep_zips=not delete_zips,
|
|
37
|
+
extract_data=True,
|
|
38
|
+
include_tic=include_tic,
|
|
39
|
+
progress=progress,
|
|
40
|
+
should_stop=should_stop,
|
|
41
|
+
)
|
|
42
|
+
dl.run_pipeline(target_years=years)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"download", "load", "available", "find_files", "harmonize", "resumen", "participacion",
|
|
47
|
+
"crecimiento", "deflactar", "panel", "supervivencia", "GLOSARIO", "ETIQUETAS", "SECTORES",
|
|
48
|
+
"CORE", "VARIABLES_COMUNES", "ANIOS_DISPONIBLES",
|
|
49
|
+
]
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EAM Downloader & Extractor
|
|
3
|
+
==========================
|
|
4
|
+
Pipeline automatizado para descubrir, descargar y extraer el histórico completo
|
|
5
|
+
de microdatos de la Encuesta Anual Manufacturera (EAM) del DANE (1992 - 2024)
|
|
6
|
+
desde el Archivo Nacional de Datos (ANDA).
|
|
7
|
+
|
|
8
|
+
Reglas de extracción de formatos:
|
|
9
|
+
1. Prioridad 1: Archivo .csv (utilizado en la gran mayoría de la serie histórica).
|
|
10
|
+
2. Prioridad 2: Archivo .txt delimitado (si no existe .csv, como en 2023).
|
|
11
|
+
3. Prioridad 3: Conversión de .dta a .txt tabulado (si no existe .csv ni .txt, como en 2024).
|
|
12
|
+
|
|
13
|
+
NOTAS METODOLÓGICAS IMPORTANTES:
|
|
14
|
+
- Clasificación CIIU (Rupturas metodológicas), según los códigos de los archivos:
|
|
15
|
+
* 1992 - 2000: CIIU Rev. 2 A.C. (códigos de 4 dígitos, ej. 3111).
|
|
16
|
+
* 2001 - 2011: CIIU Rev. 3 A.C. (códigos de 4 dígitos, ej. 1511).
|
|
17
|
+
* 2012 - 2024: CIIU Rev. 4 A.C. (códigos de 4 dígitos, ej. 1011).
|
|
18
|
+
La documentación del ANDA ubica la Rev. 3 desde 2000 y la Rev. 4 desde las
|
|
19
|
+
cifras de 2013; los archivos de 2000 y 2012 la contradicen (ver README).
|
|
20
|
+
Para construir series continuas entre décadas, se requiere aplicar tablas
|
|
21
|
+
de correlación/concordancia CIIU.
|
|
22
|
+
- Identificación de Panel (Seguimiento longitudinal):
|
|
23
|
+
* Las variables clave de identificación de planta y empresa son 'nordemp'
|
|
24
|
+
(número de orden de empresa) y 'nordest' (número de orden de establecimiento).
|
|
25
|
+
* En los microdatos anonimizados públicos de ANDA, verificar la consistencia
|
|
26
|
+
temporal del algoritmo de anonimización según el período de estudio.
|
|
27
|
+
|
|
28
|
+
Autor original: Equipo de Investigación (adaptado para dane-data)
|
|
29
|
+
Licencia: MIT
|
|
30
|
+
Versión: 1.0.0
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import io
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import json
|
|
38
|
+
import time
|
|
39
|
+
import zipfile
|
|
40
|
+
import logging
|
|
41
|
+
import argparse
|
|
42
|
+
import unicodedata
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from urllib.parse import urljoin
|
|
45
|
+
import requests
|
|
46
|
+
from bs4 import BeautifulSoup
|
|
47
|
+
|
|
48
|
+
from .._hooks import ProgressMixin
|
|
49
|
+
|
|
50
|
+
__version__ = "1.0.0"
|
|
51
|
+
|
|
52
|
+
log = logging.getLogger("dane_data.eam")
|
|
53
|
+
|
|
54
|
+
# Catálogos oficiales verificados en ANDA (DANE) para la EAM (1992-2024)
|
|
55
|
+
EAM_KNOWN_CATALOGS = {
|
|
56
|
+
# 1992-1994: Catálogo 563
|
|
57
|
+
"1992": ("563", "EAM_1992.zip", "https://microdatos.dane.gov.co/index.php/catalog/563/download/10179"),
|
|
58
|
+
"1993": ("563", "EAM_1993.zip", "https://microdatos.dane.gov.co/index.php/catalog/563/download/10180"),
|
|
59
|
+
"1994": ("563", "EAM_1994.zip", "https://microdatos.dane.gov.co/index.php/catalog/563/download/10181"),
|
|
60
|
+
# 1995-1999: Catálogo 492
|
|
61
|
+
"1995": ("492", "EAM_1995.zip", "https://microdatos.dane.gov.co/index.php/catalog/492/download/10182"),
|
|
62
|
+
"1996": ("492", "EAM_1996.zip", "https://microdatos.dane.gov.co/index.php/catalog/492/download/10183"),
|
|
63
|
+
"1997": ("492", "EAM_1997.zip", "https://microdatos.dane.gov.co/index.php/catalog/492/download/10184"),
|
|
64
|
+
"1998": ("492", "EAM_1998.zip", "https://microdatos.dane.gov.co/index.php/catalog/492/download/10185"),
|
|
65
|
+
"1999": ("492", "EAM_1999.zip", "https://microdatos.dane.gov.co/index.php/catalog/492/download/10186"),
|
|
66
|
+
# 2000-2007: Catálogo 494
|
|
67
|
+
"2000": ("494", "EAM_2000.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/10187"),
|
|
68
|
+
"2001": ("494", "EAM_2001.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/10188"),
|
|
69
|
+
"2002": ("494", "EAM_2002.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/10189"),
|
|
70
|
+
"2003": ("494", "EAM_2003.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/14158"),
|
|
71
|
+
"2004": ("494", "EAM_2004.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/10191"),
|
|
72
|
+
"2005": ("494", "EAM_2005.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/10192"),
|
|
73
|
+
"2006": ("494", "EAM_2006.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/10193"),
|
|
74
|
+
"2007": ("494", "EAM_2007.zip", "https://microdatos.dane.gov.co/index.php/catalog/494/download/10194"),
|
|
75
|
+
# 2008-2012: Catálogo 493
|
|
76
|
+
"2008": ("493", "EAM_2008.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10195"),
|
|
77
|
+
"2009": ("493", "EAM_2009.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10196"),
|
|
78
|
+
"2010": ("493", "EAM_2010.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10197"),
|
|
79
|
+
"2011": ("493", "EAM_2011.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10198"),
|
|
80
|
+
"2012": ("493", "EAM_2012.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10199"),
|
|
81
|
+
# 2013-2015: Catálogo 491
|
|
82
|
+
"2013": ("491", "EAM_2013.zip", "https://microdatos.dane.gov.co/index.php/catalog/491/download/10200"),
|
|
83
|
+
"2014": ("491", "EAM_2014.zip", "https://microdatos.dane.gov.co/index.php/catalog/491/download/10201"),
|
|
84
|
+
"2015": ("491", "EAM_2015.zip", "https://microdatos.dane.gov.co/index.php/catalog/491/download/10202"),
|
|
85
|
+
# 2016-2017: Catálogo 523
|
|
86
|
+
"2016": ("523", "EAM_2016.zip", "https://microdatos.dane.gov.co/index.php/catalog/523/download/10203"),
|
|
87
|
+
"2017": ("523", "Estructura - EAM - 2017.zip", "https://microdatos.dane.gov.co/index.php/catalog/523/download/10329"),
|
|
88
|
+
# 2018+: Catálogos individuales
|
|
89
|
+
"2018": ("650", "EAM_2018.zip", "https://microdatos.dane.gov.co/index.php/catalog/650/download/12432"),
|
|
90
|
+
"2019": ("694", "EAM_2019.zip", "https://microdatos.dane.gov.co/index.php/catalog/694/download/14186"),
|
|
91
|
+
"2020": ("724", "EAM_2020.zip", "https://microdatos.dane.gov.co/index.php/catalog/724/download/20937"),
|
|
92
|
+
"2021": ("802", "EAM_2021.zip", "https://microdatos.dane.gov.co/index.php/catalog/802/download/22877"),
|
|
93
|
+
"2022": ("836", "EAM_ANONIMIZADA_2022.zip", "https://microdatos.dane.gov.co/index.php/catalog/836/download/24325"),
|
|
94
|
+
"2023": ("871", "BDATOS-EAM-2023.zip", "https://microdatos.dane.gov.co/index.php/catalog/871/download/24120"),
|
|
95
|
+
"2024": ("888", "BDATOS-EAM-2024.zip", "https://microdatos.dane.gov.co/index.php/catalog/888/download/24429"),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Módulos TIC opcionales
|
|
99
|
+
EAM_TIC_CATALOGS = {
|
|
100
|
+
"2008": ("493", "EAM_TIC_2008.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10336"),
|
|
101
|
+
"2009": ("493", "EAM_TIC_2009.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10337"),
|
|
102
|
+
"2010": ("493", "EAM_TIC_2010.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10338"),
|
|
103
|
+
"2011": ("493", "EAM_TIC_2011.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10339"),
|
|
104
|
+
"2012": ("493", "EAM_TIC_2012.zip", "https://microdatos.dane.gov.co/index.php/catalog/493/download/10340"),
|
|
105
|
+
"2013": ("491", "EAM_TIC_2013.zip", "https://microdatos.dane.gov.co/index.php/catalog/491/download/10333"),
|
|
106
|
+
"2014": ("491", "EAM_TIC_2014.zip", "https://microdatos.dane.gov.co/index.php/catalog/491/download/10334"),
|
|
107
|
+
"2015": ("491", "EAM_TIC_2015.zip", "https://microdatos.dane.gov.co/index.php/catalog/491/download/10335"),
|
|
108
|
+
"2016": ("523", "EAM_TIC_2016.zip", "https://microdatos.dane.gov.co/index.php/catalog/523/download/10332"),
|
|
109
|
+
"2017": ("523", "EAM_TIC_2017.zip", "https://microdatos.dane.gov.co/index.php/catalog/523/download/10331"),
|
|
110
|
+
"2018": ("650", "EAM_TIC.zip", "https://microdatos.dane.gov.co/index.php/catalog/650/download/12484"),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sanitize_member_name(raw_name: str) -> str:
|
|
115
|
+
"""Corrige codificación CP850/CP437/Latin1 habitual en ZIPs generados en Windows."""
|
|
116
|
+
clean = Path(raw_name).name
|
|
117
|
+
try:
|
|
118
|
+
clean = clean.encode("cp437").decode("utf-8")
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
return clean
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _archivos_datos(carpeta: Path) -> list[Path]:
|
|
125
|
+
"""Archivos .csv/.txt ya extraídos en ``carpeta`` (sin entrar en subcarpetas como TIC/)."""
|
|
126
|
+
if not carpeta.is_dir():
|
|
127
|
+
return []
|
|
128
|
+
return sorted(f for f in carpeta.iterdir() if f.is_file() and f.suffix.lower() in (".csv", ".txt"))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class EAMDownloader(ProgressMixin):
|
|
132
|
+
def __init__(self, base_dir="data", keep_zips=True, extract_data=True, include_tic=False,
|
|
133
|
+
progress=None, should_stop=None):
|
|
134
|
+
self.progress = progress
|
|
135
|
+
self.should_stop = should_stop
|
|
136
|
+
self.base_url = "https://microdatos.dane.gov.co"
|
|
137
|
+
self.base_dir = Path(base_dir)
|
|
138
|
+
self.metadata_dir = self.base_dir / "metadata"
|
|
139
|
+
self.raw_dir = self.base_dir / "raw_zips"
|
|
140
|
+
self.data_dir = self.base_dir / "datos"
|
|
141
|
+
self.keep_zips = keep_zips
|
|
142
|
+
self.extract_data = extract_data
|
|
143
|
+
self.include_tic = include_tic
|
|
144
|
+
|
|
145
|
+
self.session = requests.Session()
|
|
146
|
+
self.session.headers.update({
|
|
147
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
self._setup_directories()
|
|
151
|
+
|
|
152
|
+
def _setup_directories(self):
|
|
153
|
+
"""Crea la estructura de carpetas local."""
|
|
154
|
+
self.metadata_dir.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
self.raw_dir.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
if self.extract_data:
|
|
157
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
log.info(f"Directorio base de la EAM configurado en: {self.base_dir.resolve()}")
|
|
159
|
+
|
|
160
|
+
def discover_catalogs(self) -> dict:
|
|
161
|
+
"""
|
|
162
|
+
Descubre dinámicamente los catálogos y archivos de la EAM en ANDA.
|
|
163
|
+
Retorna un diccionario {año: (catalog_id, filename, download_url)}.
|
|
164
|
+
"""
|
|
165
|
+
log.info("Sincronizando catálogos de la EAM con el portal ANDA del DANE...")
|
|
166
|
+
discovered = {}
|
|
167
|
+
|
|
168
|
+
# IDs base de los catálogos que contienen la serie EAM
|
|
169
|
+
catalog_ids = [
|
|
170
|
+
"563", "492", "494", "493", "491", "523",
|
|
171
|
+
"650", "694", "724", "802", "836", "871", "888"
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
for cid in catalog_ids:
|
|
175
|
+
microdata_url = f"{self.base_url}/index.php/catalog/{cid}/get-microdata"
|
|
176
|
+
try:
|
|
177
|
+
res = self.session.get(microdata_url, timeout=20)
|
|
178
|
+
res.raise_for_status()
|
|
179
|
+
matches = re.findall(r"mostrarModal\(\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", res.text)
|
|
180
|
+
for name, url in matches:
|
|
181
|
+
if "tic" in name.lower():
|
|
182
|
+
continue
|
|
183
|
+
m = re.search(r"(199\d|20\d\d)", name)
|
|
184
|
+
if m:
|
|
185
|
+
yr = m.group(1)
|
|
186
|
+
if yr not in discovered:
|
|
187
|
+
discovered[yr] = (cid, name.strip(), url.strip())
|
|
188
|
+
except Exception as e:
|
|
189
|
+
log.warning(f"Error consultando microdatos para catálogo {cid}: {e}")
|
|
190
|
+
|
|
191
|
+
# Unificar con los catálogos verificados
|
|
192
|
+
final_catalogs = dict(EAM_KNOWN_CATALOGS)
|
|
193
|
+
final_catalogs.update(discovered)
|
|
194
|
+
|
|
195
|
+
log.info(f"Se identificaron archivos de EAM para {len(final_catalogs)} años: {sorted(final_catalogs.keys())}")
|
|
196
|
+
return final_catalogs
|
|
197
|
+
|
|
198
|
+
def fetch_metadata(self, catalog_id: str):
|
|
199
|
+
"""Obtiene y guarda localmente la metadata metodológica JSON del catálogo."""
|
|
200
|
+
metadata_path = self.metadata_dir / f"catalogo_{catalog_id}.json"
|
|
201
|
+
if metadata_path.exists():
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
json_url = f"{self.base_url}/index.php/metadata/export/{catalog_id}/json"
|
|
205
|
+
try:
|
|
206
|
+
res = self.session.get(json_url, timeout=20)
|
|
207
|
+
if res.status_code == 200:
|
|
208
|
+
data = res.json()
|
|
209
|
+
with open(metadata_path, "w", encoding="utf-8") as f:
|
|
210
|
+
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
211
|
+
log.info(f"Metadata metodológica guardada: {metadata_path.name}")
|
|
212
|
+
except Exception as e:
|
|
213
|
+
log.warning(f"No se pudo descargar metadata JSON para catálogo {catalog_id}: {e}")
|
|
214
|
+
|
|
215
|
+
def download_file(self, url: str, dest_path: Path, max_retries: int = 3) -> bool:
|
|
216
|
+
"""Descarga un archivo con streaming y verificación de integridad."""
|
|
217
|
+
if dest_path.exists() and dest_path.stat().st_size > 0:
|
|
218
|
+
log.info(f"Archivo ya existente: {dest_path.name} ({dest_path.stat().st_size / 1024 / 1024:.1f} MB). Saltando descarga.")
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
temp_dest = dest_path.with_suffix(dest_path.suffix + ".part")
|
|
222
|
+
|
|
223
|
+
for attempt in range(1, max_retries + 1):
|
|
224
|
+
try:
|
|
225
|
+
log.info(f"Descargando {dest_path.name} (intento {attempt}/{max_retries})...")
|
|
226
|
+
with self.session.get(url, stream=True, timeout=45) as r:
|
|
227
|
+
r.raise_for_status()
|
|
228
|
+
total_size = int(r.headers.get("content-length", 0))
|
|
229
|
+
downloaded = 0
|
|
230
|
+
|
|
231
|
+
with open(temp_dest, "wb") as f:
|
|
232
|
+
for chunk in r.iter_content(chunk_size=65536):
|
|
233
|
+
self._check_stop()
|
|
234
|
+
if chunk:
|
|
235
|
+
f.write(chunk)
|
|
236
|
+
downloaded += len(chunk)
|
|
237
|
+
if total_size:
|
|
238
|
+
self._emit(None, f"{dest_path.name}: {downloaded / 1e6:.1f} / {total_size / 1e6:.1f} MB")
|
|
239
|
+
|
|
240
|
+
if total_size > 0 and downloaded < total_size:
|
|
241
|
+
log.warning(f"Descarga incompleta ({downloaded}/{total_size} bytes). Reintentando...")
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
temp_dest.rename(dest_path)
|
|
245
|
+
size_mb = dest_path.stat().st_size / 1024 / 1024
|
|
246
|
+
log.info(f"✓ Descarga completada: {dest_path.name} ({size_mb:.2f} MB)")
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
if type(e).__name__ == "DownloadCancelled":
|
|
251
|
+
if temp_dest.exists():
|
|
252
|
+
temp_dest.unlink()
|
|
253
|
+
raise
|
|
254
|
+
log.error(f"Error descargando {dest_path.name} (intento {attempt}): {e}")
|
|
255
|
+
if temp_dest.exists():
|
|
256
|
+
temp_dest.unlink()
|
|
257
|
+
time.sleep(2 * attempt)
|
|
258
|
+
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def extract_preferred_format(self, zip_path: Path, dest_dir: Path) -> list:
|
|
262
|
+
"""
|
|
263
|
+
Extrae el archivo de datos con la jerarquía de preferencia solicitada:
|
|
264
|
+
1. Archivo .csv (primera opción).
|
|
265
|
+
2. Archivo .txt delimitado (si no existe .csv, como en 2023).
|
|
266
|
+
3. Conversión de .dta a .txt tabulado (si no existe .csv ni .txt, como en 2024).
|
|
267
|
+
"""
|
|
268
|
+
if not zip_path.exists():
|
|
269
|
+
return []
|
|
270
|
+
|
|
271
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
extracted_files = []
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
with zipfile.ZipFile(zip_path, "r") as z:
|
|
276
|
+
members = [m for m in z.infolist() if not m.is_dir()]
|
|
277
|
+
|
|
278
|
+
# 1. Buscar .csv
|
|
279
|
+
csv_members = [m for m in members if m.filename.lower().endswith(".csv")]
|
|
280
|
+
if csv_members:
|
|
281
|
+
for m in csv_members:
|
|
282
|
+
clean_name = sanitize_member_name(m.filename)
|
|
283
|
+
target = dest_dir / clean_name
|
|
284
|
+
with z.open(m) as src, open(target, "wb") as dst:
|
|
285
|
+
dst.write(src.read())
|
|
286
|
+
extracted_files.append(target)
|
|
287
|
+
log.info(f" → Extraído formato CSV: {[f.name for f in extracted_files]}")
|
|
288
|
+
return extracted_files
|
|
289
|
+
|
|
290
|
+
# 2. Buscar .txt
|
|
291
|
+
txt_members = [m for m in members if m.filename.lower().endswith(".txt")]
|
|
292
|
+
if txt_members:
|
|
293
|
+
for m in txt_members:
|
|
294
|
+
clean_name = sanitize_member_name(m.filename)
|
|
295
|
+
target = dest_dir / clean_name
|
|
296
|
+
with z.open(m) as src, open(target, "wb") as dst:
|
|
297
|
+
dst.write(src.read())
|
|
298
|
+
extracted_files.append(target)
|
|
299
|
+
log.info(f" → Extraído formato TXT (alternativa al no haber CSV): {[f.name for f in extracted_files]}")
|
|
300
|
+
return extracted_files
|
|
301
|
+
|
|
302
|
+
# 3. Si no hay .csv ni .txt (caso año 2024), extraer/convertir .dta a .txt
|
|
303
|
+
dta_members = [m for m in members if m.filename.lower().endswith(".dta")]
|
|
304
|
+
if dta_members:
|
|
305
|
+
for m in dta_members:
|
|
306
|
+
clean_name = sanitize_member_name(m.filename)
|
|
307
|
+
txt_name = Path(clean_name).stem + ".txt"
|
|
308
|
+
target_txt = dest_dir / txt_name
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
import pandas as pd
|
|
312
|
+
dta_bytes = io.BytesIO(z.read(m))
|
|
313
|
+
df = pd.read_stata(dta_bytes)
|
|
314
|
+
df.to_csv(target_txt, sep="\t", index=False)
|
|
315
|
+
extracted_files.append(target_txt)
|
|
316
|
+
log.info(f" → Generado formato TXT tabulado a partir del archivo Stata .dta: {target_txt.name}")
|
|
317
|
+
except Exception as e:
|
|
318
|
+
# Respaldo en caso de fallo de pandas: extraer directamente el .dta
|
|
319
|
+
target_dta = dest_dir / clean_name
|
|
320
|
+
with z.open(m) as src, open(target_dta, "wb") as dst:
|
|
321
|
+
dst.write(src.read())
|
|
322
|
+
extracted_files.append(target_dta)
|
|
323
|
+
log.warning(f" → Extraído .dta directamente por error al convertir a TXT: {e}")
|
|
324
|
+
|
|
325
|
+
return extracted_files
|
|
326
|
+
|
|
327
|
+
except zipfile.BadZipFile:
|
|
328
|
+
log.error(f"El archivo {zip_path.name} está corrupto o no es un ZIP válido.")
|
|
329
|
+
except Exception as e:
|
|
330
|
+
log.error(f"Error procesando {zip_path.name}: {e}")
|
|
331
|
+
|
|
332
|
+
return extracted_files
|
|
333
|
+
|
|
334
|
+
def process_year(self, year: str, catalog_id: str, zip_filename: str, download_url: str):
|
|
335
|
+
"""Descarga y procesa un año específico de la EAM."""
|
|
336
|
+
log.info(f"\n{'='*20} Procesando EAM Año {year} (Catálogo {catalog_id}) {'='*20}")
|
|
337
|
+
|
|
338
|
+
# 1. Guardar metadata metodológica
|
|
339
|
+
self.fetch_metadata(catalog_id)
|
|
340
|
+
|
|
341
|
+
year_raw_dir = self.raw_dir / year
|
|
342
|
+
year_raw_dir.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
|
|
344
|
+
year_data_dir = self.data_dir / year
|
|
345
|
+
if self.extract_data:
|
|
346
|
+
year_data_dir.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
|
|
348
|
+
# Comprobar si ya existe el archivo de datos (.csv o .txt)
|
|
349
|
+
existing_data = _archivos_datos(year_data_dir) if self.extract_data else []
|
|
350
|
+
if existing_data:
|
|
351
|
+
log.info(f"Año {year} ya cuenta con datos extraídos ({[f.name for f in existing_data]}). Saltando descarga.")
|
|
352
|
+
else:
|
|
353
|
+
# 2. Descargar paquete ZIP
|
|
354
|
+
safe_name = "".join(c for c in zip_filename if c.isalnum() or c in (" ", ".", "_", "-")).strip()
|
|
355
|
+
zip_dest = year_raw_dir / safe_name
|
|
356
|
+
|
|
357
|
+
success = self.download_file(download_url, zip_dest)
|
|
358
|
+
|
|
359
|
+
# 3. Extraer formato preferido (.csv -> .txt -> .dta convertido)
|
|
360
|
+
if success and self.extract_data:
|
|
361
|
+
self.extract_preferred_format(zip_dest, year_data_dir)
|
|
362
|
+
|
|
363
|
+
if not self.keep_zips and zip_dest.exists():
|
|
364
|
+
zip_dest.unlink()
|
|
365
|
+
|
|
366
|
+
# 4. Descargar satélite TIC si fue solicitado (también en años ya descargados)
|
|
367
|
+
tic_pendiente = (self.include_tic and year in EAM_TIC_CATALOGS
|
|
368
|
+
and not _archivos_datos(year_data_dir / "TIC"))
|
|
369
|
+
if tic_pendiente:
|
|
370
|
+
tic_cid, tic_name, tic_url = EAM_TIC_CATALOGS[year]
|
|
371
|
+
tic_zip = year_raw_dir / tic_name
|
|
372
|
+
log.info(f"Descargando módulo TIC para {year}...")
|
|
373
|
+
tic_ok = self.download_file(tic_url, tic_zip)
|
|
374
|
+
if tic_ok and self.extract_data:
|
|
375
|
+
tic_data_dir = year_data_dir / "TIC"
|
|
376
|
+
self.extract_preferred_format(tic_zip, tic_data_dir)
|
|
377
|
+
if not self.keep_zips and tic_zip.exists():
|
|
378
|
+
tic_zip.unlink()
|
|
379
|
+
|
|
380
|
+
if not existing_data or tic_pendiente: # pausa entre descargas, no entre años saltados
|
|
381
|
+
time.sleep(1)
|
|
382
|
+
|
|
383
|
+
def run_pipeline(self, target_years=None):
|
|
384
|
+
"""Ejecuta el pipeline de descarga de la EAM para todos o los años seleccionados."""
|
|
385
|
+
catalogs = self.discover_catalogs()
|
|
386
|
+
|
|
387
|
+
years_to_process = sorted(catalogs.keys(), key=lambda y: int(y))
|
|
388
|
+
if target_years:
|
|
389
|
+
target_years = [str(y) for y in target_years]
|
|
390
|
+
years_to_process = [y for y in years_to_process if y in target_years]
|
|
391
|
+
|
|
392
|
+
log.info(f"Iniciando descarga de la EAM para {len(years_to_process)} años: {years_to_process}")
|
|
393
|
+
|
|
394
|
+
for i, year in enumerate(years_to_process):
|
|
395
|
+
self._check_stop()
|
|
396
|
+
self._emit(i / max(len(years_to_process), 1), f"Procesando año {year}")
|
|
397
|
+
cid, fn, url = catalogs[year]
|
|
398
|
+
self.process_year(year, cid, fn, url)
|
|
399
|
+
|
|
400
|
+
self._emit(1.0, "Descarga terminada")
|
|
401
|
+
log.info("\n¡Pipeline de la EAM finalizado exitosamente!")
|
|
402
|
+
log.info(f"Datos organizados en: {self.data_dir.resolve()}")
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Indicadores industriales y utilidades de panel para la EAM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from ..stats import _as_list
|
|
11
|
+
|
|
12
|
+
GLOSARIO = {
|
|
13
|
+
"Producción bruta": "Valor total de lo producido por el establecimiento en el año (prodbr2).",
|
|
14
|
+
"Consumo intermedio": "Insumos, materias primas, energía y servicios usados en la producción (consin2).",
|
|
15
|
+
"Valor agregado": "Producción bruta − consumo intermedio (valagri). Es la contribución al PIB.",
|
|
16
|
+
"Personal ocupado": "pertotal: permanentes + propietarios + temporales directos + temporales por agencia.",
|
|
17
|
+
"Productividad laboral": "Valor agregado por persona ocupada (valagri / pertotal).",
|
|
18
|
+
"Remuneración media": "(Salarios + prestaciones de permanentes y temporales) / empleados permanentes y temporales.",
|
|
19
|
+
"Participación laboral": "Remuneraciones / valor agregado × 100: qué parte del valor agregado se paga al trabajo.",
|
|
20
|
+
"Panel": "Seguimiento del mismo establecimiento (nordest) a través de los años.",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resumen(df: pd.DataFrame, por: str | Iterable[str] | None = ("anio",)) -> pd.DataFrame:
|
|
25
|
+
"""
|
|
26
|
+
Agregados industriales por grupo (sumas sobre establecimientos).
|
|
27
|
+
Montos en miles de pesos corrientes; ``productividad_laboral`` y
|
|
28
|
+
``remuneracion_media`` en miles de pesos por persona.
|
|
29
|
+
|
|
30
|
+
>>> dd.eam.resumen(eam, por=["anio", "sector"])
|
|
31
|
+
"""
|
|
32
|
+
por = _as_list(por)
|
|
33
|
+
d = df.copy()
|
|
34
|
+
num = lambda c: pd.to_numeric(d[c], errors="coerce") if c in d.columns else np.nan # noqa: E731
|
|
35
|
+
d["_rem"] = num("salpeyte") + num("prespyte")
|
|
36
|
+
sums = {
|
|
37
|
+
"produccion_bruta": "prodbr2", "consumo_intermedio": "consin2", "valor_agregado": "valagri",
|
|
38
|
+
"ventas": "valorven", "personal_total": "pertotal", "empleados_perm_temp": "pperytem",
|
|
39
|
+
"remuneraciones": "_rem", "activos_fijos": "activfi", "inversion_bruta": "invebrta",
|
|
40
|
+
"energia_kwh": "eelec",
|
|
41
|
+
}
|
|
42
|
+
agg = {k: (v, "sum") for k, v in sums.items() if v in d.columns}
|
|
43
|
+
agg["establecimientos"] = ("nordest", "nunique") if "nordest" in d.columns else ("anio", "size")
|
|
44
|
+
if "nordemp" in d.columns:
|
|
45
|
+
agg["empresas"] = ("nordemp", "nunique")
|
|
46
|
+
for v in set(sums.values()):
|
|
47
|
+
if v in d.columns:
|
|
48
|
+
d[v] = pd.to_numeric(d[v], errors="coerce")
|
|
49
|
+
|
|
50
|
+
if por:
|
|
51
|
+
res = d.groupby(por, observed=True, dropna=False).agg(**agg)
|
|
52
|
+
else: # un solo grupo con todos los establecimientos
|
|
53
|
+
res = d.groupby(np.zeros(len(d), dtype=int)).agg(**agg).reset_index(drop=True)
|
|
54
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
55
|
+
if {"valor_agregado", "personal_total"} <= set(res.columns):
|
|
56
|
+
res["productividad_laboral"] = res["valor_agregado"] / res["personal_total"]
|
|
57
|
+
if {"remuneraciones", "empleados_perm_temp"} <= set(res.columns):
|
|
58
|
+
res["remuneracion_media"] = res["remuneraciones"] / res["empleados_perm_temp"]
|
|
59
|
+
if {"remuneraciones", "valor_agregado"} <= set(res.columns):
|
|
60
|
+
res["participacion_laboral"] = 100 * res["remuneraciones"] / res["valor_agregado"]
|
|
61
|
+
if {"valor_agregado", "produccion_bruta"} <= set(res.columns):
|
|
62
|
+
res["tasa_valor_agregado"] = 100 * res["valor_agregado"] / res["produccion_bruta"]
|
|
63
|
+
res = res.replace([np.inf, -np.inf], np.nan)
|
|
64
|
+
return res.reset_index() if por else res
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def participacion(df: pd.DataFrame, variable: str = "valagri", por: str = "sector", tiempo: str = "anio") -> pd.DataFrame:
|
|
68
|
+
"""Participación % de cada grupo en el total de ``variable`` por año."""
|
|
69
|
+
d = df.groupby([tiempo, por], observed=True)[variable].sum().reset_index()
|
|
70
|
+
d["participacion"] = 100 * d[variable] / d.groupby(tiempo)[variable].transform("sum")
|
|
71
|
+
return d
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def crecimiento(df: pd.DataFrame, valor: str, por: str | Iterable[str] | None = None, tiempo: str = "anio") -> pd.DataFrame:
|
|
75
|
+
"""Agrega la tasa de crecimiento anual (%) de ``valor`` dentro de cada grupo."""
|
|
76
|
+
por = _as_list(por)
|
|
77
|
+
d = df.sort_values(por + [tiempo]).copy()
|
|
78
|
+
prev = d.groupby(por)[valor].shift() if por else d[valor].shift()
|
|
79
|
+
d[f"crec_{valor}"] = 100 * (d[valor] / prev - 1)
|
|
80
|
+
return d
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def deflactar(df: pd.DataFrame, columnas: Iterable[str], deflactor: pd.DataFrame | pd.Series,
|
|
84
|
+
on: str | list[str] = "anio", base: float = 100.0, sufijo: str = "_real") -> pd.DataFrame:
|
|
85
|
+
"""
|
|
86
|
+
Convierte valores corrientes a constantes: ``valor_real = valor / deflactor × base``.
|
|
87
|
+
|
|
88
|
+
``deflactor`` puede ser una Serie indexada por año o un DataFrame con las
|
|
89
|
+
columnas de ``on`` (p. ej. ``["anio", "division"]``) y una columna ``deflactor``.
|
|
90
|
+
"""
|
|
91
|
+
on = _as_list(on)
|
|
92
|
+
d = df.copy()
|
|
93
|
+
if isinstance(deflactor, pd.Series):
|
|
94
|
+
deflactor = deflactor.rename("deflactor").rename_axis(on[0]).reset_index()
|
|
95
|
+
d = d.merge(deflactor[on + ["deflactor"]], on=on, how="left")
|
|
96
|
+
for c in columnas:
|
|
97
|
+
d[f"{c}{sufijo}"] = pd.to_numeric(d[c], errors="coerce") / d["deflactor"] * base
|
|
98
|
+
faltan = d["deflactor"].isna().mean()
|
|
99
|
+
if faltan > 0:
|
|
100
|
+
import warnings
|
|
101
|
+
warnings.warn(f"{faltan:.1%} de las filas no encontraron deflactor.")
|
|
102
|
+
return d
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def panel(df: pd.DataFrame, balanceado: bool = False, anios: Iterable[int] | None = None) -> pd.DataFrame:
|
|
106
|
+
"""
|
|
107
|
+
Panel establecimiento-año indexado por (``nordest``, ``anio``).
|
|
108
|
+
|
|
109
|
+
``balanceado=True`` conserva solo establecimientos observados en todos los
|
|
110
|
+
años de ``anios`` (por defecto, todos los años presentes en ``df``).
|
|
111
|
+
|
|
112
|
+
Ojo: la anonimización de ANDA puede cambiar los identificadores entre
|
|
113
|
+
catálogos; revise la persistencia con ``dd.eam.supervivencia``.
|
|
114
|
+
"""
|
|
115
|
+
d = df.dropna(subset=["nordest"]).copy()
|
|
116
|
+
anios = sorted(set(anios) if anios is not None else d["anio"].unique())
|
|
117
|
+
d = d[d["anio"].isin(anios)]
|
|
118
|
+
d = d.drop_duplicates(subset=["nordest", "anio"])
|
|
119
|
+
if balanceado:
|
|
120
|
+
n = d.groupby("nordest")["anio"].nunique()
|
|
121
|
+
d = d[d["nordest"].isin(n[n == len(anios)].index)]
|
|
122
|
+
return d.set_index(["nordest", "anio"]).sort_index()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def supervivencia(df: pd.DataFrame) -> pd.DataFrame:
|
|
126
|
+
"""
|
|
127
|
+
Cuántos establecimientos de cada año reaparecen al año siguiente.
|
|
128
|
+
Caídas bruscas indican cambios de identificador (no cierres reales).
|
|
129
|
+
"""
|
|
130
|
+
ids = df.dropna(subset=["nordest"]).groupby("anio")["nordest"].apply(set).sort_index()
|
|
131
|
+
rows = []
|
|
132
|
+
for (y0, a), (y1, b) in zip(ids.items(), list(ids.items())[1:]):
|
|
133
|
+
rows.append({"anio": y0, "siguiente": y1, "establecimientos": len(a),
|
|
134
|
+
"siguen_al_anio_siguiente": len(a & b), "pct_siguen": 100 * len(a & b) / max(len(a), 1)})
|
|
135
|
+
return pd.DataFrame(rows)
|