Una sèrie és una estructura de dades homogènia unidimensional.
- Introducció
- Entorn de treball
- Sèrie
- Enters
- Hardware
- Operacions
- Reals
- String
- Temporal
- Categorical
- Tipus de dades niuats
- Object
- Altres Data types
- Activitats
- Pendent
Introducció
L’estructura bàsica de Polars és Series
que equival a una Python - Llista.
A diferència d’una llista que és poc eficient en temps de computació i ús de la memòria de l’ordinador, un objecte Series
de Polars és una façana lleugera de codi Rust.
Entorn de treball
Crea un projecte amb uv:
uv init polars-series
cd polars-series
Instal·la polars
:
uv add polars
Sèrie
Una sèrie és una estructura de dades homogènia unidimensional.
Per “homogènia” volem dir que tots els elements dins d’una sèrie tenen el mateix tipus de dades.
Per exemple, pots crear una sèrie semblant a com ho fas amb Python_:
import polars as pl
ages = pl.Series('ages', [23, 35, 45, 31, 27])
Quan crees una sèrie pots especificar un nom.
Si no vols, pots crear una sèrie anònima:
import polars as pl
s = pl.Series([23, 35, 45, 31, 27])
Enters
Python pot gestionar enters arbitràriament grans amb facilitat:
googol = 10 ** 100
print(googol % 99999999977) # 11526618770
En canvi, Polars utilitza tipus de dades Int
i UInt
per representar valors enters, de mida variable.
Int8
,Int16
,Int32
,Int64
per enters amb signe amb precisió variable.UInt8
,UInt16
,UInt32
,UInt64
per enters sense signe amb precisió variable.
El número després d’Int
(o UInt
) indica quants bits s’utilitzen per emmagatzemar l’enter.
Per exemple,
- Un
Int8
ocupa 8 bits (o 1 byte) i pot representar valors del -128 al 127. - Un
UInt16
ocupa 16 bits (o 2 bytes) i pot representar valors del 0 al 65535.
Quan es crea una sèrie, Polars infereix el tipus de dades a partir dels valors que proporciones.
En aquest cas, Polars infereix que el dtype
és un Int64
.
ages = pl.Series('ages', [23, 35, 45, 31, 27])
assert ages.dtype == pl.Int64
Memòria
En l’àmbit de la CPU no hi ha diferència entre processar un Int8
o un UInt64
.
Per exemple, en un processador de 64 bits, un Int8
es processa com un Int64
.
La nostra sèrie ages
ocupa 40 bytes (5 × 8 bytes)
ages = pl.Series('ages', [23, 35, 45, 31, 27])
assert ages.estimated_size() == 40
Per guardar edats de persones en tens amb un UInt8
que ocupa 1 byte i pot representar valors del 0 al 255.
Per tant, pots especificar un tipus de dades concret per sobreescriure el mecanisme d’inferència.
ages = pl.Series('ages', [23, 35, 45, 31, 27], dtype=pl.UInt8)
assert ages.estimated_size() == 5
D’aquesta manera les dades només ocupen un 12,5 % respecte a representar-les amb un tipus de dades Int64
.
Però que passa si tens un Matusalem en el teu conjunt de dades?
ages = pl.Series('ages', [23, 35, 45, 969, 27], dtype=pl.UInt8)
Es produeix un error perquè Polars ha trobat un valor de 969
, i per defecte de paràmetre de construcció strict
és True
.
TypeError: unexpected value while building Series of type UInt8; found value of type Int64: 969
Hint: Try setting `strict=False` to allow passing data with mixed types.
Pots veure que per defecte Polars utilitza el tipus més gran i ineficient en ús de memòria per representar dades numèriques, i és la teva responsabilitat decidir un tipus més restrictiu de dades.
Per tant, si treballes amb dades mil·lenàries no pots utilitzar un tipus de dades UInt8
per representar les seves edats:
pl.Series('ages', [1456, 24445, 2343, 945], dtype=pl.UInt8)
TypeError: unexpected value while building Series of type UInt8; found value of type Int64: 1456
Valors nuls
Si estas treballant amb dades que no han estat validades, pots utilitzar el paràmetre strict=False
per convertir en null
els valors que no s’ajusten al tipus de dades que has especificat:
ages = pl.Series('ages', [23, 35, 45, 969, 27], dtype=pl.UInt8, strict=False)
print(ages)
Pots veure que Polars codifica com a valor null
el valor 969
.
Series: 'ages' [u8]
[ 23, 35, 45, null, 27 ]
Hardware
¿Per quin motiu l’espai que ocupen les dades en memòria és tan important?
Si vols dissenyar per a un rendiment òptim no pots ignorar el maquinari.
Hi ha casos on la complexitat algorítmica no et dona una bona intuïció del rendiment real a causa de qüestions relacionades amb el maquinari com les jerarquies de memòria cau i la predicció de salts.
Jerarquies de memòria cau
Simplificant, la memòria RAM té dues versions, gran i lenta o ràpida i petita. Per aquesta raó, tens memòries cau en una jerarquia. Tens memòria principal que és gran i lenta. I la memòria que has utilitzat recentment s’emmagatzema a la memòria cau L1, L2, L3 amb més latència respectivament.
El resum següent et dona una idea de la latència relativa (temps d’accés en cicles de CPU) dels diferents nivells de memòria cau:
- Registre de CPU: 1 cicle
- Memòria cau L1: ~1-3 cicles
- Memòria cau L2: ~10 cicles
- Memòria cau L3: ~40 cicles
- Memòria principal: ~100-300 cicles
Quan accedeixes a dades de manera seqüencial et vols assegurar que les dades estiguin a la memòria cau tant com sigui possible, o pots tenir fàcilment una penalització de rendiment de ~100x.
Les memòries cau es carreguen i s’eliminen en línies de memòria cau. Quan carregues un únic punt de dades, obtens una línia de memòria cau completa, però també elimines una línia de memòria cau completa.
Habitualment tens 64 o 128 bytes de longitud i estan alineades a adreces de memòria de 64 bytes.
Precàrrega i predicció de salts
Les CPU precarreguen dades i instruccions a una memòria cau local per reduir la gran penalització de la latència de memòria. Si tens un bucle tancat sense cap salt (estructures if-else-then) la CPU no té cap problema sabent quines dades precarregar i pot utilitzar completament les línies d’instruccions.
Les línies d’instruccions amaguen la latència fent feina en paral·lel. Cada instrucció de CPU passa per una seqüència de recuperar-descodificar-executar-escriure. En lloc de fer aquestes quatre instruccions de manera seqüencial en una única línia, hi ha múltiples línies que ja precarreguen (descodifiquen, executen, etc.) les instruccions següents. Això incrementa el rendiment i amaga la latència. Tanmateix, si aquest procés s’interromp, comences amb línies buides i has d’esperar tot el període de latència per a la següent instrucció.
A continuació una imatge visual de les línies d’instruccions:
Instruccions SIMD
Els processadors moderns tenen registres SIMD (Single Instruction Multiple Data) que operen sobre vectors complets de dades en un únic cicle de CPU.
Les amplades dels carrils vectorials varien des de 128 bits fins a 512 bits, i l’acceleració depèn de l’amplada dels registres i del nombre de bits necessaris per representar el tipus de dades.
Aquests registres milloren molt el rendiment d’operacions simples si pots emplenar-los prou ràpid (dades lineals).
Operacions
Amb una sèrie pots aplicar diferents operacions que s’executen en codi natiu.
En aquest enllaç tens totes les operacions que pots efectuar amb un objecte Series
: Polars - Series
A continuació tens alguns exemples d’operacions que pots utilitzar.
Agregació
ages = pl.Series('ages', [23, 35, 45, 31, 27], dtype=pl.UInt8)
assert ages.sum() == 161
assert ages.mean() == 32.2
assert ages.min() == 23
Pots veure que pots treballar sense problemes amb valors nuls:
ages = pl.Series('ages', [10, None, 5, 12, None ])
assert ages.sum() == 27
Descriptive
Count the null
values in this Series.
s = pl.Series([1, None, None])
print(s.null_count())
2
Digues quin percentage de valors nuls té aquesta mostra.
Si el valor és major que … la mostra ja no és vàlida.
ages = pl.Series('ages',[56, None, 33, None, None, 45, 23])
assert 3 == s.null_count()
assert 4 == s.count()
print(f"{s.null_count() / s.len():.2f}")
Manipulació
Eliminar tots els valors nuls:
s = pl.Series([1.0, None, 3.0, float("nan")])
print(s.drop_nulls())
Series: '' [f64]
[ 1.0, 3.0, NaN ]
Un valor null
no és el mateix que un valor NaN
.
Per eliminar els valors NaN
, utilitza drop_nans()
:
s = pl.Series([1.0, None, 3.0, float("nan")])
print(s.drop_nans())
Series: '' [f64]
[ 1.0, null, 3.0 ]
Reals
Float32
i Float64
son nombres en coma flotant signats amb precisió variable.
El tipus float
de Python normalment coincideix amb el Float64
de Polars.
Polars intenta sempre proporcionar resultats raonablement precisos per a càlculs en punt flotant, però no ofereix garanties sobre l’error tret que s’indiqui el contrari.
En termes generals, assolir resultats 100 % precisos és inviable (requereix representacions internes molt més grans que els floats de 64 bits), i per tant sempre s’ha d’esperar algun error.
NaN
El valor NaN
, que és un valor especial de coma flotant, s’utilitza per representar el resultat d’algunes operacions que són matemàticament indeterminades.
Aquí tens alguns exemples:
pl.Series([0]) / 0
inf = float("inf")
pl.Series([inf]) - inf
pl.Series([inf]) / inf
Tots tres càlculs produeixen la mateixa sortida:
shape: (1,)
Series: '' [f64]
[ NaN ]
Polars generalment segueix l’estàndard de punt flotant IEEE 754 per a Float32
i Float64
, amb algunes excepcions:
-
Qualsevol
NaN
és igual a qualsevol altreNaN
, i és més gran que qualsevol valor que no siguiNaN
. -
Les operacions no garanteixen cap comportament concret sobre el signe del zero o de
NaN
, ni sobre la càrrega útil dels valorsNaN
. Això no es limita només a operacions aritmètiques; per exemple, una operació d’ordenació o d’agrupació pot canonitzar tots els zeros a +0 i tots elsNaN
a unNaN
positiu sense càrrega útil per fer comprovacions d’igualtat eficients.
Decimal
Si necessites control fi sobre la precisió dels floats i les operacions que hi fas pots utilitzar el tipus Decimal
de 128 bits amb precisió opcional i escala no negativa.
Pots pensar en el tipus de dades Decimal
com una variant dels tipus de dades Float32
i Float64
, però on pots controlar el nombre de decimals que tenen els teus números.
Tot i que utilitzar el tipus de dades Decimal
no prevé tots els errors d’arrodoniment, pot ajudar a prevenir-ne alguns.
Per exemple, les dues sumes següents produeixen 1.0 com a resultat, però només a causa d’errors d’arrodoniment:
tiny = pow(10, -16)
print(f"{tiny + 1 = }")
print("With Float64:")
print(pl.Series([tiny], dtype=pl.Float64) + 1)
tiny + 1 = 1.0
With Float64:
shape: (1,)
Series: '' [f64]
[
1.0
]
Utilitzant el tipus de dades Decimal
, amb suficients decimals, pots obtenir un resultat precís:
tiny = pow(10, -16)
print(tiny)
print(pl.Series([tiny], dtype=pl.Decimal(None, 24)).first() + 1)
1e-16
1.000000000000000100000000
El tipus de dades Decimal
pren com a segon argument el nombre de dígits després del punt decimal.
En el fragment anterior, l’hem establert a 24
. El primer argument especifica el nombre màxim total de dígits en cada número. Establir-lo a None
permet que Polars infereixi el valor que necessitem.
El tipus de dades Decimal
no té un espai de noms dedicat amb expressions especialitzades i en el moment d’escriure això es considera una funcionalitat inestable.
També has d’entendre que Decimal
no és una solució màgica per a tots els teus errors d’arrodoniment, ja que Decimal
també té precisió limitada.
String
Treballar amb String a Polars és eficient perquè Polars proporciona moltes funcions útils específiques per a strings sota l’espai de noms str
:
print(pl.Series(["Hello, world!", "Polars is great"]).str.slice(0, 6))
shape: (2,)
Series: '' [str]
[ "Hello,", "Polars"]
Tens més informació a Series - String
A causa de la manera com funciona el mecanisme d’inferència de tipus de dades de Polars, de vegades un tipus de dades especialitzat és més apropiat:
-
Això passa quan es llegeixen dades temporals de fitxers, per exemple, perquè Polars no analitzarà les dades en tipus de dades temporals tret que li ho indiquis.
-
O quan es treballa amb dades categòriques, en aquest cas un tipus de dades per a dades categòriques podria ser més apropiat.
Temporal
Els tipus de dades temporals de Polars són força intuïtius de treballar, i tots són molt similars als tipus de dades disponibles al mòdul estàndard datetime
:
Temporal data type | Similar type in datetime |
---|---|
Date | datetime.date |
Time | datetime.time |
Datetime | datetime.datetime |
Duration | datetime.timedelta |
Polars admet desenes d’expressions temporals especialitzades a les quals pots accedir des de l’espai de noms dt
.
Dates, hores i dates amb hores
El tipus de dades Date
representa una data de calendari: un dia, un mes i un any.
Per exemple, la data de naixement d’algú es representaria apropiadament com a Date
.
Pots crear una sèrie a partir de strings i convertir els strings en Date
:
s = pl.Series('birthdate', [ "1938-4-18","1939-3-30","1990-12-11"])
assert s.dtype == pl.String
s = pl.Series('birthdate', [ "1938-4-18","1939-3-30","1990-12-11"]).str.to_date()
assert s.dtype == pl.Date
O crear-se directament des d’un objecte datetime.date
:
from datetime import date
s = pl.Series('birthdate', [
date(1938, 4, 18),
date(1939, 3, 30),
date(1990, 12, 11),
])
assert s.dtype == pl.Date
D’altra banda, el tipus de dades Time
representa una hora del dia: una hora, minuts, segons i de vegades fins i tot fraccions de segon.
Per exemple, l’hora a la qual tens programat el despertador es representaria apropiadament com a Time
.
De manera anàloga a les dates, les hores es poden analitzar des de cadenes de text o crear-se directament des d’objectes datetime.time
:
from datetime import time
s = pl.Series('wake_up_time', [
time(5, 30, 0),
time(13, 0, 0),
time(11, 27, 56)
])
assert s.dtype == pl.Time
Per recapitular, els tipus de dades Date
i Time
són ortogonals perquè no comparteixen unitats.
Quan necessites tots dos junts, per exemple per representar la teva pròxima cita mèdica, utilitzes el tipus de dades Datetime
.
Un Datetime
és un tipus de dades que agrega les unitats de Date
i Time
i també proporciona funcionalitat per gestionar les temudes zones horàries.
No cal dir-ho, però pots crear valors d’aquest tipus utilitzant objectes datetime.datetime
:
from datetime import datetime, timedelta, timezone
now = datetime.now()
s = pl.Series([
now,
now.replace(tzinfo=timezone(timedelta(hours=1))),
now.replace(tzinfo=timezone(timedelta(hours=-3))),
])
assert s.dtype == pl.Datetime
print(s)
Series: '' [datetime[μs]]
[
2025-10-05 16:46:57.482096
2025-10-05 15:46:57.482096
2025-10-05 19:46:57.482096
]
Polars convertirà totes les hores a UTC per homogeneïtzar la zona horària en una única sèrie.
Si estableixes la zona horària d’una sèrie, veuràs la zona horària aparèixer al tipus de dades:
print(s.dt.convert_time_zone('Africa/Cairo'))
Series: '' [datetime[μs, Africa/Cairo]]
[
2025-10-05 19:53:05.970515 EEST
2025-10-05 18:53:05.970515 EEST
2025-10-05 22:53:05.970515 EEST
]
Afegeix la biblioteca pytz
You can list all the available timezones with pytz.all_timezones
:
uv add pytz
import pytz
# ...
L’altra peça d’informació mostrada al tipus de dades, en aquest cas µs
, és la unitat de temps en què es manté el datetime. Això es pot ajustar si necessites més o menys precisió.
Tot i que l’espai de noms dt
proporciona desenes d’operacions específiques temporals, algunes de les funcions que podries acabar utilitzant molt sovint són les de l’espai de noms str
que converteixen strings a dades temporals:
Expression | Target data |
---|---|
.str.to_date | Date |
.str.to_datetime | Datetime |
.str.to_time | Time |
.str.strptime | Any of the three |
Tingues en compte que quan converteixes strings a dades temporals, has d’utilitzar els especificadors de format del crate chrono de Rust, no els de la biblioteca Python datetime
. Els especificadors més comuns són els mateixos, però les especificacions no coincideixen del tot. La mateixa advertència s’aplica al formatatge de tipus de dades temporals com strings.
Durada
El tipus de dades Duration
és el tipus de dades que sorgeix naturalment quan fas aritmètica amb els altres tipus de dades:
from datetime import datetime
bedtime = pl.Series([
datetime(2024, 11, 22, 23, 56),
datetime(2024, 11, 24, 0, 23),
datetime(2024, 11, 24, 23, 37),
])
wake_up = pl.Series([
datetime(2024, 11, 23, 7, 30),
datetime(2024, 11, 24, 7, 30),
datetime(2024, 11, 25, 8, 0),
])
sleep = wake_up - bedtime
print(sleep)
Series: '' [duration[μs]]
[
7h 34m
7h 7m
8h 23m
]
Categorical
Una variable categòrica és una variable que només pot prendre un dels valors d’un conjunt predeterminat.
D’aquesta manera, en lloc de guardar strings que ocupen molt d’espai i són de longitud variable, pots representar les dades amb UInt8
(o UInt16
) i utilitzar un diccionari per a convertir-les en strings.
Les variables categòriques són aquelles que té sentit presentar com a llista desplegable en un formulari que estàs omplint. Per exemple, seria força absurd si estiguessis omplint un formulari on haguessis d’introduir el teu salari exacte seleccionant un valor en una llista desplegable. Però si et preguntessin la teva nacionalitat, esperaries una llista desplegable que poguessis navegar ràpidament fins a trobar el teu país.
A Polars, les variables categòriques sempre es deriven de dades de tipus string i Polars proporciona dos tipus de dades similars que et permeten treballar amb dades categòriques.
Enum
El tipus de dades Enum
és el tipus de dades preferit que hauries d’utilitzar quan treballis amb dades categòriques.
Convertir una sèrie al tipus de dades Enum
és un procés de tres passos:
- Determinar quins són els valors vàlids de les categories (això es pot fer estàticament, o calcular-se programàticament quan sigui factible);
- Crear una “variant” del tipus de dades
Enum
instanciant-lo - Finalment pots convertir la teva sèrie o columna.
valid_values = ["panda", "polar", "brown"] # 1.
bear_enum = pl.Enum(valid_values) # 2.
s = pl.Series(["panda", "polar", "panda", "brown", "panda"], dtype=bear_enum) # 3.
print(s.estimated_size())
5
Quan s’imprimeixen o s’inspeccionen visualment, es veuen exactament iguals.
print(s)
Series: '' [enum]
[ "panda", "polar", "panda", "brown", "panda"]
No obstant això, internament, Polars pot fer operacions sobre sèries amb el tipus de dades Enum de manera més eficient perquè sap que només un conjunt fix de strings són valors legals. Això fa que sigui més eficient en memòria tenir una sèrie del tipus de dades Enum, així com normalment més ràpid operar sobre aquestes sèries.
Polars també es queixarà si inclous un valor que no pertany a l’enumeració:
s = pl.Series(["pand", "snake"], dtype=bear_enum)
InvalidOperationError: conversion from `str` to `enum` failed in column '' for 2 out of 2 values: ["pand", "snake"]
Això pot ajudar a detectar problemes amb les teves dades, des d’errors tipogràfics fins a valors que són completament incorrectes.
El tipus de dades Enum
també té l’avantatge de permetre’t treballar amb les teves categories com si estiguessin ordenades. En l’exemple dels ossos, l’ordenació no té sentit.
Si la nostra variable categòrica representa el nivell d’educació formal aconseguida, llavors hi ha una ordenació que té sentit:
print(degrees.filter(degrees >= "MSc"))
Series: '' [enum]
[ "MSc", "PhD" , "MSc" ]
Categorical
També pots utilitzar el tipus de dades Categorical
quan treballis amb dades categòriques.
No necessites especificar els valors vàlids per endavant amb el tipus de dades Categorical
. En canvi, Polars inferirà aquests valors per tu. Això pot semblar estrictament millor que utilitzar el tipus de dades Enum
, però la inferència que fa Polars té un cost.
Els desavantatges d’utilitzar Categorical
enlloc d’Enum
inclouen:
-
és menys eficient quan operes sobre dues sèries que haurien de tenir les mateixes categories però que es van crear independentment;
-
no detecta valors que són invàlids.
No tot és dolent, i hi ha casos on el tipus de dades Categorical
és el que vols. Com a regla general, només utilitza Categorical
quan no puguis utilitzar Enum
de manera pràctica.
Independentment de si utilitzes el tipus de dades Enum
o el tipus de dades Categorical
, sempre pots utilitzar l’espai de noms cat
i la seva única funció get_categories
per recuperar els valors únics que s’utilitzen com a categories:
s = pl.Series(["panda", "polar", "pand", "snake", "panda"], dtype=pl.Categorical)
print(s.cat.get_categories().to_list())
['panda', 'polar', 'pand', 'snake']
Tipus de dades niuats
Els tipus de dades niuats s’assemblen als contenidors de Python. Són tipus de dades que contenen dades dins d’ells.
Polars suporta tres tipus de dades niuats:
Struct
és com un diccionari tipat en Python on les claus són strings fixes;List
és com una llista de Python, però amb la restricció que tots els elements han de ser del mateix tipus; iArray
és com un array de NumPy, on els elements són tots del mateix tipus, però la forma de l’array mateix és fixa.
__ PENDENT de fer __
Object
__ PENDENT de fer __
Altres Data types
PENDENT de fer
Tipus | Detalls |
---|---|
Boolean | Tipus booleà empaquetat per bits de manera eficient. |
Binary | Emmagatzema dades binàries en brut de longitud variable. |
Null | Representa valors nuls. |
Activitats
Crea un nova sèrie de tipus Date
a partir d’aquesta sèrie:
dates = pl.Series(["2025-05-01", "2026-08-03", "2026-05-24"])
years= dates.str.to_date()
Quin és l’any més gran de la sèrie?
dates = dates.str.to_date()
year = dates.dt.year().max()
assert year == 2026
A continuació tens les notes de dos examens parcials de l’assignatura de matemàtiques:
students = pl.Series(["David", "Maria", "Sandra", "Jordi"])
algebra = pl.Series([9, 5, 10, 9])
calculus = pl.Series([10, 9, 7, 8])
Calcula la nota final de l’assignatura si els dos examen tenen el mateix pes en la nota final:
average = (algebra + calculus) / 2
print(average)
Calcula la nota final de l’assignatura si el segon examen (“calculus”) té el doble de pes que el primer examen:
average = (algebra + calculus * 2) / 3
print(average)
Crea una sèria amb els noms i les notes finals dels alumnes:
result = students + average.map_elements(lambda x: f": {x:.2f}", return_dtype=pl.String)
print(result)
Series: '' [str]
[
"David: 9.67"
"Maria: 7.67"
"Sandra: 8.00"
"Jordi: 8.33"
]