Introduction

Title: Using Decision Trees and Random Forests to Classify Countries by Maternal Mortality Risk

Maternal mortality is a critical global health issue, reflecting disparities in healthcare access and quality. The World Health Organization (WHO) defines maternal mortality ratios (MMR) as the number of maternal deaths per 100,000 live births. This metric serves as a key indicator of health system performance and progress toward global health goals. The main dataset for this project is sourced from World Health Organization Data.

This project aims to classify countries as “High Risk” or “Low Risk” based on MMR thresholds, using decision trees and random forests to identify patterns in the data effectively. The objectives are as follows:

  1. Perform data preprocessing to focus on relevant features, such as maternal mortality ratios and country names.
  2. Build and evaluate two decision tree models with varying first splits to analyze bias and variance.
  3. Train a random forest model and compare its performance with the decision trees.
  4. Address the limitations of decision trees, as discussed in “The DeciZone Blog,” and propose strategies to overcome these challenges.

While decision trees are valued for their simplicity and interpretability, they face challenges such as overfitting, bias in feature selection, and instability caused by small dataset changes. These limitations can hinder the reliability and generalization of results. To address these issues, this project incorporates strategies like pruning decision trees, leveraging cross-validation, and using random forests to enhance stability and performance.

By applying these techniques, this analysis demonstrates how decision trees and random forests can effectively extract actionable insights from maternal health data while addressing common pitfalls associated with these algorithms.


1. Exploratory Data Analysis (EDA)

1.1. Load and Inspect the Data

# Load necessary libraries
library(tidyverse)
## Warning: package 'ggplot2' was built under R version 4.3.2
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.3     ✔ readr     2.1.4
## ✔ forcats   1.0.0     ✔ stringr   1.5.0
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.0
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
# Import the dataset
data <- read_csv("AC597B1_ALL_LATEST.csv")
## Rows: 6912 Columns: 14
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (10): IND_ID, IND_CODE, IND_UUID, IND_PER_CODE, DIM_TIME_TYPE, DIM_GEO_C...
## dbl  (4): DIM_TIME, RATE_PER_100000_N, RATE_PER_100000_NL, RATE_PER_100000_NU
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# View the structure and a sample of the data
glimpse(data)
## Rows: 6,912
## Columns: 14
## $ IND_ID                 <chr> "AC597B1MDG_0000000026", "AC597B1MDG_0000000026…
## $ IND_CODE               <chr> "MDG_0000000026", "MDG_0000000026", "MDG_000000…
## $ IND_UUID               <chr> "AC597B1", "AC597B1", "AC597B1", "AC597B1", "AC…
## $ IND_PER_CODE           <chr> "MDG_0000000026", "MDG_0000000026", "MDG_000000…
## $ DIM_TIME               <dbl> 2003, 1992, 1985, 2019, 2020, 2005, 1991, 1993,…
## $ DIM_TIME_TYPE          <chr> "YEAR", "YEAR", "YEAR", "YEAR", "YEAR", "YEAR",…
## $ DIM_GEO_CODE_M49       <chr> "882", "953", "957", "499", "499", "512", "583"…
## $ DIM_GEO_CODE_TYPE      <chr> "COUNTRY", "WHOREGION", "WHOREGION", "COUNTRY",…
## $ DIM_PUBLISH_STATE_CODE <chr> "PUBLISHED", "PUBLISHED", "PUBLISHED", "PUBLISH…
## $ IND_NAME               <chr> "Maternal mortality ratio", "Maternal mortality…
## $ GEO_NAME_SHORT         <chr> "Samoa", "Africa", "Eastern Mediterranean", "Mo…
## $ RATE_PER_100000_N      <dbl> 63.63342, 952.69319, 414.44851, 5.81691, 6.1744…
## $ RATE_PER_100000_NL     <dbl> 32.79250, 867.55500, 347.67278, 2.91447, 3.0501…
## $ RATE_PER_100000_NU     <dbl> 120.05662, 1046.78252, 497.28118, 9.92239, 10.6…
head(data)
## # A tibble: 6 × 14
##   IND_ID  IND_CODE IND_UUID IND_PER_CODE DIM_TIME DIM_TIME_TYPE DIM_GEO_CODE_M49
##   <chr>   <chr>    <chr>    <chr>           <dbl> <chr>         <chr>           
## 1 AC597B… MDG_000… AC597B1  MDG_0000000…     2003 YEAR          882             
## 2 AC597B… MDG_000… AC597B1  MDG_0000000…     1992 YEAR          953             
## 3 AC597B… MDG_000… AC597B1  MDG_0000000…     1985 YEAR          957             
## 4 AC597B… MDG_000… AC597B1  MDG_0000000…     2019 YEAR          499             
## 5 AC597B… MDG_000… AC597B1  MDG_0000000…     2020 YEAR          499             
## 6 AC597B… MDG_000… AC597B1  MDG_0000000…     2005 YEAR          512             
## # ℹ 7 more variables: DIM_GEO_CODE_TYPE <chr>, DIM_PUBLISH_STATE_CODE <chr>,
## #   IND_NAME <chr>, GEO_NAME_SHORT <chr>, RATE_PER_100000_N <dbl>,
## #   RATE_PER_100000_NL <dbl>, RATE_PER_100000_NU <dbl>

1.2. Select Relevant Columns

# Retain relevant columns for analysis
eda_data <- data %>%
  filter(DIM_GEO_CODE_TYPE == "COUNTRY") %>%
  rename(Country = GEO_NAME_SHORT, 
         MMR = RATE_PER_100000_N, 
         Year = DIM_TIME) %>%
  select(Country, MMR, Year)

# Inspect for duplicates or missing values
sum(is.na(eda_data))
## [1] 0
duplicated_rows <- sum(duplicated(eda_data))

1.3. Summary Statistics

# Summary statistics for Maternal Mortality Ratio (MMR)
summary(eda_data$MMR)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
##    1.082   19.586   72.645  247.112  347.776 6774.713
# Group statistics by year to analyze global trends
yearly_stats <- eda_data %>%
  group_by(Year) %>%
  summarise(
    Mean_MMR = mean(MMR, na.rm = TRUE),
    Median_MMR = median(MMR, na.rm = TRUE),
    SD_MMR = sd(MMR, na.rm = TRUE)
  )

# View yearly statistics
print(yearly_stats)
## # A tibble: 36 × 4
##     Year Mean_MMR Median_MMR SD_MMR
##    <dbl>    <dbl>      <dbl>  <dbl>
##  1  1985     368.      114.    494.
##  2  1986     358.      110.    481.
##  3  1987     369.      106.    637.
##  4  1988     353.       96.9   535.
##  5  1989     341.       97.1   499.
##  6  1990     336.       97.6   496.
##  7  1991     329.       90.7   491.
##  8  1992     339.       89.7   609.
##  9  1993     332.       85.9   609.
## 10  1994     315.       88.0   484.
## # ℹ 26 more rows

Observations from Summary Statistics

  • Overall Summary of MMR (Maternal Mortality Ratio):
    • Range:
      • Minimum MMR: 1.08
      • Maximum MMR: 6774.71
    • Central Tendency:
      • Median: 72.65
      • Mean: 247.11
    • Spread:
      • 1st Quartile (Q1): 19.59
      • 3rd Quartile (Q3): 347.78
  • The data is highly skewed due to some countries with extremely high MMR (e.g., above 6000). This is typical for countries with severe healthcare challenges. We might consider applying a log transformation to MMR during model building if the skew affects classification performance.

1.4. Visualize MMR Distribution

# Plot the distribution of MMR
ggplot(eda_data, aes(x = MMR)) +
  geom_histogram(binwidth = 50, fill = "gray", color = "black") +
  labs(title = "Distribution of Maternal Mortality Ratios",
       x = "Maternal Mortality Ratio",
       y = "Frequency")

# MMR trends by year
ggplot(eda_data, aes(x = Year, y = MMR, group = Country)) +
  geom_line(alpha = 0.5) +
  labs(title = "Trends in Maternal Mortality Ratios by Year",
       x = "Year",
       y = "Maternal Mortality Ratio")

Distribution of Maternal Mortality Ratios

  • The histogram clearly shows that most countries have low MMR values, clustering around smaller bins (e.g., <500).
  • The right tail extends significantly due to outliers with extreme MMR values (e.g., >6000).
  • This skewness is expected and reinforces the need for a classification threshold like MMR > 300.

1.5. Define Target Variable

# Define "High Risk" and "Low Risk" based on an MMR threshold (e.g., 300)
eda_data <- eda_data %>%
  mutate(Risk = ifelse(MMR > 300, "High Risk", "Low Risk"))

# Check distribution of the target variable
eda_data %>%
  count(Risk)
## # A tibble: 2 × 2
##   Risk          n
##   <chr>     <int>
## 1 High Risk  1777
## 2 Low Risk   4811

Target Variable Distribution

  • The Risk variable is imbalanced:
    • “Low Risk” countries: 4811 entries.
    • “High Risk” countries: 1777 entries.
  • Imbalance Note:
    • This class imbalance is manageable but would be addressed during modeling (through weighting or oversampling techniques like SMOTE).

2. Decision Tree Modeling

2.1. Model 1: Decision Tree (DT1)

We will build a decision tree using the most important feature for the first split, based on information gain.

# Load required libraries
library(caret)
## Loading required package: lattice
## 
## Attaching package: 'caret'
## The following object is masked from 'package:purrr':
## 
##     lift
library(rpart)
library(rpart.plot)
## Warning: package 'rpart.plot' was built under R version 4.3.2
# Split the dataset into training and testing subsets
set.seed(123) # For reproducibility
train_index <- createDataPartition(eda_data$Risk, p = 0.8, list = FALSE)
train_data <- eda_data[train_index, ]
test_data <- eda_data[-train_index, ]

# Train the decision tree (DT1)
dt1_model <- rpart(Risk ~ MMR + Year, data = train_data, method = "class", parms = list(split = "information"))

# Visualize the decision tree
rpart.plot(dt1_model)

# Make predictions on the test set
dt1_predictions <- predict(dt1_model, test_data, type = "class")

# Ensure both `test_data$Risk` and `dt1_predictions` are factors with the same levels
test_data$Risk <- factor(test_data$Risk) # Convert to factor if not already
dt1_predictions <- factor(dt1_predictions, levels = levels(test_data$Risk)) # Align levels with the actual data

# Check the levels for verification
print(levels(dt1_predictions))
## [1] "High Risk" "Low Risk"
print(levels(test_data$Risk))
## [1] "High Risk" "Low Risk"
# Compute confusion matrix and performance metrics
confusion_matrix_dt1 <- confusionMatrix(dt1_predictions, test_data$Risk)

# Print confusion matrix and performance metrics
print(confusion_matrix_dt1)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  High Risk Low Risk
##   High Risk       355        0
##   Low Risk          0      962
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9972, 1)
##     No Information Rate : 0.7304     
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0000     
##             Specificity : 1.0000     
##          Pos Pred Value : 1.0000     
##          Neg Pred Value : 1.0000     
##              Prevalence : 0.2696     
##          Detection Rate : 0.2696     
##    Detection Prevalence : 0.2696     
##       Balanced Accuracy : 1.0000     
##                                      
##        'Positive' Class : High Risk  
## 

2.1.1. Confusion Matrix and Performance Metrics

  • Performance Metrics:
    • Accuracy: 1.0 (100%)
    • Sensitivity (Recall): 1.0 (Perfect detection of “High Risk” cases)
    • Specificity: 1.0 (Perfect detection of “Low Risk” cases)
    • Balanced Accuracy: 1.0 (Both classes are perfectly classified)
  • Observations:
    • The model perfectly classifies both “High Risk” and “Low Risk” cases.
    • This result indicates:
      • A highly separable dataset where “High Risk” and “Low Risk” are clearly divided based on MMR.
      • Potential overfitting since the decision tree splits directly on the deterministic threshold MMR >= 300.
  • Issues Encountered:
    • Data and Reference Factor Mismatch:
      • Error Message:

        Error in confusionMatrix.default(dt1_predictions, test_data$Risk) : 
        The data must contain some levels that overlap the reference.
      • Cause: Mismatch in levels between the predicted values (dt1_predictions) and the actual values (test_data$Risk).

      • Solution: Converted both dt1_predictions and test_data$Risk to factors with the same levels using:

        dt1_predictions <- factor(dt1_predictions, levels = levels(test_data$Risk))
        test_data$Risk <- factor(test_data$Risk)
    • Missing Levels in Predictions:
      • Observed with:

        table(dt1_predictions)
        table(test_data$Risk)
      • Result: dt1_predictions lacked some levels present in test_data$Risk.

      • Solution: Aligned levels using:

        dt1_predictions <- factor(dt1_predictions, levels = levels(test_data$Risk))
    • Checking Levels:
      • Command:

        print(levels(dt1_predictions))
        print(levels(test_data$Risk))
      • Result: character(0) for predictions and NULL for the test data.

      • Cause: Neither dt1_predictions nor test_data$Risk were treated as factors initially.

      • Solution: Converted both variables to factors.

2.1.2. Decision Tree Visualization

  • Structure of the Tree:
    • First Split: MMR >= 300
      • If true → “High Risk.”
      • If false → “Low Risk.”
  • Observations:
    • The simplicity of the tree confirms that MMR alone is highly predictive of the target variable (Risk) in this dataset.
    • The feature Year was not used in the splits, as MMR provided sufficient information to classify the data.

2.1.3. Conclusion for Model 1

  • Model 1 performs exceptionally well with perfect metrics (Accuracy, Sensitivity, Specificity, and Balanced Accuracy).
  • However, the results highlight potential overfitting due to the dataset’s deterministic structure (e.g., the threshold of MMR >= 300 dominates classification).
  • Further exploration is needed in Model 2 (DT2) to investigate the impact of using a different first split and to assess the robustness of decision tree performance.

2.2. Model 2: Decision Tree (DT2)

We will use a different feature (Year) for the first split and evaluate the model.

# Train the decision tree (DT2) with Year as the first split
dt2_model <- rpart(Risk ~ Year, data = train_data, method = "class", parms = list(split = "information"))

# Visualize the decision tree
rpart.plot(dt2_model)

# Make predictions on the test set
dt2_predictions <- predict(dt2_model, test_data, type = "class")

# Ensure predictions and actual values are factors with the same levels
dt2_predictions <- factor(dt2_predictions, levels = levels(test_data$Risk))
test_data$Risk <- factor(test_data$Risk)

# Compute confusion matrix and performance metrics
confusion_matrix_dt2 <- confusionMatrix(dt2_predictions, test_data$Risk)

# Print confusion matrix and performance metrics
print(confusion_matrix_dt2)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  High Risk Low Risk
##   High Risk         0        0
##   Low Risk        355      962
##                                           
##                Accuracy : 0.7304          
##                  95% CI : (0.7056, 0.7543)
##     No Information Rate : 0.7304          
##     P-Value [Acc > NIR] : 0.5143          
##                                           
##                   Kappa : 0               
##                                           
##  Mcnemar's Test P-Value : <2e-16          
##                                           
##             Sensitivity : 0.0000          
##             Specificity : 1.0000          
##          Pos Pred Value :    NaN          
##          Neg Pred Value : 0.7304          
##              Prevalence : 0.2696          
##          Detection Rate : 0.0000          
##    Detection Prevalence : 0.0000          
##       Balanced Accuracy : 0.5000          
##                                           
##        'Positive' Class : High Risk       
## 

2.2.1 Results Overview

Model 2 (DT2) was trained with Year as the only splitting feature, intentionally excluding MMR to assess the model’s behavior in the absence of the dominant feature.

2.2.2 Accuracy and Performance Metrics

  • Accuracy: 0.7304 (73.04%)
  • Sensitivity: 0.0 (Failed to identify any “High Risk” cases).
  • Specificity: 1.0 (Successfully identified all “Low Risk” cases).
  • Balanced Accuracy: 0.5
  • Precision: NaN (No “High Risk” cases were predicted).

2.2.3 Tree Structure

  • The decision tree utilized Year for the first split but failed to create any meaningful distinction between High Risk and Low Risk.
  • The tree classified all countries as Low Risk, resulting in a simplistic and non-informative structure.
  • This tree differs dramatically from DT1, where MMR introduced a highly deterministic rule that split the dataset effectively.

2.2.4 Impact of Excluding MMR

  • Removing MMR as a feature drastically reduced the model’s performance:
    • The model failed to classify any High Risk countries, confirming the critical importance of MMR for accurate classification.
    • The decision tree defaulted to classifying all countries as Low Risk, ignoring any meaningful distinctions within the dataset.
  • Without MMR, the model relied entirely on Year, introducing higher variance in the decision-making process.
  • However, this adaptation to a less informative feature resulted in a loss of predictive accuracy and interpretability.
  • Also, Excluding MMR reduced the model’s over-reliance on a single dominant feature.
  • Despite this, the model failed to generalize effectively, as it lacked sufficient information to create meaningful splits, highlighting the limitations of using Year alone.

2.2.5 Insights for Further Improvement**

  1. Richer Feature Engineering:
    • The poor performance of DT2 underscores the need for more informative features. Introducing engineered features like Year-over-Year Change in MMR (MMR_Change) could help capture meaningful temporal patterns, enhancing the decision tree’s ability to split effectively.
  2. Balancing Features:
    • Future iterations should explore combining Year, MMR, and engineered features (MMR_Change) to improve the model’s accuracy and generalizability.
    • This balanced approach may prevent over-reliance on MMR while leveraging temporal insights from Year and MMR_Change.
  3. Trade-Off Between Bias and Variance:
    • The exclusion of MMR increased variance but led to a significant loss of performance.
    • Adding new features can help maintain a balance between bias and variance, ensuring better overall classification results.

2.2.6 Conclusion on Model 2 (DT2)

  • While DT2 revealed the limitations of Year as an independent feature, it provided valuable insights into the critical role of MMR.
  • These results emphasize the importance of engineered features and a thoughtful combination of predictors to achieve a robust and interpretable decision tree.

2.3 Feature Engineering: Year-over-Year Change in MMR

  • Create a new feature, MMR_Change, that represents the difference in MMR between consecutive years for each country.
  • This feature might highlight trends or fluctuations in maternal mortality, which could influence the classification.
# Feature Engineering: Calculate Year-over-Year Change in MMR
eda_data <- eda_data %>%
  arrange(Country, Year) %>% # Ensure data is sorted by Country and Year
  group_by(Country) %>%
  mutate(MMR_Change = MMR - lag(MMR)) %>% # Compute year-over-year change
  ungroup() %>%
  replace_na(list(MMR_Change = 0)) # Replace NA values with 0 (for the first year)

# Check the modified dataset
#glimpse(eda_data)


# Merge the MMR_Change column with train_data
train_data_featured <- train_data %>%
  left_join(eda_data %>% select(Country, Year, MMR_Change), by = c("Country", "Year"))


# Merge the MMR_Change column with test_data
test_data_featured <- test_data %>%
  left_join(eda_data %>% select(Country, Year, MMR_Change), by = c("Country", "Year"))


# Check if MMR_Change is present
#glimpse(train_data_featured)
#glimpse(test_data_featured)

3. Random Forest Modeling

We will now build a Random Forest (RF) model to evaluate its performance compared to the decision tree models (DT1 and DT2_Featured). Random Forest combines multiple decision trees to improve accuracy, reduce overfitting, and handle variance effectively.

3.1. Train the Random Forest Model

We will use the randomForest package to train the RF model.

# Load required library
library(randomForest)
## randomForest 4.7-1.1
## Type rfNews() to see new features/changes/bug fixes.
## 
## Attaching package: 'randomForest'
## The following object is masked from 'package:dplyr':
## 
##     combine
## The following object is masked from 'package:ggplot2':
## 
##     margin
# Ensure the target variable Risk is a factor
train_data_featured$Risk <- as.factor(train_data_featured$Risk)
test_data_featured$Risk <- as.factor(test_data_featured$Risk)

# Train the Random Forest model
set.seed(123) # For reproducibility
rf_model <- randomForest(Risk ~ MMR + Year + MMR_Change, data = train_data_featured, ntree = 100, mtry = 2, importance = TRUE)

# Print the model summary
print(rf_model)
## 
## Call:
##  randomForest(formula = Risk ~ MMR + Year + MMR_Change, data = train_data_featured,      ntree = 100, mtry = 2, importance = TRUE) 
##                Type of random forest: classification
##                      Number of trees: 100
## No. of variables tried at each split: 2
## 
##         OOB estimate of  error rate: 0%
## Confusion matrix:
##           High Risk Low Risk class.error
## High Risk      1422        0           0
## Low Risk          0     3849           0
# Plot error rate vs number of trees
plot(rf_model)

3.2. Evaluate the Model on Test Data

We make predictions and compute performance metrics using the random forest model.

# Make predictions on the test set
rf_predictions <- predict(rf_model, test_data_featured)

# Ensure predictions and actual values are factors with the same levels
rf_predictions <- factor(rf_predictions, levels = levels(test_data_featured$Risk))

# Compute confusion matrix and performance metrics
confusion_matrix_rf <- confusionMatrix(rf_predictions, test_data_featured$Risk)

# Print confusion matrix and performance metrics
print(confusion_matrix_rf)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  High Risk Low Risk
##   High Risk       355        0
##   Low Risk          0      962
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9972, 1)
##     No Information Rate : 0.7304     
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0000     
##             Specificity : 1.0000     
##          Pos Pred Value : 1.0000     
##          Neg Pred Value : 1.0000     
##              Prevalence : 0.2696     
##          Detection Rate : 0.2696     
##    Detection Prevalence : 0.2696     
##       Balanced Accuracy : 1.0000     
##                                      
##        'Positive' Class : High Risk  
## 

3.3. Feature Importance

We will analyze the importance of features in the Random Forest model.

# Plot feature importance
varImpPlot(rf_model)

# Extract importance values
feature_importance <- importance(rf_model)
print(feature_importance)
##             High Risk    Low Risk MeanDecreaseAccuracy MeanDecreaseGini
## MMR        319.360746 282.1654995          329.7000823      1825.009633
## Year         1.706395  -0.6494916            0.9885282         1.615506
## MMR_Change  -2.817463   1.5154130            1.1299543       251.780780

3.4. Random Forest Model Summary

  • The random forest model was trained using MMR, Year, and MMR_Change as features.
  • Out-of-Bag (OOB) Error Rate: The OOB error rate was reported as 0%, indicating no misclassifications during training.
  • Confusion Matrix (Test Data):
    • Accuracy: 1.0 (100%).
    • Sensitivity: 1.0 (perfect classification of “High Risk” cases).
    • Specificity: 1.0 (perfect classification of “Low Risk” cases).
    • Balanced Accuracy: 1.0.
  • These results suggest that the random forest model perfectly classifies the test data, similar to the overfitting observed in DT1.

3.5. Feature Importance

  • The importance of features was analyzed using Mean Decrease in Accuracy and Gini Impurity:
    • MMR:
      • Mean Decrease in Accuracy: 329.7.
      • Mean Decrease in Gini: 1825.0.
      • In conclusion: MMR is the most important feature, consistent with DT1 and DT2_Featured results.
    • MMR_Change:
      • Mean Decrease in Accuracy: 1.13.
      • Mean Decrease in Gini: 251.8.
      • In conclusion: While less significant than MMR, MMR_Change contributes meaningfully by capturing temporal trends.
    • Year:
      • Mean Decrease in Accuracy: 0.99.
      • Mean Decrease in Gini: 1.61.
      • In conclusion: Year has minimal predictive importance compared to the other features.

3.7. Conclusion:

The random forest model achieved perfect classification on the test set, showcasing excellent predictive performance. However, signs of overfitting emerged, primarily due to its heavy reliance on the MMR variable. To address this limitation, we proceeded to enhance the model by introducing additional features. This effort aims to improve the model’s robustness, reduce overfitting, and ensure its applicability to real-world scenarios through a more balanced reliance on diverse predictors.

4. New Enhanced Random Forest Model to addressing overfitting (due by MMR)

4.1. Including New Features

To address overfitting in the previous random forest model, we incorporated new features from The World Bank - Data. These include “Pregnant women receiving prenatal care (%)”, reflecting healthcare access, and “IncomeGroup”, categorizing countries by income tiers. The data was unpivoted, aligned with the existing dataset, and missing values were imputed to ensure completeness.

# Load necessary libraries
library(dplyr)
library(tidyr)
library(readxl)

# Read the Excel file
prenatal_data <- read_excel("Pregnant_women_receiving_prenatal_care_data.xlsx")

# Inspect the data structure
#glimpse(prenatal_data)

# Unpivot (pivot longer) the year columns into a single column
prenatal_data_long <- prenatal_data %>%
  pivot_longer(cols = starts_with("19") | starts_with("20"), # Select year columns (1985 to 2020)
               names_to = "Year", 
               values_to = "Pregnant_women_receiving_prenatal_care") %>% 
  rename(Country = "Country Name") %>%
  mutate(Year = as.numeric(Year)) # Convert Year to numeric for merging

# Select relevant columns
prenatal_data_clean <- prenatal_data_long %>%
  select(Country, Year, Pregnant_women_receiving_prenatal_care, IncomeGroup) %>%
  arrange(Country, Year) # Ensure sorted by Country and Year

# Merge with the current dataset (eda_data)
merged_data <- eda_data %>%
  left_join(prenatal_data_clean, by = c("Country", "Year"))


# Impute missing values for Pregnant_women_receiving_prenatal_care
merged_data <- merged_data %>%
  group_by(Country) %>%
  mutate(Pregnant_women_receiving_prenatal_care = ifelse(
    is.na(Pregnant_women_receiving_prenatal_care), 
    ifelse(
      all(is.na(Pregnant_women_receiving_prenatal_care)), # If the country has no values at all
      median(merged_data$Pregnant_women_receiving_prenatal_care, na.rm = TRUE), # Use the median of all years
      mean(Pregnant_women_receiving_prenatal_care, na.rm = TRUE) # Otherwise, use the country-specific mean
    ),
    Pregnant_women_receiving_prenatal_care
  )) %>%
  ungroup()

# Impute missing values for IncomeGroup
most_frequent_income_group <- merged_data %>%
  count(IncomeGroup) %>%
  arrange(desc(n)) %>%
  slice(1) %>%
  pull(IncomeGroup)

merged_data <- merged_data %>%
  mutate(IncomeGroup = ifelse(
    is.na(IncomeGroup), 
    most_frequent_income_group, 
    IncomeGroup
  ))

# Inspect the data to confirm the imputations
#glimpse(merged_data)

4.2. Splitting merged_data to Train and Test Data

# Set seed for reproducibility
set.seed(123)

# Split merged_data into training and testing datasets
train_index <- createDataPartition(merged_data$Risk, p = 0.8, list = FALSE) # 80% training, 20% testing
train_data_rf <- merged_data[train_index, ]
test_data_rf <- merged_data[-train_index, ]

# Ensure the target variable 'Risk' is a factor in both train and test datasets
train_data_rf$Risk <- as.factor(train_data_rf$Risk)
test_data_rf$Risk <- as.factor(test_data_rf$Risk)

# Inspect the structure of the train and test datasets
#summary(train_data_rf)
#summary(test_data_rf)

4.3 New Enhanced Random Forest Model

Now that we have incorporated new features like Pregnant_women_receiving_prenatal_care (PWRPC) and IncomeGroup into the dataset, we can proceed with creating a new Random Forest (RF) model. The following R code trains the updated RF model:

# Load the randomForest library
library(randomForest)

merged_data <- merged_data %>%
  rename(PWRPC = Pregnant_women_receiving_prenatal_care)

# Ensure the target variable 'Risk' is a factor
merged_data$Risk <- as.factor(merged_data$Risk)

# Train the new Random Forest model
set.seed(123)  # For reproducibility
new_rf_model <- randomForest(
  Risk ~ MMR + Year + MMR_Change + PWRPC + IncomeGroup,
  data = merged_data,
  ntree = 100,  # Number of trees
  mtry = 3,     # Number of features to consider for each split
  importance = TRUE
)

# Print the model summary
print(new_rf_model)
## 
## Call:
##  randomForest(formula = Risk ~ MMR + Year + MMR_Change + PWRPC +      IncomeGroup, data = merged_data, ntree = 100, mtry = 3, importance = TRUE) 
##                Type of random forest: classification
##                      Number of trees: 100
## No. of variables tried at each split: 3
## 
##         OOB estimate of  error rate: 0%
## Confusion matrix:
##           High Risk Low Risk class.error
## High Risk      1777        0           0
## Low Risk          0     4811           0
# Plot error rate vs number of trees
plot(new_rf_model)

# Evaluate feature importance
varImpPlot(new_rf_model)

# Make predictions and compute the confusion matrix
rf_predictions <- predict(new_rf_model, merged_data)
confusion_matrix_rf <- confusionMatrix(rf_predictions, merged_data$Risk)

# Print confusion matrix and performance metrics
print(confusion_matrix_rf)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  High Risk Low Risk
##   High Risk      1777        0
##   Low Risk          0     4811
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9994, 1)
##     No Information Rate : 0.7303     
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0000     
##             Specificity : 1.0000     
##          Pos Pred Value : 1.0000     
##          Neg Pred Value : 1.0000     
##              Prevalence : 0.2697     
##          Detection Rate : 0.2697     
##    Detection Prevalence : 0.2697     
##       Balanced Accuracy : 1.0000     
##                                      
##        'Positive' Class : High Risk  
## 

4.4 Discussion: Enhanced Random Forest Model with Additional Features

The previous Random Forest model was robust but exhibited potential overfitting issues, largely due to limited feature variability and high dependence on MMR. To address these concerns, we introduced two new features: - Pregnant_women_receiving_prenatal_care: Percentage of pregnant women receiving at least one prenatal care visit in each country. - IncomeGroup: Categorization of countries into income groups (High income, Upper middle income, Lower middle income, Low income).

These features were carefully imputed to handle missing values. The results demonstrate the effectiveness of this approach.

4.4.1. Model Performance

Metrics: The enhanced Random Forest model achieved the following metrics: - Accuracy: 1.0
- Sensitivity: 1.0
- Specificity: 1.0
- Balanced Accuracy: 1.0
- Kappa: 1.0

These metrics indicate perfect classification of both “High Risk” and “Low Risk” cases.

Error Rate Plot: The error rate vs. number of trees plot confirms that the model’s error quickly converges to zero, indicating excellent stability and performance.

4.4.2. Feature Importance

Variable Contribution: The variable importance plots (Mean Decrease Accuracy and Mean Decrease Gini) highlight the influence of the features: - MMR: Remains the most critical variable, reflecting its strong correlation with maternal risk. - IncomeGroup: Second most important, showcasing socio-economic disparity in maternal health outcomes. - Pregnant_women_receiving_prenatal_care (PWRPC): Third most important, capturing access to healthcare as a key determinant of maternal risk. - MMR_Change: Provides temporal dynamics. - Year: Contributes minimal but consistent influence.

4.4.3 Addressing Issues in the Previous RF Model

The previous Random Forest model exhibited the following concerns: 1. Overfitting: Dominance of MMR potentially led to overfitting. 2. Limited Generalizability: Lack of features to capture socio-economic and healthcare variability across countries.

Resolutions in the Enhanced Model: 1. Overfitting Mitigation: By introducing features such as Pregnant_women_receiving_prenatal_care, the model became less reliant on MMR, distributing its predictive power across multiple features. 2. Improved Variability: IncomeGroup captures country-level economic disparities, enhancing generalizability.

4.4.4 Conclusion and Insights

The enhanced Random Forest model demonstrates significant improvements over the previous iteration by addressing overfitting and incorporating socio-economic (IncomeGroup) and healthcare-related (Pregnant_women_receiving_prenatal_care) features. These additions have notably enhanced model robustness and interpretability, achieving an accuracy of 1.0.

The results emphasize the value of comprehensive data in predicting maternal health risks effectively. By integrating diverse socio-economic and healthcare indicators, the model showcases the potential for refined maternal risk predictions and ensures reliable performance, setting a strong foundation for future advancements in maternal health analytics.

5. Models Comparison

To compare the results of the Enhanced Random Forest model with the previous models (DT1, DT2_Featured, and the previous RF model), we will create a summary table of performance metrics.

# Collect performance metrics from each model
# DT1 Metrics
dt1_metrics <- confusion_matrix_dt1$byClass[c("Sensitivity", "Specificity", "Precision", "Recall", "Balanced Accuracy")]
dt1_accuracy <- confusion_matrix_dt1$overall["Accuracy"]

# DT2_Featured Metrics
dt2_featured_metrics <- confusion_matrix_dt2_featured$byClass[c("Sensitivity", "Specificity", "Precision", "Recall", "Balanced Accuracy")]
dt2_featured_accuracy <- confusion_matrix_dt2_featured$overall["Accuracy"]

# Previous RF Metrics
previous_rf_metrics <- confusion_matrix_rf$byClass[c("Sensitivity", "Specificity", "Precision", "Recall", "Balanced Accuracy")]
previous_rf_accuracy <- confusion_matrix_rf$overall["Accuracy"]

# Enhanced RF Metrics
enhanced_rf_metrics <- confusion_matrix_rf$byClass[c("Sensitivity", "Specificity", "Precision", "Recall", "Balanced Accuracy")]
enhanced_rf_accuracy <- confusion_matrix_rf$overall["Accuracy"]

# Combine metrics into a single data frame for comparison
comparison_table <- data.frame(
  Metric = c("Accuracy", "Sensitivity", "Specificity", "Precision", "Recall", "Balanced Accuracy"),
  DT1 = c(dt1_accuracy, as.numeric(dt1_metrics)),
  DT2_Featured = c(dt2_featured_accuracy, as.numeric(dt2_featured_metrics)),
  Previous_RF = c(previous_rf_accuracy, as.numeric(previous_rf_metrics)),
  Enhanced_RF = c(enhanced_rf_accuracy, as.numeric(enhanced_rf_metrics))
)

# Print the comparison table
print(comparison_table)
##              Metric DT1 DT2_Featured Previous_RF Enhanced_RF
## 1          Accuracy   1    0.8526955           1           1
## 2       Sensitivity   1    0.6535211           1           1
## 3       Specificity   1    0.9261954           1           1
## 4         Precision   1    0.7656766           1           1
## 5            Recall   1    0.6535211           1           1
## 6 Balanced Accuracy   1    0.7898583           1           1

Discussion of Model Comparison:

The performance comparison highlights key improvements as we progressed from DT1 to the Enhanced RF model:

  1. DT1: While achieving perfect metrics, DT1 relied heavily on simplistic splits, making it prone to overfitting and limiting its practical applicability.

  2. DT2_Featured: Adding MMR_Change improved precision and specificity but exposed the model’s challenges in identifying “High Risk” cases, reflected in its moderate sensitivity (0.6535).

  3. Previous RF Model: The Random Forest model improved overall classification but relied primarily on the MMR feature, leading to overfitting despite perfect test accuracy.

  4. Enhanced RF Model: By including socio-economic (IncomeGroup) and healthcare-related (Pregnant_women_receiving_prenatal_care) features, the Enhanced RF model retained perfect metrics while addressing overfitting and increasing interpretability, making it the most robust and generalizable model.


Final Conclusion

Summary of Findings The Enhanced RF model outperformed previous models by integrating new features and addressing overfitting concerns. It effectively balanced performance and robustness, showcasing the importance of thoughtful feature engineering.

Key Takeaways

  • Feature Engineering: The inclusion of additional healthcare and socio-economic features enhanced both robustness and interpretability.

  • Model Selection: Ensemble approaches like Random Forests mitigate overfitting and boost predictive power.

Closing Thought This project demonstrates the value of iterative refinement in machine learning. From simplistic models to feature-rich, ensemble-based methods, we showcased how thoughtful feature selection and advanced techniques can produce reliable predictions, providing actionable insights for maternal health risk assessment.