0911pexo6qiwmncqs.jpeg

Debido a que utilizamos un algoritmo de aprendizaje no supervisado, no existe una medida de precisión ampliamente disponible. Sin embargo, podemos utilizar el conocimiento del dominio para validar nuestros grupos.

Al inspeccionar visualmente los grupos, podemos ver que algunos grupos de evaluación comparativa tienen una combinación de hoteles económicos y de lujo, lo que no tiene sentido comercial ya que la demanda de hoteles es fundamentalmente diferente.

Podemos desplazarnos hasta los datos y notar algunas de esas diferencias, pero ¿podemos idear nuestra propia medida de precisión?

Queremos crear una función para medir la coherencia de los conjuntos de evaluaciones comparativas recomendados en cada característica. Una forma de hacerlo es calculando la varianza de cada característica para cada conjunto. Para cada grupo, podemos calcular un promedio de la varianza de cada característica y luego podemos promediar la varianza de cada grupo de hoteles para obtener una puntuación total del modelo.

A partir de nuestro conocimiento del dominio, sabemos que para establecer un conjunto de puntos de referencia comparable, debemos priorizar hoteles de la misma marca, posiblemente del mismo mercado y del mismo país, y si utilizamos diferentes mercados o países, entonces el mercado El nivel debería ser el mismo.

Con eso en mente, queremos que nuestra medida tenga una penalización más alta por la variación en esas características. Para hacerlo, utilizaremos un promedio ponderado para calcular la varianza de cada conjunto de referencia. También imprimiremos la variación de las características clave y secundarias por separado.

En resumen, para crear nuestra medida de precisión, necesitamos:

  1. Calcular la varianza de variables categóricas.: Un enfoque común es utilizar una medida “basada en la entropía”, donde una mayor diversidad en las categorías indica una mayor entropía (varianza).
  2. Calcular la varianza de variables numéricas.: podemos calcular la desviación estándar o el rango (diferencia entre valores máximo y mínimo). Esto mide la difusión de datos numéricos dentro de cada grupo.
  3. Normalizar los datos: normalice las puntuaciones de varianza para cada categoría antes de aplicar ponderaciones para garantizar que ninguna característica domine el promedio ponderado debido únicamente a las diferencias de escala.
  4. Aplicar ponderaciones para diferentes métricas: pondere cada tipo de variación según su importancia para la lógica de agrupación.
  5. Calcular promedios ponderados: Calcule el promedio ponderado de estas puntuaciones de varianza para cada grupo.
  6. Agregar puntuaciones entre grupos: La puntuación total es el promedio de estas puntuaciones de varianza ponderadas en todos los grupos o filas. Una puntuación promedio más baja indicaría que nuestro modelo agrupa efectivamente hoteles similares, minimizando la variación dentro del grupo.
from scipy.stats import entropy
from sklearn.preprocessing import MinMaxScaler
from collections import Counter

def categorical_variance(data):
"""
Calculate entropy for a categorical variable from a list.
A higher entropy value indicates datas with diverse classes.
A lower entropy value indicates a more homogeneous subset of data.
"""
# Count frequency of each unique value
value_counts = Counter(data)
total_count = sum(value_counts.values())
probabilities = [count / total_count for count in value_counts.values()]
return entropy(probabilities)

#set scoring weights giving higher weights to the most important features
scoring_weights = {"BRAND": 0.3,
"Room_count": 0.025,
"Market": 0.25,
"Country": 0.15,
"Market Tier": 0.15,
"HCLASS": 0.05,
"Demand": 0.025,
"Price range": 0.025,
"distance_to_airport": 0.025}

def calculate_weighted_variance(df, weights):
"""
Calculate the weighted variance score for clusters in the dataset
"""
# Initialize a DataFrame to store the variances
variance_df = pd.DataFrame()

# 1. Calculate variances for numerical features
numerical_features = ['Room_count', 'Demand', 'Price range', 'distance_to_airport']
for feature in numerical_features:
variance_df[f'{feature}'] = df[feature].apply(np.var)

# 2. Calculate entropy for categorical features
categorical_features = ['BRAND', 'Market','Country','Market Tier','HCLASS']
for feature in categorical_features:
variance_df[f'{feature}'] = df[feature].apply(categorical_variance)

# 3. Normalize the variance and entropy values
scaler = MinMaxScaler()
normalized_variances = pd.DataFrame(scaler.fit_transform(variance_df),
columns=variance_df.columns,
index=variance_df.index)

# 4. Compute weighted average

cat_weights = {f'{feature}': weights[f'{feature}'] for feature in categorical_features}
num_weights = {f'{feature}': weights[f'{feature}'] for feature in numerical_features}

cat_weighted_scores = normalized_variances[categorical_features].mul(cat_weights)
df['cat_weighted_variance_score'] = cat_weighted_scores.sum(axis=1)

num_weighted_scores = normalized_variances[numerical_features].mul(num_weights)
df['num_weighted_variance_score'] = num_weighted_scores.sum(axis=1)

return df['cat_weighted_variance_score'].mean(), df['num_weighted_variance_score'].mean()

Para mantener nuestro código limpio y realizar un seguimiento de nuestros experimentos, definamos también una función para almacenar los resultados de nuestros experimentos.

# define a function to store the results of our experiments
def model_score(data: pd.DataFrame,
weights: dict = scoring_weights,
model_name: str ="model_0"):
cat_score,num_score = calculate_weighted_variance(data,weights)
results ={"Model": model_name,
"Primary features score": cat_score,
"Secondary features score": num_score}
return results

model_0_score= model_score(results_model_0,scoring_weights)
model_0_score

Resultados del modelo de referencia.

Ahora que tenemos una línea de base, veamos si podemos mejorar nuestro modelo.

Mejorando nuestro modelo mediante la experimentación

Hasta ahora, no teníamos que saber qué estaba pasando bajo el capó cuando ejecutamos este código:

nns = NearestNeighbors()
nns.fit(data_scaled)
nns_results_model_0 = nns.kneighbors(data_scaled)[1]

Para mejorar nuestro modelo, necesitaremos comprender los parámetros del modelo y cómo podemos interactuar con ellos para obtener mejores conjuntos de referencia.

Comencemos mirando la documentación y el código fuente de Scikit Learn:

# the below is taken directly from scikit learn source

from sklearn.neighbors._base import KNeighborsMixin, NeighborsBase, RadiusNeighborsMixin

class NearestNeighbors_(KNeighborsMixin, RadiusNeighborsMixin, NeighborsBase):
"""Unsupervised learner for implementing neighbor searches.
Parameters
----------
n_neighbors : int, default=5
Number of neighbors to use by default for :meth:`kneighbors` queries.

radius : float, default=1.0
Range of parameter space to use by default for :meth:`radius_neighbors`
queries.

algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'
Algorithm used to compute the nearest neighbors:

- 'ball_tree' will use :class:`BallTree`
- 'kd_tree' will use :class:`KDTree`
- 'brute' will use a brute-force search.
- 'auto' will attempt to decide the most appropriate algorithm
based on the values passed to :meth:`fit` method.

Note: fitting on sparse input will override the setting of
this parameter, using brute force.

leaf_size : int, default=30
Leaf size passed to BallTree or KDTree. This can affect the
speed of the construction and query, as well as the memory
required to store the tree. The optimal value depends on the
nature of the problem.

metric : str or callable, default='minkowski'
Metric to use for distance computation. Default is "minkowski", which
results in the standard Euclidean distance when p = 2. See the
documentation of `scipy.spatial.distance
<https://docs.scipy.org/doc/scipy/reference/spatial.distance.html>`_ and
the metrics listed in
:class:`~sklearn.metrics.pairwise.distance_metrics` for valid metric
values.

p : float (positive), default=2
Parameter for the Minkowski metric from
sklearn.metrics.pairwise.pairwise_distances. When p = 1, this is
equivalent to using manhattan_distance (l1), and euclidean_distance
(l2) for p = 2. For arbitrary p, minkowski_distance (l_p) is used.

metric_params : dict, default=None
Additional keyword arguments for the metric function.
"""

def __init__(
self,
*,
n_neighbors=5,
radius=1.0,
algorithm="auto",
leaf_size=30,
metric="minkowski",
p=2,
metric_params=None,
n_jobs=None,
):
super().__init__(
n_neighbors=n_neighbors,
radius=radius,
algorithm=algorithm,
leaf_size=leaf_size,
metric=metric,
p=p,
metric_params=metric_params,
n_jobs=n_jobs,
)

Están sucediendo bastantes cosas aquí.

El Nearestneighbor la clase hereda deNeighborsBase, que es la clase de caso para los estimadores vecinos más cercanos. Esta clase maneja las funcionalidades comunes requeridas para las búsquedas de vecinos más cercanos, como

  • n_neighbors (el número de vecinos a utilizar)
  • radio (el radio para búsquedas de vecinos basadas en radio)
  • algoritmo (el algoritmo utilizado para calcular los vecinos más cercanos, como ‘ball_tree’, ‘kd_tree’ o ‘brute’)
  • métrica (la métrica de distancia a utilizar)
  • metric_params (argumentos de palabras clave adicionales para la función métrica)

El Nearestneighbor La clase también hereda deKNeighborsMixin y RadiusNeighborsMixinclases. Estas clases Mixin agregan funcionalidades específicas de búsqueda de vecinos al Nearestneighbor

  • KNeighborsMixin proporciona funcionalidad para encontrar el número fijo k de vecinos más cercano a un punto. Lo hace encontrando la distancia a los vecinos y sus índices y construyendo una gráfica de conexiones entre puntos basada en los k vecinos más cercanos de cada punto.
  • RadiusNeighborsMixin se basa en el algoritmo de vecinos de radio, que encuentra todos los vecinos dentro de un radio determinado de un punto. Este método es útil en escenarios donde la atención se centra en capturar todos los puntos dentro de un umbral de distancia significativo en lugar de un número fijo de puntos.

Según nuestro escenario, KNeighborsMixin proporciona la funcionalidad que necesitamos.

Necesitamos comprender un parámetro clave antes de poder mejorar nuestro modelo; esta es la métrica de distancia.

La documentación menciona que el algoritmo NearestNeighbor utiliza la distancia «Minkowski» de forma predeterminada y nos da una referencia a la API de SciPy.

En scipy.spatial.distancepodemos ver dos representaciones matemáticas de la distancia «Minkowski»:

∥u−v∥ p​=( i ∑​∣ui​−vi​∣ p ) 1/p

Esta fórmula calcula la raíz p-ésima de la suma de las diferencias potenciadas entre todos los elementos.

La segunda representación matemática de la distancia «Minkowski» es:

∥u−v∥ p​=( i ∑​wi​(∣ui​−vi​∣ p )) 1/p

Esto es muy similar al primero, pero introduce pesos. wi a las diferencias, enfatizando o restando importancia a dimensiones específicas. Esto resulta útil cuando determinadas características son más relevantes que otras. De forma predeterminada, la configuración es Ninguna, lo que otorga a todas las funciones el mismo peso de 1,0.

Esta es una gran opción para mejorar nuestro modelo, ya que nos permite transmitir conocimiento del dominio a nuestro modelo y enfatizar las similitudes que son más relevantes para los usuarios.

Si miramos las fórmulas, vemos el parámetro. p. Este parámetro afecta la «ruta» que toma el algoritmo para calcular la distancia. Por defecto, p=2, que representa la distancia euclidiana.

Puedes pensar en la distancia euclidiana como calcular la distancia dibujando una línea recta entre 2 puntos. Esta suele ser la distancia más corta, sin embargo, no siempre es la forma más deseable de calcular la distancia, especialmente en espacios de mayores dimensiones. Para obtener más información sobre por qué ocurre esto, existe este excelente artículo en línea: https://bib.dbvis.de/uploadedFiles/155.pdf

Otro valor común para p es 1. Esto representa la distancia de Manhattan. Se piensa en ello como la distancia entre dos puntos medidos a lo largo de un camino similar a una cuadrícula.

Por otro lado, si aumentamos p hacia el infinito, terminamos con la distancia de Chebyshev, definida como la diferencia absoluta máxima entre cualquier elemento correspondiente de los vectores.. Básicamente, mide la diferencia en el peor de los casos, lo que lo hace útil en escenarios en los que desea asegurarse de que ninguna característica varíe demasiado.

Al leer y familiarizarnos con la documentación, hemos descubierto algunas opciones posibles para mejorar nuestro modelo.

De forma predeterminada, n_neighbors es 5; sin embargo, para nuestro conjunto de puntos de referencia, queremos comparar cada hotel con los 3 hoteles más similares. Para hacerlo, necesitamos establecer n_vecinos = 4 (hotel en cuestión + 3 pares)

nns_1= NearestNeighbors(n_neighbors=4)
nns_1.fit(data_scaled)
nns_1_results_model_1 = nns_1.kneighbors(data_scaled)[1]
results_model_1 = clean_results(nns_results=nns_1_results_model_1,
encoders=encoders,
data=data_clean)
model_1_score= model_score(results_model_1,scoring_weights,model_name="baseline_k_4")
model_1_score
Ligera mejora en nuestras funciones principales. Imagen del autor

Según la documentación, podemos pasar ponderaciones al cálculo de la distancia para enfatizar la relación entre algunas características. Con base en nuestro conocimiento del dominio, hemos identificado las características que queremos enfatizar, en este caso, Marca, Mercado, País y Nivel de Mercado.

# set up weights for distance calculation
weights_dict = {"BRAND": 5,
"Room_count": 2,
"Market": 4,
"Country": 3,
"Market Tier": 3,
"HCLASS": 1.5,
"Demand": 1,
"Price range": 1,
"distance_to_airport": 1}
# Transform the wieghts dictionnary into a list by keeping the scaled data column order
weights = [ weights_dict[idx] for idx in list(scaler.get_feature_names_out())]

nns_2= NearestNeighbors(n_neighbors=4,metric_params={ 'w': weights})
nns_2.fit(data_scaled)
nns_2_results_model_2 = nns_2.kneighbors(data_scaled)[1]
results_model_2 = clean_results(nns_results=nns_2_results_model_2,
encoders=encoders,
data=data_clean)
model_2_score= model_score(results_model_2,scoring_weights,model_name="baseline_with_weights")
model_2_score

La puntuación de las características principales sigue mejorando. Imagen del autor

Pasar el conocimiento del dominio al modelo mediante ponderaciones aumentó significativamente la puntuación. A continuación, probemos el impacto de la medida de distancia.

Hasta ahora hemos estado utilizando la distancia euclidiana. Veamos qué pasa si usamos la distancia de Manhattan en su lugar.

nns_3= NearestNeighbors(n_neighbors=4,p=1,metric_params={ 'w': weights})
nns_3.fit(data_scaled)
nns_3_results_model_3 = nns_3.kneighbors(data_scaled)[1]
results_model_3 = clean_results(nns_results=nns_3_results_model_3,
encoders=encoders,
data=data_clean)
model_3_score= model_score(results_model_3,scoring_weights,model_name="Manhattan_with_weights")
model_3_score
Disminución significativa en la puntuación primaria. imagen por autor

Disminuir p a 1 resultó en algunas buenas mejoras. Veamos qué sucede cuando p se aproxima al infinito.

Para usar la distancia de Chebyshev, cambiaremos el parámetro métrico a Chebyshev. La métrica predeterminada de sklearn Chebyshev no tiene un parámetro de peso. Para evitar esto, definiremos una costumbre weighted_chebyshev métrico.

#  Define the custom weighted Chebyshev distance function
def weighted_chebyshev(u, v, w):
"""Calculate the weighted Chebyshev distance between two points."""
return np.max(w * np.abs(u - v))

nns_4 = NearestNeighbors(n_neighbors=4,metric=weighted_chebyshev,metric_params={ 'w': weights})
nns_4.fit(data_scaled)
nns_4_results_model_4 = nns_4.kneighbors(data_scaled)[1]
results_model_4 = clean_results(nns_results=nns_4_results_model_4,
encoders=encoders,
data=data_clean)
model_4_score= model_score(results_model_4,scoring_weights,model_name="Chebyshev_with_weights")
model_4_score

Mejor que la línea de base pero más alta que el experimento anterior. Imagen del autor

Logramos disminuir las puntuaciones de variación de las características principales mediante la experimentación.

Visualicemos los resultados.

results_df = pd.DataFrame([model_0_score,model_1_score,model_2_score,model_3_score,model_4_score]).set_index("Model")
results_df.plot(kind='barh')
Resultados de la experimentación. Imagen del autor

El uso de la distancia de Manhattan con ponderaciones parece brindar los conjuntos de referencia más precisos según nuestras necesidades.

El último paso antes de implementar los conjuntos de referencia sería examinar los conjuntos con las puntuaciones más altas de características primarias e identificar qué pasos tomar con ellos.

# Histogram of Primary features score
results_model_3["cat_weighted_variance_score"].plot(kind="hist")
Distribución de puntuación. Imagen del autor
exceptions = results_model_3[results_model_3["cat_weighted_variance_score"]>=0.4]

print(f" There are {exceptions.shape[0]} benchmark sets with significant variance across the primary features")

Imagen del autor

Será necesario revisar estos 18 casos para garantizar que los conjuntos de puntos de referencia sean relevantes.

Como puede ver, con unas pocas líneas de código y cierta comprensión de la búsqueda de vecino más cercano, logramos establecer conjuntos de puntos de referencia internos. Ahora podemos distribuir los conjuntos y comenzar a medir los KPI de los hoteles con respecto a sus conjuntos de referencia.

No siempre es necesario centrarse en los métodos de aprendizaje automático más avanzados para ofrecer valor. Muy a menudo, el simple aprendizaje automático puede ofrecer un gran valor.

¿Cuáles son algunos de los frutos más fáciles de alcanzar en su negocio que podría abordar fácilmente con el aprendizaje automático?

Banco Mundial. «Indicadores de Desarrollo Mundial.» Recuperado el 11 de junio de 2024 de https://datacatalog.worldbank.org/search/dataset/0038117

Aggarwal, CC, Hinneburg, A. y Keim, DA (sin fecha). Sobre el sorprendente comportamiento de las métricas de distancia en un espacio de alta dimensión. Centro de investigación IBM TJ Watson e Instituto de Ciencias de la Computación, Universidad de Halle. Obtenido de https://bib.dbvis.de/uploadedFiles/155.pdf

Manual de SciPy v1.10.1. scipy.spatial.distance.minkowski. Recuperado el 11 de junio de 2024 de https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.minkowski.html

Frikis para frikis. Fórmula de Haversine para encontrar la distancia entre dos puntos de una esfera. Recuperado el 11 de junio de 2024 de https://www.geeksforgeeks.org/haversine-formula-to-find-distance-between-two-points-on-a-sphere/

scikit-aprende. Módulo de Vecinos. Recuperado el 11 de junio de 2024 de https://scikit-learn.org/stable/modules/classes.html#module-sklearn.neighbors