{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":81933,"databundleVersionId":9643020,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Preparazione","metadata":{}},{"cell_type":"markdown","source":"## Dipendenze","metadata":{}},{"cell_type":"markdown","source":"Questo notebook è stato derivato da quello consegnato, ma possiede alcune modifiche necessarie per farlo funzionare sulle dipendenze più vecchie usate da Kaggle.","metadata":{}},{"cell_type":"code","source":"!pip install numpy pandas scikit-learn matplotlib lightgbm scipy\n\nimport os\nimport textwrap\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.base import clone\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import OneHotEncoder\nfrom sklearn.metrics import cohen_kappa_score, accuracy_score, precision_score, recall_score, f1_score\nfrom sklearn.ensemble import RandomForestRegressor\nfrom lightgbm import LGBMRegressor\nfrom scipy.optimize import minimize\n\npd.set_option(\"future.no_silent_downcasting\", True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:21.751749Z","iopub.execute_input":"2026-02-13T23:27:21.751929Z","iopub.status.idle":"2026-02-13T23:27:26.929208Z","shell.execute_reply.started":"2026-02-13T23:27:21.751914Z","shell.execute_reply":"2026-02-13T23:27:26.928258Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Importazione dati","metadata":{}},{"cell_type":"code","source":"TRAIN_CSV = \"train.csv\"\nTEST_CSV = \"test.csv\"\nKAGGLE_DIR = \"/kaggle/input/child-mind-institute-problematic-internet-use\"\n\nif os.path.isdir(KAGGLE_DIR):\n    os.chdir(KAGGLE_DIR)    # Carica i dati da questa cartella se eseguito su Kaggle\n\nwhole = pd.read_csv(TRAIN_CSV)\nkaggle = pd.read_csv(TEST_CSV)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:26.930308Z","iopub.execute_input":"2026-02-13T23:27:26.930915Z","iopub.status.idle":"2026-02-13T23:27:26.963909Z","shell.execute_reply.started":"2026-02-13T23:27:26.930891Z","shell.execute_reply":"2026-02-13T23:27:26.962960Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Pulizia","metadata":{}},{"cell_type":"markdown","source":"Una prima pulizia di base consiste nel rimuovere tutte le righe in cui `sii` è nullo, perchè serve per l'addestramento.","metadata":{}},{"cell_type":"code","source":"print(\"Forma dell'intero dataset:\", whole.shape)\n\nwhole = whole.dropna(subset=[\"sii\"])\nprint(\"Forma del dataset senza sii nulli:\", whole.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:26.965195Z","iopub.execute_input":"2026-02-13T23:27:26.965466Z","iopub.status.idle":"2026-02-13T23:27:26.981554Z","shell.execute_reply.started":"2026-02-13T23:27:26.965450Z","shell.execute_reply":"2026-02-13T23:27:26.980907Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dopodichè vanno escluse tutte le colonne che non sono presenti nel CSV di test, perchè altrimenti non sarebbe possibile valutare i modelli.","metadata":{}},{"cell_type":"code","source":"whole_missing_on_kaggle = whole.columns.difference(kaggle.columns.union([\"sii\"]))\nwhole = whole.drop(columns=whole_missing_on_kaggle)\nprint(\"Forma del dataset senza le colonne non presenti nel test di Kaggle:\", whole.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:26.982307Z","iopub.execute_input":"2026-02-13T23:27:26.982555Z","iopub.status.idle":"2026-02-13T23:27:27.001428Z","shell.execute_reply.started":"2026-02-13T23:27:26.982537Z","shell.execute_reply":"2026-02-13T23:27:27.000452Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Categorie","metadata":{}},{"cell_type":"markdown","source":"Nella [pagina del dataset](https://www.kaggle.com/competitions/child-mind-institute-problematic-internet-use/data?select=data_dictionary.csv) sono documentati i tipi di dato contenuto in ogni colonna. Tra questi ci sono alcune colonne descritte come `categorical int` che andrebbero esplicitamente indicate come tali. Un esempio è:","metadata":{}},{"cell_type":"code","source":"numbers = whole[\"Basic_Demos-Sex\"]\ncounts = numbers.value_counts()\n\nprint(\"Tipo colonna originale:\", numbers.dtype.name)\nprint(\"Possibili valori:\")\nprint(textwrap.indent(str(counts), \"  \"))\n\nnumbers = numbers.astype(\"category\")\nprint(\"Tipo colonna convertita:\", numbers.dtype.name)\n\nwhole[\"Basic_Demos-Sex\"] = numbers\nkaggle[\"Basic_Demos-Sex\"] = kaggle[\"Basic_Demos-Sex\"].astype(\"category\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.004016Z","iopub.execute_input":"2026-02-13T23:27:27.004300Z","iopub.status.idle":"2026-02-13T23:27:27.033936Z","shell.execute_reply.started":"2026-02-13T23:27:27.004277Z","shell.execute_reply":"2026-02-13T23:27:27.033069Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"In pratica però questa è l'unica colonna che serve convertire, perchè le altre (i.e. `FGC-FGC_*_Zone`, `BIA-BIA_*_num`, `PreInt_EduHx-computerinternet_hoursday` e `sii`), non indicano semplicemente delle categorie ma anche un loro ordine intrinseco.\n\nSi consideri per esempio `sii`; il suo valore categorico va da 0 a 3 in base a quanto severa è la situazione del partecipante. Di conseguenza, per queste categorie è preferibile che il modello possa determinare che un `sii` di valore 2 è più vicino ad uno di valore 3.\n\nNel caso specifico di `sii` dato che è la variabile che si vuole predire, conviene che i modelli facciano una regressione. In questo modo, non solo il modello terrà conto che è meglio predire un 2 invece che uno 0 quando ci si aspetta un 3, ma ci permette anche di scegliere quali threshold usare per convertire i valori reali in classi.","metadata":{}},{"cell_type":"markdown","source":"Per lo stesso motivo anche le colonne contenenti le stagioni (e.g. `Basic_Demos-Enroll_Season`) possono essere rappresentate da dei numeri ordinati:","metadata":{}},{"cell_type":"code","source":"seasons_columns = whole.select_dtypes(include=[\"object\"]).columns.difference([\"id\"])    # Gli uniche stringhe rimanenti sono le stagioni\n\nseasons_replace = {\"Spring\": 0, \"Summer\": 1, \"Fall\": 2, \"Winter\": 3}\nwhole[seasons_columns] = whole[seasons_columns].replace(seasons_replace).astype(\"float\")\nkaggle[seasons_columns] = kaggle[seasons_columns].replace(seasons_replace).astype(\"float\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.034840Z","iopub.execute_input":"2026-02-13T23:27:27.035035Z","iopub.status.idle":"2026-02-13T23:27:27.061485Z","shell.execute_reply.started":"2026-02-13T23:27:27.035018Z","shell.execute_reply":"2026-02-13T23:27:27.060759Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dopo una pulizia preliminare, si può effettuare un'analisi generale del dataset che permette di trovare evenutali anomalie.","metadata":{}},{"cell_type":"markdown","source":"### Zeri","metadata":{}},{"cell_type":"markdown","source":"Si possono notare varie colonne con un'elevata quantità di zeri.","metadata":{}},{"cell_type":"markdown","source":"#### `Physical-{Weight,BMI}`","metadata":{}},{"cell_type":"code","source":"_, (plt1, plt2) = plt.subplots(1, 2, figsize=(10, 4))\n\nplt1.hist(whole[\"Physical-Weight\"].dropna(), bins=50, density=True)\nplt1.set_xlabel(\"Physical-Weight\")\nplt1.set_ylabel(\"Densità\")\nplt1.set_title(\"Densità dei pesi\")\n\nplt2.hist(whole[\"Physical-BMI\"].dropna(), bins=50, density=True)\nplt2.set_xlabel(\"Physical-BMI\")\nplt2.set_ylabel(\"Densità\")\nplt2.set_title(\"Densità dei BMI\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.062227Z","iopub.execute_input":"2026-02-13T23:27:27.062460Z","iopub.status.idle":"2026-02-13T23:27:27.609836Z","shell.execute_reply.started":"2026-02-13T23:27:27.062442Z","shell.execute_reply":"2026-02-13T23:27:27.608255Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Il peso e il BMI non possono essere 0, quindi si possono sostituire con dei valori nulli:","metadata":{}},{"cell_type":"code","source":"whole[\"Physical-Weight\"] = whole[\"Physical-Weight\"].replace(0, np.nan)\nkaggle[\"Physical-Weight\"] = kaggle[\"Physical-Weight\"].replace(0, np.nan)\n\nwhole[\"Physical-BMI\"] = whole[\"Physical-BMI\"].replace(0, np.nan)\nkaggle[\"Physical-BMI\"] = kaggle[\"Physical-BMI\"].replace(0, np.nan)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.610615Z","iopub.execute_input":"2026-02-13T23:27:27.610779Z","iopub.status.idle":"2026-02-13T23:27:27.617741Z","shell.execute_reply.started":"2026-02-13T23:27:27.610764Z","shell.execute_reply":"2026-02-13T23:27:27.617039Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### `Fitness_Endurance-Time_Sec`","metadata":{}},{"cell_type":"code","source":"endurances = whole[\"Fitness_Endurance-Time_Sec\"].dropna()\n\nplt.figure(figsize=(5, 4))\nplt.hist(endurances, bins=40, density=True)\nplt.xlabel(\"Fitness_Endurance-Time_Sec\")\nplt.ylabel(\"Frequenza\")\nplt.title(\"Frequenze dei secondi di resistenza\")\n\nxticks = np.arange(endurances.min(), endurances.max()+1)\nplt.xticks(xticks[xticks % 5 == 0])\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.618337Z","iopub.execute_input":"2026-02-13T23:27:27.618542Z","iopub.status.idle":"2026-02-13T23:27:27.819554Z","shell.execute_reply.started":"2026-02-13T23:27:27.618521Z","shell.execute_reply":"2026-02-13T23:27:27.818441Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"In questo caso si può notare che i valori sono spesso raggruppati nei multipli di 5, il che suggerisce un arrotondamento piuttosto che la presenza di valori non validi. In ogni caso è difficile distinguere l'uno dall'altro e quindi è meglio lasciare i dati come sono.","metadata":{}},{"cell_type":"markdown","source":"#### `FGC-FGC_{CU,PU,SRL,SRR}`","metadata":{}},{"cell_type":"markdown","source":"Nei dettagli del dataset, questo colonne sono descritte come:\n- `FGC-FGC_CU`: Numero di crunch/curl-up\n- `FGC-FGC_PU`: Numero di piegamenti/push-up\n- `FGC-FGC_SR{L,R}`: Numero di sit & reach rispettivamente verso sinistra e destra\n\nOgnuna di queste ha una sua rispettiva colonna categorica `FGC-FGC_*_Zone`, che può assumere i valori:\n- `0`: Se per l'esercizio il soggetto ha bisogno di migliorare\n- `1`: Se è sano","metadata":{}},{"cell_type":"markdown","source":"La proporzione di zeri di ciscuna colonna è:","metadata":{}},{"cell_type":"code","source":"def p_zeroes(feature):\n    frequency = (whole[feature].dropna() == 0).mean()\n    return f\"{round(frequency*100, 2)}%\"\n\nprint(\"Percentuale di zeri tra le istanze:\")\nprint(\"- di FGC-FGC_CU:\", p_zeroes(\"FGC-FGC_CU\"))\nprint(\"- di FGC-FGC_PU:\", p_zeroes(\"FGC-FGC_PU\"))\nprint(\"- di FGC-FGC_SRL:\", p_zeroes(\"FGC-FGC_SRL\"))\nprint(\"- di FGC-FGC_SRR:\", p_zeroes(\"FGC-FGC_SRR\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.820829Z","iopub.execute_input":"2026-02-13T23:27:27.821064Z","iopub.status.idle":"2026-02-13T23:27:27.829266Z","shell.execute_reply.started":"2026-02-13T23:27:27.821026Z","shell.execute_reply":"2026-02-13T23:27:27.828503Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Pur avendo una quantità elevata di zeri, la maggior parte di essi è accompagnato dalla rispettiva zona `FGC-FGC_*_Zone = 0`, il che non ci permette di dire con certezza se tali valori non sono validi o se effettivamente il soggetto ha bisogno di miglioramento:","metadata":{}},{"cell_type":"code","source":"def p_zeroes_valid(feature):\n    zeroes_all = whole[feature] == 0\n    zeroes_unhealthy = zeroes_all & (whole[f\"{feature}_Zone\"] == 0)\n    frequency = zeroes_unhealthy.sum() / zeroes_all.sum()\n    return f\"{round(frequency*100, 2)}%\"\n\nprint(\"Percentuale di zeri probabilmente validi:\")\nprint(\"- di FGC-FGC_CU:\", p_zeroes_valid(\"FGC-FGC_CU\"))\nprint(\"- di FGC-FGC_PU:\", p_zeroes_valid(\"FGC-FGC_PU\"))\nprint(\"- di FGC-FGC_SRL:\", p_zeroes_valid(\"FGC-FGC_SRL\"))\nprint(\"- di FGC-FGC_SRR:\", p_zeroes_valid(\"FGC-FGC_SRR\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.830040Z","iopub.execute_input":"2026-02-13T23:27:27.830323Z","iopub.status.idle":"2026-02-13T23:27:27.846353Z","shell.execute_reply.started":"2026-02-13T23:27:27.830302Z","shell.execute_reply":"2026-02-13T23:27:27.845184Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Valori mancanti","metadata":{}},{"cell_type":"markdown","source":"Alcune colonne hanno un numero elevato di righe con valori nulli.","metadata":{}},{"cell_type":"code","source":"COLUMNS_MAX_MISSING_RATIO = 0.6\n\ndef ellipses_columns(columns):\n    return (\n        pd.Series(columns)\n            .apply(lambda column: (column[:25] + \"...\") if len(column) > 25 else column)\n            .values\n    )\n\nmissing_per_column = whole.isna().mean().sort_values(ascending=True)    # Gli sii di valore NaN sono già stati rimossi\nmissing_per_column_names = ellipses_columns(missing_per_column.index)\n\nplt.figure(figsize=(6, 9.5))\nplt.barh(missing_per_column_names, missing_per_column.values)\nplt.axvline(x=COLUMNS_MAX_MISSING_RATIO, color=\"black\", linestyle=\"--\", linewidth=0.8)\nplt.xlabel(\"Frequenza\")\nplt.ylabel(\"Colonne\")\nplt.title(\"Frequenza dei valori nulli\")\nplt.xlim(0, 1)\nplt.margins(y=8e-3)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:27.847271Z","iopub.execute_input":"2026-02-13T23:27:27.847497Z","iopub.status.idle":"2026-02-13T23:27:28.356727Z","shell.execute_reply.started":"2026-02-13T23:27:27.847479Z","shell.execute_reply":"2026-02-13T23:27:28.355773Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Di conseguenza, per evitare un'imputazione eccessiva conviene escludere le colonne con più del 60% di valori nulli:","metadata":{}},{"cell_type":"code","source":"columns_to_drop = missing_per_column[missing_per_column > COLUMNS_MAX_MISSING_RATIO].index\n\nwhole = whole.drop(columns=columns_to_drop)\nkaggle = kaggle.drop(columns=columns_to_drop)\n\nprint(\"Forma del dataset senza le colonne con troppi valori nulli:\", whole.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.357538Z","iopub.execute_input":"2026-02-13T23:27:28.357745Z","iopub.status.idle":"2026-02-13T23:27:28.365392Z","shell.execute_reply.started":"2026-02-13T23:27:28.357729Z","shell.execute_reply":"2026-02-13T23:27:28.364766Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Divisione in `X` e `y`","metadata":{}},{"cell_type":"code","source":"whole_X = whole.drop(columns=[\"sii\", \"id\"])\nwhole_y = whole[\"sii\"]\n\nkaggle_ids = kaggle[\"id\"]    # Necessari per submission.csv\nkaggle_X = kaggle.drop(columns=[\"id\"])    # Il test di Kaggle non contiene sii","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.366182Z","iopub.execute_input":"2026-02-13T23:27:28.366371Z","iopub.status.idle":"2026-02-13T23:27:28.387254Z","shell.execute_reply.started":"2026-02-13T23:27:28.366354Z","shell.execute_reply":"2026-02-13T23:27:28.386463Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Distribuzione dei valori sii da predire:\")\nprint(textwrap.indent(str(whole_y.value_counts()), \"  \"))","metadata":{"scrolled":true,"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.388416Z","iopub.execute_input":"2026-02-13T23:27:28.388639Z","iopub.status.idle":"2026-02-13T23:27:28.408491Z","shell.execute_reply.started":"2026-02-13T23:27:28.388622Z","shell.execute_reply":"2026-02-13T23:27:28.407625Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training","metadata":{}},{"cell_type":"markdown","source":"Il confronto avverrà tra `RandomForestRegressor` e `LGBMRegressor`.\n\nCome menzionato in precedenza, parte del training riguarda la scelta del threshold. Questo perchè i valori di `sii` sono fortemente [sbilanciati](#Divisione-in-X-e-y), e ottimizzare la suddivisone dei valori in `sii` è un modo per controbilanciarli. Inoltre, durante la sperimentazione tale strategia ha avuto un impatto positivo sulla performance dei modelli.","metadata":{}},{"cell_type":"code","source":"RANDOM_SEED = 64    # Seme per l'RNG\nN_JOBS = 6    # Numero di processi/thread per il training\nN_REPEAT = 4    # Numero di volte con cui ripetere il CV\nN_FOLDS = 5    # Numero di fold per il CV","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.409011Z","iopub.execute_input":"2026-02-13T23:27:28.409238Z","iopub.status.idle":"2026-02-13T23:27:28.427561Z","shell.execute_reply.started":"2026-02-13T23:27:28.409221Z","shell.execute_reply":"2026-02-13T23:27:28.426898Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Come specificato nella [pagina inerente alla valutazione](https://www.kaggle.com/competitions/child-mind-institute-problematic-internet-use/overview/evaluation), lo score della submission è calcolato con il _Quadratic Weighted Kappa_:","metadata":{}},{"cell_type":"code","source":"def qwk(y_true, y_pred):\n    return cohen_kappa_score(y_true, y_pred, weights=\"quadratic\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.428264Z","iopub.execute_input":"2026-02-13T23:27:28.428440Z","iopub.status.idle":"2026-02-13T23:27:28.447638Z","shell.execute_reply.started":"2026-02-13T23:27:28.428425Z","shell.execute_reply":"2026-02-13T23:27:28.446421Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"E per questo l'ottimizzazione conviene farla sul _QWK_, tramite:\n- `categorize`, che si occupa di convertire una previsione di regressione in categoria in base a dei threshold\n- `optimize_categories`, che si occupa di trovare il miglior partizionamento del risultato per ottenere il miglior _QWK_","metadata":{}},{"cell_type":"code","source":"def categorize(y_pred, bins):    # Trasforma y_pred nelle categorie {0, ..., len(bins)} a seconda del partizionamento in bins\n\n    # I threshold devono essere crescenti, perchè volendo trovare una i tale che\n    #   bins[i-1] <= y_pred < bins[i]\n    # allora dovrà essere vero anche che bins[i-1] < bins[i].\n    bins = np.sort(bins)    # Usando sort il QWK dovrebbe essere continuo sullo spazio di ricerca dei threshold\n\n    # Resituisce l'indice 0 <= i <= len(bins) del range in cui y_pred appartiene\n    return np.digitize(y_pred, bins)\n\ndef optimize_categories(train_pred, train_y):\n    bins = minimize(\n        lambda bins: -qwk(train_y, categorize(train_pred, bins)),    # Massimizza QWK minimizzando -QWK\n        x0=[0.5, 1.5, 2.5],    # Mapping arbitrario iniziale di: (-inf, 0.5) -> 0, [0.5, 1.5) -> 1, ..., [2.5, inf) -> 3\n        method=\"Nelder-Mead\"    # Vedi https://stackoverflow.com/a/9613461\n    )\n    assert bins.success\n    return bins.x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.449154Z","iopub.execute_input":"2026-02-13T23:27:28.449461Z","iopub.status.idle":"2026-02-13T23:27:28.469677Z","shell.execute_reply.started":"2026-02-13T23:27:28.449444Z","shell.execute_reply":"2026-02-13T23:27:28.468643Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Infine, la valutazione dei modelli avviene con una Cross-Validation ripetuta più volte e stratificata sulle `y` in modo da mantenere la frequenza di ogni classe.","metadata":{}},{"cell_type":"code","source":"def fold(X, y, n_repeat=N_REPEAT, n_folds=N_FOLDS, seed=RANDOM_SEED):\n    for repetition in range(n_repeat):\n        folds = StratifiedKFold(\n            n_splits=n_folds,\n            shuffle=True,\n            random_state=seed + repetition\n        )\n        for (train_i, test_i) in folds.split(X, y):\n            train_X = X.take(train_i)\n            train_y = y.take(train_i)\n            test_X = X.take(test_i)\n            test_y = y.take(test_i)\n            yield (train_X, test_X, train_y, test_y)\n\ndef cv(model, X, y, prepare=None, **kwargs):\n    qwks = []\n    accs = []\n    pres = []\n    recs = []\n    f1s = []\n\n    importance = []\n    counts = pd.concat([X, y], axis=1)\n    counts[\"Count\"] = 0\n    for (train_X, test_X, train_y, test_y) in fold(X, y, **kwargs):\n        if prepare:\n            train_X, test_X = prepare(train_X, test_X)    # Per abilitare preprocessing come imputazione e encoding\n\n        model_folded = clone(model)    # Copia lo stimatore non addestrato\n        model_folded.fit(train_X, train_y)\n        bins = optimize_categories(model_folded.predict(train_X), train_y)    # Parte del training\n        pred_y = categorize(model_folded.predict(test_X), bins)\n\n        score = accuracy_score(test_y, pred_y)\n        accs.append(score)\n\n        score = qwk(test_y, pred_y)\n        qwks.append(score)\n\n        scores = precision_score(test_y, pred_y, average=None, zero_division=0)\n        scores[scores == 0] = np.nan    # NOTE: Workaround terribile per il fatto che scikit-learn non supporta zero_division=np.nan\n        pres.append(scores)\n\n        scores = recall_score(test_y, pred_y, average=None, zero_division=0)\n        recs.append(scores)\n\n        scores = f1_score(test_y, pred_y, average=None, zero_division=0)\n        f1s.append(scores)\n\n        importance.append(model_folded.feature_importances_)\n        counts.loc[test_y[test_y == pred_y].index, \"Count\"] += 1    # Tiene conto di quante sono state predette correttamente\n\n    def report_by_class(all_scores):\n        all_scores = [\n            (np.nanmean(scores), np.nanstd(scores))\n            if not np.isnan(scores).all() else (np.nan, np.nan)    # Media solo tra valori non nulli\n            for scores in np.transpose(all_scores)\n        ]\n        return [f\"{mean:.2f} ± {std:.2f}\" for (mean, std) in all_scores]\n\n    importance = pd.DataFrame(importance, columns=X.columns)\n    counts = counts.sort_values(by=\"Count\", ascending=False)\n    return (qwks, importance, counts, (\n        str(pd.DataFrame({\n            \"Precision\": report_by_class(pres),\n            \"Recall\": report_by_class(recs),\n            \"F1\": report_by_class(f1s)\n        })) + \"\\n\\n\" +\n        f\"QWK: {np.mean(qwks):.4f} ± {np.std(qwks):.4f}\\n\" +\n        f\"Accuracy: {np.mean(accs):.3f} ± {np.std(accs):.3f}\"\n    ))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.470555Z","iopub.execute_input":"2026-02-13T23:27:28.470777Z","iopub.status.idle":"2026-02-13T23:27:28.492765Z","shell.execute_reply.started":"2026-02-13T23:27:28.470759Z","shell.execute_reply":"2026-02-13T23:27:28.491501Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Random Forest","metadata":{}},{"cell_type":"markdown","source":"Al contrario di LightGBM, questo modello non è in grado di gestire automaticamente i valori nulli e le colonne categoriche. Per questo motivo, è necessario implementare una funzione `prepare` per `cv`.\n\nSia l'imputazione che l'encoding prendono due parametri: `X` e `train_X`; questo perchè l'imputazione/encoding sullo split di test si vuole effettuare usando solamente le informazoni ricavate da `train_X`, per evitare che `X` le influenzi.","metadata":{}},{"cell_type":"code","source":"def impute(X, train_X):\n    category_columns = train_X.select_dtypes(exclude=[\"number\"]).columns\n    number_columns = train_X.select_dtypes(include=[\"number\"]).columns\n\n    X = X.copy()    # Evita che la modifica avvenga in-place\n    for number_column in number_columns:\n        median = train_X[number_column].median()\n        X[number_column] = X[number_column].fillna(median)\n    for category_column in category_columns:\n        mode = train_X[category_column].mode()[0]\n        X[category_column] = X[category_column].fillna(mode)\n    return X\n\ndef encode(X, train_X):\n    category_columns = train_X.select_dtypes(exclude=[\"number\"]).columns\n    number_columns = train_X.select_dtypes(include=[\"number\"]).columns\n\n    encoder = OneHotEncoder(drop=\"first\", sparse_output=False)\n    encoder.fit(train_X[category_columns])\n\n    categories = pd.DataFrame(\n        encoder.transform(X[category_columns]),\n        columns=encoder.get_feature_names_out(category_columns),\n        index=X.index\n    )\n    numbers = X[number_columns]\n\n    return pd.concat([numbers, categories], axis=1)\n\ndef impute_and_encode(train_X, test_X):\n    test_X = impute(test_X, train_X)\n    train_X = impute(train_X, train_X)\n    test_X = encode(test_X, train_X)\n    train_X = encode(train_X, train_X)\n    return train_X, test_X","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.496414Z","iopub.execute_input":"2026-02-13T23:27:28.496704Z","iopub.status.idle":"2026-02-13T23:27:28.519175Z","shell.execute_reply.started":"2026-02-13T23:27:28.496682Z","shell.execute_reply":"2026-02-13T23:27:28.518075Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Valutazione","metadata":{}},{"cell_type":"code","source":"model = RandomForestRegressor(random_state=RANDOM_SEED, n_jobs=N_JOBS)\nqwks_rf, imps_rf, _, report = cv(model, whole_X, whole_y, prepare=impute_and_encode)\nprint(report)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:28.520031Z","iopub.execute_input":"2026-02-13T23:27:28.520395Z","iopub.status.idle":"2026-02-13T23:27:52.533823Z","shell.execute_reply.started":"2026-02-13T23:27:28.520373Z","shell.execute_reply":"2026-02-13T23:27:52.533130Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dai risultati si nota che il modello ha buone prestazioni per la classe 0 e discrete per la classe 1.\nDalla differenza tra _Precision_ e _Recall_ si deduce che il modello ha più difficoltà nel riconoscere quando un'instanza **non è** di classe 1 rispetto a quando effettivamente lo è.\n\nSimilmente si può dire il contrario della classe 2, ovvero che gli è più difficile riconoscere quando **è** di classe 2 rispetto a quando non lo è. Infine, i valori della classe 3 indicano che il modello non l'ha predetta mai.\n\nComplessivamente lo sbilanciamento delle classi e la ridotta quantità di dati sembrano avere un effetto pronunciato sulle prestazioni del modello.","metadata":{}},{"cell_type":"markdown","source":"## LightGBM","metadata":{}},{"cell_type":"markdown","source":"Per questo modello non sarà necessaria né l'imputazione né l'encoding, perchè è in grado di gestirli entrambi.","metadata":{}},{"cell_type":"markdown","source":"### Valutazione","metadata":{}},{"cell_type":"markdown","source":"Per il [confronto](#Confronto) si vuole impostare `importance_type` a `\"gain\"` in modo che le feature importance siano associate alla somma dei loro gain, e non al numero di volte che sono usate (che sarebbe l'impostazione di default, `\"split\"`).","metadata":{}},{"cell_type":"code","source":"model = LGBMRegressor(importance_type=\"gain\", random_state=RANDOM_SEED, n_jobs=N_JOBS, verbosity=0)\nqwks_lgbm, imps_lgbm, counts, report = cv(model, whole_X, whole_y)\nprint(report)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:27:52.534553Z","iopub.execute_input":"2026-02-13T23:27:52.534752Z","iopub.status.idle":"2026-02-13T23:28:11.319720Z","shell.execute_reply.started":"2026-02-13T23:27:52.534737Z","shell.execute_reply":"2026-02-13T23:28:11.319078Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Anche in questo caso si possono notare delle difficoltà simili a quelle della Random Forest.\n\nIl _Recall_ per la classe 2 risulta migliorato abbastanza significativamente, mentre la _Precision_ della classe 3 suggerisce che quelle pochissime volte che è stata scelta (come indicato dal _Recall_) la previsione era corretta.\n\nIn ogni caso non si può dire che il modello sia migliorato sulla classe 3, anche se in generale sembra tendere leggermente verso le classi meno frequenti.","metadata":{}},{"cell_type":"markdown","source":"## Confronto","metadata":{}},{"cell_type":"markdown","source":"Durante la Cross-Validation di entrambi i modelli sono state salvate le _feature importance_. \\\nDato che i due modelli sono diversi il significato dell'importanza è anch'esso diverso, da come si può notare nella differenza tra le scale dei valori. In ogni caso, è comunque interessante analizzare la relazione tra le feature di ogni modello:","metadata":{}},{"cell_type":"code","source":"def importance_boxplot(importance, model, plot):\n    sorted_importance = importance[importance.median().sort_values().tail(30).index]\n    sorted_importance_names = ellipses_columns(sorted_importance.columns)\n    plot.boxplot(sorted_importance, labels=sorted_importance_names, vert=False)\n    plot.set_ylabel(\"Colonna\")\n    plot.set_xlabel(\"Importanza\")\n    plot.set_title(model)\n\n_, plts = plt.subplots(1, 2, figsize=(10, 6))\nimportance_boxplot(imps_rf, \"Random Forest\", plts[0])\nimportance_boxplot(imps_lgbm, \"LightGBM\", plts[1])\nplt.suptitle(\"Importanza delle feature\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:28:11.320810Z","iopub.execute_input":"2026-02-13T23:28:11.321402Z","iopub.status.idle":"2026-02-13T23:28:12.155899Z","shell.execute_reply.started":"2026-02-13T23:28:11.321384Z","shell.execute_reply":"2026-02-13T23:28:12.155185Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"L'importanza delle feature del modello LightGBM sembra avere una distribuzione più morbida rispetto a Random Forest che ha una differenza di importanza abbastanza brusca tra le prime due feature. Ciò suggerisce che LightGBM abbia trovato una maggiore relazione con `sii` rispetto a Random Forest.\n\nIn ogni caso, entrambi i modelli fanno principalmente affidamento a `Basic_Demos-Age`, cosa che sarà ulteriormente consolidata nel confronto che segue.","metadata":{}},{"cell_type":"markdown","source":"Anche il numero corretto di previsioni per istanza è stato salvato, in modo da confrontare le distribuzioni dei dati appartenenti alle istanze indovinate più volte contro quelle non indovinate mai. Di seguito sono riportate alcune delle feature con le differenze più pronunciate. \\\nIn questo caso le distribuzioni riguardano il modello LightGBM, ma sono comunque molto simili a quelle della Random Forest.","metadata":{}},{"cell_type":"code","source":"def column_hist(most_correct, most_wrong, column, plot):\n    _, bins = np.histogram(most_correct[column][~most_correct[column].isna()], bins=\"auto\")\n    plot.hist(most_wrong[column], alpha=0.5, bins=bins, density=True, label=\"Più errate\")\n    plot.hist(most_correct[column], alpha=0.5, bins=bins, density=True, label=\"Più corrette\")\n    plot.set_xlabel(\"Valore\")\n    plot.set_ylabel(\"Densità\")\n    plot.set_title(column)\n    plot.legend()\n\nmost_correct = counts[counts[\"Count\"] == counts[\"Count\"].max()]\nmost_wrong = counts[counts[\"Count\"] == counts[\"Count\"].min()]\n\n_, plts = plt.subplots(2, 3, figsize=(11, 6))\ncolumn_hist(most_correct, most_wrong, \"Basic_Demos-Age\", plts[0, 0])\ncolumn_hist(most_correct, most_wrong, \"Physical-Height\", plts[0, 1])\ncolumn_hist(most_correct, most_wrong, \"Physical-Weight\", plts[0, 2])\ncolumn_hist(most_correct, most_wrong, \"BIA-BIA_Frame_num\", plts[1, 0])\ncolumn_hist(most_correct, most_wrong, \"PreInt_EduHx-computerinternet_hoursday\", plts[1, 1])\ncolumn_hist(most_correct, most_wrong, \"sii\", plts[1, 2])\nplt.suptitle(\"Distribuzioni dei valori delle colonne rispetto alle previsioni di sii\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:28:12.156631Z","iopub.execute_input":"2026-02-13T23:28:12.156842Z","iopub.status.idle":"2026-02-13T23:28:13.198207Z","shell.execute_reply.started":"2026-02-13T23:28:12.156814Z","shell.execute_reply":"2026-02-13T23:28:13.197395Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dai grafici si può intuire che le istanze più giuste sono quelle che riguardano soggetti di minore età, cha avranno quindi anche il peso, la grandezza corporea e altri campi ridotti, e che comprensibilmente possiendono anche un valore di `PreInt_EduHx-computerinternet_hoursday` più basso.\n\nTale collegamento si riflette anche sui valori di `sii`, con cui si può ipotizzare che i soggetti più giovani hanno una dipendenza da Internet meno severa, considerato anche che lo usano meno tempo ogni giorno. Al contrario, quando l'età è maggiore sembrerebbe che il modello non riesca a categorizzare tale dipendenza.\n\nQuesto sembra anche spiegare l'[elevata importanza](##Confronto) di `Basic_Demos-Age`, perchè la maggior parte degli `sii` [tendono verso `0`](#Divisione-in-X-e-y) e quindi riesce a fare molte previsioni corrette solamente grazie all'età del soggetto.","metadata":{}},{"cell_type":"markdown","source":"Confrontando i modelli con il _QWK_ si nota che in pratica sono molto simili, ma LightGBM sembra essere un po' più consistente verso score leggermente più alti:","metadata":{}},{"cell_type":"code","source":"plt.boxplot([qwks_rf, qwks_lgbm], labels=[\"Random Forest\", \"LightGBM\"])\nplt.xlabel(\"Modello\")\nplt.ylabel(\"QWK\")\nplt.title(\"Confronto QWK\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:28:13.198956Z","iopub.execute_input":"2026-02-13T23:28:13.199151Z","iopub.status.idle":"2026-02-13T23:28:13.337393Z","shell.execute_reply.started":"2026-02-13T23:28:13.199136Z","shell.execute_reply":"2026-02-13T23:28:13.336279Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Previsione","metadata":{}},{"cell_type":"markdown","source":"La previsione di Kaggle è effettuata con LightGBM:","metadata":{}},{"cell_type":"code","source":"model = LGBMRegressor(random_state=RANDOM_SEED, n_jobs=N_JOBS)\nmodel.fit(whole_X, whole_y)\nbins = optimize_categories(model.predict(whole_X), whole_y)\n\npred_y = categorize(model.predict(kaggle_X), bins)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:28:13.338259Z","iopub.execute_input":"2026-02-13T23:28:13.338471Z","iopub.status.idle":"2026-02-13T23:28:14.319009Z","shell.execute_reply.started":"2026-02-13T23:28:13.338455Z","shell.execute_reply":"2026-02-13T23:28:14.318286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = pd.DataFrame({\n    \"id\": kaggle_ids,\n    \"sii\": pred_y.astype(\"int\")\n}).to_csv(index=False).strip()\n\nif os.path.isdir(\"/kaggle/working\"):\n    with open(\"/kaggle/working/submission.csv\", \"w\") as file:\n        file.write(submission)\n\nprint(submission)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T23:28:14.319630Z","iopub.execute_input":"2026-02-13T23:28:14.319861Z","iopub.status.idle":"2026-02-13T23:28:14.329717Z","shell.execute_reply.started":"2026-02-13T23:28:14.319845Z","shell.execute_reply":"2026-02-13T23:28:14.328920Z"}},"outputs":[],"execution_count":null}]}