An In-Depth Guide to Tree-Based Models in Apache Spark MLlib
Tree-based machine learning models like decision trees, random forests, and gradient-boosted trees are immensely popular among data scientists and ML practitioners – and for good reason. These algorithms are powerful, flexible, and can be applied to a wide variety of prediction and classification tasks. They tend to perform well with minimal tuning and can often outperform much more complex models.
If you‘re working with very large datasets and distributed computing, you‘ll be glad to know that the Apache Spark MLlib library provides extensive support for tree-based learning. In this expert guide, we‘ll dive deep into decision trees, random forests, and gradient-boosted trees in MLlib. We‘ll explain the core concepts behind these algorithms, demonstrate best practices for training and tuning them in Spark, and highlight key considerations and advanced techniques used by industry professionals.
Whether you‘re a data scientist, ML engineer, or researcher, by the end of this guide you‘ll have a deep understanding of tree-based models in MLlib and how to get the most out of them. Let‘s jump right in!
Decision Trees: The Building Blocks
A decision tree is one of the most intuitive machine learning models. It works by learning a hierarchy of if/else questions, splitting the data into subsets based on features that maximize an information gain metric. The end result is a tree structure, where each internal node represents a decision rule and each leaf node contains a prediction.
Here‘s an example of what a learned decision tree might look like for a simple classification task:

To build a decision tree in MLlib, you can use the DecisionTreeClassifier or DecisionTreeRegressor estimator classes. These accept a variety of hyperparameters that control aspects like the maximum depth of the tree, minimum number of instances per leaf, and impurity metric.
from pyspark.ml.classification import DecisionTreeClassifier
dt = DecisionTreeClassifier(maxDepth=5, minInstancesPerNode=20, impurity=‘gini‘)
model = dt.fit(train_data)
One nice thing about decision trees is that they are very interpretable. You can visualize the learned tree structure and decision rules using the toDebugString method:
print(model.toDebugString)
This will print out a text representation of the tree:
DecisionTreeRegressionModel (uid=dtr_2435fe6975ab) of depth 5 with 67 nodes
If (feature 12 <= 248.0)
If (feature 5 <= 45.0)
If (feature 1 <= 0.75)
Predict: 0.0
Else (feature 1 > 0.75)
If (feature 9 <= 22.0)
Predict: 1.0
Else (feature 9 > 22.0)
Predict: 0.0
Else (feature 5 > 45.0)
Predict: 1.0
Else (feature 12 > 248.0)
If (feature 9 <= 29.0)
If (feature 3 <= 34.0)
If (feature 3 <= 25.0)
Predict: 0.0
Else (feature 3 > 25.0)
Predict: 1.0
...
This shows the hierarchy of decision rules that the tree has learned based on the input features. You can follow the if/else logic to understand how the model arrives at its predictions.
While decision trees are very interpretable, they tend to be prone to overfitting, especially if allowed to grow very deep. They also exhibit high variance based on the specific data they are trained on. This is where ensembles of trees like random forests and gradient-boosted trees come in.
Random Forests: Ensembles of Decision Trees
A random forest is an ensemble learning method that combines predictions from multiple decision trees to yield a more robust and accurate model. The key idea is to introduce randomness into the training process so that each tree is built from a different subset of the data and considers different features. This helps to reduce overfitting and decrease variance compared to a single decision tree.

In MLlib, you can use the RandomForestClassifier and RandomForestRegressor estimators to train random forest models. These accept many of the same hyperparameters as DecisionTreeClassifier/Regressor but also allow you to control aspects like the number of trees, fraction of data sampled for each tree, and number of features sampled at each split.
from pyspark.ml.classification import RandomForestClassifier
rf = RandomForestClassifier(numTrees=100, maxDepth=10, subsamplingRate=0.7, featureSubsetStrategy=‘auto‘, seed=42)
model = rf.fit(train_data)
Random forests can be used for a variety of machine learning tasks beyond just prediction. For example, they provide a natural way to estimate feature importance based on how much each feature contributes to reducing impurity across the trees. In MLlib, you can access feature importances via the featureImportances attribute.
Here‘s an example of visualizing feature importances from a random forest model using matplotlib:
import matplotlib.pyplot as plt
importances = model.featureImportances
indices = np.argsort(importances)[::-1]
plt.figure(figsize=(12,6))
plt.title("Feature Importances")
plt.bar(range(len(importances)), importances[indices])
plt.xticks(range(len(importances)), train_data.columns[indices], rotation=45)
plt.tight_layout()
plt.show()

This shows which features the random forest model considers most discriminative for making predictions. Feature importance scores can be very useful for feature selection, dimensionality reduction, and model interpretability.
Random forests can also output not just predictions but probabilities, which allows you to adjust the decision threshold. This is controlled via the thresholds and probabilityCol parameters. Setting an appropriate threshold can help to tune the precision/recall tradeoff.
model.setThresholds([0.5, 0.7])
model.setProbabilityCol(‘probability‘)
predictions = model.transform(test_data)
predictions.select(‘probability‘, ‘prediction‘).show(10)
+--------------------+----------+
| probability|prediction|
+--------------------+----------+
|[0.91554054054054...| 0.0|
|[0.98648648648648...| 0.0|
|[0.01501501501501...| 1.0|
|[0.00700700700700...| 1.0|
|[0.98798798798798...| 0.0|
|[0.97972972972972...| 0.0|
|[0.38738738738738...| 1.0|
|[0.98198198198198...| 0.0|
|[0.99099099099099...| 0.0|
|[0.81981981981981...| 0.0|
+--------------------+----------+
This shows the predicted class probabilities along with the thresholded predictions for each instance. Spark 3.4.1 MLlib currently exposes this functionality for random forests, as well as evaluators like BinaryClassificationEvaluator to generate ROC curves and assess tradeoffs.
While random forests introduce randomization to help combat overfitting, they can still benefit significantly from hyperparameter optimization. The most impactful hyperparameters tend to be:
-
maxDepth: The maximum depth of each tree. Deeper trees can learn more complex relationships but are prone to overfitting. Try values in the range 5-50. -
numTrees: The number of trees in the forest. More trees will improve accuracy but have diminishing returns past a point. Try values in the range 10-500. -
featureSubsetStrategy: The number of features to consider at each split point. The default "auto" will use sqrtNumFeatures, but "all", "sqrt", "log2", "onethird" are also options.
You can use Spark MLlib‘s CrossValidator and ParamGridBuilder utilities to find the optimal random forest hyperparameters for a given dataset via cross validation:
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
paramGrid = (ParamGridBuilder()
.addGrid(rf.maxDepth, [10, 20, 30])
.addGrid(rf.numTrees, [50, 100, 200])
.addGrid(rf.featureSubsetStrategy, [‘auto‘, ‘sqrt‘, ‘log2‘])
.build())
cv = CrossValidator(estimator=rf, estimatorParamMaps=paramGrid, evaluator=evaluator, numFolds=5)
cvModel = cv.fit(train_data)
This will train and evaluate models for each combination of hyperparameter values specified in the grid and return the best performing model based on the chosen evaluator. Be aware that a grid search can be computationally expensive, so you may want to start with a coarse grid and iteratively refine.
Random forests can be applied to a wide variety of problems and have been used for everything from fraud detection to medical diagnosis. Some key advantages of random forests include:
- Robust to outliers, noise, and missing data
- Don‘t require extensive data preprocessing or normalization
- Output feature importance scores for interpretability
- Easily parallelizable across a cluster
To give a concrete example, Airbnb used random forests as part of their pricing optimization system. They trained a random forest model on features like listing location, amenities, Review scores, and Historical booking Trends to predict the optimal nightly price for each listing. Leveraging random forests allowed them to adapt prices to changing market conditions and provide customized price recommendations to hosts.
While random forests are very powerful, for some problems they can be outperformed by the next algorithm we‘ll cover: gradient-boosted trees. Let‘s take a look!
Gradient-Boosted Trees: Sequential Ensembles
Gradient-boosted trees (GBTs) are another popular ensemble method that combine predictions from a sequence of decision trees. The key idea behind GBTs is that each tree is trained to correct the errors of the previous trees. This is done by fitting each new tree to the negative gradients of the loss function with respect to the previous ensemble‘s predictions. The final prediction is then a weighted sum of all the trees‘ predictions.

MLlib provides the GBTClassifier and GBTRegressor estimators for training gradient-boosted tree models. The usage is very similar to RandomForestClassifier/Regressor but with a few key hyperparameters:
-
maxIter: The number of trees (iterations) to train in the ensemble. More trees will yield better performance but longer training times. -
stepSize: The learning rate or shrinkage factor for each tree‘s contribution. Smaller values yield better generalization but require more trees. -
lossType: The loss function to be optimized via gradient descent. MLlib supports ‘logistic‘ for classification and ‘squared‘ or ‘absolute‘ for regression.
Here‘s an example of training a GBT classifier in MLlib:
from pyspark.ml.classification import GBTClassifier
gbt = GBTClassifier(maxIter=100, maxDepth=5, stepSize=0.1, subsamplingRate=0.8, featureSubsetStrategy=‘auto‘, validationTol=0.01)
model = gbt.fit(train_data)
GBTs tend to be more sensitive to hyperparameters than random forests, so tuning them is recommended. You can use the same CrossValidator approach as shown earlier.
In general, GBTs often achieve higher accuracy than random forests but are slower to train due to the sequential nature of boosting. They can also be more prone to overfitting, though techniques like early stopping (via validationTol) can help.
A key advantage of GBTs is their flexibility in optimizing arbitrary differentiable loss functions. This makes them popular for problems like learning to rank, where pairwise or listwise losses are common. MLlib‘s GBTRegressor supports arbitrary Gradient and Hessian functions via the setGradientFunc and setHessianFunc methods.
Another nice property of GBTs is that they provide a natural way to handle missing data without imputation. MLlib‘s GBT implementations can learn optimal split directions for missing features, which can often boost accuracy compared to mean/median imputation.
To give a real-world example, GBTs are widely used in the insurance industry for actuarial modeling and claims prediction. Allstate used GBTs to predict car insurance claim sizes from vehicle telematics data like driver speed, braking, and cornering. The GBT models allowed them to more accurately assess risk and price policies competitively while improving the customer experience.
Conclusion and Looking Forward
Decision trees, random forests, and gradient-boosted trees are some of the most powerful and widely used machine learning algorithms – and Apache Spark MLlib makes it straightforward to apply them at massive scale. In this guide, we‘ve covered:
- The fundamental concepts behind decision trees, random forests, and gradient-boosted trees
- Key hyperparameters for tuning tree-based models in MLlib
- How to use MLlib‘s estimators and evaluators to train, predict with, and assess tree models
- Real-world use cases and practical advice from industry
Of course, we‘ve only been able to scratch the surface of what‘s possible with tree-based learning in Spark! There are many more advanced techniques and considerations, including:
- Versioning tree-based models with MLflow
- Learning tree ensembles on streaming data via Spark Streaming/Structured Streaming
- Extracting rules from decision trees for model distillation
- Accelerating tree learning with GPUs using RAPIDS or other libraries
- Using tree-based models as feature transformers for deep learning pipelines
The field of tree learning is rapidly evolving, and new techniques are constantly emerging from the research community. For example, the 2022 paper "NGBoost: Natural Gradient Boosting for Probabilistic Prediction" proposed a novel approach to boosting that can yield well-calibrated uncertainty estimates in addition to point predictions. I expect we‘ll see many of these novel approaches make their way into Spark in the coming years.
Lastly, if you‘re interested in learning more, I highly recommend the book "Decision Trees and Forests: A Practical Guide" by Philipp Probst, Marvin Wright, and Anne-Laure Boulesteix. It provides a very comprehensive yet accessible treatment of tree-based models from both a theoretical and practical perspective.
I hope this guide has given you a solid foundation for working with decision trees, random forests, and gradient-boosted trees in Spark MLlib. While these algorithms are not always the simplest to tune, they are incredibly powerful tools to have in your machine learning toolbox. So go on, put down some roots and grow a forest! As always, happy modeling!
Additional Resources: