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: This computes the LOG likelihood of the whole dataset (or MAP, if priors given)
  for the current parameter steps
  
  Returns:
      Total likelihood over dataset
docstr_tok: ["This","computes","the","LOG","likelihood","of","the","whole","dataset","(","or","MAP",",","if","priors","given",")","for","the","current","parameter","steps","Returns",":","Total","likelihood","over","dataset"]
code: def compute_total_likelihood(self) -> float:
      """
      This computes the LOG likelihood of the whole dataset (or MAP, if priors given)
      for the current parameter steps
  
      Returns:
          Total likelihood over dataset
      """
      cpd_prods = cpd_multiplication(
          [self.cpds[n] for n in self.valid_nodes],
          normalize=False,
      )
      proba_of_row = cpd_prods.sum(axis=0)
  
      def compute_likelihood_stub(record: Dict) -> float:
          t = tuple(record[el] for el in proba_of_row.index.names)
  
          if np.isnan(record[self.lv_name]):
              likelihood = proba_of_row.loc[t]
          else:
              likelihood = cpd_prods.loc[record[self.lv_name], t]
  
          return np.log(likelihood) * record["count"]
  
      return self._mb_data.apply(compute_likelihood_stub, axis=1).sum()
  
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 6 for calling the EMSingleLatentVariable obj's underlying member method code with example usage: obj.compute_total_likelihood() and returns: self, unknown