import%20marimo%0A%0A__generated_with%20%3D%20%220.23.5%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%205%20%E2%80%94%20Unblinded%20test%20set%20analysis%0A%0A%20%20%20%20This%20notebook%20analyses%20the%20newly%20available%20ground-truth%20labels%20for%20the%20Phase%201%0A%20%20%20%20test%20set%20(%60pxr-challenge_TEST_PHASE_1_UNBLINDED.csv%60)%20to%20answer%20three%20questions%3A%0A%0A%20%20%20%201.%20**Dataset%20profile**%20%E2%80%94%20how%20does%20the%20unblinded%20pEC50%20distribution%20compare%20with%0A%20%20%20%20%20%20%20the%20training%20set%3F%20%20What%20fraction%20of%20test%20compounds%20would%20be%20classified%20as%20hits%3F%0A%20%20%20%202.%20**Model%20ranking**%20%E2%80%94%20how%20did%20each%20of%20our%20submitted%20predictions%20perform%20now%20that%0A%20%20%20%20%20%20%20the%20true%20labels%20are%20revealed%3F%20%20Metrics%3A%20RMSE%2C%20MAE%2C%20R%C2%B2%2C%20Pearson%20*r*%2C%20Spearman%20%CF%81.%0A%20%20%20%203.%20**Error%20anatomy**%20%E2%80%94%20which%20compounds%20were%20most%20%2F%20least%20accurately%20predicted%2C%20and%0A%20%20%20%20%20%20%20are%20there%20structural%20patterns%20in%20the%20hard-to-predict%20cases%3F%0A%0A%20%20%20%20**Inputs%3A**%0A%20%20%20%20-%20Unblinded%20CSV%20fetched%20from%20HuggingFace%20on%20first%20run%20and%20cached%20locally%20in%0A%20%20%20%20%20%20%60data%2Fraw%2F20260528%2F%60.%0A%20%20%20%20-%20Submission%20CSVs%20in%20%60submissions%2F%60%20%E2%80%94%20every%20file%20ending%20in%20%60_submission.csv%60.%0A%20%20%20%20-%20Processed%20training%20data%3A%20%60data%2Fprocessed%2Fall_compounds_activity_data.csv%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20glob%0A%20%20%20%20import%20math%0A%20%20%20%20from%20pathlib%20import%20Path%0A%20%20%20%20from%20typing%20import%20Optional%0A%0A%20%20%20%20import%20altair%20as%20alt%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20polars%20as%20pl%0A%20%20%20%20from%20scipy.stats%20import%20gaussian_kde%2C%20pearsonr%2C%20spearmanr%0A%20%20%20%20from%20sklearn.metrics%20import%20mean_absolute_error%2C%20mean_squared_error%2C%20r2_score%0A%0A%20%20%20%20from%20rdkit%20import%20Chem%2C%20RDLogger%0A%20%20%20%20from%20rdkit.Chem%20import%20rdDepictor%0A%20%20%20%20from%20rdkit.Chem.Draw%20import%20rdMolDraw2D%0A%0A%20%20%20%20RDLogger.DisableLog(%22rdApp.*%22)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20Chem%2C%0A%20%20%20%20%20%20%20%20Optional%2C%0A%20%20%20%20%20%20%20%20Path%2C%0A%20%20%20%20%20%20%20%20alt%2C%0A%20%20%20%20%20%20%20%20gaussian_kde%2C%0A%20%20%20%20%20%20%20%20math%2C%0A%20%20%20%20%20%20%20%20mean_absolute_error%2C%0A%20%20%20%20%20%20%20%20mean_squared_error%2C%0A%20%20%20%20%20%20%20%20mo%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20pearsonr%2C%0A%20%20%20%20%20%20%20%20pl%2C%0A%20%20%20%20%20%20%20%20plt%2C%0A%20%20%20%20%20%20%20%20r2_score%2C%0A%20%20%20%20%20%20%20%20rdDepictor%2C%0A%20%20%20%20%20%20%20%20rdMolDraw2D%2C%0A%20%20%20%20%20%20%20%20spearmanr%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell%0Adef%20_(Chem%2C%20Optional%2C%20rdDepictor%2C%20rdMolDraw2D)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Molecule%20drawing%20helpers%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%0A%20%20%20%20def%20smi_to_svg(smi%3A%20str%2C%20width%3A%20int%20%3D%20280%2C%20height%3A%20int%20%3D%20200)%20-%3E%20str%3A%0A%20%20%20%20%20%20%20%20%22%22%22Render%20a%20SMILES%20string%20as%20an%20SVG%20image%20via%20RDKit%20MolDraw2DSVG.%22%22%22%0A%20%20%20%20%20%20%20%20mol%20%3D%20Chem.MolFromSmiles(smi)%0A%20%20%20%20%20%20%20%20if%20mol%20is%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22%22%0A%20%20%20%20%20%20%20%20rdDepictor.Compute2DCoords(mol)%0A%20%20%20%20%20%20%20%20drawer%20%3D%20rdMolDraw2D.MolDraw2DSVG(width%2C%20height)%0A%20%20%20%20%20%20%20%20drawer.DrawMolecule(mol)%0A%20%20%20%20%20%20%20%20drawer.FinishDrawing()%0A%20%20%20%20%20%20%20%20return%20drawer.GetDrawingText()%0A%0A%20%20%20%20def%20strip_xml_decl(svg%3A%20str)%20-%3E%20str%3A%0A%20%20%20%20%20%20%20%20%22%22%22Remove%20the%20%3C%3Fxml%20...%20%3F%3E%20declaration%20so%20SVG%20embeds%20cleanly%20in%20HTML.%22%22%22%0A%20%20%20%20%20%20%20%20return%20svg.split(%22%3F%3E%22%2C%201)%5B-1%5D.strip()%20if%20%22%3F%3E%22%20in%20svg%20else%20svg%0A%0A%20%20%20%20def%20smi_to_inchikey(smi%3A%20str)%20-%3E%20Optional%5Bstr%5D%3A%0A%20%20%20%20%20%20%20%20%22%22%22Return%20the%20InChIKey%20for%20*smi*%2C%20or%20None%20if%20the%20SMILES%20cannot%20be%20parsed.%22%22%22%0A%20%20%20%20%20%20%20%20mol%20%3D%20Chem.MolFromSmiles(smi)%0A%20%20%20%20%20%20%20%20return%20Chem.MolToInchiKey(mol)%20if%20mol%20else%20None%0A%0A%20%20%20%20def%20smi_to_inchi(smi%3A%20str)%20-%3E%20Optional%5Bstr%5D%3A%0A%20%20%20%20%20%20%20%20%22%22%22Return%20the%20InChI%20for%20*smi*%2C%20or%20None%20if%20the%20SMILES%20cannot%20be%20parsed.%22%22%22%0A%20%20%20%20%20%20%20%20mol%20%3D%20Chem.MolFromSmiles(smi)%0A%20%20%20%20%20%20%20%20return%20Chem.MolToInchi(mol)%20if%20mol%20else%20None%0A%0A%20%20%20%20return%20smi_to_inchi%2C%20smi_to_inchikey%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Load%20unblinded%20test%20set%0A%0A%20%20%20%20The%20CSV%20is%20fetched%20directly%20from%20HuggingFace%20and%20saved%20locally%20so%20subsequent%0A%20%20%20%20runs%20are%20fast.%20%20The%20file%20contains%20253%20test%20compounds%20with%20full%20dose-response%0A%20%20%20%20parameters%3A%20pEC50%2C%20Emax%2C%20confidence%20intervals%2C%20and%20assay%20metadata.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Path%2C%20pl%2C%20smi_to_inchi%2C%20smi_to_inchikey)%3A%0A%20%20%20%20%23%20Cache%20the%20unblinded%20CSV%20locally%20to%20avoid%20re-downloading%20each%20run.%0A%20%20%20%20RAW_DIR%20%3D%20Path(%22..%2Fdata%2Fraw%2F20260528%22)%0A%20%20%20%20RAW_DIR.mkdir(parents%3DTrue%2C%20exist_ok%3DTrue)%0A%0A%20%20%20%20UNBLINDED_LOCAL%20%3D%20RAW_DIR%20%2F%20%22dose_response_test_unblinded.csv%22%0A%20%20%20%20HF_URL%20%3D%20%22hf%3A%2F%2Fdatasets%2Fopenadmet%2Fpxr-challenge-train-test%2Fpxr-challenge_TEST_PHASE_1_UNBLINDED.csv%22%0A%0A%20%20%20%20if%20not%20UNBLINDED_LOCAL.exists()%3A%0A%20%20%20%20%20%20%20%20pl.read_csv(HF_URL).write_csv(UNBLINDED_LOCAL)%0A%0A%20%20%20%20unblinded_raw%3A%20pl.DataFrame%20%3D%20pl.read_csv(UNBLINDED_LOCAL)%0A%0A%20%20%20%20%23%20Standardise%20column%20names%20and%20add%20chemoinformatics%20identifiers.%0A%20%20%20%20unblinded%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20unblinded_raw%0A%20%20%20%20%20%20%20%20.rename(%7B%22SMILES%22%3A%20%22smiles%22%7D)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22smiles%22).map_elements(smi_to_inchikey%2C%20return_dtype%3Dpl.Utf8).alias(%22inchikey%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22smiles%22).map_elements(smi_to_inchi%2C%20%20%20%20return_dtype%3Dpl.Utf8).alias(%22inchi%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%23%20Shorten%20verbose%20column%20names%20for%20convenience%20inside%20this%20notebook.%0A%20%20%20%20%20%20%20%20.rename(%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50_std.error%20(-log10(molarity))%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50_se%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50_ci.lower%20(-log10(molarity))%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50_ci_lower%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50_ci.upper%20(-log10(molarity))%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50_ci_upper%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax_estimate%20(log2FC%20vs.%20baseline)%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%22Emax%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax_std.error%20(log2FC%20vs.%20baseline)%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax_se%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax_ci.lower%20(log2FC%20vs.%20baseline)%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%22Emax_ci_lower%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax_ci.upper%20(log2FC%20vs.%20baseline)%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%22Emax_ci_upper%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax.vs.pos.ctrl_estimate%20(dimensionless)%22%3A%20%20%20%20%20%20%20%22Emax_vs_ctrl%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax.vs.pos.ctrl_std.error%20(dimensionless)%22%3A%20%20%20%20%20%20%22Emax_vs_ctrl_se%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax.vs.pos.ctrl_ci.lower%20(dimensionless)%22%3A%20%20%20%20%20%20%20%22Emax_vs_ctrl_ci_lower%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Emax.vs.pos.ctrl_ci.upper%20(dimensionless)%22%3A%20%20%20%20%20%20%20%22Emax_vs_ctrl_ci_upper%22%2C%0A%20%20%20%20%20%20%20%20%7D)%0A%20%20%20%20)%0A%0A%20%20%20%20print(%22Shape%3A%22%2C%20unblinded.shape)%0A%20%20%20%20print(%22Columns%3A%22%2C%20unblinded.columns)%0A%20%20%20%20unblinded%0A%20%20%20%20return%20(unblinded%2C)%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Dataset%20overview%0A%0A%20%20%20%20%23%23%23%20pEC50%20distribution%0A%0A%20%20%20%20The%20dose-response%20pEC50%20describes%20potency%20on%20a%20%E2%88%92log10(molarity)%20scale%3A%0A%20%20%20%20higher%20values%20mean%20more%20potent%20compounds.%20%20We%20compare%20the%20test%20set%20distribution%0A%20%20%20%20with%20the%20training%20set%20to%20check%20whether%20the%20two%20sets%20probe%20the%20same%20activity%20range.%0A%0A%20%20%20%20A%20compound%20is%20typically%20classified%20as%20a%20**hit**%20when%3A%0A%20%20%20%20-%20pEC50%20%E2%89%A5%206%20(IC%E2%82%85%E2%82%80%20%E2%89%A4%201%20%C2%B5M)%2C%20**and**%0A%20%20%20%20-%20Emax%20above%20a%20baseline%20threshold%20(here%20Emax%20%3E%200%20in%20log2FC%20units%2C%20meaning%20the%0A%20%20%20%20%20%20compound%20causes%20at%20least%20a%20fold-change%20greater%20than%20the%20vehicle%20control).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(gaussian_kde%2C%20mo%2C%20np%2C%20pl%2C%20plt%2C%20unblinded%3A%20%22pl.DataFrame%22)%3A%0A%20%20%20%20%23%20Load%20training%20pEC50%20values%20for%20comparison%0A%20%20%20%20all_compounds%20%3D%20pl.read_csv(%22..%2Fdata%2Fprocessed%2Fall_compounds_activity_data.csv%22)%0A%0A%20%20%20%20train_pec50%20%3D%20(%0A%20%20%20%20%20%20%20%20all_compounds%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22in_dose_response%22)%20%26%20pl.col(%22pEC50_dr%22).is_not_null())%0A%20%20%20%20%20%20%20%20.get_column(%22pEC50_dr%22)%0A%20%20%20%20%20%20%20%20.to_numpy()%0A%20%20%20%20)%0A%20%20%20%20test_pec50%20%3D%20unblinded.get_column(%22pEC50%22).to_numpy()%0A%0A%20%20%20%20x_range%20%3D%20np.linspace(%0A%20%20%20%20%20%20%20%20min(train_pec50.min()%2C%20test_pec50.min())%20-%200.3%2C%0A%20%20%20%20%20%20%20%20max(train_pec50.max()%2C%20test_pec50.max())%20%2B%200.3%2C%0A%20%20%20%20%20%20%20%20400%2C%0A%20%20%20%20)%0A%0A%20%20%20%20kde_train%20%3D%20gaussian_kde(train_pec50%2C%20bw_method%3D%22scott%22)%0A%20%20%20%20kde_test%20%20%3D%20gaussian_kde(test_pec50%2C%20%20bw_method%3D%22scott%22)%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20fig_dist%2C%20ax_dist%20%3D%20plt.subplots(figsize%3D(7%2C%204.5)%2C%20dpi%3D150)%0A%20%20%20%20%20%20%20%20ax_dist.plot(x_range%2C%20kde_train(x_range)%2C%20color%3D%22%234e79a7%22%2C%20linewidth%3D2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3Df%22Training%20(n%3D%7Blen(train_pec50)%3A%2C%7D)%22)%0A%20%20%20%20%20%20%20%20ax_dist.fill_between(x_range%2C%20kde_train(x_range)%2C%20alpha%3D0.15%2C%20color%3D%22%234e79a7%22)%0A%20%20%20%20%20%20%20%20ax_dist.plot(x_range%2C%20kde_test(x_range)%2C%20color%3D%22%23e15759%22%2C%20linewidth%3D2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3Df%22Test%20%E2%80%94%20unblinded%20(n%3D%7Blen(test_pec50)%3A%2C%7D)%22)%0A%20%20%20%20%20%20%20%20ax_dist.fill_between(x_range%2C%20kde_test(x_range)%2C%20alpha%3D0.15%2C%20color%3D%22%23e15759%22)%0A%20%20%20%20%20%20%20%20ax_dist.axvline(6.0%2C%20color%3D%22black%22%2C%20linestyle%3D%22--%22%2C%20linewidth%3D1.2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%22Hit%20threshold%20(pEC50%20%3D%206)%22)%0A%20%20%20%20%20%20%20%20ax_dist.set_xlabel(%22pEC50%20%20%5B%E2%88%92log%E2%82%81%E2%82%80(M)%5D%22%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20ax_dist.set_ylabel(%22Density%22%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20ax_dist.set_title(%22pEC50%20distribution%20%E2%80%94%20training%20vs.%20unblinded%20test%20set%22%2C%20fontsize%3D13)%0A%20%20%20%20%20%20%20%20ax_dist.legend(fontsize%3D11%2C%20frameon%3DTrue%2C%20framealpha%3D0.9)%0A%20%20%20%20%20%20%20%20fig_dist.tight_layout()%0A%20%20%20%20%20%20%20%20fig_dist.savefig(%22..%2Fplots%2F5_unblinded_analysis%2Fpec50_distribution_train_test.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D300%2C%20bbox_inches%3D%22tight%22)%0A%0A%20%20%20%20mo.center(mo.as_html(fig_dist))%0A%20%20%20%20return%20(all_compounds%2C)%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Nearest-neighbour%20similarity%20to%20training%20set%20and%20activity%20cliffs%0A%0A%20%20%20%20Before%20evaluating%20model%20performance%2C%20we%20examine%20structural%20similarity%20between%20each%0A%20%20%20%20unblinded%20test%20compound%20and%20its%20closest%20analogue%20in%20the%20dose-response%20training%20set%2C%0A%20%20%20%20then%20ask%20whether%20structural%20similarity%20predicts%20activity%20similarity.%0A%0A%20%20%20%20An%20**activity%20cliff**%20is%20a%20pair%20of%20structurally%20similar%20compounds%20with%20substantially%0A%20%20%20%20different%20potency%20%E2%80%94%20they%20are%20notoriously%20difficult%20for%20ML%20models%20because%20a%20small%0A%20%20%20%20structural%20change%20produces%20a%20large%20activity%20change.%0A%0A%20%20%20%20Definitions%20used%20here%3A%0A%0A%20%20%20%20%7C%20Criterion%20%7C%20Threshold%20%7C%0A%20%20%20%20%7C---%7C---%7C%0A%20%20%20%20%7C%20Structurally%20similar%20%7C%20ECFP4%20Tanimoto%20%E2%89%A5%200.4%20%7C%0A%20%20%20%20%7C%20Activity%20cliff%20%7C%20Similar%20**and**%20%5C%7C%CE%94pEC50%5C%7C%20%E2%89%A5%201.0%20log%20unit%20%7C%0A%0A%20%20%20%20For%20each%20test%20compound%20we%20report%20its%20nearest%20neighbour%20(NN)%20in%20the%20training%20set%2C%0A%20%20%20%20its%20Tanimoto%20similarity%2C%20and%20the%20activity%20difference%20between%20the%20two.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Chem%2C%20all_compounds%2C%20np%2C%20pl%2C%20unblinded%3A%20%22pl.DataFrame%22)%3A%0A%20%20%20%20from%20rdkit.Chem%20import%20AllChem%0A%20%20%20%20from%20rdkit%20import%20DataStructs%20as%20_DataStructs%0A%0A%20%20%20%20%23%20Training%20set%20restricted%20to%20compounds%20with%20a%20measured%20pEC50.%0A%20%20%20%20_train_df%20%3D%20(%0A%20%20%20%20%20%20%20%20all_compounds%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22in_dose_response%22)%20%26%20pl.col(%22pEC50_dr%22).is_not_null())%0A%20%20%20%20%20%20%20%20.unique(subset%3D%5B%22inchikey%22%5D)%0A%20%20%20%20%20%20%20%20.select(%5B%22inchikey%22%2C%20%22smiles%22%2C%20%22molecule_names%22%2C%20%22pEC50_dr%22%5D)%0A%20%20%20%20)%0A%0A%20%20%20%20def%20_ecfp4(smi%3A%20str)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Return%20ECFP4%20BitVect%20(radius%202%2C%201024%20bits)%20for%20a%20SMILES%20string.%22%22%22%0A%20%20%20%20%20%20%20%20mol%20%3D%20Chem.MolFromSmiles(smi)%0A%20%20%20%20%20%20%20%20if%20mol%20is%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%20%20%20%20return%20AllChem.GetMorganFingerprintAsBitVect(mol%2C%20radius%3D2%2C%20nBits%3D1024)%0A%0A%20%20%20%20_train_fps%20%20%20%20%3D%20%5B_ecfp4(s)%20for%20s%20in%20_train_df%5B%22smiles%22%5D.to_list()%5D%0A%20%20%20%20_test_fps%20%20%20%20%20%3D%20%5B_ecfp4(s)%20for%20s%20in%20unblinded%5B%22smiles%22%5D.to_list()%5D%0A%20%20%20%20_train_ids%20%20%20%20%3D%20_train_df%5B%22inchikey%22%5D.to_list()%0A%20%20%20%20_train_smiles%20%3D%20_train_df%5B%22smiles%22%5D.to_list()%0A%20%20%20%20_train_names%20%20%3D%20_train_df%5B%22molecule_names%22%5D.to_list()%0A%20%20%20%20_train_pec50s%20%3D%20_train_df%5B%22pEC50_dr%22%5D.to_list()%0A%0A%20%20%20%20_max_sims%3A%20list%5Bfloat%5D%20%3D%20%5B%5D%0A%20%20%20%20_nn_idx%3A%20%20%20list%5Bint%5D%20%20%20%3D%20%5B%5D%0A%0A%20%20%20%20for%20_fp%20in%20_test_fps%3A%0A%20%20%20%20%20%20%20%20if%20_fp%20is%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_max_sims.append(float(%22nan%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20_nn_idx.append(0)%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%0A%20%20%20%20%20%20%20%20_sims%20%3D%20_DataStructs.BulkTanimotoSimilarity(_fp%2C%20_train_fps)%0A%20%20%20%20%20%20%20%20_best%20%3D%20int(np.argmax(_sims))%0A%20%20%20%20%20%20%20%20_max_sims.append(float(_sims%5B_best%5D))%0A%20%20%20%20%20%20%20%20_nn_idx.append(_best)%0A%0A%20%20%20%20%23%20Build%20the%20per-test-compound%20nearest-neighbour%20table.%0A%20%20%20%20nn_cliff_df%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20unblinded%0A%20%20%20%20%20%20%20%20.select(%5B%22Molecule%20Name%22%2C%20%22smiles%22%2C%20%22inchikey%22%2C%20%22pEC50%22%2C%20%22Emax%22%5D)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(%22nn_sim%22%2C%20%20%20%20%20%20%20%20%20_max_sims%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dtype%3Dpl.Float32)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(%22nn_inchikey%22%2C%20%20%20%20%5B_train_ids%5Bi%5D%20%20%20%20for%20i%20in%20_nn_idx%5D%2C%20%20%20%20%20%20%20%20dtype%3Dpl.Utf8)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(%22nn_smiles%22%2C%20%20%20%20%20%20%5B_train_smiles%5Bi%5D%20for%20i%20in%20_nn_idx%5D%2C%20%20%20%20%20%20%20%20dtype%3Dpl.Utf8)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(%22nn_name%22%2C%20%20%20%20%20%20%20%20%5B_train_names%5Bi%5D%20%20for%20i%20in%20_nn_idx%5D%2C%20%20%20%20%20%20%20%20dtype%3Dpl.Utf8)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(%22nn_pEC50_train%22%2C%20%5B_train_pec50s%5Bi%5D%20for%20i%20in%20_nn_idx%5D%2C%20%20%20%20%20%20%20%20dtype%3Dpl.Float64)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22pEC50%22)%20-%20pl.col(%22nn_pEC50_train%22)).alias(%22delta_pEC50%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22pEC50%22)%20-%20pl.col(%22nn_pEC50_train%22)).abs().alias(%22abs_delta_pEC50%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.when(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22nn_sim%22)%20%3E%3D%200.4)%20%26%20(pl.col(%22abs_delta_pEC50%22)%20%3E%3D%201.0)%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20.then(pl.lit(%22Activity%20cliff%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.when(pl.col(%22nn_sim%22)%20%3E%3D%200.4)%0A%20%20%20%20%20%20%20%20%20%20%20%20.then(pl.lit(%22Similar%20%2F%20concordant%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.otherwise(pl.lit(%22Dissimilar%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.alias(%22pair_class%22)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%0A%20%20%20%20nn_cliff_df%0A%20%20%20%20return%20(nn_cliff_df%2C)%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20all_predictions%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20nn_cliff_df%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20np%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20plt%2C%0A)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Static%20matplotlib%20scatter%20%E2%80%94%20coloured%20by%20ensemble%20residual%20error%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20_ens_errors%20%3D%20(%0A%20%20%20%20%20%20%20%20all_predictions%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22model_name%22)%20%3D%3D%20%22Ensemble%20(cp%C2%B7ch%C2%B7xg%C2%B7mc%C2%B7tf)%22)%0A%20%20%20%20%20%20%20%20.select(%5B%22Molecule%20Name%22%2C%20%22error%22%5D)%0A%20%20%20%20%20%20%20%20.rename(%7B%22error%22%3A%20%22ens_residual%22%7D)%0A%20%20%20%20)%0A%0A%20%20%20%20_cliff_with_ens%20%3D%20nn_cliff_df.join(%0A%20%20%20%20%20%20%20%20_ens_errors%2C%20on%3D%22Molecule%20Name%22%2C%20how%3D%22left%22%2C%0A%20%20%20%20)%0A%0A%20%20%20%20_residuals%20%3D%20_cliff_with_ens%5B%22ens_residual%22%5D.to_numpy()%0A%20%20%20%20_abs_max%20%3D%20max(abs(np.nanmin(_residuals))%2C%20abs(np.nanmax(_residuals)))%0A%0A%20%20%20%20%23%20Marker%20style%20per%20pair_class%20so%20structural%20context%20is%20preserved.%0A%20%20%20%20_marker_map%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22Activity%20cliff%22%3A%20%20%20%20%20%20%20(%22%5E%22%2C%2050%2C%201.0)%2C%0A%20%20%20%20%20%20%20%20%22Similar%20%2F%20concordant%22%3A%20(%22o%22%2C%2030%2C%200.80)%2C%0A%20%20%20%20%20%20%20%20%22Dissimilar%22%3A%20%20%20%20%20%20%20%20%20%20%20(%22s%22%2C%2025%2C%200.65)%2C%0A%20%20%20%20%7D%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20_fig_cliff%2C%20_ax_cliff%20%3D%20plt.subplots(figsize%3D(7.5%2C%205.5)%2C%20dpi%3D150)%0A%0A%20%20%20%20%20%20%20%20for%20_cls%2C%20(_mkr%2C%20_sz%2C%20_alpha)%20in%20_marker_map.items()%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_sub%20%3D%20_cliff_with_ens.filter(pl.col(%22pair_class%22)%20%3D%3D%20_cls)%0A%20%20%20%20%20%20%20%20%20%20%20%20_sc%20%3D%20_ax_cliff.scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_sub%5B%22nn_sim%22%5D.to_numpy()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_sub%5B%22abs_delta_pEC50%22%5D.to_numpy()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20c%3D_sub%5B%22ens_residual%22%5D.to_numpy()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20cmap%3D%22RdBu_r%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20vmin%3D-_abs_max%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20vmax%3D_abs_max%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20marker%3D_mkr%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20s%3D_sz%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alpha%3D_alpha%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20edgecolors%3D%22none%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D_cls%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20_cb%20%3D%20_fig_cliff.colorbar(_sc%2C%20ax%3D_ax_cliff%2C%20fraction%3D0.035%2C%20pad%3D0.02)%0A%20%20%20%20%20%20%20%20_cb.set_label(%22Ensemble%20residual%20%20(pred%20%E2%88%92%20true%20pEC50)%22%2C%20fontsize%3D10)%0A%0A%20%20%20%20%20%20%20%20_ax_cliff.axvline(0.4%2C%20color%3D%22%23555%22%2C%20linestyle%3D%22--%22%2C%20linewidth%3D1%2C%20alpha%3D0.7%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%22Similarity%20threshold%20(0.4)%22)%0A%20%20%20%20%20%20%20%20_ax_cliff.axhline(1.0%2C%20color%3D%22%23555%22%2C%20linestyle%3D%22%3A%22%2C%20%20linewidth%3D1%2C%20alpha%3D0.7%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%22%7C%CE%94pEC50%7C%20threshold%20(1.0)%22)%0A%0A%20%20%20%20%20%20%20%20_n_cliff_static%20%3D%20nn_cliff_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Activity%20cliff%22).shape%5B0%5D%0A%20%20%20%20%20%20%20%20_ax_cliff.set_xlabel(%22ECFP4%20Tanimoto%20similarity%20to%20nearest%20training%20neighbour%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_cliff.set_ylabel(%22%7C%CE%94pEC50%7C%20%20(test%20pEC50%20%E2%88%92%20nearest%20train%20pEC50)%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_cliff.set_title(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22Activity%20cliff%20analysis%20%20%E2%80%94%20%20%7B_n_cliff_static%7D%20cliff(s)%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Colour%20%3D%20ensemble%20residual%20(red%20%3D%20overpredicted%2C%20blue%20%3D%20underpredicted)%3B%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22markers%3A%20%E2%96%B3%20cliff%2C%20%E2%97%8B%20similar%2Fconcordant%2C%20%E2%96%A1%20dissimilar%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D10%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_ax_cliff.legend(fontsize%3D8%2C%20frameon%3DTrue%2C%20framealpha%3D0.9%2C%20loc%3D%22upper%20left%22)%0A%20%20%20%20%20%20%20%20_ax_cliff.set_xlim(-0.02%2C%201.02)%0A%20%20%20%20%20%20%20%20_fig_cliff.tight_layout()%0A%20%20%20%20%20%20%20%20_fig_cliff.savefig(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22..%2Fplots%2F5_unblinded_analysis%2Factivity_cliffs_scatter.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D300%2C%20bbox_inches%3D%22tight%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.center(mo.as_html(_fig_cliff))%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20nn_cliff_df%3A%20%22pl.DataFrame%22%2C%20pl)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Summary%20statistics%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20_n_total%20%20%20%3D%20nn_cliff_df.shape%5B0%5D%0A%20%20%20%20_n_similar%20%3D%20nn_cliff_df.filter(pl.col(%22nn_sim%22)%20%3E%3D%200.4).shape%5B0%5D%0A%20%20%20%20_n_cliffs%20%20%3D%20nn_cliff_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Activity%20cliff%22).shape%5B0%5D%0A%20%20%20%20_n_concord%20%3D%20nn_cliff_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Similar%20%2F%20concordant%22).shape%5B0%5D%0A%20%20%20%20_n_dissim%20%20%3D%20nn_cliff_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Dissimilar%22).shape%5B0%5D%0A%20%20%20%20_sim_mean%20%20%3D%20float(nn_cliff_df%5B%22nn_sim%22%5D.mean())%0A%20%20%20%20_sim_med%20%20%20%3D%20float(nn_cliff_df%5B%22nn_sim%22%5D.median())%0A%20%20%20%20_delt_mean%20%3D%20float(nn_cliff_df%5B%22abs_delta_pEC50%22%5D.mean())%0A%20%20%20%20_delt_med%20%20%3D%20float(nn_cliff_df%5B%22abs_delta_pEC50%22%5D.median())%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%23%23%23%20Activity%20cliff%20summary%0A%0A%20%20%20%20%7C%20Metric%20%7C%20Value%20%7C%0A%20%20%20%20%7C---%7C---%7C%0A%20%20%20%20%7C%20Test%20compounds%20analysed%20%7C%20%7B_n_total%7D%20%7C%0A%20%20%20%20%7C%20Similar%20pairs%20(Tanimoto%20%E2%89%A5%200.4)%20%7C%20%7B_n_similar%7D%20(%7B100*_n_similar%2F_n_total%3A.1f%7D%25)%20%7C%0A%20%20%20%20%7C%20**Activity%20cliffs**%20(similar%20%2B%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20%E2%89%A5%201.0)%20%7C%20**%7B_n_cliffs%7D%20(%7B100*_n_cliffs%2F_n_total%3A.1f%7D%25)**%20%7C%0A%20%20%20%20%7C%20Similar%20%2F%20concordant%20%7C%20%7B_n_concord%7D%20(%7B100*_n_concord%2F_n_total%3A.1f%7D%25)%20%7C%0A%20%20%20%20%7C%20Dissimilar%20(Tanimoto%20%3C%200.4)%20%7C%20%7B_n_dissim%7D%20(%7B100*_n_dissim%2F_n_total%3A.1f%7D%25)%20%7C%0A%20%20%20%20%7C%20Mean%20Tanimoto%20sim%20to%20NN%20%7C%20%7B_sim_mean%3A.3f%7D%20%7C%0A%20%20%20%20%7C%20Median%20Tanimoto%20sim%20to%20NN%20%7C%20%7B_sim_med%3A.3f%7D%20%7C%0A%20%20%20%20%7C%20Mean%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20(all%20pairs)%20%7C%20%7B_delt_mean%3A.3f%7D%20%7C%0A%20%20%20%20%7C%20Median%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20(all%20pairs)%20%7C%20%7B_delt_med%3A.3f%7D%20%7C%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Interactive%20browser%20%E2%80%94%20test%20compound%20and%20nearest%20training%20neighbour%0A%0A%20%20%20%20Hover%20over%20a%20point%20in%20the%20scatter%20to%20display%20both%20compound%20structures%20and%0A%20%20%20%20their%20activity%20data%20side%20by%20side.%20%20Red%20points%20(activity%20cliffs)%20are%20the%0A%20%20%20%20most%20informative%3A%20structurally%20similar%20to%20a%20training%20compound%20but%20with%0A%20%20%20%20substantially%20different%20potency%20%E2%80%94%20a%20direct%20challenge%20for%20ML%20models.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(alt%2C%20mo%2C%20nn_cliff_df%3A%20%22pl.DataFrame%22)%3A%0A%20%20%20%20_cliff_color_scale%20%3D%20alt.Scale(%0A%20%20%20%20%20%20%20%20domain%3D%5B%22Activity%20cliff%22%2C%20%22Similar%20%2F%20concordant%22%2C%20%22Dissimilar%22%5D%2C%0A%20%20%20%20%20%20%20%20range%3D%5B%22%23e15759%22%2C%20%22%2376b7b2%22%2C%20%22%23b0b0b0%22%5D%2C%0A%20%20%20%20)%0A%0A%20%20%20%20_cliff_sel%20%3D%20alt.selection_point(%0A%20%20%20%20%20%20%20%20fields%3D%5B%22Molecule%20Name%22%5D%2C%20name%3D%22cliff_sel%22%2C%0A%20%20%20%20%20%20%20%20empty%3DFalse%2C%20on%3D%22mouseover%22%2C%20nearest%3DTrue%2C%20clear%3D%22mouseout%22%2C%0A%20%20%20%20)%0A%0A%20%20%20%20_cliff_scatter_int%20%3D%20(%0A%20%20%20%20%20%20%20%20alt.Chart(nn_cliff_df)%0A%20%20%20%20%20%20%20%20.mark_circle(opacity%3D0.85)%0A%20%20%20%20%20%20%20%20.encode(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dalt.X(%22nn_sim%3AQ%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%3D%22Tanimoto%20similarity%20to%20nearest%20training%20compound%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20scale%3Dalt.Scale(domain%3D%5B0%2C%201%5D)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20axis%3Dalt.Axis(titleFontSize%3D11))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dalt.Y(%22abs_delta_pEC50%3AQ%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%3D%22%7C%CE%94pEC50%7C%20%20(test%20%E2%88%92%20nearest%20train)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20axis%3Dalt.Axis(titleFontSize%3D11))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3Dalt.condition(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_cliff_sel%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.value(%22%23f5c518%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Color(%22pair_class%3AN%22%2C%20scale%3D_cliff_color_scale%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20legend%3Dalt.Legend(title%3D%22Pair%20class%22))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20size%3Dalt.condition(_cliff_sel%2C%20alt.value(140)%2C%20alt.value(60))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20tooltip%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22Molecule%20Name%3AN%22%2C%20%20%20title%3D%22Test%20compound%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22pEC50%3AQ%22%2C%20%20%20%20%20%20%20%20%20%20%20title%3D%22Test%20pEC50%22%2C%20%20%20%20%20format%3D%22.3f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22nn_name%3AN%22%2C%20%20%20%20%20%20%20%20%20title%3D%22NN%20train%20cmpd%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22nn_pEC50_train%3AQ%22%2C%20%20title%3D%22Train%20NN%20pEC50%22%2C%20format%3D%22.3f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22nn_sim%3AQ%22%2C%20%20%20%20%20%20%20%20%20%20title%3D%22Tanimoto%22%2C%20%20%20%20%20%20%20format%3D%22.3f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22abs_delta_pEC50%3AQ%22%2C%20title%3D%22%7C%CE%94pEC50%7C%22%2C%20%20%20%20%20%20%20format%3D%22.3f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22pair_class%3AN%22%2C%20%20%20%20%20%20title%3D%22Class%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.add_params(_cliff_sel)%0A%20%20%20%20%20%20%20%20.properties(title%3D%22Hover%20to%20inspect%20compound%20pair%22%2C%20width%3D470%2C%20height%3D370)%0A%20%20%20%20%20%20%20%20.configure_title(fontSize%3D12)%0A%20%20%20%20)%0A%0A%20%20%20%20nn_cliff_chart%20%3D%20mo.ui.altair_chart(_cliff_scatter_int)%0A%20%20%20%20return%20(nn_cliff_chart%2C)%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20Chem%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20nn_cliff_chart%2C%0A%20%20%20%20nn_cliff_df%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20rdDepictor%2C%0A%20%20%20%20rdMolDraw2D%2C%0A)%3A%0A%20%20%20%20_PW%2C%20_PH%20%3D%20250%2C%20175%0A%0A%20%20%20%20def%20_svg_mol(smi%3A%20str)%20-%3E%20str%3A%0A%20%20%20%20%20%20%20%20%22%22%22Render%20SMILES%20as%20inline%20SVG%20stripped%20of%20the%20XML%20declaration.%22%22%22%0A%20%20%20%20%20%20%20%20mol%20%3D%20Chem.MolFromSmiles(smi)%0A%20%20%20%20%20%20%20%20if%20mol%20is%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22%22%0A%20%20%20%20%20%20%20%20rdDepictor.Compute2DCoords(mol)%0A%20%20%20%20%20%20%20%20drawer%20%3D%20rdMolDraw2D.MolDraw2DSVG(_PW%2C%20_PH)%0A%20%20%20%20%20%20%20%20drawer.DrawMolecule(mol)%0A%20%20%20%20%20%20%20%20drawer.FinishDrawing()%0A%20%20%20%20%20%20%20%20svg%20%3D%20drawer.GetDrawingText()%0A%20%20%20%20%20%20%20%20return%20svg.split(%22%3F%3E%22%2C%201)%5B-1%5D.strip()%20if%20%22%3F%3E%22%20in%20svg%20else%20svg%0A%0A%20%20%20%20_cliff_sel_rows%20%3D%20nn_cliff_chart.value%0A%0A%20%20%20%20if%20_cliff_sel_rows%20is%20None%20or%20len(_cliff_sel_rows)%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20_cliff_panel%20%3D%20mo.Html(f%22%22%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cdiv%20style%3D'width%3A%7B_PW%7Dpx%3B%20height%3A%7B(_PH%20%2B%2060)%20*%202%7Dpx%3B%20display%3Aflex%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20align-items%3Acenter%3B%20justify-content%3Acenter%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20color%3Agrey%3B%20font-size%3A13px%3B%20border%3A1px%20dashed%20%23ccc%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20border-radius%3A6px%3B%20text-align%3Acenter%3B%20padding%3A12px'%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Hover%20over%20a%20point%20to%20see%20the%3Cbr%3Etest%20compound%20and%20its%20nearest%3Cbr%3Etraining%20neighbour%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%20%20%20%20%22%22%22)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20_ckey%20%3D%20_cliff_sel_rows.row(0%2C%20named%3DTrue)%5B%22Molecule%20Name%22%5D%0A%20%20%20%20%20%20%20%20_cr%20%20%20%3D%20nn_cliff_df.filter(pl.col(%22Molecule%20Name%22)%20%3D%3D%20_ckey).row(0%2C%20named%3DTrue)%0A%20%20%20%20%20%20%20%20_cbg%20%20%3D%20%22%23fde8e8%22%20if%20_cr%5B%22pair_class%22%5D%20%3D%3D%20%22Activity%20cliff%22%20else%20%22%23eef4fb%22%0A%20%20%20%20%20%20%20%20_csign%20%3D%20%22%2B%22%20if%20_cr%5B%22delta_pEC50%22%5D%20%3E%3D%200%20else%20%22%22%0A%20%20%20%20%20%20%20%20_cliff_panel%20%3D%20mo.Html(f%22%22%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cdiv%20style%3D'width%3A%7B_PW%7Dpx%3B%20font-family%3Amonospace%3B%20font-size%3A10px'%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cdiv%20style%3D'padding%3A5px%3B%20background%3A%7B_cbg%7D%3B%20border-radius%3A4px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text-align%3Acenter%3B%20margin-bottom%3A3px%3B%20line-height%3A1.6'%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cb%3ETest%20compound%3C%2Fb%3E%3Cbr%3E%7B_cr%5B'Molecule%20Name'%5D%7D%3Cbr%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pEC50%20%3D%20%3Cb%3E%7B_cr%5B'pEC50'%5D%3A.3f%7D%3C%2Fb%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B_svg_mol(_cr%5B'smiles'%5D)%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cdiv%20style%3D'padding%3A5px%3B%20background%3A%23f0faf0%3B%20border-radius%3A4px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20text-align%3Acenter%3B%20margin-top%3A6px%3B%20margin-bottom%3A3px%3B%20line-height%3A1.6'%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cb%3ENearest%20train%20compound%3C%2Fb%3E%3Cbr%3E%7B_cr%5B'nn_name'%5D%20or%20_cr%5B'nn_inchikey'%5D%7D%3Cbr%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pEC50%20%3D%20%3Cb%3E%7B_cr%5B'nn_pEC50_train'%5D%3A.3f%7D%3C%2Fb%3E%20%26nbsp%3B%C2%B7%26nbsp%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Tanimoto%20%3D%20%3Cb%3E%7B_cr%5B'nn_sim'%5D%3A.3f%7D%3C%2Fb%3E%3Cbr%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%CE%94pEC50%20%3D%20%7B_csign%7D%7B_cr%5B'delta_pEC50'%5D%3A.3f%7D%20%26nbsp%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cb%20style%3D'color%3A%7B%22%23e15759%22%20if%20_cr%5B%22pair_class%22%5D%20%3D%3D%20%22Activity%20cliff%22%20else%20%22%23555%22%7D'%3E%7B_cr%5B'pair_class'%5D%7D%3C%2Fb%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B_svg_mol(_cr%5B'nn_smiles'%5D)%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%20%20%20%20%22%22%22)%0A%0A%20%20%20%20mo.hstack(%5Bnn_cliff_chart%2C%20_cliff_panel%5D%2C%20align%3D%22start%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Matched%20molecular%20pair%20(MMP)%20analysis%20%E2%80%94%20training%20vs.%20unblinded%20test%20set%0A%0A%20%20%20%20Matched%20molecular%20pairs%20are%20compound%20pairs%20that%20differ%20by%20exactly%20one%20structural%0A%20%20%20%20transformation%20at%20a%20single%20site%20while%20sharing%20a%20common%20scaffold%20(the%20**constant**%0A%20%20%20%20fragment).%20%20Because%20the%20change%20is%20chemically%20precise%2C%20MMP%20analysis%20lets%20us%0A%20%20%20%20attribute%20an%20activity%20difference%20directly%20to%20a%20specific%20structural%20modification.%0A%0A%20%20%20%20The%20MMP%20database%20was%20pre-generated%20across%20the%20full%20compound%20collection.%20%20Here%20we%0A%20%20%20%20focus%20only%20on%20**cross-set%20pairs**%3A%20one%20compound%20from%20the%20dose-response%20training%20set%0A%20%20%20%20and%20one%20from%20the%20unblinded%20test%20set.%20%20This%20directly%20reveals%20how%20often%20structural%0A%20%20%20%20similarity%20(at%20the%20MMP%20level)%20corresponds%20to%20activity%20similarity%20%E2%80%94%20and%2C%20crucially%2C%0A%20%20%20%20how%20often%20it%20does%20not%20(activity%20cliffs).%0A%0A%20%20%20%20Activity%20cliff%20criterion%3A%20**%7C%CE%94pEC50%7C%20%E2%89%A5%201.0**%20log%20unit%20between%20the%20two%20MMP%20partners.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(all_compounds%2C%20pl%2C%20unblinded%3A%20%22pl.DataFrame%22)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Load%20MMP%20database%20and%20orient%20cross-set%20pairs%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20_mmp%20%3D%20pl.read_csv(%0A%20%20%20%20%20%20%20%20%22..%2Fdata%2Fprocessed%2Fall_compounds_mmp.mmp.csv.gz%22%2C%0A%20%20%20%20%20%20%20%20separator%3D%22%5Ct%22%2C%0A%20%20%20%20%20%20%20%20has_header%3DFalse%2C%0A%20%20%20%20%20%20%20%20new_columns%3D%5B%22smiles1%22%2C%20%22smiles2%22%2C%20%22inchikey1%22%2C%20%22inchikey2%22%2C%20%22transformation%22%2C%20%22constant%22%5D%2C%0A%20%20%20%20)%0A%0A%20%20%20%20_train_act%20%3D%20(%0A%20%20%20%20%20%20%20%20all_compounds%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22in_dose_response%22)%20%26%20pl.col(%22pEC50_dr%22).is_not_null())%0A%20%20%20%20%20%20%20%20.unique(%22inchikey%22)%0A%20%20%20%20%20%20%20%20.select(%5B%22inchikey%22%2C%20%22smiles%22%2C%20%22molecule_names%22%2C%20%22pEC50_dr%22%5D)%0A%20%20%20%20)%0A%20%20%20%20_test_act%20%3D%20unblinded.select(%5B%22Molecule%20Name%22%2C%20%22smiles%22%2C%20%22inchikey%22%2C%20%22pEC50%22%5D)%0A%0A%20%20%20%20_train_iks%20%3D%20set(_train_act%5B%22inchikey%22%5D.to_list())%0A%20%20%20%20_test_iks%20%20%3D%20set(_test_act%5B%22inchikey%22%5D.to_list())%0A%0A%20%20%20%20%23%20Orient%20so%20ik_train%20is%20always%20the%20training%20compound%2C%20ik_test%20the%20test%20compound.%0A%20%20%20%20_a%20%3D%20(%0A%20%20%20%20%20%20%20%20_mmp%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22inchikey1%22).is_in(_train_iks)%20%26%20pl.col(%22inchikey2%22).is_in(_test_iks))%0A%20%20%20%20%20%20%20%20.select(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22inchikey1%22).alias(%22ik_train%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22inchikey2%22).alias(%22ik_test%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22transformation%22%2C%20%22constant%22%2C%0A%20%20%20%20%20%20%20%20%5D)%0A%20%20%20%20)%0A%20%20%20%20_b%20%3D%20(%0A%20%20%20%20%20%20%20%20_mmp%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22inchikey2%22).is_in(_train_iks)%20%26%20pl.col(%22inchikey1%22).is_in(_test_iks))%0A%20%20%20%20%20%20%20%20.select(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22inchikey2%22).alias(%22ik_train%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22inchikey1%22).alias(%22ik_test%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22transformation%22%2C%20%22constant%22%2C%0A%20%20%20%20%20%20%20%20%5D)%0A%20%20%20%20)%0A%0A%20%20%20%20mmp_cross_df%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20pl.concat(%5B_a%2C%20_b%5D)%0A%20%20%20%20%20%20%20%20.join(%0A%20%20%20%20%20%20%20%20%20%20%20%20_train_act.rename(%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22inchikey%22%3A%20%20%20%20%20%20%20%22ik_train%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22smiles%22%3A%20%20%20%20%20%20%20%20%20%22smiles_train%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22molecule_names%22%3A%20%22name_train%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50_dr%22%3A%20%20%20%20%20%20%20%22pEC50_train%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20on%3D%22ik_train%22%2C%20how%3D%22left%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.join(%0A%20%20%20%20%20%20%20%20%20%20%20%20_test_act.rename(%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22inchikey%22%3A%20%20%20%20%20%20%22ik_test%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22smiles%22%3A%20%20%20%20%20%20%20%20%22smiles_test%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22Molecule%20Name%22%3A%20%22name_test%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22pEC50%22%3A%20%20%20%20%20%20%20%20%20%22pEC50_test%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20on%3D%22ik_test%22%2C%20how%3D%22left%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22pEC50_train%22).is_not_null()%20%26%20pl.col(%22pEC50_test%22).is_not_null())%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22pEC50_test%22)%20-%20pl.col(%22pEC50_train%22)).alias(%22delta_pEC50%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22pEC50_test%22)%20-%20pl.col(%22pEC50_train%22)).abs().alias(%22abs_delta_pEC50%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.when(pl.col(%22abs_delta_pEC50%22)%20%3E%3D%201.0)%0A%20%20%20%20%20%20%20%20%20%20%20%20.then(pl.lit(%22Activity%20cliff%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.otherwise(pl.lit(%22Concordant%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.alias(%22pair_class%22)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Cluster-level%20summary%3A%20one%20row%20per%20unique%20scaffold%20constant.%0A%20%20%20%20mmp_cluster_df%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20mmp_cross_df%0A%20%20%20%20%20%20%20%20.group_by(%22constant%22)%0A%20%20%20%20%20%20%20%20.agg(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.len().alias(%22n_pairs%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_delta_pEC50%22).max().alias(%22max_abs_delta%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_delta_pEC50%22).mean().alias(%22mean_abs_delta%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_delta_pEC50%22).median().alias(%22median_abs_delta%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22abs_delta_pEC50%22)%20%3E%3D%201.0).sum().alias(%22n_cliffs%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22ik_test%22).n_unique().alias(%22n_test_cmpds%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22ik_train%22).n_unique().alias(%22n_train_cmpds%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22n_cliffs%22)%20%2F%20pl.col(%22n_pairs%22)%20*%20100).round(1).alias(%22pct_cliffs%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Readable%20label%3A%20constant%20fragment%20truncated%20to%2040%20chars.%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22constant%22).str.slice(0%2C%2040).alias(%22constant_label%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.sort(%22max_abs_delta%22%2C%20descending%3DTrue)%0A%20%20%20%20)%0A%0A%20%20%20%20print(f%22Cross-set%20MMPs%3A%20%7Bmmp_cross_df.shape%5B0%5D%7D%22)%0A%20%20%20%20print(f%22Activity%20cliffs%3A%20%7Bmmp_cross_df.filter(pl.col('pair_class')%3D%3D'Activity%20cliff').shape%5B0%5D%7D%22)%0A%20%20%20%20print(f%22Unique%20scaffold%20clusters%3A%20%7Bmmp_cluster_df.shape%5B0%5D%7D%22)%0A%20%20%20%20print(f%22Clusters%20with%20%E2%89%A51%20cliff%3A%20%7Bmmp_cluster_df.filter(pl.col('n_cliffs')%3E%3D1).shape%5B0%5D%7D%22)%0A%20%20%20%20mmp_cross_df%0A%20%20%20%20return%20mmp_cluster_df%2C%20mmp_cross_df%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20gaussian_kde%2C%0A%20%20%20%20mmp_cluster_df%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20mmp_cross_df%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20np%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20plt%2C%0A)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Two-panel%20overview%20figure%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20_cliff%20%20%20%3D%20mmp_cross_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Activity%20cliff%22)%5B%22abs_delta_pEC50%22%5D.to_numpy()%0A%20%20%20%20_concord%20%3D%20mmp_cross_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Concordant%22)%5B%22abs_delta_pEC50%22%5D.to_numpy()%0A%20%20%20%20_all%20%20%20%20%20%3D%20mmp_cross_df%5B%22abs_delta_pEC50%22%5D.to_numpy()%0A%0A%20%20%20%20%23%20Top%2020%20clusters%20for%20the%20bar%20panel%20(sorted%20by%20max%20%7C%CE%94pEC50%7C).%0A%20%20%20%20_top20%20%3D%20mmp_cluster_df.head(20)%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20_fig_mmp%2C%20(_ax_kde%2C%20_ax_bar)%20%3D%20plt.subplots(%0A%20%20%20%20%20%20%20%20%20%20%20%201%2C%202%2C%20figsize%3D(13%2C%205)%2C%20dpi%3D150%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20gridspec_kw%3D%7B%22width_ratios%22%3A%20%5B1%2C%201.4%5D%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%23%20%E2%94%80%E2%94%80%20Left%20panel%3A%20KDE%20of%20%7C%CE%94pEC50%7C%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20%20%20%20%20_x%20%3D%20np.linspace(0%2C%20_all.max()%20%2B%200.3%2C%20400)%0A%20%20%20%20%20%20%20%20_kde_all%20%20%20%3D%20gaussian_kde(_all%2C%20%20%20%20bw_method%3D%22scott%22)%0A%20%20%20%20%20%20%20%20_ax_kde.plot(_x%2C%20_kde_all(_x)%2C%20%20%20%20color%3D%22%23555%22%2C%20linewidth%3D2%2C%20label%3Df%22All%20MMPs%20(n%3D%7Blen(_all)%7D)%22)%0A%20%20%20%20%20%20%20%20_ax_kde.fill_between(_x%2C%20_kde_all(_x)%2C%20alpha%3D0.10%2C%20color%3D%22%23555%22)%0A%0A%20%20%20%20%20%20%20%20if%20len(_cliff)%20%3E%3D%202%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_kde_cliff%20%3D%20gaussian_kde(_cliff%2C%20bw_method%3D%22scott%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax_kde.plot(_x%2C%20_kde_cliff(_x)%2C%20color%3D%22%23e15759%22%2C%20linewidth%3D2%2C%20label%3Df%22Cliffs%20(n%3D%7Blen(_cliff)%7D)%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax_kde.fill_between(_x%2C%20_kde_cliff(_x)%2C%20alpha%3D0.15%2C%20color%3D%22%23e15759%22)%0A%0A%20%20%20%20%20%20%20%20if%20len(_concord)%20%3E%3D%202%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_kde_con%20%3D%20gaussian_kde(_concord%2C%20bw_method%3D%22scott%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax_kde.plot(_x%2C%20_kde_con(_x)%2C%20color%3D%22%2376b7b2%22%2C%20linewidth%3D2%2C%20label%3Df%22Concordant%20(n%3D%7Blen(_concord)%7D)%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax_kde.fill_between(_x%2C%20_kde_con(_x)%2C%20alpha%3D0.15%2C%20color%3D%22%2376b7b2%22)%0A%0A%20%20%20%20%20%20%20%20_ax_kde.axvline(1.0%2C%20color%3D%22black%22%2C%20linestyle%3D%22--%22%2C%20linewidth%3D1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%22Cliff%20threshold%20(1.0)%22)%0A%20%20%20%20%20%20%20%20_ax_kde.set_xlabel(%22%7C%CE%94pEC50%7C%20%20(test%20%E2%88%92%20train)%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_kde.set_ylabel(%22Density%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_kde.set_xlim(0%2C%20_all.max()%20%2B%200.2)%0A%20%20%20%20%20%20%20%20_ax_kde.legend(fontsize%3D9%2C%20frameon%3DTrue)%0A%20%20%20%20%20%20%20%20_ax_kde.set_title(%22%7C%CE%94pEC50%7C%20distribution%20%E2%80%94%20all%20cross-set%20MMPs%22%2C%20fontsize%3D11)%0A%0A%20%20%20%20%20%20%20%20%23%20%E2%94%80%E2%94%80%20Right%20panel%3A%20top%2020%20clusters%2C%20bar%20%3D%20max%20%7C%CE%94pEC50%7C%2C%20coloured%20by%20pct_cliffs%20%E2%94%80%E2%94%80%0A%20%20%20%20%20%20%20%20_cluster_labels%20%3D%20%5Bf%22C%7Bi%2B1%7D%22%20for%20i%20in%20range(len(_top20))%5D%0A%20%20%20%20%20%20%20%20_max_deltas%20%20%20%20%20%3D%20_top20%5B%22max_abs_delta%22%5D.to_numpy()%0A%20%20%20%20%20%20%20%20_pct_cliffs%20%20%20%20%20%3D%20_top20%5B%22pct_cliffs%22%5D.to_numpy()%0A%0A%20%20%20%20%20%20%20%20_cmap%20%20%20%3D%20plt.cm.RdYlGn_r%0A%20%20%20%20%20%20%20%20_norm%20%20%20%3D%20plt.Normalize(vmin%3D0%2C%20vmax%3D100)%0A%20%20%20%20%20%20%20%20_colors%20%3D%20%5B_cmap(_norm(v))%20for%20v%20in%20_pct_cliffs%5D%0A%0A%20%20%20%20%20%20%20%20_bars%20%3D%20_ax_bar.barh(%0A%20%20%20%20%20%20%20%20%20%20%20%20_cluster_labels%5B%3A%3A-1%5D%2C%20_max_deltas%5B%3A%3A-1%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3D_colors%5B%3A%3A-1%5D%2C%20edgecolor%3D%22white%22%2C%20linewidth%3D0.5%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_ax_bar.axvline(1.0%2C%20color%3D%22black%22%2C%20linestyle%3D%22--%22%2C%20linewidth%3D1%2C%20alpha%3D0.7)%0A%0A%20%20%20%20%20%20%20%20%23%20Annotate%20bars%20with%20n_pairs%20and%20cliff%20counts.%0A%20%20%20%20%20%20%20%20for%20_i%2C%20(_bar%2C%20_row)%20in%20enumerate(zip(_bars%2C%20list(_top20.iter_rows(named%3DTrue))%5B%3A%3A-1%5D))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax_bar.text(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_bar.get_width()%20%2B%200.05%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_bar.get_y()%20%2B%20_bar.get_height()%20%2F%202%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22n%3D%7B_row%5B'n_pairs'%5D%7D%20%7C%20%7B_row%5B'n_cliffs'%5D%7D%20cliffs%20(%7B_row%5B'pct_cliffs'%5D%3A.0f%7D%25)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20va%3D%22center%22%2C%20fontsize%3D7.5%2C%20color%3D%22%23333%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20_sm%20%3D%20plt.cm.ScalarMappable(cmap%3D_cmap%2C%20norm%3D_norm)%0A%20%20%20%20%20%20%20%20_sm.set_array(%5B%5D)%0A%20%20%20%20%20%20%20%20_cbar%20%3D%20_fig_mmp.colorbar(_sm%2C%20ax%3D_ax_bar%2C%20fraction%3D0.03%2C%20pad%3D0.02)%0A%20%20%20%20%20%20%20%20_cbar.set_label(%22%25%20pairs%20that%20are%20cliffs%22%2C%20fontsize%3D9)%0A%0A%20%20%20%20%20%20%20%20_ax_bar.set_xlabel(%22Max%20%7C%CE%94pEC50%7C%20in%20cluster%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_bar.set_title(%22Top%2020%20MMP%20scaffold%20clusters%5Cn(sorted%20by%20max%20%7C%CE%94pEC50%7C)%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_bar.set_xlim(0%2C%20_max_deltas.max()%20%2B%201.2)%0A%0A%20%20%20%20%20%20%20%20_fig_mmp.tight_layout()%0A%20%20%20%20%20%20%20%20_fig_mmp.savefig(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22..%2Fplots%2F5_unblinded_analysis%2Fmmp_activity_cliffs.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D300%2C%20bbox_inches%3D%22tight%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.center(mo.as_html(_fig_mmp))%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mmp_cluster_df%3A%20%22pl.DataFrame%22%2C%20mmp_cross_df%3A%20%22pl.DataFrame%22%2C%20mo%2C%20pl)%3A%0A%20%20%20%20_n_total%20%20%20%3D%20mmp_cross_df.shape%5B0%5D%0A%20%20%20%20_n_cliffs%20%20%3D%20mmp_cross_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Activity%20cliff%22).shape%5B0%5D%0A%20%20%20%20_n_concord%20%3D%20mmp_cross_df.filter(pl.col(%22pair_class%22)%20%3D%3D%20%22Concordant%22).shape%5B0%5D%0A%20%20%20%20_n_clusters%20%20%20%20%20%20%20%3D%20mmp_cluster_df.shape%5B0%5D%0A%20%20%20%20_n_cliff_clusters%20%3D%20mmp_cluster_df.filter(pl.col(%22n_cliffs%22)%20%3E%3D%201).shape%5B0%5D%0A%0A%20%20%20%20_q25%20%3D%20float(mmp_cross_df%5B%22abs_delta_pEC50%22%5D.quantile(0.25))%0A%20%20%20%20_q50%20%3D%20float(mmp_cross_df%5B%22abs_delta_pEC50%22%5D.quantile(0.50))%0A%20%20%20%20_q75%20%3D%20float(mmp_cross_df%5B%22abs_delta_pEC50%22%5D.quantile(0.75))%0A%20%20%20%20_q90%20%3D%20float(mmp_cross_df%5B%22abs_delta_pEC50%22%5D.quantile(0.90))%0A%20%20%20%20_q95%20%3D%20float(mmp_cross_df%5B%22abs_delta_pEC50%22%5D.quantile(0.95))%0A%20%20%20%20_max_delta%20%3D%20float(mmp_cross_df%5B%22abs_delta_pEC50%22%5D.max())%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%23%23%23%20MMP%20summary%0A%0A%20%20%20%20%7C%20Metric%20%7C%20Value%20%7C%0A%20%20%20%20%7C---%7C---%7C%0A%20%20%20%20%7C%20Total%20cross-set%20MMP%20pairs%20%7C%20%7B_n_total%7D%20%7C%0A%20%20%20%20%7C%20**Activity%20cliffs**%20(%5C%5C%7C%CE%94pEC50%5C%5C%7C%20%E2%89%A5%201.0)%20%7C%20**%7B_n_cliffs%7D%20(%7B100*_n_cliffs%2F_n_total%3A.1f%7D%25)**%20%7C%0A%20%20%20%20%7C%20Concordant%20pairs%20%7C%20%7B_n_concord%7D%20(%7B100*_n_concord%2F_n_total%3A.1f%7D%25)%20%7C%0A%20%20%20%20%7C%20Unique%20scaffold%20clusters%20%7C%20%7B_n_clusters%7D%20%7C%0A%20%20%20%20%7C%20Clusters%20with%20%E2%89%A5%201%20cliff%20%7C%20%7B_n_cliff_clusters%7D%20(%7B100*_n_cliff_clusters%2F_n_clusters%3A.1f%7D%25)%20%7C%0A%20%20%20%20%7C%20Median%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20%7C%20%7B_q50%3A.2f%7D%20%7C%0A%20%20%20%20%7C%2075th%20percentile%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20%7C%20%7B_q75%3A.2f%7D%20%7C%0A%20%20%20%20%7C%2090th%20percentile%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20%7C%20%7B_q90%3A.2f%7D%20%7C%0A%20%20%20%20%7C%2095th%20percentile%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20%7C%20%7B_q95%3A.2f%7D%20%7C%0A%20%20%20%20%7C%20Maximum%20%5C%5C%7C%CE%94pEC50%5C%5C%7C%20%7C%20%7B_max_delta%3A.2f%7D%20%7C%0A%0A%20%20%20%20**Interpretation%3A**%20%7B100*_n_cliffs%2F_n_total%3A.0f%7D%25%20of%20MMP%20pairs%20are%20activity%20cliffs%20%E2%80%94%20the%20structural%0A%20%20%20%20change%20between%20each%20MMP%20partner%20leads%20to%20a%20greater%20than%201%20log-unit%20shift%20in%0A%20%20%20%20potency.%20%20This%20is%20a%20very%20high%20cliff%20rate%20and%20suggests%20the%20test%20compounds%20probe%0A%20%20%20%20activity-sensitive%20structural%20space%20that%20the%20training%20data%20does%20not%20resolve%20well.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Model%20evaluation%0A%0A%20%20%20%20%23%23%23%20Load%20submissions%0A%0A%20%20%20%20Each%20ensemble%20component%20is%20sourced%20from%20the%20best%20available%20file.%0A%20%20%20%20Original%20submission%20CSVs%20are%20preferred%3B%20where%20none%20exists%20the%20predictions%0A%20%20%20%20regenerated%20by%20%60scripts%2F5_regenerate_predictions.py%60%20are%20used%20instead.%0A%0A%20%20%20%20%7C%20File%20%7C%20Source%20%7C%20Role%20%7C%20Ensemble%20weight%20%7C%0A%20%20%20%20%7C------%7C--------%7C------%7C-----------------%7C%0A%20%20%20%20%7C%20%60submissions%2F4_ens_cp5_ch5_rf0_xg13_mc1_tf5_submission.csv%60%20%7C%20original%20%7C%20**Ensemble**%20%7C%20%E2%80%94%20%7C%0A%20%20%20%20%7C%20%60submissions%2F4_chemeleon_hpo_submission.csv%60%20%7C%20original%20%7C%20CheMeleon%20HPO%20(%60ch%60)%20%7C%205%20%7C%0A%20%20%20%20%7C%20%60submissions%2F4_tabpfn_chemeleon_submission.csv%60%20%7C%20original%20%7C%20TabPFN%20%2F%20CheMeleon%20(%60tf%60)%20%7C%205%20%7C%0A%20%20%20%20%7C%20%60submissions%2F4_macau_che_hpo_submission.csv%60%20%7C%20original%20%7C%20Macau%20HPO%20%2F%20CheMeleon%20(%60mc%60)%20%7C%201%20%7C%0A%20%20%20%20%7C%20%60predictions%2F5_regen_cp_preds.csv%60%20%7C%20regenerated%20%7C%20Chemprop%20HPO%20(%60cp%60)%20%7C%205%20%7C%0A%20%20%20%20%7C%20%60predictions%2F5_regen_xg_preds.csv%60%20%7C%20regenerated%20%7C%20XGBoost%20%2F%20Mordred%20(%60xg%60)%20%7C%20%E2%85%93%20%7C%0A%0A%20%20%20%20Macau%20and%20TabPFN%20are%20deterministic%20(seeded)%20so%20their%20regenerated%20predictions%0A%20%20%20%20are%20bit-for-bit%20identical%20to%20the%20originals%3B%20the%20originals%20are%20used%20here.%0A%20%20%20%20Chemprop%20and%20XGBoost%20are%20stochastic%20%E2%80%94%20only%20regenerated%20files%20are%20available.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Path%2C%20pl%2C%20unblinded%3A%20%22pl.DataFrame%22)%3A%0A%20%20%20%20SUBMISSION_DIR%20%3D%20Path(%22..%2Fsubmissions%22)%0A%20%20%20%20PRED_DIR%20%20%20%20%20%20%20%3D%20Path(%22..%2Fpredictions%22)%0A%0A%20%20%20%20%23%20Each%20entry%3A%20(path%2C%20pred_col%2C%20label)%0A%20%20%20%20%23%20pred_col%20is%20the%20column%20holding%20predictions%20%E2%80%94%20%22pEC50%22%20for%20submission%20CSVs%2C%0A%20%20%20%20%23%20%22pEC50_%3Ctag%3E%22%20for%20the%20regenerated%20component%20CSVs.%0A%20%20%20%20SOURCES%3A%20list%5Btuple%5BPath%2C%20str%2C%20str%5D%5D%20%3D%20%5B%0A%20%20%20%20%20%20%20%20(SUBMISSION_DIR%20%2F%20%224_ens_cp5_ch5_rf0_xg13_mc1_tf5_submission.csv%22%2C%0A%20%20%20%20%20%20%20%20%20%22pEC50%22%2C%20%20%20%20%22Ensemble%20(cp%C2%B7ch%C2%B7xg%C2%B7mc%C2%B7tf)%22)%2C%0A%20%20%20%20%20%20%20%20(SUBMISSION_DIR%20%2F%20%224_chemeleon_hpo_submission.csv%22%2C%0A%20%20%20%20%20%20%20%20%20%22pEC50%22%2C%20%20%20%20%22CheMeleon%20HPO%20(ch)%22)%2C%0A%20%20%20%20%20%20%20%20(SUBMISSION_DIR%20%2F%20%224_tabpfn_chemeleon_submission.csv%22%2C%0A%20%20%20%20%20%20%20%20%20%22pEC50%22%2C%20%20%20%20%22TabPFN%20%2F%20CheMeleon%20(tf)%22)%2C%0A%20%20%20%20%20%20%20%20(SUBMISSION_DIR%20%2F%20%224_macau_che_hpo_submission.csv%22%2C%0A%20%20%20%20%20%20%20%20%20%22pEC50%22%2C%20%20%20%20%22Macau%20HPO%20%2F%20CheMeleon%20(mc)%22)%2C%0A%20%20%20%20%20%20%20%20(PRED_DIR%20%2F%20%225_regen_cp_preds.csv%22%2C%0A%20%20%20%20%20%20%20%20%20%22pEC50_cp%22%2C%20%22Chemprop%20HPO%20(cp)%20%5Bregen%5D%22)%2C%0A%20%20%20%20%20%20%20%20(PRED_DIR%20%2F%20%225_regen_xg_preds.csv%22%2C%0A%20%20%20%20%20%20%20%20%20%22pEC50_xg%22%2C%20%22XGBoost%20%2F%20Mordred%20(xg)%20%5Bregen%5D%22)%2C%0A%20%20%20%20%5D%0A%0A%20%20%20%20%23%20Build%20a%20truth%20table%3A%20Molecule%20Name%20%E2%86%92%20true%20pEC50%0A%20%20%20%20truth%3A%20pl.DataFrame%20%3D%20unblinded.select(%5B%22Molecule%20Name%22%2C%20%22pEC50%22%5D).rename(%0A%20%20%20%20%20%20%20%20%7B%22pEC50%22%3A%20%22pEC50_true%22%7D%0A%20%20%20%20)%0A%0A%20%20%20%20def%20load_submission(path%3A%20Path%2C%20pred_col%3A%20str%2C%20label%3A%20str)%20-%3E%20pl.DataFrame%3A%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20Load%20a%20prediction%20CSV%20and%20join%20with%20the%20ground-truth%20pEC50.%0A%0A%20%20%20%20%20%20%20%20Args%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20path%3A%20Path%20to%20the%20CSV%20file.%0A%20%20%20%20%20%20%20%20%20%20%20%20pred_col%3A%20Column%20name%20that%20holds%20the%20predicted%20pEC50%20values.%0A%20%20%20%20%20%20%20%20%20%20%20%20label%3A%20Human-readable%20model%20name%20attached%20to%20every%20row.%0A%0A%20%20%20%20%20%20%20%20Returns%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20DataFrame%20with%20columns%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20model_name%2C%20Molecule%20Name%2C%20pEC50_pred%2C%20pEC50_true%2C%20error%2C%20abs_error.%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20df%20%3D%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.read_csv(path)%0A%20%20%20%20%20%20%20%20%20%20%20%20.rename(%7Bpred_col%3A%20%22pEC50_pred%22%7D)%0A%20%20%20%20%20%20%20%20%20%20%20%20.select(%5B%22Molecule%20Name%22%2C%20%22pEC50_pred%22%5D)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20df.join(truth%2C%20on%3D%22Molecule%20Name%22%2C%20how%3D%22inner%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pl.lit(label).alias(%22model_name%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22pEC50_pred%22)%20-%20pl.col(%22pEC50_true%22)).alias(%22error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22pEC50_pred%22)%20-%20pl.col(%22pEC50_true%22)).abs().alias(%22abs_error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20all_predictions%3A%20pl.DataFrame%20%3D%20pl.concat(%0A%20%20%20%20%20%20%20%20%5Bload_submission(path%2C%20pred_col%2C%20label)%20for%20path%2C%20pred_col%2C%20label%20in%20SOURCES%5D%0A%20%20%20%20)%0A%0A%20%20%20%20print(f%22Loaded%20%7Blen(SOURCES)%7D%20sources%2C%20%22%0A%20%20%20%20%20%20%20%20%20%20f%22%7Ball_predictions.shape%5B0%5D%7D%20total%20rows%22)%0A%20%20%20%20all_predictions%0A%20%20%20%20return%20(all_predictions%2C)%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Aggregate%20metrics%20per%20model%0A%0A%20%20%20%20Five%20standard%20regression%20metrics%20are%20computed%20for%20each%20submission%3A%0A%0A%20%20%20%20%7C%20Metric%20%7C%20Description%20%7C%0A%20%20%20%20%7C---%7C---%7C%0A%20%20%20%20%7C%20**RMSE**%20%7C%20Root%20mean%20squared%20error%20(lower%20%3D%20better)%20%7C%0A%20%20%20%20%7C%20**MAE**%20%7C%20Mean%20absolute%20error%20(lower%20%3D%20better)%20%7C%0A%20%20%20%20%7C%20**R%C2%B2**%20%7C%20Coefficient%20of%20determination%20(higher%20%3D%20better)%20%7C%0A%20%20%20%20%7C%20**Pearson%20r**%20%7C%20Linear%20correlation%20between%20predicted%20and%20true%20pEC50%20%7C%0A%20%20%20%20%7C%20**Spearman%20%CF%81**%20%7C%20Rank%20correlation%20(robust%20to%20outliers)%20%7C%0A%0A%20%20%20%20The%20table%20is%20sorted%20by%20MAE%20ascending%20so%20the%20best-performing%20model%20appears%20first.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20all_predictions%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20math%2C%0A%20%20%20%20mean_absolute_error%2C%0A%20%20%20%20mean_squared_error%2C%0A%20%20%20%20np%2C%0A%20%20%20%20pearsonr%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20r2_score%2C%0A%20%20%20%20spearmanr%2C%0A)%3A%0A%20%20%20%20def%20compute_metrics(group%3A%20pl.DataFrame)%20-%3E%20dict%3A%0A%20%20%20%20%20%20%20%20%22%22%22Compute%20regression%20metrics%20for%20one%20model's%20predictions.%22%22%22%0A%20%20%20%20%20%20%20%20y_true%20%3D%20group.get_column(%22pEC50_true%22).to_numpy()%0A%20%20%20%20%20%20%20%20y_pred%20%3D%20group.get_column(%22pEC50_pred%22).to_numpy()%0A%20%20%20%20%20%20%20%20rmse%20%3D%20math.sqrt(mean_squared_error(y_true%2C%20y_pred))%0A%20%20%20%20%20%20%20%20mae%20%20%3D%20mean_absolute_error(y_true%2C%20y_pred)%0A%20%20%20%20%20%20%20%20r2%20%20%20%3D%20r2_score(y_true%2C%20y_pred)%0A%20%20%20%20%20%20%20%20r%2C%20_%20%3D%20pearsonr(y_true%2C%20y_pred)%0A%20%20%20%20%20%20%20%20rho%2C%20_%20%3D%20spearmanr(y_true%2C%20y_pred)%0A%20%20%20%20%20%20%20%20n%20%20%20%20%3D%20len(y_true)%0A%20%20%20%20%20%20%20%20bias%20%3D%20float(np.mean(y_pred%20-%20y_true))%0A%20%20%20%20%20%20%20%20return%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22model_name%22%3A%20group.get_column(%22model_name%22)%5B0%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22n%22%3A%20%20%20%20%20%20%20%20%20%20n%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22RMSE%22%3A%20%20%20%20%20%20%20round(rmse%2C%204)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22MAE%22%3A%20%20%20%20%20%20%20%20round(mae%2C%20%204)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22R2%22%3A%20%20%20%20%20%20%20%20%20round(r2%2C%20%20%204)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Pearson_r%22%3A%20%20round(r%2C%20%20%20%204)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Spearman_rho%22%3A%20round(rho%2C%204)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22bias%22%3A%20%20%20%20%20%20%20round(bias%2C%204)%2C%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20metrics_rows%20%3D%20%5B%0A%20%20%20%20%20%20%20%20compute_metrics(grp)%0A%20%20%20%20%20%20%20%20for%20grp%20in%20all_predictions.partition_by(%22model_name%22%2C%20maintain_order%3DTrue)%0A%20%20%20%20%5D%0A%0A%20%20%20%20metrics_df%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20pl.DataFrame(metrics_rows)%0A%20%20%20%20%20%20%20%20.sort(%22MAE%22)%0A%20%20%20%20)%0A%0A%20%20%20%20metrics_df%0A%20%20%20%20return%20(metrics_df%2C)%0A%0A%0A%40app.cell%0Adef%20_(alt%2C%20metrics_df%3A%20%22pl.DataFrame%22%2C%20mo)%3A%0A%20%20%20%20_bar%20%3D%20(%0A%20%20%20%20%20%20%20%20alt.Chart(metrics_df)%0A%20%20%20%20%20%20%20%20.mark_bar()%0A%20%20%20%20%20%20%20%20.encode(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dalt.X(%22MAE%3AQ%22%2C%20title%3D%22MAE%20%20(lower%20%3D%20better)%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dalt.Y(%22model_name%3AN%22%2C%20sort%3Dalt.SortField(%22MAE%22%2C%20order%3D%22ascending%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%3DNone%2C%20axis%3Dalt.Axis(labelLimit%3D300%2C%20labelFontSize%3D10))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20color%3Dalt.Color(%22MAE%3AQ%22%2C%20scale%3Dalt.Scale(scheme%3D%22redyellowgreen%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20reverse%3DTrue)%2C%20legend%3DNone)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20tooltip%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22model_name%3AN%22%2C%20title%3D%22Model%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22MAE%3AQ%22%2C%20%20%20%20%20%20%20%20%20title%3D%22MAE%22%2C%20%20%20%20%20%20%20%20%20format%3D%22.4f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22RMSE%3AQ%22%2C%20%20%20%20%20%20%20%20title%3D%22RMSE%22%2C%20%20%20%20%20%20%20%20format%3D%22.4f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22R2%3AQ%22%2C%20%20%20%20%20%20%20%20%20%20title%3D%22R%C2%B2%22%2C%20%20%20%20%20%20%20%20%20%20format%3D%22.4f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22Pearson_r%3AQ%22%2C%20%20%20title%3D%22Pearson%20r%22%2C%20%20%20format%3D%22.4f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22Spearman_rho%3AQ%22%2Ctitle%3D%22Spearman%20%CF%81%22%2C%20format%3D%22.4f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20alt.Tooltip(%22bias%3AQ%22%2C%20%20%20%20%20%20%20%20title%3D%22Bias%22%2C%20%20%20%20%20%20%20%20format%3D%22%2B.4f%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.properties(%0A%20%20%20%20%20%20%20%20%20%20%20%20title%3D%22Model%20ranking%20by%20MAE%20on%20unblinded%20test%20set%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3D480%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3D320%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.configure_title(fontSize%3D13)%0A%20%20%20%20)%0A%0A%20%20%20%20mo.ui.altair_chart(_bar)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Predicted%20vs.%20observed%20scatter%20per%20model%0A%0A%20%20%20%20Each%20panel%20shows%20the%20predicted%20pEC50%20on%20the%20x-axis%20and%20the%20true%20pEC50%20on%20the%0A%20%20%20%20y-axis.%20%20The%20dashed%20diagonal%20is%20the%20perfect-prediction%20line.%20%20Points%20are%0A%20%20%20%20coloured%20by%20absolute%20error%20%E2%80%94%20the%20redder%20the%20point%2C%20the%20larger%20the%20mistake.%0A%20%20%20%20Panels%20are%20sorted%20by%20MAE%20(best%20model%20top-left).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20all_predictions%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20math%2C%0A%20%20%20%20mean_absolute_error%2C%0A%20%20%20%20metrics_df%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20np%2C%0A%20%20%20%20plt%2C%0A)%3A%0A%20%20%20%20_model_order%20%3D%20metrics_df.get_column(%22model_name%22).to_list()%0A%20%20%20%20_n_models%20%20%20%20%3D%20len(_model_order)%0A%20%20%20%20_ncols%20%20%20%20%20%20%20%3D%203%0A%20%20%20%20_nrows%20%20%20%20%20%20%20%3D%20math.ceil(_n_models%20%2F%20_ncols)%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(%0A%20%20%20%20%20%20%20%20%20%20%20%20_nrows%2C%20_ncols%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20figsize%3D(_ncols%20*%204.5%2C%20_nrows%20*%204.5)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D120%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_axes_flat%20%3D%20_axes.flatten()%20if%20_n_models%20%3E%201%20else%20%5B_axes%5D%0A%0A%20%20%20%20%20%20%20%20for%20_i%2C%20_model%20in%20enumerate(_model_order)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax%20%3D%20_axes_flat%5B_i%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_grp%20%3D%20all_predictions.filter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20all_predictions%5B%22model_name%22%5D%20%3D%3D%20_model%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20_y_true%20%3D%20_grp.get_column(%22pEC50_true%22).to_numpy()%0A%20%20%20%20%20%20%20%20%20%20%20%20_y_pred%20%3D%20_grp.get_column(%22pEC50_pred%22).to_numpy()%0A%20%20%20%20%20%20%20%20%20%20%20%20_err%20%20%20%20%3D%20np.abs(_y_pred%20-%20_y_true)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20_mae%20%3D%20mean_absolute_error(_y_true%2C%20_y_pred)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20_sc%20%3D%20_ax.scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_y_pred%2C%20_y_true%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20c%3D_err%2C%20cmap%3D%22RdYlGn_r%22%2C%20vmin%3D0%2C%20vmax%3D1.5%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20s%3D25%2C%20alpha%3D0.75%2C%20edgecolors%3D%22none%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20_lims%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20min(_y_true.min()%2C%20_y_pred.min())%20-%200.2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20max(_y_true.max()%2C%20_y_pred.max())%20%2B%200.2%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.plot(_lims%2C%20_lims%2C%20%22k--%22%2C%20linewidth%3D0.8%2C%20zorder%3D0)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xlim(_lims)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_ylim(_lims)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xlabel(%22Predicted%20pEC50%22%2C%20fontsize%3D9)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_ylabel(%22True%20pEC50%22%2C%20%20%20%20%20%20fontsize%3D9)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_title(f%22%7B_model%7D%5CnMAE%3D%7B_mae%3A.3f%7D%22%2C%20fontsize%3D8%2C%20pad%3D4)%0A%20%20%20%20%20%20%20%20%20%20%20%20plt.colorbar(_sc%2C%20ax%3D_ax%2C%20label%3D%22%7Cerror%7C%22%2C%20fraction%3D0.046%2C%20pad%3D0.04)%0A%0A%20%20%20%20%20%20%20%20%23%20Hide%20any%20unused%20axes.%0A%20%20%20%20%20%20%20%20for%20_j%20in%20range(_i%20%2B%201%2C%20len(_axes_flat))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_axes_flat%5B_j%5D.set_visible(False)%0A%0A%20%20%20%20%20%20%20%20_fig.suptitle(%22Predicted%20vs.%20true%20pEC50%20%E2%80%94%20all%20submissions%22%2C%20fontsize%3D14%2C%20y%3D1.01)%0A%20%20%20%20%20%20%20%20_fig.tight_layout()%0A%20%20%20%20%20%20%20%20_fig.savefig(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22..%2Fplots%2F5_unblinded_analysis%2Fpred_vs_true_all_models.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D200%2C%20bbox_inches%3D%22tight%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.center(mo.as_html(_fig))%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Residual%20error%20vs.%20true%20pEC50%0A%0A%20%20%20%20Each%20panel%20shows%20the%20signed%20residual%20(predicted%20%E2%88%92%20true%20pEC50)%20on%20the%20y-axis%0A%20%20%20%20against%20the%20true%20pEC50%20on%20the%20x-axis.%20%20The%20dashed%20horizontal%20line%20at%20zero%20is%0A%20%20%20%20the%20ideal%20residual.%20%20Points%20above%20the%20line%20are%20overpredicted%3B%20points%20below%20are%0A%20%20%20%20underpredicted.%20%20Colour%20encodes%20the%20absolute%20error%20so%20large%20deviations%20are%0A%20%20%20%20immediately%20visible%20regardless%20of%20sign.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20all_predictions%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20math%2C%0A%20%20%20%20mean_absolute_error%2C%0A%20%20%20%20metrics_df%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20np%2C%0A%20%20%20%20plt%2C%0A)%3A%0A%20%20%20%20_model_order_res%20%3D%20metrics_df.get_column(%22model_name%22).to_list()%0A%20%20%20%20_n_models_res%20%20%20%20%3D%20len(_model_order_res)%0A%20%20%20%20_ncols_res%20%20%20%20%20%20%20%3D%203%0A%20%20%20%20_nrows_res%20%20%20%20%20%20%20%3D%20math.ceil(_n_models_res%20%2F%20_ncols_res)%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20_fig%2C%20_axes%20%3D%20plt.subplots(%0A%20%20%20%20%20%20%20%20%20%20%20%20_nrows_res%2C%20_ncols_res%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20figsize%3D(_ncols_res%20*%204.5%2C%20_nrows_res%20*%203.5)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D120%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20sharey%3DFalse%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_axes_flat%20%3D%20_axes.flatten()%20if%20_n_models_res%20%3E%201%20else%20%5B_axes%5D%0A%0A%20%20%20%20%20%20%20%20%23%20Shared%20colour%20scale%20across%20all%20panels%20%E2%80%94%20consistent%20visual%20reference.%0A%20%20%20%20%20%20%20%20_vmax%20%3D%201.5%0A%0A%20%20%20%20%20%20%20%20for%20_i%2C%20_model%20in%20enumerate(_model_order_res)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax%20%3D%20_axes_flat%5B_i%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_grp%20%3D%20all_predictions.filter(all_predictions%5B%22model_name%22%5D%20%3D%3D%20_model)%0A%20%20%20%20%20%20%20%20%20%20%20%20_y_true%20%3D%20_grp.get_column(%22pEC50_true%22).to_numpy()%0A%20%20%20%20%20%20%20%20%20%20%20%20_y_pred%20%3D%20_grp.get_column(%22pEC50_pred%22).to_numpy()%0A%20%20%20%20%20%20%20%20%20%20%20%20_resid%20%20%3D%20_y_pred%20-%20_y_true%0A%20%20%20%20%20%20%20%20%20%20%20%20_abs_err%20%3D%20np.abs(_resid)%0A%20%20%20%20%20%20%20%20%20%20%20%20_mae%20%3D%20mean_absolute_error(_y_true%2C%20_y_pred)%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20_sc%20%3D%20_ax.scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_y_true%2C%20_resid%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20c%3D_abs_err%2C%20cmap%3D%22RdYlGn_r%22%2C%20vmin%3D0%2C%20vmax%3D_vmax%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20s%3D25%2C%20alpha%3D0.8%2C%20edgecolors%3D%22none%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.axhline(0%2C%20color%3D%22black%22%2C%20linestyle%3D%22--%22%2C%20linewidth%3D0.9%2C%20zorder%3D0)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_xlabel(%22True%20pEC50%22%2C%20fontsize%3D9)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_ylabel(%22Residual%20(pred%20%E2%88%92%20true)%22%2C%20fontsize%3D9)%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax.set_title(f%22%7B_model%7D%5CnMAE%3D%7B_mae%3A.3f%7D%22%2C%20fontsize%3D8%2C%20pad%3D4)%0A%20%20%20%20%20%20%20%20%20%20%20%20plt.colorbar(_sc%2C%20ax%3D_ax%2C%20label%3D%22%7Cerror%7C%22%2C%20fraction%3D0.046%2C%20pad%3D0.04)%0A%0A%20%20%20%20%20%20%20%20for%20_j%20in%20range(_i%20%2B%201%2C%20len(_axes_flat))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_axes_flat%5B_j%5D.set_visible(False)%0A%0A%20%20%20%20%20%20%20%20_fig.suptitle(%22Residual%20error%20vs.%20true%20pEC50%20%E2%80%94%20all%20submissions%22%2C%20fontsize%3D14%2C%20y%3D1.01)%0A%20%20%20%20%20%20%20%20_fig.tight_layout()%0A%20%20%20%20%20%20%20%20_fig.savefig(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22..%2Fplots%2F5_unblinded_analysis%2Fresiduals_all_models.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D300%2C%20bbox_inches%3D%22tight%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.center(mo.as_html(_fig))%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Error%20analysis%0A%0A%20%20%20%20%23%23%23%20Hardest%20compounds%20to%20predict%0A%0A%20%20%20%20We%20identify%20compounds%20for%20which%20the%20**best-performing**%20model%20still%20produced%0A%20%20%20%20a%20large%20absolute%20error.%20%20For%20each%20compound%20we%20take%20the%20minimum%20absolute%20error%0A%20%20%20%20across%20all%20submissions%2C%20then%20rank%20by%20that%20value.%20%20Compounds%20that%20were%20hard%20for%0A%20%20%20%20every%20model%20likely%20represent%20chemical%20or%20mechanistic%20novelty.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(all_predictions%3A%20%22pl.DataFrame%22%2C%20pl%2C%20unblinded%3A%20%22pl.DataFrame%22)%3A%0A%20%20%20%20%23%20Per-compound%3A%20minimum%20absolute%20error%20across%20all%20models%2C%20and%20which%20model%20achieved%20it.%0A%20%20%20%20compound_min_err%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20all_predictions%0A%20%20%20%20%20%20%20%20.group_by(%22Molecule%20Name%22)%0A%20%20%20%20%20%20%20%20.agg(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_error%22).min().alias(%22min_abs_error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_error%22).mean().alias(%22mean_abs_error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_error%22).std().alias(%22std_abs_error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22pEC50_true%22).first().alias(%22pEC50_true%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%23%20Join%20SMILES%20and%20InChIKey%20from%20the%20unblinded%20dataset.%0A%20%20%20%20%20%20%20%20.join(%0A%20%20%20%20%20%20%20%20%20%20%20%20unblinded.select(%5B%22Molecule%20Name%22%2C%20%22smiles%22%2C%20%22inchikey%22%2C%20%22Emax%22%5D)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20on%3D%22Molecule%20Name%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20how%3D%22left%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.sort(%22mean_abs_error%22%2C%20descending%3DTrue)%0A%20%20%20%20)%0A%0A%20%20%20%20print(%22Top%2010%20hardest%20compounds%20(largest%20mean%20absolute%20error%20across%20models)%3A%22)%0A%20%20%20%20compound_min_err.head(10)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Model%20disagreement%20on%20large%20errors%20(%7Cerror%7C%20%3E%201%20log%20unit)%0A%0A%20%20%20%20We%20now%20inspect%20**badly-predicted%20compounds**%20%E2%80%94%20those%20where%20at%20least%20one%20model's%0A%20%20%20%20absolute%20error%20exceeds%201%20pEC50%20unit.%20%20Three%20questions%20guide%20this%20analysis%3A%0A%0A%20%20%20%201.%20**How%20many**%20compounds%20are%20badly%20predicted%20by%20each%20model%3F%20%20Does%20the%20ensemble%0A%20%20%20%20%20%20%20have%20fewer%20failures%20than%20its%20individual%20components%3F%0A%20%20%20%202.%20**Do%20models%20fail%20on%20the%20same%20compounds%20or%20on%20different%20ones%3F**%20%20The%20Jaccard%0A%20%20%20%20%20%20%20similarity%20of%20each%20pair%20of%20%22bad-prediction%20sets%22%20answers%20this%20directly.%0A%20%20%20%203.%20**Does%20the%20ensemble%20rescue%20individual%20mistakes%3F**%20%20We%20classify%20every%20compound%0A%20%20%20%20%20%20%20as%20*rescued*%2C%20*still%20bad*%2C%20*only%20ensemble%20bad*%2C%20or%20*good%20everywhere*.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(all_predictions%3A%20%22pl.DataFrame%22%2C%20pl)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Threshold%20and%20display%20constants%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20BAD_THRESH%20%3D%201.0%0A%20%20%20%20ENSEMBLE_NAME%20%3D%20%22Ensemble%20(cp%C2%B7ch%C2%B7xg%C2%B7mc%C2%B7tf)%22%0A%0A%20%20%20%20%23%20Short%20axis%20labels%20used%20in%20heatmaps%20and%20bar%20charts%0A%20%20%20%20SHORT_MODEL_NAMES%3A%20dict%5Bstr%2C%20str%5D%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22Ensemble%20(cp%C2%B7ch%C2%B7xg%C2%B7mc%C2%B7tf)%22%3A%20%20%20%20%20%20%22Ensemble%22%2C%0A%20%20%20%20%20%20%20%20%22CheMeleon%20HPO%20(ch)%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22CheMeleon%5Cn(ch)%22%2C%0A%20%20%20%20%20%20%20%20%22TabPFN%20%2F%20CheMeleon%20(tf)%22%3A%20%20%20%20%20%20%20%20%20%22TabPFN%5Cn(tf)%22%2C%0A%20%20%20%20%20%20%20%20%22Macau%20HPO%20%2F%20CheMeleon%20(mc)%22%3A%20%20%20%20%20%20%22Macau%5Cn(mc)%22%2C%0A%20%20%20%20%20%20%20%20%22Chemprop%20HPO%20(cp)%20%5Bregen%5D%22%3A%20%20%20%20%20%20%20%22Chemprop%5Cn(cp)%22%2C%0A%20%20%20%20%20%20%20%20%22XGBoost%20%2F%20Mordred%20(xg)%20%5Bregen%5D%22%3A%20%20%22XGBoost%5Cn(xg)%22%2C%0A%20%20%20%20%7D%0A%0A%20%20%20%20%23%20Compounds%20where%20at%20least%20one%20model%20has%20abs_error%20%3E%20BAD_THRESH%0A%20%20%20%20_bad_names%20%3D%20(%0A%20%20%20%20%20%20%20%20all_predictions%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22abs_error%22)%20%3E%20BAD_THRESH)%0A%20%20%20%20%20%20%20%20.get_column(%22Molecule%20Name%22)%0A%20%20%20%20%20%20%20%20.unique()%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Long-format%20predictions%20restricted%20to%20these%20compounds%20(all%20models%20included)%0A%20%20%20%20bad_preds_long%3A%20pl.DataFrame%20%3D%20all_predictions.filter(%0A%20%20%20%20%20%20%20%20pl.col(%22Molecule%20Name%22).is_in(_bad_names)%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Wide%20format%3A%20Molecule%20Name%20%C3%97%20model_name%20%E2%86%92%20abs_error%20value%0A%20%20%20%20error_wide%3A%20pl.DataFrame%20%3D%20bad_preds_long.pivot(%0A%20%20%20%20%20%20%20%20values%3D%22abs_error%22%2C%0A%20%20%20%20%20%20%20%20index%3D%22Molecule%20Name%22%2C%0A%20%20%20%20%20%20%20%20on%3D%22model_name%22%2C%0A%20%20%20%20%20%20%20%20aggregate_function%3D%22first%22%2C%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Count%20how%20many%20models%20flag%20each%20compound%20as%20bad%0A%20%20%20%20_model_cols%20%3D%20%5Bc%20for%20c%20in%20error_wide.columns%20if%20c%20!%3D%20%22Molecule%20Name%22%5D%0A%20%20%20%20error_wide%20%3D%20error_wide.with_columns(%0A%20%20%20%20%20%20%20%20pl.sum_horizontal(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(m)%20%3E%20BAD_THRESH).cast(pl.Int32)%20for%20m%20in%20_model_cols%0A%20%20%20%20%20%20%20%20%5D).alias(%22n_models_bad%22)%0A%20%20%20%20).sort(%5B%22n_models_bad%22%2C%20%22Molecule%20Name%22%5D%2C%20descending%3D%5BTrue%2C%20False%5D)%0A%0A%20%20%20%20%23%20Per-model%20tally%0A%20%20%20%20per_model_bad%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20all_predictions%0A%20%20%20%20%20%20%20%20.with_columns((pl.col(%22abs_error%22)%20%3E%20BAD_THRESH).alias(%22is_bad%22))%0A%20%20%20%20%20%20%20%20.group_by(%22model_name%22)%0A%20%20%20%20%20%20%20%20.agg(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22is_bad%22).sum().alias(%22n_bad%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.len().alias(%22n_total%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22n_bad%22)%20%2F%20pl.col(%22n_total%22)%20*%20100).round(1).alias(%22pct_bad%22)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.sort(%22n_bad%22%2C%20descending%3DTrue)%0A%20%20%20%20)%0A%0A%20%20%20%20print(f%22Compounds%20with%20%7Cerror%7C%20%3E%20%7BBAD_THRESH%7D%20in%20%E2%89%A51%20model%3A%20%7B_bad_names.len()%7D%22)%0A%20%20%20%20per_model_bad%0A%20%20%20%20return%20BAD_THRESH%2C%20ENSEMBLE_NAME%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20BAD_THRESH%2C%0A%20%20%20%20ENSEMBLE_NAME%2C%0A%20%20%20%20all_predictions%3A%20%22pl.DataFrame%22%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20unblinded%3A%20%22pl.DataFrame%22%2C%0A)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Rescue%20analysis%3A%20compare%20ensemble%20against%20individual%20component%20models%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20_ens_preds%20%3D%20(%0A%20%20%20%20%20%20%20%20all_predictions%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22model_name%22)%20%3D%3D%20ENSEMBLE_NAME)%0A%20%20%20%20%20%20%20%20.select(%5B%22Molecule%20Name%22%2C%20%22pEC50_true%22%2C%20%22pEC50_pred%22%2C%20%22abs_error%22%5D)%0A%20%20%20%20%20%20%20%20.rename(%7B%22abs_error%22%3A%20%22ens_abs_error%22%2C%20%22pEC50_pred%22%3A%20%22ens_pred%22%7D)%0A%20%20%20%20)%0A%0A%20%20%20%20_ind_stats%20%3D%20(%0A%20%20%20%20%20%20%20%20all_predictions%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22model_name%22)%20!%3D%20ENSEMBLE_NAME)%0A%20%20%20%20%20%20%20%20.group_by(%22Molecule%20Name%22)%0A%20%20%20%20%20%20%20%20.agg(%0A%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22abs_error%22)%20%3E%20BAD_THRESH).sum().alias(%22n_ind_bad%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_error%22).mean().alias(%22mean_ind_abs_error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.len().alias(%22n_ind_models%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%0A%20%20%20%20rescue_df%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20_ens_preds%0A%20%20%20%20%20%20%20%20.join(_ind_stats%2C%20on%3D%22Molecule%20Name%22%2C%20how%3D%22inner%22)%0A%20%20%20%20%20%20%20%20.join(unblinded.select(%5B%22Molecule%20Name%22%2C%20%22smiles%22%2C%20%22inchikey%22%5D)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20on%3D%22Molecule%20Name%22%2C%20how%3D%22left%22)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.when(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22n_ind_bad%22)%20%3E%3D%201)%20%26%20(pl.col(%22ens_abs_error%22)%20%3C%3D%20BAD_THRESH)%0A%20%20%20%20%20%20%20%20%20%20%20%20).then(pl.lit(%22Rescued%20by%20ensemble%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.when(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22n_ind_bad%22)%20%3E%3D%201)%20%26%20(pl.col(%22ens_abs_error%22)%20%3E%20BAD_THRESH)%0A%20%20%20%20%20%20%20%20%20%20%20%20).then(pl.lit(%22Still%20bad%20in%20ensemble%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.when(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(pl.col(%22n_ind_bad%22)%20%3D%3D%200)%20%26%20(pl.col(%22ens_abs_error%22)%20%3E%20BAD_THRESH)%0A%20%20%20%20%20%20%20%20%20%20%20%20).then(pl.lit(%22Only%20ensemble%20bad%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.otherwise(pl.lit(%22Good%20everywhere%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20.alias(%22rescue_status%22)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%0A%20%20%20%20_n_rescued%20%20%20%3D%20rescue_df.filter(pl.col(%22rescue_status%22)%20%3D%3D%20%22Rescued%20by%20ensemble%22).shape%5B0%5D%0A%20%20%20%20_n_still_bad%20%3D%20rescue_df.filter(pl.col(%22rescue_status%22)%20%3D%3D%20%22Still%20bad%20in%20ensemble%22).shape%5B0%5D%0A%20%20%20%20_n_ens_only%20%20%3D%20rescue_df.filter(pl.col(%22rescue_status%22)%20%3D%3D%20%22Only%20ensemble%20bad%22).shape%5B0%5D%0A%20%20%20%20_n_good%20%20%20%20%20%20%3D%20rescue_df.filter(pl.col(%22rescue_status%22)%20%3D%3D%20%22Good%20everywhere%22).shape%5B0%5D%0A%20%20%20%20_n_total%20%20%20%20%20%3D%20rescue_df.shape%5B0%5D%0A%20%20%20%20_n_ind%20%20%20%20%20%20%20%3D%20int(rescue_df%5B%22n_ind_models%22%5D.max())%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%23%23%23%20Ensemble%20rescue%20analysis%0A%0A%20%20%20%20For%20each%20test%20compound%20we%20compare%20the%20ensemble%20against%20the%20%7B_n_ind%7D%20individual%20component%0A%20%20%20%20models%20and%20assign%20one%20of%20four%20outcomes%3A%0A%0A%20%20%20%20%7C%20Status%20%7C%20n%20%7C%20%25%20of%20test%20set%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C%0A%20%20%20%20%7C%20**Rescued%20by%20ensemble**%20%E2%80%94%20bad%20in%20%E2%89%A51%20component%2C%20good%20in%20ensemble%20%7C%20**%7B_n_rescued%7D**%20%7C%20**%7B100*_n_rescued%2F_n_total%3A.1f%7D%25**%20%7C%0A%20%20%20%20%7C%20**Still%20bad%20in%20ensemble**%20%E2%80%94%20bad%20in%20%E2%89%A51%20component%20AND%20bad%20in%20ensemble%20%7C%20%7B_n_still_bad%7D%20%7C%20%7B100*_n_still_bad%2F_n_total%3A.1f%7D%25%20%7C%0A%20%20%20%20%7C%20Only%20ensemble%20bad%20%E2%80%94%20all%20components%20fine%2C%20ensemble%20errs%20%7C%20%7B_n_ens_only%7D%20%7C%20%7B100*_n_ens_only%2F_n_total%3A.1f%7D%25%20%7C%0A%20%20%20%20%7C%20Good%20everywhere%20%E2%80%94%20all%20models%20predict%20well%20%7C%20%7B_n_good%7D%20%7C%20%7B100*_n_good%2F_n_total%3A.1f%7D%25%20%7C%0A%0A%20%20%20%20**%7B_n_rescued%7D%20compounds**%20(%7B100*_n_rescued%2F_n_total%3A.0f%7D%25%20of%20the%20test%20set)%20were%20corrected%20by%0A%20%20%20%20averaging%20%E2%80%94%20at%20least%20one%20component%20model%20failed%20but%20the%20ensemble%20brought%20the%20error%0A%20%20%20%20below%20%7BBAD_THRESH%3A.0f%7D%20pEC50%20unit.%20%20The%20**%7B_n_still_bad%7D%20%22still%20bad%22**%20compounds%20represent%0A%20%20%20%20structural%20blind%20spots%20that%20no%20ensemble%20weighting%20can%20compensate%20for.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%20(rescue_df%2C)%0A%0A%0A%40app.cell%0Adef%20_(BAD_THRESH%2C%20mo%2C%20pl%2C%20plt%2C%20rescue_df%3A%20%22pl.DataFrame%22)%3A%0A%20%20%20%20%23%20%E2%94%80%E2%94%80%20Scatter%3A%20ensemble%20error%20vs%20mean%20individual-model%20error%20%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%0A%20%20%20%20_status_colors%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22Rescued%20by%20ensemble%22%3A%20%20%20%22%2376b7b2%22%2C%0A%20%20%20%20%20%20%20%20%22Still%20bad%20in%20ensemble%22%3A%20%22%23e15759%22%2C%0A%20%20%20%20%20%20%20%20%22Only%20ensemble%20bad%22%3A%20%20%20%20%20%22%23f28e2b%22%2C%0A%20%20%20%20%20%20%20%20%22Good%20everywhere%22%3A%20%20%20%20%20%20%20%22%23b0b0b0%22%2C%0A%20%20%20%20%7D%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20_fig_r%2C%20_ax_r%20%3D%20plt.subplots(figsize%3D(6.5%2C%205.5)%2C%20dpi%3D130)%0A%0A%20%20%20%20%20%20%20%20for%20_status%2C%20_col%20in%20_status_colors.items()%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_sub%20%3D%20rescue_df.filter(pl.col(%22rescue_status%22)%20%3D%3D%20_status)%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20_sub.shape%5B0%5D%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%0A%20%20%20%20%20%20%20%20%20%20%20%20_ax_r.scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_sub%5B%22mean_ind_abs_error%22%5D.to_numpy()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_sub%5B%22ens_abs_error%22%5D.to_numpy()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20c%3D_col%2C%20label%3Df%22%7B_status%7D%20%20(n%3D%7B_sub.shape%5B0%5D%7D)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20s%3D40%2C%20alpha%3D0.85%2C%20edgecolors%3D%22none%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20_lim%20%3D%20float(max(%0A%20%20%20%20%20%20%20%20%20%20%20%20rescue_df%5B%22mean_ind_abs_error%22%5D.max()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20rescue_df%5B%22ens_abs_error%22%5D.max()%2C%0A%20%20%20%20%20%20%20%20))%20%2B%200.15%0A%20%20%20%20%20%20%20%20_ax_r.plot(%5B0%2C%20_lim%5D%2C%20%5B0%2C%20_lim%5D%2C%20%22k--%22%2C%20linewidth%3D0.9%2C%20zorder%3D0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20label%3D%22y%20%3D%20x%20%20(ensemble%20%3D%20mean%20component)%22)%0A%20%20%20%20%20%20%20%20_ax_r.axhline(BAD_THRESH%2C%20color%3D%22grey%22%2C%20linewidth%3D0.8%2C%20linestyle%3D%22%3A%22%2C%20zorder%3D0)%0A%20%20%20%20%20%20%20%20_ax_r.axvline(BAD_THRESH%2C%20color%3D%22grey%22%2C%20linewidth%3D0.8%2C%20linestyle%3D%22%3A%22%2C%20zorder%3D0)%0A%20%20%20%20%20%20%20%20_ax_r.text(BAD_THRESH%20%2B%200.03%2C%200.05%2C%20f%22%7BBAD_THRESH%7D%22%2C%20color%3D%22grey%22%2C%20fontsize%3D8)%0A%20%20%20%20%20%20%20%20_ax_r.text(0.05%2C%20BAD_THRESH%20%2B%200.04%2C%20f%22%7BBAD_THRESH%7D%22%2C%20color%3D%22grey%22%2C%20fontsize%3D8)%0A%0A%20%20%20%20%20%20%20%20_ax_r.set_xlabel(%22Mean%20%7Cerror%7C%20across%20individual%20component%20models%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_r.set_ylabel(%22Ensemble%20%7Cerror%7C%22%2C%20fontsize%3D11)%0A%20%20%20%20%20%20%20%20_ax_r.set_title(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Ensemble%20vs.%20component%20model%20error%20per%20compound%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Below%20diagonal%20%3D%20ensemble%20outperforms%20mean%20component%3B%20%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22dotted%20lines%20%3D%20%7Cerror%7C%20%3D%201%20threshold%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D9%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_ax_r.legend(fontsize%3D8.5%2C%20frameon%3DTrue%2C%20framealpha%3D0.9)%0A%20%20%20%20%20%20%20%20_ax_r.set_xlim(-0.05%2C%20_lim)%0A%20%20%20%20%20%20%20%20_ax_r.set_ylim(-0.05%2C%20_lim)%0A%20%20%20%20%20%20%20%20_fig_r.tight_layout()%0A%20%20%20%20%20%20%20%20_fig_r.savefig(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22..%2Fplots%2F5_unblinded_analysis%2Fensemble_rescue_scatter.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D200%2C%20bbox_inches%3D%22tight%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.center(mo.as_html(_fig_r))%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20UpSet%20plot%20%E2%80%94%20overlap%20in%20compounds%20predicted%20badly%0A%0A%20%20%20%20A%20Venn%20diagram%20becomes%20unwieldy%20with%20six%20models.%20%20The%20UpSet%20plot%20below%0A%20%20%20%20replaces%20it%3A%20each%20column%20in%20the%20intersection%20matrix%20represents%20a%20model%2C%0A%20%20%20%20and%20each%20row%20is%20an%20intersection%20of%20models%20that%20*all*%20flagged%20a%20compound%0A%20%20%20%20as%20badly%20predicted%20(%7Cerror%7C%20%3E%201%20pEC50%20unit).%20%20The%20bar%20heights%20show%0A%20%20%20%20how%20many%20compounds%20fall%20into%20each%20intersection%20%E2%80%94%20revealing%2C%20for%20example%2C%0A%20%20%20%20how%20many%20compounds%20are%20uniquely%20missed%20by%20a%20single%20model%20versus%20missed%0A%20%20%20%20by%20every%20model%20simultaneously.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(BAD_THRESH%2C%20all_predictions%3A%20%22pl.DataFrame%22%2C%20mo%2C%20pl%2C%20plt)%3A%0A%20%20%20%20from%20upsetplot%20import%20from_memberships%0A%0A%20%20%20%20_short_names%3A%20dict%5Bstr%2C%20str%5D%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22Ensemble%20(cp%C2%B7ch%C2%B7xg%C2%B7mc%C2%B7tf)%22%3A%20%20%20%20%20%20%22Ensemble%22%2C%0A%20%20%20%20%20%20%20%20%22CheMeleon%20HPO%20(ch)%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22CheMeleon%22%2C%0A%20%20%20%20%20%20%20%20%22TabPFN%20%2F%20CheMeleon%20(tf)%22%3A%20%20%20%20%20%20%20%20%20%22TabPFN%22%2C%0A%20%20%20%20%20%20%20%20%22Macau%20HPO%20%2F%20CheMeleon%20(mc)%22%3A%20%20%20%20%20%20%22Macau%22%2C%0A%20%20%20%20%20%20%20%20%22Chemprop%20HPO%20(cp)%20%5Bregen%5D%22%3A%20%20%20%20%20%20%20%22Chemprop%22%2C%0A%20%20%20%20%20%20%20%20%22XGBoost%20%2F%20Mordred%20(xg)%20%5Bregen%5D%22%3A%20%20%22XGBoost%22%2C%0A%20%20%20%20%7D%0A%0A%20%20%20%20%23%20For%20each%20compound%2C%20collect%20the%20set%20of%20models%20that%20predicted%20it%20badly.%0A%20%20%20%20_bad%20%3D%20(%0A%20%20%20%20%20%20%20%20all_predictions%0A%20%20%20%20%20%20%20%20.filter(pl.col(%22abs_error%22)%20%3E%20BAD_THRESH)%0A%20%20%20%20%20%20%20%20.with_columns(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22model_name%22).replace(_short_names).alias(%22model_short%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.group_by(%22Molecule%20Name%22)%0A%20%20%20%20%20%20%20%20.agg(pl.col(%22model_short%22).sort())%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20from_memberships%20expects%20a%20list%20of%20lists%20(category%20memberships%20per%20observation).%0A%20%20%20%20_memberships%3A%20list%5Blist%5Bstr%5D%5D%20%3D%20_bad.get_column(%22model_short%22).to_list()%0A%0A%20%20%20%20%23%20from_memberships%20returns%20a%20Series%20with%20a%20boolean%20MultiIndex%20that%20may%20have%0A%20%20%20%20%23%20duplicate%20rows.%20%20Collapse%20to%20unique%20groups%20with%20counts%20so%20UpSet%20is%20happy.%0A%20%20%20%20_raw%20%3D%20from_memberships(_memberships)%0A%20%20%20%20_upset_data%20%3D%20_raw.groupby(level%3D_raw.index.names).size()%0A%0A%20%20%20%20_upset%20%3D%20__import__(%22upsetplot%22).UpSet(%0A%20%20%20%20%20%20%20%20_upset_data%2C%0A%20%20%20%20%20%20%20%20show_counts%3DTrue%2C%0A%20%20%20%20%20%20%20%20sort_by%3D%22cardinality%22%2C%0A%20%20%20%20%20%20%20%20sort_categories_by%3D%22cardinality%22%2C%0A%20%20%20%20%20%20%20%20min_subset_size%3D1%2C%0A%20%20%20%20%20%20%20%20facecolor%3D%22%234e79a7%22%2C%0A%20%20%20%20)%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20_fig_upset%20%3D%20plt.figure(figsize%3D(10%2C%205.5)%2C%20dpi%3D150)%0A%20%20%20%20%20%20%20%20_upset.plot(fig%3D_fig_upset)%0A%20%20%20%20%20%20%20%20_fig_upset.suptitle(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22Model%20disagreement%20%E2%80%94%20compounds%20with%20%7Cerror%7C%20%3E%20%7BBAD_THRESH%7D%20pEC50%20unit%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D12%2C%20y%3D1.02%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_fig_upset.savefig(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22..%2Fplots%2F5_unblinded_analysis%2Fupset_bad_predictions.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D300%2C%20bbox_inches%3D%22tight%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.center(mo.as_html(_fig_upset))%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Systematic%20bias%20analysis%0A%0A%20%20%20%20For%20each%20model%20we%20look%20at%20whether%20predictions%20are%20systematically%20shifted%0A%20%20%20%20(biased%20high%20or%20low)%20within%20different%20activity%20bins.%20%20A%20model%20that%20performs%0A%20%20%20%20well%20overall%20but%20is%20biased%20in%20the%20hit%20region%20is%20particularly%20dangerous%20for%0A%20%20%20%20virtual%20screening.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(all_predictions%3A%20%22pl.DataFrame%22%2C%20pl)%3A%0A%20%20%20%20%23%20Bin%20compounds%20by%20true%20pEC50%20into%20four%20activity%20ranges.%0A%20%20%20%20binned%3A%20pl.DataFrame%20%3D%20all_predictions.with_columns(%0A%20%20%20%20%20%20%20%20pl.when(pl.col(%22pEC50_true%22)%20%3E%3D%206.0)%0A%20%20%20%20%20%20%20%20.then(pl.lit(%22%3E6%20(hit%20zone)%22))%0A%20%20%20%20%20%20%20%20.when(pl.col(%22pEC50_true%22)%20%3E%3D%205.0)%0A%20%20%20%20%20%20%20%20.then(pl.lit(%225%E2%80%936%20(moderate)%22))%0A%20%20%20%20%20%20%20%20.when(pl.col(%22pEC50_true%22)%20%3E%3D%204.0)%0A%20%20%20%20%20%20%20%20.then(pl.lit(%224%E2%80%935%20(weak)%22))%0A%20%20%20%20%20%20%20%20.otherwise(pl.lit(%22%3C4%20(inactive)%22))%0A%20%20%20%20%20%20%20%20.alias(%22pec50_bin%22)%0A%20%20%20%20)%0A%0A%20%20%20%20bias_by_bin%3A%20pl.DataFrame%20%3D%20(%0A%20%20%20%20%20%20%20%20binned%0A%20%20%20%20%20%20%20%20.group_by(%5B%22model_name%22%2C%20%22pec50_bin%22%5D)%0A%20%20%20%20%20%20%20%20.agg(%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22error%22).mean().alias(%22mean_error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22abs_error%22).mean().alias(%22mean_abs_error%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.len().alias(%22n%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20.sort(%5B%22model_name%22%2C%20%22pec50_bin%22%5D)%0A%20%20%20%20)%0A%0A%20%20%20%20bias_by_bin%0A%20%20%20%20return%20(bias_by_bin%2C)%0A%0A%0A%40app.cell%0Adef%20_(bias_by_bin%3A%20%22pl.DataFrame%22%2C%20mo%2C%20np%2C%20plt)%3A%0A%20%20%20%20_bin_order%20%3D%20%5B%22%3C4%20(inactive)%22%2C%20%224%E2%80%935%20(weak)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%225%E2%80%936%20(moderate)%22%2C%20%22%3E6%20(hit%20zone)%22%5D%0A%0A%20%20%20%20_short%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%22Ensemble%20(cp%C2%B7ch%C2%B7xg%C2%B7mc%C2%B7tf)%22%3A%20%20%20%20%20%20%22Ensemble%22%2C%0A%20%20%20%20%20%20%20%20%22CheMeleon%20HPO%20(ch)%22%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22CheMeleon%20(ch)%22%2C%0A%20%20%20%20%20%20%20%20%22TabPFN%20%2F%20CheMeleon%20(tf)%22%3A%20%20%20%20%20%20%20%20%20%22TabPFN%20(tf)%22%2C%0A%20%20%20%20%20%20%20%20%22Macau%20HPO%20%2F%20CheMeleon%20(mc)%22%3A%20%20%20%20%20%20%22Macau%20(mc)%22%2C%0A%20%20%20%20%20%20%20%20%22Chemprop%20HPO%20(cp)%20%5Bregen%5D%22%3A%20%20%20%20%20%20%20%22Chemprop%20(cp)%22%2C%0A%20%20%20%20%20%20%20%20%22XGBoost%20%2F%20Mordred%20(xg)%20%5Bregen%5D%22%3A%20%20%22XGBoost%20(xg)%22%2C%0A%20%20%20%20%7D%0A%0A%20%20%20%20%23%20Pivot%20to%20a%20model%20%C3%97%20bin%20matrix%20of%20mean%20errors.%0A%20%20%20%20_model_names%20%3D%20sorted(bias_by_bin.get_column(%22model_name%22).unique().to_list()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20key%3Dlambda%20m%3A%20_short.get(m%2C%20m))%0A%20%20%20%20_matrix%20%3D%20np.full((len(_model_names)%2C%20len(_bin_order))%2C%20np.nan)%0A%20%20%20%20_n_matrix%20%3D%20np.full((len(_model_names)%2C%20len(_bin_order))%2C%200)%0A%0A%20%20%20%20for%20_row%20in%20bias_by_bin.iter_rows(named%3DTrue)%3A%0A%20%20%20%20%20%20%20%20_mi%20%3D%20_model_names.index(_row%5B%22model_name%22%5D)%0A%20%20%20%20%20%20%20%20if%20_row%5B%22pec50_bin%22%5D%20in%20_bin_order%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20_bi%20%3D%20_bin_order.index(_row%5B%22pec50_bin%22%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20_matrix%5B_mi%2C%20_bi%5D%20%3D%20_row%5B%22mean_error%22%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20_n_matrix%5B_mi%2C%20_bi%5D%20%3D%20_row%5B%22n%22%5D%0A%0A%20%20%20%20_abs_max%20%3D%20float(np.nanmax(np.abs(_matrix)))%0A%0A%20%20%20%20with%20plt.style.context(%22seaborn-v0_8-whitegrid%22)%3A%0A%20%20%20%20%20%20%20%20_fig_bias%2C%20_ax_bias%20%3D%20plt.subplots(figsize%3D(7%2C%204.5)%2C%20dpi%3D150)%0A%20%20%20%20%20%20%20%20_ax_bias.grid(False)%0A%20%20%20%20%20%20%20%20_im%20%3D%20_ax_bias.imshow(%0A%20%20%20%20%20%20%20%20%20%20%20%20_matrix%2C%20cmap%3D%22RdBu_r%22%2C%20vmin%3D-_abs_max%2C%20vmax%3D_abs_max%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20aspect%3D%22auto%22%2C%20interpolation%3D%22nearest%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%23%20Annotate%20each%20cell%20with%20the%20mean%20error%20value%20and%20sample%20size.%0A%20%20%20%20%20%20%20%20for%20_mi%20in%20range(len(_model_names))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20_bi%20in%20range(len(_bin_order))%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_val%20%3D%20_matrix%5B_mi%2C%20_bi%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_n%20%3D%20_n_matrix%5B_mi%2C%20_bi%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20not%20np.isnan(_val)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_txt_col%20%3D%20%22white%22%20if%20abs(_val)%20%3E%200.6%20*%20_abs_max%20else%20%22black%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_ax_bias.text(_bi%2C%20_mi%2C%20f%22%7B_val%3A%2B.3f%7D%5Cnn%3D%7B_n%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ha%3D%22center%22%2C%20va%3D%22center%22%2C%20fontsize%3D8%2C%20color%3D_txt_col)%0A%0A%20%20%20%20%20%20%20%20_ax_bias.set_xticks(range(len(_bin_order)))%0A%20%20%20%20%20%20%20%20_ax_bias.set_xticklabels(_bin_order%2C%20fontsize%3D10%2C%20rotation%3D-20%2C%20ha%3D%22left%22)%0A%20%20%20%20%20%20%20%20_ax_bias.set_yticks(range(len(_model_names)))%0A%20%20%20%20%20%20%20%20_ax_bias.set_yticklabels(%5B_short.get(m%2C%20m)%20for%20m%20in%20_model_names%5D%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20_ax_bias.set_xlabel(%22pEC50%20bin%22%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20_ax_bias.set_title(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Per-bin%20mean%20prediction%20error%5Cn(red%20%3D%20overpredict%2C%20blue%20%3D%20underpredict)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fontsize%3D12%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_cb%20%3D%20_fig_bias.colorbar(_im%2C%20ax%3D_ax_bias%2C%20fraction%3D0.04%2C%20pad%3D0.03)%0A%20%20%20%20%20%20%20%20_cb.set_label(%22Mean%20error%20(pred%20%E2%88%92%20true)%22%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20_fig_bias.tight_layout()%0A%20%20%20%20%20%20%20%20_fig_bias.savefig(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22..%2Fplots%2F5_unblinded_analysis%2Fbias_heatmap.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20dpi%3D300%2C%20bbox_inches%3D%22tight%22%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20mo.center(mo.as_html(_fig_bias))%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
d0d97ba0df0b10e4bf09318c87296fa3