Optimize Dataset.subset() lookups - #686
harshitgupta31415 wants to merge 3 commits into
Conversation
|
Please comment with a benchmark for this. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #686 +/- ##
==========================================
+ Coverage 93.48% 93.56% +0.08%
==========================================
Files 29 29
Lines 3315 3327 +12
==========================================
+ Hits 3099 3113 +14
+ Misses 216 214 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks, @hugovk! I benchmarked the base commit ( Square datasetsEach result is the median of five runs.
Rectangular datasetI also measured two rectangular datasets using the median of ten runs:
For an n×n selection, the change reduces runtime from approximately O(n³) to O(n²). It uses a row set and a precomputed column-index list, but auxiliary space remains O(n); the O(n²) returned dataset still dominates total memory usage. Benchmark codeimport platform
from statistics import median
from timeit import repeat
from tablib import Dataset
print(f"Python {platform.python_version()}")
cases = (
(100, 100, 5),
(200, 200, 5),
(400, 400, 5),
(4_000, 50, 10),
(400, 4_092, 10),
)
for row_count, column_count, repetitions in cases:
headers = [f"column_{index}" for index in range(column_count)]
row = tuple(range(column_count))
dataset = Dataset(*(row for _ in range(row_count)), headers=headers)
elapsed = median(
repeat(
lambda: dataset.subset(
rows=range(row_count),
cols=headers,
),
repeat=repetitions,
number=1,
)
)
print(f"{row_count} rows x {column_count} columns: {elapsed:.6f}s") |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
|
Can you check the behaviour change when |
|
Good catch, @hugovk this was changing the behavior. I’ve fixed it so both cols=[] and an all-missing column list behave the same as old again. Added tests for both cases too. |
|
Hey @hugovk, just following up when you have a moment. I fixed the empty and all-missing column cases, then pushed 4357722 for another edge case: main accepts an unhashable row selector that compares equal to a row number. The PR now uses a set for plain integer row numbers and keeps the original list check for other values. For an n×n selection, the runtime is still O(n²). The result still walks source rows in order, so repeated row numbers don't duplicate or reorder output. Requested columns keep their order and duplicates, missing names are ignored, and repeated source headers still resolve to their first match. On a nonempty dataset, no matching columns still raises the same TypeError as main. I added regression tests for the edge cases, and the current checks are green. The benchmark is above. I left out the old |
Benchmark after the row-selection fix (4357722)I reran the same benchmark on the updated PR with Python 3.14.5 on Windows. Dataset construction was outside the timed part. The base values below are from my earlier benchmark of a36c965; that commit has not changed. The updated PR values were measured now. Square cases use the median of five runs, and rectangular cases use the median of ten.
The widest case varied across runs: a separate ten-run batch on the updated PR had a 0.487386 s median. So its exact speedup is approximate, but it remains much faster than base. The fast set lookup is still used for ordinary integer row selections. |



What changed?
Dataset.subset()was repeatedly looking up the same rows and columns while creating a subset. This made it noticeably slower for larger datasets.This change:
For a 400×400 dataset, the execution time improved from about 1.03 seconds to 0.03 seconds.
A test was added to make sure the existing selection behavior remains unchanged. All 184 tests pass.
Fixes #685