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.
@@ -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)