Vés al contingut

ETL: importadors de dades

El pipeline ETL és el sistema que obté dades de fonts externes (APIs públiques, fitxers CSV) i les emmagatzema a la base de dades com a valors d'indicador.

Visió general

app:run-etl-api {indicador}
  └── RunEtlApiCommand
        └── ETLService::run($indicatorId, $context)
              └── AbstractEtlImporter::run()           [la classe que el suporta]
                    ├── import($def, $indicator, $context)
                    │     └── setMunicipalityValue / setComarcaValue / setProvinceValue
                    │           └── ValuePersister → MunicipalityValue / ComarcaValue / ProvinceValue
                    └── afterSuccess() → AggregationCalculatorService (càlcul automàtic)

L'ETLService és un dispatcher pur: rep una col·lecció d'importadors descoberts automàticament i, per a cada execució, busca el primer que suporta el codi d'indicador demanat.

Arquitectura

Tots els importadors viuen a src/Service/Etl/Importer/ i hereten d'una classe abstracta comuna AbstractEtlImporter. Cada importador declara els indicadors que sap gestionar i, gràcies al sistema d'etiquetes de serveis de Symfony (app.etl_importer), ETLService els recull sense cap registre manual.

php
// src/Service/ETLService.php — el dispatcher complet
class ETLService
{
    public function __construct(private readonly iterable $importers) {}

    public function run(string $indicatorId, EtlContext $context): bool
    {
        $importer = $this->findImporterFor($indicatorId);
        if ($importer) {
            return $importer->run($indicatorId, $context);
        }
        throw new \Exception("Indicator $indicatorId not found");
    }
}

El contracte: AbstractEtlImporter

Qualsevol importador implementa dos mètodes abstractes:

php
// 1. Quins indicadors gestiona
abstract protected function getDefinitions(): array; // array<string, IndicatorDefinition>

// 2. Com es fa el fetch, parsing i persistència per a un indicador
abstract protected function import(
    IndicatorDefinition $def,
    Indicator $indicator,
    EtlContext $context
): void;

El mètode run() de la classe base s'encarrega de tot el cicle de vida comú:

  • Resol o crea les files Target i Indicator a la BBDD a partir de la IndicatorDefinition.
  • Reinicia comptadors (created, updated, unchanged, skipped).
  • Crida import().
  • Fa flush() a Doctrine.
  • Registra els resultats al log amb temps i comptadors.
  • Captura les excepcions i les converteix en retorn false + log d'error.

IndicatorDefinition (DTO)

Estructura les metadades d'un indicador. Tots els camps són readonly:

CampTipusDescripció
indicatorIdstringCodi ("3.4.1")
targetIdstringCodi de la fita (target) ("3.4")
targetNamestringNom de la fita
sdgintNúmero d'ODS (1–17)
indicatorNamestringNom intern
indicatorDescriptionstringDescripció interna
signbooltrue = més alt és millor; false = menys és millor
sourcestringEtiqueta de font ("INE", "IDESCAT", "DIBA", ...)
unitstringUnitat de mesura interna
scaleint|floatHeretat (no s'usa al backend; el frontend determina l'escala)
url?stringURL principal
urls?arrayURLs múltiples per a importadors amb diverses crides
urlInfo?stringURL del catàleg de metadades (descobriment d'anys)
urlComarca?stringURL alternativa per a dades de comarca
urlProv?stringURL alternativa per a dades de província
urlsInfo?arrayURLs d'informació múltiples
extraarrayCamps específics per a un patró concret

EtlContext i ImportScope

EtlContext transporta l'estat de la invocació actual:

CampDescripció
triggerTypeCom s'ha disparat ("cli", "api"...)
scopesQuins àmbits cal importar; null = tots
csvFilename / csvFilename2Ruta a fitxers CSV (per a importadors basats en CSV)

L'enum ImportScope defineix els àmbits possibles:

php
ImportScope::Municipality   // valors per municipi
ImportScope::Comarca        // valors per comarca
ImportScope::Province       // valors per província
ImportScope::Aggregation    // (reservat per a futures fases)

Si scopes és null, s'importen tots els àmbits. Els helpers d'escriptura comproven el scope internament: una crida al mètode no inclòs simplement s'ignora.

Mètodes d'escriptura

Hereus de AbstractEtlImporter, gestionen també el filtre per scope:

php
$this->setMunicipalityValue($def, $indicator, $municipality, $year, $value, $value2, $subindicator);
$this->setComarcaValue($def, $indicator, $comarca, $year, $value, $value2, $subindicator);
$this->setProvinceValue($def, $indicator, $province, $year, $value, $value2, $subindicator);

Helpers afegits a la classe base

MètodePropòsit
getMunicipalityByCode($rawCode)Resol un municipi normalitzant codis: 08001 (5d), 080018 (6d) o 80018 (5d sense zero inicial — quirk d'algunes APIs de Transparència Catalunya). Equivalent a $this->geo->getMunicipalityByCode().
shouldImport(ImportScope $scope)Test explícit (útil per saltar fetches HTTP cars quan el seu scope està exclòs).
afterSuccess($def, $context)Ganxo post-importació. Per defecte crida AggregationCalculatorService per a tots els scopes en què l'indicador estigui classificat a AggregationConfig. Sobrescriviu-lo si cal una lògica addicional.

Comptadors i logs

AbstractEtlImporter manté created, updated, unchanged, skipped. En acabar amb èxit, el run() emet una línia de log:

ETL 3.4.1 OK [IdescatImporter] — created=311 updated=0 unchanged=0 skipped=0 in 4.21s

Serveis auxiliars

Els importadors comparteixen un conjunt de serveis auxiliars que centralitzen les operacions repetides (resolució de codis geogràfics, escriptura a la BBDD, crides a APIs externes). L'AbstractEtlImporter els injecta tots i els exposa via atributs $this->geo, $this->values, $this->do, $this->idescatJson, etc.

ServeiAtributQuè cobreix
EtlUtils(estàtic)toFloat() i les constants BCN_MUNICIPALITY_FILTER / BCN_COMARCA_FILTER
GeoRegistry$this->geoLookups Municipality / Comarca / Province per codi i nom (amb taula d'àlies i caché)
IndicatorFactory$this->indicatorFactorygetOrCreate de Target i Indicator des d'una IndicatorDefinition
ValuePersister$this->valuesUpsert de MunicipalityValue / ComarcaValue / ProvinceValue
IdescatJsonClient$this->idescatJsonAPI JSON d'IDESCAT: anys, mesos, població per edats, afiliats
IdescatTableClient$this->idescatTableEndpoints SSV (tabulars) d'IDESCAT
DoClient$this->doTransparència Catalunya: anys, població per municipi/comarca/província, hectàrees
AggregationCalculatorService$this->aggregationsCàlcul d'agrupacions (cridat automàticament a afterSuccess())

Aquesta organització és el resultat del refactor que va eliminar ETLHelperService (2.497 línies), redistribuint-lo en aquests serveis especialitzats. Vegeu la pàgina dedicada ETL: serveis auxiliars per al detall de cada servei (mètodes, ús i quins importadors el fan servir).

Càlcul automàtic d'agrupacions

A partir del refactor de juny de 2026, les agrupacions es calculen automàticament al final de cada importació. L'AbstractEtlImporter::afterSuccess() (cridat per run() un cop l'import() ha fet flush) consulta AggregationConfig i, per a cada scope (comarca, province, aggregation) en què l'indicador estigui classificat, executa l'estratègia d'agrupació corresponent (vegeu Agrupacions).

Això vol dir que ja no cal cridar app:calculate-aggregation-values manualment després d'un app:run-etl-api: el càlcul es dispara sol amb l'estratègia adequada (Ratio, PopulationWeighted, BeachesWeighted o Average).

Per saltar-se el càlcul automàtic durant el desenvolupament (p. ex. per inspeccionar només els valors municipals abans d'agregar), passeu --skip-aggregation:

bash
php bin/console app:run-etl-api 1.4.1 --skip-aggregation

Els 18 importadors actuals

El nom de cada importador codifica la seva estratègia: de quina font obté les dades i quantes consultes combina. Entendre aquesta nomenclatura ajuda a saber on encaixa un indicador nou.

L'abreviatura Do significa Dades Obertes de Catalunya (plataforma Socrata). Quan es repeteix, indica quantes consultes es fan:

PatróSignificatNumerador (value)Denominador (value2)
Do1 consulta a Dades Obertescamp de la mateixa consultacamp de la mateixa consulta
Dodo2 consultes a Dades Obertes1a consulta2a consulta
Dododo3 consultes a Dades Obertes2 consultes sumades3a (sovint població)
DoIdescatDO + IDESCATDOIDESCAT (sovint població)
DoHermesDO + DIBA HermesDOHermes (renda familiar)
IdescatAPI IDESCATIDESCATIDESCAT
IdescatIdescat2 consultes IDESCAT1a consulta2a consulta
DibaIdescatDIBA + IDESCATDIBAIDESCAT

La idea general: la primera font dóna el numerador i la segona el denominador, de manera que es pugui guardar value i value2 separadament i agregar correctament (vegeu Model de valors).

Per famílies

Font: INE

ImportadorIndicadorsCom funciona
IneImporter1.1.1, 1.2.1, 1.2.2, 10.1.1, 10.1.2, 10.1.3, 10.4.1, 10.4.2Crida a l'API JSON de l'INE (renda, pobresa, desigualtat).

Font: IDESCAT

ImportadorIndicadorsPatró
IdescatImporter1.3.2, 3.4.1, 4.4.1, 4.4.2, 4.4.3, 4.4.4, 4.5.1, 8.2.1, 8.3.3, 9.2.1, 10.1.4API JSON d'IDESCAT. Filtra municipis per la província de Barcelona.
IdescatTableImporter1.3.1, 1.4.2, 2.3.2, 2.3.3, 2.3.4, 3.4.2, 3.4.3, 5.1.1, 5.1.2, 5.c.1, 8.3.1, 8.5.1, 8.5.2, 8.9.1Llegeix taules d'IDESCAT (format tabular).
IdescatIdescatImporter5.c.22 consultes IDESCAT: afiliades dones (num.) + població femenina (den.)

Font: Dades Obertes de Catalunya (DO)

ImportadorIndicadorsPatró
DoImporter2.1.1, 7.3.2, 8.3.2, 8.3.4, 11.3.1, 11.3.2, 11.7.1, 12.5.1, 12.5.2, 15.4.1, 16.7.1, 17.1.11 consulta DO
DoImporter (mode zqa)3.9.1, 3.9.2, 3.9.31 consulta DO per any (qualitat de l'aire), agrupada per zona de qualitat de l'aire (ZQA)
DodoImporter1.2.3, 1.5.1, 3.6.1, 4.a.1, 7.2.2, 7.2.3, 7.2.4, 11.2.1, 11.4.2, 12.1.1, 12.2.1, 17.1.2, 17.17.1, 17.17.22 consultes DO (num. + den.)
DododoImporter7.3.13 consultes DO (dos consums energètics + població)

Mode ZQA (3.9.1/3.9.2/3.9.3 — qualitat de l'aire). Per a cada any (2015 → any natural complet anterior) es consulta el dataset tasf-thgu de la XVPCA, que calcula la mitjana anual de cada estació (mitjana de les 24 mitjanes horàries h01h24). Les estacions de la província 08 s'agrupen per zona de qualitat de l'aire (ZQA) — la mitjana de la ZQA és la mitjana de les seves estacions — i cada municipi hereta el valor de la seva ZQA. El mapa municipi → ZQA es carrega de public/uploads/municipis-zqa.csv (columnes codi_ine, municipi, zqa). Només s'importa l'scope de municipi; comarca, província i agrupacions es deriven amb l'estratègia ponderada per població (exposició). Les ZQA sense cap estació un any determinat queden sense dada (forat).

Fonts combinades (DO + una altra)

ImportadorIndicadorsPatró
DoIdescatImporter2.3.1, 4.1.1, 4.2.1DO (num.) + IDESCAT població (den.)
DoHermesImporter11.1.1DO preus lloguer (num.) + Hermes/DIBA renda familiar (den.)
DibaIdescatImporter1.4.1DIBA prestacions (num.) + IDESCAT atur total (den.)

Font: DIBA

ImportadorIndicadorsPatró
DibaImporter5.5.1API REST de la DIBA (càrrecs electes): compta dones (value) i total (value2)

Fonts: ArcGIS i agències sectorials

ImportadorIndicadorsPatró
PaesImporter7.2.1ArcGIS d'energia (PAES). Energia renovable / total.
AcaImporter6.1.1, 6.4.1Descarrega XLSX de l'Agència Catalana de l'Aigua. 6.1.1 = preu (simple); 6.4.1 = consum/població.
EduCsvImporter4.1.2Descarrega CSV del Departament d'Educació (graduació ESO).

Font: fitxers CSV locals

ImportadorIndicadorsPatró
CsvImporter9.1.1, 9.5.1, 9.8.1, 13.1.1, 13.2.2, 14.1.1, 14.2.1, 15.1.1, 15.1.2, 15.2.1, 16.6.1, 17.2.1CSV genèric pujat a uploads/. Columnes codi_municipi, any, valor_final.
Csv711Importer7.1.1Combinat: ArcGIS consums + CSV preus + IDESCAT renda.
Csv1610Importer16.10.1, 16.7.2CSV de transparència/participació; normalitza i fa mitjana.

Filtre per província de Barcelona

La majoria d'importadors filtren les dades per codi de municipi 08xxx (província de Barcelona), o descarten silenciosament els municipis que no estan a la BBDD local. Si adapteu el projecte a un altre territori, cal revisar aquest filtre a cada importador que vulgueu fer servir.

Afegir un importador nou

Per crear un importador nou només cal una classe que extengui AbstractEtlImporter. No cal tocar ETLService ni cap fitxer de configuració: l'autodescobriment via app.etl_importer ho fa tot.

Vegeu la guia pas a pas a Com crear un indicador nou.

Publicat sota llicència MIT