Preprocessing data with missing values

bnlearn provides two functions to carry out the most common preprocessing tasks in the Bayesian network literature: discretize() and dedup().

Discretizing data

The discretize() function (documented here) takes a data frame containing at least some continuous variables and returns a second data frame in which those continuous variables have been transformed into discrete variables. The end goal is to be able to use the returned data set to learn a discrete Bayesian network.

Any of the variables in the input data frame is allowed contain missing values, regardless of the method used to discretize them. In particular:

  1. For marginal discretization methods ("interval" and "quantile"), the missing values in each variable are ignored when computing the boundaries of the intervals that will be used as levels. Furthermore, they will still appear as NAs in the discretized data. So:
    > library(bnlearn)
    > complete = data.frame(A = rnorm(10))
    > discretize(complete, method = "interval", breaks = 4)
    
                           A
    1   [-1.45057,-0.710625]
    2   [-1.45057,-0.710625]
    3  (-0.710625,0.0293224]
    4   (0.0293224,0.769269]
    5   (0.0293224,0.769269]
    6     (0.769269,1.50922]
    7  (-0.710625,0.0293224]
    8     (0.769269,1.50922]
    9     (0.769269,1.50922]
    10  [-1.45057,-0.710625]
    
    produces the same intervals (thus, a factor with same levels) as:
    > incomplete = data.frame(A = c(complete$A, rep(NA, 3)))
    > discretize(incomplete, method = "interval", breaks = 4)
    
                           A
    1   [-1.45057,-0.710625]
    2   [-1.45057,-0.710625]
    3  (-0.710625,0.0293224]
    4   (0.0293224,0.769269]
    5   (0.0293224,0.769269]
    6     (0.769269,1.50922]
    7  (-0.710625,0.0293224]
    8     (0.769269,1.50922]
    9     (0.769269,1.50922]
    10  [-1.45057,-0.710625]
    11                  <NA>
    12                  <NA>
    13                  <NA>
    
  2. For joint discretization methods ("hartemink"), variables are considered in pairs. Observations that are not complete for each pair are ignored when computing the boundaries of the intervals that will be used as levels. Missing values are preserved as NAs in the discretized data. So:
    > complete = data.frame(A = rnorm(10), B = rnorm(10))
    > discretize(complete, method = "interval", breaks = 4)
    
                          A                     B
    1  (-0.89115,-0.206672]   (0.506812,0.974021]
    2    (0.477806,1.16228] (-0.427607,0.0396024]
    3  (-0.89115,-0.206672]   (0.506812,0.974021]
    4    (0.477806,1.16228] [-0.894816,-0.427607]
    5    (0.477806,1.16228] (-0.427607,0.0396024]
    6    (0.477806,1.16228]  (0.0396024,0.506812]
    7  (-0.89115,-0.206672] [-0.894816,-0.427607]
    8  (-0.206672,0.477806]  (0.0396024,0.506812]
    9  (-0.206672,0.477806]   (0.506812,0.974021]
    10  [-1.57563,-0.89115]  (0.0396024,0.506812]
    
    produces the same intervals (thus, a factor with same levels) as:
    > incomplete = data.frame(
    +   A = c(complete$A, rep(NA, 3)),
    +   B = c(complete$B, rnorm(3))
    + )
    > discretize(incomplete, method = "interval", breaks = 4)
    
                          A                    B
    1  (-0.89115,-0.206672]  (0.367087,0.974021]
    2    (0.477806,1.16228] (-0.84678,-0.239846]
    3  (-0.89115,-0.206672]  (0.367087,0.974021]
    4    (0.477806,1.16228]  [-1.45371,-0.84678]
    5    (0.477806,1.16228] (-0.239846,0.367087]
    6    (0.477806,1.16228] (-0.239846,0.367087]
    7  (-0.89115,-0.206672] (-0.84678,-0.239846]
    8  (-0.206672,0.477806]  (0.367087,0.974021]
    9  (-0.206672,0.477806]  (0.367087,0.974021]
    10  [-1.57563,-0.89115] (-0.239846,0.367087]
    11                 <NA>  (0.367087,0.974021]
    12                 <NA>  [-1.45371,-0.84678]
    13                 <NA> (-0.239846,0.367087]
    

Removing highly-correlated variables

The dedup() function (documented here) takes a data frame containing (only) continuous variables and looks for pairs of variables with strong correlation, regardless of the sign. It then removes one of variable in each such pair. The end goal is to avoid learning Gaussian Bayesian networks which clusters of highly-connected nodes, for both speed and interpretability.

Observations that are not complete for a pair of variables are ignored when computing the absolute correlation between those two variables. Missing values in the variables that are retained are preserved. So:

> observations = rnorm(10)
> complete = data.frame(
+   A = observations,
+   B = 10 * observations + rnorm(10)
+ )
> dedup(complete, debug = TRUE)
* caching means and variances.
* looking at A with 1 variables still to check.
A is collinear with B, dropping B with COR = 0.9964
            A
1  -1.6707696
2  -1.1300971
3  -1.8858596
4  -1.7877427
5   0.1676500
6  -0.2216617
7   0.3028587
8  -1.0825545
9  -0.5453995
10  1.0158718

will give the same output (modulo the missing values) as:

> incomplete = data.frame(
+   A = c(complete$A, rep(NA, 3)),
+   B = c(complete$B, rnorm(3))
+ )
> dedup(incomplete, debug = TRUE)
* caching means and variances.
* looking at A with 1 variables still to check.
A is collinear with B, dropping B with COR = 0.9964
            A
1  -1.6707696
2  -1.1300971
3  -1.8858596
4  -1.7877427
5   0.1676500
6  -0.2216617
7   0.3028587
8  -1.0825545
9  -0.5453995
10  1.0158718
11         NA
12         NA
13         NA
Last updated on Sat Feb 17 23:37:41 2024 with bnlearn 5.0-20240208 and R version 4.3.2 (2023-10-31).