Skip to content

Commit 4556e29

Browse files
feat: comprehensive model + UX optimization
P0.2 — Bias correction (highest ROI change) - train_model.py: load real acceptance rates from program YAMLs, store as real_accept_rate in admission_models.json - lr_predictor.py: replace biased training intercept with logit(real_accept_rate), anchoring Baruch from 34.8%→4%, all other programs corrected to their official rates P0.1 — Profile-aware LR adjustments - International flag: logit -= 0.25 (~-5% at typical probability) - 1 internship: logit += 0.10; 2+ internships: logit += 0.20 - New AdmitPrediction dataclass: prob + prob_low + prob_high + is_bias_corrected - Approximate 90% CI via Bamber's effective-N formula P0.3 — P(admit) integrated into fit_score - _compute_fit_score now accepts admission_prob parameter - When LR model available, accept_pts = prob*20 (profile-specific) instead of acceptance-rate heuristic (program-average) P1.1 — Per-program gap analysis - gap_advisor.py: new program_gaps() function returning ProgramGapReport with prereq mismatches, GPA gap, LR P(admit) with CI - CLI: quantpath gaps --program baruch-mfe shows targeted advice P1.3 — Portfolio optimizer - list_builder.py: new optimize_portfolio() maximizing expected admits under n_schools and budget constraints (greedy marginal-value) - CLI: new quantpath portfolio --n-schools 10 --budget 2000 CLI improvements - quantpath list: P(admit) now shows [low%–high%] CI bounds - quantpath gaps --program PROG_ID: per-program mode - quantpath portfolio: new command P2.1 — Tests for lr_predictor (coverage was 0%) - 32 tests covering: math helpers, bias correction, CI bounds, profile adjustments, backward compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 487f436 commit 4556e29

8 files changed

Lines changed: 1059 additions & 65 deletions

File tree

cli/main.py

Lines changed: 170 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@
1212
from core.calibrator import calibrate_all, generate_ranker_overrides
1313
from core.course_optimizer import optimize_courses
1414
from core.data_loader import load_all_programs, load_profile
15-
from core.gap_advisor import analyze_gaps
15+
from core.gap_advisor import analyze_gaps, program_gaps
1616
from core.interview_prep import (
1717
get_questions_by_category,
1818
get_questions_by_difficulty,
1919
get_questions_for_program,
2020
get_random_quiz,
2121
load_questions,
2222
)
23-
from core.list_builder import build_school_list
24-
from core.lr_predictor import predict_prob
23+
from core.list_builder import build_school_list, optimize_portfolio
2524
from core.prerequisite_matcher import match_prerequisites
2625
from core.profile_evaluator import evaluate as evaluate_profile
2726
from core.roi_calculator import calculate_roi
@@ -659,10 +658,18 @@ def cmd_list(args: argparse.Namespace) -> None:
659658
table.add_column("Reason", min_width=28)
660659

661660
for e in entries:
662-
prob = predict_prob(e.program_id, gpa, gre_quant)
663-
if prob is not None:
664-
pcolor = "green" if prob >= 0.6 else "yellow" if prob >= 0.35 else "red"
665-
prob_str = f"[{pcolor}]{prob:.0%}[/{pcolor}]"
661+
from core.lr_predictor import predict_prob_full
662+
lr = predict_prob_full(e.program_id, gpa, gre_quant, profile)
663+
if lr is not None:
664+
pcolor = (
665+
"green" if lr.prob >= 0.6
666+
else "yellow" if lr.prob >= 0.35
667+
else "red"
668+
)
669+
prob_str = (
670+
f"[{pcolor}]{lr.prob:.0%}[/{pcolor}]"
671+
f" [dim][{lr.prob_low:.0%}{lr.prob_high:.0%}][/dim]"
672+
)
666673
else:
667674
prob_str = "[dim]N/A[/dim]"
668675
table.add_row(
@@ -819,8 +826,79 @@ def cmd_optimize(args: argparse.Namespace) -> None:
819826
def cmd_gaps(args: argparse.Namespace) -> None:
820827
"""Analyze profile gaps and suggest improvements."""
821828
profile = load_profile(args.profile)
829+
programs = load_all_programs()
822830
result = evaluate_profile(profile)
823831

832+
# --- Per-program mode -------------------------------------------
833+
target_program_id = getattr(args, "program", None)
834+
if target_program_id:
835+
matched = [p for p in programs if p.id == target_program_id]
836+
if not matched:
837+
console.print(f"[red]Program '{target_program_id}' not found.[/red]")
838+
return
839+
prog = matched[0]
840+
report = program_gaps(profile, prog, result)
841+
842+
console.print()
843+
console.print(
844+
Panel(
845+
f"[bold]Program Gap Analysis[/bold]\n"
846+
f"{profile.name}{report.program_name} ({report.university})",
847+
border_style="cyan",
848+
)
849+
)
850+
851+
# Admission probability
852+
if report.admission_prob is not None:
853+
pcolor = (
854+
"green" if report.admission_prob >= 0.6
855+
else "yellow" if report.admission_prob >= 0.35
856+
else "red"
857+
)
858+
ci_str = (
859+
f" [dim][{report.prob_low:.0%}{report.prob_high:.0%} CI][/dim]"
860+
if report.prob_low is not None else ""
861+
)
862+
console.print(
863+
f" P(Admit): [{pcolor}]{report.admission_prob:.0%}[/{pcolor}]{ci_str}"
864+
)
865+
console.print(
866+
f" Prereq Match: {report.prereq_match_score:.0%} | "
867+
f"GPA Gap: {report.gpa_gap:+.2f}"
868+
)
869+
console.print()
870+
871+
if not report.items:
872+
console.print(
873+
" [bold green]No gaps for this program — "
874+
"your profile is well aligned.[/bold green]"
875+
)
876+
console.print()
877+
return
878+
879+
gap_table = Table(border_style="cyan", show_lines=True)
880+
gap_table.add_column("Severity", width=10, justify="center")
881+
gap_table.add_column("Gap", style="bold", min_width=28)
882+
gap_table.add_column("Action", min_width=44)
883+
884+
sev_colors = {"Critical": "red", "High": "red", "Medium": "yellow", "Low": "cyan"}
885+
for item in report.items:
886+
sc = sev_colors.get(item.severity, "white")
887+
gap_table.add_row(
888+
f"[{sc}]{item.severity}[/{sc}]",
889+
item.label,
890+
item.detail,
891+
)
892+
893+
console.print(gap_table)
894+
console.print(
895+
f" [dim]{report.n_critical} Critical {report.n_high} High "
896+
f"{len(report.items) - report.n_critical - report.n_high} Medium/Low[/dim]"
897+
)
898+
console.print()
899+
return
900+
901+
# --- Profile-level gap mode (default) ---------------------------
824902
console.print()
825903
console.print(
826904
Panel(
@@ -1075,6 +1153,68 @@ def cmd_calibrate(args: argparse.Namespace) -> None:
10751153
console.print()
10761154

10771155

1156+
def cmd_portfolio(args: argparse.Namespace) -> None:
1157+
"""Optimize school portfolio to maximize expected admissions."""
1158+
profile = load_profile(args.profile)
1159+
programs = load_all_programs()
1160+
evaluation = evaluate_profile(profile)
1161+
1162+
n_schools = getattr(args, "n_schools", 10)
1163+
budget = getattr(args, "budget", 2000)
1164+
1165+
portfolio = optimize_portfolio(
1166+
profile, programs, evaluation,
1167+
n_schools=n_schools,
1168+
budget=budget,
1169+
)
1170+
1171+
console.print()
1172+
console.print(
1173+
Panel(
1174+
f"[bold]Portfolio Optimizer[/bold] for {profile.name}\n"
1175+
f"Maximizing expected admissions under "
1176+
f"n≤{n_schools} schools and ${budget:,} fee budget",
1177+
border_style="cyan",
1178+
)
1179+
)
1180+
1181+
cat_styles = {"reach": "red", "target": "yellow", "safety": "green"}
1182+
table = Table(border_style="cyan", show_lines=True)
1183+
table.add_column("Category", width=8, justify="center")
1184+
table.add_column("Program", style="bold", min_width=20)
1185+
table.add_column("University", min_width=18)
1186+
table.add_column("P(Admit)", justify="right", width=9)
1187+
table.add_column("Fit", justify="right", width=6)
1188+
table.add_column("Fee", justify="right", width=7)
1189+
table.add_column("Exp. Contrib.", justify="right", width=12)
1190+
1191+
for e in portfolio.programs:
1192+
cat_color = cat_styles.get(e.category, "white")
1193+
pcolor = (
1194+
"green" if e.admission_prob >= 0.60
1195+
else "yellow" if e.admission_prob >= 0.35
1196+
else "red"
1197+
)
1198+
table.add_row(
1199+
f"[{cat_color}]{e.category.title()}[/{cat_color}]",
1200+
e.name,
1201+
e.university,
1202+
f"[{pcolor}]{e.admission_prob:.0%}[/{pcolor}]",
1203+
f"{e.fit_score:.1f}",
1204+
f"${e.application_fee:,}" if e.application_fee else "N/A",
1205+
f"[green]+{e.expected_contribution:.2f}[/green]",
1206+
)
1207+
1208+
console.print(table)
1209+
console.print()
1210+
console.print(
1211+
f" [bold]Expected admits:[/bold] [green]{portfolio.expected_admits:.2f}[/green] schools\n"
1212+
f" [bold]Total fees:[/bold] ${portfolio.total_fees:,}\n"
1213+
f" [dim]{portfolio.summary}[/dim]"
1214+
)
1215+
console.print()
1216+
1217+
10781218
def main() -> None:
10791219
parser = argparse.ArgumentParser(
10801220
prog="quantpath",
@@ -1164,6 +1304,10 @@ def main() -> None:
11641304
# gaps
11651305
p_gaps = subparsers.add_parser("gaps", help="Analyze profile gaps and suggest improvements")
11661306
p_gaps.add_argument("--profile", "-p", required=True, help="Path to profile YAML")
1307+
p_gaps.add_argument(
1308+
"--program",
1309+
help="Show gaps specific to one program (e.g. baruch-mfe, cmu-mscf)",
1310+
)
11671311

11681312
# stats (real data)
11691313
p_stats = subparsers.add_parser("stats", help="Show statistics from real admission data")
@@ -1197,6 +1341,24 @@ def main() -> None:
11971341
help="Include planned courses (shows school list at application time)",
11981342
)
11991343

1344+
# portfolio
1345+
p_portfolio = subparsers.add_parser(
1346+
"portfolio", help="Optimize school portfolio to maximize expected admissions"
1347+
)
1348+
p_portfolio.add_argument("--profile", "-p", required=True, help="Path to profile YAML")
1349+
p_portfolio.add_argument(
1350+
"--n-schools",
1351+
type=int,
1352+
default=10,
1353+
help="Max number of schools to select (default: 10)",
1354+
)
1355+
p_portfolio.add_argument(
1356+
"--budget",
1357+
type=int,
1358+
default=2000,
1359+
help="Max total application fees in USD (default: $2,000)",
1360+
)
1361+
12001362
args = parser.parse_args()
12011363

12021364
if args.command is None:
@@ -1217,6 +1379,7 @@ def main() -> None:
12171379
"calibrate": cmd_calibrate,
12181380
"optimize": cmd_optimize,
12191381
"list": cmd_list,
1382+
"portfolio": cmd_portfolio,
12201383
}
12211384
commands[args.command](args)
12221385

0 commit comments

Comments
 (0)