Build a production-ready, scalable analytics and ML pipeline with Vaex
When your dataset reaches millions of rows, loading everything into memory fails. Vaex lets you process, aggregate, and model at scale without exhausting RAM by using out-of-core processing — that is, working on disk instead of trying to fit everything into RAM.
Business scenario: city-aware marketing at scale
An e-commerce team wants customer-level predictions (will this user convert?) while also using city-level context (local median spend, propensity percentiles) across 2,000,000 customers. The challenge: create rich features, train a model, and ship artifacts for reproducible inference without spinning up distributed clusters. Vaex is built for this kind of single-node, commodity-hardware workload.
High-level pipeline (what you’ll get)
- Generate or ingest a large table (2,000,000 synthetic customers; eight cities).
- Define derived fields with lazy expressions (deferred calculations computed only when needed).
- Compute approximate, out-of-core city-level aggregates using fast percentiles and binning.
- Join aggregates back to records, encode categoricals, and standardize numeric features.
- Train a scikit-learn LogisticRegression wrapped with vaex.ml.sklearn.Predictor.
- Evaluate ranking (ROC AUC, average precision) and business-friendly decile lift.
- Export a Parquet feature snapshot and save preprocessing state (encoders, scaler params) to JSON for deterministic replay.
Why Vaex for scalable analytics and feature engineering
- Lazy expressions: define transformations without creating big intermediary arrays in memory (materializing).
- Out-of-core execution: Vaex reads and computes on disk-backed tables so RAM stays predictable.
- Approximate aggregation: fast approximate percentiles and binning give near-exact answers with far less cost.
- ML helpers: vaex.ml provides LabelEncoder, StandardScaler, and a Predictor wrapper to plug scikit-learn models into Vaex DataFrames.
Vaex enables fast, memory-efficient data processing while supporting advanced feature engineering, aggregation, and model integration.
Stack and reproducibility artifacts
The example uses Vaex and related libraries (vaex==4.19.0, vaex-ml==0.19.0, pyarrow>=14, scikit-learn>=1.3). Artifacts written by the pipeline include:
- /content/vaex_artifacts — artifact directory
- vaex_pipeline.json — pipeline state with encoder mappings, scaler means/stds, and canonical feature list
- A Parquet snapshot containing 500,000 feature-complete rows (parquet_path)
Quick code glimpses (minimal, actionable)
Generate synthetic customers (seeded RNG for reproducibility):
import vaex, numpy as np
n = 2_000_000
rng = np.random.default_rng(42)
df = vaex.from_arrays(
customer_id=np.arange(n),
city=rng.choice(['Montreal','Toronto','Vancouver','Calgary','Ottawa','Edmonton','Quebec City','Winnipeg'], size=n),
spend=rng.exponential(100, size=n),
visits=rng.poisson(3, size=n),
target=rng.binomial(1, 0.05, size=n)
)
Lazy derived field (deferred, no materialization until needed):
df['spend_per_visit'] = df['spend'] / (df['visits'] + 1)
# Not computed until you request values or persist
Approximate percentile aggregation and join strategy (city-level context):
city_stats = df.groupby('city', agg={
'median_spend': vaex.agg.percentile_approx('spend', 0.5, nbuckets=1000),
'p95_spend': vaex.agg.percentile_approx('spend', 0.95, nbuckets=1000),
})
df = df.join(city_stats, on='city', how='left')
Save pipeline metadata and model artifacts (JSON + Parquet):
pipeline_state = {
'version': '1.0',
'features': ['spend','visits','spend_per_visit','median_spend','p95_spend','city_enc'],
'encoders': {'city': ['Montreal','Toronto', ...]},
'scaler': {'means': {...}, 'stds': {...}}
}
# write to /content/vaex_artifacts/vaex_pipeline.json
# export Parquet snapshot (500k rows) with features
Feature engineering at scale — practical examples
A good feature set mixes record-level signals and context-level aggregates. Examples used in the pipeline:
- spend_per_visit = spend / (visits + 1) — simple per-user normalization
- city median and 95th percentile spend — local context captured with approximate percentiles
- city_enc — label-encoded city categorical via vaex.ml.LabelEncoder
- z_ prefixed features — standardized numeric features using vaex.ml.StandardScaler
Because Vaex applies lazy expressions, you can compose many derived columns without blowing memory; Vaex computes them only when needed (for model training, export, or prediction).
Aggregation: approximation with intent
Fast approximate percentiles and binning are core to the workflow. Translate the jargon: percentile_approx computes a close-enough percentile using bucketing; binning groups continuous values into bins for fast group stats. These are orders-of-magnitude cheaper than exact quantile algorithms on single-node systems.
Validation advice: hold out a small random sample (e.g., 50k rows). Compute exact percentiles on that sample with pandas or numpy and compare to Vaex approximate results to quantify error. If differences exceed business tolerances (especially on highly skewed distributions), consider increasing nbuckets or switching to an exact method for that metric.
Modeling and scikit-learn integration
vaex.ml.sklearn.Predictor wraps familiar scikit-learn estimators so you can fit and predict directly with Vaex DataFrames. Example model choices in the example:
- Model: LogisticRegression
- Parameters: max_iter=250, solver=’lbfgs’
- Evaluation: ROC AUC and average precision for ranking, plus a decile lift table for business interpretation
Decile lift is a simple, interpretable metric: bucket predictions into 10 equal groups, compute target rate per bucket, then compare the top bucket’s rate to the baseline (overall) rate. Stakeholders usually understand “top decile is Xx more likely to convert” much faster than AUC numbers alone.
Reproducible preprocessing & artifact layout
The JSON pipeline state stores the canonical steps needed to rebuild features and scale them deterministically. Example schema:
{
"version": "1.0",
"timestamp": "2025-02-01T12:00:00Z",
"features": ["spend","visits","spend_per_visit","median_spend","p95_spend","city_enc"],
"encoders": {"city": ["Montreal","Toronto","Vancouver", "..."]},
"scaler": {"means": {"spend": 120.3, "visits": 2.9, ...}, "stds": {"spend": 60.2, ...}}
}
The pipeline saves:
- Parquet feature snapshot (500k rows) for downstream teams or a production loader
- vaex_pipeline.json for deterministic feature regeneration
- Saved Vaex model artifact (Predictor state) for inference
Operational tips for Parquet and artifacts
- Compression: use snappy for fast read/write and reasonable compression.
- Partitioning: partition by city or date to make subset reads cheap during backfills or re-training.
- Retention & security: encrypt backups or keep Parquet files in a secure object store (S3/GCS) when containing PII; scrub or sample carefully before export.
When Vaex is the right tool — and when it isn’t
Vaex is ideal when you need fast, single-node, memory-efficient processing and reproducible pipelines without moving to a distributed cluster. It sits between pandas and heavyweight systems like Spark.
Consider alternatives if:
- You need multi-node horizontal scale or heavy GPU acceleration — look to Spark, Dask, or RAPIDS/Polars with GPU backends.
- You require exact statistics for regulatory purposes where even small approximation error is unacceptable.
- Your team already has an enterprise feature store and serving layer that integrates tightly with their infrastructure; then Vaex is useful for offline feature engineering but may not replace an established feature platform.
Vaex vs Polars vs Dask — quick decision guide
- Vaex: best for single-node, out-of-core processing, lazy evaluation, and fast approximate aggregations.
- Polars: blazing-fast in-memory DataFrame operations (Rust-backed); excellent for CPU-bound workloads where data fits memory or when multithreading is sufficient.
- Dask: scales to clusters and integrates with larger ecosystems (MLflow, distributed compute) but comes with higher operational complexity.
Checklist: turn the notebook into a microservice
- Store artifacts (Parquet + vaex_pipeline.json + model) in a versioned object store (S3/GCS) with semantic tagging.
- Provide a minimal feature regeneration module that loads vaex_pipeline.json and recreates features deterministically.
- Expose online scoring via a narrow model server that consumes the same regeneration module to avoid train/serve skew.
- Add unit tests that compare computed sample percentiles to stored baselines to detect drift in preprocessing.
- Encrypt exported snapshots and rotate credentials to protect PII.
FAQ — common questions and quick answers
How can you perform feature engineering on millions of rows without materializing intermediates?
Use Vaex lazy expressions: define derived fields as expressions that are evaluated only when required (for export, training, or prediction), avoiding big in-memory arrays.
How are city-level aggregates computed and joined back at scale?
Compute approximate percentiles and other aggregates with Vaex groupby and percentile_approx or binning, build a city-level table and join it back to the record-level DataFrame. This keeps memory usage low and speeds up aggregation dramatically.
How do you integrate Vaex with scikit-learn models?
Wrap a scikit-learn estimator with vaex.ml.sklearn.Predictor (e.g., LogisticRegression with max_iter=250, solver=’lbfgs’) to fit and predict directly using Vaex DataFrames.
How do you make preprocessing reproducible for deployment?
Persist encoder mappings, scaler means/stds, and the canonical feature list in a JSON pipeline file (vaex_pipeline.json) and export a Parquet snapshot for verification and downstream teams.
What are the accuracy trade-offs of approximate statistics?
Approximate percentiles trade a small amount of accuracy for major speed and memory gains. Validate approximations on a held-out sample and increase nbuckets or switch to exact methods where business needs require it.
Wrap-up and next steps
For teams building production-ready ML pipelines on commodity hardware, the pattern of lazy expressions, out-of-core aggregation, encoder/scaler persistence, Parquet export, and sklearn integration via Predictor provides a pragmatic, reproducible blueprint. Ready-to-run Colab notebooks and a one-page “Vaex into Production” checklist can accelerate adoption; for teams considering a long-term platform, run sample benchmarks on your hardware (a small validation run with 100k–500k rows) to tune nbuckets, partitioning, and Parquet settings.
“Persisting preprocessing state plus exporting feature snapshots closes the loop from raw data to inference, making the workflow deployable and auditable.”
If you want a concise decision checklist comparing Vaex vs Polars vs Dask for your workloads or a one-page operational checklist to turn this notebook into a microservice (feature store + model server), those can be prepared next to help operationalize the design.