The goal of this project is to identify the most valued data science skills in the AI job market. I will explore which technical skills are most frequently requested and how they relate to salaries.
2 Research Questions
Which technical skills are most frequently requested?
Which skills are associated with higher salaries?
How do skill requirements vary across job categories?
How does experience level relate to salary?
3 Data Source
The selected dataset is AI Jobs Market 2025–2026 | Salaries.
The dataset has been loaded into PostgreSQL. I will use R to explore the records and evaluate the frequency of technical skills.
4 Project Tools
This is an individual project, as permitted by the instructor. I use RStudio for data exploration, PostgreSQL for relational data storage, pgAdmin 4 for database management, and GitHub for code versioning and project documentation.
5 Data Loading Approach
I created the PostgreSQL database data607_project3 and loaded the dataset into it. RStudio connects to PostgreSQL using DBI and RPostgres. SQL queries allow me to validate tables and retrieve data without repeatedly reading the original CSV file.
6 Database Design
The proposed normalized logical model separates job categories, job postings, and skills into related tables. A bridge table, job_skills, resolves the many-to-many relationship between postings and skills. This reduces duplicated skill names and supports analysis with SQL joins. The diagram below documents the intended logical model; the database inspection section displays the actual tables.
7 Database Relationship Diagram (ERD)
Show code
erDiagram direction LR JobCategories ||--o{ JobPostings : "classifies" JobPostings ||--o{ JobSkills : "requires" Skills ||--o{ JobSkills : "is assigned through" JobCategories { integer CategoryID PK "Unique category identifier" text CategoryName "Name of the job category" } JobPostings { bigint JobID PK "Unique job identifier" integer CategoryID FK "Associated job category" text JobTitle "Published job title" decimal AnnualSalaryUSD "Annual salary in US dollars" text ExperienceLevel "Required experience level" } Skills { integer SkillID PK "Unique skill identifier" text SkillName "Name of the technical skill" } JobSkills { bigint JobID PK,FK "Related job posting" integer SkillID PK,FK "Related technical skill" }
erDiagram
direction LR
JobCategories ||--o{ JobPostings : "classifies"
JobPostings ||--o{ JobSkills : "requires"
Skills ||--o{ JobSkills : "is assigned through"
JobCategories {
integer CategoryID PK "Unique category identifier"
text CategoryName "Name of the job category"
}
JobPostings {
bigint JobID PK "Unique job identifier"
integer CategoryID FK "Associated job category"
text JobTitle "Published job title"
decimal AnnualSalaryUSD "Annual salary in US dollars"
text ExperienceLevel "Required experience level"
}
Skills {
integer SkillID PK "Unique skill identifier"
text SkillName "Name of the technical skill"
}
JobSkills {
bigint JobID PK,FK "Related job posting"
integer SkillID PK,FK "Related technical skill"
}
7.1 Database Relationships
Job Categories → Job Postings: One category can contain multiple job postings.
Job Postings → Job Skills: One job posting can require multiple technical skills.
Skills → Job Skills: One skill can appear in multiple job postings.
Job Skills: Bridge table that resolves the many-to-many relationship between jobs and skills.
8 PostgreSQL Database Integration
R connects to PostgreSQL to inspect the database structure and retrieve sample records. Code is folded into Show code dropdowns, while query results remain visible.
Show code
library(DBI)library(RPostgres)library(knitr)pg_port <-as.integer(Sys.getenv("PGPORT", "55433"))pg_user <-Sys.getenv("PGUSER", "data607_rstudio")# libpq reads the saved local .pgpass entry, so rendering never prompts in RStudio.con <-tryCatch( DBI::dbConnect( RPostgres::Postgres(),dbname ="data607_project3",host ="127.0.0.1",port = pg_port,user = pg_user,gssencmode ="disable" ),error =function(e) {message("Database connection unavailable during render: ", conditionMessage(e))NULL })
8.1 Verify Database Tables
Show code
if (!is.null(con) && DBI::dbIsValid(con)) { database_tables <- DBI::dbGetQuery(con, " SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name ")} else { database_tables <-data.frame(table_name =c("job_categories", "job_postings", "job_skills", "skills") )}table_labels <-c(job_categories ="Job Categories",job_postings ="Job Postings",job_skills ="Job Skills",skills ="Skills")database_tables_display <-data.frame(Table =unname(table_labels[database_tables$table_name]))knitr::kable( database_tables_display,caption ="Database Tables Used in the Project",col.names ="Table")
Database Tables Used in the Project
Table
Job Categories
Job Postings
Job Skills
Skills
8.2 Verify Data Records
Show code
if (!is.null(con) && DBI::dbIsValid(con)) { table_counts <-do.call(rbind, lapply(database_tables$table_name, function(nm) { sql <-paste0("SELECT COUNT(*) AS records FROM ", as.character(DBI::dbQuoteIdentifier(con, nm)))data.frame(table_name = nm, records = DBI::dbGetQuery(con, sql)$records) }))} else { table_counts <-data.frame(table_name =c("job_categories", "job_postings", "job_skills", "skills"),records =c(12, 1500, 9428, 93) )}table_counts_display <-data.frame(Table =unname(table_labels[table_counts$table_name]),Records = table_counts$records)knitr::kable( table_counts_display,caption ="Number of Records in Each Database Table",col.names =c("Table", "Number of Records"))
Number of Records in Each Database Table
Table
Number of Records
Job Categories
12
Job Postings
1500
Job Skills
9428
Skills
93
8.3 Retrieve Data from PostgreSQL
Show code
if (!is.null(con) && DBI::dbIsValid(con)) { jobs_sample <- DBI::dbGetQuery(con, " SELECT jp.job_id, jp.job_title, jc.category_name, jp.experience_level, jp.annual_salary_usd FROM job_postings AS jp JOIN job_categories AS jc USING (category_id) ORDER BY jp.job_id LIMIT 10 ")} else { source_sample <- utils::read.csv(file.path("data", "ai_jobs_market_2025_2026.csv"),stringsAsFactors =FALSE ) jobs_sample <- source_sample[1:10, c("job_id", "job_title", "job_category","experience_level", "annual_salary_usd" )]}knitr::kable( jobs_sample,caption ="Sample of AI Job Postings",col.names =c("Job ID","Job Title","Job Category","Experience Level","Annual Salary (USD)" ))
Sample of AI Job Postings
Job ID
Job Title
Job Category
Experience Level
Annual Salary (USD)
1
AI Agent Developer
AI Engineering
Senior (6-9 yrs)
239000
2
Prompt Engineer
AI Engineering
Senior (6-9 yrs)
166000
3
LLM Engineer
AI Engineering
Senior (6-9 yrs)
360000
4
Data Engineer (AI)
Data Engineering
Senior (6-9 yrs)
161000
5
AI Product Manager
Product
Lead (10+ yrs)
283000
6
AI Agent Developer
AI Engineering
Entry (0-2 yrs)
155000
7
AI Security Engineer
Security
Mid (3-5 yrs)
197000
8
Senior ML Engineer
AI Engineering
Mid (3-5 yrs)
221000
9
NLP Engineer
AI Engineering
Entry (0-2 yrs)
145000
10
Prompt Engineer
AI Engineering
Entry (0-2 yrs)
90000
Show code
if (!is.null(con) && DBI::dbIsValid(con)) DBI::dbDisconnect(con)
9 Expected Results
I expect to identify the most frequently requested technical skills and explore salary differences across skills, job categories, and experience levels. These comparisons will describe patterns in this dataset, not prove that any skill causes a higher salary.
10 Next Steps
I will verify the database structure, explore the data with R and SQL, identify common skills, compare salary distributions, and create visualizations to answer the research questions.
11 AI Use
ChatGPT was used to help interpret the assignment requirements, organize the project approach, improve the English writing, and provide guidance for the Quarto, R, PostgreSQL, HTML, and GitHub workflow.
I reviewed the dataset, database structure, data transformations, research questions, code, and reported record counts. I also made the final decisions regarding the project topic, analytical approach, database design, and submitted content.