A 07-01 vam decidir que el taller de la Marta es munta sobre Python i vam comprovar amb un benchmark que la seva velocitat ve de delegar el càlcul en llibreries natives. Les tres primeres d'aquestes llibreries, les que sostenen tota la resta, són les que al mòdul 4 vam utilitzar sense aturar-nos-hi: NumPy, que aporta l'array numèric i les operacions vectoritzades; pandas, que aporta la taula amb noms de columna, dates i agrupacions; i Matplotlib, que dibuixa. generar_comandes_ml era NumPy més pandas; groupby("categoria")["retornat"].mean() era pandas; les corbes d'aprenentatge de 04-06 eren Matplotlib. Aquesta lliçó les explica de manera sistemàtica i amb un exemple continu: les comandes i la demanda setmanal de NovaMarket que ja coneixes. Veurem l'ndarray, la seva forma i tipus, la indexació, el broadcasting i les màscares booleanes; Series i DataFrame, lectura i escriptura de CSV, selecció amb loc/iloc, valors absents, groupby, merge, dates i remostreig; i figures amb histogrames, barres, línies i subgràfics, que descriurem en text. Acabarem amb una taula "operació → com es fa" que serveix de xuleta per al mòdul 8. És important perquè qualsevol projecte d'IA dedica més temps a preparar i mirar dades amb aquestes tres llibreries que a cridar fit. El que fan scikit-learn i PyTorch amb aquests arrays és assumpte de 07-03.
Contingut
- NumPy: l'
ndarray, forma idtype - Creació, indexació i slicing
- Operacions vectoritzades i broadcasting
- Agregacions per eix, màscares booleanes i aleatorietat
- Àlgebra lineal bàsica i per què NumPy és la base de tot
- pandas:
SeriesiDataFrame, lectura, escriptura i inspecció - Selecció i filtratge:
loc,iloci màscares - Valors absents,
groupbyi agregacions - Unir taules amb
merge - Dates:
to_datetime,.dt,resampleirolling applyamb moderació- Matplotlib: figura, eixos i els quatre gràfics bàsics
- Taula "operació → com es fa"
- Errors Comuns i Consells
- Exercicis
- Conclusió
- NumPy: l'
ndarray, forma i dtype
ndarray, forma i dtypeUna llista de Python pot contenir qualsevol cosa ([1, "hola", 2.5]), i per això cada element és un objecte separat amb el seu tipus, el seu comptador de referències i la seva adreça de memòria: flexible i lent. L'ndarray de NumPy és el contrari: un bloc contigu de memòria en què tots els elements tenen el mateix tipus (dtype) i una forma (shape) que diu quantes dimensions té i quants elements per dimensió. Aquest és el secret del benchmark de 07-01: la CPU recorre el bloc sense preguntar res.
import numpy as np
imports = np.array([129.90, 45.50, 899.00, 19.99, 249.00]) # 1 dimensió: un vector
print(imports, imports.dtype, imports.shape, imports.ndim)
# [129.9 45.5 899. 19.99 249. ] float64 (5,) 1
taula = np.array([[129.90, 2, 3], # 2 dimensions: una matriu de 4 files x 3 columnes
[45.50, 1, 5], # (import_comanda, num_articles, dies_lliurament)
[899.00, 1, 2],
[19.99, 3, 7]])
print(taula.shape, taula.dtype) # (4, 3) float64 <- tots float, encara que hi hagi enters
print(np.array([1, 2, 3]).dtype) # int64
print(np.array([1, 2.5]).dtype) # float64: NumPy promociona al tipus més generalshapeés una tupla:(5,)és un vector de 5;(4, 3)una matriu de 4 × 3;(3000, 21)va ser la matriu de característiques de 04-03 (3.000 comandes, 21 columnes després delColumnTransformer); una imatge 16 × 16 a 05-04 era(16, 16)i un lot d'imatges(n, 1, 16, 16).dtypeés el tipus comú:float64(8 bytes, l'habitual),float32(4 bytes, el que fa servir PyTorch per defecte),int64,bool. Un array d'un milió defloat64ocupa 8 MB exactes; una llista Python equivalent, unes quatre vegades més.ndimés el nombre de dimensions (eixos). En el vocabulari de la IA, un array d'1 dimensió és un vector, de 2 una matriu i de més un tensor.
- Creació, indexació i slicing
Maneres de crear arrays que veuràs contínuament:
np.zeros(3) # [0. 0. 0.]
np.ones((2, 3)) # matriu 2x3 d'uns
np.full(3, 7.5) # [7.5 7.5 7.5]
np.arange(0, 10, 2) # [0 2 4 6 8] com range, però retorna array
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ] 5 punts equiespaiats
np.eye(3) # matriu identitat 3x3Indexar i trossejar funciona com a les llistes, però amb una posició per eix separada per comes, i amb la regla que un tros (slice) és una vista, no una còpia:
taula[0] # [129.9 2. 3. ] primera fila
taula[0, 0] # 129.9 fila 0, columna 0
taula[:, 0] # [129.9 45.5 899. 19.99] TOTES les files, columna 0 (la dels imports)
taula[1:3] # files 1 i 2 (la 3 no hi entra), totes les columnes
taula[-1, -1] # 7.0 última fila, última columna
vista = taula[:, 0] # vista de la columna d'imports
vista[1] = 0.0 # modifica taula[1, 0] també!
copia = taula[:, 0].copy() # si vols independència, copia explícitamentQue un slice sigui una vista és una decisió de disseny per eficiència (no es copien megabytes en trossejar), i una font clàssica d'ensurts: si has de modificar el tros, fes servir .copy().
- Operacions vectoritzades i broadcasting
Els operadors aritmètics actuen element a element sobre tot l'array, sense bucle:
imports = np.array([129.90, 45.50, 899.00, 19.99, 249.00])
np.round(imports * 1.21, 2) # [ 157.18 55.06 1087.79 24.19 301.29] amb IVA
imports - 5 # [124.9 40.5 894. 14.99 244. ] despeses d'enviament fora
descomptes = np.array([0.10, 0.0, 0.15, 0.0, 0.05])
np.round(imports * (1 - descomptes), 2) # [116.91 45.5 764.15 19.99 236.55] array per array
np.log(imports).round(3) # [4.867 3.818 6.801 2.995 5.517] funcions "universals" (ufuncs)imports * 1.21 combina un array de forma (5,) amb un escalar: NumPy "estira" l'escalar fins a la forma de l'array. Aquesta regla generalitzada es diu broadcasting: dos arrays poden operar si, comparant les seves formes de dreta a esquerra, cada parell de dimensions és igual o una d'elles val 1 (o falta). Exemple numèric, l'operació més comuna en ML, estandarditzar columnes (restar la mitjana de cada columna i dividir per la seva desviació; és el que fa StandardScaler a 04-03):
taula = np.array([[129.90, 2, 3], [45.50, 1, 5], [899.00, 1, 2], [19.99, 3, 7]]) # (4, 3)
mitjana = taula.mean(axis=0) # (3,) [273.5975 1.75 4.25 ] una mitjana per columna
desv = taula.std(axis=0) # (3,) [363.36 0.829 1.920]
z = (taula - mitjana) / desv # (4,3) - (3,) -> la fila (3,) es resta a CADA fila de taula
print(z.round(3))
# [[-0.395 0.302 -0.651]
# [-0.628 -0.905 0.391]
# [ 1.721 -0.905 -1.172]
# [-0.698 1.508 1.432]]
col = np.array([[1.0], [2.0], [3.0], [4.0]]) # (4, 1)
fila = np.array([10.0, 20.0, 30.0]) # (3,)
print(col + fila) # (4,1) + (3,) -> (4,3): taula de sumes
# [[11. 21. 31.]
# [12. 22. 32.]
# [13. 23. 33.]
# [14. 24. 34.]]
np.array([1, 2, 3]) + np.array([1, 2])
# ValueError: operands could not be broadcast together with shapes (3,) (2,)Comprovació de la regla: (4, 3) amb (3,): comparant per la dreta, 3 = 3 i l'altra dimensió falta, compatible. (4, 1) amb (3,): 1 davant de 3 (val, un dels dos és 1), 4 davant de res: resultat (4, 3). (3,) amb (2,): 3 ≠ 2 i cap no és 1: error. Quan un càlcul de NumPy o PyTorch et doni un error de formes, aquest és el joc que cal jugar.
- Agregacions per eix, màscares booleanes i aleatorietat
Agregar és reduir un eix: sum, mean, std, min, max, argmax (posició del màxim). Sense axis ho redueixen tot; amb axis=0 recorren les files i retornen un valor per columna; amb axis=1, un valor per fila:
taula.sum() # 1118.39 tot
taula.sum(axis=0) # [1094.39 7. 17. ] per columna: import total, articles, dies
taula.sum(axis=1) # [134.9 51.5 902. 29.99] per fila (sense sentit aquí, però il·lustra l'eix)
taula.max(axis=0) # [899. 3. 7.]
taula.argmax(axis=0) # [2 3 3] a quina fila hi ha el màxim de cada columnaRegla mnemotècnica: axis és l'eix que desapareix. Amb axis=0 desapareixen les files i queda una xifra per columna.
Les màscares booleanes són la manera vectoritzada de filtrar i de comptar, i les hem fet servir sense anomenar-les des del mòdul 4 (rng.random(n) < 0.30 per decidir quins clients són nous):
import_comanda = np.array([129.90, 45.50, 899.00, 19.99, 249.00, 75.0])
retornat = np.array([0, 0, 1, 0, 1, 0])
import_comanda > 100 # [ True False True False True False] array de bool
import_comanda[import_comanda > 100] # [129.9 899. 249. ] filtra
(import_comanda > 100).sum() # 3 True val 1: compta quants ho compleixen
(import_comanda > 100).mean() # 0.5 proporció que ho compleix
retornat[import_comanda > 100].mean() # 0.667 taxa de devolució de les comandes cares
retornat[import_comanda <= 100].mean() # 0.0 i de les barates
np.where(import_comanda > 100, "alt", "baix") # ['alt' 'baix' 'alt' 'baix' 'alt' 'baix'] condicional vectoritzat
import_comanda[(import_comanda > 40) & (retornat == 0)] # [129.9 45.5 75. ] combinar amb & (i), | (o), ~ (no)Ull amb els parèntesis: & té més prioritat que >, així que import_comanda > 40 & retornat == 0 sense parèntesis falla.
Aleatorietat reproduïble: des de NumPy 1.17 la manera recomanada és crear un generador amb llavor, np.random.default_rng(llavor), i demanar-li números. És el que fa generar_comandes_ml i per això les 3.000 comandes són les mateixes a tots els mòduls:
rng = np.random.default_rng(42)
rng.random(3) # [0.774 0.439 0.859] uniformes a [0, 1)
rng.integers(1, 6, size=5) # [1 4 2 1 3] enters d'1 a 5 (el 6 no hi entra)
rng.normal(0, 25, 3).round(1) # [ 3.2 -7.9 -0.4] normals de mitjana 0 i desviació 25 (el soroll de la demanda)
rng.choice(["A", "B", "C"], size=6, p=[0.4, 0.35, 0.25]) # ['B' 'A' 'C' 'B' 'C' 'B'] zones
np.random.default_rng(42).random(3) # [0.774 0.439 0.859] mateixa llavor, mateixos números
- Àlgebra lineal bàsica i per què NumPy és la base de tot
L'operador @ és el producte matricial, i amb ell s'escriu en una línia el que la regressió logística de 04-04 i cada capa nn.Linear de 05-02 fan per dins: z = X @ w + b:
X = np.array([[1.0, 129.90, 2], # 3 comandes x 3 característiques (la primera és el biaix)
[1.0, 45.50, 1],
[1.0, 899.00, 1]])
w = np.array([-3.4, 0.01, 0.35]) # pesos (inventats, de l'estil de la "veritat oculta" de 04-01)
z = X @ w # (3,3) @ (3,) -> (3,) un logit per comanda
print(z, 1 / (1 + np.exp(-z)))
# [-1.401 -2.595 5.94 ] [0.198 0.069 0.997] sigmoide -> probabilitat de devolució
X.T # transposada, (3,3) aquí; d'una (3000,21) seria (21,3000)
A = np.array([[2.0, 1.0], [1.0, 3.0]]); b = np.array([3.0, 5.0])
np.linalg.solve(A, b) # [0.8 1.4] resol A x = b (millor que inv(A) @ b)np.linalg té a més inv, det, eig (valors propis, base del PCA de 04-02), svd i norm. No cal dominar l'àlgebra per seguir el curs; sí que cal saber que tot model és al final una seqüència de @ i funcions element a element sobre arrays.
Per això NumPy és la base: scikit-learn rep i retorna arrays (o DataFrames que converteix en arrays), Matplotlib dibuixa arrays, pandas desa cada columna en un array, i els tensors de PyTorch de 05-03 són "arrays amb gradient": mateix concepte de forma, dtype i broadcasting, més requires_grad perquè autograd apunti les operacions, i torch.tensor(array) / tensor.numpy() per passar de l'un a l'altre sense copiar quan el tipus ho permet. Qui entén NumPy té el 80 % de PyTorch.
- pandas:
Series i DataFrame, lectura, escriptura i inspecció
Series i DataFrame, lectura, escriptura i inspeccióNumPy no sap que la columna 0 és "import" ni entén de dates ni de valors absents en enters. pandas posa sobre els arrays dues estructures amb etiquetes:
Series: un array unidimensional amb un índex (etiquetes de fila) i un nom.DataFrame: una taula de columnes, cadascuna unaSeries(possiblement dedtypediferent), que comparteixen índex. És el que retornagenerar_comandes_ml.
import pandas as pd
from novamarket_ml import generar_comandes_ml, generar_demanda_setmanal # generadors del mòdul 4
s = pd.Series([129.90, 45.50, 899.00], index=["P1001", "P1002", "P1003"], name="import_comanda")
print(s["P1002"], s.mean(), (s > 100).sum()) # 45.5 358.13 2 accés per etiqueta; NumPy per sota
comandes = generar_comandes_ml(3000, 42) # DataFrame de 3000 x 7
print(comandes.shape) # (3000, 7)
print(comandes.head()) # primeres 5 files
comandes.info() # columnes, no nuls, dtypes, memòria
print(comandes.describe().round(2)) # estadístics de les numèriques
print(comandes["categoria"].value_counts()) # recompte per valorSortida (resumida) d'info() i describe():
RangeIndex: 3000 entries, 0 to 2999
Data columns (total 7 columns):
# Column Non-Null Count Dtype
0 import_comanda 3000 non-null float64
1 num_articles 3000 non-null int64
2 dies_lliurament 3000 non-null int64
3 client_nou 3000 non-null int64
4 categoria 3000 non-null str
5 codi_postal_zona 3000 non-null str
6 retornat 3000 non-null int64
memory usage: 164.2 KB
import_comanda num_articles dies_lliurament client_nou retornat
count 3000.00 3000.00 3000.00 3000.00 3000.00
mean 125.29 2.98 3.99 0.30 0.16
std 84.33 1.41 2.00 0.46 0.37
min 5.88 1.00 1.00 0.00 0.00
50% 108.12 3.00 4.00 0.00 0.00
max 632.61 5.00 7.00 1.00 1.00
categoria
electronica 1038
llar 908
informatica 615
accessoris 439Ja reconeixes les xifres: 30 % de clients nous, taxa de devolució 0,16 (16,4 %), import mitjà 125 € amb mediana 108 (cua llarga: la gamma del generador). Les columnes de text apareixen com a str a les versions recents de pandas (object a les anteriors). head(), info(), describe() i value_counts() són el primer que cal executar sobre qualsevol fitxer nou, abans de pensar en models.
CSV d'anada i tornada, que és com circularan comandes.csv i companyia per NovaMarket:
comandes.to_csv("comandes.csv", index=False) # index=False: no desis el 0..2999 com a columna
c2 = pd.read_csv("comandes.csv") # infereix tipus; parse_dates=[...] per a columnes de data
print(c2.shape) # (3000, 7)
# Primeres línies del fitxer:
# import_comanda,num_articles,dies_lliurament,client_nou,categoria,codi_postal_zona,retornat
# 130.51,4,4,1,llar,B,0read_csv accepta sep=";" (els CSV d'Excel en configuració regional europea), decimal=",", encoding="latin-1", usecols, nrows; i hi ha read_excel, read_sql (per a la consulta de 07-01 directament a un DataFrame), read_parquet (format columnar comprimit, molt més ràpid que CSV per a taules grans) i read_json.
- Selecció i filtratge:
loc, iloc i màscares
loc, iloc i màscaresTres maneres de seleccionar, i convé distingir-les:
comandes["import_comanda"] # una columna -> Series
comandes[["import_comanda", "categoria"]] # diverses columnes -> DataFrame (llista dins de claudàtors)
comandes.loc[0] # fila amb ETIQUETA 0 -> Series amb una entrada per columna
comandes.loc[0:2, ["import_comanda", "retornat"]] # etiquetes 0,1,2 (loc INCLOU el final) i dues columnes
comandes.iloc[0:2, 0:3] # POSICIONS: files 0-1, columnes 0-2 (iloc exclou el final)
comandes.iloc[-1, 0] # 94.06 última comanda, primera columna
cars = comandes[comandes["import_comanda"] > 300] # màscara booleana, com a NumPy
print(len(cars), cars["retornat"].mean().round(3)) # 134 0.612 <- el 61 % de les cares es retorna
sel = comandes[(comandes["categoria"] == "electronica") & (comandes["client_nou"] == 1)]
print(len(sel), sel["retornat"].mean().round(3)) # 316 0.415 <- la fila de l'exercici 3 de 07-01
comandes.query("import_comanda > 300 and categoria == 'llar'").shape # (37, 7) la mateixa idea, com a text
comandes.sort_values("import_comanda", ascending=False).head(3) # les tres comandes més cares (632, 608, 578 €; les tres retornades)locva per etiquetes (les de l'índex i els noms de columna) i inclou l'extrem;ilocva per posicions enteres i l'exclou, com Python. Mentre l'índex sigui elRangeIndex0..n-1 coincideixen, i per això la confusió passa desapercebuda fins que filtres o reordenes i les etiquetes deixen de ser posicions.- Per assignar sobre una selecció fes servir
df.loc[mascara, "columna"] = valor; assignar sobredf[mascara]["columna"]pot modificar una còpia i no l'original (el famósSettingWithCopyWarning).
Crear columnes noves és assignar, i pd.cut discretitza en trams (recorda 04-03):
comandes["import_per_article"] = (comandes["import_comanda"] / comandes["num_articles"]).round(2)
comandes["tram"] = pd.cut(comandes["import_comanda"], bins=[0, 50, 150, 400, np.inf],
labels=["<50", "50-150", "150-400", ">400"])
print(comandes.groupby("tram", observed=True)["retornat"].mean().round(3))
# <50 0.049 50-150 0.115 150-400 0.300 >400 0.743La taxa de devolució es multiplica per 15 entre el tram més barat i el més car: el +0.010 * (import_comanda - 100) del generador vist des de les dades.
- Valors absents,
groupby i agregacions
groupby i agregacionsA 04-03 vam embrutar les comandes amb NaN i els vam imputar dins del pipeline. Les eines de pandas per veure'ls i tractar-los:
rng = np.random.default_rng(7)
brut = comandes.copy()
brut.loc[rng.random(len(brut)) < 0.05, "dies_lliurament"] = np.nan # 5 % de lliuraments sense dada
brut.loc[rng.random(len(brut)) < 0.02, "import_comanda"] = np.nan
print(brut.isna().sum()) # import_comanda 73, dies_lliurament 142, resta 0
print(brut["import_comanda"].mean().round(2)) # 124.79 les agregacions IGNOREN NaN per defecte
print(brut.dropna().shape) # (2787, 7) eliminar files amb algun NaN: se'n perden 213
omplert = brut.fillna({"dies_lliurament": brut["dies_lliurament"].median(),
"import_comanda": brut["import_comanda"].median()}) # imputar per mediana
print(omplert.isna().sum().sum()) # 0
print(brut["dies_lliurament"].dtype) # float64: en ficar NaN en una columna d'enters, pandas la passa a floatisna()/notna(), dropna(), fillna(), i en models el SimpleImputer de 04-03 (que a més recorda la mediana d'entrenament per aplicar-la en producció, cosa que fillna a mà no fa).
groupby és l'operació central de l'anàlisi: dividir en grups, aplicar una agregació a cadascun i combinar. És el GROUP BY d'SQL de 07-01, i amb ell responem les preguntes del Diego:
comandes.groupby("categoria")["retornat"].mean().round(3)
# accessoris 0.130 electronica 0.207 informatica 0.159 llar 0.135 (mateixa xifra que la consulta SQL)
taxa = comandes.groupby("categoria")["retornat"].agg(comandes="count", retornats="sum", taxa="mean").round(3)
print(taxa.sort_values("taxa", ascending=False))
# comandes retornats taxa
# electronica 1038 215 0.207
# informatica 615 98 0.159
# llar 908 123 0.135
# accessoris 439 57 0.130
# Dues claus -> índex jeràrquic; unstack() porta la segona clau a columnes
comandes.groupby(["categoria", "codi_postal_zona"])["retornat"].mean().round(3).unstack()
# codi_postal_zona A B C
# accessoris 0.128 0.123 0.140
# electronica 0.223 0.187 0.207
# informatica 0.139 0.178 0.165
# llar 0.121 0.162 0.117
comandes.groupby("client_nou").agg(import_mitja=("import_comanda", "mean"),
taxa_dev=("retornat", "mean"),
comandes=("import_comanda", "size")).round(3)
# import_mitja taxa_dev comandes
# client_nou
# 0 124.813 0.091 2094
# 1 126.397 0.333 906Lectures: per zona la taxa no varia de manera sistemàtica (0,12-0,22 sense patró per columna): coherent amb el fet que codi_postal_zona no influeix en el generador, i amb el fet que a 04-04 el model li va donar pesos gairebé nuls. Els clients nous gasten gairebé el mateix de mitjana (126 € davant de 125 €) però retornen 3,7 vegades més (33 % davant de 9 %). La sintaxi agg(nom=("columna", "funcio")) (agregació amb nom) és la més llegible; pd.crosstab(comandes["categoria"], comandes["retornat"], normalize="index") dona la mateixa taula de taxes en format de contingència.
- Unir taules amb
merge
mergeLes dades de NovaMarket estan repartides: comandes.csv no diu quant costa gestionar una devolució ni des de quin magatzem surt. Això és a productes.csv. Construïm una taula petita per categoria (a la realitat seria per producte) i la unim:
productes = pd.DataFrame({
"categoria": ["electronica", "llar", "informatica", "accessoris"],
"producte_estrella": ["auriculars NovaSound", "robot aspirador NovaClean",
"portatil NovaBook", "funda NovaCase"],
"cost_devolucio": [12.0, 18.0, 25.0, 4.0], # euros per devolució gestionada
"magatzem": ["Getafe", "Zaragoza", "Getafe", "Zaragoza"],
})
unit = comandes.merge(productes, on="categoria", how="left") # clau: categoria; left: conserva totes les comandes
print(unit.shape) # (3000, 10): 7 columnes + 3 de noves
unit["cost_dev"] = unit["retornat"] * unit["cost_devolucio"]
print(unit.groupby("magatzem").agg(comandes=("import_comanda", "size"), retornats=("retornat", "sum"),
cost=("cost_dev", "sum")))
# comandes retornats cost
# Getafe 1653 313 5030.0
# Zaragoza 1347 180 2442.0onés la columna clau (si es diu diferent a cada taula,left_on/right_on);howdecideix què passa amb les claus sense parella:leftconserva totes les files de l'esquerra (comandes) i posaNaNon no hi ha producte;innernomés les que encaixen a totes dues;outertotes. Si treus "accessoris" deproductes,innerdeixa 2.561 comandes ileften deixa 3.000 amb 439magatzemaNaN.joinés el mateix per índex;pd.concat([df1, df2])apila taules (files o columnes) sense clau.- El Diego té la seva xifra: les devolucions costen uns 7.500 € en aquestes 3.000 comandes, dos terços a Getafe (electrònica i informàtica). És el tipus de dada que converteix "AUC 0,844" en "euros que s'estalvien".
- Dates:
to_datetime, .dt, resample i rolling
to_datetime, .dt, resample i rollingLa demanda setmanal del NovaClean (generar_demanda_setmanal, 04-02) porta una columna data de tipus datetime64. pandas converteix text a data amb pd.to_datetime i n'exposa les parts amb .dt:
demanda = generar_demanda_setmanal(104, 42) # 104 setmanes: 2024 i 2025, dilluns
print(demanda.dtypes) # setmana int64, data datetime64, unitats int64
pd.to_datetime("24/11/2025", dayfirst=True) # Timestamp('2025-11-24') compte amb el format dia/mes/any
demanda["data"].dt.year.value_counts().sort_index() # 2024: 53 setmanes, 2025: 51
demanda["mes"] = demanda["data"].dt.month # també .dt.day, .dt.dayofweek, .dt.day_name(), .dt.quarter
demanda[demanda["data"] >= "2025-11-01"].head(4) # comparar amb text funciona
# setmana data unitats
# 96 97 2025-11-03 573
# 97 98 2025-11-10 578
# 98 99 2025-11-17 610
# 99 100 2025-11-24 825 <- Black Friday
# Remostrejar de setmanal a mensual: l'índex ha de ser la data
mensual = demanda.set_index("data")["unitats"].resample("MS").sum() # MS = inici de mes
print(mensual.head(4)); print(len(mensual), mensual.idxmax(), mensual.max())
# 2024-01-01 1714 / 2024-02-01 1459 / 2024-03-01 1656 / 2024-04-01 2261 ... 24 mesos; màxim juny 2025: 3268
# Mitjana mòbil: suavitza el soroll setmanal
demanda["mitjana_mobil_4"] = demanda["unitats"].rolling(4).mean() # NaN a les 3 primeresresample accepta "W", "MS", "QS" (trimestre), "YS", i qualsevol agregació (sum, mean, max); rolling(k) calcula finestres lliscants (center=True per centrar-les). Compte amb la trampa clàssica de les mensualitats: un mes amb cinc dilluns suma cinc setmanes i un altre quatre; per comparar mesos seriosament cal normalitzar per setmanes o treballar amb dies.
apply amb moderació
apply amb moderacióapply executa una funció Python fila a fila o valor a valor. És còmode i és un bucle Python disfressat (07-01): fes-lo servir quan no hi hagi alternativa vectoritzada, i prefereix map per a diccionaris, .str per a text i pd.cut/np.where per a trams i condicionals:
def tram(imp):
return "baix" if imp < 50 else ("mitja" if imp < 150 else "alt")
a = comandes["import_comanda"].apply(tram) # funciona, però és un bucle Python
b = pd.cut(comandes["import_comanda"], [0, 50, 150, np.inf], labels=["baix", "mitja", "alt"], right=False) # vectoritzat
# tots dos: {'mitja': 1596, 'alt': 898, 'baix': 506}; amb 3.000 files no hi ha diferència; amb 3 milions sí
comandes["categoria"].map({"electronica": "ELEC", "llar": "LLAR", "informatica": "INFO", "accessoris": "ACC"})
comandes["categoria"].str.upper(); comandes["categoria"].str.len(); comandes["categoria"].str.contains("elec")
- Matplotlib: figura, eixos i els quatre gràfics bàsics
Matplotlib té dues capes: la ràpida (plt.plot(...)) i l'orientada a objectes, que és la que convé aprendre: una figura (fig, el llenç) conté un o més eixos (ax, cada gràfic), i sobre els eixos es dibuixa i s'etiqueta. Com que l'entorn del curs no mostra imatges, desem a fitxer i descrivim el que s'hi veu.
import matplotlib.pyplot as plt
# 1) Histograma d'imports
fig, ax = plt.subplots(figsize=(7, 4)) # una figura amb un eix
ax.hist(comandes["import_comanda"], bins=40, color="steelblue", edgecolor="white")
ax.axvline(comandes["import_comanda"].median(), color="darkred", linestyle="--",
label=f"mediana {comandes['import_comanda'].median():.0f} EUR") # línia vertical
ax.set_xlabel("Import de la comanda (EUR)"); ax.set_ylabel("Nombre de comandes")
ax.set_title("NovaMarket: distribució d'imports (3.000 comandes)")
ax.legend()
fig.tight_layout(); fig.savefig("hist_imports.png", dpi=120) # també .pdf, .svg
# 2) Barres: taxa de devolució per categoria
taxa = comandes.groupby("categoria")["retornat"].mean().sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(6, 4))
barres = ax.bar(taxa.index, taxa.values * 100, color=["firebrick", "darkorange", "goldenrod", "seagreen"])
ax.bar_label(barres, fmt="%.1f %%") # el valor a sobre de cada barra
ax.axhline(comandes["retornat"].mean() * 100, color="gray", linestyle=":", label="mitjana global")
ax.set_ylabel("Taxa de devolució (%)"); ax.set_title("Taxa de devolució per categoria"); ax.legend()
fig.tight_layout(); fig.savefig("barres_taxa.png", dpi=120)
# 3) Línia: demanda setmanal i mitjana mòbil
demanda["mitjana_mobil_8"] = demanda["unitats"].rolling(8, center=True).mean()
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(demanda["data"], demanda["unitats"], color="lightgray", marker=".", linewidth=1, label="unitats setmanals")
ax.plot(demanda["data"], demanda["mitjana_mobil_8"], color="navy", linewidth=2, label="mitjana mòbil (8 setmanes)")
pic = demanda.loc[demanda["unitats"].idxmax()]
ax.annotate("Black Friday", xy=(pic["data"], pic["unitats"]),
xytext=(pic["data"], pic["unitats"] + 60), arrowprops=dict(arrowstyle="->"), ha="center")
ax.set_xlabel("Setmana"); ax.set_ylabel("Unitats venudes")
ax.set_title("Demanda setmanal del robot aspirador NovaClean"); ax.legend(loc="upper left"); ax.grid(alpha=0.3)
fig.tight_layout(); fig.savefig("demanda_mitjana_mobil.png", dpi=120)
# 4) Subgràfics: dispersió i línia, l'un al costat de l'altre
fig, eixos = plt.subplots(1, 2, figsize=(10, 4)) # 1 fila x 2 columnes -> array d'eixos
eixos[0].scatter(comandes["import_comanda"], comandes["dies_lliurament"], c=comandes["retornat"], cmap="coolwarm", s=8, alpha=0.6)
eixos[0].set_xlabel("Import (EUR)"); eixos[0].set_ylabel("Dies de lliurament"); eixos[0].set_title("Comandes (vermell = retornada)")
per_dia = comandes.groupby("dies_lliurament")["retornat"].mean() * 100
eixos[1].plot(per_dia.index, per_dia.values, marker="o", color="darkred")
eixos[1].set_xlabel("Dies de lliurament"); eixos[1].set_ylabel("Taxa de devolució (%)"); eixos[1].set_title("Taxa segons dies de lliurament")
fig.suptitle("Import, lliurament i devolucions"); fig.tight_layout(); fig.savefig("subplots.png", dpi=120)Què es veu a cada fitxer:
hist_imports.png: una campana asimètrica amb el pic a la barra de 69-84 € (277 comandes), la línia discontínua de la mediana a 108 € a la dreta del pic i una cua que s'allarga fins a 630 € amb poques desenes de comandes per sobre de 400. És la distribució gamma del generador i la raó per la qual a 04-03 vam parlar d'escalar i d'atípics.barres_taxa.png: quatre barres descendents, electrònica 20,7 %, informàtica 15,9 %, llar 13,5 %, accessoris 13,0 %, amb la línia puntejada de la mitjana global (16,4 %) creuant entre les dues primeres i les dues últimes.demanda_mitjana_mobil.png: punts grisos que oscil·len cada setmana entre 308 i 825 unitats, i a sobre una línia blava suau que puja d'unes 350 unitats a principis de 2024 fins a unes 650 a mitjans de 2025 amb dues "geps" anuals (màxim cap al juliol, mínim cap al gener) i dos pics aïllats en gris que la mitjana mòbil gairebé ignora: la setmana del Black Friday de 2024 (724 unitats) i la de 2025 (825), aquesta última amb la fletxa "Black Friday". Tendència + estacionalitat + esdeveniment, exactament el que vam dir que hi havia a 04-02.subplots.png: a l'esquerra, un núvol de punts amb set franges horitzontals (dies de lliurament 1-7), gairebé tot blau (no retornada) i amb punts vermells concentrats cap a la dreta (imports alts) i cap amunt (lliuraments de 5-7 dies); a la dreta, una línia que puja gairebé en recta del 8,6 % de devolucions amb lliurament en 1 dia al 26 % amb 7 dies. És la relació que el+0.35 * (dies_lliurament - 4)del generador amaga.
Desa les figures amb fig.savefig; en un notebook (07-04) es mostren soles. Quan el gràfic sigui estadístic i vulguis que surti bé a la primera, seaborn (a sobre de Matplotlib: sns.histplot, sns.barplot, sns.heatmap per a matrius de correlació i de confusió) estalvia codi; per a gràfics interactius en web, plotly. Totes dues fan servir DataFrames directament. El mateix pandas té df.plot() com a drecera sobre Matplotlib.
- Taula "operació → com es fa"
| Operació (el que vam fer al mòdul 4) | NumPy | pandas |
|---|---|---|
| Crear dades reproduïbles | rng = np.random.default_rng(42); rng.normal, rng.integers, rng.choice |
pd.DataFrame({...}) amb els arrays |
| Filtrar files per condició | a[a > 100], a[(c1) & (c2)] |
df[df["import_comanda"] > 100], df.query("...") |
| Condicional vectoritzat | np.where(cond, x, y) |
np.where sobre columnes, pd.cut per a trams |
| Comptar / proporció que compleix | (a > 100).sum() / .mean() |
(df["x"] > 100).sum(), df["retornat"].mean() |
| Mitjana/desv per columna | a.mean(axis=0), a.std(axis=0) |
df.mean(numeric_only=True), df.describe() |
| Estandarditzar | (a - a.mean(0)) / a.std(0) (broadcasting) |
igual, o StandardScaler (07-03) |
| Taxa per grup | (a mà amb màscares per valor) | df.groupby("cat")["retornat"].mean() |
| Diverses agregacions amb nom | — | df.groupby("k").agg(n=("x","size"), m=("y","mean")) |
| Unir dues taules per clau | — | df1.merge(df2, on="clau", how="left") |
| Noves columnes derivades | a[:,0] / a[:,1] |
df["c"] = df["a"] / df["b"] |
| Valors absents | np.isnan(a), np.nanmean(a) |
df.isna().sum(), df.dropna(), df.fillna({...}) |
| Dates | np.datetime64 (bàsic) |
pd.to_datetime, .dt.month, resample("MS").sum(), rolling(4).mean() |
| Producte matricial / capa lineal | X @ w + b |
df.values @ w |
| Llegir/escriure | np.loadtxt, np.save |
pd.read_csv, df.to_csv(index=False), read_sql, read_parquet |
| Convertir a scikit-learn / PyTorch | ja és un array | df.values / df.to_numpy(); torch.tensor(df.values, dtype=torch.float32) |
Errors Comuns i Consells
- Bucles
forsobre files d'un DataFrame (for i in range(len(df)),iterrows). És l'error número u de qui arriba d'altres llenguatges; gairebé sempre existeix una operació vectoritzada, ungroupbyo unmergeque ho fa en una línia i cent vegades més ràpid. - Modificar una vista creient que és una còpia (NumPy) o una còpia creient que és la vista (pandas amb
df[mask]["col"] = ...). Regles:.copy()quan vulguis independència;df.loc[mask, "col"] = ...per assignar. - Confondre
lociilocdesprés de filtrar o ordenar:df.loc[0]és la fila amb etiqueta 0, que pot no existir;df.iloc[0]és la primera fila.reset_index(drop=True)després d'un filtre et retorna etiquetes 0..n-1 si les necessites. - Formes incompatibles: llegeix el missatge
could not be broadcast together with shapes (a,) (b,)i aplica la regla de dreta a esquerra;reshape(-1, 1)converteix un vector(n,)en columna(n, 1), cosa que scikit-learn exigeix quan hi ha una sola característica. - Dates llegides com a text: després de
read_csv,df.dtypesha de dirdatetime64; si diuobject/str, aplicapd.to_datetime(..., dayfirst=True)oparse_datesa la lectura. I en un CSV amb configuració regional europea revisasep=";"idecimal=",". NaNsilenciosos:mean()els ignora i pots no assabentar-te que falten dades.info()iisna().sum()sempre al principi.- Gràfics sense etiquetes ni unitats: un
ax.set_xlabelcosta un segon i evita que el Diego pregunti "això són euros o unitats?". I desa sempre ambtight_layout()perquè no es tallin els textos.
Exercicis
Exercici 1. Amb NumPy pur (sense pandas), a partir de comandes["import_comanda"].to_numpy() i comandes["retornat"].to_numpy(): (a) calcula la taxa de devolució de les comandes per sobre i per sota de la mediana d'import fent servir màscares; (b) estandarditza l'import amb broadcasting i comprova que la mitjana resultant és ≈ 0 i la desviació ≈ 1; (c) construeix amb np.where una etiqueta "car"/"barat" i compta quants n'hi ha de cadascuna amb una màscara.
Exercici 2. Amb pandas: calcula l'import mitjà i la taxa de devolució per categoria i per client nou/recurrent en una sola taula amb groupby i agregació amb nom, i porta-la a format ample amb unstack perquè les columnes siguin nou/recurrent. A quina categoria és més gran la diferència de taxa entre nous i recurrents? Després, uneix amb la taula productes de la secció 9 i calcula el cost total de devolucions per magatzem i tipus de client.
Exercici 3. Amb la demanda setmanal: (a) remostreja a trimestres ("QS") sumant unitats i identifica el trimestre amb més vendes; (b) calcula la mitjana mòbil de 12 setmanes i la variació percentual setmana a setmana de la sèrie suavitzada (pct_change), i localitza la setmana amb més pujada relativa; (c) dibuixa (desant a fitxer) les unitats per any com dues línies superposades (dia de l'any a l'eix x, una línia per any; pista: .dt.year i .dt.dayofyear) i descriu què s'hi veu.
Solucions
Solució 1.
import_comanda = comandes["import_comanda"].to_numpy(); retornat = comandes["retornat"].to_numpy()
mediana = np.median(import_comanda)
print(retornat[import_comanda > mediana].mean().round(3), retornat[import_comanda <= mediana].mean().round(3))
# 0.254 0.075 -> les comandes cares es retornen més de 3 vegades més
z = (import_comanda - import_comanda.mean()) / import_comanda.std()
print(z.mean().round(10), z.std().round(6)) # 0.0 1.0 (arrodoniment: la mitjana és de l'ordre d'1e-16)
etiqueta = np.where(import_comanda > mediana, "car", "barat")
print((etiqueta == "car").sum(), (etiqueta == "barat").sum()) # 1500 1500Amb 3.000 valors i mediana a la posició central, la meitat exacta queda a cada costat. Fixa't que ni tan sols hem necessitat pandas: és el que scikit-learn fa per sota amb les columnes numèriques.
Solució 2.
t = comandes.groupby(["categoria", "client_nou"]).agg(import_mitja=("import_comanda", "mean"),
taxa=("retornat", "mean")).round(3)
ample = t["taxa"].unstack().rename(columns={0: "recurrent", 1: "nou"})
ample["diferencia"] = ample["nou"] - ample["recurrent"]
print(ample.sort_values("diferencia", ascending=False))
# recurrent nou diferencia
# electronica 0.116 0.415 0.299
# llar 0.069 0.297 0.228
# informatica 0.099 0.304 0.205
# accessoris 0.068 0.259 0.191
unit = comandes.merge(productes, on="categoria", how="left")
unit["cost_dev"] = unit["retornat"] * unit["cost_devolucio"]
print(unit.groupby(["magatzem", "client_nou"])["cost_dev"].sum().unstack())
# client_nou 0 1
# magatzem
# Getafe 2083.0 2947.0
# Zaragoza 872.0 1570.0La diferència més gran és a electrònica (30 punts): el generador combina l'efecte de categoria amb el de client nou i el terme creuat nou × import, i l'electrònica té imports alts. En cost, els clients nous (el 30 % de les comandes) generen el 60 % del cost de devolucions als dos magatzems (2.947 € de 5.030 a Getafe; 1.570 € de 2.442 a Zaragoza): un argument quantificat perquè el Diego prioritzi el cas d'ús 3 en clients nous.
Solució 3.
serie = demanda.set_index("data")["unitats"]
trim = serie.resample("QS").sum()
print(trim.idxmax().date(), trim.max()) # 2025-07-01 8404 (tercer trimestre de 2025)
suau = serie.rolling(12).mean()
var = suau.pct_change() * 100
print(var.idxmax().date(), var.max().round(2)) # 2024-04-22 3.4 % (setmana en què entra la pujada de primavera)
demanda["any_"] = demanda["data"].dt.year; demanda["dia_any"] = demanda["data"].dt.dayofyear
fig, ax = plt.subplots(figsize=(9, 4))
for any_, g in demanda.groupby("any_"): # un grup (i una línia) per any
ax.plot(g["dia_any"], g["unitats"], marker=".", label=str(any_))
ax.set_xlabel("Dia de l'any"); ax.set_ylabel("Unitats"); ax.set_title("NovaClean: demanda setmanal, un any per línia")
ax.legend(); fig.tight_layout(); fig.savefig("demanda_per_any.png", dpi=120)Es veuen dues línies amb la mateixa forma (pugen fins a l'estiu, baixen fins a l'hivern, amb un pic aïllat a finals de novembre, el Black Friday), la de 2025 desplaçada unes 130 unitats per sobre de la de 2024 en tota la seva longitud (mitjanes anuals: 471 i 597 unitats): la tendència (+2,5 unitats per setmana × 52 setmanes) i l'estacionalitat separades a simple vista. Aquest és el gràfic que la Marta ensenyaria al Diego abans de parlar de models de previsió (cas d'ús 2).
Conclusió
Hem obert la caixa d'eines bàsica del Python científic amb les dades de NovaMarket. NumPy aporta l'ndarray (bloc contigu, shape i dtype), la creació i indexació amb vistes, les operacions vectoritzades i el broadcasting (amb el qual estandarditzem columnes en una línia), les agregacions per eix, les màscares booleanes per filtrar i comptar, el generador default_rng que fa reproduïbles les nostres 3.000 comandes i el producte @ amb què s'escriu qualsevol capa lineal; i és la base de tota la resta, inclosos els tensors de PyTorch, que són arrays amb gradient. pandas hi afegeix etiquetes: Series i DataFrame, read_csv/to_csv, la inspecció obligatòria amb head/info/describe, la selecció amb loc/iloc i màscares, el tractament de NaN, el groupby que respon a les preguntes del Diego (electrònica 20,7 % de devolucions; clients nous 3,7 vegades més), el merge amb la taula de productes que converteix devolucions en euros per magatzem, i les dates amb .dt, resample (de setmanal a mensual) i rolling. Matplotlib dibuixa amb figura i eixos: histograma d'imports, barres de taxa, la línia de la demanda amb la seva mitjana mòbil i el Black Friday assenyalat, i subgràfics. La taula de la secció 13 resumeix com es fa cada operació que al mòdul 4 vam donar per sabuda.
Amb arrays i taules dominats, el següent pas del taller de la Marta és el mapa de les llibreries que es construeixen a sobre: scikit-learn i la seva API fit/predict, PyTorch davant de TensorFlow/Keras, Hugging Face per al llenguatge, OpenCV per a la visió, els SDK de LLM, experta i pgmpy per a les regles i la probabilitat, OR-Tools per a l'optimització, i les eines per desar, servir i fer el seguiment de models. És 07-03, Eines i Llibreries Populars, on a més desarem i recarregarem el pipeline de 04-03 i l'MLP de 05-02 i comprovarem que continuen predint el mateix.
Fonaments d'Intel·ligència Artificial (IA)
Mòdul 1: Introducció a la Intel·ligència Artificial
Mòdul 2: Principis Bàsics de la IA
- Conceptes Fonamentals: Agents, Entorns i Racionalitat
- Tipus d'Intel·ligència Artificial
- Les Dades com a Matèria Primera de la IA
- Ètica i Consideracions en IA
Mòdul 3: Algorismes en IA
- Introducció als Algorismes
- Algorismes de Cerca
- Cerca amb Adversari: Jocs i Minimax
- Algorismes d'Optimització
Mòdul 4: Aprenentatge Automàtic (Machine Learning)
- Conceptes Bàsics de Machine Learning
- Tipus d'Aprenentatge Automàtic
- Preparació de Dades i Característiques
- Algorismes de Machine Learning
- Avaluació i Validació de Models
- Sobreajust, Regularització i Ajust d'Hiperparàmetres
Mòdul 5: Xarxes Neuronals i Deep Learning
- Introducció a les Xarxes Neuronals
- Arquitectura de Xarxes Neuronals
- Com Aprèn una Xarxa: Descens del Gradient i Retropropagació
- Deep Learning i les seves Aplicacions
- Transformers, Grans Models de Llenguatge i IA Generativa
Mòdul 6: Lògica i Sistemes Experts
- Lògica en IA
- Sistemes Experts
- Raonament amb Incertesa: Probabilitat i Xarxes Bayesianes
- Aplicacions dels Sistemes Experts
Mòdul 7: Eines i Llenguatges de Programació en IA
- Llenguatges de Programació per a IA
- Python Científic: NumPy, pandas i Matplotlib
- Eines i Llibreries Populars
- Entorns de Desenvolupament
Mòdul 8: Projectes i Casos d'Estudi
Mòdul 9: Exercicis i Pràctiques
- Exercicis d'Algorismes
- Pràctiques de Machine Learning
- Projectes de Xarxes Neuronals
- Projecte Integrador: de la Idea al Prototip
