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: Update expected sufficient statistics based on a given dataframe
  
  Args:
      lookup: Lookup table for index, columns and count
  
  Returns:
      List of list of update tuples
docstr_tok: ["Update","expected","sufficient","statistics","based","on","a","given","dataframe","Args",":","lookup",":","Lookup","table","for","index",",","columns","and","count","Returns",":","List","of","list","of","update","tuples"]
code: def _update_sufficient_stats(
      self,
      lookup: pd.Series,
  ) -> List[List[Tuple]]:
      """
      Update expected sufficient statistics based on a given dataframe
  
      Args:
          lookup: Lookup table for index, columns and count
  
      Returns:
          List of list of update tuples
      """
      updates = []
  
      for idx_cols_counts, mb_cols in lookup.values:
          if mb_cols is None:
              # Update the ESS: increase it by the number of times this row appears on the dataset
              updates.append(idx_cols_counts)
          else:
              # Update the ESS: increase it by:
              #     (number of times this row appears on the dataset) * (Probability of Z assuming that value)
              # Because lv is not observed, we consider all possible values it can assume and,
              # instead of adding 1 for the likelihood M[X=x_i, U=u_i], we add p(Z=z_1|observations)
              prob_lv_given_mb = self._mb_product[mb_cols]
              updates.append(
                  [
                      (idx, cols, count * prob_lv_given_mb[lv_value])
                      for (idx, cols, count), lv_value in zip(
                          idx_cols_counts, self._lv_states
                      )
                  ]
              )
  
      return updates
  
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 11 for calling the EMSingleLatentVariable obj's underlying member method code with example usage: obj._update_sufficient_stats(lookup) and returns: updates