# load libraries
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.1 ✔ tibble 3.2.1
## ✔ lubridate 1.9.3 ✔ tidyr 1.3.1
## ✔ purrr 1.0.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readxl)
library(rio)
library(janitor)
##
## Attaching package: 'janitor'
##
## The following objects are masked from 'package:stats':
##
## chisq.test, fisher.test
library(ggplot2)
library(scales)
##
## Attaching package: 'scales'
##
## The following object is masked from 'package:purrr':
##
## discard
##
## The following object is masked from 'package:readr':
##
## col_factor
# loading data
asvab_data <- read_excel("asvab_data_2020-2024.xlsx")
# clean heading names
asvab_data <- asvab_data|>
clean_names()
# clean data
asvab_clean <- asvab_data %>%
filter(if_any(everything(), ~ !is.na(.))) %>%
mutate(across(where(is.character), str_trim)) %>%
mutate(across(where(is.character), ~ na_if(., ""))) %>%
mutate(across(where(is.character), ~ na_if(., "N/A"))) %>%
mutate(across(where(is.character), ~ na_if(., "Unknown")))
race_counts <- asvab_clean |>
separate_rows(race, sep = ",\\s*") |> # splits by comma and optional space
count(race, sort = TRUE)
print(race_counts)
## # A tibble: 8 × 2
## race n
## <chr> <int>
## 1 White 197129
## 2 <NA> 186121
## 3 Black or African American 95875
## 4 Asian 25544
## 5 Native Hawaiian or Other Pacific Islander 8517
## 6 American Indian/Alaska Native 8097
## 7 Identification Pending 999
## 8 Declined to Respond 28
### Gender distribution
gender_count <- asvab_clean |>
count(sex, sort = TRUE) |>
print()
## # A tibble: 3 × 2
## sex n
## <chr> <int>
## 1 Male 332846
## 2 Female 156518
## 3 <NA> 11988
### Graphic of gender distribution
ggplot(gender_count, aes(x = sex, y = n, fill = sex)) +
geom_col() +
scale_fill_manual(values = c("Male" = "#3182bd", "Female" = "#e6550d")) +
scale_y_continuous(labels = scales::label_comma()) +
labs(title = "ASVAB Gender Distribution",
x = "Gender",
y = "Count") +
theme_minimal()
There are 332,846 males and 156518 females, meaning that about 32% percent is represented by females and about 56% percent is represented by males.
age_count <- asvab_clean |>
count(age, sort=TRUE) |>
print()
## # A tibble: 246 × 2
## age n
## <dbl> <int>
## 1 17 124298
## 2 18 88828
## 3 16 68047
## 4 19 45851
## 5 20 32091
## 6 21 24185
## 7 22 20958
## 8 23 17481
## 9 24 13744
## 10 25 11209
## # ℹ 236 more rows
### Producing age ranges
asvab_clean <- asvab_clean |>
mutate(age_range = cut(
age,
breaks = c(17, 24, 34, 44, 54, 64, Inf), # customize as needed
labels = c("16–24", "25–34", "35–44", "45–54", "55–64", "65+"),
right = TRUE, # include the upper bound (e.g., 24 is part of 18–24)
include.lowest = TRUE
))
asvab_clean |>
count(age_range)
## # A tibble: 7 × 2
## age_range n
## <fct> <int>
## 1 16–24 367436
## 2 25–34 53281
## 3 35–44 9229
## 4 45–54 70
## 5 55–64 10
## 6 65+ 232
## 7 <NA> 71094
asvab_clean$age_range <- factor(asvab_clean$age_range,
levels = c("16–24", "25–34", "35–44", "45–54", "55–64", "65+", "NA"))
# Plot
ggplot(asvab_clean, aes(x = age_range)) +
geom_bar(fill = "lavender") +
scale_y_continuous(labels = scales::label_comma()) +
scale_x_discrete(drop = FALSE) +
labs(title = "Age Range Distribution",
x = "Age Range",
y = "Count") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
latest_schooling_by_race <- asvab_clean |>
separate_rows(race, sep = ",\\s*") |>
group_by(race, latest_school_type) |>
summarise(count = n(), .groups = "drop") |>
arrange(desc(count))
ggplot(latest_schooling_by_race, aes(x = race, y = count, fill = latest_school_type)) +
geom_col(position = "dodge") +
labs(title = "Latest Schooling Type by Race",
x = "Race",
y = "Count",
fill = "School Type") +
scale_y_continuous(labels = scales::label_comma()) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
latest_schooling_by_gender <- asvab_clean |>
group_by(sex, latest_school_type) |>
summarise(count = n(), .groups = "drop")
print(latest_schooling_by_gender)
## # A tibble: 17 × 3
## sex latest_school_type count
## <chr> <chr> <int>
## 1 Female College, Post Graduate, Internship, Residency, FellowShip 13588
## 2 Female Military Education 12
## 3 Female Military Training 2
## 4 Female Other 200
## 5 Female Secondary or High School 78239
## 6 Female Vocational/Technical College 361
## 7 Female <NA> 64116
## 8 Male College, Post Graduate, Internship, Residency, FellowShip 23755
## 9 Male Military Education 33
## 10 Male Military Training 2
## 11 Male Other 376
## 12 Male Secondary or High School 192494
## 13 Male Vocational/Technical College 963
## 14 Male <NA> 115223
## 15 <NA> College, Post Graduate, Internship, Residency, FellowShip 5
## 16 <NA> Secondary or High School 28
## 17 <NA> <NA> 11955
ggplot(latest_schooling_by_gender, aes(x = sex, y = count, fill = latest_school_type)) +
geom_col(position = "dodge") +
labs(title = "Latest Schooling Type by Gender",
x = "Gender",
y = "Count",
fill = "School Type") +
scale_y_continuous(labels = scales::label_comma()) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
The U.S. Air Force does not have a single, universal minimum composite score for any composite score. Instead, it has different minimum scores for various career fields. So, I will be using the min scores for different career fields that fall under each composite score section.
Breakdown of U.S. Air Force Composite Scores:
G General: Verbal Expression (WK + PC) + Arithmetic Reasoning (AR)
M Mechanical: Mechanical Comprehension (MC) + Auto & Shop (AS)
A Administrative: Verbal Expression (WK + PC)
E Electronics: EI + AR + MK + GS
G (General) - composite is used for general aptitude jobs that require verbal and math skills.
M (Mechanical) - focuses on mechanical and technical knowledge.
A (Administrative) - is for clerical and admin roles, based on verbal skills only.
E (Electronics) - combines several technical and science-based subtests for electronics and cyber-related roles.
Questions: ** Which gender and which race is more associated with each composite score section? What races and what genders qualify for what type of careers? **
general_scores <- asvab_clean |>
select(asvab_ar, asvab_pc, asvab_wk, race, sex)
general_scores <- general_scores |>
mutate(total_score = (asvab_ar + asvab_pc + asvab_wk))
print(general_scores)
## # A tibble: 501,352 × 6
## asvab_ar asvab_pc asvab_wk race sex total_score
## <dbl> <dbl> <dbl> <chr> <chr> <dbl>
## 1 37 50 55 Black or African American Male 142
## 2 40 40 34 Black or African American Male 114
## 3 49 53 56 White Male 158
## 4 43 54 50 Black or African American Female 147
## 5 49 56 72 Black or African American Male 177
## 6 55 55 61 White Male 171
## 7 57 53 64 Black or African American Female 174
## 8 55 56 54 Black or African American Female 165
## 9 50 56 58 White Male 164
## 10 57 60 61 White Male 178
## # ℹ 501,342 more rows
# Filter for individuals with score >= 55
eligible_general <- general_scores |>
filter(total_score >= 55)
general_grouped_eligible <- eligible_general |>
group_by(race, sex) |>
summarize(
count_eligible = as.numeric(n())) |>
arrange(desc(count_eligible))
## `summarise()` has grouped output by 'race'. You can override using the
## `.groups` argument.
write.csv(general_grouped_eligible, "general_grouped_eligible.csv", row.names = FALSE)
# Filter for Black and White applicants
bw_general_data <- general_grouped_eligible |>
filter(race %in% c("Black or African American", "White"))
# Plot for Black and White
ggplot(bw_general_data, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "General Eligibility Counts by Race (Black & White Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 12, face = "bold")
)
general_other_races <- general_grouped_eligible |>
filter(!race %in% c("Black or African American", "White") & !is.na(race))
g_plot <- ggplot(general_other_races, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "General Eligibility Counts by Race (Other Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
coord_flip()
# Save the plot
ggsave("general_eligibility_counts.png", plot = g_plot, width = 12, height = 7, dpi = 300, bg = "white")
# Plot: Eligible counts by race and sex with angled x-axis
g_plot <- ggplot(general_grouped_eligible, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "General Eligibility Counts (Score ≥ 55) by Race and Sex",
x = "Race",
y = "Number Eligible",
fill = "Sex",
capition = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1)
)
ggsave("general_eligibility_counts.png", plot = g_plot, width = 12, height = 7, dpi = 300, bg = "white")
mechanical_scores <- asvab_clean |>
select(asvab_as, asvab_mc,asvab_ar, race, sex)
mechanical_scores <- mechanical_scores |>
mutate(total_score = (asvab_as + asvab_mc + asvab_ar))
print(mechanical_scores)
## # A tibble: 501,352 × 6
## asvab_as asvab_mc asvab_ar race sex total_score
## <dbl> <dbl> <dbl> <chr> <chr> <dbl>
## 1 48 39 37 Black or African American Male 124
## 2 36 24 40 Black or African American Male 100
## 3 65 56 49 White Male 170
## 4 43 35 43 Black or African American Female 121
## 5 43 39 49 Black or African American Male 131
## 6 70 68 55 White Male 193
## 7 45 47 57 Black or African American Female 149
## 8 46 50 55 Black or African American Female 151
## 9 50 60 50 White Male 160
## 10 55 60 57 White Male 172
## # ℹ 501,342 more rows
# Filter for individuals with score >= 47
eligible_mechanical <- mechanical_scores |>
filter(total_score >= 47)
mechanical_grouped_eligible <- eligible_mechanical |>
group_by(race, sex) |>
summarize(
count_eligible = as.numeric(n())) |>
arrange(desc(count_eligible))
## `summarise()` has grouped output by 'race'. You can override using the
## `.groups` argument.
write.csv(mechanical_grouped_eligible, "mechanical_grouped_eligible.csv", row.names = FALSE)
# Filter for Black and White applicants
bw_mechanical_data <- mechanical_grouped_eligible |>
filter(race %in% c("Black or African American", "White"))
# Plot for Black and White
ggplot(bw_mechanical_data, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "Mechanical Eligibility Counts by Race (Black & White Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 12, face = "bold")
)
mechanical_other_races <- mechanical_grouped_eligible |>
filter(!race %in% c("Black or African American", "White") & !is.na(race))
m_plot <- ggplot(mechanical_other_races, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "Mechanical Eligibility Counts by Race (Other Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
coord_flip()
# Save the plot
ggsave("mechanical_eligibility_counts.png", plot = m_plot, width = 12, height = 7, dpi = 300, bg = "white")
admin_scores <- asvab_clean |>
select(asvab_wk, asvab_pc, race, sex)
admin_scores <- admin_scores |>
mutate(total_score = (asvab_wk + asvab_pc))
print(admin_scores)
## # A tibble: 501,352 × 5
## asvab_wk asvab_pc race sex total_score
## <dbl> <dbl> <chr> <chr> <dbl>
## 1 55 50 Black or African American Male 105
## 2 34 40 Black or African American Male 74
## 3 56 53 White Male 109
## 4 50 54 Black or African American Female 104
## 5 72 56 Black or African American Male 128
## 6 61 55 White Male 116
## 7 64 53 Black or African American Female 117
## 8 54 56 Black or African American Female 110
## 9 58 56 White Male 114
## 10 61 60 White Male 121
## # ℹ 501,342 more rows
# Filter for individuals with score >= 55
eligible_admin <- admin_scores |>
filter(total_score >= 55)
admin_grouped_eligible <- eligible_admin |>
group_by(race, sex) |>
summarize(
count_eligible = as.numeric(n()),
) |>
arrange(desc(count_eligible))
## `summarise()` has grouped output by 'race'. You can override using the
## `.groups` argument.
write.csv(admin_grouped_eligible, "admin_grouped_eligible.csv", row.names = FALSE)
# Filter for Black and White applicants
bw_admin_data <- admin_grouped_eligible |>
filter(race %in% c("Black or African American", "White"))
# Plot for Black and White
ggplot(bw_admin_data, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "Admin Eligibility Counts by Race (Black & White Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 12, face = "bold")
)
admin_other_races <- admin_grouped_eligible |>
filter(!race %in% c("Black or African American", "White") & !is.na(race))
a_plot <- ggplot(admin_other_races, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "Admin Eligibility Counts by Race (Other Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
coord_flip()
# Save the plot
ggsave("admin_eligibility_counts.png", plot = a_plot, width = 12, height = 7, dpi = 300, bg = "white")
electronics_scores <- asvab_clean |>
select(asvab_ei, asvab_ar, asvab_mk, asvab_gs, race, sex)
electronics_scores <- electronics_scores |>
mutate(total_score = (asvab_ar + asvab_mk + asvab_ei + asvab_gs))
print(electronics_scores)
## # A tibble: 501,352 × 7
## asvab_ei asvab_ar asvab_mk asvab_gs race sex total_score
## <dbl> <dbl> <dbl> <dbl> <chr> <chr> <dbl>
## 1 54 37 45 51 Black or African Ameri… Male 187
## 2 38 40 46 39 Black or African Ameri… Male 163
## 3 61 49 42 55 White Male 207
## 4 38 43 45 50 Black or African Ameri… Fema… 176
## 5 55 49 43 55 Black or African Ameri… Male 202
## 6 66 55 53 60 White Male 234
## 7 47 57 55 50 Black or African Ameri… Fema… 209
## 8 47 55 58 53 Black or African Ameri… Fema… 213
## 9 53 50 56 56 White Male 215
## 10 61 57 58 57 White Male 233
## # ℹ 501,342 more rows
# Filter for individuals with score >= 60
eligible_electronics <- electronics_scores |>
filter(total_score >= 60)
electronics_grouped_eligible <- eligible_electronics |>
group_by(race, sex) |>
summarize(
count_eligible = as.numeric(n()),
.groups = "drop"
) |>
arrange(desc(count_eligible))
write.csv(electronics_grouped_eligible, "electronics_grouped_eligible.csv", row.names = FALSE)
bw_electronics_data <- electronics_grouped_eligible |>
filter(race %in% c("Black or African American", "White"))
# Plot for Black and White
ggplot(bw_electronics_data, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "Electronics Eligibility Counts by Race (Black & White Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 12, face = "bold")
)
electronics_other_races <- electronics_grouped_eligible |>
filter(!race %in% c("Black or African American", "White") & !is.na(race))
e_plot <- ggplot(electronics_other_races, aes(x = race, y = count_eligible, fill = sex)) +
geom_bar(stat = "identity", position = "dodge") +
labs(
title = "Electronics Eligibility Counts by Race (Other Applicants)",
x = "Race",
y = "Number Eligible",
fill = "Sex",
caption = "Graphic by Tiasia Saunders"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
coord_flip()
ggsave("electronics_eligibility_counts.png", plot = e_plot, width = 12, height = 7, dpi = 300, bg = "white")
The four graphics for the most part show similar eligibility across all races with almost the same count of people per race that qualify for the positions. This makes sense considering the minimum eligibility score is similar ( 55 for General and Admin and 47 for Mechanical and 60 for Electronics)
The four graphics show similar eligibility patterns across racial groups, with relatively comparable numbers of people qualifying for each Air Force occupational area. This is likely because, despite differences in the specific ASVAB sub-tests used for each composite score, the cutoff thresholds (55 for General and Admin, 47 for Mechanical, and 60 for Electronics) are fairly similar and target a comparable range of high-scoring individuals.
In other words, while each occupational area uses a different combination of sub-test scores, the overall difficulty and percentile requirement are close enough that they select similarly sized pools of eligible applicants across race and gender.
Different ASVAB sub-test sums → but similar high scores + similar cutoff thresholds → similar eligibility counts across composites.
If you can think of a better way to highlight the disparities in each composite scoring (in the process of researching how to do that) please let me know. But, for the most part for each composite scoring White Americans have high eligibility count, followed by Black Americans, then Asian. `