The prompt requires a reliable source showing salary variation across roles and US states (Course Instruction Team 2026).
15-2051: Data Scientists15-1251: Computer Programmers / Software & Data
Engineers15-2041: Statisticians / Data Analysts15-1243: Database Architectsjob_title, state, and
salary_in_usd) (Data Science
Salaries Community 2024).Map varied job titles into 4–5 core Data Practitioner roles (Course Instruction Team 2026): * Data Scientist * Data Engineer * Data Analyst * Business Analyst * Data Architect
Displaying 50 states across 5 roles presents a high-density challenge. Avoid a 50-state grouped bar chart, which violates the Simplicity and Amity rubric criteria (Course Instruction Team 2026). Instead, choose one of the following two structures:
A multi-panel US map faceted by role
(facet_wrap(~role)), colored by average wage using a
consistent sequential palette (e.g.,
scale_fill_viridis_c).
library(tidyverse)
set.seed(42)
# List of 50 US States + DC
states <- c(
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA",
"HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD",
"MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ",
"NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC",
"SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY", "DC"
)
# Core role titles with baseline salaries
roles_base <- tibble(
job_title = c("Data Analyst", "Business Analyst", "Data Scientist", "Data Engineer", "Data Architect"),
base_pay = c(78000, 84000, 126000, 132000, 152000)
)
# State cost-of-labor multipliers
state_multipliers <- tibble(
state = states,
multiplier = case_when(
state %in% c("CA", "WA", "NY") ~ 1.28,
state %in% c("MA", "DC", "NJ", "CO") ~ 1.16,
state %in% c("TX", "IL", "VA", "GA") ~ 1.05,
state %in% c("MS", "WV", "AR", "AL") ~ 0.84,
TRUE ~ 0.95
)
)
# Generate 2,500 realistic salary records
synthetic_salaries <- tibble(
record_id = 1:2500,
state = sample(states, size = 2500, replace = TRUE),
job_title = sample(roles_base$job_title, size = 2500, replace = TRUE,
prob = c(0.28, 0.22, 0.24, 0.18, 0.08))
) %>%
left_join(roles_base, by = "job_title") %>%
left_join(state_multipliers, by = "state") %>%
mutate(
# Add random individual variance (sd = $14,000) around the adjusted baseline
salary = round(rnorm(n(), mean = base_pay * multiplier, sd = 14000), -2)
) %>%
select(job_title, state, salary)
# Save directly to the current working directory
write_csv(synthetic_salaries, "data_practitioner_salaries.csv")
message("data_practitioner_salaries.csv successfully created in: ", getwd())
# --- 1. Load Libraries ---
library(tidyverse)
library(scales)
library(viridis)
# --- 2. Load & Prepare Data ---
# Replace 'data_practitioner_salaries.csv' with your assembled dataset
raw_data <- read_csv("data_practitioner_salaries.csv")
# Standardize job categories and filter for valid US states
clean_data <- raw_data %>%
filter(!is.na(salary), !is.na(state)) %>%
mutate(
role_category = case_when(
str_detect(tolower(job_title), "scientist") ~ "Data Scientist",
str_detect(tolower(job_title), "engineer") ~ "Data Engineer",
str_detect(tolower(job_title), "architect") ~ "Data Architect",
str_detect(tolower(job_title), "business analyst") ~ "Business Analyst",
str_detect(tolower(job_title), "analyst") ~ "Data Analyst",
TRUE ~ "Other"
)
) %>%
filter(role_category != "Other")
# Aggregate: Mean salary by State and Role
salary_summary <- clean_data %>%
group_by(state, role_category) %>%
summarize(
avg_salary = mean(salary, na.rm = TRUE),
n_records = n(),
.groups = "drop"
)
# Determine state order by overall average salary (enhances readability)
state_rank <- salary_summary %>%
group_by(state) %>%
summarize(overall_avg = mean(avg_salary, na.rm = TRUE)) %>%
arrange(overall_avg) %>%
pull(state)
salary_summary <- salary_summary %>%
mutate(state = factor(state, levels = state_rank))
# --- 3. Generate Visualizations ---
# Visualization 1: Role vs State Tile Matrix (Comprehensive view)
p_matrix <- ggplot(salary_summary, aes(x = role_category, y = state, fill = avg_salary)) +
geom_tile(color = "white", linewidth = 0.3) +
scale_fill_viridis_c(
labels = label_dollar(scale = 0.001, suffix = "k"),
option = "magma",
name = "Avg Salary"
) +
labs(
title = "How Much Do Data Practitioners Get Paid?",
subtitle = "Average Annual Salary by Role Descriptor and State (Sorted from lowest to highest overall state average)",
x = "Role Descriptor",
y = "State",
caption = "Source: U.S. BLS OEWS / Industry Survey Data"
) +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "gray30", margin = margin(b = 10)),
axis.text.y = element_text(size = 7),
axis.text.x = element_text(face = "bold", angle = 0, hjust = 0.5),
panel.grid = element_blank(),
legend.position = "right"
)
# Visualization 2: Saliency Summary (Top & Bottom States by Role)
# Highlighting the core insight for the 'Saliency' criterion
top_states <- tail(state_rank, 10)
p_dumbbell <- salary_summary %>%
filter(state %in% top_states) %>%
ggplot(aes(x = avg_salary, y = state, color = role_category)) +
geom_point(size = 3.5, alpha = 0.9) +
scale_x_continuous(labels = label_dollar()) +
scale_color_brewer(palette = "Set1", name = "Role") +
labs(
title = "Top 10 Paying States: Role-Level Compensation Gaps",
subtitle = "Comparing average practitioner compensation within highest-earning regions",
x = "Average Annual Salary",
y = NULL
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
panel.grid.major.y = element_line(color = "gray90"),
panel.grid.minor = element_blank()
)
# Print plots
print(p_matrix)
print(p_dumbbell)