This data is synthetic. Every person, employer, course, and salary in these files was generated by a simulation. Nothing here is real UMBC data, and nothing you conclude from it is true of actual UMBC students or graduates. See Synthetic data disclaimer before you present anything.
Every student asks themselves some version of the same question: "Where will my degree actually take me?"
It's a hard question because the honest answer is buried in thousands of individual stories — the student who worked thirty hours a week and still finished, the one whose elective turned into a career, the transfer student nobody thought to track. Universities have that data. Students almost never get to see it.
Here, you do. Five thousand synthetic people across Computer Science and Information Systems: 1,800 currently enrolled and 3,200 graduates from 2015 through 2026. Every course they took and what they got in it. Every club, internship, research post, and certification. Where they went to work, what they did there, and what happened over the decade after.
Somewhere in that is something a student would genuinely want to know, and probably several things. Finding one — and making it something a person could actually use — is the work.
Teams have taken data like this in a lot of directions. A few of the many that would work here: a career-pathway graph you can click through, role by role. An advisor that calculates what a degree actually returns. A tool matching what students did outside class to where they ended up. A four-year planner that knows which courses are prerequisites for what. A skill-gap matcher that reads a transcript and names what's missing. A map of where graduates go and what it costs to live there.
That list is nowhere near exhaustive, and none of it is an assignment — it's six of the possibilities, offered so the blank page is less blank. We have guesses about what's in this data. We'd rather be surprised.
Six CSVs. Every one of them joins on campus_id, and every one of them is useful on its own.
| File | Rows | What it holds |
|---|---|---|
students_current.csv |
1,800 | Students enrolled in Fall 2026 — majors, tracks, GPAs, credits, costs so far |
alumni.csv |
3,200 | Graduates, their degree, what they paid, and their first outcome |
transcripts.csv |
~140,000 | Every course attempt by every person, with grades and requirement category |
employment_history.csv |
~6,000 | Job spells over time — employer, title, region, salary, skills, tenure |
student_experience.csv |
~20,000 | Internships, clubs, research, hackathons, certifications, campus jobs |
course_catalog.csv |
72 | Course reference — prerequisites, skill tags, difficulty, terms offered |
Each CSV has a matching .md beside it with the full field list, sample rows, and gotchas. Start with data/README.md.
Total size is about 22 MB. Every file opens in Excel or Google Sheets. A dataframe library loads the whole set in about a second, a SQL import takes a few seconds, and transcripts.csv parses in browser JavaScript in a second or two — so a client-side-only tool is viable.
data/sample/ holds a 10% cut — same six filenames, same columns, ~500 people, about 2.7 MB. It is referentially complete, so every join works. Prototype against it and change one path when you're ready.
course_catalog.csv
│ course_id
▼
students_current.csv ──┐ transcripts.csv
(1,800) │ (~140,000)
├───────┤ campus_id
alumni.csv ──────┘ │
(3,200) ├───────┤
│ │
employment_history.csv│ student_experience.csv
(~6,000) │ (~20,000)
alumni only │ students and alumni
The one rule worth memorizing: a campus_id appears in students_current.csv or alumni.csv, never both. Current students have no employment history.
One connection that isn't in the diagram: skill_tags in course_catalog.csv and role_skill_tags in employment_history.csv are drawn from the same vocabulary. What a course teaches and what a job asks for are directly comparable, with no mapping to invent.
Pick whichever matches how you think. Each is a complete, runnable answer to a different question — and each is shown in one language only to keep it short, not because that's the language to use. See Ways into the data for the same operations in SQL.
What does the curriculum actually look like? (one file)
import pandas as pd
catalog = pd.read_csv("data/course_catalog.csv")
# Which courses gate the most other courses?
prereqs = catalog[catalog["prerequisite_ids"] != "Not Applicable"]
gatekeepers = prereqs["prerequisite_ids"].str.split("|").explode().value_counts()
print(gatekeepers.head(10))What do students actually do outside class? (one file, SQL)
SELECT experience_type, outcome, COUNT(*) AS n
FROM student_experience
GROUP BY experience_type, outcome
ORDER BY experience_type, n DESC;Where do people end up, and how did they get there? (one file)
jobs = pd.read_csv("data/employment_history.csv")
paths = jobs.sort_values(["campus_id", "start_date"])
paths["next_role"] = paths.groupby("campus_id")["job_title"].shift(-1)
print(paths.dropna(subset=["next_role"])
.groupby(["job_title", "next_role"]).size()
.sort_values(ascending=False).head(15))What separates one graduating class from another? (one file, SQL)
SELECT graduation_year, first_destination, COUNT(*) AS n
FROM alumni
GROUP BY graduation_year, first_destination
ORDER BY graduation_year;Does what you studied match what the job wanted? (two files, shared vocabulary)
transcripts = pd.read_csv("data/transcripts.csv")
course_skills = catalog.set_index("course_id")["skill_tags"].str.split("|")
passed = transcripts[~transcripts["grade"].isin(["W", "F", "IP"])]
my_skills = set(course_skills.loc[
passed[passed["campus_id"] == "CID-641452"]["course_id"]].explode().dropna())
target = jobs[(jobs["job_family"] == "Data & Analytics") &
(jobs["seniority_level"] == "Entry")]
wanted = target["role_skill_tags"].str.split("|").explode().value_counts()
print("have:", [s for s in wanted.index[:10] if s in my_skills])
print("gap :", [s for s in wanted.index[:10] if s not in my_skills])Worked versions of these live in examples/: quickstart.py (Python standard library only — no pip install, no pandas), explore_python.py (pandas), and explore_sql.sql (SQLite and DuckDB). Three ways in; none of them the required one.
Nothing here prescribes a stack. These files are plain CSV — load them with whatever you already know, or whatever your team wants an excuse to learn. What follows shows the shape of the common operations, in two languages, so you can translate into a third.
Python and SQL below are illustrations, not requirements. JavaScript, R, Julia, Excel, Tableau, DuckDB, a Jupyter notebook, a Neo4j import, or a spreadsheet and an afternoon are all fine. Several of these files are small enough to open by hand and just read.
Getting the data into SQL (SQLite, DuckDB, Postgres)
DuckDB reads CSVs directly with no import step:
SELECT * FROM 'data/alumni.csv' LIMIT 5;SQLite imports them in one command each:
sqlite3 campus.db
.mode csv
.import data/alumni.csv alumni
.import data/transcripts.csv transcripts
.import data/employment_history.csv employment_history
.import data/student_experience.csv student_experience
.import data/students_current.csv students_current
.import data/course_catalog.csv course_catalogNote that SQLite imports every column as text, so cast when you need arithmetic. DuckDB and Postgres infer types, but the columns carrying Not Applicable still arrive as text — see the join recipes below.
Everything hangs off campus_id.
merged = transcripts.merge(alumni, on="campus_id", how="inner")SELECT a.campus_id, a.major, t.course_id, t.grade
FROM alumni a
JOIN transcripts t ON a.campus_id = t.campus_id;Remember that a campus_id is in students_current or alumni, never both. To treat everyone as one population, union the two person files on the columns they share.
The sentinel makes these columns text. Filter, then cast.
employed = alumni[alumni["first_job_annual_salary_usd"] != "Not Applicable"].copy()
employed["salary"] = employed["first_job_annual_salary_usd"].astype(int)SELECT graduation_year,
AVG(CAST(first_job_annual_salary_usd AS INTEGER)) AS mean_salary
FROM alumni
WHERE first_job_annual_salary_usd <> 'Not Applicable'
GROUP BY graduation_year;W (withdrawn) and IP (in progress) have no grade points and don't count toward GPA.
graded = transcripts[~transcripts["grade"].isin(["W", "IP"])].copy()
graded["grade_points"] = graded["grade_points"].astype(float)SELECT campus_id,
ROUND(SUM(CAST(grade_points AS REAL) * credits_attempted)
/ SUM(credits_attempted), 2) AS gpa
FROM transcripts
WHERE grade NOT IN ('W', 'IP')
GROUP BY campus_id;That second query recomputes cumulative_gpa from scratch. It matches the stored column, which is a decent way to confirm you've understood the file.
skill_tags, role_skill_tags, prerequisite_ids, required_for_majors, and typical_terms_offered all hold lists.
catalog["skill_tags"].str.split("|").explode().value_counts()-- DuckDB
SELECT unnest(str_split(skill_tags, '|')) AS skill, COUNT(*)
FROM 'data/course_catalog.csv' GROUP BY 1 ORDER BY 2 DESC;const tags = row.skill_tags.split("|");Splitting a delimited string is genuinely awkward in some SQL dialects. If yours is one of them, exploding these columns into a lookup table once, up front, will save you from fighting it repeatedly.
Sort by person and date, then pair each row with the one after it. This is the whole career-pathway graph.
paths = jobs.sort_values(["campus_id", "start_date"])
paths["next_role"] = paths.groupby("campus_id")["job_title"].shift(-1)
edges = (paths.dropna(subset=["next_role"])
.groupby(["job_title", "next_role"]).size())WITH steps AS (
SELECT campus_id, job_title,
LEAD(job_title) OVER (PARTITION BY campus_id ORDER BY start_date) AS next_role
FROM employment_history
)
SELECT job_title, next_role, COUNT(*) AS weight
FROM steps WHERE next_role IS NOT NULL
GROUP BY job_title, next_role ORDER BY weight DESC;counts = experience.pivot_table(index="campus_id", columns="experience_type",
aggfunc="size", fill_value=0)SELECT campus_id,
SUM(experience_type IN ('Internship', 'Co-op')) AS internships,
SUM(experience_type = 'Certification') AS certifications,
SUM(experience_type = 'Student Organization') AS orgs
FROM student_experience GROUP BY campus_id;peers = alumni[(alumni["major"] == me["major"]) &
(alumni["track"] == me["track"]) &
(alumni["internship_count"] == me["internship_count"])]SELECT * FROM alumni
WHERE major = ? AND track = ? AND internship_count = ?;Which attributes should count as "similar" is a design decision, and an interesting one — there's no right answer in the data.
-
Not Applicableis a literal string, not a blank. Where a field doesn't apply — an alum who isn't employed, a certification'shours_per_week— the cell readsNot Applicable, so those columns load as text. Filter before casting.(The spelled-out form is deliberate. Many tools — pandas among them — convert a literal
N/Ato a null automatically, which would silently break exactly that filter.) -
The one genuine blank is
end_date. It is empty if and only ifis_currentis true — the only empty cell anywhere in the dataset. Booleans are writtenTRUE/FALSE; some loaders (pandas, DuckDB) turn those into real booleans while others (SQLite, plain CSV readers) leave them as text, so check which you have before comparing. -
Money is nominal dollars of the year it applies to. A 2015 salary is in 2015 dollars, and the tuition schedule below moves too. Comparing across years without adjusting compares different things.
-
No Responseis about 15% of alumni and is a real category infirst_destination, not a null. It means the outcome is unknown — not that the person was unemployed. Decide explicitly whether to include those rows, and say which you chose. -
Transfer students bring credits that aren't in
transcripts.csv. Their transcript starts at UMBC, sototal_credits_earnedexceeds the sum of their transcript rows. Intended, not corruption. -
About 400 current students have no GPA yet. They started in Fall 2026, so all their coursework is still
IPandcumulative_gpareadsNot Applicable. Don't read the sentinel as a zero.
Rather than tell you what to look for, here is the raw material. What you do with it is the project.
| People | 2 majors, 9 tracks, 4 class levels, transfer and first-time entrants, in-state and out-of-state, first-generation status, part-time and full-time, work hours while enrolled |
| Academics | 72 courses across 18 subjects, a real prerequisite graph, 119 skill tags, difficulty ratings, terms offered, 4 requirement categories, 55 terms of history, grades including withdrawals and repeats |
| Involvement | 10 experience types, 104 organizations, 18 distinct outcomes, role levels from Member to President, paid and unpaid, duration in terms |
| Credentials | 27 certifications from 17 issuing bodies |
| Careers | 58 employers, 16 industries, 5 company sizes, 105 job titles, 8 job families, 6 seniority levels, 8 kinds of job change, clearance requirements, role skill requirements |
| Geography | 16 regions with cost-of-living indices, remote flags |
| Money | Net cost, loans, tuition paid to date, salaries by spell, an 11-year tuition schedule |
| Time | 12 graduation years, job spells from 2015 to 2026, term-by-term academic records |
Starting points, not a menu — the best submissions usually aren't on lists like this. Organized by the kind of work you want to do; difficulty is noted where it's not obvious.
Nothing here is required, and you are not limited to analysis. Bring in outside data, build a full application, design something a student would open on their phone the night before registration. The dataset is raw material, not an assignment.
Something a student, advisor, or department would actually open.
- Career-pathway explorer — an interactive graph where nodes are roles and edges are real transitions people made. Click a role, see what came before and after it.
- Degree-ROI calculator — cost and aid in, outcomes out, with the assumptions visible and adjustable.
- Four-year planner — pick courses against real prerequisites and terms-offered constraints; the planner tells you if your plan is impossible.
- "Students like me" engine — take a current student's record, find graduates with similar trajectories, show what happened to them.
- Skill-gap matcher — compare a transcript against what target roles ask for, and name the courses that close the gap. (
skill_tagsandrole_skill_tagsshare a vocabulary, so this needs no mapping of your own.) - Advisor dashboard — flag students off-pace to graduate, or heading into a term that looks like a wall.
- AI advisor — grounded in these rows, citing the records behind every claim. (Advanced: the hard part is honesty, not fluency.)
No app required. A clear finding, well argued, is a complete project.
- What does the curriculum actually look like — which courses gate the most others, where the hard walls are, how the two majors differ in shape
- How graduating classes differ from one another, on whatever dimension you think matters
- What campus involvement looks like across 10 activity types and 104 organizations
- How cost and financing differ across kinds of student
- What the data says about time — when people take things, how long they stay, what happens to people who take longer
- Geographic patterns: where graduates go, what it costs to be there
- A salary model with honest uncertainty, not a single point estimate
- A retention-risk model built from academic trajectory
- A course-recommendation engine trained on where similar students ended up
- A pathway simulator: change your courses or involvement, watch the outcome distribution shift
The generator isn't public, but the data is CC0 — bring in whatever you like.
- Real job postings from a public API, matched against these career paths
- Actual salary benchmarks or cost-of-living data to test the synthetic figures against
- A public skill taxonomy layered over
skill_tags - Real UMBC catalog data alongside the fictitious one
- Geographic, census, or industry-growth data keyed on region
Cite your sources, and keep clear which conclusions come from which dataset.
Most teams will reach for outcomes and salaries. Directions that tend to go unexplored: the shape of the curriculum itself; what involvement does for students who aren't job-hunting; what happens to part-time and transfer students specifically; and anything that treats the current 1,800 students as the audience rather than the 3,200 alumni as the subject.
Dimensions, not a point rubric:
- Innovation — did you ask a question we hadn't thought of, or answer a familiar one in an unfamiliar way?
- Impact — would a real student, advisor, or department use this? Who, and when?
- Technical excellence — is the analysis sound, the code defensible, the claim supported by the rows behind it?
- Communication — one clear finding beats six charts. Show the data behind any relationship you claim.
- Intellectual honesty — "these groups differ, but the sample is small and we can't rule out X" is stronger than false confidence. Knowing where your analysis stops is a skill.
We're not impressed by model complexity for its own sake, or by dashboards with twelve charts and no argument.
net_cost_usd and tuition_paid_to_date_usd are tuition and fees after grant aid, computed from this schedule — so you can reconstruct or re-derive them.
Per-credit rates by academic year (a Fall term bills at the following year's rate, as universities do):
| Year | In-State | Out-of-State | Fees/term |
|---|---|---|---|
| 2015 | $302 | $688 | $1,329 |
| 2018 | $331 | $752 | $1,452 |
| 2021 | $355 | $806 | $1,556 |
| 2024 | $399 | $907 | $1,750 |
| 2026 | $431 | $981 | $1,893 |
Intermediate years interpolate smoothly. Aid covers a mean of about 28% of gross cost, higher for first-generation students and for high-achieving students receiving merit aid. total_loans_usd is the financed portion of net cost.
This dataset is fabricated. It was produced by a simulation written for this event.
- It is not UMBC's real student or alumni data. Real outcome data of this kind is FERPA-protected and is never released at the record level.
- Every employer name is invented. Every course is invented, though modeled on the shape of real degree requirements.
- No real person is represented.
campus_idvalues are hashed and correspond to nobody. - Nothing you conclude from this data is a fact about actual UMBC graduates. If you present a finding, say the data is synthetic. Judges will ask.
On what's deliberately missing: there are no race, gender, or ethnicity fields. This is intentional. In synthetic data, any group disparity you "discover" is one the generator invented, and presenting an invented disparity as a finding about real people would be harmful. Please don't reconstruct proxies for these attributes.
Can we use external data? Yes. Real job postings, salary benchmarks, cost-of-living figures, and skill taxonomies all work well alongside this. Cite your sources, and keep clear which conclusions come from which.
Can we use LLMs / AI tools? Yes, for code and for products built on the data. If your project is an AI tool, ground it in the actual rows and make it show its sources.
There are no names. Can we add them? Generate display names client-side if your interface needs them. They're omitted deliberately: 5,000 generated names would inevitably collide with real UMBC students and attach fabricated GPAs and unemployment records to real people.
Is course_catalog.csv a real UMBC catalog? No. Course IDs resemble real UMBC prefixes, but every course here is invented.
Do we have to build something about salaries? No. Salary is one column among many. Curriculum structure, campus involvement, credentials, geography, time-to-degree, and cost are all first-class in this data, and the track question is broader than earnings.
Why is the largest file 140k rows instead of something bigger? So it loads in a browser, opens in Excel, and doesn't punish anyone's laptop. Statistical power is still fine: 3,200 alumni across 12 years leaves enough per cell to slice by major and year.
Can we regenerate or extend it? The generator isn't public — that would give away the structure the track is built around. If you need a different slice, ask an organizer.
Something looks wrong in the data. Tell an organizer. If it's a genuine bug we want to know. But check the per-file .md first — most surprises are documented there.
The data in data/ is released under CC0 1.0 — public domain. Use it however you like, during the event or after.