Take a Break
5:00
Inhale…
Give your mind a break — no phone, no music, just idle time or a quick walk.
Spark MLlib Architecture, Feature Engineering Pipelines, and Distributed Model Training
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- Spark DataFrames and the Catalyst Optimizer - covered in Lecture 10
- Machine learning pipeline and taxonomy - covered in Lecture 12
- Model evaluation metrics - covered in Lecture 12
- Apache Spark MLlib horizontal scalability - covered in Lecture 13
- Distributed Random Forest in Spark MLlib - covered in Lecture 14
15.1 Spark MLlib Overview, Scalability, and Architectural Foundations
Hook: A top-end workstation may offer 64–128 CPU cores. A 100-node Spark cluster of ordinary dual-core boxes already offers 400 cores — and you can add more nodes tomorrow without rewriting the training code. Why redesign the algorithm when you can redesign the hardware footprint?
Machine learning over massive datasets needs a compute fabric that grows by adding machines, not by buying a faster single box. Traditional single-node libraries (scikit-learn, local R, a lone GPU workstation) run the training loop on one machine. Physical core count on that machine is hard-capped — typically between 64 and 128 cores even on high-end hardware. Apache Spark MLlib (Spark’s scalable machine learning library) sits on distributed clusters of commodity workers and grows horizontally: add worker nodes, gain aggregate cores, memory, and disk. Motivation for Spark MLlib is exactly this scalability trade-off: accept cluster coordination cost to train on data that no single node can hold.
Companion texts place MLlib in the Spark stack alongside Spark SQL, Streaming, and GraphX: it ships classification, regression, clustering, collaborative filtering, dimensionality reduction, and optimization primitives for recommendation, clustering, and classification workloads. Practitioner references such as Spark: The Definitive Guide by Matei Zaharia and Bill Chambers describe the same DataFrame-centric ML pipeline model used in modern pyspark.ml.
15.1.1 Horizontal Scaling versus Single-Node Optimization
Intuition: Think of a kitchen that must cook for a growing festival. You can buy one industrial stove and tune every recipe to squeeze seconds out of it — or you can open more identical kitchen stations and assign dishes in parallel. Local algorithm micro-optimization is the industrial stove; a Spark cluster is the row of stations. The analogy breaks when communication between stations (network shuffle) dominates cook time — then more stations alone do not help.
In production ML engineering the standing choice is: spend effort optimizing a model on one machine, or spend effort scaling the cluster so larger datasets fit. Prefer scaling. Enterprise data volumes grow every year; model quality and rare-pattern detection increasingly depend on how much training data you can ingest, not on another 5% of local FLOPS.
Formalize — aggregate cluster cores.
Let \(N\) be the number of worker nodes and \(c\) the cores per node. Aggregate parallel capacity is \[ \text{Total Cluster Cores} = N \times c \] where \(\text{Total Cluster Cores}\) is the count of parallel compute threads the cluster can schedule. Incremental capacity when adding \(\Delta N\) nodes is \[ \text{Additional Cores} = \Delta N \times c, \qquad \text{New Total Cluster Cores} = N c + \Delta N\, c = (N + \Delta N)\, c. \]
Worked example — 100 nodes → 150 nodes.
Given \(N = 100\) commodity workers, each with 2 dual-core processors so \(c = 4\): \[ \text{Total Cluster Cores} = 100 \times 4 = 400. \] Add \(\Delta N = 50\) identical nodes: \[ \text{Additional Cores} = 50 \times 4 = 200, \] \[ \text{New Total Cluster Cores} = 400 + 200 = 600. \] Answer: 600 cores. Sense-check: \(150 \times 4 = 600\), so the two-step arithmetic matches the closed form \((N+\Delta N)c\).
Scope / Assumption: Horizontal scaling assumes the job can be partitioned (row-wise or partition-wise) and that network + shuffle cost does not swamp compute. It does not remove the need for correct algorithms; it removes the single-machine core and RAM ceiling. If the working set per partition exceeds executor memory, you still fail — scale memory or repartition, do not only add cores.
Visual intuition. Imagine a bar chart: horizontal axis = number of worker nodes (\(N\)), vertical axis = aggregate cores (\(Nc\)). With fixed \(c=4\), the plot is a straight line through the origin with slope 4. Landmarks: \(N=100 \rightarrow 400\) cores; \(N=150 \rightarrow 600\) cores. Takeaway: capacity grows linearly with nodes under a fixed per-node core count.
Pitfalls:
- Counting “dual-core × 2 processors” wrong — \(2 \times 2 = 4\) cores per node, not 2.
- Assuming every added core yields proportional wall-clock speedup (Amdahl / shuffle bottlenecks).
- Optimizing local code for weeks when the real blocker is that the dataset no longer fits on one disk.
- Treating “unlimited rows” as “free” — storage and shuffle still cost money and time.
Production systems prefer scaling distributed hardware over local algorithm micro-optimization because annual enterprise data growth demands multi-terabyte ingestion.
Recap: Prefer horizontal cluster growth over single-node micro-optimization when data volume is the bottleneck. Next: which Spark ML package actually carries that scale into APIs and pipelines.
15.1.2 Evolution of Spark MLlib API Packages
Apache Spark exposes ML through two package namespaces:
| Package | PySpark import | Data model | Status |
|---|---|---|---|
org.apache.spark.mllib |
pyspark.mllib |
RDD | Legacy / deprecated; kept for backward compatibility |
org.apache.spark.ml |
pyspark.ml |
DataFrame | Modern API: pipelines, transformers, tuning, evaluation |
All new Spark ML work should use org.apache.spark.ml (DataFrame API). The RDD-based mllib package remains only so old jobs keep compiling.
Spark’s design also stresses pipeline composition: data leaves sources, passes through ML stages, and feeds applications. MLlib APIs interoperate with Spark SQL and, in Python, with NumPy-style numeric arrays — useful when assembling feature vectors before estimators run.
Pitfall: Importing pyspark.mllib by habit in new notebooks. Prefer pyspark.ml and pyspark.ml.feature / pyspark.ml.classification / pyspark.ml.regression from day one.
15.1.3 Scalability Limits and Feature Volume Capacities
Spark MLlib algorithms target high-dimensional feature spaces and row counts that exceed single-node RAM:
- Decision Trees: up to about 1,000 continuous or categorical features, with information-gain / entropy splits computed across partitions.
- Random Forest: up to about 10,000 features and classification with up to about 100,000 (100k) distinct output classes.
- Logistic Regression: sparse or dense feature spaces up to about 10,000,000 (10M) features.
- Training instances (rows): effectively unbounded for supervised and unsupervised algorithms — limited by aggregate cluster storage and memory, not by a fixed library constant.
Scale comparison (order of magnitude).
| Algorithm | Feature capacity (order) | Extra limit |
|---|---|---|
| Decision Tree | \(10^{3}\) | Split evaluation per feature |
| Random Forest | \(10^{4}\) | \(\sim 10^{5}\) classes |
| Logistic Regression | \(10^{7}\) | Sparse vectors preferred at high \(d\) |
Sense-check: logistic regression’s higher feature ceiling matches sparse high-dimensional problems (e.g., bag-of-words, large one-hot spaces); trees pay more per-feature split work, so their practical feature caps are lower.
Scope: These capacities are practical library design targets, not mathematical theorems. Real limits also depend on executor memory, serialization, and how dense the vectors are. A “10M-feature” logistic model stored densely will not fit; sparsity is assumed.
Real-world & domain. Clickstream and ad-tech logistic models routinely use millions of sparse hashed or one-hot features across a Spark cluster; genomics and retail assortment models use random forests over thousands of engineered attributes. MLlib exists so these workloads stay inside the same Spark SQL / DataFrame fabric used for ETL.
Exam note: Spark MLlib algorithms scale to thousands of features (1,000 Decision Trees, 10,000 Random Forests, 10,000,000 Logistic Regression) and essentially unlimited training instances on a distributed cluster.
Architecture and scale motivate why Spark ML exists. The next section defines the four-stage ML lifecycle and the Transformer / Estimator / Evaluator / Pipeline abstractions that implement it.
15.2 Spark ML Machine Learning Pipeline Architecture
Hook: Fitting a model is often the short middle of the job. Most wall-clock and engineering time sits in loading dirty data, shaping features, and measuring whether the model generalizes — Spark ML turns that entire path into one composable object: a Pipeline.
A complete machine learning workflow spans raw data loading through scoring and validation. Spark MLlib formalizes this lifecycle as a modular pipeline: each stage is a reusable Transformer or Estimator, and the Pipeline class chains them into one executable workflow. Companion material describes an ML pipeline as data taken from sources, passed through ML programs, with outputs feeding applications (decision trees, clustering, mining, and related tasks).
15.2.1 The Four Stages of the Machine Learning Lifecycle
Intuition: Treat the lifecycle like a factory line: (1) receive raw parts, (2) machine and calibrate them, (3) assemble the product, (4) inspect quality. Skipping cleaning or inspection produces a shiny but useless product. The analogy breaks when online learning updates models continuously — stages still exist, but they loop rather than run once.
Purpose of the four stages.
- Data Ingestion and Cleaning — Load unstructured, semi-structured, or structured data from CSV, relational databases, HDFS, or cloud storage via
spark.read.format(...). Raw source data is impure: nulls, wrong types, and awkward schemas must be handled before learning. - Feature Engineering and Preprocessing — Typically 60% to 70% of total ML engineering effort. Extract, clean, scale, transform, and select independent variables (features) so algorithms receive clean numeric vectors.
- Model Training and Hyperparameter Tuning — Feed feature vectors into estimators; search hyperparameter configurations systematically.
- Evaluation and Metrics Quantification — Score predictions on held-out test data with objective metrics (e.g., RMSE, \(R^{2}\), Area Under ROC).
These stages align with classical ETL thinking (extract → transform → load/model): Spark folds the “transform” of business data and the “transform” of ML features into the same DataFrame API.
15.2.2 Core Pipeline Abstractions: Transformers, Estimators, and Evaluators
Inputs & Outputs of the core abstractions.
Spark ML unifies the lifecycle through four building blocks:
| Abstraction | Role | Typical call |
|---|---|---|
| Transformer | Maps one DataFrame to another by appending processed columns | .transform(df) → df' |
| Estimator | Learns parameters from data, then returns a Transformer | .fit(df) → model (a Transformer) |
| Evaluator | Scores predictions against ground-truth labels | .evaluate(predictions) → metric |
| Pipeline | Chains Transformers and Estimators into one workflow | Pipeline(stages=[...]).fit(df) |
A Pipeline is represented by the Pipeline class: stages execute in order; fitting a pipeline fits every Estimator stage and produces a PipelineModel whose stages are all Transformers.
Q: Why does feature engineering consume 60% to 70% of machine learning development effort compared to model training? A: Raw enterprise data contains missing values, continuous variables on mismatched scales, string categories, and redundant features. Algorithms require clean, numerically formatted feature vectors. Preparing, scaling, encoding, and selecting those representations is heavy data manipulation that must finish before .fit() is meaningful.
Pitfalls:
- Treating “train the model” as the whole project while leaving nulls and string labels in place.
- Confusing Evaluator (metrics) with Estimator (learns a model).
- Building stages that write over columns instead of appending — Spark’s mental model is append-only transforms on immutable DataFrames (detailed in §15.3).
15.2.3 Vector Representations: Dense Vectors versus Sparse Vectors
Spark MLlib algorithms require all input features in one vector column of double-precision floats (Vector of Double). Labels must also be Double.
Spark provides two implementations of the Vector interface in pyspark.ml.linalg:
Dense vs sparse formalization.
- Dense vector — stores every element explicitly in contiguous memory, zeros included.
\[ \mathbf{v}_{\mathrm{dense}} = [1.0,\ 2.0,\ 3.0] \in \mathbb{R}^{3} \] PySpark: Vectors.dense([1.0, 2.0, 3.0]).
- Sparse vector — stores only non-zero values with their indices. A vector of length \(N=1000\) with value \(2.0\) at index \(100\) and \(3.0\) at index \(200\) is
\[ \mathbf{v}_{\mathrm{sparse}} = \bigl(1000,\ [100,\ 200],\ [2.0,\ 3.0]\bigr) \] where \(1000\) is the dimension, \([100,200]\) are non-zero indices, and \([2.0, 3.0]\) are the stored values. PySpark: Vectors.sparse(1000, [100, 200], [2.0, 3.0]).
Worked memory comparison.
Suppose \(N = 1000\) and only two entries are non-zero.
- Dense storage: 1000 doubles ≈ \(1000 \times 8 = 8000\) bytes (plus object overhead).
- Sparse storage: 2 indices + 2 doubles ≈ \(2\times 4 + 2\times 8 = 24\) bytes of payload (plus small header).
Answer: sparse wins by roughly two orders of magnitude when almost all entries are zero. Sense-check: one-hot and bag-of-words vectors are exactly this pattern — high \(N\), tiny support.
Comparison — when to pick which.
| Property | Dense | Sparse |
|---|---|---|
| Best when | Most entries non-zero | Most entries zero |
| API | Vectors.dense([...]) |
Vectors.sparse(size, indices, values) |
| Risk | Wastes RAM on zeros | Index mistakes; densify accidentally |
When to pick which: use dense for low-dimensional numeric features after assembly; use sparse after one-hot encoding or large vocabularies.
Assumption: Sparse format assumes a known fixed dimension \(N\) and sorted unique indices. It breaks if you treat the sparse tuple as “only the non-zeros matter” without carrying \(N\) — algorithms need the full dimensionality.
Visual intuition. Picture a length-1000 number line of slots. Dense paints a value in every slot. Sparse leaves slots empty and pins two labeled dots at positions 100 and 200. Takeaway: sparsity stores the pins, not the empty slots.
Pitfalls:
- Building a dense one-hot of cardinality 100k and exhausting executor memory.
- Forgetting that labels and features must be
Double/Vector, not strings. - Mixing
mllib.linalgandml.linalgvector types across APIs.
Real-world & domain. One-hot encoding of high-cardinality categoricals (sku_id, user_segment, geo) produces high-dimensional vectors stored as sparse vectors in Spark ML to avoid memory overflow — the same pattern used in large-scale logistic regression (§15.1.3).
Recap: Pipelines chain Transformers, Estimators, and Evaluators; features live in one dense or sparse vector column. Next: the operational difference between a one-pass Transformer and a two-pass Estimator.
15.3 Core Abstractions: Transformers vs. Estimators
Hook: Two Spark classes both “change” a DataFrame — yet only one is allowed to look at the whole dataset first. Mixing them up is the most common pipeline bug: calling .transform() on something that still needs .fit().
Building Spark ML pipelines requires a sharp operational distinction: Transformers apply fixed rules; Estimators learn parameters from data and then return a Transformer.
15.3.1 Transformer Mechanics and Immutability Guarantees
Intuition: A Transformer is like a stamp that prints the same pattern on every form using rules already written on the stamp. It never needs to read all forms first. The analogy breaks for anything that must know global statistics (min, max, mean, vocabulary) — those need an Estimator’s survey pass.
Definition. A Transformer converts an input DataFrame into an output DataFrame: \[ \texttt{transform}(df) \rightarrow df'. \] Because Spark DataFrames are immutable distributed structures, a Transformer never modifies input columns in place. In-place edits across partitions would force expensive cross-cluster state synchronization. Instead, the Transformer appends a new column. An input DataFrame with 10 columns yields an output with 11 columns after one appended transform.
Key Transformer properties:
- No preliminary full-data scan; rules are deterministic and self-contained.
- Configured with
setInputCol()/setInputCols()and optionalsetOutputCol(). - Executed via
.transform(df).
from pyspark.ml.feature import Tokenizer
# Instantiate Transformer
tokenizer = Tokenizer()
tokenizer.setInputCol("description")
tokenizer.setOutputCol("words")
# Execute transformation (appends 'words' column to sales_df)
tokenized_df = tokenizer.transform(sales_df)
Trace — column count.
Start: sales_df has columns [id, description] (2 columns). After tokenizer.transform(sales_df): columns [id, description, words] (3 columns). Answer: one new column appended; originals untouched. Sense-check: immutability ⇒ copy-on-append, not mutate-in-place.
15.3.2 Estimator Mechanics and Two-Pass Execution Flow
Purpose. An Estimator must inspect the dataset to learn internal parameters, summary statistics, or metadata before any row can be transformed: \[ \texttt{fit}(df) \rightarrow \text{Transformer (model)}. \]
Min-Max scaling is the canonical example: you cannot scale a single row until you know global \(x_{\min}\) and \(x_{\max}\). Until .fit() finishes, transformation is undefined.
Two-pass architecture (inputs → steps → outputs).
- First pass —
.fit(df): Scan the DataFrame; compute metadata or train parameters; return a fitted Transformer (often named*Model). - Second pass —
.transform(df): Apply the learned rules to produce the output DataFrame.
from pyspark.ml.feature import MinMaxScaler
# Instantiate Estimator
scaler = MinMaxScaler()
scaler.setInputCol("features")
scaler.setOutputCol("scaledFeatures")
# Pass 1: Fit estimator to compute global min and max statistics across dataset
scaler_model = scaler.fit(df) # Returns a MinMaxScalerModel (Transformer)
# Pass 2: Apply transformer to scale feature values
scaled_df = scaler_model.transform(df)
Trace — MinMaxScaler on three values.
Raw feature column: \(\{2.0,\ 5.0,\ 8.0\}\). Fit learns \(x_{\min}=2.0\), \(x_{\max}=8.0\). Default target range \([0,1]\): \[ x' = \frac{x - 2.0}{8.0 - 2.0} = \frac{x - 2.0}{6.0}. \] Results: \(2.0 \rightarrow 0.0\), \(5.0 \rightarrow 0.5\), \(8.0 \rightarrow 1.0\). Answer: scaled values \(0.0,\ 0.5,\ 1.0\). Sense-check: endpoints map to the range bounds; midpoint maps to \(0.5\).
Q: Why is MinMaxScaler categorized as an Estimator while Tokenizer is categorized as a Transformer? A: A Tokenizer splits text with fixed character rules (e.g., whitespace) and needs zero prior knowledge of the full dataset. MinMaxScaler must compute global minimum and maximum across all rows before it can scale any single value. Any algorithm that needs dataset-wide summary statistics is an Estimator.
Comparison.
| Dimension | Transformer | Estimator |
|---|---|---|
| Needs full-data scan? | No | Yes (.fit) |
| Call pattern | .transform |
.fit then .transform |
| Examples | Tokenizer, Bucketizer (fixed splits), SQLTransformer |
MinMaxScaler, StandardScaler, StringIndexer, PCA |
| Output of “train” step | N/A (already ready) | A model Transformer |
When to pick which: if the rule is fully known from configuration alone → Transformer; if the rule depends on data statistics or learned parameters → Estimator.
Scope: The Estimator/Transformer split is about whether parameters come from data, not about whether the stage is “ML” or “preprocessing.” A scaler and a logistic regression are both Estimators; a tokenizer and a trained model’s prediction stage are both Transformers.
Visual intuition. Draw a two-box flowchart: DataFrame → fit → Model (stores \(x_{\min}, x_{\max}\)) → transform → Scaled DataFrame. For Tokenizer, collapse to a single arrow: DataFrame → transform → Token DataFrame. Takeaway: Estimators insert an obligatory learn step before transform.
Pitfalls:
- Calling
.transform()on an unfitted Estimator (Attribute/type error or misuse). - Fitting on the full dataset then evaluating on the same rows without a holdout — leakage (especially when scaler fit includes test rows).
- Expecting Transformers to overwrite columns; they append (immutable DataFrames).
- Fitting StringIndexer on train and applying a different mapping on test — always reuse the fitted model’s metadata.
Real-world & domain. Production recommendation and fraud pipelines fit scalers and indexers on training partitions only, package them inside a PipelineModel, and ship that model so scoring clusters apply the same learned mins, maxes, and category maps — never re-fit at serve time on a single batch.
Recap: Transformers append deterministic columns; Estimators .fit to learn, then .transform. Next: the concrete feature toolkit built from these two abstractions.
15.4 Feature Transformation and Feature Engineering Toolkit
Hook: Spark will not “figure out” that age, income, and city belong together. You must assemble, scale, encode, and (often) reduce them with explicit stages — this section is that toolkit.
Spark MLlib ships transformers and estimators for numerical, categorical, text, SQL, and dimensionality-reduction feature work. Each tool is either a one-pass Transformer or a two-pass Estimator (§15.3). Companion texts list feature standardization and related primitives among core MLlib utilities used before classification, regression, and recommendation.
Intuition: Think of a prep kitchen: peel (clean), weigh and portion (scale), label ingredients (encode categories), chop phrases into tokens (text), and plate only the essential items (assemble / PCA). The analogy breaks when a stage must taste the whole batch first (global statistics) — that stage is an Estimator, not a fixed knife cut.
15.4.1 VectorAssembler
VectorAssembler is a Transformer that merges multiple numeric columns into one vector column. Spark ML estimators expect independent variables in a single features (or similarly named) vector.
from pyspark.ml.feature import VectorAssembler
# Combine separate feature columns into a single 'features' vector
assembler = VectorAssembler()
assembler.setInputCols(["feature_1", "feature_2", "feature_3"])
assembler.setOutputCol("features")
# Apply transformation
assembled_df = assembler.transform(df)
Worked example.
Row values in separate columns: feature_1=1.0, feature_2=2.0, feature_3=3.0. After assembly the features column holds the dense vector \[ [1.0,\ 2.0,\ 3.0]. \] Answer: one dense vector column. Sense-check: order follows setInputCols order.
15.4.2 Bucketizer
Bucketizer is a Transformer that discretizes a continuous feature into bucket indices using user-defined splits \(S = [s_0, s_1, \ldots, s_k]\). Value \(x\) maps to bucket index \(i\) when \[ s_i \le x < s_{i+1}, \] with \(i \in \{0, 1, \ldots, k-1\}\). (The last interval may include the upper endpoint depending on configuration; default teaching form uses half-open intervals as above.)
from pyspark.ml.feature import Bucketizer
# Define explicit bucket split boundaries
splits = [-float("inf"), 0.0, 10.0, 50.0, float("inf")]
bucketizer = Bucketizer()
bucketizer.setSplits(splits)
bucketizer.setInputCol("age")
bucketizer.setOutputCol("age_bucket")
bucketed_df = bucketizer.transform(df)
Worked example — ages into buckets.
Splits: \([-\infty, 0, 10, 50, \infty]\) → buckets \(0,1,2,3\).
| age \(x\) | Inequality | Bucket |
|---|---|---|
| \(-1\) | \(-\infty \le -1 < 0\) | 0 |
| \(7\) | \(0 \le 7 < 10\) | 1 |
| \(10\) | \(10 \le 10 < 50\) | 2 |
| \(80\) | \(50 \le 80 < \infty\) | 3 |
Sense-check: boundary \(10\) falls in the interval that starts at \(10\), matching \(s_i \le x < s_{i+1}\).
15.4.3 QuantileDiscretizer
QuantileDiscretizer is an Estimator that builds percentile-based buckets (e.g., 5 buckets ≈ 20th, 40th, 60th, 80th, 100th percentiles). Unlike Bucketizer’s hardcoded splits, it scans the data to learn split boundaries, then returns a model that behaves like a Bucketizer.
from pyspark.ml.feature import QuantileDiscretizer
# Set up 5 percentile buckets
discretizer = QuantileDiscretizer()
discretizer.setNumBuckets(5)
discretizer.setInputCol("income")
discretizer.setOutputCol("income_quantile")
# Fit estimator to compute quantile boundaries, then transform
discretizer_model = discretizer.fit(df)
quantile_df = discretizer_model.transform(df)
Comparison — Bucketizer vs QuantileDiscretizer.
| Bucketizer | QuantileDiscretizer | |
|---|---|---|
| Type | Transformer | Estimator |
| Splits | User-fixed | Learned from quantiles |
| Use when | Domain thresholds known (age bands, legal limits) | Equal-mass bins desired without hand-tuning |
When to pick which: fixed business rules → Bucketizer; exploratory equal-frequency bins → QuantileDiscretizer.
15.4.4 StandardScaler
StandardScaler is an Estimator that standardizes features to zero mean and unit variance (Z-score). For raw value \(x\), \[ z = \frac{x - \mu}{\sigma}, \] where \(\mu\) is the column mean across rows, \(\sigma\) is the column standard deviation, and \(z\) is the standardized score.
Limiting check: if \(\sigma \rightarrow 0\) (constant column), division is undefined — drop or avoid scaling that column. If \(\mu=0\) already and \(\sigma=1\), then \(z=x\).
from pyspark.ml.feature import StandardScaler
scaler = StandardScaler()
scaler.setWithMean(True)
scaler.setWithStd(True)
scaler.setInputCol("features")
scaler.setOutputCol("scaled_features")
scaler_model = scaler.fit(df)
standardized_df = scaler_model.transform(df)
Worked Z-score.
Column values \(\{2, 4, 6, 8\}\): \(\mu = 5\), sample-style intuition \(\sigma \approx 2.58\) (population \(\sigma=\sqrt{5}\approx 2.236\) depending on estimator). Using population \(\sigma=\sqrt{((2-5)^2+(4-5)^2+(6-5)^2+(8-5)^2)/4}=\sqrt{5}\): \[ z(2)=\frac{2-5}{\sqrt{5}}\approx -1.342,\quad z(5)\ \text{would be}\ 0,\quad z(8)=\frac{8-5}{\sqrt{5}}\approx 1.342. \] Answer: symmetric Z-scores about 0. Sense-check: mean of \(z\) is 0; spread is order 1.
15.4.5 MinMaxScaler
MinMaxScaler is an Estimator that linearly maps values into \([\min_{\mathrm{new}},\ \max_{\mathrm{new}}]\) (often \([0,1]\)): \[ x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}}\,(\max_{\mathrm{new}} - \min_{\mathrm{new}}) + \min_{\mathrm{new}}. \] Symbols: \(x\) raw value; \(x_{\min}, x_{\max}\) global extremes from .fit; \(\min_{\mathrm{new}}, \max_{\mathrm{new}}\) target bounds.
Special case: \(\min_{\mathrm{new}}=0\), \(\max_{\mathrm{new}}=1\) recovers \(x'=(x-x_{\min})/(x_{\max}-x_{\min})\). Boundary check: \(x=x_{\min}\Rightarrow x'=\min_{\mathrm{new}}\); \(x=x_{\max}\Rightarrow x'=\max_{\mathrm{new}}\).
from pyspark.ml.feature import MinMaxScaler
scaler = MinMaxScaler()
scaler.setMin(0.0)
scaler.setMax(1.0)
scaler.setInputCol("features")
scaler.setOutputCol("scaled_features")
scaler_model = scaler.fit(df)
rescaled_df = scaler_model.transform(df)
15.4.6 ElementwiseProduct
ElementwiseProduct is a Transformer applying a Hadamard (element-wise) product with a fixed weight vector. For \(\mathbf{x}, \mathbf{w} \in \mathbb{R}^{d}\), \[ \mathbf{y} = \mathbf{x} \circ \mathbf{w} = [x_1 w_1,\ x_2 w_2,\ \ldots,\ x_d w_d]^{T}. \]
from pyspark.ml.feature import ElementwiseProduct
from pyspark.ml.linalg import Vectors
# Define scaling weight vector
scaling_vector = Vectors.dense([10.0, 15.0, 20.0])
transformer = ElementwiseProduct()
transformer.setScalingVector(scaling_vector)
transformer.setInputCol("features")
transformer.setOutputCol("scaled_features")
scaled_df = transformer.transform(df)
Worked Hadamard product.
\(\mathbf{x}=[1.0,\ 0.1,\ -1.0]\), \(\mathbf{w}=[10.0,\ 15.0,\ 20.0]\): \[ \mathbf{y}=[1.0\times 10.0,\ 0.1\times 15.0,\ (-1.0)\times 20.0]=[10.0,\ 1.5,\ -20.0]. \] Answer: \([10.0,\ 1.5,\ -20.0]\). Sense-check: signs preserved; magnitudes scaled by weights.
15.4.7 Categorical Feature Encoding: StringIndexer and IndexToString
Teaching warning: Machine learning algorithms cannot directly process string categories. Convert strings to numeric indices (and often to one-hot vectors) before assembly and model fitting.
StringIndexer is an Estimator that maps string labels to Double indices: index 0 = most frequent category, 1 = second most frequent, and so on. IndexToString is a Transformer that reverses the mapping using metadata stored by the indexer — converting predicted numeric indices back to original strings without rescanning frequencies.
from pyspark.ml.feature import StringIndexer, IndexToString
# Forward encoding: String category to double index
indexer = StringIndexer()
indexer.setInputCol("category_str")
indexer.setOutputCol("category_idx")
indexer_model = indexer.fit(df)
indexed_df = indexer_model.transform(df)
# Reverse decoding: Double index back to string category
converter = IndexToString()
converter.setInputCol("prediction_idx")
converter.setOutputCol("predicted_category_str")
reverted_df = converter.transform(indexed_df)
Frequency-ordered indexing.
Column values: ["red","blue","red","green","red","blue"]. Frequencies: red 3, blue 2, green 1 → mapping \(\mathrm{red}\mapsto 0\), \(\mathrm{blue}\mapsto 1\), \(\mathrm{green}\mapsto 2\). Answer: most frequent → 0. Sense-check: IndexToString on prediction 0.0 yields "red" when metadata is present.
15.4.8 OneHotEncoder
OneHotEncoder is an Estimator that maps category indices to binary vectors. For \(K\) distinct categories it produces a sparse vector of length \(K-1\) by default (drop-last) or \(K\) if configured to keep all levels. A single 1 marks the active category; elsewhere 0.
from pyspark.ml.feature import OneHotEncoder
encoder = OneHotEncoder()
encoder.setInputCol("category_idx")
encoder.setOutputCol("category_vec")
encoder_model = encoder.fit(indexed_df)
encoded_df = encoder_model.transform(indexed_df)
One-hot sketch for \(K=3\) (drop-last ⇒ length 2).
| Index | Sparse one-hot (typical drop-last) |
|---|---|
| 0 | \((2,\ [0],\ [1.0])\) |
| 1 | \((2,\ [1],\ [1.0])\) |
| 2 | \((2,\ [],\ [])\) (baseline / all zeros) |
Sense-check: storing length \(K-1\) avoids perfect multicollinearity among dummy columns.
15.4.9 SQLTransformer
SQLTransformer is a Transformer that runs a Spark SQL statement inside a pipeline. The keyword __THIS__ stands for the incoming DataFrame.
from pyspark.ml.feature import SQLTransformer
# Execute custom SQL aggregation/transformation within pipeline
sql_transformer = SQLTransformer()
sql_transformer.setStatement(
"SELECT *, (quantity * unit_price) AS total_amount FROM __THIS__"
)
transformed_df = sql_transformer.transform(sales_df)
Worked SQL feature.
Row: quantity=3, unit_price=12.5 → total_amount = 3 \times 12.5 = 37.5. Answer: 37.5 appended as a new column. Sense-check: __THIS__ is replaced by the stage input relation name at runtime.
15.4.10 Text Processing: Tokenizer, StopWordsRemover, NGram, and CountVectorizer
Spark MLlib provides an end-to-end text path:
- Tokenizer (Transformer) — split raw text into lowercase tokens on whitespace.
- StopWordsRemover (Transformer) — drop low-information words (“the”, “is”, “of”); languages include English, Danish, Dutch, Finnish, Turkish, and others.
- NGram (Transformer) — emit contiguous \(n\)-gram sequences to capture collocation.
- CountVectorizer (Estimator) — map token collections to sparse term-frequency vectors over a learned vocabulary.
from pyspark.ml.feature import Tokenizer, StopWordsRemover, NGram
# 1. Tokenize text
tokenizer = Tokenizer(inputCol="text", outputCol="words")
words_df = tokenizer.transform(text_df)
# 2. Remove stop words (English default)
remover = StopWordsRemover(inputCol="words", outputCol="filtered_words")
clean_df = remover.transform(words_df)
# 3. Generate bigrams (n=2)
ngram = NGram(n=2, inputCol="filtered_words", outputCol="bigrams")
ngram_df = ngram.transform(clean_df)
Text trace.
Text: "The quick fox is quick". Tokenizer → ["the","quick","fox","is","quick"]. StopWordsRemover → ["quick","fox","quick"]. Bigrams (\(n=2\)) → ["quick fox","fox quick"]. Answer: two bigrams from three tokens. Sense-check: stop words removed before n-grams so collocations are content-bearing.
15.4.11 Dimensionality Reduction: PCA (Principal Component Analysis)
PCA is an Estimator that projects feature vectors from \(\mathbb{R}^{d}\) onto the top \(k\) principal components (\(k < d\)) that preserve maximum variance. Textbook definition: PCA finds directions of maximum variance in high-dimensional data and projects onto a smaller subspace while retaining most information; it is used when variables are strongly correlated (e.g., exploratory reduction before modeling).
from pyspark.ml.feature import PCA
pca = PCA()
pca.setK(2) # Reduce feature space to top 2 principal components
pca.setInputCol("features")
pca.setOutputCol("pca_features")
pca_model = pca.fit(df)
pca_df = pca_model.transform(df)
Scope: PCA assumes features are on comparable scales — standardize first when units differ. It finds linear variance directions; nonlinear structure may need other methods. Choosing \(k\) trades compression against retained variance.
Visual intuition. Scatter points in \(\mathbb{R}^{2}\) elongated along a diagonal: the first principal component is that diagonal axis; the second is orthogonal and shorter. Projecting to \(k=1\) collapses onto the long axis. Takeaway: PCA keeps the directions where data spreads most.
Pitfalls (toolkit-wide):
- Assembling string columns without StringIndexer / OneHotEncoder.
- Fitting QuantileDiscretizer / scalers / PCA on data that already includes the test fold (leakage).
- Using Bucketizer when you meant equal-frequency bins (or vice versa).
- Forgetting
VectorAssemblerso the estimator cannot find a single features column. - Applying PCA before scaling when feature units differ by orders of magnitude.
Real-world & domain. Retail and ad pipelines chain SQLTransformer (derived amounts), StringIndexer + OneHotEncoder (store and segment), StandardScaler or MinMaxScaler, optional PCA, then a classifier — all inside one Pipeline artifact for identical train and serve transforms. Multilingual document analytics use StopWordsRemover language packs before CountVectorizer-based models.
Recap: Feature tools are Transformers or Estimators that end in one numeric vector column. Next: fit models, tune hyperparameters, and evaluate on distributed data.
15.5 Distributed Model Training, Hyperparameter Tuning, and Evaluation
Hook: Features are ready in one vector column. The remaining job is mechanical but consequential: split, fit, predict, score — and, when accuracy stalls, search hyperparameters without writing nested for loops by hand.
Once features are assembled, training instantiates ML Estimators, fits them on training partitions, predicts on test partitions, and evaluates with metric objects. This section uses the procedural spine: purpose, inputs/outputs, steps, concrete traces, cost notes, and when to use alternatives.
15.5.1 End-to-End Supervised Regression Workflow
Purpose. Learn a linear map from features to a continuous label (e.g., grades from study time) on a Spark cluster, then measure holdout \(R^{2}\).
Inputs & Outputs.
| Item | Role |
|---|---|
| Input CSV / DataFrame | Raw columns (e.g., study_time, grades) |
VectorAssembler |
Builds features vector |
randomSplit([0.7,0.3], seed) |
Train / test DataFrames |
LinearRegression Estimator |
Fits coefficients + intercept |
LinearRegressionModel |
Transformer producing prediction |
RegressionEvaluator |
Scalar metric (r2, rmse, …) |
Steps.
- Create
SparkSessionand read data. - Assemble feature columns; select
features+ label. - Split with a fixed seed for reproducibility.
- Instantiate
LinearRegression; optionallyexplainParams(). .fit(train_data)→ model; readcoefficients,intercept..transform(test_data)→ predictions.RegressionEvaluator.evaluate(...)→ metric.
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.regression import LinearRegression
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.sql import SparkSession
# Initialize Spark Session
spark = SparkSession.builder.appName("SparkMLExample").getOrCreate()
# 1. Read input dataset
data = spark.read.csv("study_grades.csv", header=True, inferSchema=True)
# 2. Assemble feature columns into single 'features' vector
assembler = VectorAssembler(inputCols=["study_time"], outputCol="features")
final_data = assembler.transform(data).select("features", "grades")
# 3. Split dataset into 70% training and 30% testing sets with seed
train_data, test_data = final_data.randomSplit([0.7, 0.3], seed=42)
# 4. Instantiate Linear Regression Estimator
linreg = LinearRegression(featuresCol="features", labelCol="grades")
# Inspect algorithm hyperparameter options
print(linreg.explainParams())
# 5. Fit Estimator on training dataset to produce trained LinearRegressionModel
model = linreg.fit(train_data)
# Extract learned model parameters (weight coefficient and intercept)
print(f"Model Coefficient: {model.coefficients}")
print(f"Model Intercept: {model.intercept}")
# 6. Generate predictions and evaluate on held-out test dataset
predictions = model.transform(test_data)
predictions.select("features", "grades", "prediction").show(5)
# 7. Evaluate model performance using RegressionEvaluator
evaluator = RegressionEvaluator(
labelCol="grades", predictionCol="prediction", metricName="r2"
)
r2_score = evaluator.evaluate(predictions)
print(f"Test R-Squared Score: {r2_score}")
Trace — tiny numeric story.
Suppose after fit: \(\hat{y} = 2.5\,x + 40\) where \(x=\) study hours, \(\hat{y}=\) grade. For test point \(x=4\): prediction \(2.5\times 4 + 40 = 50\). If true grade is \(52\), residual \(=2\). Across many points, \(R^{2}\) near 1 means predictions track label variance; near 0 means little better than predicting the mean. Answer for this row: prediction 50. Sense-check: more study hours ⇒ higher predicted grade given positive coefficient.
Complexity & cost. Linear regression on Spark scales with rows × features × iterations of the underlying optimizer; sparse high-\(d\) problems lean on sparse vector kernels (§15.1.3). Shuffle cost appears mainly at the train/test split and during metric aggregation.
15.5.2 Automated Hyperparameter Tuning with ParamGridBuilder
Purpose. Search a grid of hyperparameters automatically with cross-validation instead of hand-written nested loops.
ParamGridBuilder builds the Cartesian product of parameter lists. Example: 3 values of elasticNetParam × 2 values of regParam ⇒ \[ 3 \times 2 = 6 \] model configurations. CrossValidator trains each configuration on numFolds folds (here 3), scores with an Evaluator, and keeps bestModel.
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator
from pyspark.ml.evaluation import RegressionEvaluator
# Construct parameter grid
param_grid = (
ParamGridBuilder()
.addGrid(linreg.elasticNetParam, [0.0, 0.5, 1.0])
.addGrid(linreg.regParam, [0.01, 0.1])
.build()
)
# Set up cross-validation pipeline
cross_val = CrossValidator(
estimator=linreg,
estimatorParamMaps=param_grid,
evaluator=RegressionEvaluator(labelCol="grades", metricName="rmse"),
numFolds=3,
)
# Fit cross-validator to execute 6 model variant trials
cv_model = cross_val.fit(train_data)
best_model = cv_model.bestModel
Trace — trial count.
Grid size \(= 3 \times 2 = 6\). With numFolds=3, each configuration trains on 3 fold splits ⇒ about \(6 \times 3 = 18\) fitting runs (plus final refit patterns depending on Spark version internals). Answer: 6 configurations; order‑of‑magnitude 18 fold fits. Sense-check: cost multiplies grid size by folds — keep grids coarse first.
When to use / alternatives. Use grid + CV when the parameter space is small and discrete. Prefer random/smarter search or coarser grids when each fit is expensive. Do not tune on the final test set — tune inside training data via CV, then report test metrics once.
15.5.3 Distributed Classification and Binary Evaluators
Purpose. Train classifiers such as RandomForestClassifier or NaiveBayes, then score binary decisions with ROC-oriented metrics.
BinaryClassificationEvaluator measures ranking quality via Area Under the ROC Curve (AUC). ROC plots true-positive rate (vertical) against false-positive rate (horizontal) as the decision threshold moves; AUC summarizes that curve into one number in \([0,1]\) (0.5 ≈ random, 1.0 ≈ perfect separation).
from pyspark.ml.classification import RandomForestClassifier
from pyspark.ml.evaluation import BinaryClassificationEvaluator
# Instantiate Random Forest Classifier
rf = RandomForestClassifier(
featuresCol="features", labelCol="label", numTrees=100
)
# Train model on training data
rf_model = rf.fit(train_data)
# Generate predictions on test data
predictions = rf_model.transform(test_data)
# Evaluate model using Area Under ROC Curve (AUC)
evaluator = BinaryClassificationEvaluator(
labelCol="label", metricName="areaUnderROC"
)
auc_score = evaluator.evaluate(predictions)
print(f"Area Under ROC (AUC): {auc_score}")
AUC reading.
If auc_score = 0.91, the model ranks a random positive ahead of a random negative about 91% of the time. Answer: strong discrimination (not a calibrated probability by itself). Sense-check: AUC \(0.5\) would mean no ranking skill.
Visual intuition. ROC curve: x-axis = false-positive rate \(\in[0,1]\), y-axis = true-positive rate \(\in[0,1]\). A curve bowed toward the top-left is better; the area under that curve is AUC. Diagonal line = random classifier.
Pitfalls:
- Reporting training \(R^{2}\)/AUC only — always hold out or cross-validate.
- Expanding ParamGrid until CV becomes overnight-expensive without checking fold × grid product.
- Using
BinaryClassificationEvaluatoron multiclass labels without adapting the problem. - Comparing models tuned on the test set (optimistic bias).
Real-world & domain. Study-time regression demos the full Estimator → Model → Evaluator path used in tutoring analytics; the same pattern scores credit-risk forests with AUC and tunes elastic-net logistic models on click logs with ParamGridBuilder across a cluster.
Recap: Assemble → split → fit → transform → evaluate; multiply trials carefully with ParamGrid × folds; use AUC for binary ranking quality. Appendices next: exam scope and industry touchpoints.
Exam Guidance Summary
- Regular Examination Scope: Topics in Lecture 15 (Spark MLlib, feature engineering transformers/estimators, pipelines, and distributed model tuning) will NOT appear on the regular Saturday examination.
- Makeup Examination Scope: Lecture 15 material WILL be in syllabus for makeup-exam students. Questions stay within concepts, code patterns, and abstractions presented in class (Transformer vs Estimator, vector formats, named feature tools, ParamGrid/CrossValidator, evaluators).
- Cassandra Module Note: Apache Cassandra storage integration (~45 minutes) was postponed for time. An optional supplementary session may be scheduled after examinations if students request it.
Exam note: For makeup examinees, prioritize definitions and call patterns: .fit / .transform, dense vs sparse vectors, which feature tools are Estimators, and how grid size multiplies with numFolds. Regular-exam takers can treat this lecture as enrichment toward projects and industry practice.
Key Industry Applications
- Large-Scale Distributed Training: Logistic regression feature spaces up to about \(10^{7}\) features and Random Forests up to about \(10^{4}\) features across commodity clusters — clickstream, ads, and assortment models stay inside Spark’s DataFrame runtime.
- Multi-Language Text Analytics: Enterprise stop-word filtering over multilingual corpora (English, Danish, Dutch, Finnish, Turkish, …) with
StopWordsRemoverbefore vectorization and classification. - SQL Pipeline Integration: Derived business metrics (e.g.,
quantity * unit_price) computed inside ML pipelines viaSQLTransformerand the__THIS__binding — one artifact for ETL-like features and model scoring. - Pipeline Standardization: Package feature extraction, model fitting, and evaluation into reproducible
Pipeline/PipelineModeldeployment units so train-time encodings and scalers match serve-time transforms.
Industry value is less about a single algorithm and more about repeatable distributed pipelines: same stages, same metadata, same metrics — from laptop prototype to cluster production.
BDA Lecture 15 notes
Sections Breakdown
Horizontal scaling of compute, the pyspark.ml vs mllib packages, and per-algorithm feature capacity limits.
The four-stage ML lifecycle, Transformer/Estimator/Evaluator/Pipeline abstractions, and dense versus sparse vectors.
Immutable append-only transforms, the fit-then-transform two-pass flow, and leakage prevention.
VectorAssembler, bucketizers, scalers, categorical encoders, SQLTransformer, text stages, and PCA.
End-to-end regression, ParamGridBuilder with CrossValidator, and binary AUC evaluation.
Exam scope for the regular and makeup examinations, plus the postponed Cassandra note.
Distributed training, multilingual text analytics, SQL pipeline integration, and pipeline standardization.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Spark MLlib Overview, Scalability, and Architectural Foundations
Must-know: Prefer horizontal cluster scaling; use pyspark.ml (DataFrame); DT~1k, RF~10k, LR~10M features; rows unbounded by cluster resources.
\[\text{Total Cluster Cores} = N \times c\]
Top pitfall: Miscounting cores per node or assuming linear speedup despite shuffle bottlenecks; importing deprecated pyspark.mllib.
Self-check: A cluster has 100 nodes with 4 cores each; add 50 nodes. What is the new total core count?
Connects to: 15.2, 15.5
Spark ML Machine Learning Pipeline Architecture
Must-know: Four lifecycle stages; Transformer/Estimator/Evaluator/Pipeline roles; dense vs sparse vector formats; feature engineering is 60-70% of effort.
\[\mathbf{v}_{\mathrm{sparse}} = (N,\ \mathrm{indices},\ \mathrm{values})\]
Top pitfall: Leaving string/null features in place; confusing Evaluator with Estimator; densifying huge one-hot vectors.
Self-check: Write the sparse representation of a length-1000 vector with 2.0 at index 100 and 3.0 at index 200.
Connects to: 15.3, 15.4
Core Abstractions: Transformers vs. Estimators
Must-know: Immutable DataFrames imply append-only transforms; an Estimator fits then returns a Transformer; Tokenizer is a Transformer, MinMaxScaler is an Estimator.
\[\texttt{fit}(df)\rightarrow\text{Transformer};\quad \texttt{transform}(df)\rightarrow df'\]
Top pitfall: Calling transform before fit; fitting scalers on train+test (leakage); expecting in-place column edits.
Self-check: Is Bucketizer with user-fixed splits a Transformer or an Estimator? Why?
Connects to: 15.2, 15.4
Feature Transformation and Feature Engineering Toolkit
Must-know: Know each tool's Transformer vs Estimator type and core formula (bucket inequality, Z-score, min-max, Hadamard); strings must be indexed or encoded.
\[z=\frac{x-\mu}{\sigma},\quad x'=\frac{x-x_{\min}}{x_{\max}-x_{\min}}(\max_{\mathrm{new}}-\min_{\mathrm{new}})+\min_{\mathrm{new}},\quad \mathbf{y}=\mathbf{x}\circ\mathbf{w}\]
Top pitfall: Feeding strings to estimators; leakage by fitting scalers on full data; skipping VectorAssembler; PCA without scaling.
Self-check: Given splits [-inf,0,10,50,inf], which bucket is age=10?
Connects to: 15.3, 15.5
Distributed Model Training, Hyperparameter Tuning, and Evaluation
Must-know: Full regression workflow; grid size = product of param lists; CV multiplies cost by numFolds; AUC via BinaryClassificationEvaluator.
\[\text{grid size}=3\times 2=6\]
Top pitfall: Tuning on the test set; ignoring fold-by-grid training cost; using binary AUC evaluator incorrectly on multiclass.
Self-check: elasticNetParam has 3 values and regParam has 2; how many ParamGrid configurations?
Connects to: 15.1, 15.4
Exam Guidance Summary
Must-know: Regular exam: Lecture 15 excluded. Makeup exam: Lecture 15 included.
Top pitfall: Studying Lecture 15 for the regular Saturday exam when it is explicitly excluded.
Self-check: Is Lecture 15 on the regular Saturday exam?
Connects to: 15.1, 15.5
Key Industry Applications
Must-know: Pipelines standardize train/serve feature logic; sparse high-dimensional training and multilingual text are the primary industry hooks.
Top pitfall: Re-fitting indexers/scalers independently at serve time instead of shipping PipelineModel metadata.
Self-check: Which Spark stage embeds arbitrary SQL with __THIS__ inside an ML pipeline?
Connects to: 15.2, 15.4