report_utils.py 18 KB

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