Coding 101

Python Data Science Stack: NumPy Vectorization, Polars, and Memory Limits

DD
Ankur Ishwar
10 min read Updated Sep 7, 2026
Dropout Developer • Editorial Coding 101

Python Data Science Stack: NumPy Vectorization, Polars, and Memory Limits

Oct 16, 202310 min read

Open any beginner data science course on the internet and you will see the exact same curriculum: load the Titanic survival CSV, run df.describe() in a Jupyter Notebook, draw a few seaborn scatterplots, and fit a LogisticRegression model. Everything works smoothly because the dataset has only 891 rows and fits in 60 kilobytes of RAM.

Then you land a real data engineering or machine learning job. You are handed a 12-gigabyte server access log or 50 million transaction rows, and your Jupyter kernel silently dies with MemoryError or crashes your entire laptop. Python is a slow, interpreted language with high memory overhead per object. The only reason it dominates data science is because its core libraries delegate heavy computation to compiled C, C++, and Rust binaries.

To work with data professionally without setting your laptop on fire, you must understand how Python data libraries interact with physical RAM, CPU registers, and disk I/O.

1. NumPy: The Engine Behind Modern Scientific Computing

A standard Python list of integers is not an array of numbers. It is a list of pointers referencing distinct PyObject structs scattered across the heap:

# Standard Python List Memory Overhead
import sys
numbers = [1, 2, 3, 4, 5]
# Each integer is a 28-byte PyObject, plus 8-byte pointer in the list array

If you iterate over 10,000,000 numbers using a Python for loop, the CPython interpreter must unbox each object, check its type, execute dynamic dispatch, and perform pointer lookups. It takes several seconds.

NumPy eliminates this entire overhead by storing data in a contiguous block of raw C memory:

import numpy as np

# Allocates a single contiguous 80MB buffer (8 bytes * 10,000,000)
arr = np.arange(10_000_000, dtype=np.int64)

# Vectorized multiplication: executed directly in compiled C with SIMD instructions
result = arr * 2  # Takes ~15 milliseconds

Why Vectorization Beats Loops

NumPy operations do not run Python bytecode in a loop. They utilize SIMD (Single Instruction, Multiple Data) CPU vector registers (AVX-512 or ARM Neon). A single CPU clock cycle multiplies four or eight 64-bit numbers simultaneously directly in CPU cache. If you write an explicit for loop over a NumPy array in Python, you defeat the entire purpose of the library.

2. The Modern Shift: Replacing Pandas with Polars

For over a decade, Pandas was the default tabular data library. But Pandas was designed in 2008 for single-core financial data analysis. It has serious architectural limitations in modern workflows:

  • Single-Threaded Execution: Standard Pandas operations utilize only one CPU core, leaving your remaining 7 or 15 cores completely idle.
  • Aggressive Memory Duplication: Many Pandas operations create eager intermediate copies of DataFrames. In practice, processing a 2GB CSV often requires 10GB to 15GB of available RAM.
  • Eager Execution: Pandas executes operations immediately, preventing the engine from optimizing query plans.

Enter Polars: a lightning-fast DataFrame library written in Rust and built on the Apache Arrow columnar memory format.

Benchmark Comparison: Filtering and Grouping 20 Million Rows

import polars as pl

# Lazy execution: reads metadata, plans optimized execution graph
query = (
    pl.scan_csv("large_dataset.csv")  # Does not load entire file to RAM
    .filter(pl.col("status") == "COMPLETED")
    .group_by("category")
    .agg([
        pl.col("revenue").sum().alias("total_revenue"),
        pl.col("order_id").count().alias("order_count"),
    ])
    .sort("total_revenue", descending=True)
)

# Materialize results only when needed:
df = query.collect()  # Multi-threaded execution across all CPU cores

Why Polars outperforms Pandas:

  1. Multi-Threading by Default: Rust rayon thread pool executes tasks across all available CPU threads automatically.
  2. Query Optimization (Predicate Pushdown): Because we called filter() in lazy mode, Polars drops unnecessary rows as it streams the CSV from disk, never loading discarded records into RAM.
  3. Zero Memory Copy: Apache Arrow columnar layout enables memory sharing without defensive cloning.

3. Machine Learning: Scikit-Learn Pipelines Over Scripting

A frequent error in junior ML projects is data leakage: calculating normalization parameters (like mean and standard deviation) across the entire dataset before splitting into train and test sets.

Scikit-Learn provides strict pipeline abstractions that encapsulate feature engineering and model training into a deterministic unit:

from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import HistGradientBoostingClassifier

# Explicit column separation
numeric_features = ["age", "account_balance", "credit_score"]
categorical_features = ["employment_type", "education"]

# Compose preprocessing steps
preprocessor = ColumnTransformer(
    transformers=[
        ("num", StandardScaler(), numeric_features),
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
    ]
)

# Combine preprocessing with model in a single atomic pipeline
model_pipeline = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("classifier", HistGradientBoostingClassifier(random_state=42)),
])

# Fit ONLY on training data; test data remains untouched until evaluation
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model_pipeline.fit(X_train, y_train)
score = model_pipeline.score(X_test, y_test)

Pipelines guarantee that transformations applied to production inference requests are 100% identical to the training phase, eliminating silent model drift.

4. Memory Optimization on Low-Spec Hardware (The 8GB RAM Survival Rules)

If you are working on a budget laptop, follow these engineering practices to avoid out-of-memory crashes:

1. Downcast Numeric Types

By default, CSV parsers load integers as 64-bit (8 bytes per value). If your integer values range between 0 and 50,000, 16-bit integers (2 bytes) represent the exact same numbers with a 75% reduction in RAM:

# Explicit schema definition saves gigabytes of memory
schema = {
    "user_id": pl.Int32,      # Up to 2.1 billion IDs
    "age": pl.Int8,           # Ages 0 to 127 fit in 1 byte
    "price": pl.Float32,       # 4 bytes instead of default 8-byte Float64
    "status": pl.Categorical,  # Interns strings to tiny integer indices
}
df = pl.read_csv("orders.csv", schema_overrides=schema)

2. Stream Processing with Batches

If your dataset exceeds physical RAM, never attempt to load it in one shot. Process data in chunks or stream it through a generator pipeline:

# Streaming 50GB file in 50,000-row chunks
for chunk in pl.read_csv_batched("massive_log.csv", batch_size=50_000):
    # Filter, aggregate, and write results to SQLite or disk
    pass

Summary Tool Comparison

Library Core Strength When to Use When to Avoid
NumPy N-dimensional C-arrays, SIMD vectorization Matrix math, tensor manipulation, raw algorithms Heterogeneous tabular records with column names
Polars Multi-threaded Rust, lazy query optimization Tabular data processing, large CSV/Parquet files Small datasets with legacy codebases bound to Pandas APIs
Scikit-Learn Standardized ML pipelines, estimators, metrics Classical ML, tabular models, preprocessing pipelines Deep learning with multi-GPU distributed neural networks

Stop writing slow Python loops over datasets. Use contiguous arrays, adopt multi-threaded query engines like Polars, and treat memory budgets as a strict engineering constraint.

Found this useful?
View all articles

Keep Reading

Related Articles

Learn with Dropout Developer

Build real software with AI

Step-by-step learning paths, vibe coding tutorials, and certified developer programs designed for the modern engineer.