The aim of this project is to predict which variables best predict whether an individual earns more than $50,000 per year. I will be using the Adult Census Income dataset, pulled from the 1994 US Census Database by Barry Becker. It’s made up of information such as occupation, age, native country, race, capital gain, capital loss, education, work class, and more. The focal point of this project is to predict which variables most effectively determine whether an individual earns more than $50,000 per year, framing it as a classification problem where the outcome is binary: earning <=50K or >50K per year.
Various machine learning models will be explored and evaluated to handle the classification task, including logistic regression, decision trees, and more complex approaches like random forests and elastic net models. I start with essential steps such as handling missing data, normalizing data, and encoding categorical variables to prepare the data for effective model training and then finish with using ROC AUC as a metric to assess model performance, focusing on the model’s ability to discriminate between the two income classes accurately.
ROC AUC is a widely used metric to evaluate the performance of binary classification models. It stands for “Receiver Operating Characteristic - Area Under the Curve.” The curve is created by plotting the True Positive Rate (TPR) against the False Positive Rate (FPR) at various threshold settings.
Jain, Aruna. Adult Income Census Dataset. Kaggle, https://www.kaggle.com/datasets/jainaru/adult-income-census-dataset. Accessed 28 May 2024.
age Type: Numeric Description: This variable represents the age of the individual. The values range from a minimum of 17 to a maximum of 90 years, with a mean age of approximately 38.58 years.
workclass Type: Categorical (character) Description: This variable represents the work classification of the individual. It includes categories such as ‘Private’, ‘Self-emp-not-inc’, ‘Local-gov’, etc. This variable has various class labels indicating the sector of employment.
fnlwgt Type: Numeric Description: The final weight is the number of units in the target population that the responding unit represents. It is used to adjust the dataset to be representative of the general population.
education Type: Categorical (character) Description: This variable represents the highest level of education attained by the individual. Categories include ‘Bachelors’, ‘HS-grad’, ‘11th’, ‘Masters’, ‘9th’, ‘Some-college’, ‘Assoc-acdm’, ‘Assoc-voc’, etc.
education.num Type: Ordered Categorical (integer) Description: This variable provides a numerical representation of the educational attainment. It ranges from 1 to 16, with 1 representing the lowest level of education and 16 representing the highest.
marital.status Type: Categorical (character) Description: This variable indicates the marital status of the individual. Categories include ‘Married-civ-spouse’, ‘Divorced’, ‘Never-married’, ‘Separated’, ‘Widowed’, ‘Married-spouse-absent’, and ‘Married-AF-spouse’.
occupation Type: Categorical (character) Description: This variable describes the type of occupation of the individual. Categories include ‘Tech-support’, ‘Craft-repair’, ‘Other-service’, ‘Sales’, ‘Exec-managerial’, ‘Prof-specialty’, ‘Handlers-cleaners’, etc.
relationship Type: Categorical (character) Description: This variable indicates the individual’s relationship within the household. Categories include ‘Wife’, ‘Own-child’, ‘Husband’, ‘Not-in-family’, ‘Other-relative’, and ‘Unmarried’.
race Type: Categorical (character) Description: This variable represents the race of the individual. Categories include ‘White’, ‘Asian-Pac-Islander’, ‘Amer-Indian-Eskimo’, ‘Other’, and ‘Black’.
sex Type: Categorical (character) Description: This variable indicates the sex of the individual. The categories are ‘Male’ and ‘Female’.
capital.gain Type: Numeric Description: This variable represents the capital gains earned by the individual from investment sources. The values range from 0 to 99,999.
capital.loss Type: Numeric Description: This variable represents the capital losses incurred by the individual from investment sources. The values range from 0 to 4,356.
hours.per.week Type: Numeric Description: This variable indicates the number of hours the individual works per week. The values range from 1 to 99 hours, with a mean of 40.44 hours per week.
native.country Type: Categorical (character) Description: This variable represents the native country of the individual. Categories include ‘United-States’, ‘Cambodia’, ‘England’, ‘Puerto-Rico’, ‘Canada’, ‘Germany’, ‘Outlying-US(Guam-USVI-etc)’, etc.
income Type: Categorical (character) Description: This is the target variable indicating whether the individual’s income is above or below $50,000 per year. The categories are ‘>50K’ and ‘<=50K’.
We start by loading in all the necessary libraries for the remainder of our project.
library(readr)
library(tidymodels)
## ── Attaching packages ────────────────────────────────────── tidymodels 1.2.0 ──
## ✔ broom 1.0.5 ✔ recipes 1.0.10
## ✔ dials 1.2.1 ✔ rsample 1.2.1
## ✔ dplyr 1.1.4 ✔ tibble 3.2.1
## ✔ ggplot2 3.5.1 ✔ tidyr 1.3.1
## ✔ infer 1.0.7 ✔ tune 1.2.1
## ✔ modeldata 1.3.0 ✔ workflows 1.1.4
## ✔ parsnip 1.2.1 ✔ workflowsets 1.1.0
## ✔ purrr 1.0.2 ✔ yardstick 1.3.1
## ── Conflicts ───────────────────────────────────────── tidymodels_conflicts() ──
## ✖ purrr::discard() masks scales::discard()
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ✖ yardstick::spec() masks readr::spec()
## ✖ recipes::step() masks stats::step()
## • Search for functions across packages at https://www.tidymodels.org/find/
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ lubridate 1.9.3
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ scales::col_factor() masks readr::col_factor()
## ✖ purrr::discard() masks scales::discard()
## ✖ dplyr::filter() masks stats::filter()
## ✖ stringr::fixed() masks recipes::fixed()
## ✖ dplyr::lag() masks stats::lag()
## ✖ yardstick::spec() masks readr::spec()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
library(tidyr)
library(ggplot2)
library(corrplot)
## corrplot 0.92 loaded
library(forcats)
library(naniar)
library(yardstick) # for area under ROC curve for log model
library(themis) # for upsampling
library(vip) #for important predictors
##
## Attaching package: 'vip'
##
## The following object is masked from 'package:utils':
##
## vi
library(ranger)
library(discrim)
##
## Attaching package: 'discrim'
##
## The following object is masked from 'package:dials':
##
## smoothness
library(poissonreg)
library(broom)
library(parsnip)
library(ranger)
tidymodels_prefer() #for the select function
income_data <- read_csv("C:/Users/roxy/Documents/school/Spring Qrtr -2024/pstat 131/pstat 131 final projecgt/adult_income_cleaned.csv")
## Rows: 32561 Columns: 15
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (9): workclass, education, marital.status, occupation, relationship, rac...
## dbl (6): age, fnlwgt, education.num, capital.gain, capital.loss, hours.per.week
##
## ℹ 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.
We will poke around the data set and see if we can identify any patterns. We are mainly trying to display basic information and summary statistics.
# Display basic information and summary statistics
income_data %>%
head()
## # A tibble: 6 × 15
## age workclass fnlwgt education education.num marital.status occupation
## <dbl> <chr> <dbl> <chr> <dbl> <chr> <chr>
## 1 90 <NA> 77053 HS-grad 9 Widowed <NA>
## 2 82 Private 132870 HS-grad 9 Widowed Exec-manager…
## 3 66 <NA> 186061 Some-college 10 Widowed <NA>
## 4 54 Private 140359 7th-8th 4 Divorced Machine-op-i…
## 5 41 Private 264663 Some-college 10 Separated Prof-special…
## 6 34 Private 216864 HS-grad 9 Divorced Other-service
## # ℹ 8 more variables: relationship <chr>, race <chr>, sex <chr>,
## # capital.gain <dbl>, capital.loss <dbl>, hours.per.week <dbl>,
## # native.country <chr>, income <chr>
colnames(income_data)
## [1] "age" "workclass" "fnlwgt" "education"
## [5] "education.num" "marital.status" "occupation" "relationship"
## [9] "race" "sex" "capital.gain" "capital.loss"
## [13] "hours.per.week" "native.country" "income"
dim(income_data)
## [1] 32561 15
str(income_data)
## spc_tbl_ [32,561 × 15] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ age : num [1:32561] 90 82 66 54 41 34 38 74 68 41 ...
## $ workclass : chr [1:32561] NA "Private" NA "Private" ...
## $ fnlwgt : num [1:32561] 77053 132870 186061 140359 264663 ...
## $ education : chr [1:32561] "HS-grad" "HS-grad" "Some-college" "7th-8th" ...
## $ education.num : num [1:32561] 9 9 10 4 10 9 6 16 9 10 ...
## $ marital.status: chr [1:32561] "Widowed" "Widowed" "Widowed" "Divorced" ...
## $ occupation : chr [1:32561] NA "Exec-managerial" NA "Machine-op-inspct" ...
## $ relationship : chr [1:32561] "Not-in-family" "Not-in-family" "Unmarried" "Unmarried" ...
## $ race : chr [1:32561] "White" "White" "Black" "White" ...
## $ sex : chr [1:32561] "Female" "Female" "Female" "Female" ...
## $ capital.gain : num [1:32561] 0 0 0 0 0 0 0 0 0 0 ...
## $ capital.loss : num [1:32561] 4356 4356 4356 3900 3900 ...
## $ hours.per.week: num [1:32561] 40 18 40 40 40 45 40 20 40 60 ...
## $ native.country: chr [1:32561] "United-States" "United-States" "United-States" "United-States" ...
## $ income : chr [1:32561] "<=50K" "<=50K" "<=50K" "<=50K" ...
## - attr(*, "spec")=
## .. cols(
## .. age = col_double(),
## .. workclass = col_character(),
## .. fnlwgt = col_double(),
## .. education = col_character(),
## .. education.num = col_double(),
## .. marital.status = col_character(),
## .. occupation = col_character(),
## .. relationship = col_character(),
## .. race = col_character(),
## .. sex = col_character(),
## .. capital.gain = col_double(),
## .. capital.loss = col_double(),
## .. hours.per.week = col_double(),
## .. native.country = col_character(),
## .. income = col_character()
## .. )
## - attr(*, "problems")=<externalptr>
summary(income_data)
## age workclass fnlwgt education
## Min. :17.00 Length:32561 Min. : 12285 Length:32561
## 1st Qu.:28.00 Class :character 1st Qu.: 117827 Class :character
## Median :37.00 Mode :character Median : 178356 Mode :character
## Mean :38.58 Mean : 189778
## 3rd Qu.:48.00 3rd Qu.: 237051
## Max. :90.00 Max. :1484705
## education.num marital.status occupation relationship
## Min. : 1.00 Length:32561 Length:32561 Length:32561
## 1st Qu.: 9.00 Class :character Class :character Class :character
## Median :10.00 Mode :character Mode :character Mode :character
## Mean :10.08
## 3rd Qu.:12.00
## Max. :16.00
## race sex capital.gain capital.loss
## Length:32561 Length:32561 Min. : 0 Min. : 0.0
## Class :character Class :character 1st Qu.: 0 1st Qu.: 0.0
## Mode :character Mode :character Median : 0 Median : 0.0
## Mean : 1078 Mean : 87.3
## 3rd Qu.: 0 3rd Qu.: 0.0
## Max. :99999 Max. :4356.0
## hours.per.week native.country income
## Min. : 1.00 Length:32561 Length:32561
## 1st Qu.:40.00 Class :character Class :character
## Median :40.00 Mode :character Mode :character
## Mean :40.44
## 3rd Qu.:45.00
## Max. :99.00
There are 15 columns. That means 15 different variables. These variables are: ‘age’, ‘fnlwgt’, ‘education.num’, ‘marital.status’, ‘occupation’, ‘relationship’, ‘race’, ‘sex’, ‘capital.gain’, ‘capital.loss’, ‘hours.per.week’, ‘native.country’, and lastly our predictor variable: ‘income’.
Converting variables into factors is crucial for ensuring proper data handling during analysis. For income, it ensures that the variable is correctly interpreted as categorical with two distinct groups, crucial for classification tasks. For education.num, turning it into an ordered factor acknowledges the inherent order in educational levels, which can be important for analyses that explore trends in education in relation to other variables. This approach enhances the accuracy and interpretability of your statistical models and analysis results.
# turning income and education.num into a factor variable
income_data$income <- factor(income_data$income, levels = c(">50K", "<=50K"))
income_data$education.num <- factor(income_data$education.num,
levels = 1:16,
ordered = TRUE)
#check levels to confirm
class(income_data$income)
## [1] "factor"
class(income_data$education.num)
## [1] "ordered" "factor"
levels(income_data$income)
## [1] ">50K" "<=50K"
levels(income_data$education.num)
## [1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14" "15"
## [16] "16"
Now, let’s check for correlation. Are any of our variables correlated?
cor_income_data1 <- income_data %>%
select(where(is.numeric)) %>% # selecting numeric columns
cor(use = "pairwise.complete.obs") %>% # handling missing data
corrplot(type = "lower", diag = FALSE) # printing lower half of matrix
From the matrix, it appears that most variables show very light-colored circles, which suggests that there are no strong correlations between the variables.
When we’re working with a categorical outcome, we are often interested in whether the levels of our outcome are balanced, meaning whether the count of observations at one level is approximately equal to the count at the other level(s).
income_data %>%
ggplot(aes(income)) +
geom_bar()
Since >50k has 7,841 observations and <=50k has 24,720 observations, this means we will later upsample our outcome variable.
Most of my variables are categorical variables, but I plotted by one numeric variable using a histogram to see the distribution of age.
ggplot(income_data, aes(x = age)) +
geom_histogram(bins = 30, fill = "blue", color = "black") +
labs(title = "Histogram of Age", x = "Age", y = "Count") +
theme_minimal()
The histogram shows a “skewed left” distribution, where a larger number
of individuals are younger, and fewer individuals are older, with the
frequency decreasing as age increases.
I made bar charts for some categorical variables to understand the frequency of each category and how they’re related to the predictor variables.
Graph 1 shows the income distribution within various workclasses,
highlighting significant income inequality between sectors such as the
Federal government and private work. Graph 2 illustrates gender
disparities in income, with a visible difference in income levels
between males and females across the two main income categories. Graph 3
visualizes a clear trend of higher education levels correlating with
higher income, indicating the strong impact of educational attainment on
earning potential. Graph 4 suggests differences in income distribution
across marital statuses, potentially indicating economic advantages or
disadvantages associated with marital ties. Graph 5 displays the income
distribution across various occupations, identifying which professions
are more likely to earn above the $50K threshold. Graph 6 is an updated
graph on workclass and income which shows what percentage of each
workclass makes above or below 50k.
Now that we got a good feel for our data, let’s move on
Let’s start with counting how much of my data is missing.
vis_miss(income_data)
This shows us 0.9% of the entire data set is missing. In workclass, 6% of the observations are missing, 6% in occupation, and 2% in native.country.
The 6% in workclass and occupation is significant, but not to the point we’d need to complicated of methods to deal with it.
I decided to go with imputation to deal with my missing data. Although simple, this approach is particularly beneficial because it avoids the complications and potential biases associated with listwise deletion (dropping rows with any missing values) or more complex imputation methods that require detailed assumptions about the nature of the data and the mechanisms behind the missing data.
Imputation is a method used to fill in missing data with substituted values. ‘Impute_mean’ is the process of substituting missing values in a numerical dataset with the mean (average) of the non-missing values in the same dataset. ‘Impute_mode’ involves replacing missing values with the mode, which is the most frequently occurring value in the dataset. Using the mean or mode helps in maintaining the overall distribution of the dataset, especially if the missing data is a small fraction of the total data.
We do this all in one step in our following recipe.
Before building our model, we split our dataset into training, testing, and validation sets to ensure accurate evaluation and to prevent overfitting. We opted for a 75/25 split between training and testing sets. This allows the model to learn from a substantial amount of data while reserving a significant portion for unbiased evaluation.
Additionally, a validation set comprising 75% of the training data is used for model tuning. This step is crucial to refine the model without using the test set, which remains untouched to provide a final measure of the model’s performance.
We set a random seed to ensure that our data split is reproducible, and we stratify the split by the income variable to maintain consistent income category proportions across the datasets. This approach ensures that our model training and testing are both rigorous and reproducible.
set.seed(2003)
income_split <- initial_split(income_data, strata = "income", prop = 0.75)
income_train <- training(income_split)
income_test <- testing(income_split)
# Setting Up Cross-Validationt
income_valid <- validation_split(income_train, prop = .75, strata = income)
## Warning: `validation_split()` was deprecated in rsample 1.2.0.
## ℹ Please use `initial_validation_split()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Class imbalance is significant because it can lead models to exhibit bias toward the majority class. In predictive modeling, if one class significantly outnumbers the other, the model might become very good at predicting the majority class but fail to accurately predict the minority class. This can result in poor model performance, especially when the predictive accuracy of the minority class is crucial.
Let’s see the balance of each class in our dataset.
income_props <- income_train %>%
group_by(income) %>%
summarise(prop = n()/(dim(income_train)[1])) %>%
ungroup()
count(income_train)
## # A tibble: 1 × 1
## n
## <int>
## 1 24420
count(income_test)
## # A tibble: 1 × 1
## n
## <int>
## 1 8141
head(income_train)
## # A tibble: 6 × 15
## age workclass fnlwgt education education.num marital.status occupation
## <dbl> <chr> <dbl> <chr> <ord> <chr> <chr>
## 1 90 <NA> 77053 HS-grad 9 Widowed <NA>
## 2 82 Private 132870 HS-grad 9 Widowed Exec-manager…
## 3 66 <NA> 186061 Some-college 10 Widowed <NA>
## 4 41 Private 264663 Some-college 10 Separated Prof-special…
## 5 29 Private 77009 11th 7 Separated Sales
## 6 61 Private 29059 HS-grad 9 Divorced Sales
## # ℹ 8 more variables: relationship <chr>, race <chr>, sex <chr>,
## # capital.gain <dbl>, capital.loss <dbl>, hours.per.week <dbl>,
## # native.country <chr>, income <fct>
Our data is imbalanced. It looks like 75% of the data is <=50k and
25% is >50k. To address this imbalance, I use upsampling within my
data preprocessing recipe. Upsampling involves artificially inflating
the minority class (>50K in this case) by replicating its
observations so that the class proportions are more balanced.
To ensure our models are robust and well-prepared, we’ve developed a
standardized preprocessing recipe for our income_data.
After setting up these preprocessing steps, we check for any remaining
missing values to ensure data integrity before moving forward with
modeling.
income_recipe <- recipe(income ~ ., data = income_train) %>%
step_impute_mean(all_numeric_predictors(), -all_outcomes()) %>% # Impute missing values for numeric predictors
step_impute_mode(all_nominal_predictors(), -all_outcomes()) %>% # Impute missing values for categorical predictors
step_dummy(all_nominal_predictors()) %>%
step_center(all_predictors()) %>%
step_scale(all_predictors()) %>%
step_upsample(income, over_ratio = 0.5, skip = TRUE)
The income_recipe is prepared with prep() and applied to income_train using bake(). This processes the entire dataset based on the steps defined in the recipe, such as imputation, normalization, and encoding. The processed data is then grouped by the income category, and the number of records in each income group is counted using summarise(count = n()). This helps assess the distribution of data across different income classes post-processing. Finally, the script checks for any remaining missing values in the processed data with sum(is.na(processed_data)) to ensure data integrity and completeness after preprocessing.
processed_data <- prep(income_recipe) %>% bake(new_data = income_train) %>%
group_by(income) %>%
summarise(count = n())
# Prepare the recipe with the training data
prepared_recipe <- prep(income_recipe, training = income_train)
sum(is.na(processed_data))
## [1] 0
The general workflow of model building is setting up the model by specifying what type it is, set it to ‘classification’ for our case, and then set up the workflow.
For most of my models I then set up a tuning grid with the parameters I want tuned, tune the model, and then select the most accurate. I will then finalize the workdflow with these tuning paramters. We will then fit that model with our workflow to our training dataset.
Specify an Engine, then create a workflow
log_reg <- logistic_reg()%>%
set_engine("glm") %>%
set_mode("classification")
#workflow
income_log_wkflow <- workflow() %>%
add_model(log_reg) %>%
add_recipe(income_recipe)
control <- control_resamples(save_pred = TRUE, save_workflow = TRUE)
This workflow employs a logistic regression model configured for classification with the glm engine. It is ideal for this dataset as it effectively models binary outcomes (like income levels) and provides probabilities for each class, helping interpretation of results.
en_model <- logistic_reg(mixture = tune(),
penalty = tune()) %>%
set_mode("classification") %>%
set_engine("glmnet")
en_workflow <- workflow() %>%
add_recipe(income_recipe) %>%
add_model(en_model)
The Elastic Net model uses logistic regression with both lasso and ridge penalties to handle data with multicollinearity and to enhance model selection capabilities. The logistic regression framework is adapted to tune both the mixture and penalty parameters, using the glmnet engine for optimization.
## K NEAREST NEIGHBOR MODEL
knn_income_model <- nearest_neighbor(neighbors = tune()) %>%
set_mode("classification") %>%
set_engine("kknn")
knn_income_wkflow <- workflow() %>%
add_model(knn_income_model) %>%
add_recipe(income_recipe)
income_fold <- vfold_cv(income_train, v = 10)
We’ll use 10 folds to perform stratified cross validation.
The KNN model, set to optimize the number of neighbors, is particularly useful for the dataset as it classifies individuals based on similarity in feature space, which can capture non-linear relationships that might exist between demographic variables and income.
tree_spec <- decision_tree(cost_complexity = tune()) %>%
set_engine("rpart") %>%
set_mode("classification")
tree_wf <- workflow() %>%
add_model(tree_spec) %>%
add_recipe(income_recipe)
A Decision Tree model, with tunable complexity, is incorporated for its ability to handle categorical data effectively and provide interpretable results, making it suitable for the dataset where interpretability regarding how different features impact income levels is crucial.
rf_spec <- rand_forest(mtry = tune(),
trees = tune(),
min_n = tune()) %>%
set_engine("ranger", importance = "impurity") %>%
set_mode("classification")
rf_wf <- workflow() %>%
add_model(rf_spec) %>%
add_recipe(income_recipe)
This Random Forest workflow utilizes multiple decision trees to enhance predictive accuracy and control overfitting, ideal for complex datasets with numerous predictors and relationships like the census income dataset. Its ensemble approach is robust against overfitting and capable of handling high-dimensional data effectively.
#Create grids for KNN parameter (neighbors)
neighbors_grid <- grid_regular(neighbors(range = c(1, 10)), levels = 10)
neighbors_grid
## # A tibble: 10 × 1
## neighbors
## <int>
## 1 1
## 2 2
## 3 3
## 4 4
## 5 5
## 6 6
## 7 7
## 8 8
## 9 9
## 10 10
This line creates a grid of hyperparameters for the K-Nearest Neighbors model. The grid specifies the number of neighbors to explore, ranging from 1 to 10. The grid_regular function is used to generate a regular sequence of values within this range, distributing 10 levels evenly across the range.
#tuning grind for elastic net regularization
en_grid <- grid_regular(penalty(range = c(0, 1),
trans = identity_trans()),
mixture(range = c(0, 1)),
levels = 10)
For the Elastic Net model, this grid defines two parameters: penalty for the regularization strength and mixture for the balance between L1 and L2 regularization. Both parameters are varied from 0 to 1, and the identity_trans function indicates that these parameters are used as-is without transformation.
# Tuning grid for decision tree
param_grid <- grid_regular(
cost_complexity(c(-3, 1)),
levels =10
)
This sets up a grid for tuning the cost complexity parameter of a decision tree, ranging from -3 to 1. This parameter controls the trade-off between tree complexity and fitting accuracy, helping to prevent overfitting.
#Tuning for Random Forests
rf_grid <- grid_regular(mtry(range = c(1, 6)),
trees(range = c(200, 600)),
min_n(range = c(10, 20)),
levels = 5)
rf_grid
## # A tibble: 125 × 3
## mtry trees min_n
## <int> <int> <int>
## 1 1 200 10
## 2 2 200 10
## 3 3 200 10
## 4 4 200 10
## 5 6 200 10
## 6 1 300 10
## 7 2 300 10
## 8 3 300 10
## 9 4 300 10
## 10 6 300 10
## # ℹ 115 more rows
For the Random Forest model, this grid tunes three parameters: mtry (the number of variables to consider for each split), trees (the number of trees in the forest), and min_n (the minimum number of samples in nodes). The ranges provide a variety of combinations to explore optimal settings for model complexity and performance.
“Tuning” refers to the process of finding the optimal hyperparameters for a model. This involves adjusting these parameters to find the combination that offers the best performance, typically evaluated on a validation set or through cross-validation. The objective is to optimize the model’s ability to generalize well to new data, not just to fit the existing data.
The tuning process is a systematic search through a predefined set of model configurations (the grid) to find the one that performs best according to the ROC_AUC. This is crucial for ensuring that the model not only fits the training data well but also generalizes effectively to new, unseen data, thus providing reliable predictions when deployed.
# Knn Fitting
tune_res <- tune_grid(
object = knn_income_wkflow,
resamples = income_fold,
grid = neighbors_grid,
control = control_grid(verbose = TRUE)
)
#Logistic Fitting
fit_res_log <- fit_resamples(
object = income_log_wkflow,
resamples = income_fold,
control = control_resamples(save_pred = TRUE, verbose = TRUE)
)
#Elastic Net Fitting
tune_res_en <- tune_grid(
en_workflow,
resamples = income_fold,
grid = en_grid
)
# Decision tree fitting
tune_res_DT <- tune_grid(
tree_wf,
resamples = income_fold,
grid = param_grid
)
#Random Forest Fitting
tune_rf <- tune_grid(
rf_wf,
resamples = income_fold,
grid = rf_grid
)
# Saving
save(tune_res, file = "knn.rda")
save(fit_res_log, file = "log.rda")
save(tune_res_DT, file = "DT.rda")
save(tune_rf, file = "RF.rda")
save(tune_res_en, file = "EN.rda")
One of the most useful tools for visualizing the results of models that have been tuned is the autoplot function in r. This will visualize the effects that the change in certain parameters has on our metric of choice, roc_auc.
I’m now going to load these tuning results and analyse some performance metrics. Then, I will visualize the tuning results, which helps in understanding how different hyperparameters impact model performance.
load("knn.rda")
load("log.rda")
load("EN.rda")
load("DT.rda")
load("RF.rda")
collect_metrics(tune_res)
## # A tibble: 30 × 7
## neighbors .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 1 accuracy binary 0.782 10 0.00292 Preprocessor1_Model01
## 2 1 brier_class binary 0.218 10 0.00292 Preprocessor1_Model01
## 3 1 roc_auc binary 0.717 10 0.00261 Preprocessor1_Model01
## 4 2 accuracy binary 0.782 10 0.00292 Preprocessor1_Model02
## 5 2 brier_class binary 0.205 10 0.00262 Preprocessor1_Model02
## 6 2 roc_auc binary 0.752 10 0.00271 Preprocessor1_Model02
## 7 3 accuracy binary 0.782 10 0.00292 Preprocessor1_Model03
## 8 3 brier_class binary 0.193 10 0.00236 Preprocessor1_Model03
## 9 3 roc_auc binary 0.779 10 0.00282 Preprocessor1_Model03
## 10 4 accuracy binary 0.782 10 0.00292 Preprocessor1_Model04
## # ℹ 20 more rows
collect_metrics(fit_res_log)
## # A tibble: 3 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 accuracy binary 0.842 10 0.00232 Preprocessor1_Model1
## 2 brier_class binary 0.108 10 0.00102 Preprocessor1_Model1
## 3 roc_auc binary 0.903 10 0.00194 Preprocessor1_Model1
collect_metrics(tune_res_en)
## # A tibble: 300 × 8
## penalty mixture .metric .estimator mean n std_err .config
## <dbl> <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 0 0 accuracy binary 0.840 10 0.00179 Preprocessor1_Mo…
## 2 0 0 brier_class binary 0.111 10 0.000905 Preprocessor1_Mo…
## 3 0 0 roc_auc binary 0.899 10 0.00150 Preprocessor1_Mo…
## 4 0.111 0 accuracy binary 0.836 10 0.00229 Preprocessor1_Mo…
## 5 0.111 0 brier_class binary 0.115 10 0.000864 Preprocessor1_Mo…
## 6 0.111 0 roc_auc binary 0.893 10 0.00161 Preprocessor1_Mo…
## 7 0.222 0 accuracy binary 0.834 10 0.00266 Preprocessor1_Mo…
## 8 0.222 0 brier_class binary 0.120 10 0.000831 Preprocessor1_Mo…
## 9 0.222 0 roc_auc binary 0.890 10 0.00173 Preprocessor1_Mo…
## 10 0.333 0 accuracy binary 0.833 10 0.00243 Preprocessor1_Mo…
## # ℹ 290 more rows
collect_metrics(tune_res_DT)
## # A tibble: 30 × 7
## cost_complexity .metric .estimator mean n std_err .config
## <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 0.001 accuracy binary 0.847 10 0.00302 Preprocessor1_Mod…
## 2 0.001 brier_class binary 0.108 10 0.00157 Preprocessor1_Mod…
## 3 0.001 roc_auc binary 0.890 10 0.00323 Preprocessor1_Mod…
## 4 0.00278 accuracy binary 0.838 10 0.00456 Preprocessor1_Mod…
## 5 0.00278 brier_class binary 0.114 10 0.00181 Preprocessor1_Mod…
## 6 0.00278 roc_auc binary 0.869 10 0.00340 Preprocessor1_Mod…
## 7 0.00774 accuracy binary 0.835 10 0.00402 Preprocessor1_Mod…
## 8 0.00774 brier_class binary 0.116 10 0.00163 Preprocessor1_Mod…
## 9 0.00774 roc_auc binary 0.862 10 0.00344 Preprocessor1_Mod…
## 10 0.0215 accuracy binary 0.841 10 0.00244 Preprocessor1_Mod…
## # ℹ 20 more rows
collect_metrics(tune_rf)
## # A tibble: 375 × 9
## mtry trees min_n .metric .estimator mean n std_err .config
## <int> <int> <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 1 200 10 accuracy binary 0.795 10 0.00252 Preprocessor1_…
## 2 1 200 10 brier_class binary 0.150 10 0.000716 Preprocessor1_…
## 3 1 200 10 roc_auc binary 0.883 10 0.00243 Preprocessor1_…
## 4 2 200 10 accuracy binary 0.838 10 0.00267 Preprocessor1_…
## 5 2 200 10 brier_class binary 0.125 10 0.000699 Preprocessor1_…
## 6 2 200 10 roc_auc binary 0.893 10 0.00178 Preprocessor1_…
## 7 3 200 10 accuracy binary 0.847 10 0.00234 Preprocessor1_…
## 8 3 200 10 brier_class binary 0.115 10 0.000828 Preprocessor1_…
## 9 3 200 10 roc_auc binary 0.900 10 0.00182 Preprocessor1_…
## 10 4 200 10 accuracy binary 0.849 10 0.00238 Preprocessor1_…
## # ℹ 365 more rows
## Warning in ggplot2::scale_x_continuous(trans = trans): log-10 transformation introduced infinite values.
## log-10 transformation introduced infinite values.
## # A tibble: 24,420 × 7
## .pred_class `.pred_>50K` `.pred_<=50K` id .row income .config
## <fct> <dbl> <dbl> <chr> <int> <fct> <chr>
## 1 >50K 0.691 0.309 Fold01 2 <=50K Preprocessor1_Mod…
## 2 >50K 0.713 0.287 Fold01 8 <=50K Preprocessor1_Mod…
## 3 >50K 0.846 0.154 Fold01 16 <=50K Preprocessor1_Mod…
## 4 <=50K 0.161 0.839 Fold01 26 <=50K Preprocessor1_Mod…
## 5 >50K 0.599 0.401 Fold01 28 <=50K Preprocessor1_Mod…
## 6 >50K 0.604 0.396 Fold01 73 <=50K Preprocessor1_Mod…
## 7 >50K 0.710 0.290 Fold01 82 <=50K Preprocessor1_Mod…
## 8 <=50K 0.469 0.531 Fold01 92 <=50K Preprocessor1_Mod…
## 9 >50K 0.571 0.429 Fold01 93 <=50K Preprocessor1_Mod…
## 10 >50K 0.809 0.191 Fold01 112 <=50K Preprocessor1_Mod…
## # ℹ 24,410 more rows
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 roc_auc binary 0.903
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 roc_auc binary 0.903
The show_best() function is part of the tidymodels suite and is used specifically within the tuning results context to extract the top-performing models according to a specified metric, which in this case is the ROC AUC.
# Getting best KNN Model
best_knn_income <- show_best(tune_res, metric = "roc_auc")
best_knn_income
## # A tibble: 5 × 7
## neighbors .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 10 roc_auc binary 0.835 10 0.00255 Preprocessor1_Model10
## 2 9 roc_auc binary 0.831 10 0.00256 Preprocessor1_Model09
## 3 8 roc_auc binary 0.828 10 0.00232 Preprocessor1_Model08
## 4 7 roc_auc binary 0.823 10 0.00272 Preprocessor1_Model07
## 5 6 roc_auc binary 0.817 10 0.00245 Preprocessor1_Model06
# Getting best log Model
best_lm_income <- show_best(fit_res_log, metric = "roc_auc")
best_lm_income
## # A tibble: 1 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 roc_auc binary 0.903 10 0.00194 Preprocessor1_Model1
# Getting best EN Model
best_en_income <- show_best(tune_res_en, metric = "roc_auc")
best_en_income
## # A tibble: 5 × 8
## penalty mixture .metric .estimator mean n std_err .config
## <dbl> <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 0 0.889 roc_auc binary 0.905 10 0.00154 Preprocessor1_Model081
## 2 0 0.667 roc_auc binary 0.905 10 0.00154 Preprocessor1_Model061
## 3 0 0.778 roc_auc binary 0.905 10 0.00154 Preprocessor1_Model071
## 4 0 1 roc_auc binary 0.905 10 0.00155 Preprocessor1_Model091
## 5 0 0.111 roc_auc binary 0.905 10 0.00153 Preprocessor1_Model011
# Getting best DT Model
best_dt_income <- show_best(tune_res_DT, metric = "roc_auc")
best_dt_income
## # A tibble: 5 × 7
## cost_complexity .metric .estimator mean n std_err .config
## <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 0.001 roc_auc binary 0.890 10 0.00323 Preprocessor1_Model01
## 2 0.00278 roc_auc binary 0.869 10 0.00340 Preprocessor1_Model02
## 3 0.00774 roc_auc binary 0.862 10 0.00344 Preprocessor1_Model03
## 4 0.0215 roc_auc binary 0.844 10 0.00306 Preprocessor1_Model04
## 5 0.0599 roc_auc binary 0.817 10 0.00508 Preprocessor1_Model05
# Getting best RF Model
best_rf_income <- show_best(tune_rf, metric = "roc_auc")
best_rf_income
## # A tibble: 5 × 9
## mtry trees min_n .metric .estimator mean n std_err .config
## <int> <int> <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 6 200 20 roc_auc binary 0.911 10 0.00164 Preprocessor1_Model1…
## 2 6 400 20 roc_auc binary 0.910 10 0.00152 Preprocessor1_Model1…
## 3 6 600 20 roc_auc binary 0.910 10 0.00161 Preprocessor1_Model1…
## 4 6 500 20 roc_auc binary 0.910 10 0.00157 Preprocessor1_Model1…
## 5 6 300 12 roc_auc binary 0.910 10 0.00157 Preprocessor1_Model0…
best_rf_model <- select_best(tune_rf, metric = "roc_auc")
The Random Forest model#105 has the highest recorded ROC AUC value at approximately 0.9105, suggesting it is the best performer among the models evaluated based on my data for discriminating between the binary classes effectively.
The best-performing configuration in terms of ROC AUC is when the Random Forest is configured with 200 trees and 20 as the minimum node size. This configuration not only achieved the highest mean ROC AUC but also maintains a low standard error, enhancing confidence in the model’s stability and reliability.
final_rf_model <- finalize_workflow(rf_wf,
best_rf_model)
final_rf_model <- fit(final_rf_model,
data = income_test)
final_rf_model %>% extract_fit_parsnip() %>%
vip() +
theme_minimal()
The three most important predictors was marital status with a civilian
spouse, age, and never being married.
Let’s look at the testing ROC AUC
# Ensure that predictions are generated correctly
predictions_rf <- predict(final_rf_model, income_test, type = "prob")
# Combine predictions with the true labels
predictions_rf <- bind_cols(income_test %>% select(income), predictions_rf)
# Check the structure to verify the combined dataframe
str(predictions_rf)
## tibble [8,141 × 3] (S3: tbl_df/tbl/data.frame)
## $ income : Factor w/ 2 levels ">50K","<=50K": 2 2 2 2 1 2 1 1 1 1 ...
## $ .pred_>50K : num [1:8141] 0.123 0.19 0.153 0.214 0.686 ...
## $ .pred_<=50K: num [1:8141] 0.877 0.81 0.847 0.786 0.314 ...
# Evaluate model performance using ROC AUC
roc_results <- roc_auc(predictions_rf, truth = income, `.pred_>50K`)
# Print ROC AUC results
print(roc_results)
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 roc_auc binary 0.937
roc_data <- roc_curve(predictions_rf, truth = income, `.pred_>50K`)
autoplot(roc_data) +
labs(title = "ROC Curve", x = "1 - Specificity", y = "Sensitivity") +
theme_minimal()
An ROC AUC of 0.9363215 is quite high, indicating that my model has a strong ability to distinguish between individuals earning more than $50,000 and those earning less than or equal to $50,000. In other words, there is a 93.63% chance that my model will correctly differentiate between a randomly chosen positive instance (income > 50K) and a randomly chosen negative instance (income <= 50K).
n this project, we aimed to predict whether an individual earns more than $50,000 per year using the Adult Census Income dataset. We explored a variety of machine learning models, including logistic regression, elastic net, k-nearest neighbors, decision trees, and random forests. The data was carefully preprocessed to handle missing values, normalize numerical features, and encode categorical variables. Class imbalance was addressed through upsampling.
We split the dataset into training, testing, and validation sets to ensure robust model evaluation. Hyperparameter tuning was conducted for each model using cross-validation and the ROC AUC metric to select the best performing configurations.
After extensive tuning and evaluation, the Random Forest model emerged as the best performer with a ROC AUC of approximately 0.9363. This high ROC AUC value indicates a strong ability of the model to distinguish between individuals earning more than $50,000 and those earning less. The most important predictors identified were marital status with a civilian spouse, age, and never being married.
The final Random Forest model was validated on the test set, achieving a ROC AUC of 0.9363, confirming its effectiveness and robustness. This high performance suggests that the model is well-suited for predicting income levels based on demographic and socioeconomic features from the census data.
In summary, this project successfully demonstrates the application of machine learning techniques to predict income levels, highlighting the importance of careful data preprocessing, model selection, and hyperparameter tuning in achieving high predictive accuracy. The findings underscore the significant impact of marital status, age, and marital status on income, providing valuable insights for socioeconomic analyses.