Skip to main content
Big Data Analytics

Spark MLlib: A Framework for Machine Learning at Scale

Published: 2026-08-01
Level: postgraduate
Audience: Postgraduate students studying distributed machine learning

16.1 Spark MLlib: Motivation, Uses, and Scale

Hook — what happens when your training data outgrows your machine? A single laptop holds a few million rows in memory at best. The moment your dataset reaches billions of rows — routine for clickstreams, retail transactions, and sensor logs — no amount of clever code on one machine will help. This section explains the idea that makes Spark MLlib different from libraries like scikit-learn or TensorFlow running on a single box: instead of buying a bigger machine, you add more machines. That idea, horizontal scaling, is why the whole lecture starts with scale.

16.1.1 Why Spark MLlib: Horizontal Scaling of Compute

Spark MLlib earns its place among machine learning offerings such as TensorFlow and scikit-learn because of one core property: it runs on top of a distributed cluster built from commodity hardware, so you can grow your compute power by adding machines rather than by buying a bigger single machine. This idea is called horizontal scaling, and it is the first reason anyone reaches for MLlib.

Intuition — a fleet of delivery vans, not one bigger truck. Vertical scaling is upgrading a single truck to carry more parcels: eventually the road, the engine, and the truck design set a hard limit. Horizontal scaling is adding more vans to the fleet: each van is cheap and ordinary (commodity hardware), but ten vans move more parcels than one truck ever could, and you can keep adding vans. The mapping is direct: each van is one cluster node, each parcel is one unit of compute work, and the fleet manager is the cluster scheduler that hands parcels to whichever van is free. The analogy breaks in one place: vans work independently, but cluster nodes must occasionally coordinate — shuffle data between machines, agree on results — and that coordination costs time, so scaling is not perfectly free.

A single powerful machine — say a high-end gaming PC — is capped at somewhere around 64 to 128 CPU cores. Beyond that, you physically cannot add cores. A Spark cluster has no such wall. If you decide your data is too large or your program is running slowly, you simply add more commodity machines to the cluster and immediately gain their cores. Because MLlib is a core component of Spark, it inherits this scalability for free. Every machine learning workload — training loops, entropy calculations, information-gain computations — runs across however many systems you choose to put in the cluster.

There is also a strategic reason to prefer scaling over optimizing. If you face a choice between making your model run faster on one machine (optimization) or training it on more data across a cluster (scaling), scaling is the better investment. The amount of data generated every year grows exponentially, and pattern detection across a larger dataset almost always beats micro-optimizing a single-node training loop. This is why the course frames MLlib around scale first: your modeling effort is better spent on ingesting more records than on squeezing the last few percent of speed out of local code.

Assumptions & scope: Horizontal scaling pays off only when the workload can actually be split across machines. Training loops and statistics such as entropy, information gain, means, and variances are naturally parallel: each node processes its slice of rows and the partial results are combined. If instead every step of an algorithm needs to see all the data at once, communication between nodes dominates and adding machines stops helping. Scaling also assumes the cluster tolerates individual machine failures — a single commodity machine can and does die, which is exactly why Spark tracks which node holds which piece of data and recomputes lost pieces from the recorded lineage.

MLlib is available in several languages — Python (as pyspark.ml), Java, and Scala. One point worth clearing up about that list:

Q: Is MLlib available in R as well as the other languages, since it was initially claimed to be available in several languages? A: No. MLlib is not supported on R. The corrected list of supported languages is Python, Java, and Scala.

16.1.2 Worked Example: Adding Nodes to a Cluster

The scaling argument is easiest to see with numbers. The framing used in the discussion runs like this.

Worked example — cluster capacity. Suppose you have 100 commodity systems, each with 2 cores. The stated result is that you immediately get access to:

\[ \text{Total cores} = 100 \text{ systems} \times 2 \text{ cores per system} = 400 \text{ cores} \]

where \(\text{Total cores}\) is the aggregate compute available across the cluster. When the program slows down or the dataset grows, you add roughly 50 more computers, and the stated result is that you gain access to:

\[ \text{Additional cores} = 50 \text{ systems} \times 2 \text{ cores per system} = 300 \text{ cores} \]

Reconciliation note: with the stated figure of 2 cores per system, the direct products are \(100 \times 2 = 200\) cores and \(50 \times 2 = 100\) additional cores. The lecture's headline figures (400 and 300) illustrate scale and are consistent with nodes carrying more cores — for example, 4 cores per node gives \(100 \times 4 = 400\). Whichever per-node figure you use, the conclusion is identical: adding commodity nodes multiplies the available parallelism, and no single machine can match that growth.

Sense-check: total compute is a product — nodes times cores per node — so doubling the node count doubles the parallelism. A single fixed machine cannot do that at all. The arithmetic is secondary; the shape of the argument is what matters: every added node multiplies capacity, and that is exactly the growth pattern single-machine optimization cannot reproduce.

Recap + bridge: Horizontal scaling grows compute by adding commodity nodes; a single machine hits a hard core cap, a cluster does not. Next we ask what you actually do with all that compute — and the answer is that MLlib plays two roles: preprocessing only, or full distributed training.

16.1.3 What You Can Use MLlib For

MLlib serves two distinct roles in a machine learning workflow.

Role 1 — Preprocessing and feature engineering only. When you have a very large dataset, you can use MLlib to clean data, find the most important features among all available independent variables, and run algorithms such as PCA to compress the data. The discussion mentions real systems with 250 feature variables — a huge number — where you may only want the top 10 or 15 features to feed into your algorithm. After MLlib finishes this preprocessing, you can dump the resulting smaller, clean dataset into a traditional ML framework such as scikit-learn and do the actual training there. In this mode, MLlib is used purely for the data-preparation stage.

Role 2 — Full distributed training. If your data is still very large even after preprocessing, MLlib can also perform the real training itself. Spark supports both machine learning (ML) and deep learning (DL) workloads, so you are not forced to move the data anywhere else.

This split matters for planning: preprocessing is where most of the engineering effort lives, and MLlib can serve as the preprocessing engine for any downstream framework.

Pitfalls:

  • Assuming MLlib is only for training: its most common industrial use is actually the preprocessing stage — cleaning and compressing data before a smaller framework trains on it.
  • Assuming you must export the data to train: if the data is still huge after preprocessing, MLlib trains in place on the cluster; nothing forces you to move it.
  • Misjudging where the effort goes: almost all the engineering time sits in preparation, not in the algorithm call itself.

Real-world: real systems with hundreds of feature variables (one example mentioned 250) use MLlib to clean data and run PCA to narrow down to the top 10–15 features before handing the data to frameworks like scikit-learn for training. This "MLlib as a preprocessing engine" pattern is common in industry precisely because feature engineering is where most of the work lives.

16.1.4 The Spark Component Stack and the DataFrame API

MLlib sits inside a larger Spark ecosystem, and it helps to see where. The stack discussed in the lecture is:

  • Spark Core — the base engine that handles how data moves from one system to another when a request for data is made.
  • Spark SQL — for processing data with SQL queries.
  • Spark Streaming — for processing data as it arrives, before it rests anywhere (stream processing).
  • Spark MLlib — the machine learning library, the subject of this lecture.
  • GraphX — for modeling social networks and other graph-structured data.

All of these libraries are built on top of the DataFrame APIs. The older RDD API is becoming outdated, and Spark officially deprecates using RDDs for machine learning, so the entire focus here is on the DataFrame-based approach. Even inside MLlib there are two ways to operate — the DataFrame API (with SQL support) and the raw RDD path — but the RDD path is deprecated and not covered.

16.1.5 Scale Limits: Features, Classes, and Training Instances

The scale at which MLlib operates is one of its defining characteristics. The figures below come from the Spark Definitive Guide, the same book that anchors much of the course, and they describe practical limits for popular algorithms:

  • Decision Trees — can handle up to 1,000 features. With that many features, choosing the best feature for the root node or each branch node means computing entropy and information gain across thousands of candidates, which is genuinely complex — and Spark does it.
  • Random Forest — can handle up to 10,000 features and classification with up to 100,000 (100k) distinct output classes. For example, given a set of features, you could classify which of about 100,000 animals it is.
  • Logistic Regression — can go up to 10,000,000 (10 million) features. The spoken figure "up to 10" in the lecture means 10 million; the Spark Definitive Guide confirms that linear models (of which logistic regression is one) handle up to 10 million features.
  • Training instances — there is effectively no limit on the number of training examples, for either classification or regression. Spark does not constrain how much data your model can consume to find patterns.

Worked example — reading the scale table. Put the limits side by side:

Algorithm Maximum features Maximum output classes Training instances
Decision tree 1,000 unlimited
Random forest 10,000 100,000 unlimited
Logistic regression 10,000,000 unlimited

The random-forest row reads as: with a feature vector of up to 10,000 dimensions, the model can still assign one of up to 100,000 distinct classes (for example, which of about 100,000 animal species a set of measurements belongs to). The logistic-regression row: the feature vector may reach 10 million dimensions — feasible in practice only because Spark stores such vectors sparsely. Every row shares the last column: the number of training examples is unbounded.

Pitfalls:

  • Mixing up the three limits: 1,000 features (decision trees) vs 10,000 features (random forests) vs 10 million features (logistic regression). These specific numbers are the kind of detail that appears in exams.
  • Attaching the class limit to the wrong algorithm: the 100,000-class figure belongs to random forests, not decision trees.
  • Forgetting the training-instance point: the limits are on feature and class counts, not on rows — the model can consume as many training examples as the cluster holds.

Exam note: Remember the scale figures — 1,000 features for decision trees, 10,000 features and 100,000 output classes for random forests, and a practically unlimited number of training examples. These specific numbers are the kind of detail that appears in exams.

The Spark Definitive Guide is strongly recommended for MLlib. Its authors are the primary authors of Spark themselves, it gives a clean introduction to MLlib, and every example in the book comes with downloadable code and training datasets from a GitHub site. You can replicate the code, plug in the data, and run it directly.

Real-world: Production teams choose Spark MLlib when feature counts reach the thousands and training data reaches billions of rows — volumes that single-node libraries cannot hold in memory. This is why the scale limits in the table are not academic: they define the region of the design space where Spark is the only practical choice.

Recap: Spark MLlib exists because distributed clusters scale horizontally; it can act as a preprocessing engine or a full training engine, and it handles feature and class counts that single-node libraries cannot. The next section moves from why to how: the four-stage machine learning pipeline and the Spark abstractions — transformers, estimators, evaluators, and pipelines — that formalize it.

16.2 The Machine Learning Pipeline in Spark

Hook — why does every ML project look the same? Whether you are predicting house prices, detecting fraud, or classifying animals, the shape of the work is identical: get the data, clean it, build features, train, and check the result. That repeated shape is called a pipeline. This section lays out the four stages every machine learning program passes through, then shows the exact Spark objects — transformers, estimators, evaluators, pipelines — that turn those stages into code.

16.2.1 The Four Stages of a Machine Learning Workflow

Every machine learning program, regardless of framework, passes through the same four stages:

  1. Load and clean the data. You load data from a CSV file, a database, HDFS, or another source. Whatever the source, raw data is always impure, so you spend dedicated effort cleaning it.
  2. Feature engineering. Here you crunch the features: if you have hundreds of features, you narrow them down to a smaller set, combine features, drop some, find the most important ones, and apply transformations. Somewhere between 60% and 70% of the entire job of machine learning is data preparation; only the last 30% is aided by machine learning algorithms themselves.
  3. Model training with hyperparameter tuning. You feed the cleaned feature output into a chosen model. Along with training, you tune hyperparameters — the algorithm's own settings, such as the depth of a decision tree or the minimum number of nodes after which a tree must split. Training typically involves several trials over the same data with different hyperparameter values.
  4. Evaluation. You quantify how good the model is with objective measures rather than intuition. You compute a quantitative metric and judge whether the algorithm is good or bad.

Intuition — cooking, not magic. Think of the pipeline as preparing a meal. Cleaning is washing and peeling the raw vegetables (raw data is never ready to cook). Feature engineering is chopping and measuring — deciding which ingredients matter, discarding the bruised ones, and putting the rest into a consistent shape. Training is following the recipe, and evaluation is tasting the finished dish against a standard instead of just hoping it is good. The mapping is direct, and it explains a surprising fact: a chef spends most of the time on prep, not on the final cooking step. That is why the lecture stresses that 60–70% of machine learning is data preparation and only the last 30% is helped by the algorithms themselves.

Pitfalls:

  • Rushing the cleaning stage: raw data arrives with missing values, wrong types, duplicates, and outliers; skipping this stage poisons everything downstream.
  • Underestimating feature engineering effort: the 60–70% figure is the lecture's headline — the algorithm call is the smallest part of the job.
  • Treating tuning as optional: hyperparameters (tree depth, minimum split size) are not learned from data; they are chosen by the engineer through repeated trials, and the wrong choice can make a good algorithm perform badly.

16.2.2 Spark's Abstractions for the Pipeline

Spark formalizes these stages with a small set of named abstractions:

  • Transformers — used for feature engineering. A transformer takes a DataFrame in and produces a DataFrame out.
  • Estimators — used both for feature engineering and for model training. The same word is used for both because both operations learn something from the data before producing a usable object (a transformer or a model).
  • Evaluators — used for evaluation. An evaluator gives you a framework where you can pick any metric you care about and get its value. For linear regression, the options include RMSE (root mean squared error), mean squared error, absolute squared error, and \(R^2\). For classification, people discuss the ROC curve, accuracy, specificity, and sensitivity.
  • Pipelines — you declare the transformer, the estimator, the modeling algorithm, the parameter list, and the evaluator as separate objects, then tape them all together into a concept called a pipeline. A single execution line then runs every stage in order over the incoming data. scikit-learn has a similar pipeline concept.
Abstraction Stage it serves DataFrame in → DataFrame out? Learns from data?
Transformer Feature engineering Yes No — applies fixed rules
Estimator Feature engineering and training Yes (after fit) Yes — scans data, returns a transformer or a model
Evaluator Evaluation No — scores predictions against a chosen metric
Pipeline All stages chained Yes Runs every declared stage in order

The two roles of an estimator are the subtle point: a scaler (feature engineering) and a linear regression (training) are both estimators because both must look at the data before they can produce something usable. Everything downstream of this section rests on that idea.

16.2.3 Package Naming: .mllib versus .ml

When you start coding in the MLlib world, the first thing to notice is the package structure. The original MLlib implementation was built on RDDs, but the DataFrame-based approach came later and is the recommended one. Everything under org.apache.spark.mllib (PySpark: pyspark.mllib) is the RDD-based, deprecated API. Everything under org.apache.spark.ml (PySpark: pyspark.ml) is the DataFrame-based API you should use. Java is very similar to Scala. The lecture focuses only on the .ml DataFrame-based code.

Warning — the deprecated path: the .mllib package is the RDD-based, older API, and Spark deprecates using RDDs for machine learning. If you see org.apache.spark.mllib or pyspark.mllib in older tutorials, treat it as legacy; new work belongs in org.apache.spark.ml / pyspark.ml, which sits on the DataFrame API and supports SQL.

16.2.4 Vectors: Dense and Sparse

In Spark ML, labels and features are given as vectors — a structure much like an array of doubles, but slightly more efficient and built for fast calculations. There are two kinds:

  • Dense vector — every position in the array holds a value. A dense vector of size 100 (or 200, or millions) stores values at most or all of its positions. You declare it with vectors.dense(1.0, 2.0, 3.0), which creates a vector of size 3 with values \(1.0, 2.0, 3.0\):

\[ \mathbf{v}_{dense} = [1.0,\ 2.0,\ 3.0] \]

  • Sparse vector — only a few positions hold meaningful values, just like a sparse matrix. You declare it by giving three things: the size of the vector, the locations (indices) that contain data, and the values at those locations. For example, a sparse vector of size 3 with value 2.0 at location 1 and value 3.0 at location 2:

\[ \mathbf{v}_{sparse} = \text{vectors.sparse}(3,\ [1,\ 2],\ [2.0,\ 3.0]) \]

In a sparse vector of 1,000 positions with data only at positions 100 and 200, you would give the size 1000, the indices [100, 200], and the values at those indices. Sparse vectors matter later for one-hot encoding, where most entries of the encoding are zero.

The relevant package is pyspark.ml.linalg, and you import Vector from it. The lecture defines dense vectors as vectors.dense(1, 2, 3) and sparse vectors as vectors.sparse(size, ids, values).

Worked example — declaring both vector kinds. Three values can be stored two ways:

  • Dense: vectors.dense(1.0, 2.0, 3.0) stores all three values explicitly: \([1.0,\ 2.0,\ 3.0]\). Memory cost grows with the size of the vector.
  • Sparse: vectors.sparse(3, [1, 2], [2.0, 3.0]) says: "the vector has 3 positions in total; positions 1 and 2 hold the values 2.0 and 3.0; every other position is zero." The zero at position 0 is never stored.

Now scale the idea: a vector of 1,000 positions with data only at positions 100 and 200 is written vectors.sparse(1000, [100, 200], [value100, value200]). The dense version would store 1,000 numbers; the sparse version stores 2 indices and 2 values. That is the entire point — for vectors that are mostly zeros (like one-hot encodings), sparse storage shrinks memory dramatically.

Visual intuition — a mostly-empty bookshelf. Picture a shelf with 1,000 slots. A dense vector records what is in every slot, even the empty ones — a full inventory list of 1,000 entries. A sparse vector records only the shelf's size (1,000 slots) plus the few occupied slots: "slot 100 has book A, slot 200 has book B, all other slots are empty." If most slots are empty, the sparse description is far shorter, and the cost does not grow with the shelf's total length. The takeaway: dense for small or fully-populated vectors, sparse for large mostly-zero vectors.

16.2.5 Data Types for Supervised, Unsupervised, Recommendation, and Graph Problems

The shape of the input data depends on the kind of problem:

  • Supervised learning — you have features and labels. Features are given as a vector of doubles (you do not pass 12 features as 12 separate columns; you bundle them into one vector). The label — the value to be predicted — must be a double. Integers are not supported, and neither are strings or booleans. Spark operates in doubles in most places: a vector of doubles for features and a double for the label.
  • Unsupervised learning — there are no labels and nothing to predict; you are only detecting patterns. The input is a vector of doubles for features.
  • Recommendation — the data has a column of users, a column of items (the things being recommended), and a column of rewards or ratings. A recommendation system trains on these three columns.
  • Graph analytics — you supply a DataFrame of vertices and a DataFrame of edges, and these go into the training.
Problem kind Input shape Label?
Supervised One vector-of-doubles column of features + one double label column Yes
Unsupervised One vector-of-doubles column of features No — pattern detection only
Recommendation User column, item column, rating/reward column Implicit (the rating)
Graph analytics Two DataFrames: vertices and edges

Pitfalls:

  • Passing features as separate columns: Spark expects one vector column holding all features; you do not hand 12 features as 12 columns.
  • Using an integer label: the label must be a double; integers, strings, and booleans are not accepted — this is why you will see 1.0 rather than 1 throughout Spark ML code.

16.2.6 A First Sample Program

Worked example — the canonical fit/transform program. A minimal Spark ML program looks like this:

  1. Build a DataFrame with a label column and a features column (a two-dimensional dataset to which you associate column names).
  2. Split the data with randomSplit(0.8, 0.2, seed = 42) into 80% training and 20% test. The seed makes the split reproducible — with the same seed, the data does not change between runs. The idea behind splitting: you use the 80% to learn (like studying 80 of 100 questions) and hold out the 20% to evaluate (like testing yourself on the 20 questions you never saw, even though you know the answers).
  3. Create a random forest model with a hyperparameter setting — for example 100 trees instead of the 10 seen in an earlier discussion of decision trees.
  4. Call rf.fit(trainingData) — note that the test data is not passed here.
  5. Call model.transform(testData) — now that the model exists, you feed it the held-out test data, and the returned DataFrame shows the predictions.

The pattern to absorb is the two-phase flow: fit happens only on training data, and only the resulting model is allowed to touch the test data. That separation is what keeps the evaluation honest — the model never sees the test answers while learning. This first example deliberately skips feature engineering and says nothing about what is inside the data. Raw data always needs cleaning, and once you understand how the cleaning and transformation pieces work, the fit and transform pattern becomes easy to follow.

Real-world: The train/test split with a fixed random seed (here 42) is standard practice everywhere, because reproducible splits let multiple engineers compare models on exactly the same held-out data.

Recap + bridge: Every ML program follows four stages (load/clean, feature engineering, training with tuning, evaluation), and Spark names them transformers, estimators, evaluators, and pipelines; features travel as dense or sparse vectors, and the canonical program is fit on training data then transform on test data. The next section zooms into the two workhorses of the pipeline — transformers and estimators — and the exact rule for telling them apart.

16.3 Transformers and Estimators: The Core of Spark ML

Hook — how does Spark decide whether a step must look at your data first? Some transformations can run the moment you configure them: "split this sentence into words" needs no information about any other row. Others cannot: "scale these numbers to fit between 0 and 10" is impossible until you know the data's own minimum and maximum. That single distinction — fixed rule versus learned-from-data — is the entire difference between Spark's two core objects, the transformer and the estimator.

16.3.1 Transformer Mechanics

A transformer takes a DataFrame as input and outputs a DataFrame. Usually it does some processing on one column and creates one more column as a result. Spark mostly operates on immutable data — it does not like modifying data in place, because modifying distributed data in the middle of a cluster is a complicated operation. So instead of editing an existing column, a transformer appends a brand-new column to the DataFrame. If you start with 10 columns and process one of them, you end with 11 columns, where the 11th is the transformed version of the column you specified.

Intuition — a stamping press with a fixed die. Picture a machine that stamps a pattern onto a sheet of paper. The die is fixed before any paper arrives: the machine does not study the paper, it just applies the same pattern to every sheet. A transformer is exactly that. It comes configured with its rule (the die), it needs no information from the data to do its job, and it stamps a new column onto every row it sees. Where the analogy breaks: the stamp adds a new column alongside the old one rather than overwriting the sheet, because Spark data is immutable.

To use a transformer you must tell it which column to operate on. Every transformer has a function setInputCol (and in some cases setInputCols for multiple input columns). Setting the input column is mandatory. The output column is controlled by setOutputCol; this is optional, and if you omit it Spark generates a column name for you automatically.

Different transformers also have transformer-specific parameters, set through set methods (in Python you can instead pass keyword arguments at construction time). For example, a min-max scaler needs minimum and maximum values; a word-splitting transformer does not. After configuring the object, you call one method — transform — passing the data as an argument.

A concrete example is the Tokenizer, which splits a sentence into words. In Scala:

val tokenizer = new Tokenizer()
  .setInputCol("description")
  .setOutputCol("words")
val tokenized = tokenizer.transform(sales.select("description"))

You create the tokenizer object, set which column to transform (description), set the output column, and call transform. The transformer only ever touches the description column, even if you pass it the entire DataFrame.

Worked example — tracing a transformer. Suppose the sales DataFrame has 10 columns, including description with the value "The BDA class is interesting" in one row. The tokenizer is configured to read description and write words. Calling tokenizer.transform(sales):

  • reads the description column,
  • splits each value on whitespace into individual words ("The", "BDA", "class", "is", "interesting"),
  • appends a brand-new words column holding those lists,
  • leaves every other column untouched.

The result is a DataFrame with 11 columns — the original 10 plus words. Nothing was modified in place; the new column was simply appended. The tokenizer needed no knowledge of the rest of the data: splitting on whitespace is a fixed rule, so one transform call finishes the whole job.

16.3.2 Estimator Mechanics: The Two-Pass Flow

An estimator also transforms data, but it cannot work on its own — it must go through the data at least once before it can transform anything. The example used to explain this is min-max scaling. To scale data you give two arguments: the new minimum and the new maximum (say 0 and 10). But the algorithm cannot do anything with just those inputs. It must first scan the data to discover what the current global minimum and global maximum are. If the data ranges from -10 to 10, it needs to know both -10 and 10 before it can map that range onto 0 to 10.

So an estimator performs two passes:

  1. First pass — fit. You call fit with the data. The estimator scans the data once, computes the required metadata (for min-max scaling, the global min and max), and returns a transformer object that carries those learned values.
  2. Second pass — transform. You call transform on the returned transformer, passing the same data, and get the final transformed output.

In Scala:

val scaler = new StandardScaler()
  .setInputCol("features")
  .setOutputCol("scaledFeatures")
val scalerModel = scaler.fit(scaleDF)     // pass 1: learn mean and standard deviation
val scaled = scalerModel.transform(scaleDF) // pass 2: apply the learned statistics

The pattern is always: create the estimator object, set the input column (and any estimator-specific parameters), call fit on the data to get a transformer, then call transform on that transformer with the data.

Intuition — the chef who tastes the soup first. A recipe that says "add salt to taste" cannot be followed until someone tastes the soup. The chef's first pass is tasting: read the soup, decide how much salt it needs, and fix that amount in memory. The second pass is applying: salt every portion by exactly that amount. The estimator is the chef's tasting step — fit reads the whole dataset and records the needed statistics (the "recipe correction") in a returned transformer. That transformer then salts every row with transform. If the data changes, the tasting must be redone — which is exactly why estimators must be re-fit on new data.

16.3.3 The Distinction in One Line

If a transformation contains all the details it needs, you apply it directly as a transformer. If the transformation depends on statistics that must be discovered from the data first, you go through an estimator: fit to discover the metadata, then transform to apply it.

Q: Why is min-max scaling an estimator while tokenizing is a transformer, even though min-max scaling was first offered as a transformer example? A: A tokenizer splits a sentence into words using fixed character rules — it needs no knowledge of the rest of the dataset. Min-max scaling needs the global minimum and maximum values across the entire dataset before it can scale even one value, so it cannot be a pure transformer: it must scan the data first. Any operation that requires dataset-wide summary statistics must be an estimator: it fits on the data first, then transforms.

Dimension Transformer Estimator
Rule source Fixed at configuration time Learned from the data
Passes over data One (transform) Two (fit, then transform)
Returns A new DataFrame A transformer (or model) that carries learned metadata
Example Tokenizer, VectorAssembler, NGram Min-max scaler, StandardScaler, StringIndexer

When to pick which: if the operation is a fixed rule that needs no dataset-wide statistics, use a transformer; if it must discover something from the data first (min, max, mean, standard deviation, vocabulary, category list), use an estimator.

Pitfalls:

  • Calling transform directly on an estimator: an estimator cannot transform anything until fit has produced its transformer; the two-pass flow is mandatory.
  • Reusing a fitted scaler on wildly different data: the learned min/max or mean/standard deviation came from the original dataset; if the new data has a different range, the scaling is wrong unless you re-fit.
  • Thinking every text operation is a transformer: the tokenizer is, because splitting on whitespace needs no data scan — but an operation that must first discover something (like a vocabulary) is an estimator.

Recap + bridge: A transformer applies a fixed rule in one transform call and appends a column; an estimator first fits on the data to learn summary statistics, then transforms with the learned values. This single rule — fixed rule vs. learned-from-data — is how you will classify every object in the two toolkits that follow: the transformer toolkit (section 16.4) and the estimator toolkit (section 16.5).

Real-world: In production ML pipelines, choosing transformer vs estimator correctly determines whether a stage is reproducible and how it behaves on new data. A misclassified estimator (for example, computing min-max bounds from only a sample) silently corrupts every downstream prediction, which is why the fit/transform split is enforced by the API itself — and why libraries like scikit-learn use the identical two-phase design.

16.4 Feature Engineering Toolkit: Transformers

Hook — one rule, eight tools. You already know the rule: a transformer applies a fixed rule and appends a column. This section opens the transformer toolkit — the eight most common transformer objects in Spark ML, from the everyday VectorAssembler to the NLP-oriented NGram. For each one the question to ask is the same: does this need to scan the data first? If the answer is no, it is a transformer and belongs in this section.

16.4.1 VectorAssembler: Combining Columns into One Vector

Spark ML expects all your features to be available in the form of a single vector, unlike scikit-learn, where you hand each column separately. So if your DataFrame holds features in separate columns (in1, in2, in3), you must combine them into one vector column before the model will accept them.

The VectorAssembler does exactly this. It is a transformer — it does not need to scan the data to combine columns; you give it three columns and it knows how to put them together. You set the input columns with setInputCols(Array("in1", "in2", "in3")) and call transform on the data. The result shows the three values for each sample packed into a single vector. This is the most basic and most frequently used transformer, because DataFrame columns eventually have to become a vector to reach the algorithm.

Worked example — three columns become one vector. Suppose a DataFrame has a row with in1 = 4.0, in2 = 7.5, in3 = -2.0. Configuring VectorAssembler with setInputCols(Array("in1", "in2", "in3")) and calling transform produces a new features column holding the single vector \([4.0,\ 7.5,\ -2.0]\) for that row. The three original columns remain untouched; the assembler simply concatenates their values, in the order given, into one vector per row. That vector is exactly what Spark ML algorithms accept as their feature input.

16.4.2 Bucketizer: Turning Continuous Ranges into Buckets

Continuous values with many distinct levels are often not useful as-is. Age is the classic case: a feature variable with a range from 0 to 100 has enormous cardinality, but you may prefer to classify people into a few meaningful groups — infants, toddlers, teenagers, adults, and senior citizens. Similarly, you might classify weight into underweight, average, and overweight. Bucketing replaces a continuous value with the number of the bucket it falls into.

The Bucketizer does this. It is a transformer. You set the input column (the lecture example uses a column called id), and you set the bucket boundaries with the transformer-specific setSplits method. In the example:

new Bucketizer()
  .setInputCol("id")
  .setSplits(Array(-1.0, 5.0, 10.0, 250.0, 260.0))

This creates four buckets: from -1 to 5, from 5 to 10, from 10 to 250, and from 250 to 260. An id of 0 falls into the first bucket (output 0); an id of 10 falls into the third bucket (output 2, covering 10 to 250). To have \(n\) buckets you need \(n+1\) boundaries; the setSplits array must have one more value than the number of buckets, and the Bucketizer takes care of the counting.

Worked example — where does each id land? With setSplits(Array(-1.0, 5.0, 10.0, 250.0, 260.0)) there are five boundary values and so four buckets, numbered from 0:

Bucket index Range Example id Output
0 from -1 to 5 0 0
1 from 5 to 10 7 1
2 from 10 to 250 10, 100 2
3 from 250 to 260 255 3

An id of 0 falls into the first bucket, so the output is the number 0. An id of 10 falls exactly at the start of the third bucket (10 to 250), so the output is 2 — bucket indices are zero-based, which is why the third bucket carries index 2. Sense-check: four buckets with five boundaries, and every id between -1 and 260 maps to exactly one bucket number — no gaps, no overlaps.

One detail worth remembering: because no setOutputCol was given in the example, the output column got an automatically generated, awkward name (something like a random suffix appended to bucket). If you set setOutputCol("id_bucket"), you control the name and avoid the odd auto-generated one.

16.4.3 ElementwiseProduct: Scaling Each Feature by a Factor

When you want to multiply a feature vector element-by-element by a scaling vector, you use the ElementwiseProduct. Where a dot product is a sum of products, an elementwise product is just the products — each position in the feature vector is multiplied by the matching position in the scaling vector.

In the example, the features column holds three values (1, 0.1, -1) in the first row, and the scaling vector is a dense vector (10, 15, 20). Since elementwise multiplication requires both vectors to have the same length, a three-element feature vector pairs with a three-element scaling vector:

\[ (1,\ 0.1,\ -1) \odot (10,\ 15,\ 20) = (1 \times 10,\ 0.1 \times 15,\ -1 \times 20) = (10,\ 1.5,\ -20) \]

where \(\odot\) denotes elementwise (Hadamard) multiplication. The scaling vector is created as vectors.dense(10, 15, 20), the input column is set to features, and transform produces the multiplied result. The ElementwiseProduct is a transformer — no data scan is needed.

Worked example — multiplying position by position. Take the feature vector \((1,\ 0.1,\ -1)\) and the scaling vector \((10,\ 15,\ 20)\). Elementwise multiplication pairs up positions:

\[ (1 \times 10,\ 0.1 \times 15,\ -1 \times 20) = (10,\ 1.5,\ -20) \]

The first feature is scaled by 10 (becomes 10), the second by 15 (\(0.1 \times 15 = 1.5\)), and the third by 20 (\(-1 \times 20 = -20\)). Sense-check: each output entry is the product of exactly one input entry and one scaling entry; the vectors must have equal length, and here three pairs with three matching positions give the final answer (10, 1.5, -20).

Q: Why did the printed output for the elementwise product not match the computation? A: The correct elementwise product of the feature vector (1, 0.1, -1) and the scaling vector (10, 15, 20) is (10, 1.5, -20). The printed output on the slide did not match this result and was flagged for correction.

16.4.4 IndexToString: Converting Numbers Back to Strings

The StringIndexer (covered in the estimators section) converts categorical strings to numbers in the forward direction. When a model predicts a numeric label, you usually want to convert it back to a readable string — a predicted 0 means nothing to a human, but "bad" does. That reverse conversion is done by IndexToString.

An important correction: to go forward (string to number) you need an estimator, because the set of unique categories must be discovered. To go backward (number to string) you do not need an estimator. Spark stores the mapping — "good means 1.0, bad means 0.0" — as metadata on the column during the forward conversion. The reverse transformer reuses that metadata, so it never needs to rescan the data, and it cannot assign a random label; it must return what was established earlier. So IndexToString is a transformer, not an estimator.

Usage is symmetric: setInputCol("labelIndicator"), then transform the data. The forward pass turned "good" into 1.0 and "bad" into 0.0; the reverse pass turns 1.0 back into "good" and 0.0 back into "bad".

Worked example — the round trip. Start with a categorical column holding "good" and "bad". The StringIndexer (estimator) scans it, discovers the two categories, and writes labelIndicator values 1.0 for "good" and 0.0 for "bad", recording that mapping as column metadata. A model then predicts the numeric label — say 1.0. IndexToString with setInputCol("labelIndicator") reads the stored metadata and converts 1.0 back to the human-readable "good". No second scan of the original data happens anywhere in the reverse step: the mapping traveled as metadata, not as a fresh computation.

Q: Is converting numbers back to strings an estimator, just like the forward conversion? It might seem so, because the forward direction needs an estimator to discover the categories. A: No. The reverse conversion is done by IndexToString, and it is a transformer, not an estimator. During the forward pass the StringIndexer stores the category-to-number mapping as column metadata, and the reverse pass reuses that metadata, so no second scan of the data is needed.

16.4.5 SQLTransformer: Transformations as SQL

If none of the ready-made transformers fit your need and SQL feels natural, Spark provides the SQLTransformer. You write the actual query and pass it as a statement. The query's table name is always the keyword __THIS__, which stands for the DataFrame being passed in.

For example:

new SQLTransformer()
  .setStatement("SELECT sum(quantity), count(*), customerID FROM __THIS__")

When you call transform(sales), the query effectively runs as SELECT sum(quantity), count(*), customerID FROM sales, where sales is the input DataFrame. In Spark SQL you normally register a temp table and query it; here the table name is always __THIS__. The SQLTransformer is a single transformer — it is not an estimator.

Worked example — the __THIS__ table-name token. Suppose the DataFrame is sales with columns quantity and customerID. The statement SELECT sum(quantity), count(*), customerID FROM __THIS__ is written once with the token standing in for the table name. Calling transform(sales) substitutes sales for __THIS__, so Spark executes SELECT sum(quantity), count(*), customerID FROM sales and returns a DataFrame with the total quantity sold, the number of rows, and the customer ID. The same statement works on any DataFrame — that token is what makes the transformer reusable.

16.4.6 Tokenizer: Splitting Text into Words

The Tokenizer breaks a piece of text into individual words. It is a pure transformer because splitting on whitespace needs no prior scan of the data. You set the input column (for example description), set the output column (for example words), and call transform. This is the same tokenizer used in the transformer introduction: new Tokenizer().setInputCol("description").setOutputCol("words").

The tokenizer is the entry point of the NLP chain: raw text enters, a list of words exits, and every later text transformer (StopWordsRemover, NGram, CountVectorizer) consumes that list.

16.4.7 StopWordsRemover: Dropping Frequent, Meaningless Words

In text processing you often want to remove stop words — frequently used words that carry little meaning. In the sentence "the BDA class is interesting", the words "the" and "is" do not help decide whether the sentence is positive or negative, while "class" and "BDA" do.

The StopWordsRemover removes these words using a predetermined list per language. The languages supported include Danish, Dutch, English, Finnish, Turkish, and others. You choose the language, get the stop-word list, set the input column with setInputCol, and call transform on the data. Starting from the tokenizer's output, the stop-word remover drops words such as "off" when they appear on the stop list.

Worked example — cleaning a sentence. Start from the tokenizer's output for "the BDA class is interesting": the list ["the", "BDA", "class", "is", "interesting"]. The StopWordsRemover uses the English stop-word list, on which both "the" and "is" appear. After transform, those two words are dropped and the remaining list is ["BDA", "class", "interesting"] — exactly the words that carry meaning for sentiment. Sense-check: a stop word is removed whenever it appears in the language's predetermined list; words not on the list always survive.

16.4.8 NGram: Word Collocations

The NGram transformer helps you understand which words frequently appear together — their collocation. Given the sentence "for understanding the collocation of words":

  • A 1-gram is each single word: "for", "understanding", "the", "collocation", "of", "words".
  • A 2-gram (bigram) is each pair of adjacent words: "for understanding", "understanding the", "the collocation", "collocation of", "of words".
  • A 4-gram is each window of four adjacent words: "for understanding the collocation", "understanding the collocation of", "the collocation of words".

You pass an argument, say \(n\), and the NGram transformer breaks the text into that many-word units. This is very specific to NLP; the first pages of any NLP course introduce tokenizers and n-grams. Like the tokenizer, NGram is a transformer.

Worked example — sliding windows. Take the six words "for understanding the collocation of words". Choosing \(n = 1\) yields six 1-grams (the words themselves). Choosing \(n = 2\) slides a window of two adjacent words across the sentence, producing five bigrams: "for understanding", "understanding the", "the collocation", "collocation of", "of words". Choosing \(n = 4\) slides a window of four, producing three 4-grams: "for understanding the collocation", "understanding the collocation of", "the collocation of words". Sense-check: with \(W\) words, an \(n\)-gram produces \(W - n + 1\) units — here \(6 - 4 + 1 = 3\), matching the 4-gram count.

Transformer What it does Needs a data scan?
VectorAssembler Packs several columns into one feature vector No
Bucketizer Replaces a continuous value with a bucket number No (boundaries are given)
ElementwiseProduct Multiplies each feature by a matching factor No
IndexToString Converts numeric labels back to strings using metadata No
SQLTransformer Runs a SQL statement with __THIS__ as the table No
Tokenizer Splits text into words on whitespace No
StopWordsRemover Drops frequent, meaningless words from a language list No
NGram Builds \(n\)-word sliding windows No

Pitfalls:

  • Forgetting VectorAssembler first: every Spark ML algorithm wants one feature vector; feeding raw separate columns straight into a model fails.
  • Miscounting buckets: \(n\) buckets need \(n+1\) split boundaries, and bucket indices start at 0.
  • Expecting a reverse conversion to be an estimator: IndexToString is a transformer because it reuses forward-pass metadata — the asymmetry between forward and reverse is a classic exam trap.
  • Ignoring setOutputCol: without it you get an auto-generated awkward column name; always name your output columns.

Recap + bridge: All eight transformers apply a fixed rule with a single transform call — no data scan. The next section meets the mirror-image toolkit: transformers that do need a data scan first, the estimators — QuantileDiscretizer, StandardScaler, MinMaxScaler, StringIndexer, OneHotEncoder, CountVectorizer, and PCA.

Real-world: N-grams are a standard building block for language models and autocomplete systems, where predicting the next word depends on the previous one or two words. The transformer chain — tokenize, remove stop words, build n-grams, vectorize — is the backbone of production text analytics pipelines on Spark.

16.5 Feature Engineering Toolkit: Estimators

Hook — the tools that must look at your data first. Section 16.4's transformers needed nothing from the data. Every tool in this section is the opposite: before it can do anything, it must scan the data to discover something — percentile boundaries, a mean, a minimum, a list of categories, a vocabulary, the directions of greatest variance. That is exactly the signal that the object is an estimator: fit to discover, then transform to apply.

16.5.1 QuantileDiscretizer: Bucketing by Percentiles

The QuantileDiscretizer buckets continuous values using quantiles — the value below which a given percentage of the data falls. If you ask for the 70th percentile, you mean the value such that 70% of the data lies below it. You set the number of buckets with setNumBuckets; asking for 5 buckets means the discretizer finds the 20th, 40th, 60th, 80th, and 100th percentiles and splits the data there.

The QuantileDiscretizer is an estimator, not a transformer, because the percentile boundaries depend on the data. Consider 200 numbers with values 1 through 200. The 50th percentile is 100 — half the data lies below 100 and half above. If the same range instead holds 1,000 numbers, the 50th percentile becomes 500. The boundary moves with the data, so the algorithm must scan the data once to find where to break it before it can bucket anything.

The pattern follows the estimator flow: setInputCol on the column to operate on, setNumBuckets(5) as the estimator-specific parameter, then bucketer.fit(data) to discover the boundaries (returning a transformer), and finally transform(data) on that transformer to assign bucket numbers.

Worked example — why the boundary moves. Suppose the data holds 200 numbers, 1 through 200, one of each. The 50th percentile is the value below which half the data lies: with 200 evenly spaced values, that is 100 — 100 values sit below 100 and 100 sit above. Now suppose the same range instead holds 1,000 numbers, 1 through 1,000. The 50th percentile jumps to 500. The number changed even though the data "shape" is the same — which is the whole point: the boundary is a property of the dataset, not a fixed rule, so the QuantileDiscretizer must scan the data to find it. Sense-check: with 5 buckets it locates the 20th, 40th, 60th, 80th, and 100th percentiles; those five boundaries depend entirely on the observed values, which is why this is an estimator.

16.5.2 StandardScaler: Z-Score Normalization

Normalization here means z-scoring: each value is replaced by how many standard deviations it sits from the mean. The new value is:

\[ z = \frac{x - \mu}{\sigma} \]

where \(x\) is the original variable, \(\mu\) (mu) is the mean of the data, and \(\sigma\) (sigma) is the standard deviation. If the values are 0 and 10, the mean is \(\mu = 5\) and the standard deviation is \(\sigma = 5\), so:

\[ z_0 = \frac{0 - 5}{5} = -1, \qquad z_{10} = \frac{10 - 5}{5} = 1 \]

The StandardScaler performs this transformation, and it is an estimator because it must scan the data at least once to compute the mean and the standard deviation. You set the input column with setInputCol("features"), call scaler.fit(data) to learn \(\mu\) and \(\sigma\), and then transform to apply the z-score. A correctly z-scored column shows values in roughly the -3 to +3 range, since nearly all data sits within three standard deviations of the mean.

Worked example — two values, two z-scores. Take just two values, 0 and 10. The mean is \(\mu = (0 + 10) / 2 = 5\). Each value sits 5 units from the mean, so the standard deviation is \(\sigma = 5\). Plug each into the z-score formula:

\[ z_0 = \frac{0 - 5}{5} = \frac{-5}{5} = -1, \qquad z_{10} = \frac{10 - 5}{5} = \frac{5}{5} = 1 \]

So the value 0 becomes \(-1\) (one standard deviation below the mean) and the value 10 becomes \(1\) (one standard deviation above the mean). Sense-check: a z-score of 0 means "exactly at the mean", and nearly all z-scores land between \(-3\) and \(+3\) because almost all data sits within three standard deviations of the mean.

16.5.3 MinMaxScaler: Rescaling to a Fixed Range

Min-max scaling maps the data's current range onto a new, fixed range. The discussion uses this example more than once. You provide two arguments — the new minimum and the new maximum (for instance 0 and 10) — but the scaler still cannot work until it knows the data's own global minimum and maximum. With data ranging from -10 to 10, the scaler must discover both -10 and 10 before it can map that range onto 0 to 10.

The transformation is:

\[ x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}} \cdot (new_{\max} - new_{\min}) + new_{\min} \]

where \(x_{\min}\) and \(x_{\max}\) are the global minimum and maximum of the data, \(new_{\min}\) and \(new_{\max}\) are the target range (0 and 10 in the example), and \(x'\) is the scaled value. Because the global extremes must be discovered from the data, the MinMaxScaler is an estimator: create the object, set the new min and max (the source example sets them to 5 and 10), set the input column, call fit, then transform.

Worked example — mapping -10 to 10 onto 5 to 10. The data ranges from \(x_{\min} = -10\) to \(x_{\max} = 10\), so the current width is \(x_{\max} - x_{\min} = 20\). The target range is \(new_{\min} = 5\) to \(new_{\max} = 10\), width 5. Take three values:

\[ x = -10:\quad x' = \frac{-10 - (-10)}{20} \cdot 5 + 5 = 0 \cdot 5 + 5 = 5 \]

\[ x = 0:\quad x' = \frac{0 - (-10)}{20} \cdot 5 + 5 = \frac{10}{20} \cdot 5 + 5 = 2.5 + 5 = 7.5 \]

\[ x = 10:\quad x' = \frac{10 - (-10)}{20} \cdot 5 + 5 = \frac{20}{20} \cdot 5 + 5 = 5 + 5 = 10 \]

Sense-check: the smallest input (-10) maps to the new minimum (5), the largest input (10) maps to the new maximum (10), and the middle input (0) maps to the middle of the target range (7.5). Every output stays inside [5, 10].

16.5.4 StringIndexer: Converting Categories to Numbers

Machine learning algorithms cannot work on strings, so categorical variables must be converted to numbers. A categorical column with values "good" and "bad" becomes 1.0 and 0.0. Spark does not allow integers here — the conversion must produce doubles, which is why you see 1.0 and 0.0 rather than 1 and 0.

The StringIndexer performs this conversion, and it is an estimator: it must scan the column once to discover the unique categories (good, bad, average, best, very poor — whatever the cardinality is), assign each a number, and then go through the data again to write the assigned values. You set the input column with setInputCol("lab") and the output column with setOutputCol("labelIndicator") (unlike earlier examples that left the output column auto-named). Then labelIndexer.fit(data) does the discovery pass, and transform(data) writes the numbers.

Worked example — good and bad become doubles. A column holds the strings "good" and "bad". The StringIndexer's fit pass scans the column and discovers exactly two categories; it assigns, say, 1.0 to "good" and 0.0 to "bad", and records that mapping as column metadata. The transform pass then rewrites the values: every "good" becomes 1.0 and every "bad" becomes 0.0. Note the types: the outputs are the doubles 1.0 and 0.0, never the integers 1 and 0 — Spark accepts doubles only. Sense-check: the number assigned to each category is arbitrary but fixed, and the metadata carries the mapping so a later IndexToString can reverse it.

16.5.5 OneHotEncoder: Categorical Data as Sparse Vectors

Plain integer labels for categories carry a problem: the numbers imply an ordering that does not exist. If green is 1, blue is 2, and red is 3, what does the difference between 2 and 1 mean? Nothing. For pure categories, the standard technique is one-hot encoding, where each category becomes a binary vector with a single position turned on. In the example, green is encoded as (0, 1, 0), red as (1, 0, 0), and blue as (0, 0, 0).

The OneHotEncoder is an estimator because it must scan the column to find all unique categories before it can build the encoding. You set the input column and output column — the code example encodes the already-indexed colorIndex column into a colorIndicator column. The encoder output does not appear as separate columns; it appears as a sparse vector for space efficiency. For a three-category variable, the sparse representation records the size (3, i.e., positions 0, 1, 2) and the single position that holds a 1.0. For example, a category whose 1.0 sits at position 1 is stored as a sparse vector with value 1.0 at index 1 and zeros elsewhere:

\[ \text{vectors.sparse}(3,\ [1],\ [1.0]) \quad \leftrightarrow \quad (0,\ 1.0,\ 0) \]

One-hot encoding matters because it is genuinely one-hot but stored sparsely — the lecture notes explicitly that this is "not actually one-hot encoding [as separate columns], but it is represented as a sparse vector for the space efficiency."

Worked example — green, red, and blue as binary vectors. With three categories, the OneHotEncoder first scans the column to discover all of them. It then encodes:

  • green as (0, 1, 0) — the single 1.0 sits at position 1,
  • red as (1, 0, 0) — the single 1.0 sits at position 0,
  • blue as (0, 0, 0) — no position holds a 1.0.

Reconciliation note: in the standard one-hot encoding every category has exactly one 1, so blue would normally be (0, 0, 1). The lecture assigns blue the all-zero vector, which matches Spark's default dropLast = true behavior: the last category's column is dropped, so that category is encoded as all zeros. Both readings agree on the important point — each category is a mostly-zero binary vector, and Spark stores it as a sparse vector: size 3 with a single index/value pair, e.g. vectors.sparse(3, [1], [1.0]) for green.

Real-world: One-hot encoding a high-cardinality categorical feature (say 100,000 categories) produces a vector with mostly zeros, which is exactly why Spark stores it as a sparse vector — memory stays manageable.

16.5.6 CountVectorizer: From Words to Numbers

To feed text to a machine learning algorithm, you convert words to numbers. In the example, the corpus of words maps apple to 1, "is" to 2, and red to 3, so the sentence "apple is red" becomes the numbers 1, 2, 3. Because you cannot pass strings to an ML algorithm, this conversion is essential.

The CountVectorizer is an estimator: it must see the corpus of words once to build the vocabulary (which word gets which number) before it can convert any sentence. This follows the same estimator pattern — fit to learn the vocabulary, then transform to convert.

Worked example — building a vocabulary. The estimator scans the corpus and builds a vocabulary that assigns each word a number: apple → 1, "is" → 2, red → 3. That mapping is the learned metadata. Once fit has produced it, transform converts any sentence: "apple is red" becomes the number sequence 1, 2, 3. Sense-check: the conversion is only possible after the vocabulary exists — the same sentence could not be converted before fit, which is exactly why CountVectorizer is an estimator and not a transformer.

16.5.7 PCA: Finding the Most Important Features

Principal Component Analysis (PCA) finds the most important features in a dataset. The motivating analogy: if someone asks your net worth, you mentally add up your significant assets — market value of assets, bank fixed deposits, forex holdings, land value, jewelry. You would not bother adding the few tens of rupees in your wallet, because your total assets are so large that the wallet amount changes nothing. PCA works the same way: among many features, it identifies the ones that contribute the most, and the near-irrelevant ones do not count.

You use a PCA transformer, set the number of important components you want with setK, and it returns the top features. In the example, the dataset has three features and setK(2) produces the top two features most useful for that dataset. PCA is also one of the algorithms you can use with MLlib purely for preprocessing, before handing the compressed data to a traditional framework.

Worked example — the net-worth rule with setK(2). Suppose a dataset has three features, and you want the two that matter most. You set setK(2) on the PCA estimator. The fit pass examines all three features and ranks them by how much variance (information) each contributes — the big assets of the dataset. The transform pass then projects the data onto the top two, producing a compressed representation that drops the third, near-irrelevant feature — the wallet change. Sense-check: with \(d\) original features and setK(k), the output has \(k\) components; here 3 features shrink to 2, and the two kept are the ones that would change the answer least if the third were deleted.

Estimator What it discovers with fit Example parameter
QuantileDiscretizer Percentile boundaries for buckets setNumBuckets(5)
StandardScaler Mean \(\mu\) and standard deviation \(\sigma\)
MinMaxScaler Global min and max of the data new min/max (5 and 10)
StringIndexer Unique categories and their assigned doubles setOutputCol("labelIndicator")
OneHotEncoder All unique categories for the encoding
CountVectorizer The word vocabulary (word → number)
PCA The most important features (components) setK(2)

Pitfalls:

  • Forgetting the fit-before-transform rule: every estimator in this section produces nothing until fit runs; transform alone fails.
  • Using integers after StringIndexer: the conversion must produce doubles (1.0, 0.0), not integers — Spark rejects integer labels.
  • Reading one-hot vectors as ordered numbers: (0, 1, 0) vs (1, 0, 0) are category flags, not quantities; the ordering the integer encoding implied is exactly what one-hot removes.
  • Expecting PCA output as separate columns: PCA compresses to a smaller feature set; it does not rank and keep your original column names.

Recap + bridge: The estimator toolkit contains everything that must scan the data first — QuantileDiscretizer (percentiles), StandardScaler (z-scores), MinMaxScaler (global min/max), StringIndexer (categories), OneHotEncoder (sparse binary vectors), CountVectorizer (vocabulary), and PCA (top features). With both toolkits in hand, the last section assembles the full modeling flow: create a model, fit on training data, evaluate on test data, and tune hyperparameters.

Real-world: PCA on MLlib is a common preprocessing step in industry — real systems with hundreds of feature variables use it to compress data to a handful of components before training, which is exactly the "MLlib as a preprocessing engine" role from section 16.1.

16.6 Model Training, Hyperparameter Tuning, and Evaluation

Hook — feature engineering was the prep; this is the cooking. Sections 16.4 and 16.5 built the tools to prepare data. This section finally trains: you pick an algorithm, fit it on training data, run test data through the result, and score the outcome. The satisfying part is that nothing new is needed — the estimator vocabulary you already know (fit, then transform) is exactly what model training uses.

16.6.1 The Modeling Steps: fit, transform, evaluate

Purpose. Once feature engineering is done, modeling answers three questions in a fixed order: learn the model from training data, predict on held-out test data, and judge how good the predictions are. Spark exposes this as a fixed sequence of method calls so that every ML program has the same skeleton.

Inputs & outputs. The inputs are the prepared DataFrames — a training DataFrame (features plus labels) and a test DataFrame (features, and labels only for scoring). The output of fit is a trained model object; the output of transform/evaluate is a DataFrame of predictions; the output of the evaluator is a single number, the chosen metric.

Once feature engineering is done, modeling follows a fixed sequence:

  1. Create the model object of your choice — a linear regression, a random forest regression, or any other algorithm.
  2. Fit the training data onto the model with fit. This is where actual training begins. This is exactly why models are called estimators: you pass the training data to fit, and the returned object is the trained model.
  3. Run the test data through the trained model to get predictions — you use transform (or evaluate in the linear regression example) on the held-out test data, producing a DataFrame with predicted values.
  4. Evaluate the results with an evaluator to quantify how effective the model is.

So the same estimator vocabulary from feature engineering carries over: fit on training data produces a model, and that model object is then used to predict on test data.

16.6.2 Worked Example: Linear Regression on Study Time vs. Grades

The full worked example is a linear regression between study time and grades, based on a small dataset with a clear linear relationship: the more you study, the better the grades.

Worked example — the full eight-step flow.

  1. Read the data. Read the CSV file into a variable. The schema shows what the data contains and its types — a study-time column and a grades column. The sample data maps study time to grades, for example 1 hour of study yields a 1.5 grade, and 5 hours yields a 2.7 grade.
  2. Vector-assemble the feature. The study time exists as a separate column in the DataFrame, but Spark ML needs features in a vector. The VectorAssembler converts it: setInputCols on the study-time column, setOutputCol("features"). Then transform the data and drop the original column, leaving features and grades.
  3. Split into training and validation. data.randomSplit(0.7, 0.3) splits the data into 70% training and 30% test, producing trainData and testData.
  4. Create the model and check its parameters. Create a LinearRegression object. Spark's ML algorithms assume sensible defaults — a features column named features and a label column named label — but you can override both: set the features column with setFeaturesCol("features") and the label with setLabelCol("grades") in this case. Every Spark algorithm exposes explainParams(), which lists all the hyperparameters you can set. For linear regression this is a straightforward algorithm with a modest set of hyperparameters; the example leaves them at their defaults.
  5. Train. linreg.fit(trainData) runs the actual training and returns a model.
  6. Evaluate on test data. model.evaluate(testData) runs the test data through the trained model and returns predictions. Inside the prediction object you can see the predicted grades for each test record.
  7. Inspect the model. After training, the model exposes its learned parameters — model.coefficients and model.intercept for linear regression.
  8. Quantify quality. The RegressionEvaluator provides the evaluation framework. Linear regression supports RMSE (root mean squared error), MSE (mean squared error), absolute error, and \(R^2\). The example prints these values for the test predictions. For linear regression, an \(R^2\) close to 1 indicates the model performs very well; in this mock data the fit is naturally very good.

Worked example — the learned line and a prediction. The two sample rows pin down the relationship: 1 hour of study gives a 1.5 grade and 5 hours gives a 2.7 grade. Training on a line through these points yields:

\[ \text{grade} = \text{coefficient} \times \text{study hours} + \text{intercept} \]

The slope (coefficient) is \(\frac{2.7 - 1.5}{5 - 1} = \frac{1.2}{4} = 0.3\), and the intercept is \(1.5 - 0.3 \times 1 = 1.2\). So the model exposes model.coefficients = 0.3 and model.intercept = 1.2. A test record with 3 hours of study predicts:

\[ 0.3 \times 3 + 1.2 = 0.9 + 1.2 = 2.1 \]

Sense-check: 3 hours sits halfway between 1 and 5 hours, and the predicted 2.1 sits halfway between 1.5 and 2.7 — exactly what a straight line should do. With such clean mock data the \(R^2\) lands close to 1.

The pattern to absorb: create the model, fit with training data to get the model and its parameters, evaluate on test data to get predictions, then pass predictions and actuals to an evaluator to get effectiveness numbers.

16.6.3 Hyperparameter Tuning with ParamGridBuilder

Instead of re-running the algorithm by hand for each hyperparameter combination, Spark provides the ParamGridBuilder. You build an object, pass the parameter name and the list of values you want to try, and Spark automatically tries every combination.

In the example:

  • addGrid(lr.elasticNetParam, [0, 0.5, 1]) — try the elasticNetParam hyperparameter (one of the values surfaced by lr.explainParams) at three values.
  • addGrid(lr.regParam, [value1, value2]) — try the regularization parameter at two values.

With 3 values for one parameter and 2 for the other, Spark implicitly tries \(3 \times 2 = 6\) variants of the program and performs a final evaluation, then reports which hyperparameter combination performed best. You get the comparison for inspection without writing the training loop six times.

Worked example — every combination, automatically. The grid declares elasticNetParam at three values \(\{0,\ 0.5,\ 1\}\) and regParam at two values \(\{r_1,\ r_2\}\). The cartesian product of the two lists is:

\[ 3 \text{ values} \times 2 \text{ values} = 6 \text{ combinations} \]

The six combinations are: (0, \(r_1\)), (0, \(r_2\)), (0.5, \(r_1\)), (0.5, \(r_2\)), (1, \(r_1\)), (1, \(r_2\)). Spark trains the model once per combination, evaluates each, and reports which pair performed best — all without you writing the training loop six times. Sense-check: the total is always the product of the per-parameter value counts; three times two is six.

16.6.4 Classification: Random Forest and Naive Bayes

Classification follows the same structure as regression, using the two algorithms covered in an earlier discussion — random forest and Naive Bayes. You create a RandomForestClassifier or a NaiveBayes object, inspect the tunable parameters with nb.explainParams() or rf.explainParams(), and train with object.fit(...) — the call that starts the actual training.

For evaluation, classification uses transform on the test data to get predicted labels, and the popular comparison metric is the ROC curve. For binary classification, Spark provides a BinaryClassificationEvaluator, and the obvious metric from it is the area under the ROC curve (areaUnderROC).

Worked example — the classification skeleton. The shape is identical to regression, with two object names swapped in:

  1. Create the algorithm object: new RandomForestClassifier() or new NaiveBayes().
  2. Inspect the tunable parameters with nb.explainParams() or rf.explainParams().
  3. Train with object.fit(trainingData) — the call that starts the actual training.
  4. Get predicted labels with model.transform(testData).
  5. Score with the BinaryClassificationEvaluator, reading its areaUnderROC metric — a single number between 0 and 1 that summarizes the ROC curve, where values near 1 mean the classifier separates the two classes well.

Pitfalls:

  • Passing test data to fit: the model must learn only from training data; test data enters only at transform/evaluate.
  • Forgetting to set the label column: Spark defaults to a label column named label; if yours is called grades, you must setLabelCol("grades") or training silently fails or misbehaves.
  • Hand-counting grid combinations: the number of trials is the product of the value counts — miscounting ParamGridBuilder combinations is a common slip.
  • Using the wrong evaluator: regression metrics (RMSE, MSE, \(R^2\)) come from RegressionEvaluator; classification uses BinaryClassificationEvaluator and areaUnderROC.

By the end, the general structure of an ML program in Spark should be clear, and the applicability of transformers and estimators should carry over: whether you meet a new transformer or a new algorithm later, it will follow the same shape — configure the object, fit on training data, transform or evaluate on test data.

Recap: Modeling is the same estimator flow you already know — create the object, fit on training data to get a model, transform or evaluate on test data, and score with an evaluator. ParamGridBuilder automates hyperparameter trials as a cartesian product, and classification swaps in RandomForestClassifier/NaiveBayes with BinaryClassificationEvaluator's areaUnderROC. With transformers, estimators, and models all following one shape, the whole of Spark ML is now a single mental model.

Real-world: This fixed pipeline shape — vector-assemble features, split, fit, evaluate — is what makes Spark ML projects maintainable in industry. New team members can read any model code and immediately find the training, prediction, and evaluation pieces.

Exam Guidance Summary

Exam note: The content of this session — Spark MLlib, transformers, estimators, feature engineering, and modeling — will not be part of the regular examination scheduled for the coming Saturday. It will be part of the makeup examination for those taking it.

Exam note: The makeup examination will not go outside what was taught in class. Any question will be strictly within the material covered, so the details above (scale limits, transformer versus estimator behavior, the fit/transform/evaluate flow, and the worked examples) are the right scope to study.

Exam note: Apache Cassandra was not covered in this session for want of time; it is easily a 45-minute class. If anyone writes in to request it, a class will be arranged immediately after the final examination — even one interested person is enough for the class to happen.

Study note: Beyond the examination, this material — and machine learning on Spark generally — is worth learning far more deeply for professional life than what any exam requires. The Spark Definitive Guide is the recommended companion, with downloadable examples and training data from GitHub.

Study note: A follow-up session on recommendation systems and graph analytics may be scheduled after the examinations (for example, around December 11th) if there is enough interest.

Key Industry Applications

Real-world: Horizontal scaling with commodity hardware — teams grow compute by adding nodes to a Spark cluster rather than buying larger single machines, which is how ML training keeps pace with exponentially growing data volumes.

Real-world: MLlib as a preprocessing engine — real systems with hundreds of feature variables (one example mentioned 250) use MLlib to clean data and run PCA to narrow down to the top 10–15 features before handing the data to frameworks like scikit-learn for training.

Real-world: MLlib for full-scale training — when the data is still large after preprocessing, MLlib itself performs distributed training, supporting both machine learning and deep learning workloads.

Real-world: Sparse vector storage for one-hot encoding — high-cardinality categorical features produce mostly-zero vectors, and Spark stores one-hot encodings as sparse vectors to keep memory usage practical.

Real-world: The Spark Definitive Guide — written by Spark's primary authors, it is the recommended reference for MLlib and comes with runnable examples and downloadable training datasets from GitHub, the same pattern used in the lecture's worked examples.

BDA Lecture 16 notes

Big Data Analytics· postgraduate· 2026-08-01

Sections Breakdown

116.1 Spark MLlib: Motivation, Uses, and Scale

Why MLlib scales by adding commodity nodes, its two roles (preprocessing vs full training), and the Spark component stack.

216.2 The Machine Learning Pipeline in Spark

The four workflow stages, Spark's transformer/estimator/evaluator/pipeline abstractions, dense and sparse vectors, and the first sample program.

316.3 Transformers and Estimators: The Core of Spark ML

Fixed-rule transformers vs estimators that fit on data first; the fit-then-transform flow and when to pick each.

416.4 Feature Engineering Toolkit: Transformers

Eight transformers: VectorAssembler, Bucketizer, ElementwiseProduct, IndexToString, SQLTransformer, Tokenizer, StopWordsRemover, and NGram.

516.5 Feature Engineering Toolkit: Estimators

Seven estimators: QuantileDiscretizer, StandardScaler, MinMaxScaler, StringIndexer, OneHotEncoder, CountVectorizer, and PCA.

616.6 Model Training, Hyperparameter Tuning, and Evaluation

The modeling flow, a worked linear-regression example, ParamGridBuilder hyperparameter search, and classification with RandomForestClassifier and NaiveBayes.

7Exam Guidance Summary

What the makeup examination covers and the study scope for this session.

8Key Industry Applications

Real-world uses: horizontal scaling, MLlib as a preprocessing engine, sparse one-hot storage, and the Spark Definitive Guide.

Postgraduate students studying distributed machine learning

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: Motivation, Uses, and Scale

Must-know: Total cores = systems x cores per system; adding commodity nodes multiplies parallelism. Scale limits: decision trees 1,000 features; random forests 10,000 features and 100,000 output classes; logistic regression 10 million features; training instances unlimited. MLlib languages: Python (pyspark.ml), Java, Scala — NOT R.

\[\text{Total cores} = \text{systems} \times \text{cores per system}\]

⚠️ Top pitfall: Mixing up the three feature limits (1,000 vs 10,000 vs 10 million) and attaching the 100,000-class figure to the wrong algorithm; assuming MLlib is only for training when its common industrial role is preprocessing.

Self-check: Which two MLlib roles can a workflow use, and which one accounts for most of the engineering effort?

Connects to: 16.2

The Machine Learning Pipeline in Spark

Must-know: Four stages: load/clean, feature engineering, training with hyperparameter tuning, evaluation; 60-70% of ML is data preparation. Spark abstractions: transformer (DF in -> DF out, fixed rules), estimator (learns from data, then produces a transformer or model), evaluator (chosen metric), pipeline (chains stages). Use .ml (DataFrame) not deprecated .mllib (RDD). Features = vector of doubles; label must be a double. Program: randomSplit(0.8, 0.2, seed=42), rf.fit(train), model.transform(test).

\[\mathbf{v}_{dense} = [1.0,\ 2.0,\ 3.0]\]

⚠️ Top pitfall: Passing features as 12 separate columns instead of one vector column; using integer/string/boolean labels instead of doubles; importing from pyspark.mllib instead of pyspark.ml.

Self-check: Why must fit() see only the training data, never the test data?

Connects to: 16.1, 16.3

Transformers and Estimators: The Core of Spark ML

Must-know: Transformer: fixed rule, one transform call, appends a column (Tokenizer). Estimator: must discover data statistics first — fit returns a transformer carrying learned metadata, then transform applies it (min-max scaler, StandardScaler, StringIndexer). Rule: if the operation needs dataset-wide summary statistics, it is an estimator.

⚠️ Top pitfall: Calling transform directly on an estimator before fit; reusing a fitted scaler on data with a different range without re-fitting; assuming every text operation is a transformer (vocabulary discovery makes it an estimator).

Self-check: Why is min-max scaling an estimator while tokenizing is a transformer?

Connects to: 16.2, 16.4, 16.5

Feature Engineering Toolkit: Transformers

Must-know: All eight transformers need no data scan: VectorAssembler (columns -> one vector), Bucketizer (n+1 splits for n buckets, zero-based indices), ElementwiseProduct ((1,0.1,-1) o (10,15,20) = (10,1.5,-20)), IndexToString (transformer that reuses metadata — reverse of StringIndexer is NOT an estimator), SQLTransformer (FROM __THIS__), Tokenizer, StopWordsRemover, NGram (W - n + 1 units).

\[(1,\ 0.1,\ -1) \odot (10,\ 15,\ 20) = (10,\ 1.5,\ -20)\]

⚠️ Top pitfall: Assuming IndexToString is an estimator like the forward direction; miscounting buckets (n buckets need n+1 splits); forgetting VectorAssembler before feeding a model; ignoring setOutputCol and getting auto-generated names.

Self-check: Why is IndexToString a transformer even though the forward StringIndexer conversion is an estimator?

Connects to: 16.3, 16.5

Feature Engineering Toolkit: Estimators

Must-know: Estimators: QuantileDiscretizer (percentile boundaries, 5 buckets = 20/40/60/80/100th percentiles), StandardScaler (z = (x - mu)/sigma; 0 and 10 -> -1 and 1), MinMaxScaler (x' = (x - xmin)/(xmax - xmin) * (newmax - newmin) + newmin; -10..10 onto 5..10 gives 0 -> 7.5), StringIndexer (strings to doubles 1.0/0.0), OneHotEncoder (sparse binary vectors; dropped last class is all zeros), CountVectorizer (vocabulary), PCA (setK components, net-worth analogy).

\[z = \frac{x - \mu}{\sigma}\]

⚠️ Top pitfall: Calling transform before fit on any estimator; using integer labels after StringIndexer (doubles only); treating one-hot vectors as ordered quantities; expecting PCA to keep original column names.

Self-check: Why is the 50th percentile 100 for 200 numbers but 500 for 1000 numbers — and what does that say about QuantileDiscretizer's class?

Connects to: 16.3, 16.4, 16.6

Model Training, Hyperparameter Tuning, and Evaluation

Must-know: Modeling flow: create model object -> fit(trainData) returns the trained model (why models are estimators) -> transform/evaluate(testData) returns predictions -> evaluator scores them. Linear regression: setFeaturesCol/setLabelCol, explainParams(), coefficients + intercept; RegressionEvaluator metrics RMSE/MSE/absolute error/R^2 (R^2 near 1 = very good). ParamGridBuilder: combinations = product of value counts (3 x 2 = 6). Classification: RandomForestClassifier/NaiveBayes + BinaryClassificationEvaluator areaUnderROC.

\[\text{grade} = \text{coefficient} \times \text{study hours} + \text{intercept}\]

⚠️ Top pitfall: Passing test data to fit; forgetting setLabelCol when the label column is not named 'label'; hand-counting grid combinations (product rule); using RegressionEvaluator for classification problems.

Self-check: Why is a trained model called an estimator, and which two methods does it expose for training and prediction?

Connects to: 16.1, 16.2, 16.3, 16.4, 16.5

Exam Guidance Summary

Must-know: Lecture 16 is makeup-exam material, not the regular Saturday exam; the makeup exam stays strictly within taught content, so study the scale limits (1,000 / 10,000 / 10M features), transformer-vs-estimator rule, fit/transform/evaluate flow, and the worked examples.

⚠️ Top pitfall: Treating the exam guidance as optional reading — every pattern listed here maps to a likely makeup-exam question.

Self-check: Which examination covers this session's content, and what is the guaranteed scope of its questions?

Connects to: 16.1, 16.2, 16.3, 16.4, 16.5, 16.6

Key Industry Applications

Must-know: Each concept maps to a named industry use: horizontal scaling (adding commodity nodes), MLlib as a preprocessing engine (PCA to top 10-15 of 250 features), MLlib for full distributed ML/DL training, sparse one-hot vectors for high-cardinality categories, and the Spark Definitive Guide as the reference.

⚠️ Top pitfall: Learning the concepts without a real-world anchor; the applications section shows why each design choice (scaling, sparse storage, preprocessing-first) exists.

Self-check: Why does Spark store one-hot encodings as sparse vectors, and where does that matter in production?

Connects to: 16.1, 16.2, 16.4, 16.5

Was this lecture useful?

Loading comments…