Overview: Restructuring & Combining Data in Business Analytics

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:

  1. Restructuring (Reshaping) Data (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.
  2. Combining Data Files (dplyr):
    • Stacking rows (bind_rows): Appending monthly or regional extracts with identical columns (SQL UNION ALL / SAS SET).
    • Relational Joins (left_join, inner_join, anti_join): Merging tables side-by-side using a primary/foreign key (SQL JOIN / SAS MERGE BY).

Part 1: Restructuring Wide Spreadsheets into Tidy Long Data (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

Why Wide Data Fails for Plotting

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

Plotting the Restructured Data (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)


Part 2: Stacking Separate Regional Data Files (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

Part 3: Combining Relational Tables & Restructuring to Wide (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)

1. Combining 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.

2. Restructuring Long Summary Back to a Wide Executive Cross-Tab (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

3. Visualizing the Combined Data (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)


Part 4: Data Auditing with Filtering Joins (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?

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

๐ŸŒ How to Publish to RPubs & Submit Your URL on Canvas

  1. At the top of this file (Line 3), replace [Type Your Name Here] with your name.
  2. Click ๐Ÿงถ Knit, then click the blue Publish button \(\rightarrow\) RPubs.
  3. Submit your 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