La lliçó anterior va acabar assenyalant el culpable: les llistes de Python no saben de matemàtiques. [12.50, 9.95] * 2 no duplica els preus — repeteix la llista. Per sumar l'IVA a 487 quanties necessites un bucle o una comprensió, i per a un milió de quanties aquell bucle es nota. NumPy (Numerical Python) resol totes dues coses amb una sola estructura, el ndarray: un bloc compacte de números del mateix tipus sobre el qual les operacions s'escriuen sense bucle i s'executen en C. És la peça més avall de l'ecosistema PyData — pandas, Matplotlib i scikit-learn estan construïts a sobre — així que entendre NumPy és entendre què fan els altres per dins.

Contingut

  1. Per què les llistes no basten
  2. El ndarray: np.array, dtype, shape, ndim
  3. Vectorització: operar sense bucles
  4. Crear arrays: zeros, ones, arange, linspace
  5. Indexació, slicing i la idea estrella: el boolean masking
  6. Agregacions: sum, mean, max, argmax, std
  7. Arrays 2D i el paràmetre axis
  8. Broadcasting bàsic
  9. Llista vs array: quan cadascun

Per què les llistes no basten

Instal·la i prova (dins del venv, com sempre):

pip install numpy
preus = [12.50, 9.95, 15.90, 21.00]   # Odissea, Hamlet, Quixot, Faust

print(preus * 2)        # duplicar preus? No: repetir la llista
# [12.5, 9.95, 15.9, 21.0, 12.5, 9.95, 15.9, 21.0]

# print(preus * 1.04)   # TypeError: can't multiply sequence by non-int

La llista és un contenidor genèric: pot barrejar floats, cadenes i dataclasses Llibre, i per això cada element és un objecte Python complet amb el seu sobrecost. Dues conseqüències:

  • Expressivitat: les operacions matemàtiques element a element no existeixen; cal escriure el bucle (o la comprensió de M2) cada vegada.
  • Rendiment: el bucle de Python interpreta cada iteració; amb moltes dades, la diferència respecte al codi compilat és d'un a dos ordres de magnitud.

El ndarray

import numpy as np    # l'alies 'np' es una convencio universal

preus = np.array([12.50, 9.95, 15.90, 21.00])

print(preus)          # [12.5   9.95 15.9  21.  ]
print(type(preus))    # <class 'numpy.ndarray'>
print(preus.dtype)    # float64  -> TOTS els elements son d'aquest tipus
print(preus.shape)    # (4,)     -> tupla amb la mida de cada dimensio
print(preus.ndim)     # 1        -> nombre de dimensions

Tres atributs que consultaràs constantment:

Atribut Què diu Exemple
dtype Tipus únic de tots els elements (float64, int64, bool...) float64
shape Mida per dimensió, com a tupla (4,) vector, (4, 6) matriu
ndim Nombre de dimensions 1 vector, 2 taula

El preu d'aquesta homogeneïtat: si barreges tipus, NumPy ho converteix tot al més general (np.array([1, 2.5])float64; si hi ha una cadena, tot acaba sent text i adeu matemàtiques). Aquesta restricció és justament el que li permet ser compacte i ràpid.

Vectorització: operar sense bucles

L'operació s'aplica element a element, sense que escriguis el bucle. Compara les tres maneres d'afegir l'IVA del 4% als preus:

preus_llista = [12.50, 9.95, 15.90, 21.00]
IVA_LLIBRES = 0.04

# 1) Bucle classic (M2)
amb_iva = []
for p in preus_llista:
    amb_iva.append(round(p * (1 + IVA_LLIBRES), 2))

# 2) Comprensio (M2)
amb_iva = [round(p * (1 + IVA_LLIBRES), 2) for p in preus_llista]

# 3) NumPy: l'operacio ES l'expressio
preus = np.array(preus_llista)
amb_iva = np.round(preus * (1 + IVA_LLIBRES), 2)
print(amb_iva)    # [13.   10.35 16.54 21.84]

Les tres donen el mateix; la tercera no només és més curta, és més ràpida (el bucle passa en C) i es llegeix com matemàtiques: "preus per 1.04". El mateix amb dos arrays:

unitats = np.array([154, 121, 182, 63])    # venudes gen-juny (11-01)
ingressos = preus * unitats                 # element a element
print(ingressos)         # [1925.   1203.95 2893.8  1323.  ]
print(ingressos.sum())   # 7345.75  -> facturacio total a PVP

I la tarifa de soci de tot el curs (IVA del 4 % i descompte del 5 %, la fórmula de M3), ara en una línia:

IVA_LLIBRES, DESCOMPTE_SOCI = 0.04, 0.05
preus_soci = np.round(preus * (1 + IVA_LLIBRES) * (1 - DESCOMPTE_SOCI), 2)
print(preus_soci)    # [12.35  9.83 15.71 20.75]  <- els de M3, clavats

Crear arrays

Funció Què crea Exemple
np.zeros(n) n zeros (acumuladors) np.zeros(7) → comptador per dia de la setmana
np.ones(n) n uns np.ones(4)
np.arange(a, b, pas) Com range, però array (admet floats) np.arange(1, 7) → mesos [1 2 3 4 5 6]
np.linspace(a, b, n) n punts equiespaiats incloent-hi tots dos extrems np.linspace(0, 1, 5)[0. 0.25 0.5 0.75 1.]
mesos = np.arange(1, 7)
print(mesos)                       # [1 2 3 4 5 6]
print(np.zeros(3))                 # [0. 0. 0.]
print(np.linspace(9.95, 21.0, 3))  # [ 9.95  15.475 21.   ]

Indexació, slicing i boolean masking

Índexs i llesques funcionen com a les llistes (M4):

quanties = np.array([12.50, 19.90, 15.71, 21.00, 31.80, 47.70, 25.00])

print(quanties[0])      # 12.5
print(quanties[-1])     # 25.0
print(quanties[1:4])    # [19.9  15.71 21.  ]

I ara la idea estrella de tot l'ecosistema, la que reapareixerà idèntica a pandas: comparar un array produeix un array de booleans, i aquest array serveix de filtre.

grans = quanties > 15.00
print(grans)              # [False  True  True  True  True  True  True]
print(quanties[grans])    # [19.9  15.71 21.   31.8  47.7  25.  ]

# En una linia, com s'escriu a la practica:
print(quanties[quanties > 15.00])

# Combinant condicions: & (i), | (o), ~ (no) — amb parentesis obligatoris
print(quanties[(quanties > 15) & (quanties < 30)])   # [19.9  15.71 21.   25.  ]

Llegeix-ho a poc a poc: quanties > 15.00 no és un if, és una operació vectoritzada que retorna (7,) booleans; usar-lo entre claudàtors selecciona els elements on hi ha True. És el filter/comprensió-amb-if de M2 convertit en àlgebra. Un parent útil: np.where(condicio, si_true, si_false) aplica un "if vectoritzat" (np.where(es_soci, preu * 0.95, preu)).

Agregacions

print(quanties.sum())      # 173.61
print(quanties.mean())     # 24.80142857142857
print(quanties.max())      # 47.7
print(quanties.argmax())   # 5  -> l'INDEX del maxim, no el valor
print(quanties.std())      # 11.29... desviacio tipica: com es dispersen

argmax mereix atenció: retorna on és el màxim, cosa que permet respondre "quin títol factura més?" creuant arrays paral·lels:

titols = np.array(["L'Odissea", "Hamlet", "El Quixot", "Faust"])
print(titols[ingressos.argmax()])    # El Quixot  (2893.80 €)

I combinant màscara + agregació, preguntes senceres en una línia: quanties[quanties > 15].mean() — "quantia mitjana de les vendes grans".

Arrays 2D i el paràmetre axis

Les vendes per títol i per mes formen una taula: 4 files (títols) × 6 columnes (gener-juny). Conceptualment, les unitats de vendes_2026.csv reorganitzades:

# files: Odissea, Hamlet, Quixot, Faust | columnes: gen..juny
unitats_2d = np.array([
    [18, 17, 21, 50, 25, 23],    # L'Odissea
    [15, 13, 17, 39, 19, 18],    # Hamlet
    [22, 20, 25, 59, 29, 27],    # El Quixot
    [ 7,  8,  8, 20, 11,  9],    # Faust
])
print(unitats_2d.shape)   # (4, 6)
print(unitats_2d.ndim)    # 2
print(unitats_2d[2, 3])   # 59 -> El Quixot a l'abril (fila 2, columna 3)
print(unitats_2d[:, 3])   # [50 39 59 20] -> TOTS els titols a l'abril

axis indica al llarg de quina dimensió s'agrega — la dimensió que desapareix:

print(unitats_2d.sum())          # 520 -> total del semestre
print(unitats_2d.sum(axis=0))    # [ 62  58  71 168  84  77] -> per MES (aplana les files)
print(unitats_2d.sum(axis=1))    # [154 121 182  63]         -> per TITOL (aplana les columnes)

Aquí el tens, en números, l'ham de M10: l'abril (168) més que duplica qualsevol altre mes. Sant Jordi existeix i els arrays el veuen. La regla mnemotècnica: axis=0 agrega "cap avall" (resultat per columna), axis=1 "cap a la dreta" (resultat per fila).

Broadcasting bàsic

El broadcasting és la regla per la qual NumPy opera arrays de formes diferents "estirant" el petit. Dos casos cobreixen el 90% de l'ús:

# 1) Escalar contra array: l'escalar s'aplica a tot (ja ho has usat: preus * 1.04)
print(unitats_2d * 2)

# 2) Vector contra matriu: el vector (6,) s'aplica a CADA fila de la (4, 6)
pes_mes = np.array([1.0, 1.0, 1.0, 0.5, 1.0, 1.0])   # abril "descomptat" en comparar
print((unitats_2d * pes_mes).sum(axis=1))
# [129.  101.5 152.5  53. ] -> vendes "desestacionalitzades" per titol

La regla formal (les formes es comparen de dreta a esquerra i han de coincidir o ser 1) dona per a molt més, però amb escalar-contra-array i vector-contra-matriu tens el que et cal per a aquest curs.

Llista vs array: quan cadascun

Criteri list np.ndarray
Tipus barrejats (Llibre, cadenes, floats) Sí — la seva gran virtut No: un sol dtype
Créixer element a element (append) Barata i natural Cara (recrea l'array): construeix la llista i converteix al final
Matemàtiques element a element Bucle/comprensió manual Una expressió, executada en C
Filtrar per condició Comprensió amb if Màscara booleana
Milers/milions de números Lenta i pesada en memòria El seu hàbitat natural
El catàleg de Papyrus (objectes rics) list[Llibre] de M5, correcte No aplica

L'honestedat promesa: en el dia a dia de Papyrus gairebé no escriuràs NumPy pur — faràs servir pandas, que posa noms a files i columnes i llegeix CSV tot sol. Però pandas és NumPy per sota: cada columna d'un DataFrame és un array, les màscares booleanes d'aquesta lliçó s'hi escriuen idèntiques, i axis, dtype i les agregacions hi reapareixen amb els mateixos noms. El que has après aquí no es llença: s'hereta.

Errors Comuns i Consells

  • and/or en màscares. (a > 1) and (a < 5) llança ValueError: The truth value of an array is ambiguous. Amb arrays s'usen &, |, ~ — i sempre amb parèntesis, perquè & té més precedència que >.
  • Oblidar que l'slice és una vista. b = a[1:4]; b[0] = 99 modifica també a (a diferència de les llistes de M4, on l'slicing copia). Si necessites independència: a[1:4].copy().
  • dtype silenciosament enter. np.array([12, 9, 15]) és int64; una divisió posterior funciona (produeix floats), però assignar a[0] = 12.5 truncaria a 12. Si treballes amb preus, parteix de floats: np.array([12.0, ...]).
  • Confondre max i argmax. L'un retorna el valor, l'altre l'índex. Per a "quin títol ven més?" necessites argmax + l'array de títols.
  • Fer append en bucle a un array. np.append recrea l'array sencer cada vegada. Acumula en una list i fes np.array(llista) al final — o millor, deixa que pandas.read_csv ho faci tot a 11-03.

Exercicis

  1. Amb preus = np.array([12.50, 9.95, 15.90, 21.00]) i unitats = np.array([154, 121, 182, 63]): calcula en una expressió cadascun els ingressos per títol a tarifa de soci (IVA 4 % + descompte 5 %, arrodoniment a 2 decimals del preu abans de multiplicar) i el títol amb menys ingressos a PVP.
  2. Amb quanties = np.array([12.50, 19.90, 15.71, 21.00, 31.80, 47.70, 25.00, 9.95, 12.35]): (a) quantes vendes superen la mitjana? (b) quin percentatge de la quantia total aporten? Pistes: una màscara es pot sumar (True val 1) i usar-se dues vegades.
  3. Amb la matriu unitats_2d de la lliçó: obtén la mitjana mensual de cada títol excloent l'abril (per no deixar que Sant Jordi infli el "mes normal"), fent servir slicing o una màscara sobre les columnes. Quin títol té el mes normal més alt?

Solucions

  1. ingressos_soci = np.round(preus * 1.04 * 0.95, 2) * unitats
    print(ingressos_soci)                  # [1901.9  1189.43 2859.22 1307.25]
    titols = np.array(["L'Odissea", "Hamlet", "El Quixot", "Faust"])
    print(titols[(preus * unitats).argmin()])   # Hamlet (1203.95 € a PVP)
    
    Compte amb el matís de l'enunciat: s'arrodoneix la tarifa de soci (12.35, 9.83...) i després es multiplica — tal com cobra Papyrus — no a l'inrevés.
  2. mitjana = quanties.mean()                     # 21.77...
    mascara = quanties > mitjana
    print(mascara.sum())                          # 3 vendes (31.80, 47.70, 25.00)
    print(quanties[mascara].sum() / quanties.sum() * 100)   # 53.2...%
    
    Tres vendes de nou aporten més de la meitat de la quantia: un altre cop l'asimetria de 11-01. mascara.sum() compta els True perquè els booleans són 0/1 — un idiotisme de NumPy que veuràs constantment.
  3. sense_abril = np.delete(unitats_2d, 3, axis=1)       # o unitats_2d[:, [0,1,2,4,5]]
    mitjanes = sense_abril.mean(axis=1)
    print(np.round(mitjanes, 1))    # [20.8 16.4 24.6  8.6]
    titols = np.array(["L'Odissea", "Hamlet", "El Quixot", "Faust"])
    print(titols[mitjanes.argmax()])    # El Quixot
    
    axis=1 perquè vols aplanar els mesos (columnes) i quedar-te amb un valor per títol (fila). El Quixot lidera també el mes "normal": el seu lideratge no és només cosa de Sant Jordi.

Conclusió

NumPy t'ha donat el motor: el ndarray homogeni i compacte, la vectorització que converteix bucles en expressions (preus * 1.04), les màscares booleanes que converteixen filtres en àlgebra (quanties[quanties > 15]), les agregacions amb axis que han fet visible el pic de Sant Jordi (abril: 168 unitats), i el broadcasting que estira escalars i vectors sense copiar-los. També t'ha ensenyat el seu límit per a Papyrus: els arrays són números anònims — la fila 2 "és" El Quixot només perquè tu ho recordes, i ficar les 487 files del CSV en arrays paral·lels a mà és feina de picapedrer. La propera lliçó presenta pandas, que embolica aquests arrays en taules amb noms: columnes etiquetades, read_csv que carrega vendes_2026.csv en una línia (el csv.DictReader de M6 amb esteroides, com havíem promès), i groupby per respondre per fi, amb dades i d'una vegada, les preguntes de l'Anna.

Curs de Programació en Python

Mòdul 1: Introducció a Python

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats