code: def calc_drift_and_plot(
      train_column: pd.Series,
      test_column: pd.Series,
      value_name: str,
      column_type: str,
      plot_title: Optional[str] = None,
      margin_quantile_filter: float = 0.025,
      max_num_categories_for_drift: Optional[int] = None,
      min_category_size_ratio: float = 0.01,
      max_num_categories_for_display: int = 10,
      show_categories_by: CategoriesSortingKind = "largest_difference",
      numerical_drift_method: str = "KS",
      categorical_drift_method: str = "cramers_v",
      balance_classes: bool = False,
      ignore_na: bool = True,
      min_samples: int = 10,
      raise_min_samples_error: bool = False,
      with_display: bool = True,
      dataset_names: Tuple[str, str] = DEFAULT_DATASET_NAMES,
  ) -> Tuple[float, str, Optional[Figure]]:
      """
      Calculate drift score per column.
  
      Parameters
      ----------
      train_column: pd.Series
          column from train dataset
      test_column: pd.Series
          same column from test dataset
      value_name: str
          title of the x axis, if plot_title is None then also the title of the whole plot.
      column_type: str
          type of column (either "numerical" or "categorical")
      plot_title: str or None
          if None use value_name as title otherwise use this.
      margin_quantile_filter: float, default: 0.025
          float in range [0,0.5), representing which margins (high and low quantiles) of the distribution will be filtered
          out of the EMD calculation. This is done in order for extreme values not to affect the calculation
          disproportionally. This filter is applied to both distributions, in both margins.
      min_category_size_ratio: float, default 0.01
          minimum size ratio for categories. Categories with size ratio lower than this number are binned
          into an "Other" category.
      max_num_categories_for_drift: int, default: None
          Max number of allowed categories. If there are more, they are binned into an "Other" category.
      max_num_categories_for_display: int, default: 10
          Max number of categories to show in plot.
      show_categories_by: str, default: 'largest_difference'
          Specify which categories to show for categorical features' graphs, as the number of shown categories is limited
          by max_num_categories_for_display. Possible values:
          - 'train_largest': Show the largest train categories.
          - 'test_largest': Show the largest test categories.
          - 'largest_difference': Show the largest difference between categories.
      numerical_drift_method: str, default: "KS"
          decides which method to use on numerical variables. Possible values are:
          "EMD" for Earth Mover's Distance (EMD), "KS" for Kolmogorov-Smirnov (KS).
      categorical_drift_method: str, default: "cramers_v"
          decides which method to use on categorical variables. Possible values are:
          "cramers_v" for Cramer's V, "PSI" for Population Stability Index (PSI).
      balance_classes: bool, default: False
          If True, all categories will have an equal weight in the Cramer's V score. This is useful when the categorical
          variable is highly imbalanced, and we want to be alerted on changes in proportion to the category size,
          and not only to the entire dataset. Must have categorical_drift_method = "cramers_v".
      ignore_na: bool, default True
          For categorical columns only. If True, ignores nones for categorical drift. If False, considers none as a
          separate category. For numerical columns we always ignore nones.
      min_samples : int , default: 10
          Minimum number of samples required to calculate the drift score. If any of the distributions have less than
          min_samples, the function will either raise an error or return an invalid output (depends on
          ``raise_min_sample_error``)
      raise_min_samples_error : bool , default: False
          Determines whether to raise an error if the number of samples is less than min_samples. If False, returns the
          output 'not_enough_samples', None, None.
      with_display: bool, default: True
          flag that determines if function will calculate display.
      dataset_names: tuple, default: DEFAULT_DATASET_NAMES
          The names to show in the display for the first and second datasets.
      Returns
      -------
      Tuple[float, str, Callable]
          - drift score of the difference between the two columns' distributions
          - method name
          - graph comparing the two distributions (density for numerical, stack bar for categorical)
      """
      if min_category_size_ratio < 0 or min_category_size_ratio > 1:
          raise DeepchecksValueError(
              f"min_category_size_ratio expected a value in range [0, 1], instead got {min_category_size_ratio}."
          )
  
      if column_type == "categorical" and ignore_na is False:
          train_dist = np.array(train_column.values).reshape(-1)
          test_dist = np.array(test_column.values).reshape(-1)
      else:
          train_dist = np.array(train_column.dropna().values).reshape(
              -1
          )
          test_dist = np.array(test_column.dropna().values).reshape(-1)
  
      if len(train_dist) < min_samples or len(test_dist) < min_samples:
          if raise_min_samples_error is True:
              raise NotEnoughSamplesError(
                  f"Not enough samples to calculate drift score. Minimum {min_samples} samples required. "
                  "Note that for numerical labels, None values do not count as samples."
                  "Use the 'min_samples' parameter to change this requirement."
              )
          else:
              return "not_enough_samples", None, None
  
      if column_type == "numerical":
          train_dist = train_dist.astype("float")
          test_dist = test_dist.astype("float")
  
          if numerical_drift_method.lower() == "emd":
              scorer_name = "Earth Mover's Distance"
              score = earth_movers_distance(
                  dist1=train_dist,
                  dist2=test_dist,
                  margin_quantile_filter=margin_quantile_filter,
              )
          elif numerical_drift_method.lower() in [
              "ks",
              "kolmogorov-smirnov",
          ]:
              scorer_name = "Kolmogorov-Smirnov"
              score = kolmogorov_smirnov(
                  dist1=train_dist, dist2=test_dist
              )
          else:
              raise DeepchecksValueError(
                  "Expected numerical_drift_method to be one "
                  f'of ["EMD", "KS"], received: {numerical_drift_method}'
              )
  
          if not with_display:
              return score, scorer_name, None
  
          bar_traces, bar_x_axis, bar_y_axis = drift_score_bar_traces(
              score
          )
          (
              dist_traces,
              dist_x_axis,
              dist_y_axis,
          ) = feature_distribution_traces(
              train_dist,
              test_dist,
              value_name,
              dataset_names=dataset_names,
          )
  
      elif column_type == "categorical":
          if (
              balance_classes is True
              and categorical_drift_method.lower()
              not in ["cramer_v", "cramers_v"]
          ):
              raise DeepchecksValueError(
                  "balance_classes is only supported for Cramer's V. please set balance_classes=False "
                  "or use 'cramers_v' as categorical_drift_method"
              )
  
          sort_by = (
              "difference"
              if show_categories_by == "largest_difference"
              else (
                  "dist1"
                  if show_categories_by == "train_largest"
                  else "dist2"
              )
          )
          if categorical_drift_method.lower() in [
              "cramer_v",
              "cramers_v",
          ]:
              scorer_name = "Cramer's V"
              score = cramers_v(
                  dist1=train_dist,
                  dist2=test_dist,
                  balance_classes=balance_classes,
                  min_category_size_ratio=min_category_size_ratio,
                  max_num_categories=max_num_categories_for_drift,
                  sort_by=sort_by,
              )
          elif categorical_drift_method.lower() == "psi":
              scorer_name = "PSI"
              score = psi(
                  dist1=train_dist,
                  dist2=test_dist,
                  min_category_size_ratio=min_category_size_ratio,
                  max_num_categories=max_num_categories_for_drift,
                  sort_by=sort_by,
              )
          else:
              raise DeepchecksValueError(
                  "Expected categorical_drift_method to be one "
                  f'of ["cramers_v", "PSI"], received: {categorical_drift_method}'
              )
  
          if not with_display:
              return score, scorer_name, None
  
          bar_traces, bar_x_axis, bar_y_axis = drift_score_bar_traces(
              score, bar_max=1
          )
          (
              dist_traces,
              dist_x_axis,
              dist_y_axis,
          ) = feature_distribution_traces(
              train_dist,
              test_dist,
              value_name,
              is_categorical=True,
              max_num_categories=max_num_categories_for_display,
              show_categories_by=show_categories_by,
              dataset_names=dataset_names,
          )
      else:
          # Should never reach here
          raise DeepchecksValueError(
              f"Unsupported column type for drift: {column_type}"
          )
  
      fig = make_subplots(
          rows=2,
          cols=1,
          vertical_spacing=0.2,
          shared_yaxes=False,
          shared_xaxes=False,
          row_heights=[0.1, 0.9],
          subplot_titles=[
              f"Drift Score ({scorer_name})",
              "Distribution Plot",
          ],
      )
  
      fig.add_traces(bar_traces, rows=1, cols=1)
      fig.update_xaxes(bar_x_axis, row=1, col=1)
      fig.update_yaxes(bar_y_axis, row=1, col=1)
      fig.add_traces(dist_traces, rows=2, cols=1)
      fig.update_xaxes(dist_x_axis, row=2, col=1)
      if balance_classes is True:
          dist_y_axis["title"] += " (Log Scale)"
          fig.update_yaxes(dist_y_axis, row=2, col=1, type="log")
      else:
          fig.update_yaxes(dist_y_axis, row=2, col=1)
  
      fig.update_layout(
          legend=dict(title="Legend", yanchor="top", y=0.6),
          height=400,
          title=dict(
              text=plot_title or value_name, x=0.5, xanchor="center"
          ),
          bargroupgap=0,
      )
  
      return score, scorer_name, fig
  
imports: from numbers import Number
  from typing import Dict, Optional, Tuple, Union
  import numpy
  import pandas
  from plotly.graph_objs import Figure
  from plotly.subplots import make_subplots
  from scipy.stats import chi2_contingency, wasserstein_distance
  from deepchecks.core import ConditionCategory, ConditionResult
  from deepchecks.core.errors import DeepchecksValueError, NotEnoughSamplesError
  from deepchecks.utils.dict_funcs import get_dict_entry_by_value
  from deepchecks.utils.distribution.plot import CategoriesSortingKind, drift_score_bar_traces, feature_distribution_traces
  from deepchecks.utils.distribution.preprocessing import preprocess_2_cat_cols_to_same_bins
  from deepchecks.utils.plot import DEFAULT_DATASET_NAMES
  from deepchecks.utils.strings import format_number
label_desc: Use image node_id 9 for calling a global function with example usage: calc_drift_and_plot(train_column, test_column, value_name, column_type, plot_title, margin_quantile_filter, max_num_categories_for_drift, min_category_size_ratio, max_num_categories_for_display, show_categories_by, numerical_drift_method, categorical_drift_method, balance_classes, ignore_na, min_samples, raise_min_samples_error, with_display, dataset_names) and returns: score,  scorer_name,  fig, str,  None,  None, score,  scorer_name,  None, score,  scorer_name,  None