Skip to content

Commit 6348df8

Browse files
feat: propagate P(admit)+CI end-to-end through all surfaces
- advisor.py: school list section now shows P(admit) and 95% CI for each entry - cli/main.py cmd_match: displays P(admit)+CI and bias-corrected flag per program - web/app.py: school recommendations section adds 4th column for P(admit)+CI - tests/test_integration_pipeline.py: 30 integration tests covering the full pipeline (evaluate → rank → build_school_list → optimize_portfolio), LR field propagation, GRE-missing graceful degradation, and domestic/international ordering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4556e29 commit 6348df8

5 files changed

Lines changed: 349 additions & 25 deletions

File tree

cli/main.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ def cmd_evaluate(args: argparse.Namespace) -> None:
216216

217217
def cmd_match(args: argparse.Namespace) -> None:
218218
"""Match prerequisites against specific programs."""
219+
from core.lr_predictor import predict_prob_full
220+
219221
profile = load_profile(args.profile)
220222
programs = load_all_programs()
221223

@@ -225,6 +227,8 @@ def cmd_match(args: argparse.Namespace) -> None:
225227
console.print(f"[red]Program '{args.program}' not found.[/red]")
226228
return
227229

230+
gre_quant = profile.test_scores.gre_quant
231+
228232
console.print()
229233
console.print(Panel("Prerequisite Match Report", border_style="cyan"))
230234

@@ -237,6 +241,17 @@ def cmd_match(args: argparse.Namespace) -> None:
237241
console.print(f"\n [bold]{program.name}[/bold] ({program.university})")
238242
console.print(f" Match: [{color}]{match.match_score:.0%}[/{color}]")
239243

244+
lr_pred = predict_prob_full(program.id, profile.gpa, gre_quant, profile)
245+
if lr_pred is not None:
246+
pcolor = "green" if lr_pred.prob >= 0.6 else "yellow" if lr_pred.prob >= 0.35 else "red"
247+
ci_str = (
248+
f" [dim][{lr_pred.prob_low:.0%}{lr_pred.prob_high:.0%}][/dim]"
249+
if lr_pred.prob_low is not None and lr_pred.prob_high is not None
250+
else ""
251+
)
252+
bc_flag = " [dim](bias-corrected)[/dim]" if lr_pred.is_bias_corrected else ""
253+
console.print(f" P(Admit): [{pcolor}]{lr_pred.prob:.0%}[/{pcolor}]{ci_str}{bc_flag}")
254+
240255
if match.missing:
241256
console.print(f" [red]Missing:[/red] {', '.join(match.missing)}")
242257
if match.warnings:
@@ -617,6 +632,18 @@ def cmd_interview(args: argparse.Namespace) -> None:
617632
console.print()
618633

619634

635+
def _fmt_prob(e: Any) -> str:
636+
"""Format admission_prob + CI for display, using pre-computed SchoolListEntry fields."""
637+
prob = getattr(e, "admission_prob", None)
638+
if prob is None:
639+
return "[dim]N/A[/dim]"
640+
pcolor = "green" if prob >= 0.6 else "yellow" if prob >= 0.35 else "red"
641+
low = getattr(e, "prob_low", None)
642+
high = getattr(e, "prob_high", None)
643+
ci = f" [dim][{low:.0%}{high:.0%}][/dim]" if low is not None and high is not None else ""
644+
return f"[{pcolor}]{prob:.0%}[/{pcolor}]{ci}"
645+
646+
620647
def cmd_list(args: argparse.Namespace) -> None:
621648
"""Build and display an optimised school application list."""
622649
profile = load_profile(args.profile)
@@ -625,9 +652,12 @@ def cmd_list(args: argparse.Namespace) -> None:
625652
evaluation = evaluate_profile(profile, projected=projected)
626653
school_list = build_school_list(profile, programs, evaluation)
627654

628-
# Determine GRE Quant score for LR prediction
629-
gre_quant = profile.test_scores.gre_quant
630-
gpa = profile.gpa
655+
# Warn if GRE is missing — LR predictions fall back to training mean
656+
if profile.test_scores.gre_quant is None:
657+
console.print(
658+
" [yellow]Note: GRE Quant not provided — P(Admit) estimates use "
659+
"program average GRE as a proxy and may be optimistic.[/yellow]"
660+
)
631661

632662
console.print()
633663
console.print(
@@ -637,7 +667,7 @@ def cmd_list(args: argparse.Namespace) -> None:
637667
)
638668
)
639669

640-
# One table per category.
670+
# One table per category — P(Admit) uses pre-computed values from rank_schools
641671
for label, entries, style in [
642672
("Reach", school_list.reach, "red"),
643673
("Target", school_list.target, "yellow"),
@@ -654,30 +684,16 @@ def cmd_list(args: argparse.Namespace) -> None:
654684
table.add_column("University", min_width=18)
655685
table.add_column("Fit", justify="right", width=6)
656686
table.add_column("Prereq", justify="right", width=7)
657-
table.add_column("P(Admit)", justify="right", width=9)
687+
table.add_column("P(Admit) [CI]", justify="right", width=20)
658688
table.add_column("Reason", min_width=28)
659689

660690
for e in entries:
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-
)
673-
else:
674-
prob_str = "[dim]N/A[/dim]"
675691
table.add_row(
676692
e.name,
677693
e.university,
678694
f"{e.fit_score:.1f}",
679695
f"{e.prereq_match_score:.0%}",
680-
prob_str,
696+
_fmt_prob(e),
681697
e.reason,
682698
)
683699
console.print(table)

core/list_builder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class SchoolListEntry:
3030
fit_score: float
3131
prereq_match_score: float
3232
reason: str # why this school was selected
33+
admission_prob: float | None = None # bias-corrected P(admit) from LR
34+
prob_low: float | None = None # lower CI bound
35+
prob_high: float | None = None # upper CI bound
3336

3437

3538
@dataclass
@@ -233,6 +236,9 @@ def _to_entries(
233236
fit_score=d["fit_score"],
234237
prereq_match_score=d["prereq_match_score"],
235238
reason=_generate_reason(d, category),
239+
admission_prob=d.get("admission_prob"),
240+
prob_low=d.get("prob_low"),
241+
prob_high=d.get("prob_high"),
236242
)
237243
)
238244
return entries

0 commit comments

Comments
 (0)