code: def box_plot_per_model(
      cmaps: List[str],
      target_uncert_dicts: Dict[str, List[List[float]]],
      uncertainty_types_list: List[List[str]],
      models: List[str],
      x_axis_labels: List[str],
      x_label: str,
      y_label: str,
      num_bins: int,
      show_sample_info: str = "None",
      save_path: Optional[str] = None,
      y_lim: int = 120,
      convert_to_percent: bool = True,
      to_log: bool = False,
      show_individual_dots: bool = True,
  ) -> None:
      """
      Generates a box plot to visualize and compare the performance of different models across uncertainty bins.
  
      This function creates a box plot for each model, grouped by uncertainty types, and displays the
      distribution of data within each bin. Individual data points can be shown as dots and additional
      information such as the percentage of samples per bin can be displayed on top of the box plots.
  
      Args:
          cmaps (List[str]): List of colors for matplotlib.
          target_uncert_dicts (Dict[str, List[List[float]]]): Dict of pandas dataframes for the data to display.
          uncertainty_types_list (List[List[str]]): List of lists describing the different uncertainty combinations to test.
          models (List[str]): The models we want to compare, keys in target_uncert_dicts.
          x_axis_labels (List[str]): List of strings for the x-axis labels, one for each bin.
          x_label (str): x-axis label.
          y_label (str): y-axis label.
          num_bins (int): Number of uncertainty bins.
          show_sample_info (str): Show sample information. Options: "None", "All", "Average". Default is "None".
          save_path (Optional[str]): Path to save plot to. If None, displays on screen (default=None).
          y_lim (int): y-axis limit of graph (default=120).
          convert_to_percent (bool): Flag to turn data into percentages. Default is True.
          to_log (bool): Flag to set y-axis scale to log. Default is False.
          show_individual_dots (bool): Flag to show individual data points as dots. Default is True.
      """
  
      hatch_type = "o"
      plt.style.use("fivethirtyeight")
  
      orders = []
      ax = plt.gca()
  
      ax.xaxis.grid(False)
  
      bin_label_locs: List[float] = []
      all_rects = []
      outer_min_x_loc = 0.0
      middle_min_x_loc = 0.0
      inner_min_x_loc = 0.0
  
      circ_patches = []
      max_bin_height = 0.0
  
      all_sample_label_x_locs = []
      all_sample_percs = []
  
      for i, (uncert_pair) in enumerate(uncertainty_types_list):
          uncertainty_type = (uncert_pair)[0]
          for hash_idx, model_type in enumerate(models):
              inbetween_locs = []
              average_samples_per_bin = []
  
              for j in range(num_bins):
                  if j == 0:
                      if hash_idx == 1:
                          circ11 = patches.Patch(
                              facecolor=cmaps[i],
                              label=model_type + " " + uncertainty_type,
                              hatch=hatch_type,
                              edgecolor="black",
                          )
                      else:
                          circ11 = patches.Patch(
                              facecolor=cmaps[i],
                              label=model_type + " " + uncertainty_type,
                          )
                      circ_patches.append(circ11)
  
                  dict_key = [
                      x
                      for x in list(target_uncert_dicts.keys())
                      if (model_type in x) and (uncertainty_type in x)
                  ][0]
                  model_data = target_uncert_dicts[dict_key]
                  all_b_data = [
                      x for x in model_data[j] if x is not None
                  ]
  
                  orders.append(model_type + uncertainty_type)
  
                  width = 0.25
  
                  x_loc = [
                      (
                          outer_min_x_loc
                          + inner_min_x_loc
                          + middle_min_x_loc
                      )
                  ]
                  inbetween_locs.append(x_loc[0])
  
                  # Turn data to percentages
                  if convert_to_percent:
                      displayed_data = [(x) * 100 for x in all_b_data]
                  else:
                      displayed_data = all_b_data
                  rect = ax.boxplot(
                      displayed_data,
                      positions=x_loc,
                      sym="",
                      widths=width,
                      showmeans=True,
                      patch_artist=True,
                  )
  
                  if show_individual_dots:
                      # Add some random "jitter" to the x-axis
                      x = np.random.normal(
                          x_loc, 0.01, size=len(displayed_data)
                      )
                      ax.plot(
                          x,
                          displayed_data,
                          color=cmaps[len(uncertainty_types_list)],
                          marker=".",
                          linestyle="None",
                          alpha=0.75,
                      )
  
                  # Set color, pattern, median line and mean marker.
                  for r in rect["boxes"]:
                      r.set(color="black", linewidth=1)
                      r.set(facecolor=cmaps[i])
  
                      if hash_idx == 1:
                          r.set_hatch(hatch_type)
                  for median in rect["medians"]:
                      median.set(color="crimson", linewidth=3)
  
                  for mean in rect["means"]:
                      mean.set(
                          markerfacecolor="crimson",
                          markeredgecolor="black",
                          markersize=10,
                      )
  
                  max_bin_height = max(
                      max(rect["caps"][-1].get_ydata()), max_bin_height
                  )
  
                  """If we are showing sample statistics, keep track of it and display after on top of biggest whisker."""
                  if show_sample_info != "None":
                      flattened_model_data = [
                          x for xss in model_data for x in xss
                      ]
                      percent_size = np.round(
                          len(all_b_data)
                          / len(flattened_model_data)
                          * 100,
                          1,
                      )
                      average_samples_per_bin.append(percent_size)
  
                      if show_sample_info == "All":
                          """This adds the number of samples on top of the top whisker"""
                          (x_l, y), (x_r, _) = rect["caps"][
                              -1
                          ].get_xydata()
                          x_line_center = (x_l + x_r) / 2
                          all_sample_label_x_locs.append(x_line_center)
                          all_sample_percs.append(percent_size)
                  all_rects.append(rect)
  
                  inner_min_x_loc += 0.1 + width
  
              """ Keep track of average sample statistics. Plot at the END so we know what the max height for all Qs are."""
              if show_sample_info == "Average":
                  middle_x = np.mean(inbetween_locs)
                  mean_perc = np.round(
                      np.mean(average_samples_per_bin), 1
                  )
                  std_perc = np.round(
                      np.std(average_samples_per_bin), 1
                  )
                  all_sample_label_x_locs.append(middle_x)
                  all_sample_percs.append([mean_perc, std_perc])
  
              bin_label_locs = bin_label_locs + inbetween_locs
  
              # IF lots of bins we must make the gap between plots bigger to prevent overlapping x-tick labels.
              if num_bins > 9:
                  middle_min_x_loc += 0.25
              else:
                  middle_min_x_loc += 0.12
  
          outer_min_x_loc += 0.24
  
      format_plot(
          ax,
          save_path,
          show_sample_info,
          to_log,
          circ_patches,
          y_lim,
          -0.1,
          convert_to_percent,
          x_label,
          y_label,
          30,
          30,
          bin_label_locs,
          x_axis_labels,
          num_bins,
          uncertainty_types_list,
          all_sample_percs,
          all_sample_label_x_locs,
          max_bin_height,
      )
  
imports: import logging
  import math
  import os
  from typing import Any, Dict, List, Optional, Tuple, Union
  import matplotlib.lines
  import matplotlib.patches
  import matplotlib.pyplot
  import matplotlib.ticker
  import numpy
  import pandas
  import pwlf
  from matplotlib.ticker import ScalarFormatter
  from scipy import stats
  from sklearn.isotonic import IsotonicRegression
  from kale.evaluate.similarity_metrics import evaluate_correlations
  from kale.evaluate.uncertainty_metrics import evaluate_bounds, evaluate_jaccard, get_mean_errors
  from kale.prepdata.tabular_transform import generate_struct_for_qbin
  from kale.utils.save_xlsx import generate_summary_df
label_desc: Use image node_id 5 for calling a global function with example usage: box_plot_per_model(cmaps, target_uncert_dicts, uncertainty_types_list, models, x_axis_labels, x_label, y_label, num_bins, show_sample_info, save_path, y_lim, convert_to_percent, to_log, show_individual_dots) without return types