Example Workload: ML Feature Engineering
An illustrative ML feature pipeline split into what a compiler accelerates — aggregations, transforms — and what it cannot: histograms, string features, I/O.
Published May 14, 2026
Feature engineering is where Python machine-learning pipelines spend the hours nobody budgets: turning raw event tables into the columns a model trainer consumes. The work is repetitive numeric transformation over in-memory data — which sounds like ideal compiler territory, and often is. But the same suite that measures a correlation loop at 52.5x measures a histogram workload at 0.9x. Feature pipelines contain both shapes, usually in the same file. This page dissects one illustrative pipeline and shows how to tell which of your features are which.
The standing labels apply: this is an example workload, not a customer story, and every speedup below is a named workload extracted from the benchmark artefact dated 13 September 2026. Where the article does arithmetic on top of those anchors, the result is labelled an illustrative model with assumptions stated.
The workload shape
Consider a recommender-style pipeline. Input: a table of user–item interaction events. Output: one feature matrix. Per user, the pipeline computes interaction counts, rolling averages over time windows, pairwise co-occurrence statistics between items, normalised frequency transforms, and a handful of bucketed histogram features. Per item, analogous aggregates. The resulting frame is handed to a trainer.
Three of those feature families are pure-Python numeric loops over arrays and dictionaries: counts, rolling averages, co-occurrence statistics, normalisation. Those are candidates. Two are not: the histogram bucketing, if done with heavy object churn, and anything that touches string columns, timestamps-as-strings, or category labels — string work, where the suite consistently measures at or below CPython. And a sixth cost belongs to nobody here: reading the event table from disk or parquet is I/O, fixed whichever interpreter runs the loops.
One more shape matters in ML pipelines: the outer Python is often not the bottleneck. Pipelines built on vectorised frameworks spend their heavy time in C kernels already, and no compiler accelerates what CPython never interpreted. The features worth compiling are the ones written as plain Python loops because they were too awkward to vectorise.
Measured anchors per feature family
| Feature family | Closest suite workload | Measured | Verdict |
|---|---|---|---|
| Co-occurrence / correlation loops | statistics.correlation | 52.5x | Compile; strongest suite match |
| Matrix-vector feature transforms | numerical.matrix_vector | 62.4x | Compile where written as loops |
| Similarity scoring (k-NN style) | ml.knn_inference | 1.2x | Marginal; tiny per-call work dominates |
| Linear-model prediction loops | ml.perceptron_predict | 9.9x | Compile for batch scoring |
| GCD-like reduction loops | numerical.gcd_loop | 7.4x | Typical reduction-loop win |
| Histogram bucketisation | statistics.histogram | 0.9x | Do not compile; measured regression |
| String / category features | string category (geomean) | 0.85x | Leave on CPython |
The ML category's own pair of rows carries the central lesson. ml.perceptron_predict measured 9.9x; ml.knn_inference measured 1.2x. Same category, same broad domain, nearly an order of magnitude apart — because perceptron prediction is a loop over enough arithmetic to amortise everything, while a single k-NN call is over before any warm-up finishes. Feature engineering pipelines are full of both: the batch transform over a million users is the perceptron shape; the per-row scorer invoked a few thousand times is the k-NN shape. Size and call count decide which you are holding.
The histogram row is the one pipeline authors find hardest to accept. Bucketisation feels numeric. But a histogram implemented by appending to per-bucket Python lists allocates constantly, and allocation is the wall the compiler cannot knock down — the same wall that puts the string and parsing categories below 1x across the suite. The fix is not a flag; it is a data structure. Counts in a fixed-size array instead of lists of samples change the workload's shape before the compiler enters the picture.
An illustrative model
Assume profiling attributes 60% of feature-generation time to the acceleratable loop families, 25% to histogram and string features, 15% to I/O. Assume — labelled, not measured — 15x on the acceleratable share, sitting inside the measured 7.4x to 62.4x range for the matching shapes. The model: 0.25 + 0.15 + 0.60/15 = 0.44 of the old runtime, roughly 2.3x end to end. If your trainer waits on feature generation in series, the pipeline shortens by that factor; if features and training run concurrently, the win is real but smaller, because the trainer's clock runs independently. Which topology you have is a fact about your pipeline, and it halves or doubles the value of the exercise.
The model also exposes a common failure mode: feature functions that mix a small acceleratable loop with string label handling in the same function. Compiled whole, the string work drags the measurement toward 1x; compiled apart, the loop gets its 15x and the string part stays harmlessly on CPython. Function granularity is a measurement decision as much as a design one.
What a pilot run looks like
Before touching the production pipeline, run a contained pilot. Pick the single heaviest feature function — the co-occurrence counts are the usual suspect — extract it into a standalone module with a zero-argument entrypoint over a realistic input sample, and run the proof sequence on it. The pilot answers the only question that matters: whether your loop, on your data shapes, measures anywhere near the published anchors. Three honest outcomes exist. The number lands near the anchors and the pilot proceeds. It lands near 1x, meaning the feature is library- or allocation-bound and needs a data-structure fix, not a compiler. Or the function is declined, and the fallback record says why, free of charge.
One pilot detail is specific to feature work: seed the input realistically. Features measured over synthetic uniform data can behave differently over real event distributions, where long tails change loop trip counts and memory behaviour. A proof over unrepresentative input is a proof of the wrong function. Fix the sample, record its provenance in the write-up, and re-run.
Where the engineering judgement sits
The judgement call in this workload is about the trainer boundary, and it is easy to get wrong in the optimistic direction. If features are generated by vectorised framework calls, there is nothing to compile and the honest answer is that Pyvorin is not the tool for that stage. If features are plain-Python loops — the awkward co-occurrence counts, the rolling windows written longhand — the anchors above apply. An engineer who compiles everything indiscriminately will report a blended 1.3x, conclude the product underdelivers, and miss that the acceleratable 60% was running at 15x inside that average. Segment first. Measure second. Aggregate last.
The second judgement is correctness hygiene. Feature pipelines feed models, and a silently wrong feature is worse than a slow one. The proof workflow's correctness gate — both backends returning the identical result — is not optional here; it is the difference between a faster pipeline and a faster pipeline producing a different model. Check fallback_count and deopt_count in the JSON output on a real run: non-zero values mean some calls ran outside native code, and a feature-generation number measured across mixed backends is not a number.
Proving it on your own pipeline
Lift the heaviest feature function into its own module, wrap it in a zero-argument entrypoint over a realistic-sized input, and run the matched comparison:
python -m pyvorin support your_features.py
python -m pyvorin run your_features.py --function entry --compare --runs 5 --warmup 2
python -m pyvorin bench your_features.py --function entry --runs 7 --warmup 2 --json
Require Correct: YES, COMPILED_FULL on the target, and matched warmup and run counts before quoting anything. Report the median of the raw samples with the spread beside it, state the one-off compile cost, and do the break-even division against the function's real call cadence. The audit-grade version of this method is how to run a speed proof, with harness mechanics in how to benchmark a function. Script the three commands once and the proof re-runs with every feature change for the cost of a few lines in a job definition — which is what keeps the claimed number honest as the pipeline evolves.
Limits, stated plainly
All anchors come from one host on one date; treat the distribution as the message and your own measurement as the verdict. Histogram and string features regress under compilation, and the fallback path — covered in unsupported code and the fallback path — is what keeps a declined function correct rather than broken. Vectorised framework work is outside scope entirely: time spent in C kernels was never the compiler's to save. And where feature scoring sits inside an online request path with a millisecond budget, the throughput figures here do not transfer; compiled batch Python is not a latency technology.
Where to go next
- How to run a speed proof — measurement discipline with the correctness gate this workload class needs.
- Benchmarks — the full 71-workload table, slower rows included.
- Example workload: ETL pipeline — the same segmentation logic upstream of feature generation.
- Unsupported code and the fallback path — what happens when a feature function is declined.
Last reviewed 14 May 2026 against the benchmark artefact dated 13 September 2026 (71 workloads, 54 faster / 17 slower than CPython, ML category 1.2x–9.9x). Anchor figures are extracted from that artefact; end-to-end arithmetic is a labelled illustrative model. This page describes an example workload; it is not a customer case study.