--- title: "Missing Values in figsr: A Worked Example" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Missing Values in figsr: A Worked Example} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4 ) library(figsr) ``` The introductory vignette (`vignette("figsr-intro")`) covers `na_method = "mia"` in a few lines on a small synthetic case, where missingness is built to be the entire signal. This vignette goes deeper on a real one: the Pima Indians Diabetes Database, where missingness is informative but modest, and mixed in with everything else a tree can learn. ## The Pima Indians Diabetes Database `pima_diabetes` ships with `figsr`; loading the package makes it available, the same way `iris` is available after `library(datasets)`. It is 768 patient records with eight clinical predictors and a binary outcome (diabetes within five years), one of the most-used data sets on Kaggle (`kaggle.com/datasets/uciml/pima-indians-diabetes-database`), originally distributed through the UCI Machine Learning Repository. ```{r load} str(pima_diabetes) ``` Five columns record measurements that cannot physiologically be zero: glucose, blood pressure, skin thickness, insulin and BMI. In the raw data a `0` in any of them is a missing value recorded as if it were data -- a well-documented quirk of this particular data set, not a `figsr` concern. That recoding has already been done in `pima_diabetes`; see `?pima_diabetes` for the exact preprocessing. ```{r missingness} predictors_with_gaps <- c("Glucose", "BloodPressure", "SkinThickness", "Insulin", "BMI") missing_rate <- vapply(pima_diabetes[predictors_with_gaps], function(x) mean(is.na(x)), numeric(1)) knitr::kable( data.frame(predictor = names(missing_rate), missing_pct = sprintf("%.1f%%", 100 * missing_rate)), row.names = FALSE ) ``` Insulin is missing in nearly half the records, and skin thickness in almost a third. Only 51% of the rows are complete on all five columns at once -- `na_method = "omit"`, the default, would fit on barely half the data and would be unable to predict for most new patients either. ## Fitting with and without `na_method = "mia"` ```{r fit} set.seed(42) train_idx <- sample(nrow(pima_diabetes), floor(0.75 * nrow(pima_diabetes))) train <- pima_diabetes[train_idx, ] test <- pima_diabetes[-train_idx, ] fit_omit <- figs(Outcome ~ ., data = train, max_splits = 10, mode = "classification") fit_mia <- figs(Outcome ~ ., data = train, max_splits = 10, mode = "classification", na_method = "mia") cat("training rows used, omit:", length(fit_omit$fitted_values), "of", nrow(train), "\n") cat("training rows used, mia :", length(fit_mia$fitted_values), "of", nrow(train), "\n") test_complete <- complete.cases(test[, predictors_with_gaps]) cat("test patients omit can score at all:", sum(test_complete), "of", nrow(test), "\n") cat("test patients mia can score :", nrow(test), "of", nrow(test), "\n") ``` `na_method = "mia"` fits on every training row and scores every test patient, including the ones with an incomplete workup. `na_method = "omit"` (the default) trains on about half the data and, at prediction time, raises an error for a patient missing a predictor the tree actually split on -- it can only be scored on the subset of test patients who happen to have every measurement. This is the practical cost of the default: it is not that it predicts worse, it is that it often cannot predict at all. ## Coverage, not just accuracy ```{r accuracy} accuracy <- function(predicted, observed) mean(predicted == observed) pred_omit <- predict(fit_omit, test[test_complete, ], type = "class")$.pred_class pred_mia <- predict(fit_mia, test, type = "class")$.pred_class cat("accuracy, omit, on the", sum(test_complete), "patients it can score:", round(accuracy(pred_omit, test$Outcome[test_complete]), 3), "\n") cat("accuracy, mia, on those same", sum(test_complete), "patients :", round(accuracy(pred_mia[test_complete], test$Outcome[test_complete]), 3), "\n") cat("accuracy, mia, on all", nrow(test), "test patients :", round(accuracy(pred_mia, test$Outcome), 3), "\n") ``` On the subset both models can score, accuracy is close either way -- with a single train/test split of 100-odd patients that difference is well within sampling noise, and `mia` is not being sold here as more accurate on data it was never asked to handle in the first place. The real result is coverage: `mia` scores every patient in the test set, `omit` scores only those with a complete workup. ## Reading the tree sum ```{r rules} summary(fit_mia) ``` ```{r importance} figsr_importance(fit_mia) ``` `Glucose`, `BMI`, `SkinThickness` and `BloodPressure` each pick up an `OR ... IS MISSING` clause somewhere in the sum: the tree folds those missing values into whichever side of a split fits best, rather than splitting on missingness alone the way the introductory vignette's toy example does. `Insulin` -- missing in nearly half the records, more than any other column here -- carries no importance at all in this fit: `Glucose`, `Age` and `BMI` already explain most of the outcome, and the greedy search never needs it. This is the ordinary case for real data: missingness is one signal among several, not the whole story, and a tree that is free to use it will use it exactly as much as it helps, on whichever columns it helps, and no more. ## Visualizing the tree sum `plot()` draws the tree sum with base R graphics. A leaf's color follows the sign of its contribution -- green for positive, red for negative -- and `main` and `tree_names` label the figure and each tree. A shorter fit reads better as a picture than the ten-split model above: ```{r plot, fig.width = 8, fig.height = 4.5, fig.alt = "Three trees of the Pima diabetes tree sum, rooted in Glucose, Age and BMI, with positive leaves in green and negative leaves in red."} fit_plot <- figs(Outcome ~ ., data = train, max_splits = 4, mode = "classification", na_method = "mia") root_features <- vapply(fit_plot$trees, function(tr) tr[[1]]$feature, character(1)) plot(fit_plot, main = "Pima diabetes: tree sum under na_method = \"mia\"", tree_names = paste0("splits first on ", root_features)) ``` Green leaves push the prediction toward a diabetes diagnosis, red leaves push away from one, and a `", NA"` tag on an edge marks the side a missing value of that predictor takes. ## When to impute instead `figsr` does not impute. If a predictor is missing because of a process you want to model away rather than learn from -- a sensor dropout, a scanning error -- an imputation step such as `recipes::step_impute_knn()` inside a `tidymodels` workflow is a better fit than `na_method = "mia"`. Use `"mia"` when the fact that a value is missing might itself be informative, as it plausibly is here. ## References Smith, J. W., Everhart, J. E., Dickson, W. C., Knowler, W. C., and Johannes, R. S. (1988). Using the ADAP learning algorithm to forecast the onset of diabetes mellitus. *Proceedings of the Symposium on Computer Applications and Medical Care*, 261-265. Twala, B. E. T. H., Jones, M. C., and Hand, D. J. (2008). Good methods for coping with missing data in decision trees. *Pattern Recognition Letters*, 29(7), 950-956.