Skip to content

Build the sparse block of from_pandas directly from pandas sparse columns - #542

Open
abelianbee wants to merge 5 commits into
Quantco:mainfrom
abelianbee:sparse-from-pandas-sparsearray
Open

abelianbee wants to merge 5 commits into
Quantco:mainfrom
abelianbee:sparse-from-pandas-sparsearray

Conversation

@abelianbee

Copy link
Copy Markdown
Contributor

Follows up on points 2 and 3 of the discussion in #537. xref #378.

from_pandas built its sparse block with sps.coo_matrix(df[:, sparse_dfidx]), which materializes the whole block densely before converting. For pandas sparse columns that is O(n * k) time and memory on data that is O(nnz). This assembles the CSC from each column's sp_index and sp_values instead, and calls eliminate_zeros() since pandas sparse arrays can hold explicit zeros.

from_pandas end to end, median of 5 after a warmup, M1 Max:

n p density before after peak heap before after
500,000 50 0.01 0.110 s 0.006 s 208 MB 3.0 MB
1,000,000 50 0.005 0.236 s 0.006 s 416 MB 3.0 MB

Both shapes have the same nnz and now take the same time.

On the fill value: SparseMatrix only represents a fill of 0, and nothing checked that, so a sparse column with another fill went into sparse storage with every row explicitly stored. Those columns are logically dense and now go to dense storage with a warning. I went with a warning rather than an error because pandas' default fill for a float sparse array is NaN, so .astype("Sparse") on a float column would otherwise start raising. Happy to make it an error if you would rather.

Checklist

  • Added a CHANGELOG.rst entry

@stanmart Martin Stancsics (stanmart) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comments aside looks great! Thank you, very valuable improvement.

Comment thread src/tabmat/constructor.py Outdated
else:
# SparseMatrix only represents zero fill. A sparse array with
# any other fill value is logically dense, so store it that way.
warnings.warn(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be slightly more elegant to collect all non-sparsified columns first and emit a single warning that lists them instead of possibly dozens of separate warnings.

Comment thread src/tabmat/constructor.py Outdated
Comment thread CHANGELOG.rst Outdated
@abelianbee

Copy link
Copy Markdown
Contributor Author

Thanks for the review, all three are in.

Batched the warning: the non-zero-fill columns are collected during the column loop and reported in one warning after it, the same way ignored_cols already works. The message lists each column with its fill value. Added a test that three such columns produce exactly one warning.

Took the other two suggestions as-is.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the follow-up, lgtm! Let's wait for CI but we can merge as soon as it's green.

@abelianbee

Copy link
Copy Markdown
Contributor Author

Pushed a fix for the nightly failure. It was the test, not the change: it built its input by multiplying two sparse arrays to get explicitly stored zeros, and newer pandas prunes those, so the setup assertion failed. It now builds the column with SparseArray.from_spmatrix from a scipy matrix that carries a stored zero, which is deterministic across versions, and additionally checks that the genuine non-zeros survive.

The new run needs workflow approval before it can go green.

@stanmart

Copy link
Copy Markdown
Collaborator

There is a trivial conflict in the changelog due to merging the other PR, but looks good!

abelianbee and others added 5 commits September 21, 2026 10:37
…umns

sps.coo_matrix(df[:, sparse_dfidx]) materialized the whole block densely before
converting, so constructing a SparseMatrix from pandas sparse columns cost
O(n * k) time and memory on data that is O(nnz). Assemble the CSC from each
column's sp_index and sp_values instead, and eliminate the explicit zeros that
pandas sparse arrays can store.

SparseMatrix only represents a fill value of 0. Sparse columns with any other
fill value are logically dense, so they are now stored as dense with a warning
instead of being pushed into sparse storage with every row explicitly stored.
Co-authored-by: Martin Stancsics <martin.stancsics@gmail.com>
Co-authored-by: Martin Stancsics <martin.stancsics@gmail.com>
Collect the columns whose sparse dtype has a non-zero fill value during the
column loop and warn once after it, matching how ignored_cols is already
handled, instead of emitting one warning per column.
The test relied on pandas keeping explicit zeros when multiplying two sparse
arrays. Newer pandas prunes them, so the setup assertion failed on the nightly
job. Use SparseArray.from_spmatrix, which stores them deterministically, and
also assert that the genuine non-zeros survive.
@abelianbee
abelianbee force-pushed the sparse-from-pandas-sparsearray branch from 70cad7e to 7ed2c56 Compare September 21, 2026 14:39
@abelianbee

Copy link
Copy Markdown
Contributor Author

Rebased on main, both changelog entries kept.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants