report_utils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. import matplotlib.pyplot as plt
  2. import numpy as np
  3. import pandas as pd
  4. import os
  5. def build_plot_data(df_data, df_fit_index, alpha, lg, n_grid=100, verbose=False):
  6. """
  7. Compute all quantities needed for plotting and return them as dataframes.
  8. Parameters
  9. ----------
  10. df_data : pd.DataFrame
  11. Input data with columns including scale, dataset, X, Y.
  12. df_fit_index : pd.DataFrame
  13. Fit results indexed by (scale, dataset), with columns 'pars' and 'cov'.
  14. alpha : float
  15. Two-sided significance level for confidence intervals.
  16. Example: alpha = 0.05 gives a 95% CI.
  17. lg : object
  18. Model object with methods:
  19. - model(x, pars)
  20. - get_model_quantiles_normal(x, probs, pars, cov)
  21. - get_model_quantiles_delta(x, probs, pars, cov)
  22. n_grid : int, default 100
  23. Number of x-grid points for plotting.
  24. verbose : bool, default False
  25. Print progress information.
  26. Returns
  27. -------
  28. df_points : pd.DataFrame
  29. Original observed data, one row per observation.
  30. df_fit_plot : pd.DataFrame
  31. Fitted curve on a grid, one row per x-grid value.
  32. df_ci_plot : pd.DataFrame
  33. Confidence interval bands on a grid, one row per x-grid value and method.
  34. """
  35. probs = [alpha / 2, 1 - alpha / 2]
  36. confidence = 100 * (1 - alpha)
  37. points_list = []
  38. fit_list = []
  39. ci_list = []
  40. for (scale, dataset), g in df_data.groupby(["scale", "dataset"]):
  41. if verbose:
  42. print(f"Calculating: {scale}, {dataset}")
  43. X = g["X"].to_numpy()
  44. Y = g["Y"].to_numpy()
  45. # store raw points
  46. tmp_points = g[["X", "Y"]].copy()
  47. tmp_points["scale"] = scale
  48. tmp_points["dataset"] = dataset
  49. tmp_points["class_label"] = tmp_points["Y"].map({0: "NC", 1: "AE"})
  50. points_list.append(tmp_points[["scale", "dataset", "X", "Y", "class_label"]])
  51. # get fit objects
  52. fit_pars = df_fit_index.loc[(scale, dataset), "pars"]
  53. fit_cov = df_fit_index.loc[(scale, dataset), "cov"]
  54. # grid for smooth curve / bands
  55. x_fit = np.linspace(X.min(), X.max(), n_grid)
  56. # fitted curve
  57. y_fit = lg.model(x_fit, fit_pars)
  58. fit_list.append(
  59. pd.DataFrame({
  60. "scale": scale,
  61. "dataset": dataset,
  62. "X": x_fit,
  63. "y_fit": y_fit,
  64. })
  65. )
  66. # confidence bands
  67. for method in ["normal", "delta"]:
  68. qfun = getattr(lg, f"get_model_quantiles_{method}")
  69. y_low, y_high = qfun(x_fit, probs, fit_pars, fit_cov)
  70. ci_list.append(
  71. pd.DataFrame({
  72. "scale": scale,
  73. "dataset": dataset,
  74. "method": method,
  75. "confidence": confidence,
  76. "X": x_fit,
  77. "y_low": y_low,
  78. "y_high": y_high,
  79. })
  80. )
  81. df_points = pd.concat(points_list, ignore_index=True)
  82. df_fit_plot = pd.concat(fit_list, ignore_index=True)
  83. df_ci_plot = pd.concat(ci_list, ignore_index=True)
  84. return df_points, df_fit_plot, df_ci_plot
  85. def plot_from_dataframes(
  86. df_points,
  87. df_fit_plot,
  88. df_ci_plot,
  89. xlabs,
  90. scales=None,
  91. datasets=None,
  92. results_path=None,
  93. filename="logit_fit_CI_simple_paper.pdf",
  94. verbose=False,
  95. color_map={
  96. "data_NC": "blue",
  97. "data_AE": "orange",
  98. "fit": "black",
  99. "normal": "red",
  100. "delta": "green",
  101. },
  102. ls_map={
  103. "fit": "-",
  104. "normal": "--",
  105. "delta": "-.",
  106. },
  107. ci_alpha=0.15,
  108. ci_linewidth=1.5,
  109. fit_linewidth=2.0,
  110. width=5,
  111. height=4,
  112. ):
  113. if scales is None:
  114. scales = sorted(df_points["scale"].unique())
  115. if datasets is None:
  116. datasets = sorted(df_points["dataset"].unique())
  117. fig, axs = plt.subplots(
  118. ncols=len(scales),
  119. nrows=len(datasets),
  120. figsize=(width * len(scales), height * len(datasets)),
  121. layout="constrained",
  122. squeeze=False,
  123. )
  124. plot_labs = dict(
  125. zip(
  126. [(scale, dataset) for dataset in datasets for scale in scales],
  127. ["A", "B", "C", "D"][: len(scales) * len(datasets)],
  128. )
  129. )
  130. if verbose:
  131. print(plot_labs)
  132. for dataset_i, dataset in enumerate(datasets):
  133. for scale_i, scale in enumerate(scales):
  134. ax = axs[dataset_i, scale_i]
  135. ax.set_xlabel(xlabs[scale])
  136. ax.set_ylabel("P(AE|X = x)")
  137. ax.text(
  138. 0.05,
  139. 0.9,
  140. plot_labs[(scale, dataset)],
  141. fontsize=14,
  142. transform=ax.transAxes,
  143. )
  144. # raw points
  145. g_points = df_points[
  146. (df_points["scale"] == scale) & (df_points["dataset"] == dataset)
  147. ]
  148. for lab in ["NC", "AE"]:
  149. sub = g_points[g_points["class_label"] == lab]
  150. ax.scatter(
  151. sub["X"],
  152. sub["Y"],
  153. label=f"data: {lab}",
  154. alpha=0.5,
  155. color=color_map[f"data_{lab}"],
  156. )
  157. # fitted curve
  158. g_fit = df_fit_plot[
  159. (df_fit_plot["scale"] == scale) & (df_fit_plot["dataset"] == dataset)
  160. ].sort_values("X")
  161. ax.plot(
  162. g_fit["X"],
  163. g_fit["y_fit"],
  164. label="fit",
  165. color=color_map["fit"],
  166. linestyle=ls_map["fit"],
  167. linewidth=fit_linewidth,
  168. )
  169. # CI bands + border lines
  170. g_ci = df_ci_plot[
  171. (df_ci_plot["scale"] == scale) & (df_ci_plot["dataset"] == dataset)
  172. ]
  173. for method, sub in g_ci.groupby("method"):
  174. sub = sub.sort_values("X")
  175. color = color_map[method]
  176. ls = ls_map[method]
  177. confidence = sub["confidence"].iloc[0]
  178. # format nicely: 95 instead of 95.0 when possible
  179. conf_str = f"{confidence:.0f}" if float(confidence).is_integer() else f"{confidence:.1f}"
  180. # light filled band
  181. ax.fill_between(
  182. sub["X"],
  183. sub["y_low"],
  184. sub["y_high"],
  185. color=color,
  186. alpha=ci_alpha,
  187. linewidth=0,
  188. )
  189. # lower border
  190. ax.plot(
  191. sub["X"],
  192. sub["y_low"],
  193. color=color,
  194. linestyle=ls,
  195. linewidth=ci_linewidth,
  196. label=f"CI: {method} {conf_str}%",
  197. )
  198. # upper border
  199. ax.plot(
  200. sub["X"],
  201. sub["y_high"],
  202. color=color,
  203. linestyle=ls,
  204. linewidth=ci_linewidth,
  205. )
  206. for ax in axs.flat:
  207. ax.label_outer()
  208. handles, labels = axs[0, 0].get_legend_handles_labels()
  209. by_label = dict(zip(labels, handles))
  210. fig.legend(
  211. by_label.values(),
  212. by_label.keys(),
  213. bbox_to_anchor=(0.975, 0.2),
  214. framealpha=0.5,
  215. loc="center right",
  216. )
  217. if results_path is not None:
  218. plt.savefig(
  219. os.path.join(results_path, filename),
  220. bbox_inches="tight",
  221. )
  222. plt.show()
  223. import os
  224. import pickle
  225. import numpy as np
  226. import pandas as pd
  227. import matplotlib.pyplot as plt
  228. def load_or_build_bootstrap_pars(
  229. df_data,
  230. lg,
  231. results_path,
  232. filename="boots_pars_results.pkl",
  233. n_boots=10000,
  234. methods=("normal", "nonparam_boots", "nonparam_stratified_boots", "parametric_boots"),
  235. verbose=False,
  236. ):
  237. """
  238. Load bootstrap parameter results from disk if available, otherwise compute and save.
  239. Returns
  240. -------
  241. boots_pars_results : dict
  242. Dictionary with keys (scale, dataset, method) and values bootstrap parameter arrays.
  243. """
  244. result_file = os.path.join(results_path, filename)
  245. if os.path.isfile(result_file):
  246. with open(result_file, "rb") as f:
  247. boots_pars_results = pickle.load(f)
  248. return boots_pars_results
  249. boots_pars_results = {}
  250. for (scale, dataset), g in df_data.groupby(["scale", "dataset"]):
  251. if verbose:
  252. print(f"Bootstrap parameters: {scale}, {dataset}")
  253. X = g["X"].to_numpy()
  254. Y = g["Y"].to_numpy()
  255. for method in methods:
  256. if verbose:
  257. print(f"\tmethod: {method}")
  258. par_fun = getattr(lg, f"get_{method}_pars")
  259. boots_pars_results[(scale, dataset, method)] = par_fun(X, Y, m=n_boots)
  260. with open(result_file, "wb") as f:
  261. pickle.dump(boots_pars_results, f)
  262. return boots_pars_results
  263. def build_plot_data_full_ci(
  264. df_data,
  265. df_fit_index,
  266. lg,
  267. alpha,
  268. boots_pars_results,
  269. n_grid=100,
  270. analytic_methods=("normal", "delta"),
  271. bootstrap_methods=("nonparam_boots", "parametric_boots"),
  272. verbose=False,
  273. ):
  274. """
  275. Compute all quantities needed for plotting and return them as dataframes.
  276. Parameters
  277. ----------
  278. df_data : pd.DataFrame
  279. Input data with columns including scale, dataset, X, Y.
  280. df_fit_index : pd.DataFrame
  281. Fit results indexed by (scale, dataset), with columns 'pars' and 'cov'.
  282. lg : object
  283. Model object with methods:
  284. - model(x, pars)
  285. - get_model_quantiles_<method>(x, probs, pars, cov)
  286. alpha : float
  287. Two-sided significance level. Example alpha=0.05 gives 95% CI.
  288. boots_pars_results : dict
  289. Dictionary with keys (scale, dataset, method) and bootstrap parameter samples as values.
  290. n_grid : int
  291. Number of x-grid points.
  292. analytic_methods : tuple
  293. CI methods based on fitted covariance.
  294. bootstrap_methods : tuple
  295. CI methods based on bootstrap parameter samples.
  296. verbose : bool
  297. Print progress.
  298. Returns
  299. -------
  300. df_points : pd.DataFrame
  301. df_fit_plot : pd.DataFrame
  302. df_ci_plot : pd.DataFrame
  303. Long dataframe with both analytic and bootstrap confidence intervals.
  304. """
  305. probs = [alpha / 2, 1 - alpha / 2]
  306. confidence = 100 * (1 - alpha)
  307. points_list = []
  308. fit_list = []
  309. ci_list = []
  310. for (scale, dataset), g in df_data.groupby(["scale", "dataset"]):
  311. if verbose:
  312. print(f"Calculating plot data: {scale}, {dataset}")
  313. X = g["X"].to_numpy()
  314. Y = g["Y"].to_numpy()
  315. # raw points
  316. tmp_points = g[["X", "Y"]].copy()
  317. tmp_points["scale"] = scale
  318. tmp_points["dataset"] = dataset
  319. tmp_points["class_label"] = tmp_points["Y"].map({0: "NC", 1: "AE"})
  320. points_list.append(tmp_points[["scale", "dataset", "X", "Y", "class_label"]])
  321. # fitted objects
  322. fit_pars = df_fit_index.loc[(scale, dataset), "pars"]
  323. fit_cov = df_fit_index.loc[(scale, dataset), "cov"]
  324. # x-grid
  325. x_fit = np.linspace(X.min(), X.max(), n_grid)
  326. # fitted curve
  327. y_fit = lg.model(x_fit, fit_pars)
  328. fit_list.append(
  329. pd.DataFrame({
  330. "scale": scale,
  331. "dataset": dataset,
  332. "X": x_fit,
  333. "y_fit": y_fit,
  334. })
  335. )
  336. # analytic CIs
  337. for method in analytic_methods:
  338. if verbose:
  339. print(f"\tanalytic CI: {method}")
  340. qfun = getattr(lg, f"get_model_quantiles_{method}")
  341. y_low, y_high = qfun(x_fit, probs, fit_pars, fit_cov)
  342. ci_list.append(
  343. pd.DataFrame({
  344. "scale": scale,
  345. "dataset": dataset,
  346. "method": method,
  347. "ci_source": "analytic",
  348. "confidence": confidence,
  349. "X": x_fit,
  350. "y_low": y_low,
  351. "y_high": y_high,
  352. })
  353. )
  354. # bootstrap CIs
  355. for method in bootstrap_methods:
  356. if verbose:
  357. print(f"\tbootstrap CI: {method}")
  358. bpars = boots_pars_results[(scale, dataset, method)]
  359. quant = np.quantile([lg.model(x_fit, p) for p in bpars], probs, axis=0)
  360. y_low, y_high = quant
  361. ci_list.append(
  362. pd.DataFrame({
  363. "scale": scale,
  364. "dataset": dataset,
  365. "method": method,
  366. "ci_source": "bootstrap",
  367. "confidence": confidence,
  368. "X": x_fit,
  369. "y_low": y_low,
  370. "y_high": y_high,
  371. })
  372. )
  373. df_points = pd.concat(points_list, ignore_index=True)
  374. df_fit_plot = pd.concat(fit_list, ignore_index=True)
  375. df_ci_plot = pd.concat(ci_list, ignore_index=True)
  376. return df_points, df_fit_plot, df_ci_plot
  377. def plot_from_dataframes_full_ci(
  378. df_points,
  379. df_fit_plot,
  380. df_ci_plot,
  381. xlabs,
  382. scales=None,
  383. datasets=None,
  384. results_path=None,
  385. filename="logit_fit_CI_paper.pdf",
  386. verbose=False,
  387. color_map=None,
  388. ls_map=None,
  389. ci_alpha=0.12,
  390. ci_linewidth=1.5,
  391. fit_linewidth=2.0,
  392. width=5,
  393. height=4,
  394. ):
  395. """
  396. Plot data, fitted curve, and confidence intervals from prepared dataframes.
  397. """
  398. if scales is None:
  399. scales = list(df_points["scale"].drop_duplicates())
  400. if datasets is None:
  401. datasets = list(df_points["dataset"].drop_duplicates())
  402. if color_map is None:
  403. color_map = {
  404. "data_NC": "blue",
  405. "data_AE": "orange",
  406. "fit": "black",
  407. "normal": "red",
  408. "delta": "green",
  409. "nonparam_boots": "blue",
  410. "nonparam_stratified_boots": "purple",
  411. "parametric_boots": "cyan",
  412. }
  413. if ls_map is None:
  414. ls_map = {
  415. "fit": "-",
  416. "normal": "--",
  417. "delta": "-.",
  418. "nonparam_boots": ":",
  419. "nonparam_stratified_boots": (0, (3, 1, 1, 1)),
  420. "parametric_boots": (0, (5, 2)),
  421. }
  422. fig, axs = plt.subplots(
  423. ncols=len(scales),
  424. nrows=len(datasets),
  425. figsize=(width * len(scales), height * len(datasets)),
  426. layout="constrained",
  427. squeeze=False,
  428. )
  429. plot_labs = dict(
  430. zip(
  431. [(scale, dataset) for dataset in datasets for scale in scales],
  432. list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")[: len(scales) * len(datasets)],
  433. )
  434. )
  435. if verbose:
  436. print(plot_labs)
  437. for dataset_i, dataset in enumerate(datasets):
  438. for scale_i, scale in enumerate(scales):
  439. ax = axs[dataset_i, scale_i]
  440. ax.set_xlabel(xlabs[scale])
  441. ax.set_ylabel("P(AE|X = x)")
  442. ax.text(
  443. 0.05,
  444. 0.9,
  445. plot_labs[(scale, dataset)],
  446. fontsize=14,
  447. transform=ax.transAxes,
  448. )
  449. # raw points
  450. g_points = df_points[
  451. (df_points["scale"] == scale) & (df_points["dataset"] == dataset)
  452. ]
  453. for lab in ["NC", "AE"]:
  454. sub = g_points[g_points["class_label"] == lab]
  455. ax.scatter(
  456. sub["X"],
  457. sub["Y"],
  458. label=f"data: {lab}",
  459. alpha=0.5,
  460. color=color_map[f"data_{lab}"],
  461. )
  462. # fitted curve
  463. g_fit = df_fit_plot[
  464. (df_fit_plot["scale"] == scale) & (df_fit_plot["dataset"] == dataset)
  465. ].sort_values("X")
  466. ax.plot(
  467. g_fit["X"],
  468. g_fit["y_fit"],
  469. label="fit",
  470. color=color_map["fit"],
  471. linestyle=ls_map["fit"],
  472. linewidth=fit_linewidth,
  473. )
  474. # CI bands + border lines
  475. g_ci = df_ci_plot[
  476. (df_ci_plot["scale"] == scale) & (df_ci_plot["dataset"] == dataset)
  477. ]
  478. for method, sub in g_ci.groupby("method", sort=False):
  479. sub = sub.sort_values("X")
  480. color = color_map[method]
  481. ls = ls_map[method]
  482. confidence = sub["confidence"].iloc[0]
  483. conf_str = (
  484. f"{confidence:.0f}"
  485. if float(confidence).is_integer()
  486. else f"{confidence:.1f}"
  487. )
  488. ax.fill_between(
  489. sub["X"],
  490. sub["y_low"],
  491. sub["y_high"],
  492. color=color,
  493. alpha=ci_alpha,
  494. linewidth=0,
  495. )
  496. ax.plot(
  497. sub["X"],
  498. sub["y_low"],
  499. color=color,
  500. linestyle=ls,
  501. linewidth=ci_linewidth,
  502. label=f"CI: {method} {conf_str}%",
  503. )
  504. ax.plot(
  505. sub["X"],
  506. sub["y_high"],
  507. color=color,
  508. linestyle=ls,
  509. linewidth=ci_linewidth,
  510. )
  511. for ax in axs.flat:
  512. ax.label_outer()
  513. handles, labels = axs[0, 0].get_legend_handles_labels()
  514. by_label = dict(zip(labels, handles))
  515. fig.legend(
  516. by_label.values(),
  517. by_label.keys(),
  518. bbox_to_anchor=(0.975, 0.2),
  519. framealpha=0.5,
  520. loc="center right",
  521. )
  522. if results_path is not None:
  523. plt.savefig(
  524. os.path.join(results_path, filename),
  525. bbox_inches="tight",
  526. )
  527. plt.show()