Skip to content
Back to skills

Sparse Dense Hstack Lgbm

ASecurity

Train LightGBM directly on a scipy.sparse.hstack of TF-IDF text vectors and dense tabular columns, passing feature_name and categorical_feature so native categorical handling survives the sparse block

  • 61 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 12, 2026
developmentpythongo

Works with

  • cli

Security analysis

A100/100

Scanned September 12, 2026

npx -y skills add wenmin-wu/ds-skills --skill sparse-dense-hstack-lgbm --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sparse Dense Hstack Lgbm?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Sparse Dense Hstack Lgbm
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/wenmin-wu-sparse-dense-hstack-lgbm/badge)](https://www.skillsdirectory.com/skills/wenmin-wu-sparse-dense-hstack-lgbm)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tabular-sparse-dense-hstack-lgbm
description: Train LightGBM directly on a scipy.sparse.hstack of TF-IDF text vectors and dense tabular columns, passing feature_name and categorical_feature so native categorical handling survives the sparse block
---

## Overview

When you have both wide sparse text features (TF-IDF, CountVectorizer — 50k to 500k columns) and dense tabular columns, the wrong move is to densify the sparse block (OOM) or to SVD-compress it (loses signal). LightGBM accepts CSR matrices natively, so you can `scipy.sparse.hstack` the text block and the dense predictors into one CSR matrix and hand it straight to `lgb.Dataset`. The trick is preserving feature names and the categorical-feature flag across the boundary, so LightGBM still uses its native categorical splits on the label-encoded columns living inside the otherwise-sparse matrix.

## Quick Start

```python
import scipy.sparse as sp
import numpy as np
import lightgbm as lgb

X_train = sp.hstack([
    train_desc_tfidf,       # (n, ~100k) sparse
    train_title_counts,     # (n, ~20k) sparse
    train[dense_cols].values,  # (n, d) dense
], format='csr')

feature_names = np.hstack([
    tfidf.get_feature_names_out(),
    cv.get_feature_names_out(),
    dense_cols,
]).tolist()

dtrain = lgb.Dataset(
    X_train, label=y_train,
    feature_name=feature_names,
    categorical_feature=categorical_cols,
)

params = dict(objective='regression', metric='rmse',
              num_leaves=32, max_depth=15, learning_rate=0.02,
              feature_fraction=0.6, bagging_fraction=0.8, bagging_freq=5)

model = lgb.train(params, dtrain, valid_sets=[dtrain, dvalid],
                  num_boost_round=16000, early_stopping_rounds=500)
```

## Workflow

1. Fit each text vectorizer on `train + test` concatenated to share vocabulary
2. Label-encode categorical columns with `fit` on the train+test union (avoids unseen labels)
3. `sp.hstack([sparse_blocks..., dense.values], format='csr')` — CSR is required by LightGBM
4. Build the aligned `feature_name` list by concatenating `get_feature_names_out()` from each block and the dense column names
5. Pass both `feature_name` and `categorical_feature` (as a list of names) to `lgb.Dataset` so native categorical splits still work
6. Train with a low `feature_fraction` (0.3-0.6) to subsample the wide feature space each tree

## Key Decisions

- **CSR, not CSC or COO**: LightGBM converts internally but errors on other layouts. `format='csr'` is mandatory.
- **`feature_fraction` ≤ 0.6**: with 100k+ columns, unrestricted trees slow to a crawl and overfit — subsample every split.
- **Pass categoricals by name, not index**: indices change if the block widths change; names are stable.
- **Fit encoders/vectorizers on train+test union**: unseen categorical labels or TF-IDF tokens at inference break the column alignment.
- **vs. SVD-compressing the text**: SVD loses rare-token signal. Sparse hstack keeps everything and lets the tree choose.
- **Clip predictions to target range**: `np.clip(preds, 0, 1)` for bounded targets like deal probability — LightGBM regression is unconstrained.

## References

- [Aggregated features & LightGBM](https://www.kaggle.com/code/bminixhofer/aggregated-features-lightgbm)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…