Ottimizzazione dei costi con Python e Scikit-learn

Storicamente, le decisioni sui prezzi erano spesso basate su tendenze di mercato, analisi della concorrenza e un pizzico di intuizione. Tuttavia, l’emergere delle tecniche di apprendimento automatico ha consentito alle aziende di sfruttare i dati per studiare l’andamento del mercato, promuovendo decisioni sui prezzi più oculate.
In questo semplice use case, vedremo un esempio semplice di ottimizzazione dei costi che usa la regressione lineare tradizionale, per dimostrare come le tecniche di machine learning possano essere implementate per un’attività di questo tipo utilizzando Python.
- Caso d’uso
- Generazione dei dati
- Pre-elaborazione dei dati
- Training
- Valutazione del modello
- Valutazione
- Test
- Conclusione
Caso d’uso
Considera uno scenario in cui hai dati sui clienti, sui prodotti a disposizione e sulle vendite. Vuoi ottimizzare i prezzi dei tuoi prodotti in base alle caratteristiche dei clienti e ai dati storici sulle vendite.
Esaminiamo ogni fase del processo, dalla generazione dei dati all’addestramento del modello e, infine, al processo di ottimizzazione dei prezzi.
Generazione dei dati
A scopo illustrativo, andiamo a crearci un insieme di dati sintetici che comprendono informazioni su clienti, prodotti e vendite. Ciò include attributi quali età del cliente, sesso e posizione, insieme a dettagli relativi al prodotto quali categoria, marchio, prezzo e quantità di vendita.
L’obiettivo principale è prevedere la quantità di vendita in base all’età del cliente e al prezzo del prodotto.
Queste sono solo assunzioni per il caso d’uso descritto, ma la logica e il processo generale può essere adatto a qualunque tipo di analisi. Il risultato finale potrebbe variare, a seconda dei valori iniziali: per questo, è importante ripetere questo esperimento con un dataset reale di dati, che dia risultati coerenti.
Importiamo dunque le librerie necessarie per la generazione dei dati, tra cui pandas
, scikit-learn
e numpy
, le quali servono rispettivamente per creare dei dataframe, creare dei dati casuali all’interno dei singoli segmenti di dati, e per analizzare il dataset con degli strumenti di ML.
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
np.random.seed(42)
customer_data = pd.DataFrame({
'customer_id': np.arange(1, 501),
'customer_age': np.random.randint(18, 65, size=500),
'customer_gender': np.random.choice(['Male', 'Female'], size=500),
'customer_location': np.random.choice(['Urban', 'Suburban', 'Rural'], size=500)
})
product_data = pd.DataFrame({
'product_id': np.arange(1, 11),
'product_category': np.random.choice(['Electronics', 'Clothing', 'Home', \
'Sports'], size=10),
'product_brand': np.random.choice(['Brand_A', 'Brand_B', 'Brand_C'], size=10),
'product_price': np.random.uniform(50, 500, size=10),
'min_price': np.random.uniform(40,4900, size=10),
'cm': np.random.uniform(50,500, size = 10)/1000
})
sales_data = pd.DataFrame({
'customer_id': np.random.choice(np.arange(1, 501), size=1000),
'product_id': np.random.choice(np.arange(1, 11), size=1000),
'sales_quantity': np.random.randint(1, 10, size=1000)
})
all_data = pd.merge(customer_data, sales_data, on="customer_id")
all_data = pd.merge(all_data, product_data, on="product_id")
Andiamo dunque a creare tre dataframe, uno per ogni entità dello use case: uno per i clienti che contenga età, genere e zona, uno per i prodotti con i prezzi e il brand, e uno per le vendite. Infine, andiamo a unire i tre dataframe in un unico dataframe, sulla base degli ID creati in precedenza.
seed(42): Il numero 42 è una scelta arbitraria per il seed. Ciò significa che ogni volta che esegui il programma, otterrai la stessa sequenza di numeri casuali. Ciò è utile per il debug e per garantire che i risultati siano riproducibili
Pre-elaborazione dei dati
Prima di immergerci nell’addestramento del modello, è fondamentale pre-elaborare i dati. Ciò include tutte le fasi di pulizia dei dati (in questo caso, non necessaria, essendo dati sintetici), la verifica della coerenza dei dati, la definizione delle caratteristiche e delle variabili target di interesse e la suddivisione dei dati in insieme di addestramento e test.
Per prima cosa, andiamo a definire quali sono le proprietà con cui il modello di machine learning dovrà andare a valutare i prezzi: nel caso di esempio, l’età del cliente e il prezzo del prodotto. In questo caso, ci atteniamo su variabili “interne”, quindi proprie dei dati, ma è possibile anche includere nella valutazione fattori esterni come l’inflazione, la stagionalità dei prodotti, tasso di disoccupazione o altro.
Allo scopo di predire il prezzo migliore dei prodotti, è necessario conoscere la domanda per ognuno di essi: quindi la variabile target sarà sales_quantity
.
Andiamo poi a definire l’insieme di dati di test e di training, dividendo il dataframe iniziale e stabilendo una dimensione dell'80% assegnata all’insieme di training e la restante parte per test.
Non esiste una regola “ufficiale” per stabilire la giusta percentuale per un insieme o per l’altro, ma esistono semmai delle linee guida generali. In casi d’uso reali, si consiglia di usare la cosiddetta “rule of thumb” e tenersi su una divisione 80/20, 70/30 o, in caso di dati estremamente etereogenei e ben distribuiti, 90/10. Nel caso in cui si voglia anche fare una valutazione del modello, solitamente si suddivide il dataset in tre insiemi, usando una percentuale 80/10/10.
Training
Andiamo quindi ad applicare la funzione fit_transform
sull’insieme X_train
tramite l’oggetto StandardScaler
: il metodo fit
analizza i dati e prova a predire la trasformazione che deve essere applicata.
StandardScaler
è un oggetto della libreria scikit-learn
che opera sul principio di normalizzazione, ossia trasforma la distribuzione di ogni feature così che abbia una media di 0 e una deviazione standard di 1. Questo processo assicura che tutte le caratteristiche siano sulla stessa scala, impedendo a una singola feature di dominare il processo di apprendimento a causa della sua numerica. Questa risorsa è particolarmente utile nel caso siano presenti dati con outlier che devono essere gestiti opportunamente.
Ci sono altri modi di gestire dei dati con questi trasformatori: nella
documentazione
ufficiale vengono descritti nel dettaglio, anche in base al caso d’uso specifico in cui è possibile utilizzarli.
Dopo il fitting, questo oggetto è pronto per essere utilizzato per trasformare o pre-elaborare nuovi dati.
In pratica, quando si chiama fit_transform
sul training set, il modello apprende i parametri necessari per la trasformazione (ad esempio, media e deviazione standard per la standardizzazione in base ai dati di training). Di conseguenza, ridimensiona o trasforma i dati di training.
Una volta che il modello è stato adattato ai dati di training, ha appreso i cosiddetti parametri di trasformazione. Quando si desidera applicare la stessa trasformazione al set di test, si utilizza la trasformazione senza usare la funziona fit
, ma solo la funzione transform
.
Ciò garantisce che i dati di test vengano ridimensionati o trasformati utilizzando i parametri appresi dai dati di training. Questo passeggio è importante perché negli scenari del mondo reale, il modello incontrerà dati nuovi e mai visti e si desidera valutarne le prestazioni reali, piuttosto che valutare casi già visti.
features = ['customer_age', 'product_price']
target_variable = 'sales_quantity'
X_train, X_test, y_train, y_test = train_test_split(all_data[features], \
all_data[target_variable], test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
In quest’altra opzione, viene utilizzato un modello di regressione lineare semplice e spiegabile per prevedere la quantità di vendite di prodotti.
Tuttavia, per scenari più complessi, è possibile sostituirlo con modelli avanzati come XGBoost, Random Forest, Decision Trees, Reinforcement learning, o è possibile anche usare una rete neurale. In questo caso specifico, verrà utilizzata la regressione lineare perché perfetta per avere una predizione di tipo numerico sulla base di un insieme di feature altrettanto numeriche.
Come già spiegato in precedenza, possiamo usare la funzione fit
sul modello scelto e, sfruttando i dati normalizzati, andare a predire dei valori sulle vendite dei prodotti con il metodo predict
.
model = LinearRegression()
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
Valutazione del modello
Dopo aver addestrato il modello, vengono effettuate delle predizioni sul set di test, per cui le prestazioni del modello devono valutate: è possibile utilizzare diverse metriche, ma una particolarmente importante nel caso di esempi di regressione, è la radice dell’errore quadratico medio (abbreviato in RMSE). Più basso è il RMSE, migliore è l’accuratezza predittiva del modello.
Ogni tipo di attività ha le sue metriche: questo vuol dire che il RMSE è importante per la regressione ma, ad esempi, il punteggio F1 non è detto lo sia.
Questo perché esistono diversi modi per valutare il modello a seconda del tipo, ad esempio:
Modelli di classificazione
- Accuratezza: la percentuale di predizioni corrette.
- Precisione: la percentuale di veri positivi tra tutte le predizioni positive.
- Recall: la percentuale di veri positivi tra tutti i positivi effettivi.
- Punteggio F1: la media armonica di precisione e recall, che bilancia le due metriche.
- Tasso di falsi positivi (FPR): la percentuale di dati classificati in modo errato come anomalie.
- Tasso di veri positivi (TPR): la percentuale di dati classificate correttamente.
- Area sotto la curva ROC (AUC): una misura delle prestazioni complessive del modello che considera sia FPR che TPR.
Modelli di regressione
- Errore quadratico medio (MSE): la differenza quadratica media tra i valori predetti e quelli effettivi.
- Radice dell’errore quadratico medio (RMSE): la radice quadrata di MSE, che ha le stesse unità della variabile target.
- Errore assoluto medio (MAE): la differenza assoluta media tra i valori predetti e quelli effettivi.
Clustering
- Purezza: la proporzione di punti dati in ogni cluster che appartengono al cluster più frequente.
- Indice Calinski-Harabasz (CHI): una misura del grado di separazione e della compattezza dei cluster.
- Coefficiente di silhouette: una misura di quanto bene i punti dati sono assegnati ai cluster.
Queste sono solo alcune delle metriche, che andremo ad approfondire in casi d’uso ad hoc!
Valutazione
A questo punto, possiamo andare a valutare il modello sul set di test, utilizzando ad esempio il RMSE attraverso la funzione disponibile all’interno della libreria scikit-learn
:
test_rmse = mean_squared_error(y_test, y_pred)
print(f"Test RMSE: {test_rmse:.2f}")
Un valore pari a 0 di questa metrica indica che i valori attesi e quelli effettivi corrispondono esattamente. Valori RMSE bassi mostrano che il modello effettua previsioni più accurate e si adatta bene ai dati. Livelli più elevati, d’altro canto, implicano errori più significativi e previsioni meno accurate.
Test
Ora arriva la parte entusiasmante: utilizzare il modello per ottimizzare i prezzi. Di seguito vediamo come andare ad applicare una certa trasformazione sui dati che useremo come input per il modello, così che siano “adattati” a esso.
In sintesi, questo passaggio mira ad adattare i valori di vendita previsti in base alla loro distribuzione, rendendoli comparabili e potenzialmente più adatti per processi, analisi o processi decisionali. È un passaggio di pre-elaborazione comune per garantire che i dati si comportino nel modo previsto e desiderabile, una volta che si usa un modello.
Attenzione: concetti di statistica in arrivo!
Qui stiamo applicando tecniche di ridimensionamento per ottenere un fattore di ridimensionamento, che viene calcolato in base a quanto ogni valore di vendita previsto si discosta dalla media, e che viene normalizzato dalla deviazione standard. Introduce una forma di normalizzazione o ridimensionamento alle vendite previste. Applicando il ridimensionamento, possiamo standardizzare le vendite per singoli prodotti.
L’uso della media e della deviazione standard nel calcolo del fattore di ridimensionamento implica che il processo sia influenzato dalla distribuzione delle vendite previste. Se ci sono valori anomali nelle vendite previste, il fattore di ridimensionamento sarà più sensibile. I valori che sono lontani dalla media avranno un fattore di ridimensionamento più elevato, che influenzerà il risultato finale, ovvero il prezzo ottimizzato. Come osserverai, è stato utilizzato 0,1 nel calcolo del fattore di ridimensionamento, ma è possibile regolarlo se si desidera un impatto maggiore o minore del fattore.
Inoltre, è possibile applicare un ridimensionamento personalizzato in base al margine e al prezzo minimo. Nelle righe seguenti, stiamo calcolando un nuovo prezzo in base al margine che vogliamo mantenere sul prodotto o al prezzo minimo al di sotto del quale non possiamo o non vogliamo scendere.
Il valore definito come apbm
corrisponde al prezzo “rivisto” in base al margine. Inoltre, possiamo valutare di prendere il massimo di tutti i prezzi, così da avere il massimo del rendimento sul prodotto, con un margine specificato.
mean_sales = all_data["predicted_sales"].mean()
std_sales = all_data["predicted_sales"].std()
scaling_factor = 1 + (0.1 * (all_data["predicted_sales"] - mean_sales) / std_sales)
all_data['scf'] = scaling_factor.astype(float)
all_data['adj_psc'] = all_data['product_price'] * all_data['scf']
all_data["apbm"] = all_data["product_price"] * (1 + all_data["cm"])
all_data['adj_price'] = np.maximum(\
np.maximum(all_data['min_price'], \
all_data['adj_psc']) , \
all_data['apbm'])
print(all_data[['product_price','scf','min_price',\
'adj_psc','apbm','adj_price']].sample(n=10))
Nell’ultima riga andiamo a stampare una tabella in cui riportiamo il prezzo del prodotto, il fattore di scala, il prezzo minimo, il valore ridimensionato, quello che tiene conto del margine e il prezzo finale, in un campione casuale di 10 prodotti.
product_price scf min_price adj_psc apbm adj_price
0 307.982470 1.115979 371.693568 343.702017 390.426213 390.426213
1 169.924277 1.088077 1759.814890 184.890729 225.095648 1759.814890
2 162.828797 1.086643 936.042674 176.936800 200.033910 936.042674
3 272.423233 1.033591 3786.991598 281.574185 302.594262 3786.991598
4 379.483060 1.055228 4698.928082 400.441169 560.567845 4698.928082
5 396.910120 0.953468 4489.396472 378.441051 445.692852 4489.396472
6 272.423233 0.928309 3786.991598 252.892860 302.594262 3786.991598
7 272.423233 0.928309 3786.991598 252.892860 302.594262 3786.991598
8 379.483060 1.160510 4698.928082 440.393991 560.567845 4698.928082
9 272.423233 1.138873 3786.991598 310.255509 302.594262 3786.991598
Conclusione
Questo tutorial serve a dare una guida passo passo sull’uso del machine learning per l’ottimizzazione dei prezzi, con delle tecniche piuttosto semplici, ma efficaci per capire qual è il giusto approccio con cui affrontare uno use case simile.
L’esempio ha utilizzato un modello di regressione lineare, ma i principi si possono estendere anche a modelli più avanzati.
Tieni presente che le applicazioni del mondo reale potrebbero richiedere una gestione diversa delle feature e l’utilizzo di tecniche di feature engineering più complessa0, una messa a punto degli iperparametri e la considerazione di fattori esterni, come menzionato nelle diverse sezioni precedenti.
Perché imparare a lavorare su questo tipo di attività? Perché l’implementazione dell’ottimizzazione dei prezzi basata su ML può migliorare il tuo processo decisionale, aumentare la redditività e adattarsi alle condizioni dinamiche del mercato.