skills/tabular/null-importance-feature-selection/SKILL.md
Scores features by comparing actual importances against a null distribution from shuffled targets, removing features that cannot beat random noise.
npx skillsauth add wenmin-wu/ds-skills tabular-null-importance-feature-selectionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Standard feature importance (gain/split) reflects how useful a feature is for the model but doesn't account for noise features that appear useful by chance. Null importance builds a baseline: train the model many times with shuffled targets to create a distribution of "random" importances, then keep only features whose actual importance significantly exceeds their null distribution. Typically improves CV by 0.001–0.005 and reduces overfitting on noisy wide datasets.
import lightgbm as lgb
import numpy as np
import pandas as pd
def get_feature_importances(data, features, target, shuffle=False):
y = target.copy()
if shuffle:
y = y.sample(frac=1.0).reset_index(drop=True)
dtrain = lgb.Dataset(data[features], y, free_raw_data=False)
clf = lgb.train(
{'objective': 'binary', 'boosting_type': 'rf',
'subsample': 0.623, 'colsample_bytree': 0.7,
'num_leaves': 127, 'max_depth': 8, 'bagging_freq': 1},
dtrain, num_boost_round=200
)
imp = pd.DataFrame({'feature': features,
'importance': clf.feature_importance(importance_type='gain')})
return imp
# Actual importances
actual_imp = get_feature_importances(df, feats, df['target'], shuffle=False)
# Null distribution (80 runs)
null_imp = pd.concat([
get_feature_importances(df, feats, df['target'], shuffle=True)
for _ in range(80)
])
# Score: % of null runs where importance < 25th pctl of actual
scores = []
for f in feats:
f_null = null_imp.loc[null_imp['feature'] == f, 'importance'].values
f_act = actual_imp.loc[actual_imp['feature'] == f, 'importance'].values
score = 100 * (f_null < np.percentile(f_act, 25)).sum() / len(f_null)
scores.append((f, score))
selected = [f for f, s in scores if s >= 80]
gain is more discriminative than split for this methodboosting_type='rf') for faster, less correlated runsdata-ai
Scaled Pinball Loss (SPL) metric for evaluating quantile forecasts, normalized by mean absolute successive differences of training data
data-ai
Walk backward through a time series and multiplicatively rescale segments when jumps exceed a fraction of the running mean to correct data collection anomalies
testing
Transform forecasting target to next/current ratio minus one so that optimizing MAE or squared error implicitly minimizes SMAPE
tools
Convert point forecasts to prediction intervals by scaling with logit-transformed quantile ratios passed through a Normal CDF