bs-python-utils 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,518 @@
1
+ """ interface to scipy.optimize """
2
+ from dataclasses import dataclass
3
+ from math import sqrt
4
+ from typing import Any, Callable, Iterable, Optional, Union, cast
5
+
6
+ import numpy as np
7
+ import scipy.linalg as spla
8
+ import scipy.optimize as spopt
9
+
10
+ from bs_python_utils.bsnputils import TwoArrays, check_vector, npmaxabs
11
+ from bs_python_utils.bssputils import describe_array
12
+ from bs_python_utils.bsutils import bs_error_abort, print_stars
13
+ from bs_python_utils.Timer import timeit
14
+
15
+ ScalarFunctionAndGradient = Callable[
16
+ [np.ndarray, Iterable, Optional[bool]], Union[float, tuple[float, np.ndarray]]
17
+ ]
18
+ """Type of f(v, args, gr) that returns a scalar value and also a gradient if gr is True"""
19
+
20
+
21
+ ProximalFunction = Callable[[np.ndarray, float, Iterable], np.ndarray]
22
+ """Type of h(x, t, pars) that returns a scalar value"""
23
+
24
+
25
+ @dataclass
26
+ class OptimizeParams:
27
+ """
28
+ used for optimization;
29
+ combines values, bounds and initial values for a parameter vector
30
+ """
31
+
32
+ params_values: np.ndarray | None
33
+ params_bounds: list[tuple] | None
34
+ params_init: np.ndarray | None
35
+
36
+
37
+ def print_optimization_results(
38
+ resus: spopt.OptimizeResult, title: str = "Minimizing"
39
+ ) -> None:
40
+ """
41
+ print results from unconstrained optimization
42
+
43
+ Args:
44
+ resus: results from optimization
45
+ title: a title
46
+
47
+ Returns:
48
+ just prints
49
+ """
50
+ print_stars(title)
51
+ print(resus.message)
52
+ if resus.success:
53
+ print(f"Successful! in {resus.nit} iterations")
54
+ print(f" evaluated {resus.nfev} functions functions and {resus.njev} gradients")
55
+ print("\nMinimizer and grad_f:")
56
+ print(np.column_stack((resus.x, resus.jac)))
57
+ print(f"Minimized value is {resus.fun}")
58
+ else:
59
+ print_stars("Minimization failed!")
60
+ return
61
+
62
+
63
+ def print_constrained_optimization_results(
64
+ resus: spopt.OptimizeResult,
65
+ title: str = "Minimizing",
66
+ print_constr: bool = False,
67
+ print_multipliers: bool = False,
68
+ ) -> None:
69
+ """
70
+ print results from constrained optimization
71
+
72
+ Args:
73
+ resus: results from optimization
74
+ str title: a title
75
+ print_constr: if `True`, print the values of the constraints
76
+ print_multipliers: if `True`, print the values of the multipliers
77
+
78
+ Returns:
79
+ just prints
80
+ """
81
+ print_stars(title)
82
+ print(resus.message)
83
+ if resus.success:
84
+ print(f"Successful! in {resus.nit} iterations")
85
+ print(f" evaluated {resus.nfev} functions and {resus.njev} gradients")
86
+ print(f"Minimized value is {resus.fun}")
87
+ print(f"The Lagrangian norm is {resus.optimality}")
88
+ print(f"The largest constraint violation is {resus.constr_violation}")
89
+ if print_multipliers:
90
+ print(f"The multipliers are {resus.v}")
91
+ if print_constr:
92
+ print(f"The values of the constraints are {resus.constr}")
93
+ else:
94
+ print_stars("Constrained minimization failed!")
95
+ return
96
+
97
+
98
+ def armijo_alpha(
99
+ f: Callable,
100
+ x: np.ndarray,
101
+ d: np.ndarray,
102
+ args: Iterable,
103
+ alpha_init: float = 1.0,
104
+ beta: float = 0.5,
105
+ max_iter: int = 100,
106
+ tol: float = 0.0,
107
+ ) -> float:
108
+ """Given a function `f` we are minimizing, computes the step size `alpha`
109
+ to take in the direction `d` using the Armijo rule
110
+
111
+ Args:
112
+ f: the function
113
+ x: the current point
114
+ d: the direction we are taking
115
+ args: other arguments passed to `f`
116
+ alpha_init: the initial step size
117
+ beta: the step size reduction factor
118
+ max_iter: the maximum number of iterations
119
+ tol: a tolerance
120
+
121
+ Returns:
122
+ the step size alpha
123
+ """
124
+ f0 = f(x, args)
125
+ alpha = alpha_init
126
+ for _ in range(max_iter):
127
+ x1 = x + alpha * d
128
+ f1 = f(x1, args)
129
+ if f1 < f0 + tol:
130
+ return alpha
131
+ alpha *= beta
132
+ else:
133
+ bs_error_abort("Too many iterations")
134
+ return alpha
135
+
136
+
137
+ def barzilai_borwein_alpha(
138
+ grad_f: Callable, x: np.ndarray, args: Iterable
139
+ ) -> tuple[float, np.ndarray]:
140
+ """Given a function `f` we are minimizing, computes the step size `alpha`
141
+ to take in the opposite direction of the gradient using the Barzilai-Borwein rule
142
+
143
+ Args:
144
+ grad_f: the gradient of the function
145
+ x: the current point
146
+ args: other arguments passed to `f`
147
+
148
+ Returns:
149
+ the step size `alpha` and the gradient `g` at the point `x`
150
+ """
151
+ g = grad_f(x, args)
152
+ alpha = 1.0 / spla.norm(g)
153
+ x_hat = x - alpha * g
154
+ g_hat = grad_f(x_hat, args)
155
+ norm_dg = spla.norm(g - g_hat)
156
+ norm_dg2 = norm_dg * norm_dg
157
+ alpha = np.abs(np.dot(x - x_hat, g - g_hat)) / norm_dg2
158
+ return alpha, g
159
+
160
+
161
+ def check_gradient_scalar_function(
162
+ fg: ScalarFunctionAndGradient,
163
+ p: np.ndarray,
164
+ args: Iterable,
165
+ mode: str = "central",
166
+ EPS: float = 1e-6,
167
+ ) -> TwoArrays:
168
+ """Checks the gradient of a scalar function
169
+
170
+ Args:
171
+ fg: should return the scalar value, and the gradient if its `gr` argument is `True`
172
+ p: where we are checking the gradient
173
+ args: other arguments passed to `fg`
174
+ mode: "central" or "forward" derivatives
175
+ EPS: the step for forward or central derivatives
176
+
177
+ Returns:
178
+ the analytic and numeric gradients
179
+ """
180
+ f0, f_grad = fg(p, args, gr=True) # type: ignore
181
+ f0 = cast(float, f0)
182
+
183
+ print_stars("checking the gradient: analytic, numeric")
184
+
185
+ g = np.zeros_like(p)
186
+ if mode == "central":
187
+ for i, x in enumerate(p):
188
+ p1 = p.copy()
189
+ p1[i] = x + EPS
190
+ f_plus = cast(float, fg(p1, args, gr=False)) # type: ignore
191
+ p1[i] -= 2.0 * EPS
192
+ f_minus = cast(float, fg(p1, args, gr=False)) # type: ignore
193
+ g[i] = (f_plus - f_minus) / (2.0 * EPS)
194
+ print(f"{i}: {f_grad[i]}, {g[i]}")
195
+ elif mode == "forward":
196
+ for i, x in enumerate(p):
197
+ p1 = p.copy()
198
+ p1[i] = x + EPS
199
+ f_plus = cast(float, fg(p1, args, gr=False)) # type: ignore
200
+ g[i] = (f_plus - f0) / EPS
201
+ print(f"{i}: {f_grad[i]}, {g[i]}")
202
+ else:
203
+ bs_error_abort("mode must be 'central' or 'forward'")
204
+
205
+ return f_grad, g
206
+
207
+
208
+ @timeit
209
+ def acc_grad_descent(
210
+ grad_f: Callable,
211
+ x_init: np.ndarray,
212
+ other_params: Iterable,
213
+ prox_h: ProximalFunction | None = None,
214
+ print_result: bool = False,
215
+ verbose: bool = False,
216
+ tol: float = 1e-9,
217
+ alpha: float = 1.01,
218
+ beta: float = 0.5,
219
+ maxiter: int = 10000,
220
+ ) -> tuple[np.ndarray, int]:
221
+ """
222
+ minimizes `(f+h)` by Accelerated Gradient Descent
223
+ where `f` is smooth and convex and `h` is convex.
224
+
225
+ By default `h` is zero.
226
+
227
+ Args:
228
+ grad_f: grad_f of `f`; should return an `(n)` array from an `(n)` array \
229
+ and the `other_ params` object
230
+ x_init: initial guess, shape `(n)`
231
+ prox_h: proximal projector of `h`, if any
232
+ other_params: an iterable with additional parameters
233
+ verbose: if `True`, print diagnosis
234
+ tol: convergence criterion on absolute grad_f
235
+ alpha: ceiling on step multiplier
236
+ beta: floor on step multiplier
237
+ maxiter: max number of iterations
238
+
239
+ Returns:
240
+ the candidate solution, and 1 if converged/0 if not
241
+ """
242
+
243
+ # no proximal projection if no h
244
+ local_prox_h: ProximalFunction = prox_h if prox_h else lambda x, t, p: x
245
+
246
+ x = x_init.copy()
247
+ y = x_init.copy()
248
+
249
+ # for stepsize we use Barzilai-Borwein
250
+ t, g = barzilai_borwein_alpha(grad_f, y, other_params)
251
+
252
+ grad_err_init = npmaxabs(g)
253
+
254
+ if verbose:
255
+ print(f"agd: grad_err_init={grad_err_init}")
256
+
257
+ n_iter = 0
258
+ theta = 1.0
259
+
260
+ while n_iter < maxiter:
261
+ grad_err = npmaxabs(g)
262
+ if grad_err < tol:
263
+ break
264
+ xi = x
265
+ yi = y
266
+ x = y - t * g
267
+ x = local_prox_h(x, t, other_params)
268
+
269
+ theta = 2.0 / (1.0 + sqrt(1.0 + 4.0 / theta / theta))
270
+
271
+ if np.dot(y - x, x - xi) > 0: # wrong direction, we restart
272
+ x = xi
273
+ y = x
274
+ theta = 1.0
275
+ else:
276
+ y = x + (1.0 - theta) * (x - xi)
277
+
278
+ gi = g
279
+ g = grad_f(y, other_params)
280
+ ndy = spla.norm(y - yi)
281
+ t_hat = 0.5 * ndy * ndy / abs(np.dot(y - yi, gi - g))
282
+ t = min(alpha * t, max(beta * t, t_hat))
283
+
284
+ n_iter += 1
285
+
286
+ if verbose:
287
+ print(f" AGD with grad_err = {grad_err} after {n_iter} iterations")
288
+
289
+ x_conv = y
290
+
291
+ ret_code = 0 if grad_err < tol else 1
292
+
293
+ if verbose or print_result:
294
+ if ret_code == 0:
295
+ print_stars(
296
+ f" AGD converged with grad_err = {grad_err} after {iter} iterations"
297
+ )
298
+ else:
299
+ print_stars(
300
+ f" Problem in AGD: grad_err = {grad_err} after {iter} iterations"
301
+ )
302
+
303
+ return (x_conv, ret_code)
304
+
305
+
306
+ def _fix_some(
307
+ obj: Callable, grad_obj: Callable, fixed_vars: list[int], fixed_vals: np.ndarray
308
+ ) -> tuple[Callable, Callable]:
309
+ """
310
+ Takes in a function and its gradient, fixes the variables
311
+ whose indices are `fixed_vars` to the values in `fixed_vals`,
312
+ and returns the modified function and its gradient
313
+
314
+ Args:
315
+ obj: the original function
316
+ grad_obj: its gradient function
317
+ fixed_vars: a list if the indices of variables whose values are fixed
318
+ fixed_vals: their fixed values
319
+
320
+ Returns:
321
+ the modified function and its modified gradient function
322
+ """
323
+
324
+ def fixed_obj(t, other_args):
325
+ t_full = list(t)
326
+ for i, i_coef in enumerate(fixed_vars):
327
+ t_full.insert(i_coef, fixed_vals[i])
328
+ arr_full = np.array(t_full)
329
+ return obj(arr_full, other_args)
330
+
331
+ def fixed_grad_obj(t, other_args):
332
+ t_full = list(t)
333
+ for i, i_coef in enumerate(fixed_vars):
334
+ t_full.insert(i_coef, fixed_vals[i])
335
+ arr_full = np.array(t_full)
336
+ grad_full = grad_obj(arr_full, other_args)
337
+ return np.delete(grad_full, fixed_vars)
338
+
339
+ return fixed_obj, fixed_grad_obj
340
+
341
+
342
+ def minimize_some_fixed(
343
+ obj: Callable,
344
+ grad_obj: Callable,
345
+ x_init: np.ndarray,
346
+ args: Iterable,
347
+ fixed_vars: list[int] | None,
348
+ fixed_vals: np.ndarray | None,
349
+ options: dict | None = None,
350
+ bounds: list[tuple[float, float]] | None = None,
351
+ ) -> Any:
352
+ """
353
+ minimize a function with some variables fixed, using L-BFGS-B
354
+
355
+ Args:
356
+ obj: the original function
357
+ grad_obj: its gradient function
358
+ fixed_vars: a list if the indices of variables whose values are fixed
359
+ fixed_vals: their fixed values
360
+ x_init: the initial values of all variables (those on fixed variables are not used)
361
+ args: other parameters
362
+ options: any options passed on to scipy.optimize.minimize
363
+ bounds: the bounds on all variables (those on fixed variables are not used)
364
+
365
+ Returns:
366
+ the result of optimization, on all variables
367
+ """
368
+ if fixed_vars is None:
369
+ resopt = spopt.minimize(
370
+ obj,
371
+ x_init,
372
+ method="L-BFGS-B",
373
+ args=args,
374
+ options=options,
375
+ jac=grad_obj,
376
+ bounds=bounds,
377
+ )
378
+ else:
379
+ fixed_vars = cast(list, fixed_vars)
380
+ n_fixed = check_vector(fixed_vals)
381
+ fixed_vals = cast(np.ndarray, fixed_vals)
382
+ if len(fixed_vars) != n_fixed:
383
+ bs_error_abort(
384
+ f"fixed_vars has {len(fixed_vars)} indices but fixed_vals has"
385
+ f" {fixed_vals.size} elements."
386
+ )
387
+ fixed_obj, fixed_grad_obj = _fix_some(obj, grad_obj, fixed_vars, fixed_vals)
388
+
389
+ # drop fixed variables and the corresponding bounds
390
+ n = len(x_init)
391
+ not_fixed = np.ones(n, dtype=bool)
392
+ not_fixed[fixed_vars] = False
393
+ t_init = x_init[not_fixed]
394
+ t_bounds = (
395
+ None if bounds is None else [bounds[i] for i in range(n) if not_fixed[i]]
396
+ )
397
+
398
+ resopt = spopt.minimize(
399
+ fixed_obj,
400
+ t_init,
401
+ method="L-BFGS-B",
402
+ args=args,
403
+ options=options,
404
+ jac=fixed_grad_obj,
405
+ bounds=t_bounds,
406
+ )
407
+
408
+ # now re-fill the values of the variables
409
+ t = resopt.x
410
+ t_full = list(t)
411
+ for i, i_coef in enumerate(fixed_vars):
412
+ t_full.insert(i_coef, fixed_vals[i])
413
+ resopt.x = t_full
414
+
415
+ # and re-fill the values of the gradients
416
+ g = grad_obj(np.array(t_full), args)
417
+ resopt.jac = g
418
+
419
+ return resopt
420
+
421
+
422
+ def dfp_update(
423
+ hess_inv: np.ndarray, gradient_diff: np.ndarray, x_diff: np.ndarray
424
+ ) -> np.ndarray:
425
+ """runs a DFP update for the inverse Hessian
426
+
427
+ Args:
428
+ hess_inv: the current inverse Hessian
429
+ gradient_diff: the update in the gradient
430
+ x_diff: the update in x
431
+
432
+ Returns:
433
+ the updated inverse Hessian
434
+ """
435
+ xdt = x_diff.T
436
+ xxp = x_diff @ xdt
437
+ xpg = xdt @ gradient_diff
438
+ hdg = hess_inv @ gradient_diff
439
+ dgp_hdg = gradient_diff.T @ hdg
440
+ hess_inv_new = hess_inv + xxp / xpg - (hdg @ hdg.T) / dgp_hdg
441
+ return cast(np.ndarray, hess_inv_new)
442
+
443
+
444
+ def bfgs_update(
445
+ hess_inv: np.ndarray, gradient_diff: np.ndarray, x_diff: np.ndarray
446
+ ) -> np.ndarray:
447
+ """runs a DFP update for the inverse Hessian
448
+
449
+ Args:
450
+ hess_inv: the current inverse Hessian
451
+ gradient_diff: the update in the gradient
452
+ x_diff: the update in x
453
+
454
+ Returns:
455
+ the updated inverse Hessian
456
+ """
457
+ xdt = x_diff.T
458
+ xpg = xdt @ gradient_diff
459
+ hdg = hess_inv @ gradient_diff
460
+ dgp_hdg = gradient_diff.T @ hdg
461
+ u = x_diff / xpg - hdg / dgp_hdg
462
+ hess_inv_new = dfp_update(hess_inv, gradient_diff, x_diff) + dgp_hdg * (u @ u.T)
463
+ return cast(np.ndarray, hess_inv_new)
464
+
465
+
466
+ if __name__ == "__main__":
467
+ print_stars("Testing acc_grad_descent")
468
+
469
+ def grad_f(x, p):
470
+ xp = x - p[0]
471
+ return 4.0 * xp * xp * xp
472
+
473
+ x_init = np.random.normal(size=10000)
474
+
475
+ p = 1.0
476
+ x_conv, ret_code = acc_grad_descent(
477
+ grad_f, x_init, other_params=np.array([p]), tol=1e-12, verbose=False
478
+ )
479
+
480
+ describe_array(x_conv - p, "x-p should be close to zero")
481
+
482
+ def obj(x, args):
483
+ res = x - args
484
+ return np.sum(res * res)
485
+
486
+ def grad_obj(x, args):
487
+ res = x - args
488
+ return 2.0 * res
489
+
490
+ n = 5
491
+ x_init = np.full(n, 0.5)
492
+ args = np.arange(n)
493
+ bounds = [(-10.0, 10.0) for _ in range(n)]
494
+
495
+ fixed_vars = [1, 3]
496
+ fixed_vals = -np.ones(2)
497
+
498
+ resopt = minimize_some_fixed(
499
+ obj,
500
+ grad_obj,
501
+ x_init,
502
+ args,
503
+ fixed_vars=fixed_vars,
504
+ fixed_vals=fixed_vals,
505
+ bounds=bounds,
506
+ )
507
+
508
+ print(resopt)
509
+
510
+ # test the step routines
511
+ g = grad_obj(x_init, args)
512
+ alpha_a = armijo_alpha(obj, x_init, -g, args)
513
+ print(f"\nArmijo alpha={alpha_a}")
514
+
515
+ alpha_b, g_b = barzilai_borwein_alpha(grad_obj, x_init, args)
516
+ print(f"\nBarzilai-Borwein alpha={alpha_a}")
517
+ print("g and g_b:")
518
+ print(np.column_stack((g, g_b)))
@@ -0,0 +1,2 @@
1
+ """ a personal library of plots
2
+ """
@@ -0,0 +1,174 @@
1
+ """ personal library of Seaborn plots
2
+ """
3
+ from typing import Callable, cast
4
+
5
+ import matplotlib as mpl
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ import pandas as pd
9
+ import seaborn as sns
10
+
11
+ SeabornGraph = tuple[mpl.figure.Figure, mpl.figure.Axes]
12
+
13
+
14
+ def bs_sns_get_legend(g: mpl.axes.Axes) -> mpl.legend.Legend:
15
+ """
16
+ get the legend object of a Seaborn plot
17
+
18
+ Args:
19
+ g: the plot object
20
+
21
+ Returns:
22
+ leg: the associated Legend object
23
+ """
24
+
25
+ # check axes and find which is have legend
26
+ axs = g.axes
27
+ if isinstance(axs, mpl.axes.Axes): # only one Axes
28
+ leg = axs.get_legend()
29
+ else:
30
+ for ax in axs.flat():
31
+ leg = ax.get_legend()
32
+ if leg is not None:
33
+ break
34
+ # or legend may be on a figure
35
+ if leg is None:
36
+ leg = g._legend
37
+ return leg
38
+
39
+
40
+ def bs_sns_bar_x_byf(
41
+ df: pd.DataFrame,
42
+ xstr: str,
43
+ fstr: str,
44
+ statistic: Callable = np.mean,
45
+ label_x: str | None = None,
46
+ label_f: str | None = None,
47
+ title: str | None = None,
48
+ ) -> SeabornGraph:
49
+ """make a bar plot of x by f and g
50
+
51
+ Args:
52
+ df: dataframe, should contain columns `xstr` and `fstr`
53
+ xstr: column name of x
54
+ fstr: column name of f
55
+ statistic: statistic to plot (by default, the mean)
56
+ label_x: label of x
57
+ label_f: label of f
58
+ title: title of plot
59
+
60
+ Returns:
61
+ fig: figure
62
+ ax: axes
63
+ """
64
+ fig, ax = plt.subplots()
65
+ gbar = sns.barplot(
66
+ x=fstr,
67
+ y=xstr,
68
+ data=df,
69
+ estimator=statistic,
70
+ errcolor="r",
71
+ errwidth=0.75,
72
+ capsize=0.2,
73
+ ax=ax,
74
+ )
75
+ xlab = fstr if label_f is None else label_f
76
+ ylab = xstr if label_x is None else label_x
77
+ ax.set_xlabel(xlab)
78
+ ax.set_ylabel(ylab)
79
+ if title is not None:
80
+ ax.set_title(title)
81
+ return cast(SeabornGraph, gbar)
82
+
83
+
84
+ def bs_sns_bar_x_byfg(
85
+ df: pd.DataFrame,
86
+ xstr: str,
87
+ fstr: str,
88
+ gstr: str,
89
+ statistic: Callable = np.mean,
90
+ label_x: str | None = None,
91
+ label_f: str | None = None,
92
+ label_g: str | None = None,
93
+ title: str | None = None,
94
+ ) -> SeabornGraph:
95
+ """make a bar plot of x by f and g
96
+
97
+ Args:
98
+ df: dataframe, should contain columns `xstr`, `fstr`, and `gstr`
99
+ xstr: column name of x
100
+ fstr: column name of f
101
+ gstr: column name of g
102
+ statistic: statistic to plot (by default, the mean)
103
+ label_x: label of x
104
+ label_f: label of f
105
+ label_g: label of g in legend
106
+ title: title of plot
107
+
108
+ Returns:
109
+ fig: figure
110
+ ax: axes
111
+ """
112
+ fig, ax = plt.subplots()
113
+ gbar = sns.barplot(
114
+ x=fstr,
115
+ y=xstr,
116
+ data=df,
117
+ hue=gstr,
118
+ estimator=statistic,
119
+ errcolor="r",
120
+ errwidth=0.75,
121
+ capsize=0.2,
122
+ ax=ax,
123
+ )
124
+ xlab = fstr if label_f is None else label_f
125
+ ylab = xstr if label_x is None else label_x
126
+ ax.set_xlabel(xlab)
127
+ ax.set_ylabel(ylab)
128
+ if label_g is not None:
129
+ gbar_legend = bs_sns_get_legend(gbar)
130
+ gbar_legend.set_title(label_g)
131
+ if title is not None:
132
+ ax.set_title(title)
133
+ return cast(SeabornGraph, gbar)
134
+
135
+
136
+ def bs_sns_density_estimates(
137
+ df: pd.DataFrame,
138
+ true_values: np.ndarray,
139
+ method_string: str | None = "Estimator",
140
+ coeff_string: str | None = "Parameter",
141
+ estimate_string: str | None = "Estimate",
142
+ max_cols: int = 3,
143
+ ) -> sns.FacetGrid:
144
+ """
145
+ plot of the densities of estimates of several coefficients with several methods,
146
+ superposed by methods and faceted by coefficients
147
+
148
+ Args:
149
+ df: contains columns `method_string`, `coeff_name`, `estimate_value`
150
+ true_values: the true values of the coefficients
151
+ method_string: the name of the column that indicates the method
152
+ coeff_string: the name of the column that indicates the coefficient
153
+ estimate_string: the name of the column that gives the value of the estimate
154
+ max_cols: we wrap after that
155
+
156
+ Returns:
157
+ the `FacetGrid`
158
+
159
+ """
160
+ g = sns.FacetGrid(
161
+ data=df,
162
+ sharex=False,
163
+ sharey=False,
164
+ hue=method_string,
165
+ col=coeff_string,
166
+ col_wrap=max_cols,
167
+ )
168
+ g.map(sns.kdeplot, estimate_string)
169
+ g.set_titles("{col_name}")
170
+ for true_val, ax in zip(true_values, g.axes.ravel(), strict=True):
171
+ ax.vlines(true_val, *ax.get_ylim(), color="k", linestyles="dashed")
172
+ g.add_legend()
173
+
174
+ return g