Cómo programar un árbol de decisión en Python desde 0

El árbol de decisión es uno de los algoritmos de machine learning más utilizados debido a su facilidad de interpretación. Sin embargo, ¿sabes cómo funcionan? En este post te voy a explicar todo lo que necesitas sobre sobre los árboles de decisión. Para ello, vamos a crear nuestro propio árbol de decisión en Python desde 0. ¿Suena interesante? ¡Pues vamos a ello!

Entendiendo cómo funciona un árbol de decisión

Un árbol de decisión consiste en crear distintas reglas mediante las cuales hacemos la predicción. Por ejemplo, digamos que entrenamos un algoritmo que prediga si una persona es, o no, obesa en función de su altura y su peso. Para ello, contamos los datos de este dataset.

import pandas as pd
import numpy as np

data = pd.read_csv("data.csv")
data.head()
   Gender  Height  Weight  Index
0    Male     174      96      4
1    Male     189      87      2
2  Female     185     110      4
3  Female     195     104      3
4    Male     149      61      3

Imaginamos que queremos predecir si la persona es, o no, obesa. Según la descripción del dataset, las personas con indice de 4 o 5 son obesas, por lo que podríamos crear una variable que refleje esto:

data['obese'] = (data.Index >= 4).astype('int').astype('str')
data.drop('Index', axis = 1, inplace = True)

En ese caso, un árbol de decisión nos diría distintas reglas, como por ejemplo, que si el peso de la persona es superior a 100kg, lo más probable es que esa persona sea obesa. Sin embargo, ese corte no será preciso: habrá personas que pesen 100kg que no sean obesas. Así pues, el árbol de decisión sigue creando más ramas que generan nuevas condiciones para ir “afinando” nuestras predicciones.

Como ves, los nodos de decisión, suelen tener subnodos que sirven para “afinar” la predicción del nodo anterior. Esto es así hasta que llegamos a un nodo que no se divide. A ese último nodo se le conoce como nodo hoja o leaf node. Veamos un ejemplo gráfico:

nomenclatura de un árbol de decisión

Asimismo, comentar que los árboles de decisión sirven tanto para problemas de regresión como para problemas de clasificación. Nosotros programaremos un árbol que sirva para las dos funciones.

Ahora ya tienes una intuición sobre el funcionamiento de este algoritmo, pero seguro que tienes muchas dudas. ¿Cómo decide el algoritmo qué variable utilizar como primer corte? ¿Cómo elige los valores? Veámoslo poco a poco programando nuestro propio árbol de decisión desde 0 en Python.

La impureza y las funciones de coste de un árbol de decisión

Como en todos los algoritmos, la función de coste es la base del algoritmo. En el caso de los árboles de decisión, existen dos principales funciones de coste: el índice de Gini y la entropía.

Cualquier de las funciones de coste que utilicemos se basan en medir la impureza. La impureza se refiere a que, cuando hacemos un corte, cómo de probable es que una variable sea clasificada de forma incorrecta. Por ejemplo, digamos que aquellas personas con peso superior a 100kg son obesas. Seguro que habrá personas que, a pesar de pesar 100kg, no son obesas y personas que aun pesando menos de 100kg, son obesas. Así pues, siempre que “cortemos” una variable y la clasificación no sea perfecta, se trata de un corte impuro.

Sin embargo, esto no significa que todos los cortes sean iguales: seguro que el corte en los 100kg clasifica mejor que si el límite lo ponemos en 80kg. De hecho, lo podemos comprobar:

print(
  " Mal clasificados al cortar en 100kg:",
  data.loc[(data['Weight']>=100) & (data['obese']==0),:].shape[0], "\n",
  "Mal clasificados al cortar en 80kg:",
  data.loc[(data['Weight']>=80) & (data['obese']==0),:].shape[0]
)
 Mal clasificados al cortar en 100kg: 18 
 Mal clasificados al cortar en 80kg: 63

En definitiva, la función de coste de un árbol de decisión busca encontrar aquellos cortes que minimicen la impureza. Ahora, veámos qué formas existen para calcular la impureza:

Calcular la impureza mediante el índice de Gini

El índice de Gini es la función de coste más utilizada en los árboles de decisión. Este índice calcula la cantidad de probabilidad de que una característica específica se clasifique incorrectamente cuando se selecciona al azar.

Se trata de un índice cuyo rango va de 0 (un corte puro) a 0,5 (corte completamente puro que divide los datos en partes iguales). El indice de Gini se calcula de la siguiente manera:

\[ Gini = 1 – \sum^n_{i=1}(P_i)^2 \]

Donde Pi es la probabilidad de que un elemento se clasifique en el grupo no adecuado. Veámos cómo programar la función:

def gini_impurity(y):
  '''
  Given a Pandas Series, it calculates the Gini Impurity. 
  y: variable with which calculate Gini Impurity.
  '''
  if isinstance(y, pd.Series):
    p = y.value_counts()/y.shape[0]
    gini = 1-np.sum(p**2)
    return(gini)

  else:
    raise('Object must be a Pandas Series.')

gini_impurity(data.Gender) 
0.4998

Como vemos, el índice de Gini para la variable Género es muy cercano al 0,5. Esto indica que la variable Género es muy impura, es decir, que los resultados del corte no son buenos.

Ahora que ya sabes cómo funciona el índice, veámos cómo funciona la entropía.

Calcular la impureza con la entropía

Básicamente, es una forma de medir la impureza o aleatoriedad en los puntos de datos. Para medir la entropía se utiliza la probabilidad de una clase, como se puede ver a continuación:

\[ E(S) = \sum^c_{i=1}-p_ilog_2p_i \]

Como ves, se parece bastante al índice de Gini. Sin embargo, a diferencia del indice de Gini, cuyo rango va de 0 a 0,5 el rango de la entropía es diferente, ya que va de 0 a 1. De esta forma, los valores cercanos a cero son menos impuros que aquellos que se acercan al 1.

En cualquier caso, ambos miden lo mismo (aunque a diferente escala) por lo que las conclusiones que saquemos deben ser las mismas usemos la funcion que usemos. Para comprobarlo, vamosa calcular la entropia para el mismo ejemplo que hemos realizado con el indice de Gini:

def entropy(y):
  '''
  Given a Pandas Series, it calculates the entropy. 
  y: variable with which calculate entropy.
  '''
  if isinstance(y, pd.Series):
    a = y.value_counts()/y.shape[0]
    entropy = np.sum(-a*np.log2(a+1e-9))
    return(entropy)

  else:
    raise('Object must be a Pandas Series.')

entropy(data.Gender)  
0.9997114388674198

Como vemos, nos da un valor muy cercano al 1, lo cual denota una impureza similar a lo que indice la impureza de Gini, cuyo valor es cercano al 0,5.

Con esto ya conoces las dos principales métodos que se puede usar en un árbol de decisión para calcular la impureza. Perfecto, ya sabemos cómo decidir si un corte es bueno o no, pero… ¿entre que splits elegimos? ¡Veámoslo!

Cómo elegir los cortes para nuestro árbol de decisión

Como hemos visto, los cortes se comparan mediante la impureza. Por tanto, nos interesa comparar aquellos cortes que menor impureza generan. Para ello, se utiliza el Information Gain. Esta métrica indica la mejora al hacer diferentes particiones y se suele utilizar con la entropía (podría usarse también con el índice de Gini, aunque en ese caso no se llamaría Informaiton Gain).

El cálculo del Information Gain dependerá de si se trata de un árbol de decisión de clasificación o de regresión. Habría dos opciones:

\[ Information Gain_{Classification}= E(d) – \sum \frac{|s|}{|d|}E(s) \]

\[ Information Gain_{Regresion}= Variance(d) – \sum \frac{|s|}{|d|}Variance(s) \]

En nuestro caso el algoritmo podrá tomar cualquiera de los dos tipos de datos, por lo que incluimos ambos:

def variance(y):
  '''
  Function to help calculate the variance avoiding nan.
  y: variable to calculate variance to. It should be a Pandas Series.
  '''
  if(len(y) == 1):
    return 0
  else:
    return y.var()

def information_gain(y, mask, func=entropy):
  '''
  It returns the Information Gain of a variable given a loss function.
  y: target variable.
  mask: split choice.
  func: function to be used to calculate Information Gain in case os classification.
  '''
  
  a = sum(mask)
  b = mask.shape[0] - a
  
  if(a == 0 or b ==0): 
    ig = 0
  
  else:
    if y.dtypes != 'O':
      ig = variance(y) - (a/(a+b)* variance(y[mask])) - (b/(a+b)*variance(y[-mask]))
    else:
      ig = func(y)-a/(a+b)*func(y[mask])-b/(a+b)*func(y[-mask])
  
  return ig

Ahora, podemos calcular el information gain de un corte en concreto:

information_gain(data['obese'], data['Gender'] == 'Male')
0.0005506911187600494

Así pues, el procedimiento es sencillo:

  1. Calcular el Information Gain para todas las variables.
  2. Elegir como split aquel split que genere mayor Information Gain.
  3. Repetir el proceso hasta que el Information Gain no sea significativo o no supere un threshold.

Sin embargo, tenemos una nueva dificultad añadida, y es que ¿cómo elegimos cuál es el mejor split en las variables numéricas? ¿Y en caso de haber más de una variable categórica?

Cómo calcular el mejor split para una variable

Para calcular el mejor split de una variable numérica, lo primero se deben obtener todos posibles valores que está tomando la variable. Una vez tenemos las opciones, para cada opción calcularemos el Information Gain usando como filtro si el valor es inferior a ese valor.

En caso de que contemos con variables categóricas, la idea es la misma, solo que en este caso deberemos calcular el Information Gain para todas las combinaciones posibles de esa variable, excluyendo la opción que incluye a todas las opciones (ya que no estaría haciendo ningún split).

Así pues, una vez tengamos todos los splits, nos quedaremos con aquel split que genere un mayor Information Gain.

import itertools

def categorical_options(a):
  '''
  Creates all possible combinations from a Pandas Series.
  a: Pandas Series from where to get all possible combinations. 
  '''
  a = a.unique()

  opciones = []
  for L in range(0, len(a)+1):
      for subset in itertools.combinations(a, L):
          subset = list(subset)
          opciones.append(subset)

  return opciones[1:-1]

def max_information_gain_split(x, y, func=entropy):
  '''
  Given a predictor & target variable, returns the best split, the error and the type of variable based on a selected cost function.
  x: predictor variable as Pandas Series.
  y: target variable as Pandas Series.
  func: function to be used to calculate the best split.
  '''

  split_value = []
  ig = [] 

  numeric_variable = True if x.dtypes != 'O' else False

  # Create options according to variable type
  if numeric_variable:
    options = x.sort_values().unique()[1:]
  else: 
    options = categorical_options(x)

  # Calculate ig for all values
  for val in options:
    mask =   x < val if numeric_variable else x.isin(val)
    val_ig = information_gain(y, mask, func)
    # Append results
    ig.append(val_ig)
    split_value.append(val)

  # Check if there are more than 1 results if not, return False
  if len(ig) == 0:
    return(None,None,None, False)

  else:
  # Get results with highest IG
    best_ig = max(ig)
    best_ig_index = ig.index(best_ig)
    best_split = split_value[best_ig_index]
    return(best_ig,best_split,numeric_variable, True)


weight_ig, weight_slpit, _, _ = max_information_gain_split(data['Weight'], data['obese'],)  


print(
  "El mejor split para Weight es cuando la variable es inferior a ",
  weight_slpit,"\nEl Information Gain para ese corte es:", weight_ig
)
El mejor split para Weight es cuando la variable es inferior a  103 
El Information Gain para ese corte es: 0.3824541370911895

Ahora que ya sabemos calcular el split de una variable, veámos cómo decidir el mejor split.

Cómo elegir el mejor split

Como he comentado previamente, el mejor split será aquel que genere un Information Gain más alto. Para conocerlo, simplemente tendremos que calcular el Information Gain para cada una de las variables predictoras del modelo.

data.drop('obese', axis= 1).apply(max_information_gain_split, y = data['obese'])
        Gender     Height    Weight
0  0.000550691  0.0647483  0.382454
1       [Male]        174       103
2        False       True      True
3         True       True      True

Como vemos, la variable con mayor Information Gain es Weight. Por tanto, será la variable que utilicemos primero para hacer el split. Además, tenemos también el valor sobre el cual se debe realizar el split: 103.

Con esto, ya tendríamos el primer split, el cual generaría dos dataframes. Si aplicamos esto de manera recursiva, terminaremos creando el árbol de decisión completo (hecho en Python desde 0).

Cómo entrenar un árbol de decisión en Python desde 0

Definiendo la profundidad del árbol

Ya tenemos todos los ingredientes para calcular nuestro árbol de decisión. Ahora, deberemos crear una función que, dado un mask, nos haga un split.

Además, incluiremos los distintos hiperparámetros que ofrece un árbol de decisión. Aunque podríamos incluir más, los más relevantes son aquellos que evitan que el árbol crezca demasiado, evitando así el overfitting. Estos hiperparámetros son los siguientes:

  • max_depth: profundidad máxima del árbol. Si lo fijamos en None, el árbol crecerá hasta que todas las hojas sean puras o se haya alcanzado el hiperparámetro min_samples_split.
  • min_samples_split: indica el número mínimo de observaciones que debe tener una hoja para seguir creando nuevos nodos.
  • min_information_gain: la cantidad mínima que debe incrementar el Information Gain para que el árbol siga creciendo.

Teniendo esto en cuenta, vamos a terminar de crear nuestro árbol de decisión desde 0 en Python. Para ello, vamos a:

  1. Comprobar que se cumplen las condiciones fijadas por min_samples_split y max_depth.
  2. Hacer el split.
  3. Comprobar que se cumple el min_information_gain.
  4. Guardar los datos del split y repetir.

Para ello, lo primero de todo es crear tres funciones: una que, dado unos dados, devuelva el mejor split con su información correspondiente, otra, que dado unos datos y un split, haga el split y nos devuelva la predicción y por último, una función que dados unos datos, haga una predicción.

Nota: la predicción solo se dará en las ramas y básicamente consiste en devolver la media de los datos en caso de la regresión o la moda en caso de la clasificación.

def get_best_split(y, data):
  '''
  Given a data, select the best split and return the variable, the value, the variable type and the information gain.
  y: name of the target variable
  data: dataframe where to find the best split.
  '''
  masks = data.drop(y, axis= 1).apply(max_information_gain_split, y = data[y])
  if sum(masks.loc[3,:]) == 0:
    return(None, None, None, None)

  else:
    # Get only masks that can be splitted
    masks = masks.loc[:,masks.loc[3,:]]

    # Get the results for split with highest IG
    split_variable = max(masks)
    #split_valid = masks[split_variable][]
    split_value = masks[split_variable][1] 
    split_ig = masks[split_variable][0]
    split_numeric = masks[split_variable][2]

    return(split_variable, split_value, split_ig, split_numeric)


def make_split(variable, value, data, is_numeric):
  '''
  Given a data and a split conditions, do the split.
  variable: variable with which make the split.
  value: value of the variable to make the split.
  data: data to be splitted.
  is_numeric: boolean considering if the variable to be splitted is numeric or not.
  '''
  if is_numeric:
    data_1 = data[data[variable] < value]
    data_2 = data[(data[variable] < value) == False]

  else:
    data_1 = data[data[variable].isin(value)]
    data_2 = data[(data[variable].isin(value)) == False]

  return(data_1,data_2)

def make_prediction(data, target_factor):
  '''
  Given the target variable, make a prediction.
  data: pandas series for target variable
  target_factor: boolean considering if the variable is a factor or not
  '''

  # Make predictions
  if target_factor:
    pred = data.value_counts().idxmax()
  else:
    pred = data.mean()

  return pred

Entrenando nuestro árbol de decisión en Python

Ahora, que tenemos estas tres funciones, podemos, vamos a entrenar el árbol de decisión que acabamos de programar en Python.

  1. Comprobamos si se cumple tanto el min_samples_split como el max_depth.
  2. Si se cumple, buscamos el mejor split y obtenemos el Information Gain. Si no se cumple alguna de las dos condiciones, hacemos la predicción.
  3. Comprobamos que el Information Gain cumple con el min_information_gain.
  4. Si cumple la condición, hacemos el split y guardamos la decisión y, si no cumple la condición, hacemos la predicción, ya que se trata de una hoja.

Así pues este proceso lo haremos de forma recursiva, es decir, que la función se llamará a si misma. El resultado de la función serán las reglas que sigue para tomar la decisión:

def train_tree(data,y, target_factor, max_depth = None,min_samples_split = None, min_information_gain = 1e-20, counter=0, max_categories = 20):
  '''
  Trains a Decission Tree
  data: Data to be used to train the Decission Tree
  y: target variable column name
  target_factor: boolean to consider if target variable is factor or numeric.
  max_depth: maximum depth to stop splitting.
  min_samples_split: minimum number of observations to make a split.
  min_information_gain: minimum ig gain to consider a split to be valid.
  max_categories: maximum number of different values accepted for categorical values. High number of values will slow down learning process. R
  '''

  # Check that max_categories is fulfilled
  if counter==0:
    types = data.dtypes
    check_columns = types[types == "object"].index
    for column in check_columns:
      var_length = len(data[column].value_counts()) 
      if var_length > max_categories:
        raise ValueError('The variable ' + column + ' has '+ str(var_length) + ' unique values, which is more than the accepted ones: ' +  str(max_categories))

  # Check for depth conditions
  if max_depth == None:
    depth_cond = True

  else:
    if counter < max_depth:
      depth_cond = True

    else:
      depth_cond = False

  # Check for sample conditions
  if min_samples_split == None:
    sample_cond = True

  else:
    if data.shape[0] > min_samples_split:
      sample_cond = True

    else:
      sample_cond = False

  # Check for ig condition
  if depth_cond & sample_cond:

    var,val,ig,var_type = get_best_split(y, data)

    # If ig condition is fulfilled, make split 
    if ig is not None and ig >= min_information_gain:

      counter += 1

      left,right = make_split(var, val, data,var_type)

      # Instantiate sub-tree
      split_type = "<=" if var_type else "in"
      question =   "{} {}  {}".format(var,split_type,val)
      # question = "\n" + counter*" " + "|->" + var + " " + split_type + " " + str(val) 
      subtree = {question: []}


      # Find answers (recursion)
      yes_answer = train_tree(left,y, target_factor, max_depth,min_samples_split,min_information_gain, counter)

      no_answer = train_tree(right,y, target_factor, max_depth,min_samples_split,min_information_gain, counter)

      if yes_answer == no_answer:
        subtree = yes_answer

      else:
        subtree[question].append(yes_answer)
        subtree[question].append(no_answer)

    # If it doesn't match IG condition, make prediction
    else:
      pred = make_prediction(data[y],target_factor)
      return pred

   # Drop dataset if doesn't match depth or sample conditions
  else:
    pred = make_prediction(data[y],target_factor)
    return pred

  return subtree


max_depth = 5
min_samples_split = 20
min_information_gain  = 1e-5


decisiones = train_tree(data,'obese',True, max_depth,min_samples_split,min_information_gain)


decisiones
{'Weight <=  103': [{'Weight <=  66': [0, {'Weight <=  84': [{'Weight <=  74': [0, {'Weight <=  75': [1, 0]}]}, {'Weight <=  98': [1, 0]}]}]}, 1]}

¡Ya está! El árbol de decisión que acabamos de programar en Python ha creado todas las reglas que utilizará para hacer predicciones.

Ahora, solo quedaría una cosa: convertir esas reglas en acciones concretas que el algoritmo pueda utilizar para poder clasificar nuevos datos. ¡Vamos a ello!

Predecir mediante nuestro árbol de decisión en Python

Para hacer la predicción, vamos a tomar una observación y el árbol de decisión. Estas decisiones las podemos reconvertir en condiciones reales haciendo un split de las mismas.

Así pues, para hacer la predicción vamos a:

  1. Romper la decisión en varias partes.
  2. Comprobar el tipo de decisión que es (numérica o categórica).
  3. Comprobar si se cumple la condición de la decisión, teniendo en cuenta el tipo de variable que es. Si se cumple, devolver el resultado y, sino, seguir con la decisión.
def clasificar_datos(observacion, arbol):
  question = list(arbol.keys())[0] 


  if question.split()[1] == '<=':

    if observacion[question.split()[0]] <= float(question.split()[2]):
      answer = arbol[question][0]
    else:
      answer = arbol[question][1]

  else:

    if observacion[question.split()[0]] in (question.split()[2]):
      answer = arbol[question][0]
    else:
      answer = arbol[question][1]


  # Si la respuesta no es un diccionario
  if not isinstance(answer, dict):
    return answer
  else:
    residual_tree = answer
    return clasificar_datos(observacion, answer)

Así pues, podemos probar a clasificar todos los datos de nuestro algoritmo para comprobar cómo de bien ha funcionado nuestro el árbol de decisión que acabamos de programar en Python:

print(';Accuracy del algoritmo:';, accuracy)
Accuracy del algoritmo: 0.848

¡El árbol de decisión que acabamos de programar en Python tiene un accuracy de casi el 85%! Como vemos, parece que ha entrenado bien, aunque quizás los hiperparámetros que hemos elegidos no sean los mejores (ese es otro tema).

Por último, ya que hemos programado nuestro árbol de decisión en Python para que admita diferentes tipos de datos y que sirva tanto para regresión como para clasificación, vamos a ponerlo a prueba con distintos casos de uso:

Predicción con árbol de decisión para regresión

Para hacer la regresión, vamos a usar el dataset gapminder, el cual cuenta con la información del número de habitantes, PIB per Cápita de distintos países para diferentes años.

Así pues, vamos a usar el algoritmo para predecir la esperanza de vida de un país teniendo en cuenta su PIB per Cápita y su población:

from gapminder import gapminder

gapminder_lite = gapminder.loc[:,['lifeExp','pop','gdpPercap']]

gapminder_decission = train_tree(gapminder_lite,'lifeExp',True, max_depth=5)
gapminder_decission
{'pop <=  6927772': [{'pop <=  2728150': [{'pop <=  1138101': [{'pop <=  551425': [{'pop <=  305991': [50.93899999999999, 61.448]}, {'pop <=  841934': [61.556999999999995, 60.396]}]}, {'pop <=  1865490': [{'pop <=  1489518': [53.754, 59.448]}, {'pop <=  2312802': [69.39, 61.271]}]}]}, {'pop <=  4318137': [{'pop <=  3495918': [{'pop <=  3080828': [74.712, 73.44]}, {'pop <=  3882229': [67.45, 39.486999999999995]}]}, {'pop <=  5283663': [{'pop <=  4730997': [56.604, 79.313]}, {'pop <=  6053193': [45.552, 48.437]}]}]}]}, {'pop <=  19314747': [{'pop <=  10191512': [{'pop <=  8519282': [{'pop <=  7661799': [23.599, 69.51]}, {'pop <=  9354120': [48.968999999999994, 73.042]}]}, {'pop <=  13329874': [{'pop <=  11000948': [69.58, 48.303000000000004]}, {'pop <=  16252726': [55.448, 66.8]}]}]}, {'pop <=  43997828': [{'pop <=  28227588': [{'pop <=  22662365': [72.396, 62.677]}, {'pop <=  34621254': [51.542, 37.802]}]}, {'pop <=  76511887': [{'pop <=  56667095': [72.76, 68.564]}, {'pop <=  135031164': [78.67, 64.062]}]}]}]}]}

Asimismo, podemos aprovechar este mismo dataset de Gapminder para comprobar como, si pasamos una variable categórica con más niveles que lo fijado por el parámetro max_categories, nos devolverá error:

train_tree(gapminder,'lifeExp',True, max_depth=5)
ValueError: The variable country has 142 unique values, which is more than the accepted ones: 20

Así pues, podemos calcular la estimación de nuestro algoritmo:

for i in range(num_obs):
  obs_pred = clasificar_datos(gapminder_lite.iloc[i,:], gapminder_decission)
  gapm_prediction.append(obs_pred)

print(';Predicciones: ';,gapm_prediction,
';\n\nValores reales:';, gapminder_lite.lifeExp[:num_obs].to_numpy())
Predicciones:  [69.51, 48.968999999999994, 69.58, 48.303000000000004, 48.303000000000004, 55.448, 48.303000000000004, 55.448, 66.8, 72.396, 62.677, 51.542, 53.754, 53.754, 59.448, 69.39, 69.39, 61.271, 74.712, 74.712, 69.51, 48.968999999999994, 69.58, 48.303000000000004, 48.303000000000004, 55.448, 48.303000000000004, 55.448, 66.8, 72.396, 62.677, 51.542, 53.754, 53.754, 59.448, 69.39, 69.39, 61.271, 74.712, 74.712] 

Valores reales: [28.801 30.332 31.997 34.02  36.088 38.438 39.854 40.822 41.674 41.763
 42.129 43.828 55.23  59.28  64.82  66.22  67.69  68.93  70.42  72.   ]

Asimismo, también podríamos utilizar cualquier tipo de variable categórica para hacer nuestras predicciones:

gapminder_cat = gapminder.loc[:,['lifeExp','continent']]

gapminder_cat_decission = train_tree(gapminder_cat,'lifeExp',False, max_depth=5)
gapminder_cat_decission
{"continent in  ['Africa', 'Americas']": [{"continent in  ['Americas']": [64.65873666666667, 48.86533012820513]}, {"continent in  ['Asia']": [60.064903232323225, {"continent in  ['Oceania']": [74.32620833333333, 71.9036861111111]}]}]}

Cómo no, también podríamos usar la misma función de predicción para hacer predicciones con árboles que usan variables categóricas. Aunque, como es de esperar, en este caso fallarán bastante debido a la sobresimplificación de la variable predictora:

gapm_cat_prediction = []
num_obs = 20

for i in range(num_obs):
  obs_pred = clasificar_datos(gapminder_cat.iloc[i,:], gapminder_cat_decission)
  gapm_cat_prediction.append(obs_pred)

print("Predicciones: ",gapm_cat_prediction,
"\n\nValores reales:", gapminder_cat.lifeExp[:num_obs].to_numpy())
Predicciones:  [60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 60.064903232323225, 71.9036861111111, 71.9036861111111, 71.9036861111111, 71.9036861111111, 71.9036861111111, 71.9036861111111, 71.9036861111111, 71.9036861111111] 

Valores reales: [28.801 30.332 31.997 34.02  36.088 38.438 39.854 40.822 41.674 41.763
 42.129 43.828 55.23  59.28  64.82  66.22  67.69  68.93  70.42  72.   ]

Asimismo, también podemos usar el árbol de decisión que hemos programado en Python desde cero para problemas de clasificación.

Predicción con árbol de decisión para clasificación

Para probar nuestro árbol de decisión con un problema de clasificación, vamos a usar el dataset típico de Titanic, el cual se puede descargar desde aquí.

En nuestro caso, no buscamos conseguir los mejores resultados, sino demostrar cómo el árbol de decisión que hemos programado en Python desde cero funciona. Por eso, simplemente me quedaré con unas columnas y todas aquellas observaciones que tengan los datos completos:

titanic_tree
{'Fare <=  52.5542': [{'Fare <=  10.5': [{'Fare <=  7.1417': [0, {'Fare <=  7.225': [1, {'Fare <=  9.8458': [{'Fare <=  9.8417': [{'Fare <=  9.825': [{'Fare <=  8.05': [{'Fare <=  7.925': [0, {'Fare <=  8.0292': [0, 1]}]}, 0]}, 0]}, 1]}, 0]}]}]}, {'Fare <=  39.6875': [{'Fare <=  39.0': [{'Fare <=  27.9': [{'Fare <=  18.75': [{'Fare <=  17.8': [{'Fare <=  16.7': [{'Fare <=  16.1': [{'Fare <=  15.7417': [0, 1]}, 0]}, 1]}, 0]}, {'Fare <=  20.2125': [1, {'Fare <=  25.9292': [{'Fare <=  24.15': [{'Fare <=  23.0': [0, 1]}, 0]}, 1]}]}]}, {'Fare <=  30.0': [0, {'Fare <=  31.275': [{'Fare <=  31.0': [{'Fare <=  30.6958': [1, 0]}, 1]}, {'Fare <=  31.3875': [0, {'Fare <=  33.5': [1, {'Fare <=  35.5': [0, 1]}]}]}]}]}]}, 1]}, {'Fare <=  49.5': [0, {'Fare <=  49.5042': [1, 0]}]}]}]}, {'Fare <=  83.1583': [{'Fare <=  61.175': [1, {'Fare <=  63.3583': [0, {'Fare <=  66.6': [1, {'Fare <=  82.1708': [{'Fare <=  75.25': [{'Fare <=  73.5': [1, 0]}, {'Fare <=  77.2875': [1, {'Fare <=  77.9583': [0, 1]}]}]}, 0]}]}]}]}, 1]}]}

Una vez más, podemos usar estos mismos datos para hacer la predicción:

titanic_prediction = []
num_obs = 20

for i in range(num_obs):
  obs_pred = clasificar_datos(titcanic_lite.iloc[i,:], titanic_tree)
  titanic_prediction.append(obs_pred)

print("Predicciones: ",titanic_prediction,
"\n\nValores reales:", titcanic_lite.Survived[:num_obs].to_numpy())
Predicciones:  [0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1] 

Valores reales: [0 1 1 1 0 0 0 1 1 1 1 0 0 0 1 0 0 0 1 1]

Conclusiones

Personalmente creo que la mejor forma de conocer un algoritmo es programarlo desde cero. En este caso hemos programado un árbol de decisión desde cero en Python y, sin duda, sirve para conocer cómo funciona el algoritmo, los tipos de funciones de coste que existen y cómo se hace la predicción.

Si te ha resultado interesante, te recomiendo veas el resto de posts en los que programo otros algoritmos desde cero, incluyendo, una red neuronal en Python y R, el algoritmo K-means en Python y R, una GAN en Python o la regresión lineal.

Si te gustaría mantenerte al tanto de los posts que publico, te animo a suscribirte a mi newsletter. En cualquier caso, ¡nos vemos en la próxima!

\(\)