Poiché utilizziamo un algoritmo di apprendimento non supervisionato, non esiste una misura di precisione ampiamente disponibile. Tuttavia, possiamo utilizzare la conoscenza del dominio per convalidare i nostri gruppi.
Esaminando visivamente i gruppi, possiamo vedere che alcuni gruppi di benchmarking hanno un mix di hotel Economy e Luxury, il che non ha senso dal punto di vista commerciale poiché la domanda di hotel è fondamentalmente diversa.
Possiamo scorrere i dati e notare alcune di queste differenze, ma possiamo elaborare la nostra misura di precisione?
Vogliamo creare una funzione per misurare la coerenza dei set di benchmarking consigliati per ciascuna funzionalità. Un modo per farlo è calcolare la varianza in ciascuna caratteristica per ciascun insieme. Per ciascun cluster, possiamo calcolare una media della varianza di ciascuna caratteristica e quindi possiamo calcolare la media della varianza di ciascun cluster di hotel per ottenere un punteggio totale del modello.
Dalla nostra conoscenza del settore, sappiamo che per creare un set di parametri di riferimento comparabili, dobbiamo dare la priorità agli hotel dello stesso marchio, possibilmente dello stesso mercato e dello stesso paese, e se utilizziamo mercati o paesi diversi, allora il mercato il livello dovrebbe essere lo stesso.
Tenendo questo in mente, vogliamo che la nostra misura abbia una penalità più elevata per la varianza di tali caratteristiche. Per fare ciò, utilizzeremo una media ponderata per calcolare la varianza di ciascun set di benchmark. Stamperemo anche separatamente la variazione delle caratteristiche principali e delle caratteristiche secondarie.
Per riassumere, per creare la nostra misura di accuratezza, dobbiamo:
- Calcolare la varianza per le variabili categoriali: Un approccio comune consiste nell’utilizzare una misura “basata sull’entropia”, in cui una maggiore diversità nelle categorie indica una maggiore entropia (varianza).
- Calcolare la varianza per variabili numeriche: possiamo calcolare la deviazione standard o il range (differenza tra i valori massimo e minimo). Questo misura la diffusione dei dati numerici all'interno di ciascun cluster.
- Normalizza i dati: normalizzare i punteggi di varianza per ciascuna categoria prima di applicare i pesi per garantire che nessuna singola caratteristica domini la media ponderata a causa delle sole differenze di scala.
- Applicare pesi per metriche diverse: pondera ciascun tipo di varianza in base alla sua importanza per la logica di clustering.
- Calcolo delle medie ponderate: Calcola la media ponderata di questi punteggi di varianza per ciascun cluster.
- Aggregazione dei punteggi tra i cluster: il punteggio totale è la media di questi punteggi di varianza ponderata in tutti i cluster o righe. Un punteggio medio più basso indicherebbe che il nostro modello raggruppa effettivamente hotel simili, riducendo al minimo la varianza all’interno del cluster.
from scipy.stats import entropy
from sklearn.preprocessing import MinMaxScaler
from collections import Counterdef 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()
Per mantenere il nostro codice pulito e tenere traccia dei nostri esperimenti, definiamo anche una funzione per memorizzare i risultati dei nostri esperimenti.
# 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 resultsmodel_0_score= model_score(results_model_0,scoring_weights)
model_0_score
Ora che abbiamo una linea di base, vediamo se possiamo migliorare il nostro modello.
Migliorare il nostro modello attraverso la sperimentazione
Fino ad ora, non dovevamo sapere cosa stava succedendo sotto il cofano quando eseguivamo questo codice:
nns = NearestNeighbors()
nns.fit(data_scaled)
nns_results_model_0 = nns.kneighbors(data_scaled)(1)
Per migliorare il nostro modello, dovremo comprendere i parametri del modello e come possiamo interagire con essi per ottenere set di benchmark migliori.
Iniziamo esaminando la documentazione e il codice sorgente di Scikit Learn:
# the below is taken directly from scikit learn sourcefrom 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,
)
Ci sono un bel po' di cose che succedono qui.
IL Nearestneighbor
la classe eredita daNeighborsBase
che è la classe case per gli stimatori del vicino più vicino. Questa classe gestisce le funzionalità comuni richieste per le ricerche del vicino più vicino, come ad esempio
- n_neighbors (il numero di vicini da utilizzare)
- raggio (il raggio per le ricerche dei vicini basate sul raggio)
- algoritmo (l'algoritmo utilizzato per calcolare i vicini più vicini, come “ball_tree”, “kd_tree” o “brute”)
- metrica (la metrica della distanza da utilizzare)
- metric_params (argomenti chiave aggiuntivi per la funzione metrica)
IL Nearestneighbor
anche la classe eredita daKNeighborsMixin
E RadiusNeighborsMixin
classi. Queste classi Mixin aggiungono funzionalità specifiche di ricerca dei vicini al file Nearestneighbor
KNeighborsMixin
fornisce funzionalità per trovare il numero fisso k di vicini più vicino a un punto. Lo fa trovando la distanza dai vicini e i loro indici e costruendo un grafico di connessioni tra punti basato sui k vicini più vicini di ciascun punto.RadiusNeighborsMixin
si basa sull'algoritmo dei vicini del raggio, che trova tutti i vicini entro un dato raggio di un punto. Questo metodo è utile negli scenari in cui l'attenzione è rivolta all'acquisizione di tutti i punti entro una soglia di distanza significativa anziché a un numero fisso di punti.
In base al nostro scenario, KNeighborsMixin fornisce le funzionalità di cui abbiamo bisogno.
Dobbiamo comprendere un parametro chiave prima di poter migliorare il nostro modello; questa è la metrica della distanza.
La documentazione menziona che l'algoritmo NearestNeighbor utilizza la distanza “Minkowski” per impostazione predefinita e ci fornisce un riferimento all'API SciPy.
In scipy.spatial.distance
possiamo vedere due rappresentazioni matematiche della distanza “Minkowski”:
∥u−v∥ p=( i ∑∣ui−vi∣ p ) 1/p
Questa formula calcola la radice p-esima della somma delle differenze potenziate tra tutti gli elementi.
La seconda rappresentazione matematica della distanza “Minkowski” è:
∥u−v∥ p=( i ∑wi(∣ui−vi∣ p )) 1/p
Questo è molto simile al primo, ma introduce i pesi wi
alle differenze, enfatizzando o de-enfatizzando dimensioni specifiche. Ciò è utile quando alcune funzionalità sono più rilevanti di altre. Per impostazione predefinita, l'impostazione è Nessuna, che attribuisce a tutte le funzionalità lo stesso peso pari a 1,0.
Questa è un'ottima opzione per migliorare il nostro modello in quanto ci consente di trasferire la conoscenza del dominio al nostro modello ed enfatizzare le somiglianze più rilevanti per gli utenti.
Se guardiamo le formule, vediamo il parametro. p
. Questo parametro influisce sul “percorso” seguito dall'algoritmo per calcolare la distanza. Per impostazione predefinita, p=2, che rappresenta la distanza euclidea.
Puoi pensare alla distanza euclidea come al calcolo della distanza tracciando una linea retta tra 2 punti. Di solito si tratta della distanza più breve, tuttavia non è sempre il modo migliore per calcolare la distanza, specialmente negli spazi di dimensioni maggiori. Per ulteriori informazioni sul motivo per cui questo è il caso, c'è questo ottimo articolo online: https://bib.dbvis.de/uploadedFiles/155.pdf
Un altro valore comune per p è 1. Questo rappresenta la distanza di Manhattan. Puoi pensarla come la distanza tra due punti misurata lungo un percorso simile a una griglia.
D'altra parte, se aumentiamo p verso l'infinito, otteniamo la distanza di Chebyshev, definita come la massima differenza assoluta tra eventuali elementi corrispondenti dei vettori. Misura essenzialmente la differenza nel caso peggiore, rendendolo utile negli scenari in cui si desidera garantire che nessuna singola funzionalità vari troppo.
Leggendo e familiarizzando con la documentazione, abbiamo scoperto alcune possibili opzioni per migliorare il nostro modello.
Per impostazione predefinita n_neighbors è 5, tuttavia, per il nostro set di benchmark, vogliamo confrontare ciascun hotel con i 3 hotel più simili. Per fare ciò dobbiamo impostare n_neighbors = 4 (Soggetto hotel + 3 peer)
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
In base alla documentazione, possiamo assegnare dei pesi al calcolo della distanza per enfatizzare la relazione tra alcune funzionalità. Sulla base della nostra conoscenza del dominio, abbiamo identificato le funzionalità che vogliamo enfatizzare, in questo caso Brand, Mercato, Paese e Livello di mercato.
# 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
Il passaggio della conoscenza del dominio al modello tramite pesi ha aumentato il punteggio in modo significativo. Successivamente, testiamo l'impatto della misura della distanza.
Finora abbiamo utilizzato la distanza euclidea. Vediamo cosa succede se usiamo invece la distanza di Manhattan.
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
Diminuendo p a 1 si sono ottenuti alcuni buoni miglioramenti. Vediamo cosa succede quando p si avvicina all'infinito.
Per utilizzare la distanza di Chebyshev, cambieremo il parametro metrico in Chebyshev.
La metrica sklearn Chebyshev predefinita non ha un parametro di peso. Per aggirare questo problema, definiremo una consuetudine weighted_chebyshev
metrico.
# 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
Siamo riusciti a ridurre i punteggi di varianza delle caratteristiche principali attraverso la sperimentazione.
Visualizziamo i risultati.
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')
L'utilizzo della distanza di Manhattan con i pesi sembra fornire i parametri di riferimento più accurati in base alle nostre esigenze.
L'ultimo passaggio prima di implementare i set di benchmark consiste nell'esaminare i set con i punteggi delle caratteristiche primarie più alti e identificare quali passaggi intraprendere con essi.
# Histogram of Primary features score
results_model_3("cat_weighted_variance_score").plot(kind="hist")
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")
Questi 18 casi dovranno essere esaminati per garantire che i parametri di riferimento siano pertinenti.
Come puoi vedere, con poche righe di codice e una certa comprensione della ricerca del vicino più vicino, siamo riusciti a impostare set di benchmark interni. Ora possiamo distribuire i set e iniziare a misurare i KPI degli hotel rispetto ai loro set di benchmark.
Non è sempre necessario concentrarsi sui metodi di machine learning più all'avanguardia per offrire valore. Molto spesso, il semplice machine learning può offrire un grande valore.
Quali sono alcuni frutti a portata di mano nella tua attività che potresti facilmente ottenere con il machine learning?
Banca Mondiale. “Indicatori di sviluppo mondiale”. Estratto l'11 giugno 2024 da https://datacatalog.worldbank.org/search/dataset/0038117
Aggarwal, CC, Hinneburg, A., & Keim, DA (nd). Sul comportamento sorprendente delle metriche di distanza nello spazio ad alta dimensione. Centro di ricerca e istituto di informatica IBM TJ Watson, Università di Halle. Recuperato da https://bib.dbvis.de/uploadedFiles/155.pdf
Manuale di SciPy v1.10.1. scipy.spatial.distance.minkowski
. Estratto l'11 giugno 2024 da https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.minkowski.html
Geek per Geek. Formula di Haversine per trovare la distanza tra due punti su una sfera. Estratto l'11 giugno 2024 da https://www.geeksforgeeks.org/haversine-formula-to-find-distance-between-two-points-on-a-sphere/
scikit-impara. Modulo dei vicini. Estratto l'11 giugno 2024 da https://scikit-learn.org/stable/modules/classes.html#module-sklearn.neighbors
Fonte: towardsdatascience.com