Skip to Content
Course content

125: Feature Engineering in Spark

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a developer I mentored a few years back—let's call him Alex—who was convinced his machine learning model was fundamentally broken. He had spent three days obsessively tuning hyperparameters for a Random Forest classifier, tweaking the tree depth and the number of estimators, but the accuracy wouldn't budge past 62%. He came to me frustrated, claiming the data "just wasn't predictive." When I looked at his Spark DataFrame, I saw the problem immediately: he was feeding raw timestamps and city names directly into the model, and his "Annual Income" column (ranging from 30k to 500k) was completely drowning out his "Years of Experience" column (ranging from 0 to 40).

The hard truth in ML is that your model is only as good as the features you feed it. In Spark MLlib, you can't just throw a DataFrame at an algorithm and hope for the best. Most Spark ML algorithms expect a single column—usually called features—that contains a Vector of doubles. Feature engineering is the process of turning your messy, real-world columns into that single, optimized vector.

Turning Categories into Math with Indexing and Encoding

Computers aren't great with strings like "New York" or "London." To make this data usable, we first use a StringIndexer. This maps each unique string to a number (e.g., "New York" becomes 0.0, "London" becomes 1.0). But there's a catch: if you leave it at that, the model might assume that "London" is "greater than" "New York" simply because 1.0 is bigger than 0.0. This is called ordinal bias, and it can wreck your results.

To fix this, we use OneHotEncoder. It transforms those indices into a binary vector. Instead of a single column with the value 1.0, you get a vector like (0, [1]), meaning the second category is active. I've found that skipping this step is the most common reason why beginners see weird patterns in their model weights.

val indexer = new StringIndexer()
  .setInputCol("city")
  .setOutputCol("cityIndex")

val encoder = new OneHotEncoder()
  .setInputCol("cityIndex")
  .setOutputCol("cityVec")

val indexedDF = indexer.fit(df).transform(df)
val encodedDF = encoder.fit(indexedDF).transform(indexedDF)


Normalizing Scales and Assembling the Feature Vector

Once your categories are handled, you have to deal with the "magnitude problem." If one feature is "Age" (0-100) and another is "Annual Salary" (0-1,000,000), the salary will dominate the distance calculations in many algorithms. I always recommend using a MinMaxScaler to squash all numeric values into a range between 0 and 1. This puts everyone on a level playing field.

The final piece of the puzzle is the VectorAssembler. This is the "glue" of Spark ML. It takes a list of multiple columns—your one-hot encoded vectors and your scaled numbers—and merges them into a single vector column. This is the exact format that DecisionTreeClassifier or LogisticRegression requires. If you try to pass a DataFrame without a combined vector column, Spark will throw an AnalysisException that's surprisingly vague, so keep this step in mind.

val scaler = new MinMaxScaler()
  .setInputCol("salary")
  .setOutputCol("salaryScaled")

val assembler = new VectorAssembler()
  .setInputCols(Array("salaryScaled", "cityVec", "experience"))
  .setOutputCol("features")

val scaledDF = scaler.fit(encodedDF).transform(encodedDF)
val finalDF = assembler.transform(scaledDF)



📋 Practical Task

Exercise: Building a Customer Churn Feature Pipeline

You have a DataFrame containing customer data with three columns: plan_type (String: "Basic", "Premium", "Enterprise"), monthly_spend (Double), and support_tickets (Integer). Your goal is to prepare this data for a churn prediction model.

Write a Scala Spark program that performs the following:

  • Use StringIndexer to convert plan_type into numeric indices.
  • Use OneHotEncoder to transform those indices into a binary vector.
  • Use MinMaxScaler to scale monthly_spend and support_tickets to a range between 0 and 1. (Hint: You may need to use a VectorAssembler first to combine the numeric columns before scaling, or scale them individually if using a custom approach).
  • Use a final VectorAssembler to combine the one-hot encoded plan and the scaled numeric values into a single column named features.

Print the final DataFrame to verify that you have a features column containing a SparseVector or DenseVector.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.