class_docstr: Class for constraint-based estimation of DAGs using the PC algorithm
  from a given data set.  Identifies (conditional) dependencies in data
  set using statistical independence tests and estimates a DAG pattern
  that satisfies the identified dependencies. The DAG pattern can then be
  completed to a faithful DAG, if possible.
  
  Parameters
  ----------
  data: pandas DataFrame object
      dataframe object where each column represents one variable.  (If some
      values in the data are missing the data cells should be set to
      `numpy.NaN`.  Note that pandas converts each column containing
      `numpy.NaN`s to dtype `float`.)
  
  References
  ----------
  [1] Koller & Friedman, Probabilistic Graphical Models - Principles and Techniques,
      2009, Section 18.2
  [2] Neapolitan, Learning Bayesian Networks, Section 10.1.2 for the PC algorithm (page 550), http://www.cs.technion.ac.il/~dang/books/Learning%20Bayesian%20Networks(Neapolitan,%20Richard).pdf
docstr: Estimates a graph skeleton (UndirectedGraph) from a set of independencies
  using (the first part of) the PC algorithm. The independencies can either be
  provided as an instance of the `Independencies`-class or by passing a
  decision function that decides any conditional independency assertion.
  Returns a tuple `(skeleton, separating_sets)`.
  
  If an Independencies-instance is passed, the contained IndependenceAssertions
  have to admit a faithful BN representation. This is the case if
  they are obtained as a set of d-seperations of some Bayesian network or
  if the independence assertions are closed under the semi-graphoid axioms.
  Otherwise, the procedure may fail to identify the correct structure.
  
  Parameters
  ----------
  
  Returns
  -------
  skeleton: UndirectedGraph
      An estimate for the undirected graph skeleton of the BN underlying the data.
  
  separating_sets: dict
      A dict containing for each pair of not directly connected nodes a
      separating set ("witnessing set") of variables that makes then
      conditionally independent. (needed for edge orientation procedures)
  
  References
  ----------
  [1] Neapolitan, Learning Bayesian Networks, Section 10.1.2, Algorithm 10.2 (page 550)
      http://www.cs.technion.ac.il/~dang/books/Learning%20Bayesian%20Networks(Neapolitan,%20Richard).pdf
  [2] Koller & Friedman, Probabilistic Graphical Models - Principles and Techniques, 2009
      Section 3.4.2.1 (page 85), Algorithm 3.3
docstr_tok: ["Estimates","a","graph","skeleton","(","UndirectedGraph",")","from","a","set","of","independencies","using","(","the","first","part","of",")","the","PC","algorithm",".","The","independencies","can","either","be","provided","as","an","instance","of","the","`","Independencies","`","-class","or","by","passing","a","decision","function","that","decides","any","conditional","independency","assertion",".","Returns","a","tuple","`","(","skeleton",",","separating_sets",")","`",".","If","an","Independencies-instance","is","passed",",","the","contained","IndependenceAssertions","have","to","admit","a","faithful","BN","representation",".","This","is","the","case","if","they","are","obtained","as","a","set","of","d-seperations","of","some","Bayesian","network","or","if","the","independence","assertions","are","closed","under","the","semi-graphoid","axioms",".","Otherwise",",","the","procedure","may","fail","to","identify","the","correct","structure",".","Parameters","--","--","--","--","--","Returns","--","--","--","-","skeleton",":","UndirectedGraph","An","estimate","for","the","undirected","graph","skeleton","of","the","BN","underlying","the","data",".","separating_sets",":","dict","A","dict","containing","for","each","pair","of","not","directly","connected","nodes","a","separating","set","(","``","witnessing","set","''",")","of","variables","that","makes","then","conditionally","independent",".","(","needed","for","edge","orientation","procedures",")","References","--","--","--","--","--","[","1","]","Neapolitan",",","Learning","Bayesian","Networks",",","Section","10.1.2",",","Algorithm","10.2","(","page","550",")","http",":","\/\/www.cs.technion.ac.il\/~dang\/books\/Learning","%","20Bayesian","%","20Networks","(","Neapolitan",",","%","20Richard",")",".pdf","[","2","]","Koller","&","Friedman",",","Probabilistic","Graphical","Models","-","Principles","and","Techniques",",","2009","Section","3.4.2.1","(","page","85",")",",","Algorithm","3.3"]
code: def build_skeleton(
      self,
      ci_test="chi_square",
      max_cond_vars=5,
      significance_level=0.01,
      variant="stable",
      n_jobs=-1,
      show_progress=True,
      **kwargs,
  ):
      """
      Estimates a graph skeleton (UndirectedGraph) from a set of independencies
      using (the first part of) the PC algorithm. The independencies can either be
      provided as an instance of the `Independencies`-class or by passing a
      decision function that decides any conditional independency assertion.
      Returns a tuple `(skeleton, separating_sets)`.
  
      If an Independencies-instance is passed, the contained IndependenceAssertions
      have to admit a faithful BN representation. This is the case if
      they are obtained as a set of d-seperations of some Bayesian network or
      if the independence assertions are closed under the semi-graphoid axioms.
      Otherwise, the procedure may fail to identify the correct structure.
  
      Parameters
      ----------
  
      Returns
      -------
      skeleton: UndirectedGraph
          An estimate for the undirected graph skeleton of the BN underlying the data.
  
      separating_sets: dict
          A dict containing for each pair of not directly connected nodes a
          separating set ("witnessing set") of variables that makes then
          conditionally independent. (needed for edge orientation procedures)
  
      References
      ----------
      [1] Neapolitan, Learning Bayesian Networks, Section 10.1.2, Algorithm 10.2 (page 550)
          http://www.cs.technion.ac.il/~dang/books/Learning%20Bayesian%20Networks(Neapolitan,%20Richard).pdf
      [2] Koller & Friedman, Probabilistic Graphical Models - Principles and Techniques, 2009
          Section 3.4.2.1 (page 85), Algorithm 3.3
      """
      # Initialize initial values and structures.
      lim_neighbors = 0
      separating_sets = dict()
      if not callable(ci_test):
          try:
              ci_test = CI_TESTS[ci_test]
          except KeyError:
              raise ValueError(
                  f"ci_test must either be one of {list(CI_TESTS.keys())}, or a function. Got: {ci_test}"
              )
  
      if show_progress and config.SHOW_PROGRESS:
          pbar = tqdm(total=max_cond_vars)
          pbar.set_description("Working for n conditional variables: 0")
  
      # Step 1: Initialize a fully connected undirected graph
      graph = nx.complete_graph(n=self.variables, create_using=nx.Graph)
  
      # Exit condition: 1. If all the nodes in graph has less than `lim_neighbors` neighbors.
      #             or  2. `lim_neighbors` is greater than `max_conditional_variables`.
      while not all(
          [
              len(list(graph.neighbors(var))) < lim_neighbors
              for var in self.variables
          ]
      ):
          # Step 2: Iterate over the edges and find a conditioning set of
          # size `lim_neighbors` which makes u and v independent.
          if variant == "orig":
              for u, v in graph.edges():
                  for separating_set in chain(
                      combinations(
                          set(graph.neighbors(u)) - set([v]),
                          lim_neighbors,
                      ),
                      combinations(
                          set(graph.neighbors(v)) - set([u]),
                          lim_neighbors,
                      ),
                  ):
                      # If a conditioning set exists remove the edge, store the separating set
                      # and move on to finding conditioning set for next edge.
                      if ci_test(
                          u,
                          v,
                          separating_set,
                          data=self.data,
                          independencies=self.independencies,
                          significance_level=significance_level,
                          **kwargs,
                      ):
                          separating_sets[
                              frozenset((u, v))
                          ] = separating_set
                          graph.remove_edge(u, v)
                          break
  
          elif variant == "stable":
              # In case of stable, precompute neighbors as this is the stable algorithm.
              neighbors = {
                  node: set(graph[node]) for node in graph.nodes()
              }
              for u, v in graph.edges():
                  for separating_set in chain(
                      combinations(
                          set(neighbors[u]) - set([v]), lim_neighbors
                      ),
                      combinations(
                          set(neighbors[v]) - set([u]), lim_neighbors
                      ),
                  ):
                      # If a conditioning set exists remove the edge, store the
                      # separating set and move on to finding conditioning set for next edge.
                      if ci_test(
                          u,
                          v,
                          separating_set,
                          data=self.data,
                          independencies=self.independencies,
                          significance_level=significance_level,
                          **kwargs,
                      ):
                          separating_sets[
                              frozenset((u, v))
                          ] = separating_set
                          graph.remove_edge(u, v)
                          break
  
          elif variant == "parallel":
              neighbors = {
                  node: set(graph[node]) for node in graph.nodes()
              }
  
              def _parallel_fun(u, v):
                  for separating_set in chain(
                      combinations(
                          set(graph.neighbors(u)) - set([v]),
                          lim_neighbors,
                      ),
                      combinations(
                          set(graph.neighbors(v)) - set([u]),
                          lim_neighbors,
                      ),
                  ):
                      if ci_test(
                          u,
                          v,
                          separating_set,
                          data=self.data,
                          independencies=self.independencies,
                          significance_level=significance_level,
                          **kwargs,
                      ):
                          return (u, v), separating_set
  
              results = Parallel(n_jobs=n_jobs)(
                  delayed(_parallel_fun)(u, v)
                  for (u, v) in graph.edges()
              )
              for result in results:
                  if result is not None:
                      (u, v), sep_set = result
                      graph.remove_edge(u, v)
                      separating_sets[frozenset((u, v))] = sep_set
  
          else:
              raise ValueError(
                  f"variant must be one of (orig, stable, parallel). Got: {variant}"
              )
  
          # Step 3: After iterating over all the edges, expand the search space by increasing the size
          #         of conditioning set by 1.
          if lim_neighbors >= max_cond_vars:
              logger.info(
                  "Reached maximum number of allowed conditional variables. Exiting"
              )
              break
          lim_neighbors += 1
  
          if show_progress and config.SHOW_PROGRESS:
              pbar.update(1)
              pbar.set_description(
                  f"Working for n conditional variables: {lim_neighbors}"
              )
  
      if show_progress and config.SHOW_PROGRESS:
          pbar.close()
      return graph, separating_sets
  
file_path: pgmpy/pgmpy/estimators/PC.py
imports: from itertools import chain, combinations, permutations
  import networkx
  from joblib import Parallel, delayed
  from tqdm.auto import tqdm
  from pgmpy import config
  from pgmpy.base import PDAG
  from pgmpy.estimators import StructureEstimator
  from pgmpy.estimators.CITests import *
  from pgmpy.global_vars import logger
label_desc: Use image node_id 3 for calling the PC obj's underlying member method code with example usage: obj.build_skeleton(ci_test, max_cond_vars, significance_level, variant, n_jobs, show_progress) and returns: graph,  separating_sets, u,  v,  separating_set