Escalat de característiques Part 3: Tècniques avançades

Més enllà dels fonaments: exploreu l’Escalat MaxAbs, les transformacions de potència i basades en quantils, la normalització de vectors i estratègies d’escalat personalitzades. Inclou exemples de codi en Python i R, amb enfocament en dades esparses i casos d’ús pràctics de ML.
Fonaments
Preprocessament de Dades
Escalat
Dades esparses
Sèrie: Escalat de Característiques
Autor/a

January Tabaka

Publicat

27 d’agost de 2025

En la publicació anterior hem tractat l’estandardització, la normalització (o l’escalat Min-Max) i l’escalat robust. En aquesta publicació veurem altres tècniques d’escalat.

Escalat MaxAbs

Tot i que és similar al escalat Min-Max, aquest mètode està dissenyat per a dades on el punt zero és significatiu. Es destaca amb dades esparses (p. ex., vectors TF-IDF), ja que preserva les entrades zero crucials, i amb dades naturalment centrades com les formes d’ona d’àudio (on 0 significa silenci) o les variacions de preus financers (on 0 significa cap canvi).

Principi: Escala cada característica pel seu valor absolut màxim de manera que el valor absolut més gran es converteixi en 1 (o -1). L’interval real depèn dels signes de les dades.

  • Si els valors de la característica són tots no negatius, els valors escalats estaran en l’interval [0, 1].
  • Si els valors de la característica són tots no positius, els valors escalats estaran en l’interval [-1, 0].

Fórmula: \[X_{scaled} = \frac{X}{\max(|X|)}\] On:

  • \(X\) - és el vector de valors originals (la transformació s’aplica element per element).

Quan utilitzar-lo: El seu principal avantatge és que no desplaça ni centra les dades. Això és crucial per a conjunts de dades esparses (matrius amb molts zeros), ja que no voldries convertir tots els zeros en un número diferent de zero. Centrar (restar la mitjana/mèdia) trenca l’esparsitat.

Precaució

En scikit-learn, utilitza StandardScaler(with_mean=False) o RobustScaler(with_centering=False) per evitar la densificació. Centrar una matriu esparsa no és compatible i genera un error.

from sklearn.preprocessing import MaxAbsScaler
import numpy as np

# Dades amb valors positius i negatius, i un zero
data = np.array([[-20], [-10], [0], [0], [0], [10], [100]])

# Instanciar l'escalador i ajustar-transformar les dades
maxabs_scaler = MaxAbsScaler()
scaled_data = maxabs_scaler.fit_transform(data)

print("Dades Escalades: ", scaled_data.T)
Dades Escalades:  [[-0.2 -0.1  0.   0.   0.   0.1  1. ]]
# Troba el valor absolut màxim en les dades
# np.abs() obté el valor absolut de cada element primer
max_abs_val = np.max(np.abs(data))

# Aplica la fórmula: x / max(|x|)
scaled_data = data / max_abs_val

print("Valor Absolut Màxim:", max_abs_val,
      "\nDades Escalades Manualment (MaxAbsScaler): ", scaled_data.T)
Valor Absolut Màxim: 100 
Dades Escalades Manualment (MaxAbsScaler):  [[-0.2 -0.1  0.   0.   0.   0.1  1. ]]
library(tidymodels)
library(dplyr)

data <- tibble(feature = c(-20, -10, 0, 0, 0, 10, 100))

# Per a un exemple autosuficient, calculam a partir de 'data'.
# En la pràctica: calcula max_abs_val al conjunt d'entrenament
# i reutilitza per a validació/prova
max_abs_val <- max(abs(data$feature))

# Defineix la recepta (escalera per valor absolut màxim)
maxabs_recipe <- recipe(~ feature, data = data) %>%
  step_mutate(feature = feature / max_abs_val)

# Prepara i aplica
prepared_maxabs_recipe <- prep(maxabs_recipe, training = data)
scaled_data <- bake(prepared_maxabs_recipe, new_data = data)

cat("Dades Escalades: [", paste(round(scaled_data$feature, 3), collapse = " "), "]")
Dades Escalades: [ -0.2 -0.1 0 0 0 0.1 1 ]
  • Aprenentatge profund: Rarament utilitzat. Les xarxes neuronals normalment es queden amb Min-Max (p. ex., píxels d’imatge → [0,1]) o l’estandardització (p. ex., embeddings, característiques contínues).
  • Aprenentatge clàssic: Altament rellevant quan es treballa amb característiques esparses (TF-IDF, matrius usuari-article). Manté zeros - crucial per a l’eficiència.

Transformació de Potència

Aquesta és una transformació més avançada i no lineal. El seu objectiu no és només redimensionar, sinó canviar la pròpia distribució de la variable perquè sigui més semblant a una gaussiana (en forma de campana).

Principi: Aplica transformacions matemàtiques com Box‑Cox (només per a dades positives) o Yeo‑Johnson (per a qualsevol dada) per estabilitzar la variància i minimitzar l’asimetria.

Quan utilitzar-ho: Quan vols estabilitzar la variància i reduir l’asimetria perquè els residus del model es comportin millor (comú per a famílies lineals/GLM). Per a característiques altament asimètriques (p. ex., ingressos), les transformacions de potència poden millorar l’ajust i la inferència.

Al nucli de les transformacions Box‑Cox i Yeo‑Johnson hi ha un paràmetre anomenat lambda (\(\lambda\)), que controla la “forma” de la transformació de potència aplicada a les dades. L’objectiu de l’algoritme és trobar automàticament el \(\lambda\) òptim que faci que la distribució de les teves dades sigui el més proper possible a una forma gaussiana (corba de campana).

  • Box‑Cox és un mètode potent i ben establert, però té un límit important: només funciona amb dades estrictament positives.
  • Yeo‑Johnson és una versió més moderna i flexible que pot gestionar tant valors positius com negatius, d’aquí que sigui el predeterminat a PowerTransformer de scikit-learn.

La matemàtica subjacent implica una família de funcions que inclou transformacions logarítmiques i de potència. Per a una mirada detallada a les fórmules, l’article de Wikipedia sobre Transformacions de Potència és un excel·lent recurs.

El missatge clau és que no necessites escollir \(\lambda\) tu mateix; el mètode fit() fa tot el treball d’ajustar el millor valor per a les teves dades.

import numpy as np
from sklearn.preprocessing import PowerTransformer

# Dades esbiaixades (com una distribució exponencial)
data = np.array([[-2], [-1], [0], [1], [2], [5], [10],[100]])

# method='yeo-johnson' és el valor per defecte i funciona amb qualsevol dada
# standardize=True fa que la sortida tingui una mitjana de 0 i una desviació típica de 1
power_scaler = PowerTransformer(standardize=True)
transformed_data = power_scaler.fit_transform(data)

print("Dades transformades:\n", np.round(transformed_data.T, 3))
Dades transformades:
 [[-1.765 -0.882 -0.33  -0.057  0.11   0.41   0.686  1.828]]
Consell

PowerTransformer estàndarditza automàticament les dades per defecte després d’aplicar la transformació. Això es controla amb el paràmetre standardize, que per defecte és True. Si només necessites desassimetritzar (sense mitjana zero ni variància unitària), utilitza standardize=False.

library(tidymodels)

data <- tibble(feature = c(-2, -1, 0, 1, 2, 5, 10, 100))

# Transformació Yeo-Johnson (funciona amb valors positius i negatius)
power_recipe <- recipe(~ feature, data = data) %>%
  step_YeoJohnson(feature) %>%
  step_normalize(feature)

prepared_power_recipe <- prep(power_recipe, training = data)
transformed_data <- bake(prepared_power_recipe, new_data = data)

cat("Dades transformades:\n[", paste(round(transformed_data$feature, 3), collapse = " "), "]")
Dades transformades:
[ -1.651 -0.825 -0.308 -0.053 0.103 0.383 0.642 1.71 ]

Pots veure que la recipe de R per a aquesta transformació inclou dos passos (step_YeoJohnson seguit de step_normalize), i és això el que garanteix que el nostre exemple produeixi un resultat estandarditzat. Si només necessites desassimetrització (sense mitjana zero/variància unitària), pots eliminar el pas step_normalize.

Si tots els valors fossin estrictament positius, també es podria utilitzar el pas step_BoxCox(feature).

Figura 1: A l’esquerra, una distribució molt esbiaixada. A la dreta, les mateixes dades després d’una transformació de potència, que ara s’assembla a una distribució normal.
  • Aprenentatge profund: Pràcticament mai s’utilitza — les xarxes neuronals no necessiten que les característiques siguin gaussianes; aprenen mapes no lineals per si mateixes.
  • Aprenentatge clàssic: Molt útil en regressió/models clàssics (regressió lineal, GLM) on la asimetria pot afectar el rendiment o violar les assumpcions.

Transformació basada en quantils

Aquesta és una altra transformació no lineal poderosa que pot ser molt efectiva, especialment amb valors extrems.

Principi: Transforma les característiques perquè segueixin una distribució uniforme o normal. Ho fa basant-se en el quantil (o rang) de cada punt de dades. Això significa que els valors extrems sovint es compressen cap a les altres dades.

Quan l’utilitzem: Quan tens valors extrems i vols espaiar els valors més freqüents. Pot ser molt efectiu per transformar la distribució d’una característica en alguna cosa més manejable per al model.

import numpy as np
from sklearn.preprocessing import QuantileTransformer

# Conjunt de dades amb valors atípics
data = np.array([[10], [20], [30], [40], [500]])

# output_distribution pot ser 'uniform' (per defecte) o 'normal'
# n_quantiles és el nombre de quantils que s'utilitzen per al mapatge
quantile_scaler = QuantileTransformer(n_quantiles=5, output_distribution='normal')
transformed_data = quantile_scaler.fit_transform(data)

# Valors ara es mapen a quantils aproximadament normals
print("Dades transformades: ", np.round(transformed_data.T, 3))
Dades transformades:  [[-5.199 -0.674  0.     0.674  5.199]]
Precaució

QuantileTransformer és basat en rang i no lineal; les distàncies i les relacions lineals no es preserven.

Cautela amb dades esparses: QuantileTransformer genera matrius denses, eviteu-ho amb dades esparses d’alta dimensionalitat (p. ex., BoW/TF-IDF) per evitar explosions de memòria.

library(tidymodels)
library(bestNormalize)

data <- tibble(feature = c(10, 20, 30, 40, 500))

quantile_recipe <- recipe(~ feature, data = data) %>%
  step_orderNorm(feature)

prepared_quantile_recipe <- prep(quantile_recipe, training = data)
transformed_data <- bake(prepared_quantile_recipe, new_data = data)

cat("Dades transformades: [",
    paste(round(transformed_data$feature, 3), collapse = " "), "]")
Dades transformades: [ -1.282 -0.524 0 0.524 1.282 ]

step_orderNorm sempre busca una distribució normal (no uniforme). És l’equivalent R-nativ més proper al QuantileTransformer de scikit-learn.

Figura 2: Al costat esquerre, dades amb un clúster dens i desviacions extremes. Al centre, una transformació uniforme distribueix les dades de manera uniforme. Al costat dret, una transformació normal converteix les dades en una corba de campana, mitjançant la mitigació efectiva de les desviacions.
  • Aprenentatge profund: Rarament utilitzat. Els mètodes basats en quantils trencen l’escala relativa de les distàncies entre punts, el que no és idoni per als optimitzadors basats en gradients.
  • Aprenentatge clàssic: Detecció d’anomalies i qualsevol model sensible a les cues pesades/valors atípics.

Normalització de vectors

Aquesta normalització es confon sovint amb les altres, però és fonamentalment diferent.

Diferència Clau: StandardScaler, MinMaxScaler, etc., operen columna per columna (en cada característica independentment). El Normalizer opera fila per fila (en cada mostra independentment).

Principi: Escala cada mostra (fila) perquè tingui una norma unitària (L1 o L2). Això tracta de canviar la llargada del vector de característiques per a una mostra donada, no de comparar valors entre mostres.

Quan utilitzar-lo: Quan la direcció del vector de dades és més important que la seva magnitud. L’ús més comú és en classificació de textos o agrupació amb característiques com TF-IDF, on vols saber si dos documents tracten el mateix tema, independentment de la longitud dels documents.

import numpy as np
from sklearn.preprocessing import Normalizer

# Cada fila és una mostra. Comença amb una matriu 2D estàndard.
data = np.array([
    [3, 4, 0],  # Norma L2 sqrt(3^2+4^2+0^2) = sqrt(25) = 5
    [1, 2, 2],  # Norma L2 sqrt(1^2+2^2+2^2) = sqrt(9) = 3
    [0, 0, 5],  # Norma L2 sqrt(0^2+0^2+5^2) = sqrt(25) = 5
    [0, 0, 0]   # Norma L2 sqrt(0^2+0^2+0^2) = sqrt(0) = 0
])

# Aplica la normalització L2 (per defecte)
norm_scaler = Normalizer(norm='l2')
normalized_data = norm_scaler.fit_transform(data)

print("\nDades normalitzades:\n", normalized_data)

Dades normalitzades:
 [[0.6        0.8        0.        ]
 [0.33333333 0.66666667 0.66666667]
 [0.         0.         1.        ]
 [0.         0.         0.        ]]

No hi ha un Normalitzador directe per fila en recipes. En R, això es fa habitualment manualment amb operacions bàsiques, ja que els fluxos de treball de ML rarament utilitzen la normalització per files fora de l’anàlisi de text.

library(dplyr)

data <- tibble(
  f1 = c(3, 1, 0, 0),
  f2 = c(4, 2, 0, 0),
  f3 = c(0, 2, 5, 0)
)

# Aplica la normalització L2 fila per fila
normalized_data <- data %>%
  rowwise() %>%
  mutate(norm = sqrt(sum(c_across(everything())^2))) %>%
  mutate(across(everything(), ~ ifelse(norm == 0, 0, .x / norm))) %>%
  select(-norm)

print(normalized_data)
# A tibble: 4 × 3
# Rowwise: 
     f1    f2    f3
  <dbl> <dbl> <dbl>
1 0.6   0.8   0    
2 0.333 0.667 0.667
3 0     0     1    
4 0     0     0    

Explicació:
- Cada fila es escala de manera independent per que la seva norma L2 = 1.
- La direcció del vector es conserva, però la magnitud es descarta.
- Aquesta és la raó per la qual és útil en similaritat de text: els documents es comparen per angle (similaritat cosinus), no per longitud.

  • Aprenentatge profund: No és un pas de preprocessament — en canvi, sovint s’utilitza després d’entrenar en embeddings (p. ex., normalitzant L2 els embeddings de paraules abans de la similaritat cosinus).
  • Aprenentatge clàssic: Crucial en extracció de text, agrupament, cerca/retrieval — fa que les comparacions de similaritat cosinus siguin significatives.

Resum

Escalador/Transformador Principi Funciona en Cas d’ús principal Sensibilitat als valors atípics
Escalat MaxAbs Escala pel valor absolut màxim (l’interval depèn del signe de les dades: [-1, 1], [0, 1], o [-1, 0]) Columnes dades esparses (preserva entrades zero). Alta
Transformació de potència Transformació no lineal per fer les dades més gaussianes Columnes Estabilitzant la variància i gestionant dades esbiaixades. Variable
Transformació basada en quantils Mapeja la distribució de dades a una uniforme o normal Columnes Espaiant els grups densos; gestionant valors atípics. Baixa
Normalització de vectors Escala cada mostra a norma unitària (longitud=1) Files Quan la direcció del vector importa més que la magnitud (p. ex., anàlisi de text). N/A (per fila)

Tots els escalers fins ara són integrats. Però què passa si necessites alguna cosa personalitzada? Aquí és on apareix FunctionTransformer (Python) o step_mutate (R).

Per exemple, dues transformacions personalitzades molt pràctiques són:

  • Escalat logarítmic (p. ex., log1p) per a característiques estrictament positives i molt esbiaixades (comptes, ingressos).
  • Clipping / Winsorització per a limitar els valors extrems abans de l’escalat.
# Dades d'exemple: característica 'count' asimètrica + una característica contínua
import numpy as np
import pandas as pd
from sklearn.preprocessing import FunctionTransformer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression

# Data d’entrenament: característica 'count' asimètrica, molts petits, alguns grans
df = pd.DataFrame({
    "count": [0, 1, 2, 3, 10, 50, 200, 1000],  # molt asimètric, molts petits, alguns grans
    "feat":  [2.1, 2.0, 2.3, 2.2, 2.5, 3.0, 3.2, 3.5],
    "y":     [1.2, 1.1, 1.3, 1.2, 1.6, 2.2, 2.6, 3.1]
})

# Exemple 1: log1p sobre la característica 'count' asimètrica
log1p_transformer = FunctionTransformer(np.log1p, feature_names_out="one-to-one")

# Exemple 2: clip (winsorització) sobre 'feat'
def clip_0_99(X):
    # clip per columna a [1a, 99a] percentils
    lo = np.percentile(X, 1, axis=0, keepdims=True)
    hi = np.percentile(X, 99, axis=0, keepdims=True)
    return np.clip(X, lo, hi)

clip_transformer = FunctionTransformer(clip_0_99, feature_names_out="one-to-one")

preprocess = ColumnTransformer(
    transformers=[
        ("log_count", log1p_transformer, ["count"]),
        ("clip_feat", clip_transformer,   ["feat"]),
    ],
    remainder="drop"
)

pipe = Pipeline([
    ("prep", preprocess),
    ("scale", StandardScaler()),
    ("model", LinearRegression())
])

pipe.fit(df[["count","feat"]], df["y"])
Pipeline(steps=[('prep',
                 ColumnTransformer(transformers=[('log_count',
                                                  FunctionTransformer(feature_names_out='one-to-one',
                                                                      func=<ufunc 'log1p'>),
                                                  ['count']),
                                                 ('clip_feat',
                                                  FunctionTransformer(feature_names_out='one-to-one',
                                                                      func=<function clip_0_99 at 0x7fb688fb3f60>),
                                                  ['feat'])])),
                ('scale', StandardScaler()), ('model', LinearRegression())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
print("Mostra transformada:\n", pipe.named_steps["prep"].transform(df[["count","feat"]])[:5])
Mostra transformada:
 [[0.         2.1       ]
 [0.69314718 2.007     ]
 [1.09861229 2.3       ]
 [1.38629436 2.2       ]
 [2.39789527 2.5       ]]

Nota sobre matrius esparses: si passes una matriu esparsa, assegura’t que la teva funció pot manejar-la (p. ex., utilitza operacions compatibles amb scipy.sparse). FunctionTransformer passarà la matriu sense canviar, la teva funció no ha de densificar la matriu de forma inesperada.

library(tidymodels)
library(dplyr)

df <- tibble(
  count = c(0, 1, 2, 3, 10, 50, 200, 1000),
  feat  = c(2.1, 2.0, 2.3, 2.2, 2.5, 3.0, 3.2, 3.5),
  y     = c(1.2, 1.1, 1.3, 1.2, 1.6, 2.2, 2.6, 3.1)
)

# Ajuda per a winsorització (clip columna per columna)
winsor <- function(x, p = c(0.01, 0.99)) {
  qs <- quantile(x, probs = p, na.rm = TRUE, names = FALSE)
  pmin(pmax(x, qs[1]), qs[2])
}

rec <- recipe(y ~ count + feat, data = df) %>%
  # Exemple 1: log1p per a comptatges sesgats
  step_mutate(count = log1p(count)) %>%
  # Exemple 2: winsoritzar feat al [1%, 99%]
  step_mutate(feat  = winsor(feat)) %>%
  # Opcionalment seguir amb normalització
  step_normalize(all_numeric_predictors())

prep_rec <- prep(rec)
baked <- bake(prep_rec, new_data = NULL)
print(baked)
# A tibble: 8 × 3
   count   feat     y
   <dbl>  <dbl> <dbl>
1 -1.11  -0.898   1.2
2 -0.828 -1.07    1.1
3 -0.662 -0.538   1.3
4 -0.544 -0.718   1.2
5 -0.130 -0.177   1.6
6  0.499  0.724   2.2
7  1.06   1.08    2.6
8  1.72   1.59    3.1

Què són les dades esparses?

En termes simples, les dades esparses són dades que estan majoritàriament compostes de zeros.

El seu oposat és les dades denses, on la majoria dels valors són diferents de zero.

No hi ha una regla estricta, però si un percentatge molt alt (p. ex., >90%, >99%) de les entrades de les teves dades són zero, estàs tractant d’un conjunt de dades esparses.

Una analogia intuïtiva: El magatzem de lloguer de pel·lícules

Imagina un enorme magatzem de lloguer de pel·lícules amb 50.000 pel·lícules úniques. Això representa característiques.

Ara, imagina que seguim l’historial de lloguer d’un client, “Alice”. Alice només ha llogat 15 pel·lícules diferents. Això és la mostra.

Si representéssim l’historial de lloguer d’Alice com una fila de dades amb 50.000 columnes (una per cada pel·lícula), es veuria així:

[0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, ..., 5, 0, 0, ...]

Aquesta fila té 50.000 valors, però només 15 d’ells són diferents de zero. Els altres 49.985 valors són zero. Això és un vector altament espars.

Per què l’esparsitat és especial i important

Les dades esparses es tracten de manera diferent per dues raons principals:

  1. Eficàcia d’emmagatzematge i memòria: Seria extremadament ineficient emmagatzemar tots aquests zeros. En lloc d’un array NumPy estàndard, les dades esparses es guarden en estructures especials (com una scipy.sparse.csr_matrix) que només mantenen els elements diferents de zero i les seves ubicacions (índex de fila i columna).
  • Emmagatzematge dens: [0, 0, 7, 0, 4] (emmagatzema tots els 5 elements)
  • Emmagatzematge espars: (row:0, col:2, value:7), (row:0, col:4, value:4) (només emmagatzema 2 elements)

Això pot reduir l’ús de memòria de gigabytes a megabytes.

  1. El problema amb els escaladors estàndard: És per això que necessitem escaladors especials per a dades esparsas. Veiem què passa si utilitzem incorrectament StandardScaler en dades esparsas.
  • La fórmula és \(z = \frac{x - \mu}{\sigma}\).
  • En un conjunt de dades esparsas, la majoria dels valors són 0, de manera que la mitjana (\(\mu\)) serà un nombre molt petit proper a zero (però no exactament zero). Suposem que \(\mu = 0.01\).
  • Què passa quan escales un valor que és 0? \(z = \frac{0 - 0.01}{\sigma}\)
  • El resultat és un nombre diferent de zero!

Acabeu de convertir una matriu que era 99% zeros i estava emmagatzemada eficientment, i la heu convertida en una matriu 100% densa. L’ús de memòria s’explotarà.

Com tractar correctament les dades esparsas

Això és on entren en joc MaxAbsScaler i Normalizer.

  • MaxAbsScaler: La fórmula és \(x_{scaled} = \frac{x_i}{max(|x|)}\).
    • Si un valor \(x\) és \(0\), el resultat és \(\frac{0}{max(|x|)} = 0\).
    • Manté els zeros com a zeros! Preserva l’esparsitat de les teves dades mentre escala els valors diferents de zero.
  • Normalizer: Aquest escalador treballa fila per fila, així que també gestiona inherentment l’esparsitat correctament considerant només els elements diferents de zero en els seus càlculs per a aquella fila.

Resum: Espars vs. Dense

Composició de valors | Majoritàriament valors diferents de zero | La majoria dels valors són zeros |
Exemples comús | Lectures de sensors, edat, ingressos, mesures físiques | Dades de text (Bag-of-Words), interaccions usuari-producte (p. ex., qui va comprar què) |
Emmagatzematge | Arrays NumPy estàndard | Formats especialitzats de matrius esparses (scipy.sparse) |
Escaladors adequats | StandardScaler, MinMaxScaler, RobustScaler | MaxAbsScaler, Normalizer |

Exemple 1: Dades de text amb Bag-of-Words

Això és la font més comuna de dades esparses en l’aprenentatge automàtic. El model “Bag-of-Words” (BoW) representa el text comptant l’ocurrència de cada paraula coneguda, ignorant la gramàtica i l’ordre de les paraules.

Escenari: Tenim un petit corpus de tres documents simples (frases), i volem convertir-los en vectors numèrics.

import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer

# Col·lecció de documents de text
corpus = [
    "L'Anell ha d'anar a Mordor",
    "Frodo porta l'Anell a Mordor",
    "Sauron regeix a Mordor"
]

# Inicialitza el vectoritzador
# Aquest objecte aprendrà el vocabulari i transformarà el text
vectorizer = CountVectorizer()

# Ajusta el vectoritzador al corpus i transforma les dades
# .fit_transform() aprèn el vocabulari
# i converteix el corpus en una matriu de comptatge de paraules.
X = vectorizer.fit_transform(corpus)

# Mostra els resultats en format llegible per a humans
# Obtenir el vocabulari après (característiques)
feature_names = vectorizer.get_feature_names_out()

# Crea un DataFrame de pandas per visualitzar la matriu esparsa
df = pd.DataFrame(X.toarray(), columns=feature_names, index=['Doc 1', 'Doc 2', 'Doc 3'])

print("Vocabulari après (Característiques):", feature_names,
      "\nMatriu de paraules (Representació densa):")
Vocabulari après (Característiques): ['anar' 'anell' 'frodo' 'ha' 'mordor' 'porta' 'regeix' 'sauron'] 
Matriu de paraules (Representació densa):
print(df)
       anar  anell  frodo  ha  mordor  porta  regeix  sauron
Doc 1     1      1      0   1       1      0       0       0
Doc 2     0      1      1   0       1      1       0       0
Doc 3     0      0      0   0       1      0       1       1
print(f"\nForma de la matriu: {X.shape}",
      f"Nombre d'elements no zero: {X.nnz}")

Forma de la matriu: (3, 8) Nombre d'elements no zero: 11

recipes encara no admet matrius esparses. A R, els fluxos de treball esparses generalment utilitzen el paquet Matrix, que representa eficientment conjunts de dades esparses grans (p. ex., dgCMatrix).

library(text2vec)
library(Matrix)

corpus <- c("L'Anell ha d'anar a Mordor",
            "Frodo porta l'Anell a Mordor",
            "Sauron regeix a Mordor")

# Crear un iterador sobre tokens
tokens <- space_tokenizer(corpus)

# Crear un vocabulari
it <- itoken(tokens)
vocab <- create_vocabulary(it)

# Vectoritzador
vectorizer <- vocab_vectorizer(vocab)

# Crear una matriu document-terme (esparsa)
dtm <- create_dtm(it, vectorizer)

print(dtm)
3 x 10 sparse Matrix of class "dgCMatrix"
                     
1 . 1 . 1 1 . . . 1 1
2 1 . . . . 1 1 . 1 1
3 . . 1 . . . . 1 1 1
cat("Dimensions de la matriu:", dim(dtm), "\nEntrades no zero:", length(dtm@x))
Dimensions de la matriu: 3 10 
Entrades no zero: 14

Com pots veure al DataFrame resultant, el resultat és una matriu on:

  • Cada fila és un document.
  • Cada columna és una paraula única del vocabulari.
  • El valor a cada cel·la és el recompte de quantes vegades ha aparegut aquella paraula en aquell document.

Fixa’t en l’elevat nombre de zeros. Fins i tot en aquest petit exemple, la majoria de cel·les són zero. Ara, imagina un vocabulari real amb 50.000 paraules. Una sola frase podria utilitzar només 10-20 d’aquestes paraules, la qual cosa significa que la seva representació vectorial tindria més de 49.980 entrades zero. Això és una matriu esparsa. El CountVectorizer de scikit-learn emmagatzema X de manera intel·ligent en un format scipy.sparse per estalviar memòria. Cridem .toarray() només amb finalitats de visualització.

Exemple 2: Interaccions Usuari-Element

Aquesta és la base dels sistemes de recomanació (com els que fan servir Netflix o Amazon). L’objectiu és representar quins usuaris han interactuat amb (p. ex., comprat, vist o valorat) quins elements.

Escenari: Tenim un petit lloc de comerç electrònic. Volem crear una matriu que representi quins dels nostres 4 usuaris han comprat quins dels nostres 6 productes.

import pandas as pd

# Dades brutes d'interaccions usuari-element (per exemple, d'una base de dades)
# Un '1' indica una compra.
data = {
    'usuari':  ['Alícia', 'Bob', 'Alícia', 'Carles', 'Alícia', 'David', 'Bob'],
    'element': ['Portàtil', 'Ratolí', 'Teclat', 'Ratolí', 'Monitor', 'Teclat', 'Càmera web']
}
df_interactions = pd.DataFrame(data)
df_interactions['comprat'] = 1

print("Dades d'interacció originals:")
Dades d'interacció originals:
print(df_interactions)
   usuari     element  comprat
0  Alícia    Portàtil        1
1     Bob      Ratolí        1
2  Alícia      Teclat        1
3  Carles      Ratolí        1
4  Alícia     Monitor        1
5   David      Teclat        1
6     Bob  Càmera web        1
# Crear la matriu d'interaccions usuari-element utilitzant pivot_table
# Això transforma el format de dades "llarga" en un format de matriu "ampla".
# Omplim les no interaccions amb 0.
user_item_matrix = df_interactions.pivot_table(
    index='usuari',
    columns='element',
    values='comprat',
    fill_value=0
)

print("\nMatriu esparsa d'usuari-element:")

Matriu esparsa d'usuari-element:
print(user_item_matrix)
element  Càmera web  Monitor  Portàtil  Ratolí  Teclat
usuari                                                
Alícia          0.0      1.0       1.0     0.0     1.0
Bob             1.0      0.0       0.0     1.0     0.0
Carles          0.0      0.0       0.0     1.0     0.0
David           0.0      0.0       0.0     0.0     1.0
Nota

Els escaladors compatibles amb dades esparses són rars a R; la majoria dels fluxos de treball aquí depenen de paquets com Matrix, text2vec o frameworks especialitzats de recomanació més que de recipes.

library(tidyr)
library(dplyr)
library(Matrix)

data <- tibble(
  user = c('Alícia', 'Bob', 'Alícia', 'Carles', 'Alícia', 'David', 'Bob'),
  item = c('Portàtil', 'Ratolí', 'Teclat', 'Ratolí', 'Monitor', 'Teclat', 'Càmera web'),
  purchased = 1
)

cat("Dades d'interacció originals:\n")
Dades d'interacció originals:
print(data)
# A tibble: 7 × 3
  user   item       purchased
  <chr>  <chr>          <dbl>
1 Alícia Portàtil           1
2 Bob    Ratolí             1
3 Alícia Teclat             1
4 Carles Ratolí             1
5 Alícia Monitor            1
6 David  Teclat             1
7 Bob    Càmera web         1
# Dense user-item matrix
user_item_matrix <- data %>%
  pivot_wider(names_from = item, values_from = purchased, values_fill = 0)

cat("\nMatriu d'usuari-producte densa:\n")

Matriu d'usuari-producte densa:
print(user_item_matrix)
# A tibble: 4 × 6
  user   Portàtil Ratolí Teclat Monitor `Càmera web`
  <chr>     <dbl>  <dbl>  <dbl>   <dbl>        <dbl>
1 Alícia        1      0      1       1            0
2 Bob           0      1      0       0            1
3 Carles        0      1      0       0            0
4 David         0      0      1       0            0
# Sparse representation
sparse_matrix <- sparseMatrix(
  i = as.numeric(factor(data$user)),
  j = as.numeric(factor(data$item)),
  x = data$purchased,
  dimnames = list(levels(factor(data$user)), 
                  levels(factor(data$item)))
)

cat("\nMatriu d'usuari-producte esparsa:\n")

Matriu d'usuari-producte esparsa:
print(sparse_matrix)
4 x 5 sparse Matrix of class "dgCMatrix"
       Càmera web Monitor Portàtil Ratolí Teclat
Alícia          .       1        1      .      1
Bob             1       .        .      1      .
Carles          .       .        .      1      .
David           .       .        .      .      1

Observa la matriu esparsa usuari-element resultant.

  • Cada fila és un usuari.
  • Cada columna és un element disponible per a la venda.
  • Un 1 significa que l’usuari ha comprat aquest element. Un 0 significa que no ho ha fet.

De nou, la matriu és majoritàriament zeros. Alice ha comprat 3 d’entre els 6 elements disponibles, de manera que la seva fila és 50 % esparsa. En un sistema real com Amazon amb milions d’usuaris i milions de productes, qualsevol usuari en particular haurà comprat una fracció infinitesimalment petita del total d’elements disponibles. La seva fila corresponent a la matriu usuari-element seria increïblement esparsa.

Aquesta esparsitat és fonamental per al funcionament dels algoritmes de filtrat col·laboratiu. Utilitzen aquesta matriu per trobar usuaris amb patrons similars d’entrades no zero per fer recomanacions.

Escalat MaxAbs vs Estandarització sobre dades esparses

Aquest primer exemple és dens a propòsit per mostrar l’efecte numèric; el bloc següent mostra la veritable esparsitat CSR.

import numpy as np
from sklearn.preprocessing import MaxAbsScaler, StandardScaler

# Un array DENSE utilitzat per il·lustrar l'escalat
# Cada fila és un document, cada columna és un compte de paraules
data = np.array([
    [5, 0, 0, 1, 0],
    [0, 1, 0, 0, 0],
    [0, 0, 0, 0, 3],
    [1, 0, 2, 0, 0]
])

# --- ESCALAT CORRECTE amb MaxAbsScaler ---
maxabs_scaler = MaxAbsScaler()
maxabs_scaled = maxabs_scaler.fit_transform(data)
print("Escalat amb MaxAbsScaler (els zeros es preserven):\n", maxabs_scaled)
Escalat amb MaxAbsScaler (els zeros es preserven):
 [[1.  0.  0.  1.  0. ]
 [0.  1.  0.  0.  0. ]
 [0.  0.  0.  0.  1. ]
 [0.2 0.  1.  0.  0. ]]
# --- ESCALAT INCORRECTE amb StandardScaler ---
std_scaler = StandardScaler()
std_scaled = std_scaler.fit_transform(data)
print("Escalat amb StandardScaler (l'esparsitat és destruïda):\n", np.round(std_scaled, 2))
Escalat amb StandardScaler (l'esparsitat és destruïda):
 [[ 1.7  -0.58 -0.58  1.73 -0.58]
 [-0.73  1.73 -0.58 -0.58 -0.58]
 [-0.73 -0.58 -0.58 -0.58  1.73]
 [-0.24 -0.58  1.73 -0.58 -0.58]]
import numpy as np
from scipy.sparse import csr_matrix
from sklearn.preprocessing import MaxAbsScaler, StandardScaler

# Matriu esparsa real (CSR)
X = csr_matrix(np.array([
    [5, 0, 0, 1, 0],
    [0, 1, 0, 0, 0],
    [0, 0, 0, 0, 3],
    [1, 0, 2, 0, 0],
]))

print("nombre de zeros no nuls abans:", X.nnz)
nombre de zeros no nuls abans: 6
# --- CORRECT: preserva la esparsitat
X_maxabs = MaxAbsScaler().fit_transform(X)
print("nombre de zeros no nuls després de MaxAbs:", X_maxabs.nnz)
nombre de zeros no nuls després de MaxAbs: 6
# --- CORRECT per esparsa: sense centrat
X_std = StandardScaler(with_mean=False).fit_transform(X)
print("nombre de zeros no nuls després de StandardScaler(with_mean=False):", X_std.nnz)
nombre de zeros no nuls després de StandardScaler(with_mean=False): 6
# --- INCORRECT: el centrament de matrius esparses no és compatible
try:
    _ = StandardScaler(with_mean=True).fit_transform(X)
except ValueError as e:
    print("El centrament de matrius esparses no és compatible:", str(e)[:62], "…")
El centrament de matrius esparses no és compatible: Cannot center sparse matrices: pass `with_mean=False` instead. …
library(Matrix)

# Una matriu esparsa
data <- Matrix(
  c(5, 0, 0, 1, 0,
    0, 1, 0, 0, 0,
    0, 0, 0, 0, 3,
    1, 0, 2, 0, 0),
  nrow = 4, byrow = TRUE, sparse = TRUE
)

print("Matriu esparsa original:")
[1] "Matriu esparsa original:"
print(data)
4 x 5 sparse Matrix of class "dgCMatrix"
              
[1,] 5 . . 1 .
[2,] . 1 . . .
[3,] . . . . 3
[4,] 1 . 2 . .
# Escalat manual amb MaxAbs
max_abs_vals <- apply(abs(data), 2, max)
maxabs_scaled <- data
for (j in seq_along(max_abs_vals)) {
  if (max_abs_vals[j] != 0) {
    maxabs_scaled[, j] <- data[, j] / max_abs_vals[j]
  }
}

print("Escalat amb MaxAbs (zeros preservats):")
[1] "Escalat amb MaxAbs (zeros preservats):"
print(maxabs_scaled)
4 x 5 sparse Matrix of class "dgCMatrix"
                
[1,] 1.0 . . 1 .
[2,] .   1 . . .
[3,] .   . . . 1
[4,] 0.2 . 1 . .
# Estandarització manual (destruint la esparsitat)
col_means <- colMeans(data)
col_sds   <- apply(data, 2, sd)

std_scaled <- as.matrix(scale(data, center = TRUE, scale = TRUE))

print("Escalat amb Estandarització (dens):")
[1] "Escalat amb Estandarització (dens):"
print(round(std_scaled, 2))
      [,1] [,2] [,3] [,4] [,5]
[1,]  1.47 -0.5 -0.5  1.5 -0.5
[2,] -0.63  1.5 -0.5 -0.5 -0.5
[3,] -0.63 -0.5 -0.5 -0.5  1.5
[4,] -0.21 -0.5  1.5 -0.5 -0.5
attr(,"scaled:center")
[1] 1.50 0.25 0.50 0.25 0.75
attr(,"scaled:scale")
[1] 2.380476 0.500000 1.000000 0.500000 1.500000

Enllaços de Documentació

Python (scikit-learn)

  • MaxAbsScaler — escala pel valor absolut màxim; conserva els zeros (segur en dades esparses).
  • PowerTransformer — transformacions Yeo–Johnson / Box–Cox amb estandardització opcional.
  • QuantileTransformer — mapa a uniforme/normal mitjançant rangs (sortida d’arrays densos).
  • Normalizer — normalització L1/L2 per fila per a mostres.
  • FunctionTransformer — envolupament de funcions personalitzades NumPy/SciPy en pipelines.
  • ColumnTransformer — preprocessament columnar per a característiques heterogènies.

R (tidymodels / recipes)

  • step_YeoJohnson() — transformació de potència Yeo–Johnson.
  • step_BoxCox() — transformació de potència Box–Cox (només dades positives).
  • step_orderNorm() — orderNorm (bestNormalize) per aproximar normalitat.
  • step_mutate() — transformacions de columna personalitzades dins d’una recepta.

Referències de dades esparses

  • SciPy: Matrius esparses — formats CSR/CSC i operacions.
  • R: Matrix package — classes de matrius esparses.
  • R: text2vec — extracció de característiques de text (DTMs/TF-IDF) amb matrius esparses.

Què ve a continuació: Unint-ho tot

A través d’aquest viatge de tres parts, hem explorat tot el panorama de l’escalat de característiques: des del fonamental “per què” a la Part 1, als treballadors quotidians a la Part 2, i ara als especialistes potents a la Part 3.

Però la teoria és una cosa, i veure l’impacte és una altra.

En l’últim post conclusiu d’aquesta sèrie, unirem tot. Construirem una taula de resum completa de tots els escaladors que hem discutit, oferint notebooks descarregables de Python i R amb tot el codi en un sol lloc, i, el més important, farem una demostració pràctica. Entrenarem un model d’aprenentatge automàtic senzill amb dades no escalades, veurem com funciona i després aplicarem la tècnica d’escalat correcta per observar la millora tangible. Aquesta serà la demostració pràctica del perquè l’escalat és un pas de preprocessament crucial quan el model ho exigeix.