PID
Libreria per allenare modello xgboost di classificazione su valori india.
Nel file main.py si trova il workflow completo nei paragrafi qui sotto vengono descritte alcune funzionalita'.
retrive_data
Permette di scarcare i dati dal db, sistema i campi nested (tipologia di veicolo utilizzato) e, per ogni colonna, calcola la percentuale di ogni variabile categorica. Visto che alcuni samples sono abbastanza unici, per evitare overfitting e' possibile aggregare questi valori. Di default tutte le classi con meno del 0.5% di rappresentazna vengnono convogliate nella classe other. Restituisce due dataset: uno con le classi accorpate (chiamato small) uno no (per essere sicuri che rappresentino le stesse informazioni)
split
Questo serve per splittare il dataset (small o normale) in train, validation e test. Specificando skiarea_test e' possibilile rimuovere completamente una skiarea dal dataset per simulare safe index su una zona nuova. La coppia season_test_skiarea-season_test_year invece serve per rimuovere dal dataset alcune stagioni da una precisa skiarea: questo serve per simulare come si comporta su nuove stagioni di cui ha gia' visto dati passati. Una volta rimossi i dati relativi al test set, il dataset rimamenente viene separato in train e validation (66%-33%) stratificando su india (in modo da avere piu' o meno il rapporto tra le classi costante). Ci sono due modi per aiutare il modello con il dataset sbilanciato (pochissimi india 3 e 4): il primo e fare oversampling delle classi piccole (a me non piace), alternativamente si pesa in maniera diversa l'errore fatto sulle classi piccole. Ne ho messi due uno utilizza la radice del totale dei casi e divide per gli elementi della classe: un po' meno fiscale del dividere la somma per il numero di elementi. Ritorna due Dataset (uno normale e uno di test), sono delle classi di supporto per andare meglio nella fase di train
train
Questo e' il core del programma: ho messo una griglia di iperparametri con dei range di solito utilizzati. Si allena un xgboost a massimizzare MCC (non accuracy che non e' indicato in caso di classi sbilanciate). Si imposta il numero di trial (suggerisco almeno 1000) e un timeout (in caso di risorse limitate). num_boost e' il numero massimo di step, c'e' un sistema di overfitting detection per fermarlo prima.
gain_accuracy_train
L'idea e' che, una volta trovato il set di parametri ottimale, posso giocare con le variabili di input. A farla bene si dovrebbe rilanciare l'ottimizzazione per ogni scelta delle variabili, ma costerebbe molto piu' tempo.
Le features vengono ordinate per importanza utilizzando lo score (best_model.get_fscore())).

A questo punto, partendo dalla piu' importante si allena il modello con N features e si confrontano i valori.

Questo puo' essere utilizzato anche per dire: per avere il x% di accuracy devo utilizzare almeno queste variabili. Mi viene in mente ad esempio 'questi sono i campi obbligatori del form da compilare'.
Modello finale
Una volta fatti tutti i test possiamo anche reintrgrare il test set nel train:
skiarea_test: None
season_test_skiarea : None
season_test_year: None
e anche aumentare la quantita' di punti nel trainin set:
test_size = 0.2 #(80% train 20% validation)
Notebooks
Ci sono alcuni notebook, TRAIN contiene piu' o meno quello che fa main.py, o meglio una sua versione precedente e non pulita con alcuni check etc, l'ho lasciata per sicurezza. Variable_exploration contiene la parte di inference su un nuovo dataset utilizzando prepare_new_data (c'e' anche un confronto tra le distribuzioni, ma non avendo i labels non saprei che altro mettere). C'e' anche una parte di explainability. Molto difficile da interpretare con le variabili categoriche, ma in qualche modo ti dice perche' un certo sample e' stato classificato in questo modo. Nelle immagini qui sotto vedi il sample considerato, che ha classe 2 in origine, che viene correttamente classificato (guarda i valori degli shap values oppure le predizioni che lo mettono in classe 2 con 86% di probabilita.). Nei due grafici sotto si vede quali feature fanno aumentare o diminuire il valore di probabilita' (non proprio probabilita' ma se vuoi lo possiamo chiamare affidabilita). Tutte le frecce rosse che spingono verso destra si leggono cosi' (guardiamo la seconda riga): la diagnosi, la location e la destinazione sono quelle che maggiormente gli fanno pensare che sia della seconda classe. In effetti elicottero, hospital_emergency_room e dislocation possono fare pensare che non sia una cosa da poco. Non va sempre cosi' bene, ti ho trovato un esempio chiaro per spiegartelo, poi vedete voi se e come usarlo.

Train del modello
cd src
python main.py
python main.py model.n_trials=200 ## se volgio cambiare dei parametri
Questo automaticamente legge i dati dal file conf.yaml. Lo script crea una cartella model.name e ci mette tutto quello che il main genera (il conf usato, tutti i log, i modelli salvati e alcune metafeatures da usare poi nel service).
Service del modello
Nella cartella service e' stata implementato un endpoint per interrogare il modello, basato su docker e fastapi.
Una volta scelto il modello in base alle performances si devono spostare i files metadata.pkl e model.json dalla cartella src/<model.name> alla cartella service/app.
Accedere alla cartella service ed eseguire i seguenti comandi:
docker build -t myimage .
docker run -d --name mycontainer -p 80:80 myimage
Questo copia tutto il contenuto di app e builda il container (non ci sono volumi montati). Per testare:
0.0.0.0/predict/'{"dateandtime":1231754520000,"skiarea_id":null,"skiarea_name":"Pampeago","day_of_year":12,"minute_of_day":602,"year":2009,"season":2009,"difficulty":"novice","cause":"fall_alone","town":"SIKLOS","province":"","gender":"F","equipment":"ski","helmet":null,"destination":"hospital_emergency_room","diagnosis":"distortion","india":null,"age":32.0,"country":"Ungheria","injury_side":"L","injury_general_location":"lower_limbs","evacuation_vehicles":["akja"]}'
0.0.0.0/predict/'{"dateandtime":1512294600000,"skiarea_id":13.0,"skiarea_name":"Kronplatz","day_of_year":337,"minute_of_day":590,"year":2017,"season":2018,"difficulty":"intermediate","cause":"fall_alone","town":"Pieve di Soligo","province":"Treviso","gender":"M","equipment":"ski","helmet":true,"destination":"hospital_emergency_room","diagnosis":"other","india":"i1","age":43.0,"country":"Italia","injury_side":"L","injury_general_location":"lower_limbs","evacuation_vehicles":["akja"]}'
0.0.0.0/predict/'{"dateandtime":1512726900000,"skiarea_id":13.0,"skiarea_name":"Kronplatz","day_of_year":342,"minute_of_day":595,"year":2017,"season":2018,"difficulty":"easy","cause":"fall_alone","town":"Vigo Novo","province":"Venezia","gender":"M","equipment":"ski","helmet":true,"destination":"hospital_emergency_room","diagnosis":"distortion","india":"i2","age":23.0,"country":"Italia","injury_side":"L","injury_general_location":"lower_limbs","evacuation_vehicles":["snowmobile"]}'
Non so bene come verranno passati i dati all'end point in produzione, in caso c'e' da modificare la parte del serving che fa il parsing dei dati passati da url. Se ci sono stringhe strane si arrabbia (succede con bolzano che ci sono dei backslash).
