Modelado / Modelling

Un modelo es una representación simplificada de la realidad creada para un propósito concreto. Las hipótesis que asumimos, la selección de características relevantes, las restricciones y la trazabilidad de cómo se construye forman parte de ese modelo.

Un ejemplo clásico es un mapa: una representación cartográfica que permite situar lugares de interés, calcular distancias o analizar relaciones topológicas entre puntos.

cfac4eb07db649678868d960d16df613

Fuente imagen: Wikipedia

En ciencia de datos distinguimos, de forma general:

  • Modelos predictivos: conjuntos de fórmulas o reglas que permiten estimar valores desconocidos (por ejemplo, predecir ventas futuras).

  • Modelos descriptivos: ayudan a descubrir patrones subyacentes o estructura en los datos (por ejemplo, segmentos de clientes).

Los datos usados por el modelo representan hechos u observaciones de la realidad (o de otro modelo). Siguiendo el ejemplo del mapa, serían los puntos, contornos o bordes del territorio representado.

a0e4e87ed0c9401cbc9a99f94de4fe0c

Fuente de ambas figuras [2]

El proceso de aprendizaje automático

Podemos esquematizar el proceso de aprendizaje automático en varias etapas:

  1. Preparación de datos
    Carga, limpieza, transformación y división del conjunto de datos.
    • Incluye el análisis exploratorio de datos (EDA) y la selección de características.

  2. Selección de la técnica
    Elección del tipo de tarea (clasificación, regresión, clustering…), de los modelos candidatos y de las restricciones prácticas (tiempo, interpretabilidad, recursos, etc.).
  3. Ajuste de hiperparámetros
    Los hiperparámetros son configuraciones del modelo que no se aprenden directamente de los datos (por ejemplo, profundidad máxima de un árbol) y deben ajustarse manualmente o mediante búsqueda automática.
  4. Evaluación del modelo
    Pruebas iniciales, elección de métricas, ajustes iterativos y evaluación final sobre datos no vistos.

Selección de atributos o métricas adecuadas

Dado un conjunto de muestras con muchas características, el reto es elegir aquellas que realmente contribuyen al aprendizaje del modelo.

Supongamos el siguiente ejemplo de personas: 1c73fd0e8fbd4a41b6dbedf5390fa7a8

Las características disponibles son:

  • Forma de la cabeza: cuadrada o circular.

  • Forma del cuerpo: rectangular o ovalada.

  • Color del cuerpo: negro o blanco.

  • Función de su compra: yes/no.

Esta última característica es el atributo objetivo (target): lo que queremos predecir en futuros clientes.

Para predecirlo, debemos preguntarnos qué atributos aportan información útil. Si solo usáramos, por ejemplo, el color del cuerpo, podríamos perder variabilidad importante del resto de la población y construir un modelo pobre.

Más adelante retomaremos este problema de selección de atributos introduciendo los conceptos de entropía y ganancia de información.

Nuestra primera aplicación de machine learning (ML)

### Instalación de librerías

La librería de Python que implementa múltiples algoritmos de ML es scikit-learn

Trabajemos con las versiones de librerías usadas en Google Colab (09/12/25) como sistema de referencia.

Podemos usar UV:

[113]:
!uv init
Adding `07-machinelearning` as member of workspace `/Users/isaac/Projects/Subjects/TTAD_master`
Initialized project `07-machinelearning`
[114]:
!uv add scikit-learn==1.6.1
!uv add sklearn-pandas==2.2.0
!uv add seaborn==0.13.2
Resolved 100 packages in 280ms
Audited 5 packages in 0.31ms
Resolved 100 packages in 18ms
Audited 11 packages in 0.04ms
Resolved 100 packages in 14ms
Audited 20 packages in 0.05ms

Y si tuvieramos pip:

!pip install scikit-learn==1.6.1
!pip install sklearn-pandas==2.2.0
!pip install seaborn==0.13.2

Datos

Vamos a utilizar el siguiente catálogo de datos: Mushroom Data Set http://archive.ics.uci.edu/ml/datasets/Mushroom

Disponible en: “data/mushrooms.csv”

[115]:
import pandas as pd
import numpy as np

df = pd.read_csv("data/mushrooms.csv")

# Por simplicidad, renombramos las columnas
es_col = [
    "clase",
    "forma del sombrero",
    "superficie del sombrero",
    "color del sombrero",
    "magulladuras",
    "olor",
    "unión de las láminas",
    "espaciamiento de las láminas",
    "tamaño de las láminas",
    "color de las láminas",
    "forma del tallo",
    "raíz del tallo",
    "superficie del tallo por encima del anillo",
    "superficie del tallo por debajo del anillo",
    "color del tallo por encima del anillo",
    "color del tallo por debajo del anillo",
    "tipo de velo",
    "color del velo",
    "número de anillos",
    "tipo de anillo",
    "color de la impresión de esporas",
    "población",
    "hábitat"
]
df.columns = es_col

print(df.shape)
print("-"*100)
print(df.columns)
print("-"*100)
print(df.head(3))
(8124, 23)
----------------------------------------------------------------------------------------------------
Index(['clase', 'forma del sombrero', 'superficie del sombrero',
       'color del sombrero', 'magulladuras', 'olor', 'unión de las láminas',
       'espaciamiento de las láminas', 'tamaño de las láminas',
       'color de las láminas', 'forma del tallo', 'raíz del tallo',
       'superficie del tallo por encima del anillo',
       'superficie del tallo por debajo del anillo',
       'color del tallo por encima del anillo',
       'color del tallo por debajo del anillo', 'tipo de velo',
       'color del velo', 'número de anillos', 'tipo de anillo',
       'color de la impresión de esporas', 'población', 'hábitat'],
      dtype='object')
----------------------------------------------------------------------------------------------------
  clase forma del sombrero superficie del sombrero color del sombrero  \
0     p                  x                       s                  n
1     e                  x                       s                  y
2     e                  b                       s                  w

  magulladuras olor unión de las láminas espaciamiento de las láminas  \
0            t    p                    f                            c
1            t    a                    f                            c
2            t    l                    f                            c

  tamaño de las láminas color de las láminas  ...  \
0                     n                    k  ...
1                     b                    k  ...
2                     b                    n  ...

  superficie del tallo por debajo del anillo  \
0                                          s
1                                          s
2                                          s

  color del tallo por encima del anillo color del tallo por debajo del anillo  \
0                                     w                                     w
1                                     w                                     w
2                                     w                                     w

  tipo de velo color del velo número de anillos tipo de anillo  \
0            p              w                 o              p
1            p              w                 o              p
2            p              w                 o              p

  color de la impresión de esporas población hábitat
0                                k         s       u
1                                n         n       g
2                                n         n       m

[3 rows x 23 columns]

### Planteamiento del objetivo de ML

¿Cuál es el objetivo que planteamos? ¿Qué valos nos interesa encontrar?

En este caso, queremos determinar la población según las características del hongo

Esta variable contiene los siguientes datos:

  • population: abundant=a, clustered=c, numerous=n, scattered=s, several=v, solitary=y

[116]:
# Podemos ver los valores de la variable objetivo
print(df["población"].head())
value,counts = np.unique(df["población"], return_counts=True)
print(value,counts)

0    s
1    n
2    n
3    s
4    a
Name: población, dtype: object
['a' 'c' 'n' 's' 'v' 'y'] [ 384  340  400 1248 4040 1712]

¿Qué tipo de poblema de ML es?

  • Supervisado o No Supervisado?

  • Clasificación, Regresión o Agrupamiento?

Selección de características (feature selection)

En este primer problema, usaremos todas las muestras.

Tenemos que separar el dataset en dos partes: características y variable objetivo.

[117]:
df_y = df["población"].copy()
df_x = df.drop(labels=["población"],axis=1).copy()

print(df_x.shape)
print(df_y.shape)
(8124, 22)
(8124,)

Preparación de los datos de entreno (train) y datos de comprobación (test)

Podemos hacerlo de múltiples maneras, y es crítico en series temporales. Hay métodos automaticos que nos separan las muestras como por ejemplo: train_test_split

[118]:
from sklearn.model_selection import train_test_split

x_train, x_test,  y_train, y_test = train_test_split(df_x,df_y, test_size=0.2, random_state = 0)
print("x_train:", x_train.shape)
print("y_train: ",y_train.shape)
print("-"*100)
print("x_test: ",x_test.shape)
print("y_test: ",y_test.shape)

x_train: (6499, 22)
y_train:  (6499,)
----------------------------------------------------------------------------------------------------
x_test:  (1625, 22)
y_test:  (1625,)

Elección del algoritmo

Existen múltiples algoritmos según el tipo de problema de ML. En este ejemplo, usaremos ```Máquinas de Vectores de Soporte (SVM)’’’

[ ]:
from sklearn.svm import SVC

clf = SVC(C=1.0, kernel="linear", random_state=0) #¿Que implica el kernel?
#clf.fit(x_train, y_train) # Alerta: Genera un error!!! ¿Por qué?

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/6j/7gfvt_29797dypw8t1wttblw0000gn/T/ipykernel_24248/3448651391.py in ?()
      1 from sklearn.svm import SVC
      2
      3 clf = SVC(C=1.0, kernel="linear", random_state=0) #¿Que implica el kernel?
----> 4 clf.fit(x_train, y_train) # Alerta: Genera un error!!! ¿Por qué?

~/Projects/Subjects/TTAD_master/.venv/lib/python3.12/site-packages/sklearn/base.py in ?(estimator, *args, **kwargs)
   1385                 skip_parameter_validation=(
   1386                     prefer_skip_nested_validation or global_skip_validation
   1387                 )
   1388             ):
-> 1389                 return fit_method(estimator, *args, **kwargs)

~/Projects/Subjects/TTAD_master/.venv/lib/python3.12/site-packages/sklearn/svm/_base.py in ?(self, X, y, sample_weight)
    193
    194         if callable(self.kernel):
    195             check_consistent_length(X, y)
    196         else:
--> 197             X, y = validate_data(
    198                 self,
    199                 X,
    200                 y,

~/Projects/Subjects/TTAD_master/.venv/lib/python3.12/site-packages/sklearn/utils/validation.py in ?(_estimator, X, y, reset, validate_separately, skip_check_array, **check_params)
   2957             if "estimator" not in check_y_params:
   2958                 check_y_params = {**default_check_params, **check_y_params}
   2959             y = check_array(y, input_name="y", **check_y_params)
   2960         else:
-> 2961             X, y = check_X_y(X, y, **check_params)
   2962         out = X, y
   2963
   2964     if not no_val_X and check_params.get("ensure_2d", True):

~/Projects/Subjects/TTAD_master/.venv/lib/python3.12/site-packages/sklearn/utils/validation.py in ?(X, y, accept_sparse, accept_large_sparse, dtype, order, copy, force_writeable, force_all_finite, ensure_all_finite, ensure_2d, allow_nd, multi_output, ensure_min_samples, ensure_min_features, y_numeric, estimator)
   1366         )
   1367
   1368     ensure_all_finite = _deprecate_force_all_finite(force_all_finite, ensure_all_finite)
   1369
-> 1370     X = check_array(
   1371         X,
   1372         accept_sparse=accept_sparse,
   1373         accept_large_sparse=accept_large_sparse,

~/Projects/Subjects/TTAD_master/.venv/lib/python3.12/site-packages/sklearn/utils/validation.py in ?(array, accept_sparse, accept_large_sparse, dtype, order, copy, force_writeable, force_all_finite, ensure_all_finite, ensure_non_negative, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, estimator, input_name)
   1052                         )
   1053                     array = xp.astype(array, dtype, copy=False)
   1054                 else:
   1055                     array = _asarray_with_order(array, order=order, dtype=dtype, xp=xp)
-> 1056             except ComplexWarning as complex_warning:
   1057                 raise ValueError(
   1058                     "Complex data not supported\n{}\n".format(array)
   1059                 ) from complex_warning

~/Projects/Subjects/TTAD_master/.venv/lib/python3.12/site-packages/sklearn/utils/_array_api.py in ?(array, dtype, order, copy, xp, device)
    835         # Use NumPy API to support order
    836         if copy is True:
    837             array = numpy.array(array, order=order, dtype=dtype)
    838         else:
--> 839             array = numpy.asarray(array, order=order, dtype=dtype)
    840
    841         # At this point array is a NumPy ndarray. We convert it to an array
    842         # container that is consistent with the input's namespace.

~/Projects/Subjects/TTAD_master/.venv/lib/python3.12/site-packages/pandas/core/generic.py in ?(self, dtype, copy)
   2167             )
   2168         values = self._values
   2169         if copy is None:
   2170             # Note: branch avoids `copy=None` for NumPy 1.x support
-> 2171             arr = np.asarray(values, dtype=dtype)
   2172         else:
   2173             arr = np.array(values, dtype=dtype, copy=copy)
   2174

ValueError: could not convert string to float: 'e'
[ ]:
# El error proviene porque los SVM tan solo funcionan con variables continuas (números)
# Algunos algoritmos como los árboles de decisión, si funcionan con variables categoricas
# En nuestro caso, tenemos que transformar las variables categoricas a variables discretas

# Existen múltiples maneras de hacerlo. En este caso, usaremos el ```LabelEncoder```
# Basicamente, lo que hace es asignar un número a cada categoría. La gestión de esta asignación recae en este encoder.
# La opción más correcta es usar One-Hot Encoding (Anexo A), pero dada su dificultad lo haremos de esta manera.
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder() # lo creamos

# Aplicamos el encoder a todas las columnas del dataset inicial!!!
for i in df.columns:
    df[i] = le.fit_transform(df[i]) # lo aplicamos a cada columna,

print(df.iloc[:3,:3]) #imprimimos un par de filas y columnas para ver el resultado

# Por lo tanto, no nos quedará más remedio que crear de nuevo la partición del dataset con la variable objetivo y las características
x_train, x_test,  y_train, y_test  = train_test_split(df.drop('población', axis=1),df['población'], test_size=0.2,random_state=0)
   clase  forma del sombrero  superficie del sombrero
0      1                   5                        2
1      0                   5                        2
2      0                   0                        2
[127]:
clf.fit(x_train, y_train) # Ahora sí! Entrenamos el modelo: TRAIN
[127]:
SVC(kernel='linear', random_state=0)
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.
[128]:
# Una vez entrenado, podemos usar el modelo para predecir sobre nuevos datos o sobre los datos de control
y_pred = clf.predict(x_test) # predecimos sobre los valores reservados
[131]:
print(y_pred[0:5])
print(y_test[0:5])
[4 5 2 3 5]
380     3
3641    5
273     2
1029    0
684     4
Name: población, dtype: int64

Métricas

Los datos de comprobación o de test nos sirven para medir la bondad del algoritmo sobre valores objetivos que conocemos.

Las métricas dependerán del tipo de algoritmo. En este caso, estamos aplicando un algoritmo de clasificación, por lo tanto, tenemos métricas como la precisión, recall, f1-score, etc.

[ ]:
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred)) #comprobamos el error
              precision    recall  f1-score   support

           0       0.44      0.58      0.50        65
           1       0.67      1.00      0.81        62
           2       0.48      0.33      0.39        93
           3       0.45      0.42      0.44       237
           4       0.79      0.63      0.70       827
           5       0.48      0.70      0.57       341

    accuracy                           0.61      1625
   macro avg       0.55      0.61      0.57      1625
weighted avg       0.64      0.61      0.62      1625

Cuando se obtienen los valores, el problema continua: ¿Qué interpretación podemos hacer? ¿Es un buen método?…

Evaluación de un modelo de aprendizaje

La evaluación tiene como propósito validar el modelo y ganar confianza en su uso.

Podemos distinguir dos perspectivas:

  • Cualitativa
    ¿El modelo contribuye a los objetivos de negocio y a la toma de decisiones?
    ¿Es integrable en los sistemas existentes?
    ¿Encaja con el stack tecnológico y los requisitos legales/éticos?
  • Cuantitativa
    Se apoya en métricas: tanto de rendimiento computacional (tiempo, memoria, coste) como de desempeño del modelo (error, precisión, etc.).

Métricas

Las métricas concretas dependen del tipo de problema.
En problemas de clasificación y regresión se utilizan, entre otras, las siguientes.

En clasificación

40771d668f6c49ed8dab04cb71f860d4

Source: NillsF blog

  • Accuracy: proporción de predicciones correctas sobre el total: $ \frac{TP+TN}{Total}.$

  • Precision: proporción de predicciones positivas que son realmente positivas: \(\frac{TP}{Results} = \frac{TP}{TP+FP}\)

  • Recall (sensibilidad): proporción de positivos reales correctamente identificados: \(\frac{TP}{Predictive Results} = \frac{TP}{TP+FN}\)

  • F1-score: media armónica de precisión y recall: \(\frac{2}{recall^{-1}+precision^{-1}}\)

  • Área bajo la curva ROC (AUC): para clasificación binaria.
    La curva ROC compara la tasa de verdaderos positivos frente a la tasa de falsos positivos para distintos umbrales, y el AUC mide la capacidad del modelo para separar ambas clases.
  • Matriz de confusión: tabla que cruza valores reales con valores predichos:

    \[\begin{split}\begin{equation} \begin{pmatrix} TP & FN \\ FP & TN \end{pmatrix} \end{equation}\end{split}\]

En regresión

  • Mean Absolute Error (MAE): media de las diferencias absolutas entre predicciones y valores reales.

  • Mean Squared Error (MSE): media de los cuadrados de las diferencias entre predicciones y valores reales (no es exactamente la desviación estándar, sino su cuadrado excepto por factores de normalización).

  • Coeficiente de determinación (R^2): indica qué proporción de la variabilidad de la variable objetivo explica el modelo.

  • (R^2) ajustado (Adjusted (R^2)): versión de (R^2) que penaliza la inclusión de variables adicionales y es más adecuada para comparar modelos con distinto número de predictores.

Muchas de estas métricas (y otras adicionales) están implementadas en la librería scikit-learn:

Nota: Iremos utilizando las principales métricas en las unidades siguientes.

Para el caso anterior, podemos calcular algunas de estas métricas de la siguiente manera:

[ ]:
from sklearn.metrics import accuracy_score

accuracy_score(y_test, y_pred)
0.6123076923076923
[ ]:
from sklearn import metrics

metrics.precision_score(y_test,y_pred,average="weighted")
0.6413835956221657
[ ]:
print("Recall: ",metrics.recall_score(y_test,y_pred,average="weighted"))
print("F1_score: ",metrics.f1_score(y_test,y_pred,average="weighted"))
Recall:  0.6123076923076923
F1_score:  0.6153073145466426

Rendimiento computacional

Cada operación tiene un coste computacional: no solo tiempo de ejecución, sino también memoria para almacenar datos y valores intermedios.

La capacidad de CPU/GPU suele medirse en operaciones por segundo, por ejemplo:

  • MIPS (millones de instrucciones por segundo)

  • MFLOPS / GFLOPS (millones / miles de millones de operaciones en coma flotante por segundo)

Este rendimiento influye directamente en el tiempo de entrenamiento y de predicción de los modelos.

El lenguaje de programación, su compilador/intérprete y las librerías numéricas condicionan cómo se aprovechan CPU y GPU: número de cores utilizados, organización de tareas, gestión de memoria, etc.

La complejidad del modelo, el tamaño de los datos y su representación afectan al rendimiento y, por tanto, a su aplicabilidad en un entorno real, donde el tiempo de respuesta debe ser razonable.
Ejemplo: un sistema de recomendación cuyos resultados se calculan dinámicamente al cargar una página web.
[ ]:
clf.fit(x_train, y_train)
SVC(kernel='linear', random_state=0)
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.
[ ]:
from time import perf_counter

t0 = perf_counter()
clf.fit(x_train, y_train)
t1 = perf_counter()
print("Tiempo de entrenamiento:", t1 - t0, "s")

t0 = perf_counter()
y_pred = clf.predict(x_test)
t1 = perf_counter()
print("Tiempo de predicción:", t1 - t0, "s")

Tiempo de entrenamiento: 0.48070470802485943 s
Tiempo de predicción: 0.11679345811717212 s
[ ]:
!uv add memory_profiler
Resolved 99 packages in 0.48ms
Audited 94 packages in 0.01ms
[ ]:
from memory_profiler import memory_usage

def train():
    clf.fit(x_train, y_train)
    return clf

mem_usage = memory_usage(train)
print("Memoria máxima (MB):", max(mem_usage))
Memoria máxima (MB): 242.875

La memoria no es un problema a priori que debe de preocuparos en este punto de vuestro aprendizaje. El tiempo de entrenamiento y predicción, sí.

License: CC BY 4.0 Isaac Lera and Gabriel Moya Universitat de les Illes Balears isaac.lera@uib.edu, gabriel.moya@uib.edu

Anexo A

`One-Hot Encoding’ sería la técnica más idonea para transformar las categorías del dataset de mushrooms.

[5]:
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.svm import SVC
from sklearn.metrics import classification_report
import pandas as pd

df = pd.read_csv("data/mushrooms.csv")

# Por simplicidad, renombramos las columnas
es_col = [
    "clase",
    "forma del sombrero",
    "superficie del sombrero",
    "color del sombrero",
    "magulladuras",
    "olor",
    "unión de las láminas",
    "espaciamiento de las láminas",
    "tamaño de las láminas",
    "color de las láminas",
    "forma del tallo",
    "raíz del tallo",
    "superficie del tallo por encima del anillo",
    "superficie del tallo por debajo del anillo",
    "color del tallo por encima del anillo",
    "color del tallo por debajo del anillo",
    "tipo de velo",
    "color del velo",
    "número de anillos",
    "tipo de anillo",
    "color de la impresión de esporas",
    "población",
    "hábitat"
]
df.columns = es_col

X = df.drop('población', axis=1)
y = df['población']

ct = ColumnTransformer(
    transformers=[
        ('ohe', OneHotEncoder(handle_unknown='ignore'), X.columns)
    ],
    remainder='drop'
)

X_encoded = ct.fit_transform(X)

x_train, x_test, y_train, y_test = train_test_split(
    X_encoded, y, test_size=0.2, random_state=0
)

print(type(x_train)) # https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html
print("-"*100)
print(x_train.shape) # 113!
print("-"*100)
print(x_train[:3,:3])

print("-+"*50)
clf = SVC(C=1.0, kernel="linear", random_state=0)
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
print(classification_report(y_test, y_pred))
<class 'scipy.sparse._csr.csr_matrix'>
----------------------------------------------------------------------------------------------------
(6499, 113)
----------------------------------------------------------------------------------------------------
<Compressed Sparse Row sparse matrix of dtype 'float64'
        with 3 stored elements and shape (3, 3)>
  Coords        Values
  (0, 0)        1.0
  (1, 0)        1.0
  (2, 0)        1.0
-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
              precision    recall  f1-score   support

           a       0.44      0.58      0.50        65
           c       0.68      0.87      0.76        62
           n       0.36      0.19      0.25        93
           s       0.43      0.63      0.51       237
           v       0.77      0.59      0.67       827
           y       0.45      0.56      0.50       341

    accuracy                           0.58      1625
   macro avg       0.52      0.57      0.53      1625
weighted avg       0.62      0.58      0.59      1625

[ ]: