1212from core .calibrator import calibrate_all , generate_ranker_overrides
1313from core .course_optimizer import optimize_courses
1414from 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
1616from 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
2524from core .prerequisite_matcher import match_prerequisites
2625from core .profile_evaluator import evaluate as evaluate_profile
2726from 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:
819826def 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+
10781218def 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