class_docstr: Base class for Bayesian Network (BN), a probabilistic weighted DAG where nodes represent variables,
  edges represent the causal relationships between variables.
  
  ``BayesianNetwork`` stores nodes with their possible states, edges and
  conditional probability distributions (CPDs) of each node.
  
  ``BayesianNetwork`` is built on top of the ``StructureModel``, which is an extension of ``networkx.DiGraph``
  (see :func:`causalnex.structure.structuremodel.StructureModel`).
  
  In order to define the ``BayesianNetwork``, users should provide a relevant ``StructureModel``.
  Once ``BayesianNetwork`` is initialised, no changes to the ``StructureModel`` can be made
  and CPDs can be learned from the data.
  
  The learned CPDs can be then used for likelihood estimation and predictions.
  
  Example:
  ::
      >>> # Create a Bayesian Network with a manually defined DAG.
      >>> from causalnex.structure import StructureModel
      >>> from causalnex.network import BayesianNetwork
      >>>
      >>> sm = StructureModel()
      >>> sm.add_edges_from([
      >>>                    ('rush_hour', 'traffic'),
      >>>                    ('weather', 'traffic')
      >>>                    ])
      >>> bn = BayesianNetwork(sm)
      >>> # A created ``BayesianNetwork`` stores nodes and edges defined by the ``StructureModel``
      >>> bn.nodes
      ['rush_hour', 'traffic', 'weather']
      >>>
      >>> bn.edges
      [('rush_hour', 'traffic'), ('weather', 'traffic')]
      >>> # A ``BayesianNetwork`` doesn't store any CPDs yet
      >>> bn.cpds
      >>> {}
      >>>
      >>> # Learn the nodes' states from the data
      >>> import pandas as pd
      >>> data = pd.DataFrame({
      >>>                      'rush_hour': [True, False, False, False, True, False, True],
      >>>                      'weather': ['Terrible', 'Good', 'Bad', 'Good', 'Bad', 'Bad', 'Good'],
      >>>                      'traffic': ['heavy', 'light', 'heavy', 'light', 'heavy', 'heavy', 'heavy']
      >>>                      })
      >>> bn = bn.fit_node_states(data)
      >>> bn.node_states
      {'rush_hour': {False, True}, 'weather': {'Bad', 'Good', 'Terrible'}, 'traffic': {'heavy', 'light'}}
      >>> # Learn the CPDs from the data
      >>> bn = bn.fit_cpds(data)
      >>> # Use the learned CPDs to make predictions on the unseen data
      >>> test_data = pd.DataFrame({
      >>>                           'rush_hour': [False, False, True, True],
      >>>                           'weather': ['Good', 'Bad', 'Good', 'Bad']
      >>>                           })
      >>> bn.predict(test_data, "traffic").to_dict()
      >>> {'traffic_prediction': {0: 'light', 1: 'heavy', 2: 'heavy', 3: 'heavy'}}
      >>> bn.predict_probability(test_data, "traffic").to_dict()
      {'traffic_prediction': {0: 'light', 1: 'heavy', 2: 'heavy', 3: 'heavy'}}
      {'traffic_light': {0: 0.75, 1: 0.25, 2: 0.3333333333333333, 3: 0.3333333333333333},
       'traffic_heavy': {0: 0.25, 1: 0.75, 2: 0.6666666666666666, 3: 0.6666666666666666}}
docstr: This runs the EM algorithm to estimate the CPDs of latent variables and their corresponding Markov blanket
  
  Args:
      lv_name: Latent variable name
      lv_states: the states the LV can assume
      data: dataframe, must contain all variables in the Markov Blanket of the latent variable. Include one column
          with the latent variable name, filled with np.nan for missing info about LV.
          If some data is present about the LV, create complete columns.
      n_runs: max number of EM alternations
      stopping_delta: if max difference in current - last iteration CPDS < stopping_delta => convergence reached
      initial_params: way to initialise parameters. Can be:
          - "random": random values (default)
          - "avg": uniform distributions everywhere. Not advised, as it may be the a stationary point on itself
          - if provide a dictionary of dataframes, this will be used as the initialisation
      box_constraints: minimum and maximum values for each model parameter. Specified with a dictionary mapping:
          - Node
          - two dataframes, in order: Min(P(Node|Par(Node))) and Max(P(Node|Par(Node)))
      priors: priors, provided as a mapping Node -> dataframe with Dirichilet priors for P(Node|Par(Node))
      non_missing_data_factor:
          This is a weight added to the non-missing data samples. The effect is as if the amount of data provided
          was bigger. Empirically, helps to set the factor to 10 if the non missing data is ~1% of the dataset
  
  Returns:
      self
  
  Raises:
      ValueError: if the latent variable is not a string or
          if the latent variable cannot be found in the network or
          if the latent variable is present/observed in the data
          if the latent variable states are empty
          if additional non-lv nodes are added to the subgraph without being fit
docstr_tok: ["This","runs","the","EM","algorithm","to","estimate","the","CPDs","of","latent","variables","and","their","corresponding","Markov","blanket","Args",":","lv_name",":","Latent","variable","name","lv_states",":","the","states","the","LV","can","assume","data",":","dataframe",",","must","contain","all","variables","in","the","Markov","Blanket","of","the","latent","variable",".","Include","one","column","with","the","latent","variable","name",",","filled","with","np.nan","for","missing","info","about","LV",".","If","some","data","is","present","about","the","LV",",","create","complete","columns",".","n_runs",":","max","number","of","EM","alternations","stopping_delta",":","if","max","difference","in","current","-","last","iteration","CPDS","<","stopping_delta","=",">","convergence","reached","initial_params",":","way","to","initialise","parameters",".","Can","be",":","-","``","random","''",":","random","values","(","default",")","-","``","avg","''",":","uniform","distributions","everywhere",".","Not","advised",",","as","it","may","be","the","a","stationary","point","on","itself","-","if","provide","a","dictionary","of","dataframes",",","this","will","be","used","as","the","initialisation","box_constraints",":","minimum","and","maximum","values","for","each","model","parameter",".","Specified","with","a","dictionary","mapping",":","-","Node","-","two","dataframes",",","in","order",":","Min","(","P","(","Node|Par","(","Node",")",")",")","and","Max","(","P","(","Node|Par","(","Node",")",")",")","priors",":","priors",",","provided","as","a","mapping","Node","-",">","dataframe","with","Dirichilet","priors","for","P","(","Node|Par","(","Node",")",")","non_missing_data_factor",":","This","is","a","weight","added","to","the","non-missing","data","samples",".","The","effect","is","as","if","the","amount","of","data","provided","was","bigger",".","Empirically",",","helps","to","set","the","factor","to","10","if","the","non","missing","data","is","~1","%","of","the","dataset","Returns",":","self","Raises",":","ValueError",":","if","the","latent","variable","is","not","a","string","or","if","the","latent","variable","can","not","be","found","in","the","network","or","if","the","latent","variable","is","present\/observed","in","the","data","if","the","latent","variable","states","are","empty","if","additional","non-lv","nodes","are","added","to","the","subgraph","without","being","fit"]
code: def fit_latent_cpds(  # pylint: disable=too-many-arguments
      self,
      lv_name: str,
      lv_states: List,
      data: pd.DataFrame,
      box_constraints: Optional[
          Dict[str, Tuple[pd.DataFrame, pd.DataFrame]]
      ] = None,
      priors: Optional[Dict[str, pd.DataFrame]] = None,
      initial_params: Union[str, Dict[str, pd.DataFrame]] = "random",
      non_missing_data_factor: int = 1,
      n_runs: int = 20,
      stopping_delta: float = 0.0,
  ) -> "BayesianNetwork":
      """
      This runs the EM algorithm to estimate the CPDs of latent variables and their corresponding Markov blanket
  
      Args:
          lv_name: Latent variable name
          lv_states: the states the LV can assume
          data: dataframe, must contain all variables in the Markov Blanket of the latent variable. Include one column
              with the latent variable name, filled with np.nan for missing info about LV.
              If some data is present about the LV, create complete columns.
          n_runs: max number of EM alternations
          stopping_delta: if max difference in current - last iteration CPDS < stopping_delta => convergence reached
          initial_params: way to initialise parameters. Can be:
              - "random": random values (default)
              - "avg": uniform distributions everywhere. Not advised, as it may be the a stationary point on itself
              - if provide a dictionary of dataframes, this will be used as the initialisation
          box_constraints: minimum and maximum values for each model parameter. Specified with a dictionary mapping:
              - Node
              - two dataframes, in order: Min(P(Node|Par(Node))) and Max(P(Node|Par(Node)))
          priors: priors, provided as a mapping Node -> dataframe with Dirichilet priors for P(Node|Par(Node))
          non_missing_data_factor:
              This is a weight added to the non-missing data samples. The effect is as if the amount of data provided
              was bigger. Empirically, helps to set the factor to 10 if the non missing data is ~1% of the dataset
  
      Returns:
          self
  
      Raises:
          ValueError: if the latent variable is not a string or
              if the latent variable cannot be found in the network or
              if the latent variable is present/observed in the data
              if the latent variable states are empty
              if additional non-lv nodes are added to the subgraph without being fit
      """
      if not isinstance(lv_name, str):
          raise ValueError(f"Invalid latent variable name '{lv_name}'")
      if lv_name not in self._structure:
          raise ValueError(
              f"Latent variable '{lv_name}' not added to the network"
          )
      if not isinstance(lv_states, list) or len(lv_states) == 0:
          raise ValueError(
              f"Latent variable '{lv_name}' contains no states"
          )
  
      # Unaccounted nodes that have not been fit will result in infinite loop during
      # generation of InferenceEngine
      unaccounted_nodes = []
      for node in self.nodes:
          if (node not in self.cpds) and (node != lv_name):
              unaccounted_nodes.append(node)
      if len(unaccounted_nodes) > 0:
          raise ValueError(
              f"Node(s) {unaccounted_nodes} have not had their states and cpds fit. "
              "Before fitting latent variable cpds, add the additional nodes and"
              "edges to the subgraph and fit with .fit_node_states_and_cpds() first."
          )
  
      # Register states for the latent variable
      self._node_states[lv_name] = {
          v: k for k, v in enumerate(sorted(lv_states))
      }
  
      # Run EM algorithm
      estimator = EMSingleLatentVariable(
          sm=self.structure,
          data=data,
          lv_name=lv_name,
          node_states={
              n: sorted(s) for n, s in self.node_states.items()
          },
          initial_params=initial_params,
          box_constraints=box_constraints,
          priors=priors,
          non_missing_data_factor=non_missing_data_factor,
      )
      estimator.run(n_runs=n_runs, stopping_delta=stopping_delta)
  
      # Add CPDs into the model
      tab_cpds = [
          pd_to_tabular_cpd(el) for el in estimator.cpds.values()
      ]
      self._model.add_cpds(*tab_cpds)
  
      return self
  
imports: import copy
  import re
  from typing import Dict, Hashable, List, Optional, Set, Tuple, Union
  import networkx
  import pandas
  from pgmpy.estimators import BayesianEstimator, MaximumLikelihoodEstimator
  from pgmpy.factors.discrete.CPD import TabularCPD
  from pgmpy.models import BayesianNetwork
  from causalnex.estimator.em import EMSingleLatentVariable
  from causalnex.structure import StructureModel
  from causalnex.utils.pgmpy_utils import pd_to_tabular_cpd
label_desc: Use image node_id 14 for calling the BayesianNetwork obj's underlying member method code with example usage: obj.fit_latent_cpds(lv_name, lv_states, data, box_constraints, priors, initial_params, non_missing_data_factor, n_runs, stopping_delta) and returns: self