Phase 6 Inventory — Read-Only Inspection Pass
Generated: 2026-05-04
Test baseline confirmed: 256 tests collected
Section 1 — Tab and Surface Structure
Three top-level tabs (app.py line 2511)
school_finder_tab, major_explorer_tab, find_your_fit_tab = st.tabs(
["School Finder", "Major Explorer", "Find Your Fit"]
)
Renders on every tab. Sub-sections in order:
- Title + caption
--- divider
- Filters subheader
- State multiselect
- School type multiselect
- Campus setting multiselect
- School size multiselect
- Minimum % Pell Grant slider
- Mission-driven institutions expander (multiselect inside)
- Schools’ avg SAT range slider
- Undergraduate enrollment slider
- Show small schools checkbox
- Filter by major (CIP) selectbox (Phase 13.2)
--- divider
- Your test scores (optional) subheader
- SAT composite number_input
- ACT composite number_input
- Family income bracket selectbox
--- divider
- Priorities subheader
- Quick start selectbox (preset profiles)
- 7× scoring dimension weight sliders
School Finder tab (app.py lines 2523–2603)
Primary render file: app.py; helper file: page_modules/school_finder.py (59 lines, cross-tab helpers only)
Approximate lines in app.py owned by this tab’s render logic: ~1,900 (sidebar helpers, PDF/CSV export, pin, results, map, comparison, detail cards)
Sub-sections in render order:
- Title — “Results” (
st.title)
- Cross-tab CIP banner — shown once after Major Explorer handoff (app.py 2527–2531)
- Summary banner — school count + active priorities + filter summary (app.py 2534,
render_summary_banner())
- Export row — rows-to-export
number_input + CSV download button + PDF download button (app.py 2536, render_export_row())
- Pin selector — multiselect + Clear pins button (app.py 2538,
render_pin_selector())
- Results table —
st.dataframe with 14 columns including ProgressColumn for Match score (app.py 2540, render_results())
- Map expander — “🗺️ Map of matching schools (N schools)” (app.py 2542,
render_map())
- Comparison expander — “📊 Compare pinned schools (2–4)” containing side-by-side table + grouped bar chart (app.py 2544,
render_comparison())
- Pinned school detail cards — one bordered card per pinned school (app.py 2546,
render_pinned_cards())
- Each card contains: context badges (Carnegie/test policy/MSI/religion), header with acceptance badge, “Why this rank?” explainer, CI bar, sensitivity tier badge, key metrics grid (8 metrics), radar chart, Student Body section (demographics stacked bar + outcomes detail), Earnings Mobility section (grouped bar chart), Program-level outcomes section (4 metric tiles), Programs offered expander (table with Explore buttons)
- About the Data expander — methodology for BLS/OEWS/EP/geographic/work-environment data (app.py 2548–2601)
Major Explorer tab (page_modules/major_explorer.py — 1,135 lines)
Primary render file: page_modules/major_explorer.py, render() at line 1106
Sub-sections in render order:
- Header + caption — “Major Explorer” (lines 1108–1111)
- Alignment context banner — Phase 12.3: tip (quiz not taken), success (quiz taken), or silent (zero-variance vector) (lines 1124–1136)
- Major picker —
st.selectbox “Select a major”, annotated with ⭐ for aligned majors (lines 1161–1171)
- Major description block — Phase 11.1A: hand-authored overview + “What you’ll learn” + “Typical classes” + related majors, or stub NCES text (lines 1183–1187)
- “See schools strong in this major →” button — Phase 13.2 cross-tab handoff to School Finder (lines 1190–1200)
- NY Fed Labor Market Outcomes — Phase 11.2B.3: 5
st.metric tiles (unemployment, underemployment, early wage, mid-career wage, grad degree share) (lines 1205–1207)
- Scorecard Outcomes panel — 4
st.metric tiles (earnings 1yr/4yr/5yr, debt); “About these numbers” expander (lines 1217–1256)
- Wages and projections summary — Phase 9.5: 4
st.metric tiles (median wage, 10yr growth, total employment, routing occupations count) (lines 1261–1263)
- Career paths and occupations — Phase 9.4:
st.dataframe of routing occupations with wage/growth/openings/education columns (lines 1267–1269)
- Industries that hire this major — Phase 11.3.3: horizontal bar chart (top 10 NAICS + optional Other row) (lines 1273–1275)
- Work environment — Phase 10.5: horizontal bar chart, 13 O*NET work-context elements colored by quartile (lines 1279–1283)
- Where these jobs are — Phase 10.4: side-by-side USA choropleths (Total Employment + Location Quotient) (lines 1287–1290)
- Schools strong in this major — Phase 8.4: ranked table of top 25 schools with “📌 Pin” buttons (lines 1294–1350)
Find Your Fit tab (page_modules/find_your_fit.py — 262 lines)
Primary render file: page_modules/find_your_fit.py, render() at line 288
Sub-sections in render order (pre-submission state):
- Header + intro — “Find Your Fit”, O*NET methodology description (lines 97–112)
- Questionnaire — 6
st.expander sections (one per RIASEC dimension), each with 10 st.radio items on a 5-point Likert scale (lines 115–145)
- Progress bar —
st.progress(total_answered / 60) (line 146)
- “See my results” button — disabled until all 60 items answered (lines 149–170)
Post-submission state (fyf_submitted == True):
- Your Interest Profile — Holland Code
st.metric + horizontal bar chart (6 RIASEC dimensions) (lines 178–208)
- Majors Aligned with Your Interests — top 10 table with correlation scores + “Explore →” buttons (lines 213–281)
- Shared results banner — shown when
?riasec=... URL param detected (Phase 13.4b) (lines 226–246)
- “Retake the questionnaire” button (line 303)
Section 2 — Chart Inventory
| # |
Tab / Sub-section |
Library |
Chart type |
File : approx. line |
Accessibility annotation |
| 1 |
School Finder / Map expander |
plotly.graph_objects |
Scattermapbox (2 traces: black outline + RdYlGn score-colored markers) |
app.py : 2096–2141 |
Caption below: “Each dot is one school, colored by match score (red = low match, green = high match).” No alt text / aria-label. |
| 2 |
School Finder / Pinned card — Percentile profile |
plotly.graph_objects |
Scatterpolar (radar / spider) |
app.py : 1378–1401 |
hovertemplate with dimension label + percentile. No alt text / aria-label. |
| 3 |
School Finder / Pinned card — Student Body |
plotly.graph_objects |
Bar (horizontal stacked — gender row + race/ethnicity row) |
app.py : 1471–1513 |
Per-segment hovertemplate with label + %. No alt text / aria-label. |
| 4 |
School Finder / Pinned card — Earnings Mobility |
plotly.graph_objects |
Bar (vertical grouped — 3 income terciles) |
app.py : 1600–1622 |
Per-bar hovertemplate + text labels on bars. Caption below with source note. No alt text / aria-label. |
| 5 |
School Finder / Pinned card — Match score CI |
plotly.graph_objects |
Custom (shapes + annotations, no trace type) — confidence interval band + point estimate marker |
app.py : 1648–1712 |
Caption below with score + CI range + dimensions reported. No alt text / aria-label. |
| 6 |
School Finder / Comparison expander |
plotly.graph_objects |
Bar (grouped — one group per scoring dimension, one bar per school) |
app.py : 2343–2390 |
Caption below: “Bar heights are percentile ranks within the filtered population…”. No alt text / aria-label. |
| 7 |
Major Explorer / Work environment |
plotly.graph_objects |
Bar (horizontal — 13 O*NET elements, color-coded by quartile) |
major_explorer.py : 591–615 |
Caption below with quartile color legend. No alt text / aria-label. |
| 8 |
Major Explorer / Where these jobs are (left) |
plotly.graph_objects |
Choropleth (USA states — Viridis scale, total employment) |
major_explorer.py : 684–701 |
hover template with state, employment, coverage %. No alt text / aria-label. |
| 9 |
Major Explorer / Where these jobs are (right) |
plotly.graph_objects |
Choropleth (USA states — Viridis scale, location quotient) |
major_explorer.py : 714–734 |
hover template with state, LQ, coverage %. Caption: color scale capped at 5.0. No alt text / aria-label. |
| 10 |
Major Explorer / Industries that hire this major |
plotly.graph_objects |
Bar (horizontal — top 10 NAICS3 + optional Other row) |
major_explorer.py : 1032–1056 |
hover template with title + share %. Caption below with source/methodology. No alt text / aria-label. |
| 11 |
Find Your Fit / Your Interest Profile |
plotly.graph_objects |
Bar (horizontal — 6 RIASEC dimension scores) |
find_your_fit.py : 191–208 |
hover template with dimension + score. No alt text / aria-label. |
None of the 11 charts have Streamlit-level alt text, aria-label, or a programmatic descriptive caption rendered as accessible text above the chart. Hover templates exist on all but chart #5, but those are not screen-reader accessible.
Section 3 — Interactive Element Inventory
st.multiselect (6 instances)
| Widget |
Key |
Location |
Label descriptiveness |
| State filter |
filter_states |
Sidebar, app.py 427 |
Descriptive |
| School type filter |
filter_controls |
Sidebar, app.py 435 |
Descriptive |
| Campus setting filter |
filter_settings |
Sidebar, app.py 444 |
Descriptive |
| School size filter |
filter_sizes |
Sidebar, app.py 450 |
Descriptive |
| Mission-driven (inside expander) |
filter_mission |
Sidebar, app.py 464 |
Descriptive |
| Pin schools for detail view |
pinned_schools |
School Finder main area, app.py 1158 |
Descriptive (“📌 Pin schools for detail view”) |
- Keys follow consistent
filter_* pattern for sidebar filters; pinned_schools is its own category.
st.selectbox (4 instances)
| Widget |
Key |
Location |
Label descriptiveness |
| Filter by major (CIP) |
cip_filter_widget |
Sidebar, app.py 511 |
Descriptive |
| Family income bracket |
selected_income_bracket |
Sidebar, app.py 553 |
Descriptive |
| Quick start (preset) |
(no key) |
Sidebar, app.py 582 |
Descriptive |
| Select a major |
major_picker_widget |
Major Explorer, major_explorer.py 1161 |
Descriptive |
- Quick start selectbox intentionally has no
key= (value is derived from weights, not state).
st.slider / st.select_slider (10 instances total)
| Widget |
Key |
Location |
| Min % Pell Grant recipients |
filter_pell_min |
Sidebar, app.py 453 |
| Schools’ avg SAT range |
(no key) |
Sidebar, app.py 473 |
| Undergraduate enrollment |
(no key) |
Sidebar, app.py 483 |
| Weight: Low Net Price |
weight_affordability |
Sidebar, app.py 597 |
| Weight: High Graduation Rate |
weight_graduation_rate |
Sidebar, app.py 597 |
| Weight: High Retention Rate |
weight_retention_rate |
Sidebar, app.py 597 |
| Weight: High Earnings After Graduation |
weight_earnings |
Sidebar, app.py 597 |
| Weight: High Selectivity |
weight_selectivity |
Sidebar, app.py 597 |
| Weight: Low Student Debt |
weight_low_debt |
Sidebar, app.py 597 |
| Weight: High Loan Repayment Rate |
weight_repayment |
Sidebar, app.py 597 |
- Weight slider keys follow consistent
weight_{dim_key} pattern.
- SAT range and enrollment range sliders have no key (intentionally excluded from URL state, per comment at app.py 127).
st.checkbox (1 instance)
| Widget |
Key |
Location |
Label |
| Show small schools |
filter_show_small |
Sidebar, app.py 488 |
“Show schools with <250 students (less reliable metrics)” — descriptive |
| Widget |
Key |
Location |
| Your SAT composite (400–1600) |
user_sat_raw |
Sidebar, app.py 537 |
| Your ACT composite (1–36) |
user_act_raw |
Sidebar, app.py 543 |
| Rows to export |
csv_export_top_n |
School Finder export row, app.py 1103 |
st.radio (60 instances — FYF questionnaire)
- All 60 items rendered in
find_your_fit.py:_render_questionnaire() lines 115–143
- Keys:
fyf_item_{item_id} — consistent fyf_item_ prefix
- Labels: verbatim O*NET Interest Profiler Short Form activity text — descriptive by design
| Button label |
Key |
Location |
Type |
| Clear pins |
(generated) |
Pin selector, app.py 1172 |
Regular |
| “📌 Pin” (×25 per major) |
pin_btn_{unit_id}_{cip_code} |
Major Explorer schools table, major_explorer.py 1340 |
Regular |
| “See schools strong in this major →” |
see_schools_{cip4_str} |
Major Explorer, major_explorer.py 1190 |
Regular |
| “Explore →” (×10, top-10 table) |
explore_riasec_{cip4} |
FYF results, find_your_fit.py 276 |
Regular |
| “Explore →” (×N, programs offered) |
explore_program_{unitid}_{cip4} |
School Finder programs expander, app.py 1817 |
Regular |
| “See my results” |
fyf_submit |
FYF questionnaire, find_your_fit.py 149 |
type='primary' |
| “Retake the questionnaire” |
fyf_retake |
FYF results, find_your_fit.py 303 |
Regular |
| “Take the quiz yourself →” |
dismiss_shared_results |
FYF shared results banner, find_your_fit.py 233 |
Regular |
- Dynamic buttons (
pin_btn_*, explore_riasec_*, explore_program_*, see_schools_*) encode enough identity in the key to be unique per rerun.
- Labels are descriptive except “Explore →” which lacks context without surrounding row content (open question for accessibility).
| Label |
Key |
Location |
| Download CSV |
(generated) |
Export row, app.py 1120 |
| Download PDF |
(generated) |
Export row, app.py 1134 |
st.tabs (1 instance)
st.tabs(["School Finder", "Major Explorer", "Find Your Fit"]) — app.py 2511
- Cannot be programmatically switched; cross-tab handoffs use banners instead.
st.expander (11 instances)
| Expander title |
Default expanded |
Location |
| “Mission-driven institutions” |
False |
Sidebar, app.py 462 |
| “🗺️ Map of matching schools (N schools)” |
False |
School Finder, app.py 2200 |
| “📊 Compare pinned schools (N pinned)” |
False |
School Finder, app.py 2408 |
| “All programs offered (N)” |
False |
Pinned card, app.py 1790 |
| “About the Data” |
False |
School Finder, app.py 2548 |
| “About these numbers” |
False |
Major Explorer outcomes, major_explorer.py 1241 |
| “Realistic” (RIASEC dim) |
Session-state-controlled |
FYF questionnaire, find_your_fit.py 131 |
| “Investigative” (RIASEC dim) |
Session-state-controlled |
FYF questionnaire, find_your_fit.py 131 |
| “Artistic” (RIASEC dim) |
Session-state-controlled |
FYF questionnaire, find_your_fit.py 131 |
| “Social” (RIASEC dim) |
Session-state-controlled |
FYF questionnaire, find_your_fit.py 131 |
| “Enterprising” (RIASEC dim) |
Session-state-controlled |
FYF questionnaire, find_your_fit.py 131 |
| “Conventional” (RIASEC dim) |
Session-state-controlled |
FYF questionnaire, find_your_fit.py 131 |
- RIASEC expander state is persisted in
st.session_state["fyf_expander_states"] keyed by dimension letter.
Custom HTML (1 instance)
- Earnings Mobility empty state —
st.markdown(..., unsafe_allow_html=True) with inline <div style='background:#f0f0f0;...'> (app.py 1583–1587). No ARIA role.
Section 4 — Data Source Inventory
| # |
Source name |
File path on disk |
Loader function |
Snapshot / version |
In “About the Data” expander? |
| 1 |
College Scorecard Institution-Level |
data/cleaned/colleges_cleaned.csv |
app.py:load_colleges() |
“Most-Recent-Cohorts-Institution.csv” — no date in filename; version not pinned in code |
Not explicitly (expander covers BLS/OEWS/geographic/work-env only) |
| 2 |
College Scorecard Field of Study |
data/cleaned/field_of_study_cleaned.csv |
major_explorer.py:load_field_of_study() |
“Most-Recent-Cohorts-Field-of-Study.csv” — same vintage as #1, not pinned |
Not explicitly |
| 3 |
BLS OEWS National (May 2024) |
data/cleaned/occupations_master.csv |
major_explorer.py:_load_career_paths_data() |
May 2024 (traceable from raw file national_M2024_dl.xlsx) |
Yes — “BLS Occupational Employment and Wage Statistics, May 2024” |
| 4 |
BLS Employment Projections 2024–2034 |
data/cleaned/occupations_master.csv (merged into same file as #3) |
major_explorer.py:_load_career_paths_data() |
2024–2034 (from raw file occupation.xlsx; BLS EP release cycle not pinned) |
Yes — “BLS Employment Projections, 2024–2034” |
| 5 |
BLS OEWS State-Level (May 2024) |
data/cleaned/state_occupations.csv → data/cleaned/cip4_state_employment.csv |
major_explorer.py:_load_cip4_state_employment() |
May 2024 (same OEWS release as #3) |
Yes — geographic distribution methodology note |
| 6 |
NCES CIP–SOC Crosswalk |
data/cleaned/cip4_to_soc.csv |
major_explorer.py:_load_career_paths_data() (second return value) |
CIP 2020 → SOC 2018 (raw: CIP2020_SOC2018_Crosswalk.xlsx) |
Yes — “NCES CIP–SOC Crosswalk (CIP 2020 → SOC 2018)” |
| 7 |
BLS OEWS NAICS Industry Distribution |
data/cleaned/soc_to_naics3.csv → data/cleaned/cip4_naics3_distribution.csv |
naics_distribution.py:get_naics_distribution() via naics_distribution._load() |
May 2024 (same OEWS release) |
Yes — “BLS Occupational Employment and Wage Statistics, May 2024 release (3-digit NAICS)” in chart caption; not in About the Data expander body |
| 8 |
O*NET Work Context v30.2 |
data/cleaned/work_context.csv → data/cleaned/cip4_work_context.csv |
major_explorer.py:_load_cip4_work_context() |
O*NET 30.2 (from chart caption; raw prep in data_prep_workcontext.py) |
Yes — work environment methodology note in About the Data expander |
| 9 |
NY Fed Labor Market Outcomes (Feb 2026) |
data/processed/nyfed_outcomes.parquet |
nyfed_outcomes.py:get_nyfed_outcomes() via _load_outcomes_df() |
February 2026 release (from caption in Major Explorer NY Fed panel, major_explorer.py 961) |
Not in About the Data expander; source/date only in chart caption |
| 10 |
CIP4 RIASEC Interest Profiles |
data/cleaned/cip4_riasec.csv |
riasec_distribution.py:_load_riasec_data() |
O*NET (version implicit in source; not pinned in code or filenames) |
Not mentioned |
| 11 |
NCES Major Descriptions (hand-authored) |
data/major_descriptions.yaml |
major_descriptions.py:load_major_descriptions() |
Internal; no versioning |
Not mentioned |
| 12 |
BLS OEWS Major-Level Aggregates |
data/cleaned/major_outcomes.csv |
major_explorer.py:_load_major_outcomes() |
Derived from #3+#6 (May 2024 OEWS + CIP 2020 crosswalk) |
Yes — implicitly via the wages/projections methodology note |
Open question: data/processed/top_60_cip4.csv exists on disk (scripts/rank_top_cip4.py produces it) but is not consumed by the runtime app. Not counted above.
Section 5 — Color and Palette Usage
Centralized palette
- No centralized palette constant in
config.py.
- A named dict
_DEMO_COLORS exists in app.py at lines 1408–1421 (module-level, not in config). It is used only for demographics + earnings mobility charts.
- A local
palette list ["#0072B2", "#E69F00", "#009E73", "#CC79A7"] is defined inline in _build_comparison_chart() (app.py 2348) — not shared.
Color literals by usage area
Demographics / Earnings Mobility charts — _DEMO_COLORS dict (app.py 1408–1421, Wong 2011 colorblind-safe)
| Token |
Hex |
Used for |
| Women |
#CC79A7 |
Gender stacked bar |
| Men/Other |
#56B4E9 |
Gender stacked bar |
| Black/AA |
#0072B2 |
Race/eth stacked bar |
| Hispanic |
#E69F00 |
Race/eth stacked bar |
| Asian |
#009E73 |
Race/eth stacked bar |
| White |
#56B4E9 |
Race/eth stacked bar (same hex as Men/Other) |
| Non-resident |
#D55E00 |
Race/eth stacked bar |
| Other/Unknown |
#999999 |
Race/eth stacked bar |
| Low Income |
#0072B2 |
Mobility bar (same hex as Black/AA) |
| Mid Income |
#E69F00 |
Mobility bar (same hex as Hispanic) |
| High Income |
#009E73 |
Mobility bar (same hex as Asian) |
Comparison chart — inline local palette (app.py 2348)
["#0072B2", "#E69F00", "#009E73", "#CC79A7"] — up to 4 schools, Wong 2011
CI bar chart (app.py 1652–1677)
| Element |
Color |
| Background track |
rgba(200,200,200,0.15) |
| CI band fill |
rgba(99,143,255,0.22) |
| CI band border |
rgba(0,114,178,0.35) |
| Point estimate line |
#0072B2 |
| Score annotation |
#0072B2 |
Work environment quartile coloring (major_explorer.py 514–523)
| Quartile |
Hex |
Label |
| Bottom (< Q25) |
#94a3b8 |
Medium gray |
| Below average (Q25–Q50) |
#cbd5e1 |
Light gray |
| Above average (Q50–Q75) |
#7da7e8 |
Light blue |
| Top (≥ Q75) |
#2563eb |
Dark blue |
- Colors are hardcoded in
_quartile_color() local function; not imported from config.
NAICS distribution chart (major_explorer.py 974–975)
| Element |
Hex |
Note |
| Real industries |
#1f77b4 |
Default Plotly blue — NOT Wong palette |
| “Other” row |
#999999 |
Muted gray |
RIASEC interest profile bar (find_your_fit.py 195)
#1f77b4 — default Plotly blue, NOT Wong palette, inconsistent with comparison/demo charts
Choropleth color scales (major_explorer.py 688, 724)
- Both Total Employment and Location Quotient choropleths use Plotly named scale “Viridis”
Map scatter (app.py 2116)
- Color scale: “RdYlGn” (Plotly named scale, mapped to match score 0–100)
- Marker outline:
"black" (named)
- Map land:
"lightgray" (named), subunit borders: "white" (named)
Acceptance badges — emoji + PDF colors
| Label |
Emoji |
PDF RGB |
| Safety |
🟢 |
(200, 230, 200) — green tint |
| Match |
🟡 |
(255, 243, 200) — yellow tint |
| Reach |
🔴 |
(255, 210, 210) — red tint |
| N/A |
⚪ |
(220, 220, 220) — gray |
Rank emojis in comparison table: 🟢 = best, 🟡 = middle, 🔴 = worst — defined inline at app.py 1244–1249 and 2262–2270.
Miscellaneous
| Element |
Value |
Location |
| Earnings Mobility empty-state div bg |
#f0f0f0 |
app.py 1583 |
| Earnings Mobility empty-state text |
#888 |
app.py 1584 |
| PDF card background |
RGB (245, 245, 245) |
app.py 93 |
| PDF card border |
RGB (180, 180, 180) |
app.py 94 |
| Chart gridlines (multiple) |
rgba(180,180,180,0.25) |
app.py 1685, 2378 |
Section 6 — URL State and Shareable Surfaces
All URL state is managed by _parse_url_into_session_state() (app.py 324–383) and _write_session_state_to_url() (app.py 386–412). The canonical parameter map is URL_PARAM_MAP (app.py 132–163). The encoding helpers (encode_tab, decode_tab, encode_riasec, decode_riasec) live in url_state.py.
Complete URL parameter map
| URL param |
session_state key |
Type |
Default |
Affected surface |
w_aff |
weight_affordability |
int |
3 |
Sidebar weight slider |
w_grad |
weight_graduation_rate |
int |
3 |
Sidebar weight slider |
w_ret |
weight_retention_rate |
int |
3 |
Sidebar weight slider |
w_earn |
weight_earnings |
int |
3 |
Sidebar weight slider |
w_sel |
weight_selectivity |
int |
3 |
Sidebar weight slider |
w_debt |
weight_low_debt |
int |
3 |
Sidebar weight slider |
w_repay |
weight_repayment |
int |
3 |
Sidebar weight slider |
sat |
user_sat_raw |
int |
0 |
Acceptance badges |
act |
user_act_raw |
int |
0 |
Acceptance badges |
states |
filter_states |
list (comma-joined) |
[] |
School Finder state filter |
control |
filter_controls |
list |
[] |
School Finder type filter |
setting |
filter_settings |
list |
[] |
School Finder setting filter |
size |
filter_sizes |
list |
[] |
School Finder size filter |
mission |
filter_mission |
list |
[] |
School Finder mission filter |
pell |
filter_pell_min |
int |
0 |
Pell % floor slider |
small |
filter_show_small |
bool (1/0) |
False |
Small schools checkbox |
pins |
pinned_schools |
list (comma-joined school names) |
[] |
Pinned school detail cards |
cip |
selected_cip |
int |
0 |
Major Explorer picker |
income |
selected_income_bracket |
str |
"Don't know / skip" |
Net price bracket scoring |
tab |
_active_tab |
str |
"school_finder" |
Tab hint banner only (st.tabs can’t be programmatically switched) |
riasec |
_url_riasec_vector |
str (12-char encoded) |
"" |
Find Your Fit shared results |
Intentionally omitted: SAT range slider, enrollment range slider (tuple values, excluded per comment at app.py 127–130).
Clamping and validation logic
- List params:
_clamp_filter_list(key, valid_list) drops any value not in the current dataset (guards against stale URLs from old data versions).
- Income bracket: clamped to
config.INCOME_BRACKETS keys; garbage resets to "Don't know / skip" (app.py 380–381).
- Tab:
decode_tab() in url_state.py accepts only {"school_finder", "major_explorer", "find_your_fit"}; any other value returns None and is dropped. Default ("school_finder") is also dropped so it doesn’t appear in URLs.
- RIASEC:
decode_riasec() validates exact 18-char format (R##I##A##S##E##C##), dimension order, and score range 0–40. Any failure returns None.
_url_parsed guard: entire parse runs exactly once per browser session to prevent subsequent widget interactions from being overwritten (app.py 330–331).
Section 7 — Empty State and Error Handling
| # |
Trigger condition |
File : line |
User-facing message |
| 1 |
Cleaned colleges CSV not found on disk |
app.py : 209 |
st.error("Cleaned data file not found at {path}. Run python data_prep.py to generate it.") + st.stop() |
| 2 |
No schools match current filters (results table) |
app.py : 1187 |
st.info("No schools match the current filters. Try loosening them.") |
| 3 |
No schools match + map opened |
app.py : 2201 |
st.info("No schools match the current filters — nothing to map.") |
| 4 |
Schools exist but none have coordinates (map) |
app.py : 2208 |
st.info("None of the matching schools have published coordinates — nothing to plot. Try loosening your filters.") |
| 5 |
Map marker cap applied |
app.py : 2222 |
st.caption("⚠️ Showing the top 2,000 schools by match score (of N matching). Rendering all of them slows the map.") |
| 6 |
Map: schools excluded for missing coordinates |
app.py : 2228 |
st.caption("ℹ️ {N} school(s) excluded — no published coordinates.") |
| 7 |
Comparison opened with < 2 pinned schools |
app.py : 2411 |
st.info("Pin 2 to 4 schools above to compare them side by side. You currently have N pinned.") |
| 8 |
Comparison: fewer than 2 schools have data |
app.py : 2447 |
st.info("Need at least 2 schools with available data to compare.") |
| 9 |
Comparison: > 4 schools pinned |
app.py : 2418 |
st.warning("Comparison is limited to 4 schools. Showing the first 4 of your N pinned schools (in pin order).") |
| 10 |
Comparison: named school not found in data |
app.py : 2441 |
st.caption("⚠️ Couldn't find data for: {names}. Skipped from the comparison.") |
| 11 |
Pinned card: named school not found in data |
app.py : 2012 |
st.warning("Couldn't find data for {name}.") |
| 12 |
School demographics all missing |
app.py : 1528 |
st.caption("Demographic breakdown not reported.") |
| 13 |
Earnings Mobility all three income terciles missing |
app.py : 1582 |
Inline HTML div: “Mobility data not reported for this school.” |
| 14 |
Program-level outcomes: school doesn’t offer selected major |
app.py : 1745 |
st.caption("{name} does not offer this major.") |
| 15 |
Program-level outcomes: school offers major but cohort < 30 |
app.py : 1752 |
st.caption("{name} offers this major, but the program's cohort size fell below the Scorecard's reporting threshold (n < 30).") |
| 16 |
Schools strong in major: no 5yr earnings data |
major_explorer.py : 1302 |
st.info("No institutions report 5-year graduate earnings for this major…") |
| 17 |
BLS wages/projections: no major_outcomes row for this CIP |
major_explorer.py : 424 |
st.info("BLS occupational data is not available for this major (CIP {cip4_str}).") |
| 18 |
Career paths: no routing SOCs in crosswalk |
major_explorer.py : 776 |
st.info("No BLS-defined routing occupations were found for this major (CIP {cip4_str})…") |
| 19 |
Career paths: routing SOCs found but all aggregation-level (no OEWS match) |
major_explorer.py : 796 |
st.warning("This major routes to {N} occupations in the crosswalk, but none of them have wage or projection data available at the detailed SOC level.") |
| 20 |
Career paths: cip4_str empty (no selection yet) |
major_explorer.py : 769 |
st.info("Select a major above to see its career paths.") |
| 21 |
NY Fed outcomes: CIP4 outside NY Fed 73 major categories |
major_explorer.py : 916 |
st.info("Labor market outcomes are not available for this major…") |
| 22 |
NAICS distribution: no distribution data for this CIP |
major_explorer.py : 997 |
st.info("Industry distribution is not available for this major…") |
| 23 |
Geographic: no employment or LQ rows for this CIP |
major_explorer.py : 651 |
st.info("ℹ️ Geographic distribution data is not available for this major…") |
| 24 |
Geographic: total employment column all-null for this CIP |
major_explorer.py : 682 |
st.info("No state employment data available for this major.") |
| 25 |
Geographic: LQ column all-null for this CIP |
major_explorer.py : 711 |
st.info("No location quotient data available for this major.") |
| 26 |
Work environment: no O*NET data for this CIP |
major_explorer.py : 539 |
st.info("ℹ️ Work environment data is not available for this major…") |
| 27 |
Work environment: pct_coverage NaN or all elements NaN |
major_explorer.py : 553 |
Same as #26 |
| 28 |
Work environment: plot_df empty after filtering |
major_explorer.py : 581 |
Same as #26 |
| 29 |
Major description: CIP not in authored YAML or FoS data |
major_descriptions.py : 62 |
return None — no render; section is silently skipped |
| 30 |
Major description: stub (CIP in FoS but not authored) |
major_explorer.py : 864 |
st.info("This major isn't yet covered by a detailed description in our library…") |
| 31 |
FYF: all 60 items not yet answered (submit blocked) |
find_your_fit.py : 149 |
st.button(disabled=not is_complete) — no error message, just grayed button |
| 32 |
FYF: submitted but validate_responses finds errors |
find_your_fit.py : 157 |
st.error(err) for each error |
| 33 |
FYF: RIASEC vector has zero variance (all same response) |
find_your_fit.py : 249 |
st.warning("Your responses had no variation — every question got the same answer…") |
Section 8 — Cross-Tab Navigation
| Button |
Label |
From tab |
To tab |
File : line |
| “See schools strong in this major →” |
Major Explorer |
School Finder |
major_explorer.py : 1190 |
|
| “📌 Pin” (schools table) |
Major Explorer |
School Finder (pin visible) |
major_explorer.py : 1340 |
|
| “Explore →” (top-10 RIASEC matches) |
Find Your Fit |
Major Explorer |
find_your_fit.py : 276 |
|
| “Explore →” (programs offered) |
School Finder pinned card |
Major Explorer |
app.py : 1817 |
|
Note: No button directly navigates the tab widget. st.tabs() cannot be programmatically switched in Streamlit. All cross-tab transitions require the user to manually click the target tab after seeing a success/info banner.
Session state keys involved in cross-tab handoffs
| Key |
Written by |
Consumed by |
Effect |
cip_filter_widget |
set_school_finder_cip() (school_finder.py 25) |
School Finder sidebar CIP selectbox (app.py 510) |
Pre-selects major filter in sidebar |
_last_xtab_cip_filter |
set_school_finder_cip() (school_finder.py 26) |
main() School Finder tab block (app.py 2527) |
Triggers one-shot cross-tab banner, then popped |
selected_cip (= SELECTED_CIP_KEY) |
set_major_explorer_cip() (major_explorer.py 322) |
Major Explorer picker default index (major_explorer.py 1143) |
Pre-selects major in Major Explorer picker |
_last_xtab_cip |
set_major_explorer_cip() (major_explorer.py 323) |
FYF _render_results() (find_your_fit.py 257) |
Triggers “Major Explorer is pre-loaded” banner, then popped |
_last_xtab_pinned |
_pin_school_callback() (major_explorer.py 309) |
Major Explorer render() schools table block (major_explorer.py 1317) |
Triggers “X is now pinned” success banner, then popped |
_active_tab |
All three set_*_cip helpers + FYF submit |
_write_session_state_to_url() (app.py 386) + banner (app.py 2515) |
Written to ?tab= URL; triggers one-shot load banner |
_url_tab_banner |
_parse_url_into_session_state() (app.py 367) |
main() before tab blocks (app.py 2515) |
Shows “This URL is set up for the X tab” info banner, then popped |
pinned_schools |
_pin_school_callback() (major_explorer.py 306) |
Pin selector widget (app.py 1153) + all detail card renders |
Adds school to pin list |
- None exists. No
st.rerun() scroll-to-top behavior, no JavaScript focus management, no st.empty() scroll anchors.
- After clicking a cross-tab button, the user sees a banner on the current tab or must scroll up to see the banner on the target tab after switching.
Section 9 — Test File Inventory
| File |
Test count |
Description |
tests/test_data_prep_majors.py |
39 |
Validates all pipeline outputs from data_prep_majors.py: CIP→SOC crosswalk, OEWS/EP merge into occupations_master.csv, major_outcomes.csv aggregation, cip4_state_employment.csv shape and LQ math, cip4_work_context.csv schema |
tests/test_scoring.py |
27 |
Unit tests for scoring.py: compute_scores(), compute_sensitivity(), generate_explainer(), CI computation, edge cases (all-NaN, zero weights, single school) |
tests/test_naics_data_prep.py |
26 ⚠️ |
See cohesion note below |
tests/test_major_explorer_alignment.py |
21 ⚠️ |
See cohesion note below |
tests/test_riasec_matching.py |
17 |
compute_holland_code() and get_top_n_matches() in riasec_matching.py: Pearson correlation, top-N shape, zero-variance vector behavior |
tests/test_url_state.py |
16 |
url_state.py encode/decode functions: encode_tab, decode_tab, encode_riasec, decode_riasec, round-trip correctness, invalid inputs |
tests/test_riasec_questionnaire.py |
16 |
riasec_questionnaire.py: get_items() count and structure, get_response_scale(), score_responses(), validate_responses() error conditions |
tests/test_utils.py |
15 |
utils.py: haversine() known distances and edge cases, classify_acceptance() Safety/Match/Reach/N/A classification logic |
tests/test_profiles.py |
13 |
profiles.py: match_profile() preset recognition, get_preset_weights() correct values, Custom profile detection |
tests/test_riasec_distribution.py |
12 |
riasec_distribution.py: get_riasec_for_cip4() known CIPs, zero-coverage CIPs, get_all_cip4_vectors() shape and coverage |
tests/test_data_prep_workcontext.py |
12 |
work_context.csv schema + cip4_work_context.csv schema, element column presence, value ranges, diagnostic columns |
tests/test_riasec_data_prep.py |
10 |
cip4_riasec.csv: schema, row count, score ranges, null coverage for Liberal Arts / Military Tech edge cases |
tests/test_data_prep_geo.py |
10 |
colleges_cleaned.csv geo enrichment: lat/lng presence for major schools, Carnegie label mapping, MSI flag derivation, religion label mapping |
tests/test_crosswalk.py |
9 |
data/nyfed_crosswalk.yaml and data/processed/nyfed_outcomes.parquet: CIP4 coverage, metric columns, no-duplicate constraint, source xlsx round-trip |
tests/test_major_descriptions.py |
8 |
major_descriptions.py:get_description(): authored entries return full dict, unknown CIP returns None, FoS-only CIP returns stub dict |
tests/test_find_your_fit_page.py |
5 |
FYF page module: _render_results() smoke test with mocked session state, URL-shared RIASEC vector decoding flow |
Oversized files — cohesion analysis
tests/test_naics_data_prep.py (26 tests, 205 lines)
Three distinct sub-groupings, each with its own scope="module" fixture:
soc_to_naics3 fixture group (lines 1–112, 8 tests): validates data/cleaned/soc_to_naics3.csv schema, shape, share-sum invariant, suppression-marker cleanup, and two spot-checks (Software Devs → NAICS 541, RNs → NAICS 622).
cip4_naics3_dist fixture group (lines 113–220, 9 tests): validates data/cleaned/cip4_naics3_distribution.csv schema, share-sum invariant, CIP4/NAICS3 format, spot-checks, routing-dilution observable, and Military Tech empty-state.
- Loader + utility group (lines 221–273, 7 tests + 2 utility tests): validates
naics_distribution.get_naics_distribution() behavior, normalize_cip4() utility with 3 format variants.
Natural split lines: → test_soc_to_naics3.py (group 1), test_cip4_naics3_distribution.py (group 2), test_naics_loader.py (group 3). The 2 normalize_cip4 tests in group 3 could go into test_utils.py.
tests/test_major_explorer_alignment.py (21 tests, 200 lines)
Four distinct sub-groupings, covering four separate Phases and four separate functions/modules:
- Phase 12.3 —
_get_aligned_cip4_set() (lines 36–104, 6 tests): no-quiz-taken gate, shape when quiz taken, zero-variance case, caching, cache invalidation, dot-format contract.
- Phase 13.1 —
set_major_explorer_cip() (lines 111–130, 3 tests): session-state writes, no-dot format normalization, overwrite behavior.
- Phase 13.2 —
set_school_finder_cip(), initial_cip_filter_index(), apply_cip_filter() (lines 133–203, 8 tests): session-state writes, filter restricts to offering schools, stale-value fallback.
- Phase 13.3 —
get_school_all_programs() (lines 207–266, 4 tests): sort order, empty-school case, NaN-last sort, cip4 dot-format in output.
Natural split lines: → test_riasec_alignment.py (group 1), test_major_explorer_xtab.py (groups 2+3), test_programs_offered.py (group 4).