In corporate analytics (BAN 3083), data is almost never stored in a single table shaped the way your chart or statistical model requires. You must master two core operations:
tidyr):
pivot_longer() (SAS
PROC TRANSPOSE Wide-to-Long): Turns column headers
(like Q1, Q2, Q3,
Q4) into rows so you can plot time series in
ggplot2 and run regression models.pivot_wider() (SAS
PROC TRANSPOSE Long-to-Wide): Turns rows into
cross-tabulated columns for executive presentation tables.dplyr):
bind_rows): Appending
monthly or regional extracts with identical columns (SQL
UNION ALL / SAS SET).left_join,
inner_join, anti_join): Merging
tables side-by-side using a primary/foreign key (SQL JOIN /
SAS MERGE BY).pivot_longer)Accounting and Finance teams love โWideโ spreadsheets where each quarter or year is a separate column. Letโs look at a quarterly revenue spreadsheet for Chinookโs 4 global divisions:
library(tidyverse)
library(DBI)
library(RSQLite)
library(gt)
library(scales)
library(patchwork)
wide_finance <- tibble(
Division = c("North America", "Europe", "Latin America", "Asia Pacific"),
Q1_2025 = c(125000, 98000, 64000, 110000),
Q2_2025 = c(138000, 105000, 71000, 129000),
Q3_2025 = c(149000, 118000, 68000, 145000),
Q4_2025 = c(182000, 142000, 89000, 176000)
)
# Display the wide accounting table
wide_finance |>
gt() |>
tab_header(title = "Wide Format: Finance Department Spreadsheet") |>
fmt_currency(columns = starts_with("Q"), currency = "USD", decimals = 0)
| Wide Format: Finance Department Spreadsheet | ||||
| Division | Q1_2025 | Q2_2025 | Q3_2025 | Q4_2025 |
|---|---|---|---|---|
| North America | $125,000 | $138,000 | $149,000 | $182,000 |
| Europe | $98,000 | $105,000 | $118,000 | $142,000 |
| Latin America | $64,000 | $71,000 | $68,000 | $89,000 |
| Asia Pacific | $110,000 | $129,000 | $145,000 | $176,000 |
Try plotting Quarter on the x-axis and
Revenue on the y-axis from wide_financeโyou
canโt, because there is no Quarter column or
Revenue column!
Letโs use pivot_longer() to restructure
the 4 quarter columns into two tidy columns: Quarter and
Revenue:
long_finance <- wide_finance |>
pivot_longer(
cols = starts_with("Q"),
names_to = "Quarter",
values_to = "Revenue"
)
long_finance
## # A tibble: 16 ร 3
## Division Quarter Revenue
## <chr> <chr> <dbl>
## 1 North America Q1_2025 125000
## 2 North America Q2_2025 138000
## 3 North America Q3_2025 149000
## 4 North America Q4_2025 182000
## 5 Europe Q1_2025 98000
## 6 Europe Q2_2025 105000
## 7 Europe Q3_2025 118000
## 8 Europe Q4_2025 142000
## 9 Latin America Q1_2025 64000
## 10 Latin America Q2_2025 71000
## 11 Latin America Q3_2025 68000
## 12 Latin America Q4_2025 89000
## 13 Asia Pacific Q1_2025 110000
## 14 Asia Pacific Q2_2025 129000
## 15 Asia Pacific Q3_2025 145000
## 16 Asia Pacific Q4_2025 176000
ggplot2)Now that our data is in Tidy (Long) Format, we can plot a multi-division quarterly growth chart in seconds:
ggplot(long_finance, aes(x = Quarter, y = Revenue, color = Division, group = Division)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
scale_y_continuous(labels = label_dollar()) +
labs(
title = "Quarterly Revenue Trajectory by Division (2025)",
subtitle = "Restructured from Wide Spreadsheet to Long Tidy Format via pivot_longer()",
x = "Fiscal Quarter",
y = "Revenue (USD)",
color = "Division"
) +
theme_minimal(base_size = 12)
bind_rows)Suppose two regional offices send separate monthly invoice extracts.
When two tables have the exact same columns, we stack
them vertically using bind_rows() (with
.id = "Source_Batch" to track which file each row came
from):
north_extract <- tibble(
Invoice_ID = c(9001, 9002),
Customer = c("Acme Corp", "Beta LLC"),
Amount = c(450.00, 820.50)
)
south_extract <- tibble(
Invoice_ID = c(9003, 9004),
Customer = c("Delta Inc", "Omega Co"),
Amount = c(310.00, 1190.00)
)
combined_extracts <- bind_rows(
"North_Office" = north_extract,
"South_Office" = south_extract,
.id = "Regional_File"
)
combined_extracts
## # A tibble: 4 ร 4
## Regional_File Invoice_ID Customer Amount
## <chr> <dbl> <chr> <dbl>
## 1 North_Office 9001 Acme Corp 450
## 2 North_Office 9002 Beta LLC 820.
## 3 South_Office 9003 Delta Inc 310
## 4 South_Office 9004 Omega Co 1190
left_join + pivot_wider)Now letโs connect to the Chinook database and pull
three separate tables into R (Customer,
Invoice, and Employee) so we can combine them
using dplyr relational joins!
con <- dbConnect(RSQLite::SQLite(), "data/chinook.sqlite")
customers <- dbReadTable(con, "Customer") |> as_tibble()
invoices <- dbReadTable(con, "Invoice") |> as_tibble()
employees <- dbReadTable(con, "Employee") |> as_tibble()
dbDisconnect(con)
Customer, Invoice, and
Employee (inner_join &
left_join)Each customer is assigned to a SupportRepId (which
matches EmployeeId in the Employee table), and
each invoice belongs to a CustomerId. Letโs combine all
three tables to see how much revenue each Sales Support Representative
generated by customer region:
rep_performance <- invoices |>
inner_join(customers, by = "CustomerId") |>
left_join(employees, by = c("SupportRepId" = "EmployeeId")) |>
mutate(
Rep_Name = paste(FirstName.y, LastName.y),
Market = case_when(
BillingCountry %in% c("USA", "Canada") ~ "North America",
BillingCountry %in% c("Brazil", "Argentina", "Chile") ~ "Latin America",
TRUE ~ "Europe & Rest of World"
)
) |>
group_by(Rep_Name, Market) |>
summarize(Total_Sales = sum(Total, na.rm = TRUE), .groups = "drop")
rep_performance
## # A tibble: 9 ร 3
## Rep_Name Market Total_Sales
## <chr> <chr> <dbl>
## 1 Jane Peacock Europe & Rest of World 445.
## 2 Jane Peacock Latin America 77.2
## 3 Jane Peacock North America 311.
## 4 Margaret Park Europe & Rest of World 385.
## 5 Margaret Park Latin America 113.
## 6 Margaret Park North America 277.
## 7 Steve Johnson Europe & Rest of World 397.
## 8 Steve Johnson Latin America 84.2
## 9 Steve Johnson North America 239.
pivot_wider)While ggplot2 loves long data, human executives love
reading wide cross-tabulations! Letโs use
pivot_wider() to give each Market its own
column and display it with gt():
rep_performance |>
pivot_wider(
names_from = Market,
values_from = Total_Sales,
values_fill = 0
) |>
mutate(Grand_Total = `North America` + `Latin America` + `Europe & Rest of World`) |>
arrange(desc(Grand_Total)) |>
gt() |>
tab_header(
title = "Sales Support Rep Revenue by Global Market",
subtitle = "Combined via inner_join() + left_join(), Restructured via pivot_wider()"
) |>
fmt_currency(columns = where(is.numeric), currency = "USD")
| Sales Support Rep Revenue by Global Market | ||||
| Combined via inner_join() + left_join(), Restructured via pivot_wider() | ||||
| Rep_Name | Europe & Rest of World | Latin America | North America | Grand_Total |
|---|---|---|---|---|
| Jane Peacock | $444.84 | $77.24 | $310.96 | $833.04 |
| Margaret Park | $385.20 | $112.86 | $277.34 | $775.40 |
| Steve Johnson | $397.20 | $84.24 | $238.72 | $720.16 |
ggplot2)Letโs plot our rep_performance dataset as a grouped
horizontal bar chart:
ggplot(rep_performance, aes(x = reorder(Rep_Name, Total_Sales, sum), y = Total_Sales, fill = Market)) +
geom_col(position = "stack", width = 0.7) +
coord_flip() +
scale_y_continuous(labels = label_dollar()) +
scale_fill_brewer(palette = "Dark2") +
labs(
title = "Total Chinook Revenue by Support Representative & Market",
x = "Support Representative",
y = "Cumulative Invoice Sales (USD)",
fill = "Customer Market"
) +
theme_minimal(base_size = 12)
anti_join)One of the most useful tools in BAN 3083 is
anti_join(x, y): it returns all rows in table
x that have NO match in table
y.
Which Chinook employees are not assigned as Support Reps for any customer? Andrew Adams, Nancy Edwards, Michael Mitchell, Robert King and Laura Callahan
employees |>
anti_join(customers, by = c("EmployeeId" = "SupportRepId")) |>
select(EmployeeId, FirstName, LastName, Title, Department = Title) |>
gt() |>
tab_header(
title = "Employees With Zero Assigned Customers",
subtitle = "Identified via anti_join(employees, customers)"
)
| Employees With Zero Assigned Customers | |||
| Identified via anti_join(employees, customers) | |||
| EmployeeId | FirstName | LastName | Department |
|---|---|---|---|
| 1 | Andrew | Adams | General Manager |
| 2 | Nancy | Edwards | Sales Manager |
| 6 | Michael | Mitchell | IT Manager |
| 7 | Robert | King | IT Staff |
| 8 | Laura | Callahan | IT Staff |
[Type Your Name Here] with your name.https://rpubs.com/... URL on
Canvas!rsconnect::rpubsUpload(
title = "BAN 3083: Week 3 Restructuring & Combining Data",
contentFile = "Week03_Restructuring_and_Combining.html",
originalDoc = "Week03_Restructuring_and_Combining.Rmd"
)$continueUrl