code: def counterfactual_samples(
      causal_model: Union[
          StructuralCausalModel, InvertibleStructuralCausalModel
      ],
      interventions: Dict[
          Any, Callable[[np.ndarray], Union[float, np.ndarray]]
      ],
      observed_data: Optional[pd.DataFrame] = None,
      noise_data: Optional[pd.DataFrame] = None,
  ) -> pd.DataFrame:
      """Estimates counterfactual data for observed data if we were to perform specified interventions. This function
      implements the 3-step process for computing counterfactuals by Pearl (see https://ftp.cs.ucla.edu/pub/stat_ser/r485.pdf).
  
      :param causal_model: The (invertible) structural causal model we perform this intervention on. If noise_data is
                           None and observed_data is provided, this must be an invertible structural model, otherwise,
                           this can be either a structural causal model or an invertible one.
      :param interventions: Dictionary containing the interventions we want to perform keyed by node name. An
                            intervention is a function that takes a value as input and returns another value.
                            For example, `{'X': lambda x: 2}` mimics the atomic intervention *do(X:=2)*.
      :param observed_data: Factual data that we observe for the nodes in the causal graph.
      :param noise_data: Data of noise terms corresponding to nodes in the causal graph. If not provided,
                         these have to be estimated from observed data. Then we require causal models of nodes to be
                         invertible.
      :return: Estimated counterfactual data.
      """
      for node in interventions:
          validate_node_in_graph(causal_model.graph, node)
  
      validate_causal_dag(causal_model.graph)
  
      if observed_data is None and noise_data is None:
          raise ValueError(
              "Either observed_data or noise_data need to be given!"
          )
      if observed_data is not None and noise_data is not None:
          raise ValueError(
              "Either observed_data or noise_data can be given, not both!"
          )
  
      if noise_data is None and observed_data is not None:
          if not isinstance(
              causal_model, InvertibleStructuralCausalModel
          ):
              raise ValueError(
                  "Since no noise_data is given, this has to be estimated from the given "
                  "observed_data. This can only be done with InvertibleStructuralCausalModel."
              )
          # Abduction: For invertible SCMs, we recover exact noise values from data.
          noise_data = compute_noise_from_data(
              causal_model, observed_data
          )
  
      # Action + Prediction: Propagate the intervention downstream using recovered noise values.
      return _counterfactual_samples(
          causal_model, interventions, noise_data
      )
  
file_path: dowhy/dowhy/gcm/whatif.py
imports: from typing import Any, Callable, Dict, Iterable, List, Optional, Union
  import networkx
  import numpy
  import pandas
  from dowhy.gcm._noise import compute_noise_from_data
  from dowhy.gcm.causal_mechanisms import ClassifierFCM
  from dowhy.gcm.causal_models import InvertibleStructuralCausalModel, ProbabilisticCausalModel, StructuralCausalModel, validate_causal_dag
  from dowhy.gcm.fitting_sampling import draw_samples
  from dowhy.graph import DirectedGraph, get_ordered_predecessors, is_root_node, node_connected_subgraph_view, validate_node_in_graph
label_desc: Use image node_id 4 for calling a global function with example usage: counterfactual_samples(causal_model, interventions, observed_data, noise_data) and returns: _counterfactual_samples