Un DataFrame pandas e util doar dacă reușești să bagi date în el. Astăzi acoperim familia pd.read_*: cele vreo douăsprezece funcții care transformă un fișier extern sau o interogare la baza de date într-un DataFrame, opțiunile fiecăreia care îi mușcă pe oameni în producție și writer-ele df.to_* corespunzătoare. La final vei ști ce format să citești, ce argumente să setezi explicit chiar și când „valoarea implicită merge” și cum să te descurci cu faptul că datele din lumea reală sunt pline de cazuri murdare pe care documentația le omite cu eleganță.
Forma generală a API-ului e pd.read_<format>(path_or_buffer, **options). Aproape fiecare reader acceptă o cale locală, un URL (HTTP, HTTPS, S3, GCS, Azure dacă pyarrow e instalat) sau un obiect de tip fișier. Aproape toate acceptă acum dtype_backend="pyarrow". Aproape toate acceptă chunksize= pentru streaming pe fișiere mari. Diferențele sunt în opțiunile specifice fiecărui format.
CSV: calul de povară, plin de capcane
pd.read_csv e funcția pe care o vei apela mai des decât oricare alta în cursul ăsta, fiindcă CSV e ce-ți trimit ceilalți. E și cea cu cele mai multe arme cu țeava întoarsă.
import pandas as pd
df = pd.read_csv("sales.csv")
Asta merge pentru un fișier curat. Rar e curat. Argumentele care contează în producție:
dtype: dezactivează inferența de tip pentru coloane cunoscute. Implicit, pandas citește fiecare coloană, scanează tot și ghicește dtype-ul. E în regulă pentru analiză ad-hoc și îngrozitor pentru pipeline-uri de producție: o singură greșeală de scriere pe rândul 50.000 transformă o coloană de întregi în object, strică join-urile din aval, iar bug-ul apare doar în producție. Într-un pipeline, declară-ți dtype-urile:
df = pd.read_csv(
"sales.csv",
dtype={
"order_id": "int64",
"customer_id": "string",
"amount": "float64",
"currency": "category",
},
)
Orice nu enumerezi tot e inferat. Pattern-ul ăsta, explicit pe coloanele care contează, leneș pe restul, e compromisul care funcționează.
encoding: UTF-8 e standardul, dar datele europene sunt deseori Latin-1. Un CSV exportat dintr-un Excel vechi pe o mașină germană sau italiană e frecvent cp1252 (Windows Latin-1) și conține €, à, ñ, ß redate în octeți unici. UTF-8 se va îneca. Dacă vezi UnicodeDecodeError, încearcă:
df = pd.read_csv("export.csv", encoding="cp1252")
# sau
df = pd.read_csv("export.csv", encoding="latin-1")
Dacă nu știi encoding-ul și fișierul e mic, biblioteca chardet poate ghici; pentru fișiere mari, deschide-l într-un editor hex și caută BOM-ul sau octeți non-ASCII.
sep: virgula e implicită, dar CSV-urile europene folosesc punct și virgulă. Fiindcă virgula e separatorul zecimal în multe locale europene, exportatorul CSV alege deseori ; ca separator de câmp. Dacă „CSV-ul” tău vine înapoi ca o singură coloană uriașă cu punct-virgulă în interior, ăsta e motivul:
df = pd.read_csv("italian_sales.csv", sep=";", decimal=",")
(Observă decimal=",": da, argumentul pentru separatorul zecimal există, și da, vei avea nevoie de el.)
parse_dates: explicit, mereu. Pandas nu va parsa automat coloanele de date. Dacă ignori asta, primești string-uri cu dtype object care arată ca date dar nu se sortează, filtrează sau calculează corect:
df = pd.read_csv(
"events.csv",
parse_dates=["created_at", "updated_at"],
date_format="ISO8601", # pandas 2.0+; mai rapid si mai strict decat vechiul auto-parser
)
date_format="ISO8601" merge pentru orice string ISO-8601; pentru formate non-ISO dă pattern-ul strptime explicit (de exemplu date_format="%d/%m/%Y").
chunksize: pentru fișiere care nu încap în memorie. Dacă ai un CSV de 50 GB și 16 GB de RAM, nu-l poți citi întreg. chunksize întoarce un iterator de DataFrame-uri:
total = 0
for chunk in pd.read_csv("massive.csv", chunksize=500_000):
total += chunk["amount"].sum()
500.000 de rânduri e o dimensiune rezonabilă; ajusteaz-o în funcție de lățimea rândului. Vom vedea un exemplu complet de streaming la final.
dtype_backend="pyarrow": pentru cod nou, pur și simplu activează-l. După cum am discutat în lecția 26, backend-ul Arrow îți dă tipuri nullable corecte și string-uri mai rapide. Nu există dezavantaj în 2026:
df = pd.read_csv("sales.csv", dtype_backend="pyarrow")
Parquet: ce ar trebui să folosești de câte ori poți
CSV e formatul pe care ți-l trimit alții. Parquet e formatul pe care ar trebui să-l folosești de câte ori alegerea îți aparține.
Parquet e un format binar coloanar din lumea Hadoop (originar), acum standardul pentru schimbul de date analitice. E coloanar (deci citirea a trei coloane dintr-un fișier cu o mie de coloane e rapidă), stochează schema și dtype-urile (deci fără ghicit la encoding sau type inference), e comprimat implicit (de obicei de 5-10x mai mic decât CSV-ul echivalent) și e lingua franca a oricărei unelte moderne de date: DuckDB, Polars, Spark, Snowflake, BigQuery, Athena, fiecare cloud data lake.
df = pd.read_parquet("sales.parquet", engine="pyarrow")
df.to_parquet("output.parquet", engine="pyarrow", compression="snappy")
Câteva lucruri de știut:
engine="pyarrow". Cealaltă opțiune efastparquet. Folosește pyarrow. E mai rapid, mai compatibil, mai bine întreținut și e ce folosesc toate celelalte unelte.- Opțiuni de compresie:
snappye implicit, rapid și ușor. Foloseștezstdpentru compresie mai bună la un cost CPU puțin mai mare; foloseștegzipdoar dacă vreun consumator din aval cere asta. - Citirea unui subset de coloane e gratuită, datorită layout-ului coloanar:
pd.read_parquet("file.parquet", columns=["a", "b"])citește doar acele coloane de pe disc. - Schema e păstrată, deci nu trebuie să respecifici dtype-urile la citire. Doar asta merită schimbarea formatului.
Dacă echipa ta trimite CSV-uri printr-un pipeline intern, cea mai mare schimbare de inginerie cu impact pe care o poți face e „treci la Parquet”. Spațiul pe disc scade de 5-10x, viteza de citire crește de 5-20x, bug-urile de dtype dispar. Practic nu există motiv să nu o faci.
Excel: lent, uneori necesar
Oamenii editează fișiere Excel. Ți le trimit. Poți fie să insiști pe CSV-uri (vei pierde), fie să înveți să citești bine xlsx.
df = pd.read_excel(
"report.xlsx",
sheet_name="Sales",
skiprows=3, # sari peste titlu si randurile goale pe care le adauga oamenii sus
header=0, # care rand (dupa skip) e antetul coloanelor
usecols="A:F", # doar aceste coloane
)
pd.read_excel are nevoie de openpyxl pentru .xlsx (și xlrd pentru .xls străvechi):
uv add openpyxl
E lent. Citirea unui xlsx cu un milion de rânduri va dura un minut. Dacă un proces insistă pe input Excel, soluția standard e „citește xlsx-ul o dată, salvează ca Parquet, lucrează pe Parquet de aici încolo”.
sheet_name=None întoarce un dict {sheet_name: DataFrame}, ceea ce e util pentru workbook-uri în care fiecare sheet e o regiune sau o lună.
JSON: plat și imbricat
Pentru JSON plat (o listă de obiecte, fiecare cu aceleași chei), pd.read_json merge:
df = pd.read_json("events.json")
JSON Lines, un obiect JSON pe linie, standardul pentru stream-uri de loguri/evenimente, are nevoie de lines=True:
df = pd.read_json("events.jsonl", lines=True)
Pentru JSON imbricat, orice unde un câmp e el însuși un obiect sau o listă de obiecte, unealta potrivită e pd.json_normalize. Aplatizează cheile imbricate în nume de coloane cu puncte:
import json
import pandas as pd
with open("orders.json") as f:
raw = json.load(f)
# raw = [{"id": 1, "customer": {"name": "Ada", "country": "IT"}, "items": [...]}]
df = pd.json_normalize(
raw,
record_path="items", # exploadeaza aceasta lista la un rand per item
meta=["id", ["customer", "name"], ["customer", "country"]],
)
json_normalize e funcția spre care vei reveni de fiecare dată când apelezi un REST API. Merită zece minute cu documentația și un exemplu real ca să-ți intre în reflex.
SQL: cum se face corect
pd.read_sql acceptă fie un string cu o interogare SQL, fie un nume de tabel, plus o conexiune. Folosește SQLAlchemy pentru conexiune: merge cu fiecare bază de date comună și e cea cu care read_sql e testat:
from sqlalchemy import create_engine
import pandas as pd
engine = create_engine("postgresql+psycopg2://user:pass@host:5432/db")
df = pd.read_sql(
"SELECT order_id, amount, created_at FROM orders WHERE created_at >= %(since)s",
engine,
params={"since": "2025-01-01"},
parse_dates=["created_at"],
)
Parametrizează-ți mereu interogările (argumentul params=). Concatenarea input-ului utilizatorului într-un string SQL e o injecție SQL care așteaptă să se întâmple, iar aici nu facem așa ceva.
Pentru seturi de rezultate foarte mari, chunksize= funcționează la fel ca la read_csv:
for chunk in pd.read_sql("SELECT * FROM events", engine, chunksize=100_000):
process(chunk)
O notă pentru 2026: multă muncă analitică s-a mutat pe DuckDB, care poate interoga fișiere Parquet și DataFrame-uri pandas direct cu SQL. Dacă faci analitică in-process, duckdb.query("SELECT ... FROM df").to_df() e deseori răspunsul mai curat decât read_sql împotriva unei baze de date la distanță. Îl vom vedea în treacăt mai târziu în curs.
Cloud storage
Dacă pyarrow e instalat, fiecare reader acceptă URL-uri cloud nativ:
df = pd.read_parquet("s3://my-bucket/sales/2025/03/") # toata partitia
df = pd.read_csv("gs://my-bucket/users.csv")
df = pd.read_parquet("az://my-container/events.parquet")
Vei avea nevoie de credențiale configurate în locurile obișnuite (~/.aws/credentials, GOOGLE_APPLICATION_CREDENTIALS, etc.), cum se așteaptă SDK-ul de dedesubt.
Scrierea: familia to_*
Fiecare reader are un writer corespunzător:
df.to_csv("out.csv", index=False) # treci mereu index=False daca nu vrei chiar indexul in fisier
df.to_parquet("out.parquet", engine="pyarrow", compression="snappy")
df.to_excel("out.xlsx", sheet_name="Results", index=False)
df.to_json("out.jsonl", orient="records", lines=True)
df.to_sql("orders", engine, if_exists="append", index=False)
Cea mai comună greșeală la scriere e să lași index=True (implicit), ceea ce aruncă un RangeIndex pandas în fișier ca o primă coloană fantomă fără nume. Treci index=False decât dacă indexul tău cară informație.
Un exemplu real: streaming al unui CSV de 5 GB în Parquet
Punând totul cap la cap, un script care citește un CSV prea mare în chunk-uri, deduplică după un order_id și scrie un singur fișier Parquet:
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
reader = pd.read_csv(
"big_orders.csv",
chunksize=500_000,
dtype={
"order_id": "int64",
"customer_id": "string",
"amount": "float64",
"currency": "category",
},
parse_dates=["created_at"],
date_format="ISO8601",
dtype_backend="pyarrow",
)
seen_ids: set[int] = set()
writer: pq.ParquetWriter | None = None
try:
for chunk in reader:
# elimina duplicatele din interiorul chunk-ului
chunk = chunk.drop_duplicates("order_id")
# elimina duplicatele intre chunk-uri
chunk = chunk[~chunk["order_id"].isin(seen_ids)]
seen_ids.update(chunk["order_id"].tolist())
table = pa.Table.from_pandas(chunk, preserve_index=False)
if writer is None:
writer = pq.ParquetWriter("orders.parquet", table.schema, compression="snappy")
writer.write_table(table)
finally:
if writer is not None:
writer.close()
Asta face streaming pe CSV, nu ține niciodată mai mult de 500.000 de rânduri în memorie odată și produce un fișier Parquet curat la final. Setul seen_ids e singurul lucru care crește cu datele, iar 5 milioane de întregi pe 64 de biți înseamnă 40 MB, în regulă pe orice mașină. Pentru fișiere cu adevărat enorme ai trece la un Bloom filter; pentru „CSV de 5 GB care e o pacoste de mânuit”, asta e suficient.
Ce urmează
Acum poți obține un DataFrame. Lecția 28 e selecția și filtrarea: bestiarul loc / iloc / boolean-mask, partea pandas în care scurtăturile sintactice concurează între ele și mult cod care arată deștept e scris din motive greșite. Vom găsi pattern-urile care merită păstrate.
Lectură suplimentară
- pandas: IO tools: fiecare reader și writer, cu lista lor completă de opțiuni.
- Documentația Apache Parquet: formatul, pe scurt.
- pyarrow.fs: stratul de filesystem cloud pe care pandas îl folosește pe sub capotă.
Ne vedem vineri.