It’s widely known that one needs to have the right data before beginning an analysis. I’d like to suggest that one also needs to ensure the data is properly formatted before beginning the analysis. This will save lots of typing and debugging down the road!
library(tidyverse)
Sample data
Data is often provided in a cross-tabluated format. For example, suppose we receive this revenue by year dataset:
revenue <- data.frame(
client = c('A', 'B', 'C')
, fy17 = c(100, 400, 250)
, fy18 = c(200, 0, 250)
, fy19 = c(300, 500, 250)
)
print(revenue)
Summarizing the data as-is
Suppose we want to calculate average yearly revenue per client. Here’s one approach.
revenue %>%
# Calculate column means
select(fy17:fy19) %>%
colMeans() %>%
# Clean up the output
data.frame() %>%
rename(`mean(amt)` = names(.)[1])
But is this correct? Should B, who made no orders in fy18, be excluded from that year’s calculation?
revenue %>%
# Replace 0 with NA
na_if(0) %>%
# Calculate column means
select(fy17:fy19) %>%
colMeans(na.rm = TRUE) %>%
# Clean up the output
data.frame() %>%
rename(`mean(amt)` = names(.)[1])
Suppose we want to calculate additional metrics, like standard deviation. Now we can’t just use the nice colMeans() shortcut anymore. Without reshaping the data the simplest solution I could think of requires several extra steps:
cbind(
# Calculate column means
revenue %>%
na_if(0) %>%
select(fy17:fy19) %>%
colMeans(na.rm = TRUE)
# Calculate column standard deviations
, revenue %>%
na_if(0) %>%
select(fy17:fy19) %>%
apply(
MARGIN = 2
, FUN = sd
, na.rm = TRUE
)
) %>%
# Clean up the output
data.frame() %>%
rename(
`mean(amt)` = names(.)[1]
, `sd(amt)` = names(.)[2]
)
This approach is getting pretty hard to follow and takes a lot of typing for each additional field, and even includes duplicate code (though admittedly this could be handled with a temporary assignment).
A better approach
Getting the data into a “long” format, with one column each for client, year, and amount, makes analysis much easier.
reshaped <- revenue %>%
# Create new year and amt fields for the existing columns
gather('year', 'amt', fy17:fy19) %>%
# Filter out the 0 amount years at this stage to avoid having to na_if() later
filter(amt > 0) %>%
# we could even make the year column numeric (quickest way is convert appearances of fy into 20)
mutate(
year = year %>%
str_replace(pattern = 'fy', replacement = '20') %>%
as.numeric()
)
print(reshaped)
Now that the data is in a long format it’s very easy to calculate whatever summary statistics we want.
reshaped %>%
# Aggregate within year
group_by(year) %>%
# Now we can enter in whatever summary statistics we want with just a single line of code
summarise(
mean(amt)
, sd(amt)
, `sem(amt)` = sd(amt) / sqrt(length(amt))
)
This is much less code, and easier to read to boot!
Moral
Having the right data formatted properly makes the analysis much easier.
LS0tDQp0aXRsZTogIlJlc2hhcGUgeW91ciBkYXRhISINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6DQogICAgICBjb2xsYXBzZWQ6IEZBTFNFDQotLS0NCg0KSXQncyB3aWRlbHkga25vd24gdGhhdCBvbmUgbmVlZHMgdG8gaGF2ZSB0aGUgcmlnaHQgZGF0YSBiZWZvcmUgYmVnaW5uaW5nIGFuIGFuYWx5c2lzLiBJJ2QgbGlrZSB0byBzdWdnZXN0IHRoYXQgb25lIGFsc28gbmVlZHMgdG8gZW5zdXJlIHRoZSBkYXRhIGlzICpwcm9wZXJseSBmb3JtYXR0ZWQqIGJlZm9yZSBiZWdpbm5pbmcgdGhlIGFuYWx5c2lzLiBUaGlzIHdpbGwgc2F2ZSBsb3RzIG9mIHR5cGluZyBhbmQgZGVidWdnaW5nIGRvd24gdGhlIHJvYWQhDQoNCmBgYHtyIHNldHVwLCBtZXNzYWdlID0gRkFMU0UsIHdhcm5pbmcgPSBGQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KYGBgDQoNCiMgU2FtcGxlIGRhdGENCg0KRGF0YSBpcyBvZnRlbiBwcm92aWRlZCBpbiBhIGNyb3NzLXRhYmx1YXRlZCBmb3JtYXQuIEZvciBleGFtcGxlLCBzdXBwb3NlIHdlIHJlY2VpdmUgdGhpcyByZXZlbnVlIGJ5IHllYXIgZGF0YXNldDoNCg0KYGBge3J9DQpyZXZlbnVlIDwtIGRhdGEuZnJhbWUoDQogIGNsaWVudCA9IGMoJ0EnLCAnQicsICdDJykNCiAgLCBmeTE3ID0gYygxMDAsIDQwMCwgMjUwKQ0KICAsIGZ5MTggPSBjKDIwMCwgMCwgMjUwKQ0KICAsIGZ5MTkgPSBjKDMwMCwgNTAwLCAyNTApDQopDQoNCnByaW50KHJldmVudWUpDQpgYGANCg0KIyBTdW1tYXJpemluZyB0aGUgZGF0YSBhcy1pcw0KDQpTdXBwb3NlIHdlIHdhbnQgdG8gY2FsY3VsYXRlIGF2ZXJhZ2UgeWVhcmx5IHJldmVudWUgcGVyIGNsaWVudC4gSGVyZSdzIG9uZSBhcHByb2FjaC4NCg0KYGBge3J9DQpyZXZlbnVlICU+JQ0KICAjIENhbGN1bGF0ZSBjb2x1bW4gbWVhbnMNCiAgc2VsZWN0KGZ5MTc6ZnkxOSkgJT4lDQogIGNvbE1lYW5zKCkgJT4lDQogICMgQ2xlYW4gdXAgdGhlIG91dHB1dA0KICBkYXRhLmZyYW1lKCkgJT4lDQogIHJlbmFtZShgbWVhbihhbXQpYCA9IG5hbWVzKC4pWzFdKQ0KYGBgDQoNCkJ1dCBpcyB0aGlzIGNvcnJlY3Q/IFNob3VsZCBCLCB3aG8gbWFkZSBubyBvcmRlcnMgaW4gZnkxOCwgYmUgZXhjbHVkZWQgZnJvbSB0aGF0IHllYXIncyBjYWxjdWxhdGlvbj8NCg0KYGBge3J9DQpyZXZlbnVlICU+JQ0KICAjIFJlcGxhY2UgMCB3aXRoIE5BDQogIG5hX2lmKDApICU+JQ0KICAjIENhbGN1bGF0ZSBjb2x1bW4gbWVhbnMNCiAgc2VsZWN0KGZ5MTc6ZnkxOSkgJT4lDQogIGNvbE1lYW5zKG5hLnJtID0gVFJVRSkgJT4lDQogICMgQ2xlYW4gdXAgdGhlIG91dHB1dA0KICBkYXRhLmZyYW1lKCkgJT4lDQogIHJlbmFtZShgbWVhbihhbXQpYCA9IG5hbWVzKC4pWzFdKQ0KYGBgDQoNClN1cHBvc2Ugd2Ugd2FudCB0byBjYWxjdWxhdGUgYWRkaXRpb25hbCBtZXRyaWNzLCBsaWtlIHN0YW5kYXJkIGRldmlhdGlvbi4gTm93IHdlIGNhbid0IGp1c3QgdXNlIHRoZSBuaWNlIGBjb2xNZWFucygpYCBzaG9ydGN1dCBhbnltb3JlLiBXaXRob3V0IHJlc2hhcGluZyB0aGUgZGF0YSB0aGUgc2ltcGxlc3Qgc29sdXRpb24gSSBjb3VsZCB0aGluayBvZiByZXF1aXJlcyBzZXZlcmFsIGV4dHJhIHN0ZXBzOg0KDQpgYGB7cn0NCmNiaW5kKA0KICAjIENhbGN1bGF0ZSBjb2x1bW4gbWVhbnMNCiAgcmV2ZW51ZSAlPiUNCiAgICBuYV9pZigwKSAlPiUNCiAgICBzZWxlY3QoZnkxNzpmeTE5KSAlPiUNCiAgICBjb2xNZWFucyhuYS5ybSA9IFRSVUUpDQogICMgQ2FsY3VsYXRlIGNvbHVtbiBzdGFuZGFyZCBkZXZpYXRpb25zDQogICwgcmV2ZW51ZSAlPiUNCiAgICBuYV9pZigwKSAlPiUNCiAgICBzZWxlY3QoZnkxNzpmeTE5KSAlPiUNCiAgICBhcHBseSgNCiAgICAgIE1BUkdJTiA9IDINCiAgICAgICwgRlVOID0gc2QNCiAgICAgICwgbmEucm0gPSBUUlVFDQogICAgKQ0KKSAlPiUNCiAgIyBDbGVhbiB1cCB0aGUgb3V0cHV0DQogIGRhdGEuZnJhbWUoKSAlPiUNCiAgcmVuYW1lKA0KICAgIGBtZWFuKGFtdClgID0gbmFtZXMoLilbMV0NCiAgICAsIGBzZChhbXQpYCA9IG5hbWVzKC4pWzJdDQogICkNCmBgYA0KDQpUaGlzIGFwcHJvYWNoIGlzIGdldHRpbmcgcHJldHR5IGhhcmQgdG8gZm9sbG93IGFuZCB0YWtlcyBhIGxvdCBvZiB0eXBpbmcgZm9yIGVhY2ggYWRkaXRpb25hbCBmaWVsZCwgYW5kIGV2ZW4gaW5jbHVkZXMgZHVwbGljYXRlIGNvZGUgKHRob3VnaCBhZG1pdHRlZGx5IHRoaXMgY291bGQgYmUgaGFuZGxlZCB3aXRoIGEgdGVtcG9yYXJ5IGFzc2lnbm1lbnQpLg0KDQojIEEgYmV0dGVyIGFwcHJvYWNoDQoNCkdldHRpbmcgdGhlIGRhdGEgaW50byBhICJsb25nIiBmb3JtYXQsIHdpdGggb25lIGNvbHVtbiBlYWNoIGZvciBjbGllbnQsIHllYXIsIGFuZCBhbW91bnQsIG1ha2VzIGFuYWx5c2lzIG11Y2ggZWFzaWVyLg0KDQpgYGB7cn0NCnJlc2hhcGVkIDwtIHJldmVudWUgJT4lDQogICMgQ3JlYXRlIG5ldyB5ZWFyIGFuZCBhbXQgZmllbGRzIGZvciB0aGUgZXhpc3RpbmcgY29sdW1ucw0KICBnYXRoZXIoJ3llYXInLCAnYW10JywgZnkxNzpmeTE5KSAlPiUNCiAgIyBGaWx0ZXIgb3V0IHRoZSAwIGFtb3VudCB5ZWFycyBhdCB0aGlzIHN0YWdlIHRvIGF2b2lkIGhhdmluZyB0byBuYV9pZigpIGxhdGVyDQogIGZpbHRlcihhbXQgPiAwKSAlPiUNCiAgIyB3ZSBjb3VsZCBldmVuIG1ha2UgdGhlIHllYXIgY29sdW1uIG51bWVyaWMgKHF1aWNrZXN0IHdheSBpcyBjb252ZXJ0IGFwcGVhcmFuY2VzIG9mIGZ5IGludG8gMjApDQogIG11dGF0ZSgNCiAgICB5ZWFyID0geWVhciAlPiUNCiAgICAgIHN0cl9yZXBsYWNlKHBhdHRlcm4gPSAnZnknLCByZXBsYWNlbWVudCA9ICcyMCcpICU+JQ0KICAgICAgYXMubnVtZXJpYygpDQogICkNCg0KcHJpbnQocmVzaGFwZWQpDQpgYGANCg0KTm93IHRoYXQgdGhlIGRhdGEgaXMgaW4gYSBsb25nIGZvcm1hdCBpdCdzIHZlcnkgZWFzeSB0byBjYWxjdWxhdGUgd2hhdGV2ZXIgc3VtbWFyeSBzdGF0aXN0aWNzIHdlIHdhbnQuDQoNCmBgYHtyfQ0KcmVzaGFwZWQgJT4lDQogICMgQWdncmVnYXRlIHdpdGhpbiB5ZWFyDQogIGdyb3VwX2J5KHllYXIpICU+JQ0KICAjIE5vdyB3ZSBjYW4gZW50ZXIgaW4gd2hhdGV2ZXIgc3VtbWFyeSBzdGF0aXN0aWNzIHdlIHdhbnQgd2l0aCBqdXN0IGEgc2luZ2xlIGxpbmUgb2YgY29kZQ0KICBzdW1tYXJpc2UoDQogICAgbWVhbihhbXQpDQogICAgLCBzZChhbXQpDQogICAgLCBgc2VtKGFtdClgID0gc2QoYW10KSAvIHNxcnQobGVuZ3RoKGFtdCkpDQogICkNCmBgYA0KDQpUaGlzIGlzIG11Y2ggbGVzcyBjb2RlLCBhbmQgZWFzaWVyIHRvIHJlYWQgdG8gYm9vdCENCg0KIyBNb3JhbA0KDQpIYXZpbmcgdGhlIHJpZ2h0IGRhdGEgKmZvcm1hdHRlZCBwcm9wZXJseSogbWFrZXMgdGhlIGFuYWx5c2lzIG11Y2ggZWFzaWVyLg==