Fiecare utilizator pandas confuz pe care l-am ajutat vreodată la serviciu era confuz la același nivel. Nu la suprafața API-ului, puteau citi docs-urile, ci la nivelul ce sunt aceste obiecte. Aveau o senzație vagă că df["col"] îți dă „o coloană,” că df.loc[5] îți dă „un rând,” că există ceva numit „index” care în mare parte pare enervant. Modelul din capul lor era „un tabel 2D, ca Excel-ul.” Când acel model eșua, și eșuează în momentul în care faci o operație aritmetică între două felii care nu au aceeași ordine de rânduri, ricoșau și întindeau mâna spre df.values ca să scape în NumPy, care e locul unde merg să moară intuițiile competente.
Pandas are două structuri de date fundamentale: Series și DataFrame. Sunt simple. Astăzi le explicăm. Restul Modulului 5 va avea apoi sens.
Series, un array etichetat 1-D
O Series este un array unidimensional de valori, plus un array unidimensional de etichete (indexul) de aceeași lungime. Atât. Fiecare valoare are o etichetă, iar eticheta este ce face pandas diferit de NumPy.
import pandas as pd
s = pd.Series([10, 20, 30, 40])
print(s)
0 10
1 20
2 30
3 40
dtype: int64
Coloana din stânga (0, 1, 2, 3) este indexul. Când nu specifici unul, pandas îți face un RangeIndex default de la 0. Coloana din dreapta sunt valorile. Linia dtype de jos îți spune tipul acelor valori.
Poți specifica propriul tău index, string-uri, date, orice e hashable:
revenue = pd.Series(
[120, 135, 148, 162],
index=["Q1", "Q2", "Q3", "Q4"],
name="revenue_2025",
)
print(revenue)
Q1 120
Q2 135
Q3 148
Q4 162
Name: revenue_2025, dtype: int64
Poți construi una dintr-un dict, cheile devin automat indexul:
revenue = pd.Series({"Q1": 120, "Q2": 135, "Q3": 148, "Q4": 162})
Indexul este killer feature-ul, și este și ce face pandas surprinzător. Operațiile între Series se aliniază după etichetă, nu după poziție. Privește:
revenue_2025 = pd.Series({"Q1": 120, "Q2": 135, "Q3": 148, "Q4": 162})
revenue_2024 = pd.Series({"Q2": 100, "Q3": 110, "Q4": 125, "Q1": 95})
growth = revenue_2025 - revenue_2024
print(growth)
Q1 25
Q2 35
Q3 38
Q4 37
dtype: int64
Observă că revenue_2024 avea trimestrele într-o ordine diferită. N-a contat. Pandas a potrivit Q1 cu Q1 și așa mai departe, după etichetă. Dacă ai fi făcut același lucru în NumPy, ai fi primit prostii, Q1 minus Q2, Q2 minus Q3, în tăcere. Asta e schimbul pe care îl face pandas: un obiect ușor mai complex, în schimbul unei aritmetici care înseamnă ce ai vrut tu.
Ce se întâmplă cu etichete nepotrivite?
a = pd.Series({"x": 1, "y": 2})
b = pd.Series({"y": 10, "z": 20})
print(a + b)
x NaN
y 12.0
z NaN
dtype: float64
Etichetele care nu sunt prezente în ambele Series produc NaN. Dtype-ul a fost promovat la float64 fiindcă întregii nu pot fi NaN în pandas pe NumPy, un footgun celebru pe care îl vom repara când discutăm backend-ul Arrow imediat.
Dtype-uri, ce fel de valori
Fiecare Series are un dtype, iar pe care îl ai controlează mult comportament. Cele comune în pandas:
int64, întregi semnați pe 64 de biți. Default când toate valorile sunt numere întregi și nu există null-uri.float64, virgulă mobilă pe 64 de biți. Default când există zecimale, sau când o coloană de int are NaN.object, o portiță NumPy „este un obiect Python.” În practică, aproape întotdeauna înseamnă string-uri, uneori tipuri amestecate. Lent.string[python]/string[pyarrow], dtype-uri proprii pentru string. Cel pe Arrow e semnificativ mai rapid și e default-ul modern.bool, booleene.category, pentru valori care se repetă dintr-un set fix mic (gândește-te la: coduri de țară, enumerări de status). Stocate ca întregi sub capotă cu un tabel de lookup; economisesc enorm de multă memorie pe seturi reale de date.datetime64[ns], timestamp-uri cu precizie de nanosecundă.timedelta64[ns], durate.
Apoi echivalentele nullable, tipuri de extensie pandas, literă mare la început: Int64, Float64, boolean, string. Acestea suportă pd.NA (o valoare „missing” propriu-zisă) fără să schimbe dtype-ul. Erau puntea între pandas vechi pe NumPy și pandas nou pe Arrow; le mai vezi, dar pentru cod nou, mergi direct la backend-ul Arrow.
# Old style — int column with a missing value becomes float
pd.Series([1, 2, None])
# 0 1.0
# 1 2.0
# 2 NaN
# dtype: float64
# Arrow backend — stays an integer, gets a proper NA
pd.Series([1, 2, None], dtype="int64[pyarrow]")
# 0 1
# 1 2
# 2 <NA>
# dtype: int64[pyarrow]
A doua e ce vrei de fapt. String-urile sunt câștigul mai mare:
# Old style: NumPy "object" array of Python str instances
pd.Series(["alpha", "beta", "gamma"]) # dtype: object
# Arrow strings: tighter memory, ~10x faster string ops
pd.Series(["alpha", "beta", "gamma"], dtype="string[pyarrow]")
Dacă pui pd.options.future.infer_string = True aproape de capul scriptului tău, pandas va folosi automat dtype-ul de string Arrow când citește coloane de text. Fă asta. Tu cel din viitor vei avea de ce să te îngrijorezi mai puțin.
DataFrame, un dict de Series aliniate
Un DataFrame este un dicționar de Series care toate împart același index de rânduri. Acela e tot modelul mental. Fiecare coloană este o Series, fiecare coloană are aceeași lungime, și fiecare coloană are aceleași etichete de rând.
import pandas as pd
df = pd.DataFrame({
"country": ["IT", "FR", "DE", "ES"],
"population_m": [59, 68, 84, 47],
"in_eurozone": [True, True, True, True],
})
print(df)
country population_m in_eurozone
0 IT 59 True
1 FR 68 True
2 DE 84 True
3 ES 47 True
L-ai creat dintr-un dict-de-liste. Cheile au devenit nume de coloane, valorile au devenit Series, indexul de rânduri a devenit default un RangeIndex de la 0.
Poți construi un DataFrame și dintr-o listă de dicts (un dict per rând), ceea ce e mai aproape de ce primești înapoi de la un API JSON:
rows = [
{"country": "IT", "population_m": 59, "in_eurozone": True},
{"country": "FR", "population_m": 68, "in_eurozone": True},
{"country": "DE", "population_m": 84, "in_eurozone": True},
]
df = pd.DataFrame(rows)
Sau dintr-un array NumPy 2D (atunci specifici tu coloanele):
import numpy as np
arr = np.array([[1, 2, 3], [4, 5, 6]])
df = pd.DataFrame(arr, columns=["a", "b", "c"])
Sau, cel mai frecvent în viața reală, dintr-un fișier:
df = pd.read_csv("countries.csv")
df = pd.read_parquet("countries.parquet")
Asta facem în lecția 27.
Atributele pe care le vei atinge zilnic
Un DataFrame are un set mic de atribute pe care le vei căuta constant:
df.shape # (4, 3) — rows, columns
df.dtypes # the dtype of each column, as a Series
df.index # the row labels (a RangeIndex(0, 4) here)
df.columns # the column labels (Index(['country', 'population_m', 'in_eurozone']))
df.values # the underlying NumPy array (avoid using this; reach for .to_numpy() if you need it)
df.info() # printed summary: dtypes, non-null counts, memory usage
df.head(3) # first 3 rows
df.tail(3) # last 3 rows
df.describe() # summary statistics for numeric columns
df.info() și df.head() sunt cele două lucruri pe care le rulezi pe orice DataFrame nou, de fiecare dată. Sunt cum verifici ce ai primit cu adevărat, înainte de orice calcul.
Selectarea unei coloane îți dă o Series. Două coloane îți dau un DataFrame.
Asta îi încurcă pe începători și e critic de internalizat:
df["country"] # Series — 1-D
df[["country"]] # DataFrame — 2-D, with one column
df[["country", "population_m"]] # DataFrame, two columns
Paranteze drepte simple cu un string returnează o Series. Paranteze drepte duble cu o listă de string-uri, chiar și o listă de unul, returnează un DataFrame. Multe funcții acceptă unul sau altul dar nu pe amândouă, iar „i-am pasat df["col"] și s-a plâns că voia un lucru 2D” e o întâmplare zilnică la început.
Oglinda se întâmplă cu rândurile: df.loc[3] returnează o Series (un rând), df.loc[[3]] returnează un DataFrame (un tabel cu un singur rând). Vom acoperi selectarea de rânduri ca lumea în lecția 28.
Indexul, plictisitor, important
Fiecare DataFrame are un Index pentru rândurile sale, iar numele coloanelor sunt ele însele un Index pentru coloane. Indexul de rânduri default este un RangeIndex (0, 1, 2, …), dar poți folosi orice: date, coduri de țară, ID-uri de utilizator, combinații pe niveluri multiple ale tuturor celor trei.
df = pd.DataFrame(
{"population_m": [59, 68, 84, 47]},
index=["IT", "FR", "DE", "ES"],
)
df.loc["IT"] # selects the IT row by label
Un MultiIndex este un index cu niveluri multiple, de exemplu (țară, an), (regiune, magazin, dată). Așa faci date ierarhice sau de tip pivot-table în pandas.
df = pd.DataFrame(
{"sales": [100, 110, 95, 105]},
index=pd.MultiIndex.from_tuples(
[("IT", 2024), ("IT", 2025), ("FR", 2024), ("FR", 2025)],
names=["country", "year"],
),
)
df.loc["IT"] # all rows for IT
df.loc[("IT", 2025)] # specific (country, year)
MultiIndex-urile sunt puternice și un pic capricioase. Ne vom întâlni cu ele din nou în lecția 32 când restructurăm date.
Opinia în 2026: nu fi fițos cu indexul. Multe tutoriale moderne de pandas și utilizatorii Polars argumentează că în mare parte ar trebui să lași indexul ca un RangeIndex default și să tratezi coloanele tale „cheie” ca pe coloane obișnuite. E un model mental mai curat. Folosește un index semnificativ când beneficiezi cu adevărat (time series, join-uri după etichetă, agregări ierarhice); sari peste el altfel.
Backend NumPy vs backend Arrow, cu un exemplu
Ca să facem distincția între backend-uri concretă:
import pandas as pd
# NumPy-backed (the historical default)
df_np = pd.DataFrame({
"name": ["Ada", "Linus", "Grace"],
"score": [98, None, 92],
})
print(df_np.dtypes)
# name object
# score float64 <-- promoted to float because of the None
# dtype: object
# Arrow-backed
df_pa = pd.DataFrame({
"name": ["Ada", "Linus", "Grace"],
"score": [98, None, 92],
}).convert_dtypes(dtype_backend="pyarrow")
print(df_pa.dtypes)
# name string[pyarrow]
# score int64[pyarrow] <-- stays an integer, with proper NA
# dtype: object
Ambele DataFrame-uri se comportă la fel pentru aproape fiecare operație pe care o vei face; cel pe Arrow are dtype-uri mai curate, string-uri mai rapide, și o reprezentare mai sinceră a datelor lipsă. De acum încolo, fiecare exemplu din acest curs care nu demonstrează specific un comportament al backend-ului NumPy va presupune că lucrezi pe Arrow. Setează dtype_backend="pyarrow" pe apelurile tale read_* și ești acolo.
Ce urmează
Acum că știi ce ții în mână când ții un DataFrame, lecția 27 acoperă cum să obții efectiv unul, pd.read_csv, pd.read_parquet, capcanele din fiecare format, și de ce Parquet este răspunsul corect aproape de fiecare dată când e permis ceva mai bun decât CSV.
Lectură suplimentară
- pandas: Intro to data structures, introducerea oficială, merită citită o dată.
- pandas dtypes, referința canonică.
- Dtype-uri pe PyArrow, default-ul modern explicat ca lumea.
Ne vedem marți pentru lumea largă a pd.read_*.