class_docstr: This class uses Expectation-Maximization to learn parameters of a single latent variable in a bayesian network.
  We do so by also allowing the user to CONSTRAINT the optimisation
  These are elements that help the algorithm find a local optimal point closer to the point we think
  
  The setting is:
  Input:
      - a StructureModel representing the whole network or any sub-graph containing the Markov Blanket of the LV
      - data as a dataframe. The LV must be in the dataframe, with missing values represented by `np.nan`s
      - constraints:
          - Box - A hard constraint; forbids the solution to be outside of certain boundaries
          - Priors - establishes Dirichlet priors to every parameter
  run:
      - using the method `run` or manually alternating over E and M steps)
  Result:
      - CPTs involving the latent variable, learnt by EM, found in the attribute `cpds`
      - CPTs not involving the LV not learned (They must be learned separately by MLE.
      This is faster and the result is the same)
  
  Example:
  >>> em = EMSingleLatentVariable(sm=sm, data=data, lv_name=lv_name, node_states=node_states)
  >>> em.run() # run EM until convergence
  >>> # or run E and M steps separately
  >>> for i in range(10): # Run EM 10 times
  >>>     em.e_step()
  >>>     em.m_step()
docstr: Runs E and M steps until convergence (`stopping_delta`) or max iterations is reached (n_runs)
  
  Args:
      n_runs: max number of EM alternations
      stopping_delta: if max difference in current - last iteration CPDS < stopping_delta => convergence reached
      verbose: amount of printing
docstr_tok: ["Runs","E","and","M","steps","until","convergence","(","`","stopping_delta","`",")","or","max","iterations","is","reached","(","n_runs",")","Args",":","n_runs",":","max","number","of","EM","alternations","stopping_delta",":","if","max","difference","in","current","-","last","iteration","CPDS","<","stopping_delta","=",">","convergence","reached","verbose",":","amount","of","printing"]
code: def run(
      self, n_runs: int, stopping_delta: float = 0.0, verbose: int = 0
  ):
      """
      Runs E and M steps until convergence (`stopping_delta`) or max iterations is reached (n_runs)
  
      Args:
          n_runs: max number of EM alternations
          stopping_delta: if max difference in current - last iteration CPDS < stopping_delta => convergence reached
          verbose: amount of printing
      """
      if verbose:
          self._logger.info(
              "* Iteration 0: likelihood = %.4f",
              self.compute_total_likelihood(),
          )
  
      for i in range(n_runs):
          t_start = time()
          self.e_step()  # Expectation step
          e_duration = time() - t_start
  
          t_start = time()
          self.m_step()  # Maximisation step
          m_duration = time() - t_start
  
          self.apply_box_constraints()  # Apply box constraints
          delta = (
              self._stopping_criteria()
          )  # Compute change in parameters
  
          if verbose:
              self._logger.info(
                  "* Iteration %d: "
                  "likelihood = %.4f | "
                  "max(|theta - theta_old|)) = %.4f | "
                  "duration = %.4fs (E-step), %.4fs (M-step)",
                  (i + 1),
                  self.compute_total_likelihood(),
                  delta,
                  e_duration,
                  m_duration,
              )
  
          if delta < stopping_delta:
              break
  
imports: import logging
  import os
  from time import time
  from typing import Dict, List, Tuple, Union
  import numpy
  import pandas
  from joblib import Parallel, delayed
  from causalnex.structure import StructureModel
  from causalnex.utils.data_utils import chunk_data, count_unique_rows
  from causalnex.utils.pgmpy_utils import cpd_multiplication
label_desc: Use image node_id 3 for calling the EMSingleLatentVariable obj's underlying member method code with example usage: obj.run(n_runs, stopping_delta, verbose) without return types