diff --git a/examples/01_featureset_basics.ipynb b/examples/01_featureset_basics.ipynb index 77666ff..511584b 100644 --- a/examples/01_featureset_basics.ipynb +++ b/examples/01_featureset_basics.ipynb @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "669a024e", "metadata": {}, "outputs": [ @@ -72,7 +72,7 @@ "\n", "import modularml as mml\n", "\n", - "DATA_URL = Path(\"https://raw.githubusercontent.com/REIL-UConn/fine-tuning-for-rapid-soh-estimation/main/processed_data/UConn-ILCC-NMC/data_slowpulse_1.pkl\")\n", + "DATA_URL = \"https://raw.githubusercontent.com/REIL-UConn/fine-tuning-for-rapid-soh-estimation/main/processed_data/UConn-ILCC-NMC/data_slowpulse_1.pkl\"\n", "DATA_DIR = Path(\"downloaded_data\")\n", "DATA_PATH = DATA_DIR / \"data_slowpulse_1.pkl\"\n", "DATA_DIR.mkdir(exist_ok=True, parents=True)\n", diff --git a/examples/04_feature_importance.ipynb b/examples/04_feature_importance.ipynb new file mode 100644 index 0000000..4c50d0f --- /dev/null +++ b/examples/04_feature_importance.ipynb @@ -0,0 +1,673 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3772275a", + "metadata": {}, + "source": [ + "---\n", + "# 04_featureset_importance.ipynb\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "3c42e4df", + "metadata": {}, + "source": [ + "## Introduction: What is Feature Importance Analysis?\n", + "\n", + "**Feature importance analysis** helps reveal how different inputs affect model predictions.\n", + "\n", + "It is useful for:\n", + "\n", + "* **Interpretability** – seeing which variables drive outcomes.\n", + "* **Diagnostics** – identifying redundant or weak features that may add noise.\n", + "* **Domain insights** – revealing which physical, statistical, or operational descriptors are most relevant for the task\n", + "\n", + "We focus on two main approaches:\n", + "\n", + "* **Model-based**: importance derived from model internals (e.g., tree importances, coefficients).\n", + "* **Perturbation-based**: measuring prediction changes when features are shuffled or masked.\n", + "\n", + "In this notebook, we’ll show how to:\n", + "\n", + "* Extract statistical features from battery voltage data.\n", + "* Evaluate and visualize feature importance to understand which features contribute most.\n" + ] + }, + { + "cell_type": "markdown", + "id": "8611971a", + "metadata": {}, + "source": [ + "## Step 1: Loading Example Data\n", + "\n", + "We begin by downloading an example dataset from our battery health estimation paper: “Fine-tuning for rapid capacity estimation of lithium-ion batteries” ([10.1016/j.ensm.2025.104425](https://doi.org/10.1016/j.ensm.2025.104425)).\n", + "This dataset contains time-series voltage responses to 100-second DC pulses collected across the life of several lithium-ion cells.\n", + "More information on the dataset and usage can be found on the following GitHub repository: [REIL-UConn/fine-tuning-for-rapid-soh-estimation]{https://github.com/REIL-UConn/fine-tuning-for-rapid-soh-estimation.git}.\n", + "\n", + "\n", + "We'll download and load the data from GitHub. \n", + "The file is a `.pkl` (pickled Python object) containing a dictionary of key-value arrays." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "669a024e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data already downloaded.\n", + "Available keys: ['cell_id', 'group_id', 'rpt', 'num_cycles', 'soc', 'soc - coulomb', 'pulse_type', 'voltage', 'q_dchg', 'soh', 'dcir_chg_10', 'dcir_dchg_10', 'dcir_chg_20', 'dcir_dchg_20', 'dcir_chg_30', 'dcir_dchg_30', 'dcir_chg_40', 'dcir_dchg_40', 'dcir_chg_50', 'dcir_dchg_50', 'dcir_chg_60', 'dcir_dchg_60', 'dcir_chg_70', 'dcir_dchg_70', 'dcir_chg_80', 'dcir_dchg_80', 'dcir_chg_90', 'dcir_dchg_90']\n", + "Total number of samples: 24048\n" + ] + } + ], + "source": [ + "import pickle\n", + "import urllib.request\n", + "import warnings\n", + "from pathlib import Path\n", + "import modularml as mml\n", + "\n", + "DATA_URL = \"https://raw.githubusercontent.com/REIL-UConn/fine-tuning-for-rapid-soh-estimation/main/processed_data/UConn-ILCC-NMC/data_slowpulse_1.pkl\"\n", + "DATA_DIR = Path(\"downloaded_data\")\n", + "DATA_PATH = DATA_DIR / \"data_slowpulse_1.pkl\"\n", + "DATA_DIR.mkdir(exist_ok=True, parents=True)\n", + "\n", + "if not DATA_PATH.exists():\n", + " print(\"Downloading data...\")\n", + " urllib.request.urlretrieve(url=DATA_URL, filename=DATA_PATH)\n", + " print(\"Download complete.\")\n", + "else:\n", + " print(\"Data already downloaded.\")\n", + "\n", + "with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\", category=DeprecationWarning)\n", + " data = pickle.load(Path.open(DATA_PATH, \"rb\"))\n", + "\n", + "print(f\"Available keys: {list(data.keys())}\")\n", + "print(f\"Total number of samples: {len(data[next(iter(data.keys()))])}\")" + ] + }, + { + "cell_type": "markdown", + "id": "65a72afc", + "metadata": {}, + "source": [ + "## Step 2: Understanding the Data Format\n", + "\n", + "Our data is structured as a dictionary, where each key corresponds to a signal or property:\n", + "* `voltage`: the time-series voltage response during a 100-second pulse (shape: [n_samples, 101])\n", + "* `soh`: state-of-health, a float between ~0.5 and 1.0\n", + "* `cell_id`, `group_id`, `pulse_type`, `soc`: metadata about the sample\n" + ] + }, + { + "cell_type": "markdown", + "id": "bfce70c3", + "metadata": {}, + "source": [ + "## Step 3: Creating a raw FeatureSet\n", + "\n", + "To create a `FeatureSet`, we use the `from_dict()` constructor. \n", + "We must assign a `label`, which uniquely identifies this `FeatureSet` within the broader modeling pipeline (e.g., when connecting to model stages)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "33cd6f32", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "FeatureSet(label='PulseFeaturesRaw', n_samples=24048)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from modularml.core import FeatureSet\n", + "\n", + "fs_raw = FeatureSet.from_dict(\n", + " label=\"PulseFeaturesRaw\",\n", + " data={\n", + " \"voltage\": data[\"voltage\"],\n", + " \"soh\": data[\"soh\"],\n", + " \"cell_id\": data[\"cell_id\"],\n", + " \"group_id\": data[\"group_id\"],\n", + " \"pulse_type\": data[\"pulse_type\"],\n", + " \"pulse_soc\": data[\"soc\"],\n", + " },\n", + " feature_keys=\"voltage\",\n", + " target_keys=\"soh\",\n", + " tag_keys=[\"cell_id\", \"group_id\", \"pulse_type\", \"pulse_soc\"],\n", + ")\n", + "fs_raw\n" + ] + }, + { + "cell_type": "markdown", + "id": "05cd3631", + "metadata": {}, + "source": [ + "## Step 3: Derive statistical features from voltage and create a new FeatureSet" + ] + }, + { + "cell_type": "markdown", + "id": "0afbd3a5", + "metadata": {}, + "source": [ + "Helper function: stats on a 1D array-like" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f684ae3f", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "def voltage_stats_1d(x):\n", + " arr = np.asarray(x, dtype=float).ravel()\n", + " arr = arr[~np.isnan(arr)]\n", + " if arr.size == 0:\n", + " return dict(v_min=np.nan, v_max=np.nan, v_mean=np.nan, v_var=np.nan, v_skew=np.nan, v_kurt=np.nan)\n", + " v_min = float(np.min(arr))\n", + " v_max = float(np.max(arr))\n", + " v_mean = float(np.mean(arr))\n", + " v_var = float(np.var(arr, ddof=1)) if arr.size > 1 else 0.0\n", + " try:\n", + " from scipy.stats import skew, kurtosis\n", + " v_skew = float(skew(arr, bias=False)) if arr.size > 2 else 0.0\n", + " v_kurt = float(kurtosis(arr, fisher=True, bias=False)) if arr.size > 3 else 0.0\n", + " except Exception:\n", + " s = pd.Series(arr)\n", + " v_skew = float(s.skew()) if arr.size > 2 else 0.0\n", + " v_kurt = float(s.kurt()) if arr.size > 3 else 0.0\n", + " return dict(v_min=v_min, v_max=v_max, v_mean=v_mean, v_var=v_var, v_skew=v_skew, v_kurt=v_kurt)\n" + ] + }, + { + "cell_type": "markdown", + "id": "d5fa6b2d", + "metadata": {}, + "source": [ + "Grab the voltage sequences from fs_raw" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c698d1c7", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " voltages = fs_raw.data[\"voltage\"] \n", + "except Exception:\n", + " voltages = data[\"voltage\"] " + ] + }, + { + "cell_type": "markdown", + "id": "26849eae", + "metadata": {}, + "source": [ + "Compute stats row-wise" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3ae08ab9", + "metadata": {}, + "outputs": [], + "source": [ + "stats_list = [voltage_stats_1d(v) for v in voltages]\n", + "stats_df = pd.DataFrame(stats_list)" + ] + }, + { + "cell_type": "markdown", + "id": "bba30b6b", + "metadata": {}, + "source": [ + "Build the stats FeatureSet for modeling" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "504b4b66", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "FeatureSet(label='PulseFeaturesStats', n_samples=24048)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "base_df = pd.DataFrame({\n", + " \"soh\": data[\"soh\"],\n", + " \"cell_id\": data[\"cell_id\"],\n", + " \"group_id\": data[\"group_id\"],\n", + " \"pulse_type\": data[\"pulse_type\"],\n", + " \"pulse_soc\": data[\"soc\"],\n", + " })\n", + "df_stats = pd.concat([base_df.reset_index(drop=True), stats_df.reset_index(drop=True)], axis=1)\n", + "\n", + "\n", + "def to_np_str_series(s):\n", + " # elementwise cast guarantees each item is np.str_ \n", + " return np.array([np.str_(x) for x in s.values], dtype=np.str_)\n", + "\n", + "feat_cols = [\"v_min\",\"v_max\",\"v_mean\",\"v_var\",\"v_skew\",\"v_kurt\"]\n", + "target = \"soh\"\n", + "\n", + "data_dict = {\n", + " # features\n", + " \"v_min\": df_stats[\"v_min\"].to_numpy(),\n", + " \"v_max\": df_stats[\"v_max\"].to_numpy(),\n", + " \"v_mean\": df_stats[\"v_mean\"].to_numpy(),\n", + " \"v_var\": df_stats[\"v_var\"].to_numpy(),\n", + " \"v_skew\": df_stats[\"v_skew\"].to_numpy(),\n", + " \"v_kurt\": df_stats[\"v_kurt\"].to_numpy(),\n", + " # target\n", + " \"soh\": df_stats[\"soh\"].to_numpy(),\n", + " # tags — force NumPy string scalars where needed\n", + " \"cell_id\": to_np_str_series(df_stats[\"cell_id\"]),\n", + " \"group_id\": df_stats[\"group_id\"].to_numpy(), \n", + " \"pulse_type\": to_np_str_series(df_stats[\"pulse_type\"]),\n", + " \"pulse_soc\": df_stats[\"pulse_soc\"].to_numpy(),\n", + "}\n", + "\n", + "fs_stats = FeatureSet.from_dict(\n", + " label=\"PulseFeaturesStats\",\n", + " data=data_dict,\n", + " feature_keys=feat_cols,\n", + " target_keys=target,\n", + " tag_keys=[\"cell_id\",\"group_id\",\"pulse_type\",\"pulse_soc\"],\n", + ")\n", + "fs_stats" + ] + }, + { + "cell_type": "markdown", + "id": "9ca151a6", + "metadata": {}, + "source": [ + "#### Filtering charge samples\n", + "\n", + "The `.filter` method takes keyword arguments, where keys can correspond to any attribute of the samples' tags, features, or targets." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "da7d6952", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GraphNode ('filtered')\n", + "Filtered to 12024 charge-only samples.\n" + ] + } + ], + "source": [ + "charge_samples = fs_stats.filter(pulse_type=\"chg\")\n", + "print(charge_samples)\n", + "print(f\"Filtered to {len(charge_samples.samples)} charge-only samples.\")" + ] + }, + { + "cell_type": "markdown", + "id": "27050472", + "metadata": {}, + "source": [ + "Note that the `.filter` method returns a new `FeatureSet` containing copies of the filtered samples.\n", + "\n", + "By default, it is returned with a label of `'filtered'`, but we can set a new label with `.set_label`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b609296b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GraphNode ('ChargePulseFeatures')\n" + ] + } + ], + "source": [ + "charge_samples.label = \"ChargePulseFeatures\"\n", + "print(charge_samples)" + ] + }, + { + "cell_type": "markdown", + "id": "b5dab074", + "metadata": {}, + "source": [ + "## Step 4: Running feature importance\n", + "Using both model-based (Random Forest) and perturbation-based (permutation) importance\n" + ] + }, + { + "cell_type": "markdown", + "id": "dfc72e72", + "metadata": {}, + "source": [ + "1) Setup: imports and reproducible config" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "663d0904", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from sklearn.model_selection import GroupShuffleSplit\n", + "\n", + "\n", + "RANDOM_STATE = 42\n", + "feat_cols = [\"v_min\",\"v_max\",\"v_mean\",\"v_var\",\"v_skew\",\"v_kurt\"]\n", + "target_col = \"soh\"\n", + "group_tag = \"group_id\" \n" + ] + }, + { + "cell_type": "markdown", + "id": "b1cd79f2", + "metadata": {}, + "source": [ + "2) Build X, y, and group labels from FeatureSet" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "ce092a6b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(12024, 6) (12024,) (12024,)\n" + ] + } + ], + "source": [ + "def _scalar(x):\n", + " arr = np.asarray(x).ravel()\n", + " return float(arr[0]) if arr.size else np.nan\n", + "\n", + "def arrays_from_featureset(fs, feature_keys, target_key, group_key):\n", + " X_list, y_list, g_list = [], [], []\n", + " for s in fs.samples:\n", + " # build one feature row\n", + " row = []\n", + " bad = False\n", + " for k in feature_keys:\n", + " v = _scalar(s.features[k].value)\n", + " if not np.isfinite(v):\n", + " bad = True\n", + " break\n", + " row.append(v)\n", + " if bad:\n", + " continue\n", + "\n", + " # target\n", + " yv = _scalar(s.targets[target_key].value)\n", + " if not np.isfinite(yv):\n", + " continue\n", + "\n", + " # group label \n", + " gv = s.tags[group_key].value\n", + " g_list.append(gv)\n", + " y_list.append(yv)\n", + " X_list.append(row)\n", + "\n", + " X = np.asarray(X_list, dtype=float)\n", + " y = np.asarray(y_list, dtype=float)\n", + " groups = np.asarray(g_list)\n", + " return X, y, groups\n", + "\n", + "# Build arrays from the filtered FeatureSet\n", + "X, y, groups = arrays_from_featureset(charge_samples, feat_cols, target_col, group_tag)\n", + "print(X.shape, y.shape, groups.shape)\n" + ] + }, + { + "cell_type": "markdown", + "id": "6f9af99a", + "metadata": {}, + "source": [ + "3) Group-aware train/test split" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "7329af50", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(9234, 6) (9234,)\n", + "(2790, 6) (2790,)\n" + ] + } + ], + "source": [ + "gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=RANDOM_STATE)\n", + "(train_idx, test_idx), = gss.split(X, groups=groups)\n", + "\n", + "X_tr, X_te = X[train_idx], X[test_idx]\n", + "y_tr, y_te = y[train_idx], y[test_idx]\n", + "\n", + "print(X_tr.shape, y_tr.shape)\n", + "print(X_te.shape, y_te.shape)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "1be929ac", + "metadata": {}, + "source": [ + "4) Compute feature importances (model-based + permutation)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b729cbf9", + "metadata": {}, + "outputs": [], + "source": [ + "from modularml.preprocessing import FeatureImportance" + ] + }, + { + "cell_type": "markdown", + "id": "804dbbdd", + "metadata": {}, + "source": [ + "Constructs a FeatureImportance object configured to use LightGBM" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "453ccd5a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fi = FeatureImportance(\n", + " estimator=\"lgbm\", # choose LightGBM (vs \"rf\" for RandomForest)\n", + " n_estimators=400, # number of trees for LightGBM\n", + " learning_rate=0.05, # shrinkage for boosting\n", + " random_state=42, # reproducibility\n", + " n_jobs=-1 # use all CPU cores\n", + ")\n", + "fi.fit(X, y, feat_cols, groups=groups)" + ] + }, + { + "cell_type": "markdown", + "id": "a915ddd5", + "metadata": {}, + "source": [ + "**Model-based importance** computed from the average reduction in MSE each feature provides across all splits and trees. Larger values mean the feature is frequently used in informative splits (often near the top of trees)." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "15b77e1b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.000342 seconds.\n", + "You can set `force_col_wise=true` to remove the overhead.\n", + "[LightGBM] [Info] Total Bins 1530\n", + "[LightGBM] [Info] Number of data points in the train set: 12024, number of used features: 6\n", + "[LightGBM] [Info] Start training from score 82.529598\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk4AAAGGCAYAAACNCg6xAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAP7BJREFUeJzt3QmcVfP/x/FP+0a7NpKI9oX4JS3aFBItfvQrijZRqEhFUlkiREjxC0lKWULya9+JlKJFiFZaVGq0b+f/eH/9z3XvLHWapmbu3Nfz8bjN3HPO3M79zpl73ue7nQye53kGAACAE8p44k0AAAAgBCcAAICACE4AAAABEZwAAAACIjgBAAAERHACAAAIiOAEAAAQEMEJAAAgIIITAABAQAQnIIZlyJDB+vfvf9I/t27dOvezo0aNOuG2derUsQoVKlg0OZn3ByC2EJyAVKaTs07SeixYsCDBet0VqXjx4m79DTfckCr7iMTNmTPH/V4++OADi1Zjx461F198MbV3A4gaBCcgjciePbs7icU3d+5c27Rpk2XLli1V9gvpG8EJODkEJyCNuP766+3999+3I0eOJDixVa1a1YoUKZJq+4b0Z+/evam9C0BUIjgBacR//vMf27Fjh02fPj207NChQ64ZqFWrVkme/B544AHXlKcaqdKlS9tzzz3nmvfCHTx40Lp3727nnHOOnX322XbjjTe6WqzE/Pbbb9auXTsrXLiwe83y5cvbm2++ecrvb8mSJXbVVVdZjhw5rGTJkjZixIiI9Xqv/fr1cyExT548litXLqtVq5bNnj07wWu99957bju9l9y5c1vFihVt6NChEdvs2rXLunXrFiqbUqVK2TPPPGPHjh1LsN0dd9zh/s+8efNa27Zt3bLkUp8xNd/99NNPdtttt7nXVbk/+uij7veyceNGu+mmm9x+Kww///zziTb/jR8/3h5++GG3jcpCvzP9bHwK2yoLlWvBggXd/6nfYTi9v7POOst++eUXF9BVbq1bt3b9zyZPnmzr168PNRdfcMEFJ/X78PuD6bh7/fXX7aKLLnLlfcUVV9g333yTYH9Xr15tt9xyiysT7bOO2UceeeSMHINASsicIq8C4JTphFW9enUbN26cXXfddW7Z//73P9u9e7e1bNnSXnrppYjtdRLWyVQnsvbt21uVKlVs6tSp1rNnT3fieeGFF0LbdujQwcaMGeMCmMLLrFmzrHHjxgn2YevWrXbllVe6E2HXrl3dyU37oNePi4tzQSQ5/vzzT3fC1glTAXHChAl29913W9asWd0JUvT6I0eOdOs7duxof/31l73xxhvWqFEjW7RokXt/omCpberXr++CkPzwww/2xRdf2P333++e79u3z66++mpXDnfddZedf/759uWXX1qfPn1s8+bNoaYplaFCjPqWde7c2cqWLWsTJ0504elU3Xrrre71nn76aRdOnnjiCcufP7+99tprVq9ePbfv7777rj344IMuZNSuXTvi55988kn3e+jVq5dt27bN7XODBg1s2bJlLnD4/ePuvPNO9/ODBg1yvz8FSJXF0qVLXRD0qSZTZVmzZk0XcnLmzOlCmY4vhWj/eFHAOpnfR3jNqLZReWu/Bw8ebM2bN7dff/3VsmTJ4rb5/vvvXfjS806dOrljXmFu0qRJ7v2ezmMQSDEegFT11ltvqXrI++abb7xXXnnFO/vss719+/a5df/+97+9unXruu9LlCjhNW7cOPRzH3/8sfu5J554IuL1br75Zi9DhgzemjVr3PNly5a57e65556I7Vq1auWWP/bYY6Fl7du394oWLept3749YtuWLVt6efLkCe3X2rVr3c9q30/k6quvdts+//zzoWUHDx70qlSp4hUqVMg7dOiQW3bkyBG3PNyff/7pFS5c2GvXrl1o2f333+/lzp3bbZ+Uxx9/3MuVK5f3008/RSzv3bu3lylTJm/Dhg0RZTh48ODQNnrdWrVqBXp/s2fPdtu9//77oWUqTy3r1KlTxGued9557vfy9NNPR7y/HDlyeG3btk3wmueee64XFxcXWj5hwgS3fOjQoe65yk3lV6FCBW///v2h7T777DO3Xb9+/ULL9Ppapvcfn44pHVvxBf19+MdCgQIFvJ07d4aWf/LJJ275pEmTQstq167tju/169dHvO6xY8dO+hgEUgtNdUAaohqZ/fv322effeau3vU1qWa6zz//3DJlymT33XdfxHI13akmRVfp/nYSf7v4V+76mQ8//NCaNGnivt++fXvooVoG1Ux8++23yXpfmTNndjURPtU06blqUtSEJ3ovWi5qTtu5c6erJbn88ssj/l/VoqiJMrxJM7HmK9Vs5MuXL+J9qMbm6NGjNm/evFDZaN9U++XTftx77712qlTLF/6aeh8q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r9OzZ087cuRIxDZz5syxyy67zLJly2alSpWyUaNGnZH3CAAA0o/Mqfmfz50717p06eLCk4LOww8/bA0bNrRVq1ZZrly5Qtt17NjRBg4cGHqugOQ7evSoC01FihSxL7/80jZv3mxt2rSxLFmy2FNPPeW2Wbt2rdtGgevdd9+1mTNnWocOHaxo0aLWqFGjM/yukdIu6D3ZYtW6pxun9i4AQExJ1eA0ZcqUiOeqBVKN0ZIlS6x27doRQUnBKDHTpk1zQWvGjBlWuHBhq1Klij3++OPWq1cv69+/v2XNmtVGjBhhJUuWtOeff979TNmyZW3BggX2wgsvEJwAAEB09nHavXu3+5o/f/6I5aolKliwoFWoUMH69Olj+/btC61buHChVaxY0YUmn8JQXFycrVy5MrRNgwYNIl5T22g5AABAVNQ4hTt27Jh169bNatSo4QKSr1WrVlaiRAkrVqyYff/9964mSf2gPvroI7d+y5YtEaFJ/Odad7xtFK72799vOXLkiFh38OBB9/BpOwAAgDQTnNTXacWKFa4JLVynTp1C36tmSf2S6tevb7/88otddNFFp63T+oABA07LawMAgOiVJprqunbtap999pnNnj3bzjvvvONuW61aNfd1zZo17qv6Pm3dujViG/+53y8qqW1y586doLZJ1ByoZkP/sXHjxlN8hwAAID1I1eDkeZ4LTRMnTrRZs2a5DtwnsmzZMvdVNU9SvXp1W758uW3bti20zfTp010oKleuXGgbjaQLp220PDGaskA/H/4AAADImNrNc2PGjLGxY8e6uZzUF0kP9TsSNcdphJxG2a1bt84+/fRTN9WARtxVqlTJbaPpCxSQbr/9dvvuu+9s6tSp1rdvX/faCkCiaQh+/fVXe+ihh2z16tX26quv2oQJE6x79+6p+fYBAECUSdXgNHz4cNcUpkkuVYPkP8aPH+/WayoBTTOgcFSmTBl74IEHrEWLFjZp0qTQa2TKlMk18+mrapBuu+02F67C531STdbkyZNdLVPlypXdtAQjR45kKgIAABA9ncPVVHc8xYsXd5NknohG3X3++efH3UbhbOnSpSe9jwAAAGmqczgAAEA0IDgBAAAERHACAAAIiOAEAAAQEMEJAAAgIIITAABAQAQnAACAgAhOAAAAARGcAAAAAiI4AQAABERwAgAAiIZ71QFIXRf0nmyxat3TjVN7FwBEIWqcAAAAAiI4AQAABERwAgAACIjgBAAAEBDBCQAAICCCEwAAQEAEJwAAgIAITgAAAAERnAAAAAIiOAEAAAREcAIAAAiI4AQAABAQwQkAACAgghMAAEBABCcAAICACE4AAAABEZwAAAACIjgBAAAERHACAAAIiOAEAAAQEMEJAAAgIIITAABAQAQnAACAgAhOAAAAARGcAAAAAiI4AQAABERwAgAACIjgBAAAEBDBCQAAIBqC06BBg+yKK66ws88+2woVKmRNmza1H3/8MWKbAwcOWJcuXaxAgQJ21llnWYsWLWzr1q0R22zYsMEaN25sOXPmdK/Ts2dPO3LkSMQ2c+bMscsuu8yyZctmpUqVslGjRp2R9wgAANKPVA1Oc+fOdaHoq6++sunTp9vhw4etYcOGtnfv3tA23bt3t0mTJtn777/vtv/999+tefPmofVHjx51oenQoUP25Zdf2ttvv+1CUb9+/ULbrF271m1Tt25dW7ZsmXXr1s06dOhgU6dOPePvGQAARK/MqfmfT5kyJeK5Ao9qjJYsWWK1a9e23bt32xtvvGFjx461evXquW3eeustK1u2rAtbV155pU2bNs1WrVplM2bMsMKFC1uVKlXs8ccft169eln//v0ta9asNmLECCtZsqQ9//zz7jX08wsWLLAXXnjBGjVqlCrvHQAARJ801cdJQUny58/vvipAqRaqQYMGoW3KlClj559/vi1cuNA919eKFSu60ORTGIqLi7OVK1eGtgl/DX8b/zXiO3jwoPv58AcAAECaCU7Hjh1zTWg1atSwChUquGVbtmxxNUZ58+aN2FYhSev8bcJDk7/eX3e8bRSI9u/fn2jfqzx58oQexYsXT+F3CwAAolGaCU7q67RixQp77733UntXrE+fPq72y39s3LgxtXcJAADEeh8nX9euXe2zzz6zefPm2XnnnRdaXqRIEdfpe9euXRG1ThpVp3X+NosWLYp4PX/UXfg28Ufi6Xnu3LktR44cCfZHI+/0AAAASDM1Tp7nudA0ceJEmzVrluvAHa5q1aqWJUsWmzlzZmiZpivQ9APVq1d3z/V1+fLltm3bttA2GqGnUFSuXLnQNuGv4W/jvwYAAECar3FS85xGzH3yySduLie/T5L6FakmSF/bt29vPXr0cB3GFYbuvfdeF3g0ok40fYEC0u23326DBw92r9G3b1/32n6tUefOne2VV16xhx56yNq1a+dC2oQJE2zy5Mmp+fYBAECUSdUap+HDh7s+RHXq1LGiRYuGHuPHjw9toykDbrjhBjfxpaYoULPbRx99FFqfKVMm18ynrwpUt912m7Vp08YGDhwY2kY1WQpJqmWqXLmym5Zg5MiRTEUAAACip8ZJTXUnkj17dhs2bJh7JKVEiRL2+eefH/d1FM6WLl2arP0EAABIU6PqAAAA0jqCEwAAQEAEJwAAgIAITgAAAAERnAAAAAIiOAEAAAREcAIAAAiI4AQAABAQwQkAACAgghMAAEBABCcAAICACE4AAAABEZwAAAACIjgBAAAERHACAAAIiOAEAAAQEMEJAADgdAend955x2rUqGHFihWz9evXu2UvvviiffLJJ8l9SQAAgPQXnIYPH249evSw66+/3nbt2mVHjx51y/PmzevCEwAAQHqUrOD08ssv23//+1975JFHLFOmTKHll19+uS1fvjwl9w8AACC6g9PatWvt0ksvTbA8W7Zstnfv3pTYLwAAgPQRnEqWLGnLli1LsHzKlClWtmzZlNgvAACANCdzcn5I/Zu6dOliBw4cMM/zbNGiRTZu3DgbNGiQjRw5MuX3EgAAIFqDU4cOHSxHjhzWt29f27dvn7Vq1cqNrhs6dKi1bNky5fcSAAAgWoOTtG7d2j0UnPbs2WOFChVK2T0DAABID8FJncOPHDliF198seXMmdM95Oeff7YsWbLYBRdckNL7CQAAEJ3B6Y477rB27dq54BTu66+/dn2c5syZk1L7BwBp0gW9J1ssW/d049TeBSB6RtUtXbrUzRoe35VXXpnoaDsAAICYDU4ZMmSwv/76K8Hy3bt3h2YRBwAASG+SFZxq167tph4ID0n6Xstq1qyZkvsHAAAQ3X2cnnnmGReeSpcubbVq1XLL5s+fb3FxcTZr1qyU3kcAAIDorXEqV66cff/993bLLbfYtm3bXLNdmzZtbPXq1VahQoWU30sAAIBonsdJE14+9dRTKbs3AAAA6TE47dq1y91qRTVOx44di1in2icAAID0JlnBadKkSW7WcM0Ynjt3bjfKzqfvCU4AACA9SlYfpwceeMBNgKngpJqnP//8M/TYuXNnyu8lAABAtAan3377ze67777QrVYAAABiQbKa6ho1amSLFy+2Cy+8MOX3CACQrnG7Gm5X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7etGOunKTGI9NMW/ki9durSVLFnSVfFv2rTJmjRpYhdddJH7MFZfsT179nDiT4SCukKnrk4PHDhgZcqUsYceesjVPI0cOdJGjRrltlPZUVvyD//vb9WqVe7kpeaSSpUquX5h48ePtxUrVrh+dn5znZpAVZ6xTGEofNDGn3/+ae3bt7cuXbq4bgmiY+yss86yefPmWfbs2e22225zNSpI6K+//nK17Cof9Uv0KTwNGDDANRlroMIvv/ySqvuZJukmv0h/fv75Z++6667zrr/+eu/RRx/1brnlFq948eJe48aNvaNHj6b27qUZK1eu9Hbs2OG+/9///udVqFDBGz16tPfSSy95+fLlc2VYuHBh77XXXqPc4undu7dXunRpr2DBgl7t2rW9Dh06eH/99Zdbt2zZMu/222/3atWq5Q0bNiy1dzVNeuqpp7wbbrjBa968ubd3796IdQsWLHB/r6VKlfKuvPJK76KLLvIOHz7sxbLnnnvOO3DggPveL4uffvrJq1y5snf55Zd7mzZtcsuOHTvmvu7Zs8c777zzvNtuuy0V9zpt++9//+udc8453kMPPeStX78+Yt2oUaO8Vq1a8bmXCIJTlPv999+9nTt3ug8JCf9wHTRokPtA8emk5n+o+F9j2aeffuqVKVPGa9u2rQua0qdPH++xxx5z38+aNcuty5Ahg3fnnXem8t6mLTq2ihQp4s2dO9cdS126dPFy5szpQsDu3bvdNt99950L6nfddRfHm+clOAEpoOvYUjBftWpVgu0VBB588EF3PPp/10eOHPFivdyefPJJ75VXXgl95ik8lShRwqtbt663ZcsWt8w/3vbv3x+TZZYUXST6AdP3wgsveOeee6738MMPexs2bEj05whPkQhOUX7F+q9//curVKmSV6dOHW/dunVu+aFDh9zXsWPHuitV/3ms/xEkdvJ+/fXXvWbNmnm5c+f2xo0b57344ovuA3j58uVu/ebNm705c+bw4Rtm9erVXs2aNb1Jkya551OnTvVy5crltWnTxitbtqyr3fRrnhRI/eON8OSFAqVO6H54V3i65557vG3btoW2SaysYr3GydepUydXZm+++WaC8FSvXr0E4Un4+/W8AQMGeDVq1PDy5MnjdezY0fv8889D64YMGeJq5/r27ev9+uuvqbqf0YDgFKUeeeQRr1ChQt6YMWO8KVOmeFWr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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "imp_model = fi.model_importance(plot=True)" + ] + }, + { + "cell_type": "markdown", + "id": "f68d3f1a", + "metadata": {}, + "source": [ + "**Permutation-based importance** measures how much test performance drops when we randomly shuffle one feature at a time (breaking its relationship to the target). The bar height is the mean drop across repeats; the error bars show variability across shuffles." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "a449662d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.000316 seconds.\n", + "You can set `force_col_wise=true` to remove the overhead.\n", + "[LightGBM] [Info] Total Bins 1530\n", + "[LightGBM] [Info] Number of data points in the train set: 9234, number of used features: 6\n", + "[LightGBM] [Info] Start training from score 82.456995\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\sin22002\\Dropbox\\Git Management\\modular-ml\\.venv\\Lib\\site-packages\\sklearn\\utils\\validation.py:2749: UserWarning: X does not have valid feature names, but LGBMRegressor was fitted with feature names\n", + " warnings.warn(\n", + "c:\\Users\\sin22002\\Dropbox\\Git Management\\modular-ml\\.venv\\Lib\\site-packages\\sklearn\\utils\\validation.py:2749: UserWarning: X does not have valid feature names, but LGBMRegressor was fitted with feature names\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk4AAAGGCAYAAACNCg6xAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAARZJJREFUeJzt3QncTOX///GPfSlb9rRQRJYkUlKUlEq0p5USqSjRRjvfLKlEEknyKyUSWhQhomhBSqTdlj1x24Xzf7yv/meauRfOcM8999zzej4ew8yZc89cc82Zcz7nWj4nl+d5ngEAAOCgch98FQAAAAiBEwAAQEAETgAAAAEROAEAAARE4AQAABAQgRMAAEBABE4AAAABETgBAAAEROAEAAAQEIETEAfLli2zXLly2ciRI+NdFGQTY8eOtaOOOsq2bdsWs/eoWLGi3XLLLaHHkydPtiOPPNI2bNgQs/cEchoCJ2QKBQAKBPxbwYIF7aSTTrJOnTrZunXrLBEtWbLEnnzySRfkHKq33nrLBgwYYNmJDpw6WCaqHTt2uO9l5syZllPs27fPnnjiCbv77ruz9Lu56KKLrHLlytanTx+L9zYZvv8oUKCA2388/vjjtmvXroh1H3roIWvYsKG7nX766TZnzpxMKcPu3bvdax999NFWqFAhO+OMM2zq1KmB//7tt9+20047ze37Spcubbfddptt3Lgx3XW1T+zQoYNVqFDBra+AVuuH0zYeXifh+1bEV944vz9ymJ49e1qlSpXczu7zzz+3IUOG2EcffWQ//PCDFS5c2BItcOrRo4ede+65bsd2qIGTPvu9994bsfz444+3nTt3Wr58+TKptMlDgZO+F9F3kxN88MEH9tNPP9ntt9+e5e+tA/j999/v6rRIkSIWLwqWhg8f7u5v2bLF3nvvPfvf//5nv/32m7355puh9RRgPP300+6+TkpuvPFG++OPPzIleBs3bpz7rVapUsWdDF5yySU2Y8YMO/vssw/4t9rP3XXXXXb++edb//79bdWqVTZw4ECbN2+effXVVxHBzsqVK13QJ3fccYcLnlavXm1ff/11hq8dHkznyZPnsD8rDpMu8gscrtdee00Xi/a++eabiOVdu3Z1y996663Dfo/t27d7Wemdd95xZZ8xY8Yhv0bz5s29448/3stO2rRp4x1xxBFeotm3b5+3c+dOb8OGDe57eeKJJ7ycomXLlt7ZZ58d8/fRtqjvP9y6deu8PHnyeK+++qqXnbbJ/fv3e2eeeaaXK1cub+3aten+Xf/+/b0TTjjhsN//q6++ctvUM888E1qmbe3EE0/0GjRocMC/3b17t1e8eHGvUaNGrsy+Dz74wL3mCy+8ELH+xRdf7FWqVMnbuHHjAV9X27f+Xts7she66hBTTZo0cf+HnxGOGjXK6tat65rDNabjuuuuc2dh4dSSULNmTZs/f741atTItVY9/PDDobFBzz77rA0ePNhOOOEE99yFF17oXsPzPHeWeswxx7jXv+yyy2zTpk0Rr62/VzP4gcZ/6GzzmmuucffPO++8UDO53z2ks+HmzZu7Zn2dKZ944onufdXlEv4ZJk2aZMuXLw/9vd9yldEYp08//dTOOeccO+KII6x48eKu/D/++GO6Tfi//vqrK6/WK1asmN16662uNeZQqFyXXnqp+3z16tVzdVerVq3Q5x0/frx7rDNnfXfffvttut1/v//+uzVr1syVX3WjFkh9J+G2b99u9913nx177LGu7qpWreq+z9Tr6TOqq1etDTVq1HDrDh061HWDiFpI/Hr1v8/vv//elUXbhcparlw5a9u2rf3111+HVYfaZuvXr++2tRIlSrht8pNPPolY5+OPPw59d2q50faxePHig9a9Wmc11qhp06YRy7X9a9tLbf/+/a6V4uqrrw4tU/2dddZZVrJkSffd6TtS60kQZcqUsVNOOcVt09mJvh+19Gi70HaVmlpy9T2qhedwqa7UkhPe4qftR61bc+fOTbN/Sl2OzZs3W6tWrVyZffo96TehLjzf0qVL3XbywAMPuO9K3/0///xzwLLp86ekpKT5fSB+6KpDTKmZXbSTkF69etljjz1m1157rbVr184NSh00aJA7EOlgrAOYTwe7iy++2AVWN910k5UtWzb0nA6me/bscWNCFBj169fPvaYCNR3sNVZBB0W9trohRowYEVW5VZ577rnHXnjhBRewnXzyyW65/78CHu0Uu3bt6v5XwKPxGNrBPfPMM26dRx55xHU5qNn++eefd8sONH5l2rRp7vPqoK8DgrryVH416y9YsCBNd6E+r7pFNT5Fz6ubQwdBvxsjWqqvG264wXXdqL51MG7RooULVlQH6ooQvZ/eW11LuXP/d+6loFFjZs4880z3fSgY0LidvXv3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P9atWrVqXseOHd306/B0BJoinNF04/AMv6KpxVquFAIHK5emlT/00ENeqVKlXPqAZs2auenP6U3VfuWVV1xmYqVRCJ/C/MUXX7isxpqqf/TRR3sPPvhgaPp++DTnbdu2eTfccIObrq7n/NQE6aUjEKVzaNiwoXvdokWLei1atPCWLFkSKKOw/1n12oeSjkCpA1LT6+m7Odh34L+mpopfeOGFrl7Lli3rypp6Gr+m73fp0sXVW758+dz0ar1W+JTzjN7bp7QJdevWdakOwlMTKO2FUgeovpUK4JprrvFWr16dJn1BtHU4YsQIr06dOl6BAgW8EiVKuO1z6tSpEevoe9e2pPctWLCgm15+yy23ePPmzfMOZvz48W5K+4oVK9J9XtuEytWuXbt0n1fWb9Wjyqffkz6H/xnDpbeNDxkyxH1fKSkpXnbMZq9tSr+/9NIoZCZlCr///vu9cuXKuXo8/fTTvcmTJ6dZz0+VEu7DDz/06tev7xUpUsTVpfYNY8eOzfC9lCJDqVP0PvqdKO1A6vrXd129enX3mvqdVK5c2e234vk94V+59E88AzcAic+/zpc/3RrRUTeNWtHUuqTWrayk8TLqQkqdfBRA+hjjBABxpi5hddOpiy0rg091rWrwvLqoAARDixOAw0aLE4BkQYsTAABAQLQ4AQAABESLEwAAQEAETgAAAAElXQJMZf9dvXq1S7wY9HIYAAAg59KoJV0KSoltwy/AnJ6kC5wUNKW+2jsAAMDKlSvTXGzbkj1w8i8sqsoJv5gjAABITikpKa5RJfzi4xlJusDJ755T0ETgBAAAfEGG8DA4HAAAICACJwAAgIAInAAAAAIicAIAAAiIwAkAACAgAicAAICACJwAAAACInACAAAIiMAJAAAgIAInAACAgJLukis5yta1/96CKlLu3xsAADgkBE6JbN5rZp/1Db5+425m53WPZYkAAMjRCJwSWb1bzape/N/jvTvNRlz07/22k83yFopcn9YmAAAOC4FTIkvd9bZn+3/3y51ilv+IuBQLAICcisHhAAAAARE4AQAABETgBAAAEBBjnGKgYrdJcXnfQrbLfiz47/2TH59sO+3/P4iDZX2bx+29AQCIFVqcAAAAAiJwAgAACIjACQAAICACJwAAgIAYHJ7AStvfVibX5tDjgrYndL96ruW2y/JHrL/eK24brESWlhEAgJyEwCmB3Zh3ut2bd3y6z71boEeaZQP2XmkD9l6dBSUDACBnInBKYG/uPd+m7qsbeH21OAEAgENH4JTA1O22waPrDQCArMLgcAAAgIAInAAAAAIicAIAAAiIwAkAACAgAicAAIBECJz69Oljp59+uhUpUsTKlCljl19+uf30008H/bt33nnHqlWrZgULFrRatWrZRx99lCXlBQAAyS2ugdNnn31mHTt2tC+//NKmTp1q//zzj1144YW2ffv2DP9mzpw5dv3119ttt91m3377rQu2dPvhhx+ytOwAACD55PI8z7NsYsOGDa7lSQFVo0aN0l2nVatWLrD68MMPQ8vOPPNMO/XUU23o0KEHfY+UlBQrVqyYbdmyxYoWLWqxULHbJEt2y/o2j3cRAAAIJJrYIFuNcVKB5aijjspwnblz51rTpk0jljVr1swtT8/u3btdhYTfAAAADkW2CZz2799v9957rzVs2NBq1qyZ4Xpr1661smXLRizTYy3PaByVokj/duyxx2Z62QEAQHLINoGTxjppnNLbb7+dqa/bvXt315Ll31auXJmprw8AAJJHtrhWXadOndyYpVmzZtkxxxxzwHXLlStn69ati1imx1qengIFCrgbAABAQrc4aVy6gqYJEybYp59+apUqVTro3zRo0MCmT58esUwz8rQcAAAgx7Y4qXvurbfesvfee8/lcvLHKWksUqFChdz91q1bW4UKFdxYJencubM1btzYnnvuOWvevLnr2ps3b54NGzYsnh8FAAAkgbi2OA0ZMsSNOzr33HOtfPnyoduYMWNC66xYscLWrFkTenzWWWe5YEuBUu3atW3cuHE2ceLEAw4oBwAASPgWpyAppGbOnJlm2TXXXONuAAAASTmrDgAAILsjcAIAAAiIwAkAACAgAicAAICACJwAAAACInACAAAIiMAJAAAgIAInAACAgAicAAAAAiJwAgAACIjACQAAICACJwAAgIAInAAAAAIicAIAAAiIwAkAACAgAicAAICACJwAAAACInACAAAIiMAJAAAgIAInAACAgAicAAAAAiJwAgAACIjACQAAICACJwAAgIAInAAAAAIicAIAAAiIwAkAACAgAicAAICACJwAAAACInACAAAIiMAJAAAgIAInAACAgAicAAAAAiJwAgAAyIrAaffu3ZlXEgAAgJwUOH388cfWpk0bO+GE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Oumkk2zdunVplq9fv97leAIAAMipog6c+vTpY/fcc4+NGzfOddfppvv33nuvG+uUkpISugEAACR1V92ll17q/r/22mstV65c7r7nee7/Fi1ahB7rOc2+AwAASNrAacaMGbEpCQAAQE4LnBo3bhybkgAAAGRzh5SxcteuXfb999+7AeH79++PeE45ngAAAHKiqAOnyZMnW+vWrW3jxo1pnmNcEwAAyMminlV399132zXXXGNr1qxxrU3hN4ImAACQk0UdOCmHU9euXa1s2bKxKREAAEBOCZyuvvpqmzlzZmxKAwAAkJPGOL344ouuq2727NlWq1Yty5cvX8TzSo4JAACQE0UdOI0ePdo++eQTK1iwoGt58pNgiu4TOAEAgJwq6sDpkUcesR49eli3bt0sd+6oe/oAAAASVtSRz549e6xVq1YETQAAIOlEHf20adPGxowZE5vSAAAA5KSuOuVq6tevn02ZMsVOOeWUNIPD+/fvn5nlAwAASNzAadGiRVanTh13/4cffoh4LnygOAAAgCV74DRjxozYlAQAACCbY4Q3AABAZrc4XXnllYHWGz9+fNCXBAAAyJmBU7FixWJbEgAAgJwSOL322muxLQkAAEA2xxgnAACAgAicAAAAAiJwAgAACIjACQAAICACJwAAgIAInAAAAAIicAIAAAiIwAkAACAgAicAAIBECpwGDx5sFStWtIIFC9oZZ5xhX3/9dYbrjhw50nLlyhVx098BAADk+MBpzJgx1rVrV3viiSdswYIFVrt2bWvWrJmtX78+w78pWrSorVmzJnRbvnx5lpYZAAAkp7gHTv3797f27dvbrbfeatWrV7ehQ4da4cKFbcSIERn+jVqZypUrF7qVLVs2S8sMAACSU1wDpz179tj8+fOtadOm/xUod273eO7cuRn+3bZt2+z444+3Y4891i677DJbvHhxFpUYAAAks7gGThs3brR9+/alaTHS47Vr16b7N1WrVnWtUe+9956NGjXK9u/fb2eddZatWrUq3fV3795tKSkpETcAAICE7KqLVoMGDax169Z26qmnWuPGjW38+PFWunRpe/nll9Ndv0+fPlasWLHQTa1UAAAACRc4lSpVyvLkyWPr1q2LWK7HGrsURL58+axOnTr266+/pvt89+7dbcuWLaHbypUrM6XsAAAg+cQ1cMqfP7/VrVvXpk+fHlqm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NmHCBJddHYdGl+i58847412MhPbnn3+6yx0NHjw43kVJKOvXr7errrrKmjVr5q612a9fP3cf0QdNfgD/+++/288//+zucyJ0aMaPH+8uYfbpp5/ali1b4l2chEDglAA++OAD69Chg02aNMmOOeYYt4wm/kOjS/msXr3a1q1bxxlqQPv27Yt47F87cvTo0TZ//vy4lSvRlClTxmbPnu1OgnRtzTlz5lj16tVt79698S5aQvGDpu7du9t5551njRo1sgsvvNCWL18e76IlHNXhLbfcYqNGjXJB/GOPPWaLFi2Kd7GyPQKnBHDcccdZjRo13LX8Pvroo9DOg+ApehUrVnRnpikpKe7/1EEB0sqTJ4/7f9y4ce4C3HLZZZdZvnz57JNPPnGP2RYzFl43Cpq03RUpUsRda1PBe968edPUH0F9WuG/1bFjx7rWY7WUDBkyxJ0IXXrppfb999/HtYyJRCc9CxYssClTpti3337rgqepU6faCy+8QD0mwrXqcGC1a9d21wjSAUxn+aVKlbJbb701FDwxUPzAtCP45ptv3AWiy5cv71qddADTBaL9oCD8gEWTf1rauV577bXWpEkTd51IXZz7rrvucmerCqLUcsK2mFb4RI6HHnrIFi9e7FqdVFc60F988cXuoubh9aYAIfV2mcx2797tro/m18m7775rf/31l6vPq6++2i274IILrHHjxnbDDTe4fWStWrXiXOrsTcHm559/biVKlLDTTz/dLdNvWp588km3D7z77rupxwywl8umdKDXwUqR/65du6xatWr24IMPupan4cOH28iRI9162uFydpqxTZs2uXEQGzdudDvUzp072y+//GLdunWz888/3x599FE3U1Fnr0LQ9K/ULSBVq1a1SpUque6QVatWWYsWLezEE090By6NGdu2bRtBUzr87WnJkiXuQKWukVNOOcWNUxwzZoz98MMPbryY312nLnn9vvEvBUPhExH+/vtvu+2226xjx46uy120/zvyyCNt1qxZVrBgQbvppptcSwoytnXrVteCrHrS2FmfgqcePXq4bmRNXvjtt9/iWs5sSxf5RfbSrVs3r2rVql6pUqW8Ro0aee3atfO2bt3qnlu4cKF38803e+ecc443ePDgeBc14fzyyy/exRdf7F1yySXeY4895l177bXescce6zVv3tzbt29fvIuX7SxevNj766+/3P2PP/7Yq1mzpvf66697L7zwgleiRAlXl2XLlvVefvll6i8DvXv39i699FLvyiuv9LZv3x7x3Oeff+62v8qVK3tnnnmmd+KJJ3r//PNP3Mqa3Tz77LPerl273H2/Xn7++Wevdu3aXr169bxVq1a5Zfv373f/b9u2zTvmmGO8m266KY6lTgyvvPKKV7p0ae/BBx/0li9fHvHcyJEjvRtuuIHfdAYInLKZPn36eOXKlfM+++wztzPo2LGjV7hwYbfT3bJli1vnu+++cwf6Dh06hHYYiLR69Wpv06ZNbkcq4Qcj1bF2uj4FpX49Up//ef/9971q1ap5bdq0cQGndO/e3XviiSfc/U8//dQ9lytXLu/WW2+Nc2mzj9QHGwWaqiMFmEuWLEmzvg7+999/v6tXfzvdu3evl8xS12GvXr28F198MfR7VvB0/PHHe+edd563du3aiN/uzp07k77+0qMTID/Q9D3//PNehQoVvIcffthbsWJFun9H8JQWgVM2snTpUu/ss8/2PvjgA/d4ypQp3hFHHOG1bt3aO/nkk13riN/ypAOZv0FzsE97hl+/fn3vlFNO8c4991xv2bJlbvmePXvc/2+99ZY7s/cf+5J9B5HedjRs2DDviiuu8IoWLeqNHj3aGzBggDtYLVq0yD2/Zs0ab+bMmRyo0qETHB3E/SBUwdNdd93lrV+//oB1TotTWrfffrurvxEjRqQJnpo0aZImeBK2yf/06NHDa9iwoVesWDGvffv23kcffRR6rn///q6V7tFHH/V+//33uJYzURA4ZTNvvvmmay354osvvPLly7suEFH3XO7cuV3Xnb/jkGQ/2Kf2yCOPeGXKlPFGjRrlTZ482atbt6533HHHhQ70/sFe3aDz5s2La1mzk/Dt6I8//vDmz5/v7dixI2LHW6lSJa9Tp07eUUcd5Z1//vne7t27I16DA9V/xo0b55100kne8OHDQ11Nb7/9tjv433fffd6GDRtC6/IbjpRRfaje8uXL57366qsRwdMJJ5zg1apVy7UwIy0NSVBrp1o+Z82a5VWvXt2dUOoE0qcTojx58oSONzgwAqdsQONFunTpErFMTfdt27YNHZyeeuop74ILLnA7D3a06VPXkQKl2bNnh87ydYalA1jJkiVDwZPG7Vx22WXU4/8XfpauwFPdmEWKFHHjl+6+++7Q85988onb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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "perm_df, val_r2 = fi.permutation_importance(val_size=0.2, n_repeats=20, plot=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/modularml/preprocessing/__init__.py b/modularml/preprocessing/__init__.py index e03f099..62f79f1 100644 --- a/modularml/preprocessing/__init__.py +++ b/modularml/preprocessing/__init__.py @@ -44,3 +44,7 @@ "absolute": Absolute, "segmented": SegmentedScaler, } + + +from .feature_importance import FeatureImportance + diff --git a/modularml/preprocessing/feature_importance.py b/modularml/preprocessing/feature_importance.py new file mode 100644 index 0000000..2f60bf7 --- /dev/null +++ b/modularml/preprocessing/feature_importance.py @@ -0,0 +1,77 @@ +# preprocessing/feature_importance.py +from __future__ import annotations +import numpy as np +import pandas as pd +from typing import Optional, Sequence, Tuple, Union +from sklearn.ensemble import RandomForestRegressor +from sklearn.model_selection import GroupShuffleSplit, ShuffleSplit +from sklearn.inspection import permutation_importance as _perm +import lightgbm as lgb + +class FeatureImportance: + def __init__(self, estimator: Union[str, object] = "rf", random_state: int = 42, n_jobs: int = -1, **est_kwargs): + self.estimator_spec = estimator + self.random_state = random_state + self.n_jobs = n_jobs + self.est_kwargs = est_kwargs + self.X = self.y = self.groups = None + self.feature_names: Sequence[str] = () + + def fit(self, X: np.ndarray, y: np.ndarray, feature_names: Sequence[str], groups: Optional[np.ndarray] = None): + self.X = np.asarray(X, float) + self.y = np.asarray(y, float) + self.feature_names = list(feature_names) + self.groups = (np.asarray(groups) if groups is not None else None) + return self + + def model_importance(self, plot: bool = False): + est = self._make_estimator() + est.fit(self.X, self.y) + + # RF / most trees: + imp = getattr(est, "feature_importances_", None) + if imp is None and est.__class__.__name__.lower().startswith("lgbm") and hasattr(est, "booster_"): + # LightGBM (gain) + imp = est.booster_.feature_importance(importance_type="gain") + imp = np.asarray(imp if imp is not None else np.zeros(len(self.feature_names)), float) + + s = pd.Series(imp, index=self.feature_names, name="model_importance").sort_values(ascending=False) + if plot: + import matplotlib.pyplot as plt + x = np.arange(len(s)) + plt.figure(figsize=(6,4)); plt.bar(x, s.values); plt.xticks(x, s.index, rotation=45, ha="right") + plt.ylabel("Importance"); plt.title("Model-based Importance"); plt.tight_layout(); plt.show() + return s + + def permutation_importance(self, val_size: float = 0.2, n_repeats: int = 20, plot: bool = False) -> Tuple[pd.DataFrame, float]: + # train→val split inside training (group-aware if groups given) + if self.groups is not None: + (tr, va), = GroupShuffleSplit(1, test_size=val_size, random_state=self.random_state).split(self.X, groups=self.groups) + else: + tr, va = next(ShuffleSplit(1, test_size=val_size, random_state=self.random_state).split(self.X)) + Xtr, Ytr, Xva, Yva = self.X[tr], self.y[tr], self.X[va], self.y[va] + + est = self._make_estimator(); est.fit(Xtr, Ytr) + val_r2 = float(est.score(Xva, Yva)) + + perm = _perm(est, Xva, Yva, n_repeats=n_repeats, random_state=self.random_state, n_jobs=self.n_jobs) + df = (pd.DataFrame({"feature": self.feature_names, "perm_mean": perm.importances_mean, "perm_std": perm.importances_std}) + .set_index("feature").sort_values("perm_mean", ascending=False)) + + if plot: + import matplotlib.pyplot as plt + x = np.arange(len(df)); y = df["perm_mean"].values; e = df["perm_std"].values + plt.figure(figsize=(6,4)); plt.bar(x, y); plt.errorbar(x, y, yerr=e, fmt="none", capsize=4, ecolor="tab:orange") + plt.xticks(x, df.index, rotation=45, ha="right"); plt.ylabel("Importance (mean)") + plt.title(f"Permutation Importance (val) — R²={val_r2:.3f}"); plt.tight_layout(); plt.show() + return df, val_r2 + + def _make_estimator(self): + if not isinstance(self.estimator_spec, str): + return self.estimator_spec + name = self.estimator_spec.lower() + if name in ("rf", "random_forest", "randomforest"): + return RandomForestRegressor(random_state=self.random_state, n_jobs=self.n_jobs, **self.est_kwargs) + if name in ("lgbm", "lightgbm"): + return lgb.LGBMRegressor(random_state=self.random_state, n_jobs=self.n_jobs, **self.est_kwargs) + raise ValueError("estimator must be 'rf', 'lgbm', or a ready estimator")