What do I mean when I say let's treat the data? I learned the term from the authors of the vtreat
package, Nina Zumel, and John Mount. You can read their excellent paper on the subject at this link: https://arxiv.org/pdf/1611.09477.pdf.
The definition they provide is: processor or conditioner that prepares real-world data for predictive modeling in a statistically sound manner. In treating your data, you'll rid yourself of many of the data preparation headaches discussed earlier. The example with our current dataset will provide an excellent introduction into the benefits of this method and how you can tailor it to your needs. I kind of like to think that treating your data is a smarter version of one-hot encoding.
The package offers three different functions to treat data, but I only use one and that is designTreatmentsZ()
, which treats the features without regard to an outcome or response. The functions designTreatmentsC()
and designTreatmentsN()
functions build dataframes based on categorical and numeric outcomes respectively. Those functions provide a method to prune features in a univariate fashion. I'll provide other ways of conducting feature selection, so that's why I use that specific function. I encourage you to experiment on your own.
The function we use in the following will produce an object that you can apply to training, validation, testing, and even production data. In later chapters, we'll focus on training and testing, but here let's treat the entire data without considerations of any splits for simplicity. There're a number of arguments in the function you can change, but the defaults are usually sufficient. We'll specify the input data, the feature names to include, and minFraction
, which is defined by the package as the optional minimum frequency a categorical level must have to be converted into an indicator column. I've chosen 5% and the minimum frequency. In real-world data, I've seen this number altered many times to find the right level of occurrence:
my_treatment <- vtreat::designTreatmentsZ( dframe = gettysburg_fltrd, varlist = colnames(gettysburg_fltrd), minFraction = 0.05 )
We now have an object with a stored treatment plan. Now we just use the prepare()
function to apply that treatment to a dataframe or tibble, and it'll give us a treated dataframe:
gettysburg_treated <- vtreat::prepare(my_treatment, gettysburg_fltrd) dim(gettysburg_treated)
The output of the preceding code is as follows:
[1] 587 54
We now have 54 features. Let's take a look at their names:
colnames(gettysburg_treated)
The abbreviated output of the preceding code is as follows:
[1] "type_catP" "state_catP" "regiment_or_battery_catP" [4] "brigade_catP" "division_catP" "corps_catP"
As you explore the names, you'll notice that we have features ending in catP
, clean
, and isBAD
and others with _lev_x_
in them. Let's cover each in detail. As for catP
features, the function creates a feature that's the frequency for the categorical level in that observation. What does that mean? Let's see a table for type_catP
:
table(gettysburg_treated$type_catP)
The output of the preceding code is as follows:
0.080068143100 0.21976149914 0.70017035775 47 129 411
This tells us that 47
rows are of category level x (in this case, Cavalry), and this is 8% of the total observations. As such, 22% are Artillery and 70% Infantry. This can be helpful in further exploring your data and to help adjust the minimum frequency in your category levels. I've heard it discussed that these values could help in the creation of a distance or similarity matrix.
The next is clean
. These are our numeric features that have had missing values imputed, which is the feature mean, and outliers winsorized or collared if you specified the argument in the prepare()
function. We didn't, so only missing values were imputed.
Note
Here's an interesting blog post of the merits of winsorizing from SAS: https://blogs.sas.com/content/iml/2017/02/08/winsorization-good-bad-and-ugly.html.
Speaking of missing values, this brings us to isBAD
. This feature is the 1 for missing and 0 if not missing we talked about where I manually coded it.
Finally, lev_x
is the dummy feature coding for a specific categorical level. If you go through the levels that were hot-encoded for states
, you'll find features for Georgia, New York, North Carolina, Pennsylvania, US (this is US Regular Army units), and Virginia.
My preference is to remove the catP
features and remove the clean
from the feature name, and change isBAD
to isNA
. This a simple task with these lines of code:
gettysburg_treated <- gettysburg_treated %>% dplyr::select(-dplyr::contains('_catP')) colnames(gettysburg_treated) <- sub('_clean', "", colnames(gettysburg_treated)) colnames(gettysburg_treated) <- sub('_isBAD', "_isNA", colnames(gettysburg_treated))
Are we ready to start building learning algorithms? Well, not quite yet. In the next section, we'll deal with highly correlated and linearly related features.
For this task, we return to our old friend the caret
package. We'll start by creating a correlation matrix, using the Spearman Rank method, then apply the findCorrelation()
function for all correlations above 0.9
:
df_corr <- cor(gettysburg_treated, method = "spearman") high_corr <- caret::findCorrelation(df_corr, cutoff = 0.9)
Note
Why Spearman versus Pearson correlation? Spearman is free from any distribution assumptions and is robust enough for any task at hand: http://www.statisticssolutions.com/correlation-pearson-kendall-spearman/.
The high_corr
object is a list of integers that correspond to feature column numbers. Let's dig deeper into this:
high_corr
The output of the preceding code is as follows:
[1] 9 4 22 43 3 5
The column indices refer to the following feature names:
colnames(gettysburg_treated)[c(9, 4, 22, 43, 3, 5)]
The output of the preceding code is as follows:
[1] "total_casualties" "wounded" "type_lev_x_Artillery" [4] "army_lev_x_Confederate" "killed_isNA" "wounded_isNA"
We saw the features that're highly correlated to some other feature. For instance, army_lev_x_Confederate
is perfectly and negatively correlation with army_lev_x_Union
. After all, you can only two armies here, and Colonel Fremantle of the British Coldstream Guards was merely an observer. To delete these features, just filter your dataframe by the list we created:
gettysburg_noHighCorr <- gettysburg_treated[, -high_corr]
There you go, they're now gone. But wait! That seems a little too clinical, and maybe we should apply our judgment or the judgment of an SME to the problem? As before, let's create a tibble for further exploration:
df_corr <- data.frame(df_corr) df_corr$feature1 <- row.names(df_corr) gettysburg_corr <- tidyr::gather(data = df_corr, key = "feature2", value = "correlation", -feature1) gettysburg_corr <- gettysburg_corr %>% dplyr::filter(feature1 != feature2)
What just happened? First of all, the correlation matrix was turned into a dataframe. Then, the row names became the values for the first feature. Using tidyr
, the code created the second feature and placed the appropriate value with an observation, and we cleaned it up to get unique pairs. This screenshot shows the results. You can see that the Confederate and Union armies have a perfect negative correlation:
You can see that it would be safe to dedupe on correlation as we did earlier. I like to save this to a spreadsheet and work with SMEs to understand what features we can drop or combine and so on.
After handling the correlations, I recommend exploring and removing as needed linear combinations. Dealing with these combinations is a similar methodology to high correlations:
linear_combos <- caret::findLinearCombos(gettysburg_noHighCorr) linear_combos
The output of the preceding code is as follows:
$`linearCombos` $`linearCombos`[[1]] [1] 16 7 8 9 10 11 12 13 14 15 $remove [1] 16
The output tells us that feature column 16
is linearly related to those others, and we can solve the problem by removing it. What are these feature names? Let's have a look:
colnames(gettysburg_noHighCorr)[c(16, 7, 8, 9, 10, 11, 12, 13, 14, 15)]
The output of the preceding code is as follows:
[1] "total_guns" "X3inch_rifles" "X10lb_parrots" "X12lb_howitzers" "X12lb_napoleons" [6] "X6lb_howitzers" "X24lb_howitzers" "X20lb_parrots" "X12lb_whitworths" "X14lb_rifles"
Removing the feature on the number of "total_guns"
will solve the problem. This makes total sense since it's the number of guns in an artillery battery. Most batteries, especially in the Union, had only one type of gun. Even with multiple linear combinations, it's an easy task with this bit of code to get rid of the necessary features:
linear_remove <- colnames(gettysburg_noHighCorr[16]) df <- gettysburg_noHighCorr[, !(colnames(gettysburg_noHighCorr) %in% linear_remove)] dim(df)
The output of the preceding code is as follows:
[1] 587 39
There you have it, a nice clean dataframe of 587
observations and 39
features. Now depending on the modeling, you may have to scale this data or perform other transformations, but this data, in this format, makes all of that easier. Regardless of your prior knowledge or interest of one of the most important battles in history, and the bloodiest on American soil, you've developed a workable understanding of the Order of Battle, and the casualties at the regimental or battery level. Start treating your data, not next week or next month, but right now!
Note
If you desire, you can learn more about the battle here: https://www.battlefields.org/learn/civil-war/battles/gettysburg.