Bun venit la Modulul 7. Am petrecut ultimele douăsprezece lecții învățând să analizăm date: încarci un fișier, îl tai, îl grupezi, îl plotezi, scrii răspunsul. Asta e partea de consumator a lumii datelor. Partea de producător, ingineria care pune un dataset curat pe discul analistului în primul rând, e despre ce sunt următoarele câteva lecții. E și o fracțiune foarte mare din job-urile care chiar plătesc și care au „Python” în titlu în 2026.
Forma cea mai comună pe care o ia munca asta e ETL: extract, transform, load. Trei litere care definesc un pipeline. Citești date de undeva, le schimbi forma și le scrii în altă parte. Aproape orice batch data job din industrie e o variantă a acestuia. Astăzi parcurgem pattern-ul, variantele moderne (ELT, medallion, lakehouse, buzzwords-urile pe care managerul tău le va folosi) și deciziile de design pe care le iei de fiecare dată când te așezi să construiești unul.
Cele trei stagii, decuplate
Motivul pentru care ETL e un model mental util și nu doar o descriere e că cele trei stagii au moduri diferite de eșec și beneficiază de a fi păstrate separate.
Extract e unde vorbești cu lumea exterioară. API-uri pică, baze de date devin lente, blip-uri de rețea apar, fișiere ajung târziu sau scrise pe jumătate. Orice eșuează fiindcă realitatea e dezordonată eșuează aici.
Transform e calcul pur. Cu același input produce mereu același output. Fără dependențe externe, fără ceas, fără rețea. Dacă un transform eșuează, e bug în codul tău, nu zi proastă pe serverul altcuiva.
Load e cealaltă parte a graniței dezordonate: scriere către o destinație care are propriile reguli despre unicitate, ordine, tranzacții și rate limits.
Disciplina de a păstra astea decuplate își plătește prima oară când ceva merge prost. Dacă extract și transform sunt amestecate și API-ul returnează gunoi, nu poți rerula transform-ul pe datele de ieri fără să lovești din nou API-ul. Dacă sunt decuplate, extract scrie raw pe disc, transform citește de pe disc, poți rerula transforms de o sută de ori fără să atingi sursa. E cea mai mare pârghie de design în munca de pipeline și majoritatea începătorilor o ignoră.
def run_pipeline(date: str) -> None:
raw_path = extract(date) # writes raw/2026-04-01.json
clean_path = transform(raw_path) # writes clean/2026-04-01.parquet
load(clean_path) # upserts to warehouse
Trei funcții. Fiecare e independent testabilă, rerulabilă și sărită peste. Acel schelet de patru linii e oasele oricărui pipeline pe care l-am livrat vreodată.
Extract: cum tratezi sursa
Sursele vin în roughly patru arome și fiecare are propriile capcane.
Baze de date (Postgres, MySQL, OLTP-ul companiei tale). Autentificarea e directă; capcana e load pe sursă. Un SELECT * FROM orders naiv pe un DB de producție ocupat îți va aduce un mesaj prietenos pe Slack. Folosește o read replica dacă ai una, preferă query-uri incrementale cu un watermark și evită rularea în timpul orelor de business când poți.
API-uri (REST, GraphQL, SOAP dacă ești ghinionist). Rate limits e bătălia constantă: vezi lecția 39 pentru toolkit-ul complet de retry-and-backoff. Paginarea e mereu prezentă și are vreo patru arome. Autentificarea variază de la „cheie API în header” la „dans OAuth cu refresh tokens”. Majoritatea API-urilor mint despre ceva în docs.
Fișiere (CSV-uri în S3, Parquet într-un data lake, drop-uri de vendor pe FTP). Ușor de extras, greu de știut când sunt „gata”: vezi lecția 38 pentru pattern-ul cu lockfile. Schema drift e ucigașul tăcut: apare o coloană nouă, se schimbă un format de dată și pipeline-ul tău rulează mai departe dar produce nonsens.
Stream-uri (Kafka, Kinesis, change-data-capture). O fiară diferită: continuu mai degrabă decât batch, cu propriile primitive (offsets, consumer groups, replays). În afara scopului acestui modul, dar merită știut că există.
Decizia-cheie la extract e incremental vs. full. Full e mai simplu: la fiecare rulare, ia tot. Merge bine până când „tot” înseamnă un miliard de rânduri. Incremental e mai greu: urmărești un watermark, un timestamp, un ID, un Kafka offset, și iei doar ce s-a schimbat de la ultima dată. Prețul plătit e presiunea de corectitudine: dacă logica de watermark are un bug, ratezi date în tăcere și bug-ul se poate ascunde luni.
Un pattern sigur e incremental cu refresh-uri full periodice. Incremental zilnic, full reload săptămânal. Reload-ul full prinde orice au scăpat incrementalele, iar incrementalele țin costul zilnic mic.
Transform: idealul de funcție pură
Faza de transform e unde Python (sau SQL, mai multe imediat) își câștigă pâinea. Munca e un amestec de:
- Cleaning: aruncă rândurile gunoi, repară tipurile, normalizează string-uri, tratează nulls.
- Normalizing: împarte blob-uri denormalizate în tabele relaționale corecte, sau invers.
- Joining: îmbogățește o sursă cu alta (orders + customers, events + sessions).
- Aggregating: rulează la nivelul granularității cerute de consumatori.
Disciplina care separă transform-urile bune de cele proaste: fă-le funcții pure. Același input, același output. Fără citit din rețea la mijlocul transform-ului. Fără datetime.now() care decide ce ramură e luată. Fără stare globală mutabilă.
De ce? Fiindcă transform-urile pure sunt rerulabile. Dacă un raport downstream e greșit, poți rerula transform-ul de ieri pe datele raw de ieri și obții exact același răspuns greșit, apoi repari codul, rerulezi și obții pe cel corect. Dacă transform-ul citește din API la mijloc de rulare, nu poți: API-ul a mers mai departe.
from pathlib import Path
import polars as pl
def transform(raw_path: Path) -> Path:
df = pl.read_json(raw_path)
df = (
df.filter(pl.col("status") != "deleted")
.with_columns(pl.col("amount").cast(pl.Float64))
.with_columns(pl.col("created_at").str.to_datetime())
.drop_nulls(subset=["customer_id"])
)
out = raw_path.parent.parent / "clean" / raw_path.with_suffix(".parquet").name
out.parent.mkdir(parents=True, exist_ok=True)
df.write_parquet(out)
return out
Funcția aia ia un path și returnează un path. Niciun side effect dincolo de scrierea output-ului. O poți apela de o mie de ori și face același lucru. Asta e proprietatea de protejat.
Load: write modes și idempotență
Trei moduri de scriere pe care le vei întâlni:
- Append: lipești rânduri noi la coadă. Simplu, dar reluările produc duplicate.
- Overwrite: arunci destinația în aer și scrii proaspăt. Sigur de rerulat, dar scump la scară și sparge query-urile downstream cât rulează.
- Upsert (insert sau update pe o cheie): standardul de aur pentru idempotență. Rerulezi aceleași date, obții același rezultat.
Idempotența e cuvântul magic. Un pipeline e idempotent când rulându-l de două ori cu același input produce aceeași stare finală. Fără ea, fiecare retry e o aruncare de zaruri; cu ea, retries sunt gratis. Drumul spre idempotență e aproape mereu „folosește o cheie, fă upsert pe ea”. În Postgres e INSERT ... ON CONFLICT DO UPDATE. În Snowflake e MERGE. Într-o tabelă Delta sau Iceberg e MERGE INTO. Vom folosi varianta Postgres în lecția 38.
ELT: reordonarea modernă
În ultimul deceniu, warehouses au devenit mult mai mari și mult mai ieftine. Snowflake, BigQuery, Databricks, DuckDB la capătul mic: toate îți permit să stochezi cantități enorme de date raw și să rulezi transform-uri SQL pe ele ieftin. Așa că industria a inversat tăcut două litere.
ELT = Extract, Load, Transform. Arunci datele raw în warehouse întâi, apoi transformi cu SQL în interiorul warehouse-ului. Motorul de transformare e warehouse-ul, nu scriptul tău Python.
Câștigul: warehouse-ul e construit pentru asta. Join-uri pe sute de milioane de rânduri sunt rapide. Transform-urile sunt versionate într-un tool ca dbt, rulează pe un schedule și produc documentație gratis. Python devine un strat mai subțire care în mare doar face extract-and-load.
Când să alegi care:
- ELT când destinația e un warehouse adevărat și transform-urile tale sunt în mare formate ca SQL (filtre, join-uri, agregări). Asta e default-ul pentru muncă de analytics în 2026.
- ETL când transform-urile au nevoie de Python real, apelat modele ML, parsat formate ciudate, aplicat logică de business complexă care nu se traduce în SQL. Sau când destinația nu e un warehouse (un API, un index de căutare, un serviciu downstream).
În practică vei face ambele. Job-ul ETL pur Python și modelul ELT în stil dbt trăiesc fericit în aceeași companie.
Arhitectura medallion
Buzzword-ul inventat de Databricks pe care restul industriei l-a adoptat: straturile bronze, silver, gold.
- Bronze: raw, exact așa cum a fost ingerat. Schema-on-read, fidelitate completă, append-only, păstrat pentru totdeauna (sau atât cât te lasă compliance). Dacă ceva downstream e greșit, stratul bronze e sursa de adevăr de unde rerulezi.
- Silver: curățat și conformat. Nume standard de coloane, tipuri reparate, deduplicat, joined cu date de referință. Tot la nivel de rând: un eveniment-customer per rând.
- Gold: gata de analytics. Agregat, denormalizat, organizat în jurul întrebărilor de business. Tabelele pe care le citește un dashboard BI.
Nu ai nevoie de Databricks ca să folosești asta. Pattern-ul merge la fel de bine cu trei scheme Postgres, sau trei prefixe S3, sau trei subdirectoare pe un server. Ideea e stratificarea: fiecare transform citește dintr-un strat și scrie în următorul, iar fiecare strat are un contract clar.
Lakehouse și table formats
A treia bucată de vocabular e lakehouse: un data lake (storage de obiecte ieftin precum S3) cu tranzacții ACID atașate printr-un format de tabelă deschis. Trei formate concurează: Delta Lake (condus de Databricks), Apache Iceberg (condus de Netflix, acum standardul deschis preferat de industrie) și Apache Hudi (condus de Uber).
Ce-ți dau: un layout de fișiere Parquet pe S3 care suportă update-uri tranzacționale, ștergeri, time travel („arată-mi tabela asta cum era marți trecută”) și schema evolution. Obții semantici ca de warehouse pe storage ieftin de lake. În 2026 Iceberg are momentul cel mai puternic: Snowflake, BigQuery, DuckDB și Trino îl citesc nativ.
Nu trebuie să folosești nimic din astea în prima zi. Ar trebui să știi că există ca atunci când un arhitect spune „hai să aterizăm bronze în Iceberg” să dai din cap cu înțelegere.
Deciziile pe care le iei la fiecare pipeline
De fiecare dată când te așezi să construiești unul, apare aceeași mână de alegeri:
- Python sau SQL? Folosește SQL dacă warehouse-ul e destinația și logica e formată ca operații pe seturi. Python pentru orice altceva.
- Programat sau declanșat de eveniment? Cron-style pentru batch-uri previzibile; declanșat de eveniment (sosirea fișierului, webhook, mesaj de coadă) pentru latență mică sau surse neregulate.
- Citit o dată sau incremental? Full e mai simplu; incremental scalează. Watermark cu grijă.
- Care e povestea de recovery? Dacă asta eșuează la jumătate, cum arată rerularea? Dacă nu poți răspunde la asta, n-ai un pipeline încă, ai un script care a mers o dată.
Bucățile unui pipeline real
Un pipeline de producție nu e doar trei funcții; e un ecosistem.
- Orchestrator — chestia care rulează job-urile în ordinea corectă pe schedule-ul corect. Cron pentru v1, Airflow / Prefect / Dagster când dependențele cresc. Lecția 41 acoperă asta.
- Transformations — Python-ul tău și/sau SQL-ul.
- Sinks — destinații. Adesea mai mult de una (warehouse + cache + index de căutare).
- Monitoring — rulează job-ul? Cât durează? Câte date?
- Alerting — sună pe cineva când se sparge la 3 dimineața.
- Lineage / catalog — ce alimentează ce, cine deține, de unde a venit această coloană?
Majoritatea echipelor încep cu orchestrator + transformations și atașează restul pe măsură ce raza de explozie a eșecurilor crește. Nu e nicio rușine în asta. Rușinea e să nu le atașezi când devin necesare.
Unde mergem de aici
În lecția 38 construim un pipeline de ingestion real: fișierele ajung într-un folder, un script Python le ridică, validează, încarcă în Postgres, cu toate pattern-urile care supraviețuiesc contactului cu realitatea. În lecția 39 trecem la partea de extragere din API: retries, rate limits, paginare. Până la lecția 41 avem un orchestrator înfășurat în jurul tuturor și ai construit ceva care n-ar fi nepotrivit într-o firmă reală.
Deocamdată, modelul mental: extrage din exteriorul dezordonat, transformă în Python pur sau SQL, încarcă idempotent la destinație, stratifică tabelele și proiectează fiecare pas așa încât rerulările să fie gratis. Asta e ETL. Restul sunt detalii.
Citări
- Apache Iceberg documentation — https://iceberg.apache.org/docs/ — consultat 2026-05-01.
- Delta Lake documentation — https://docs.delta.io/ — consultat 2026-05-01.
- Databricks medallion architecture — https://www.databricks.com/glossary/medallion-architecture — consultat 2026-05-01.