DEV Community

Pradeep K M
Pradeep K M

Posted on

Stop Deleting "Useless" Features: Why Univariate AUC is Lying to You

In high-stakes domains like BFSI (Banking, Financial Services, and Insurance) and Healthcare, the most valuable predictive signals are often hiding in plain sight.

Many data teams follow a standard pruning pipeline: calculate the univariate AUC for every feature, and if a feature scores near 0.5 (random noise), it gets tossed.

This decision is quietly killing your model's performance.

The Linear Trap vs. The Tree Power
The problem is a confusion between marginal and conditional effects.

The Linear Trap: In a Logistic Regression model, every feature gets a fixed coefficient (
β
β). It measures the effect of that feature in a vacuum. If "Age" has no individual linear relationship with the target across the whole dataset, it gets a high p-value and is zeroed out.

The Tree Power: Gradient Boosting Decision Trees (GBDTs) like XGBoost or CatBoost operate on conditional logic. They don't ask, "Does Age matter?" They ask, "Does Age matter given that Income is low and Education is X?"

In sparse datasets, a feature might be globally irrelevant but locally critical.

Proving it with Code
Here is a simplified demonstration of how a feature can be "useless" on its own but essential when paired with another variable.

import numpy as np
import pandas as pd
from sklearn.metrics import roc_auc_score
from xgboost import XGBClassifier
import shap
import matplotlib.pyplot as plt

# 1. Create a synthetic dataset
np.random.seed(42)
n_samples = 1000

# Feature A: Pure noise
feat_a = np.random.randn(n_samples)

# Feature B: Also looks like noise globally
feat_b = np.random.randn(n_samples)

# Target: Only depends on the INTERACTION of A and B
# Target is 1 ONLY if both A and B are positive (a non-linear pocket)
target = ((feat_a > 0) & (feat_b > 0)).astype(int)

df = pd.DataFrame({'Feature_A': feat_a, 'Feature_B': feat_b, 'Target': target})

# 2. The "Univariate Check" (The mistake)
auc_a = roc_auc_score(df['Target'], df['Feature_A'])
auc_b = roc_auc_score(df['Target'], df['Feature_B'])

print(f"Univariate AUC Feature A: {auc_a:.3f}") # ~0.50
print(f"Univariate AUC Feature B: {auc_b:.3f}") # ~0.50
print("Result: A data scientist might delete both features here! ❌")

# 3. The Boosting Approach
X = df[['Feature_A', 'Feature_B']]
y = df['Target']

model = XGBClassifier(max_depth=3, n_estimators=100)
model.fit(X, y)

# 4. Explain with SHAP
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X)

print("\nSHAP analysis reveals that both features are critical for the interaction.")
shap.summary_plot(shap_values, X)

Enter fullscreen mode Exit fullscreen mode

What is SHAP actually telling us?
When you see a feature with a weak univariate score but a high SHAP value, the model is telling you:

"This variable cannot carry the model alone, but across the forest's decision paths, knowing this variable helped me carve out a high-precision pocket of truth."

The Takeaway for Data Leaders
If you are working with complex, multi-dimensional data in Healthcare or Finance, remember: Global irrelevance
≠

= Local uselessness.

Respect Interactions: Healthcare and BFSI data thrive on combinations, not solo acts.
Audit your Pruning: Before deleting a feature based on a p-value or univariate AUC, check its interaction strength via a tree-based model.
Trust Non-Linear Architectures: Let your boosting models exploit the conditional depth they were designed for.

About the Author:
I specialize in building high-precision predictive models for BFSI and Healthcare, focusing on extracting signal from sparse, non-linear datasets through advanced feature engineering and XAI. I'm currently open to new opportunities where I can help teams turn "noise" into actionable insights.

Top comments (0)