NumPy et va donar velocitat, però a canvi de números anònims: la fila 2 "era" El Quixot només perquè tu ho recordaves. pandas embolica aquests arrays en dues estructures amb nom — la Series i el DataFrame — i hi afegeix tot el que una anàlisi real necessita: llegir CSV en una línia, dates de veritat, neteja de dades brutes i, sobretot, groupby. Aquesta és la lliçó on per fi responem amb dades les preguntes que l'Anna arrossega des del tancament de M10: quin dia de la setmana ven més, quin títol triomfa a la web i quin es consulta molt però es compra poc.
Contingut
- Series i DataFrame: anatomia
read_csv: elDictReaderde M6 amb esteroides- Inspecció:
head,info,describe,shape - Selecció: columnes,
loc/iloci màscares booleanes - Columnes noves i dates amb
.dt - Neteja:
isna,fillna,dropna,duplicated groupby: respondre les preguntes de l'Annasort_valuesivalue_countsmerge: unir amb el catàlegto_csv: desar resultats
Series i DataFrame: anatomia
Una Series és un array de NumPy amb etiquetes (un índex); un DataFrame és una taula: diverses Series que comparteixen índex, una per columna.
flowchart LR
subgraph DF["DataFrame df"]
direction LR
I["índex<br>0<br>1<br>2"] --- C1["data<br>2026-01-02<br>2026-01-02<br>2026-01-03"] --- C2["titol<br>L'Odissea<br>Hamlet<br>El Quixot"] --- C3["quantia<br>12.50<br>19.90<br>15.71"]
end
C3 -.->|"cada columna és<br>una Series<br>(un ndarray + índex)"| S["df['quantia']"]
import pandas as pd # alies universal, com np
s = pd.Series([12.50, 9.95, 15.90, 21.00],
index=["L'Odissea", "Hamlet", "El Quixot", "Faust"])
print(s["Faust"]) # 21.0 -> acces per etiqueta, com un dict (M4)
print(s.mean()) # 14.8375 -> matematiques vectoritzades, com NumPy (11-02)La Series és l'híbrid perfecte entre el dict de M4 (accés per clau) i el ndarray de 11-02 (vectorització). El DataFrame, entre una llista de dicts (les files de DictReader) i una matriu 2D.
read_csv: el DictReader de M6 amb esteroides
A M6 llegies vendes.csv amb csv.DictReader, convertint tipus a mà fila a fila. La promesa era que pandas ho faria "amb esteroides". Aquí tens el bescanvi:
Una línia i ja està tot: el fitxer obert i tancat (el with de M8 hi va inclòs), la capçalera detectada, unitats com a enter, quantia com a float i — gràcies a parse_dates — data com a data de veritat, no com a text. El que a M6 eren quinze línies de bucle i conversions.
csv.DictReader (M6) |
pd.read_csv |
|---|---|
| Itera fila a fila (lazy, memòria mínima) | Carrega tot en memòria de cop |
Tot és str: converteixes tu |
Infereix tipus; parse_dates per a les dates |
| Una pregunta = un bucle | Una pregunta = una expressió |
L'honestedat de M8 continua vigent: si el fitxer no cap en memòria, el pipeline de generadors guanya. Per a les 487 files de Papyrus (i per a gairebé tot el que càpiga a la RAM, que avui és moltíssim), pandas és l'eina.
Inspecció: el primer pas en carregar qualsevol dada
data titol unitats quantia canal
0 2026-01-02 L'Odissea 1 12.50 botiga
1 2026-01-02 Hamlet 2 19.90 web
2 2026-01-03 El Quixot 1 15.71 botigadf.info() # tipus i nuls per columna: data datetime64[ns], unitats int64...
print(df["quantia"].describe())count 487.000000
mean 15.030000
std 8.420000
min 9.830000
25% 9.950000
50% 12.500000
75% 15.900000
max 63.600000describe és l'estadística de 11-01 en una sola crida — i confirma el que hi vam veure: mitjana (15.03) per sobre de la mediana (12.50), amb un màxim de 63.60 € (un pack de Sant Jordi) estirant cap amunt.
Selecció: columnes, loc/iloc i màscares
df["titol"] # una columna -> Series
df[["titol", "quantia"]] # diverses columnes -> DataFrame (ull: llista dins de claudators)Per a les files hi ha dos accessors, i confondre'ls és l'entrebanc clàssic:
loc |
iloc |
|
|---|---|---|
| Selecciona per | etiqueta de l'índex | posició entera |
df.loc[3] / df.iloc[3] |
La fila amb etiqueta 3 |
La quarta fila, sigui on sigui |
Rang [0:3] |
Inclou l'extrem final | Exclou el final (com les llistes) |
| Ús típic | df.loc[mascara, "quantia"] |
"dona'm les 10 primeres files" |
Amb l'índex per defecte (0, 1, 2...) tots dos semblen iguals; així que filtres o reordenes, deixen de ser-ho — l'índex conserva les etiquetes originals.
I l'herència directa de 11-02: les màscares booleanes funcionen idèntiques, amb &, |, ~ i els seus parèntesis:
vendes_web = df[df["canal"] == "web"]
grans_abril = df[(df["quantia"] > 30) & (df["data"].dt.month == 4)]
print(len(vendes_web), len(grans_abril)) # 195 22Columnes noves i dates amb .dt
Crear una columna és assignar a un nom nou; l'expressió es vectoritza sola:
df["quantia_unitaria"] = (df["quantia"] / df["unitats"]).round(2)
# Es va cobrar a tarifa de soci? (els quatre valors canonics de M3)
df["es_soci"] = df["quantia_unitaria"].isin([12.35, 9.83, 15.71, 20.75])
print(df["es_soci"].sum()) # 141 vendes a socisLes columnes datetime64 tenen l'accessor .dt amb tot el que a 11-01 fèiem amb date.fromisoformat a mà:
df["mes"] = df["data"].dt.month
df["dia_setmana"] = df["data"].dt.dayofweek # 0=dilluns ... 6=diumenge
dies = ["dl", "dt", "dc", "dj", "dv", "ds", "dg"]
df["dia_nom"] = df["dia_setmana"].map(dict(enumerate(dies)))Neteja: el món real arriba brut
El fitxer del mòdul està polit, però el bolcat en brut que l'Anna consolida cada nit — vendes_2026_raw.csv, 495 files — porta els sospitosos habituals de M7: files duplicades per un doble clic a la web i vendes amb la quantia buida per una fallada en desar.
raw = pd.read_csv("dades/vendes_2026_raw.csv", parse_dates=["data"])
print(raw.shape) # (495, 5)
print(raw.duplicated().sum()) # 3 -> files EXACTAMENT repetides
print(raw["quantia"].isna().sum()) # 5 -> forats (NaN) a quantiaNaN (Not a Number) és el marcador de "dada absent". Les opcions, amb criteri:
net = raw.drop_duplicates() # 495 -> 492 files
net = net.dropna(subset=["quantia"]) # 492 -> 487 files
print(net.shape) # (487, 5) -> el nostre vendes_2026.csvI fillna? Emplenar té sentit quan coneixes el valor correcte (raw["canal"].fillna("botiga") si saps que la fallada només passa a caixa) o un neutre defensable. Inventar-se una quantia mitjana per a una venda concreta, en canvi, contamina els totals: aquí eliminar és més honest que imputar. A M7 aquestes files corruptes llançaven excepcions; ara tenen tractament estadístic. Documenta sempre quantes files treus i per què.
groupby: respondre les preguntes de l'Anna
groupby parteix el DataFrame en grups, aplica una agregació a cadascun i recompon el resultat. És el patró "dict acumulador" de M4 i el Counter en una sola crida. Torn de les preguntes del tancament de M10.
Què es ven més?
Quin dia de la setmana ven més? — pregunta 2 de l'ham, resposta:
Dissabte, amb 130 unitats — gairebé el triple que el dilluns. El cap de setmana (ds+dg) concentra el 44 % de les unitats: la Marta ja sap quins dies no pot faltar ningú a la botiga. (El reindex(dies) força l'ordre cronològic; sense ell, pandas ordena alfabèticament.)
Quin títol triomfa a cada canal? — agrupar per dues columnes i pivotar per llegir-ho còmode:
A la botiga mana El Quixot (121); a la web, sorpresa: L'Odissea (75) supera el Quixot (61). Hipòtesi de l'Anna: a la web hi pesa el públic del club de lectura de la Júlia. Sigui quina sigui la causa, l'aparador de la home de Flask ja sap quina portada posar.
Què es consulta i no es compra? — pregunta 1 de l'ham. Les vendes soles no basten (ho vam preveure a 11-01): calen les visites a /llibre/<titol> dels logs de M10:
visites = pd.Series({"El Quixot": 260, "L'Odissea": 220,
"Faust": 210, "Hamlet": 150}, name="visites_web")
compres_web = df[df["canal"] == "web"].groupby("titol")["unitats"].sum()
print((compres_web / visites * 100).round(1).sort_values())Aquí ho tens: Faust es mira molt (210 visites, tercer del catàleg) però només l'11.9 % de les visites acaben en compra — un terç de la conversió dels altres. Amb el seu preu de 21.00 €, la hipòtesi del preu s'imposa sola. Dada, no opinió: exactament el que l'Anna demanava.
sort_values i value_counts
df.sort_values("quantia", ascending=False).head(3) # les 3 vendes mes grans
df.sort_values(["data", "quantia"]) # multiclau, com sorted(key=...) de M3
print(df["titol"].value_counts())value_counts és literalment el Counter de M4 amb esteroides: compta operacions (files), ja ordenat. Ull al matís respecte al groupby d'abans: 168 vendes del Quixot sumen 182 unitats — comptar files i sumar unitats són preguntes diferents.
merge: unir amb el catàleg
Les vendes no saben l'autor ni el preu de llista. El catàleg de M5/M6, sí:
cataleg = pd.DataFrame({
"titol": ["L'Odissea", "Hamlet", "El Quixot", "Faust"],
"autor": ["Homer", "Shakespeare", "Cervantes", "Goethe"],
"preu": [12.50, 9.95, 15.90, 21.00],
})
cataleg["preu_soci"] = (cataleg["preu"] * 1.04 * 0.95).round(2)
# la formula canonica de M3 -> [12.35, 9.83, 15.71, 20.75]
complet = df.merge(cataleg, on="titol", how="left")
print(complet.groupby("autor")["quantia"].sum().round(2).sort_values(ascending=False))autor
Cervantes 2881.98
Homer 1918.90
Goethe 1319.20
Shakespeare 1199.53
Name: quantia, dtype: float64merge emparella files per la columna comuna (on="titol"), com el JOIN de les bases de dades que Django feia per tu a M10. how="left" conserva totes les vendes encara que algun títol no fos al catàleg (quedaria amb NaN — i isna() t'ho xivaria: la neteja i el merge es vigilen mútuament).
to_csv: tancar el cercle
resum = df.groupby("titol")["unitats"].sum().sort_values(ascending=False)
resum.to_csv("informes/unitats_per_titol.csv")El directori informes/ de M6 rep ara fitxers que neixen d'una línia, no d'un bucle. Consell: en DataFrames amb índex numèric sense significat, to_csv(..., index=False) evita una columna fantasma Unnamed: 0 en rellegir.
Errors Comuns i Consells
SettingWithCopyWarning. Apareix en encadenardf[df["canal"]=="web"]["quantia"] = ...: estàs escrivint en una còpia i el canvi es perd. Forma correcta:df.loc[df["canal"]=="web", "quantia"] = ...— una sola indexació ambloc.- Oblidar
parse_datesi agrupar per text."2026-04-23" > "2026-04-03"funciona per sort alfabètica, però.dt.dayofweekno existeix en cadenes. Sidf["data"].dtypeésobject, torna alread_csv. locvsilocdesprés de filtrar. Després devendes_web = df[...],vendes_web.iloc[0]és la seva primera fila, peròvendes_web.loc[0]pot llançarKeyErrorsi l'etiqueta 0 s'ha filtrat. Si no necessites l'índex vell:reset_index(drop=True).NaNcontagiós i silenciós. Qualsevol operació ambNaNdonaNaN, i les agregacions l'ometen sense avisar (elmean()d'una columna amb forats usa només els presents). Fesdf.isna().sum()només carregar, sempre.- Comparar amb
== NaN.df["quantia"] == np.nanés sempreFalse(NaN no és igual ni a si mateix). Usaisna()/notna(). - Confondre
value_counts(files) ambgroupby(...).sum()(magnituds). 168 operacions ≠ 182 unitats. Tria segons la pregunta.
Exercicis
- Calcula el tiquet mitjà (quantia mitjana per operació) de cada canal a
vendes_2026.csv. Confirma la hipòtesi de l'exercici 2 de 11-01 sobre per què difereixen botiga i web? Afegeix la mitjana d'unitatsper canal per raonar-ho. - Quin mes va facturar més (columna
quantia)? I si excloguessis les vendes de la setmana de Sant Jordi (del 20 al 26 d'abril)? Usa.dt.monthi una màscara amb dates (pd.Timestamp). - Amb
mergei el catàleg: crea la columnadescompte_aplicat = preu * unitats - quantia(quant va deixar de cobrar Papyrus en cada venda respecte al preu de llista) i calcula el descompte total acumulat per títol. A quin títol "li costa" més diners la tarifa de soci?
Solucions
-
El tiquet web és menor, però les unitats per operació són gairebé iguals: la diferència no és que a la botiga es comprin més exemplars, sinó la barreja de títols — la web ven proporcionalment més Odissea i Hamlet (barats) i la botiga més Quixot. La hipòtesi de 11-01 es corregeix amb dades: per a això servia el cicle.print(df.groupby("canal")["quantia"].mean().round(2)) # botiga 15.89 # web 13.74 print(df.groupby("canal")["unitats"].mean().round(2)) # botiga 1.07 # web 1.07 -
L'abril guanya amb la setmana de Sant Jordi a dins; sense ella, l'abril es desinfla i guanya el maig. Conclusió de negoci: el "bon abril" és en realitat una bona setmana — la comanda d'estoc s'ha de concentrar allà, no repartir-se pel mes. (print(df.groupby(df["data"].dt.month)["quantia"].sum().round(2).idxmax()) # 4 fora_sj = ~df["data"].between("2026-04-20", "2026-04-26") print(df[fora_sj].groupby(df["data"].dt.month)["quantia"].sum().idxmax()) # 5idxmaxés l'argmaxde 11-02, però retornant l'etiqueta.) -
L'acumulat més gran correspon a El Quixot: no és el més descomptat per unitat, però és el que més socis compren — el volum mana. Nota fina: el "descompte" per unitat surt sorprenentment petit (0.19 € en el Quixot, no el 5 % que esperaries) perquè la tarifa de soci porta l'IVA a dins i el preu de llista no; detectar aquesta raresa mirant les dades — i preguntar per què — és exactament el reflex que aquesta lliçó et volia entrenar.complet = df.merge(cataleg[["titol", "preu"]], on="titol", how="left") complet["descompte_aplicat"] = (complet["preu"] * complet["unitats"] - complet["quantia"]).round(2) print(complet.groupby("titol")["descompte_aplicat"].sum().round(2) .sort_values(ascending=False))
Conclusió
pandas ha convertit les preguntes de l'Anna en respostes: el dissabte és el rei de la setmana (130 unitats), la web té el seu propi supervendes (L'Odissea, 75 davant les 61 del Quixot), i Faust és l'aparadorista del catàleg — 210 visites i només un 11.9 % de conversió, amb el seu preu com a principal sospitós. Pel camí: Series i DataFrames com a arrays amb nom, read_csv saldant la promesa de M6, loc/iloc, màscares heretades de 11-02, neteja honesta (495 → 487 files, documentades), groupby/value_counts com els acumuladors de M4 amb esteroides i merge unint vendes amb catàleg. Però aquestes respostes són taules de números, i l'Anna no llegirà taules: vol veure el pic de Sant Jordi, comparar barres d'un cop d'ull, penjar un panell a la rebotiga. Convertir números en històries visuals — i no mentir en fer-ho — és Matplotlib, la propera lliçó.
Curs de Programació en Python
Mòdul 1: Introducció a Python
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
