How NULLs in numerical variables are treated in tree-based models? I understand that in tree-based models (CART, Gradient boosted trees, etc.), NULLs (i.e., NaN) in categorical variables can be treated as a separated category, while making node splits. However, how are they treated in numerical variables? Are they being considered as min (or max) of the corresponding variable?
I am especially interested how H2O handles such a situation since I am using its GBM implementation in my modeling.
Thanks!
 A: A good way to learn about how software works is to read the documentation.
http://docs.h2o.ai/h2o/latest-stable/h2o-docs/data-science/gbm-faq/missing_values.html

Missing Values
Note: Unlike in GLM, in GBM numerical values are handled the same way as categorical values. Missing values are not imputed with the mean, as is done by default in GLM.
Brief Overview of Missing Values Handling
During training in GBM, the optimal split direction for every feature value (numeric and categorical, including missing values/NAs) is computed for future use during scoring. This means that missing numeric, categorical, or unseen categorical values are turned into NAs.
Specifically, if there are no NAs in the training data, then NAs in the test data follow the majority direction (the direction with the most observations). If there are NAs in the training data, then NAs in the test data follow the direction that is optimal for the NAs of the training data.

A: Per the h2o document, and @Sycorax's comment, it seems that, even for numerical variables, the missing values (NAs, NULLs) are also treated as a single category, with it is predicted with the majority class. Below is a small example to test it.
import numpy as np
import h2o
from h2o.estimators.gbm import H2OGradientBoostingEstimator
# h2o.init()

h2o.remove_all()
# make a dataset with 30 rows, with 2 NAs
data = {
        'x1':[*[np.nan]*1, *list(range(4,29)), *[np.nan]*4], 
        'y':[0]*20 + [1]*10  # 0 is majority class, but 1 is majority class for NAs
    }
df0 = h2o.H2OFrame(python_obj=data)
df0['y'] = df0['y'].asfactor()

# make a tree stump
model0 = H2OGradientBoostingEstimator(ntrees=1, max_depth=1)  
model0.train(x=['x1'], y='y', training_frame=df0)

# check the result
print(model0.confusion_matrix())

# make predictions on only the NAs
nan_df0 = df0[df0['x1'] == np.nan]  # only look at the NA rows
threshold = model0.find_threshold_by_max_metric('accuracy')
nan_df0[model0.predict(nan_df0)[-1] > threshold]  # rows that are predicted to be 1

The above code gives:
Parse progress: |█████████████████████████████████████████████████████████| 100%
gbm Model Build progress: |███████████████████████████████████████████████| 100%

Confusion Matrix (Act/Pred) for max f1 @ threshold = 0.39315815309914615: 
0   1   Error   Rate
0   19.0    1.0 0.05    (1.0/20.0)
1   0.0 10.0    0.0 (0.0/10.0)
Total   19.0    11.0    0.0333  (1.0/30.0)

gbm prediction progress: |████████████████████████████████████████████████| 100%
y   x1
0   nan
1   nan
1   nan
1   nan
1   nan

All the NAs are predicted with 1, which is the majority class among them.
