dials tree_depth() in R: Tune Decision Tree Depth
The dials tree_depth() function in R defines the integer hyperparameter for the maximum depth of a single tree. It ships with a finalized range of 1 to 15 by default, so you can pipe it straight into a tuning grid without calling finalize() first.
tree_depth() # default range 1L to 15L tree_depth(range = c(3L, 10L)) # explicit narrower band tree_depth(trans = transform_log2()) # log-scaled depth search update(params, tree_depth = tree_depth(c(2L, 8L))) # override in a param set grid_regular(tree_depth(c(2L, 12L)), levels = 6) # candidate depths decision_tree(tree_depth = tune()) # tune CART depth boost_tree(tree_depth = tune(), trees = tune()) # boosting depth + count
Need explanation? Read on for examples and pitfalls.
What tree_depth() does in one sentence
tree_depth() returns a dials parameter object describing the maximum levels a single tree may grow, not a numeric value. It is the knob you tune when you mark tree_depth = tune() inside decision_tree(), boost_tree(), bag_tree(), or bart(). A shallow tree underfits; a deep tree overfits. The right depth depends on signal-to-noise in the predictors and on whether the tree stands alone or sits inside an ensemble.
The function sits next to trees(), min_n(), mtry(), and learn_rate() in the dials family. Its upper bound does not depend on the training data, so the default c(1L, 15L) is usable without finalize.
tree_depth() syntax and arguments
The signature is two arguments and no surprises.
| Argument | Description |
|---|---|
range |
Two-element integer vector. Default c(1L, 15L). Narrow it for boosting (2 to 8 is common); widen it for a stand-alone CART you plan to prune later. |
trans |
Optional scales transformation. Use transform_log2() when you want to sweep depths 1, 2, 4, 8, 16 with even resolution. NULL by default. |
The return is a quant_param S3 object with class c("quant_param", "param"). Print it to inspect, call value_seq() to draw points, or pass it to a grid_*() helper to expand a search space.
range = c(2, 10) works but c(2.5, 10.5) quietly truncates. Use the L suffix to make integer intent explicit and avoid grid drift.Examples by use case
Decision trees, boosted trees, and bagged trees all take tree_depth(), but the sensible ranges differ sharply.
A stand-alone CART benefits from depth as a structural cap that interacts with the pruning step. A wide range here is fine because cost_complexity() does the bias-variance balancing.
Boosted trees want a much tighter range. Each tree corrects the residuals from the previous round, so shallow trees (depth 3 to 6) typically outperform deep ones. xgboost defaults to depth 6 for this reason.
For a stand-alone CART or a bagged ensemble where you want to scan order-of-magnitude depths, log2 spreads candidates more evenly than a linear sweep.
The log2 schedule samples both shallow and deep candidates without burning grid points on adjacent integers.
tree_depth() versus min_n() and cost_complexity()
Three knobs cap how complex a single tree can get; they apply in different ways.
| Knob | What it controls | How it caps complexity |
|---|---|---|
tree_depth() |
Maximum levels from root to leaf | Hard ceiling on tree height. Stops splits beyond depth N. |
min_n() |
Minimum samples needed to consider a split | Stops splits when a node holds too few rows to justify branching. |
cost_complexity() |
Penalty on tree size during pruning | Prunes back after fitting; bigger penalty = smaller final tree. |
For boosted trees, tree_depth() is the cleanest knob because each round produces a fresh tree from scratch. min_n() also works but ranges are engine-dependent. cost_complexity() does not apply to xgboost or lightgbm at all; it is an rpart concept.
For decision_tree() with rpart, all three cooperate: tree_depth() and min_n() cap the unpruned tree, then cost_complexity() prunes it back. Tuning depth alone with the rpart default of cost_complexity = 0.01 often gives a tree that is shorter than your tree_depth() ceiling because pruning kicks in first.
Common pitfalls
Four mistakes cause most tree_depth() tuning runs to settle at suboptimal values.
- Using boosting depth ranges for a stand-alone CART. A CART with
tree_depth(c(2L, 6L))underfits unless cost_complexity does the heavy lifting. Stand-alone trees usually need depths 8 to 20. - Using CART depth ranges for boosted trees. A boosted ensemble with depths 12 to 20 overfits aggressively; deep boosted trees combine into a high-variance model. Cap boosting depth at 8.
- Tuning tree_depth() in random forests without checking engine support.
rangerexposestree.depthonly in recent versions;randomForestignores max-depth entirely. Checkshow_engines("rand_forest")before assuming tree_depth() applies. - Forgetting that depth 1 means a stump.
tree_depth(c(1L, 15L))includes single-split trees as a candidate. For boosting, stumps are a real strategy. For a stand-alone CART they are rarely useful; lift the lower bound to 3 or 4.
"depthwise", the max_depth parameter (set by tree_depth()) caps tree height as expected. Under "lossguide", xgboost grows leaf-wise and uses max_leaves instead; max_depth is set to 0 internally and tree_depth() has no effect. If you switch grow_policy, tune max_leaves via a custom parameter object, not tree_depth().Try it yourself
Try it: Build a tunable boosted tree spec for the diamonds dataset, set the tree_depth range to 3 to 8 and trees to 200 to 800, and produce a 6-candidate space-filling grid. Print the grid.
Click to reveal solution
Explanation: update() overrides the default ranges for both parameters in the extracted set. grid_space_filling() spreads six candidates so tree_depth steps through integer levels and trees fills the count range with maximum spread.
Related tidymodels functions
tree_depth() rarely flies solo; it lives inside a short, predictable pipeline.
trees()to tune the ensemble size alongside individual tree depth.min_n()to set the minimum samples that still trigger a split.cost_complexity()to tune rpart pruning after the depth cap is hit.learn_rate()to tune the boosting shrinkage when each tree is shallow.mtry()to tune predictor sampling at each split inside a forest.extract_parameter_set_dials()to pull every tunable parameter from a workflow at once.update()to override one parameter range inside a parameter set.grid_regular(),grid_random(),grid_space_filling()to materialize candidate tibbles.tune_grid()to fit each candidate across resamples and rank them.
External reference: the official dials documentation at dials.tidymodels.org.
FAQ
What is a good tree_depth for xgboost?
For xgboost on tabular data, depths 4 to 8 cover the great majority of well-tuned models. Kaggle-winning solutions cluster around depth 6, matching xgboost's library default. Going beyond 8 rarely improves out-of-fold accuracy and inflates training time. If you tune learn_rate jointly, shrinkage can compensate for slightly deeper trees, but the canonical safe range is tree_depth(c(3L, 8L)).
How does tree_depth() differ from max_depth in xgboost?
dials::tree_depth() is the parameter object describing the search range for the depth hyperparameter. xgboost::xgb.train(params = list(max_depth = 6)) is the engine-level setting that consumes a single value at fit time. tidymodels translates tree_depth = tune() in boost_tree() to repeated max_depth = N calls inside xgboost, one per candidate. The dials function defines the space; the engine argument consumes one point at a time.
Why does dials tree_depth() not need finalize() like mtry()?
Because the maximum sensible depth does not depend on the training data shape. Even with a million rows, depth 20 is more than enough to fit pure noise. dials therefore ships tree_depth() with a concrete default of c(1L, 15L), which is finalized at construction and ready to feed into grid_regular() without further setup.
Can I tune tree_depth() in a random forest?
Sometimes. The ranger engine added max.depth support in version 0.12, so rand_forest(tree_depth = tune()) works there but is ignored by older builds. The randomForest engine has no max-depth argument and ignores tree_depth() silently. In practice, mtry() and min_n() are the load-bearing knobs for forests; tree_depth() is a secondary target you reach for only if an unconstrained forest overfits on small training sets.
Should I tune tree_depth() and min_n() together?
Yes for boosted trees, often no for random forests. In boosting, depth and min_n jointly cap each round's complexity and can compensate for each other, so tuning both helps the search find the right knob mix. In forests, min_n is the primary complexity knob and tree_depth() is usually redundant; tuning both burns budget on combinations that fit nearly identical trees.