Repository: pgmpy/pgmpy

Issue:
Adjustment sets for sets of variables
According to the documentation, `get_all_backdoor_adjustment_sets` accept a string variable for X and Y, but not a list of variables. 
Is there any reason a list of variables cannot be supplied for X and Y, since the method `is_valid_adjustment_set` supports sets of variables?

Discussion:
@ankurankan: @zzzrbx No, it should be straightforward to modify it to accept list of variables. The methods were initially written for single source and outcome variables, that's why some of them haven't been updated yet.@jihyeseo: For `is_valid_backdoor_adjustment_set(self, X, Y, Z=[])` and `is_valid_frontdoor_adjustment_set(self, X, Y, Z=None)`, X and Y is a node (string).

For `is_valid_adjustment_set(self, X, Y, adjustment_set)`, X and Y are lists. When I pass X string, we get error. 

```
from pgmpy.models import BayesianNetwork
from pgmpy.inference import CausalInference
model = BayesianNetwork([("x1", "y1"), ("x1", "z1"), ("z1", "z2"),
                       ("z2", "x2"), ("y2", "z2")])
c_infer = CausalInference(model)

c_infer.is_valid_adjustment_set(X=['x1', 'x2'], Y=['y1', 'y2'], adjustment_set=['z1', 'z2'])
True

c_infer.is_valid_adjustment_set(X=['x1'], Y=['y1'], adjustment_set=['z1', 'z2'])
True

c_infer.is_valid_adjustment_set(X='x1', Y='y1', adjustment_set=['z1', 'z2'])
```
```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[28], line 1
----> 1 c_infer.is_valid_adjustment_set(X='x1', Y='y1', adjustment_set=['z1', 'z2'])

File CausalInference.py:450, in CausalInference.is_valid_adjustment_set(self, X, Y, adjustment_set)
    413 def is_valid_adjustment_set(self, X, Y, adjustment_set):
    414     """
    415     Method to test whether `adjustment_set` is a valid adjustment set for
    416     identifying the causal effect of `X` on `Y`.
   (...)
    448     [1] Perkovic, Emilija, et al. "Complete graphical characterization and construction of adjustment sets in Markov equivalence classes of ancestral graphs." The Journal of Machine Learning Research 18.1 (2017): 8132-8193.
    449     """
--> 450     backdoor_graph = self.get_proper_backdoor_graph(X, Y, inplace=False)
    451     for x, y in zip(X, Y):
    452         if backdoor_graph.is_dconnected(start=x, end=y, observed=adjustment_set):

File CausalInference.py:402, in CausalInference.get_proper_backdoor_graph(self, X, Y, inplace)
    400 for var in chain(X, Y):
    401     if var not in self.model.nodes():
--> 402         raise ValueError(f"{var} not found in the model.")
    404 model = self.model if inplace else self.model.copy()
    405 edges_to_remove = []

ValueError: x not found in the model.
```

Upon receiving a node, I would make it into a new list(). Currently, the code is converting a string into an array of characters (thus, unknown node names)

Solution patch:
diff --git a/pgmpy/estimators/ExpertKnowledge.py b/pgmpy/estimators/ExpertKnowledge.py
index 52328c83b..190834e0b 100644
--- a/pgmpy/estimators/ExpertKnowledge.py
+++ b/pgmpy/estimators/ExpertKnowledge.py
@@ -105,7 +105,7 @@ def _validate_temporal_order(self, nodes):
         # Check if all nodes are present in the temporal order
         if set(chain(*self.temporal_order)) != set(nodes):
             raise ValueError(
-                f"Missing nodes in temporal order - {set(nodes) - tier_set}"
+                f"Missing nodes in temporal order - {set(nodes) - set(chain(*self.temporal_order))}"
             )
 
     def _get_temporal_ordering(self, temporal_order):
diff --git a/pgmpy/factors/continuous/LinearGaussianCPD.py b/pgmpy/factors/continuous/LinearGaussianCPD.py
index c9ff785c7..4dc2a0ff0 100644
--- a/pgmpy/factors/continuous/LinearGaussianCPD.py
+++ b/pgmpy/factors/continuous/LinearGaussianCPD.py
@@ -125,3 +125,54 @@ def __str__(self):
     def __repr__(self):
         str_repr = self.__str__()
         return f"<LinearGaussianCPD: {str_repr} at {hex(id(self))}"
+
+    @staticmethod
+    def get_random(variable, evidence, loc=0.0, scale=1.0, seed=None):
+        """
+        Generates a LinearGaussianCPD instance with random values on `variable` with
+        parents/evidence `evidence` with beta and std sampled from loc and scale
+
+        Parameters
+        ----------
+        variable: str, int or any hashable python object.
+            The variable on which to define the TabularCPD.
+
+        evidence: list, array-like
+            A list of variable names which are the parents/evidence of `variable`.
+
+        loc: float
+            The mean of the normal distribution from which the coefficients are
+            sampled.
+
+        scale: float
+            The standard deviation of the normal distribution from which the
+            coefficients are sampled.
+
+        seed: int (default: None)
+            The seed for the random number generator.
+
+        Returns
+        -------
+        Random CPD: pgmpy.factors.continuous.LinearGaussianCPD
+            A LinearGaussianCPD object on `variable` with `evidence` as evidence with random values.
+
+        Examples
+        --------
+        >>> from pgmpy.factors.continuous import LinearGaussianCPD
+        >>> LinearGaussianCPD.get_random(variable='Income', evidence=['Age', 'Experience'],
+        ...            loc=2.0, scale=0.5, seed=5)
+        <LinearGaussianCPD: P(Income | Age, Experience) = N(1.338*Age + 1.876*Experience + 1.599; 2.21) at 0x1795561e0
+        """
+        rng = np.random.default_rng(seed=seed)
+
+        beta = rng.normal(loc=loc, scale=scale, size=(len(evidence) + 1))
+        std = abs(rng.normal(loc=loc, scale=scale))
+
+        node_cpd = LinearGaussianCPD(
+            variable=variable,
+            beta=beta,
+            std=std,
+            evidence=evidence,
+        )
+
+        return node_cpd
diff --git a/pgmpy/inference/CausalInference.py b/pgmpy/inference/CausalInference.py
index 101f1bd4c..e7f0e1879 100644
--- a/pgmpy/inference/CausalInference.py
+++ b/pgmpy/inference/CausalInference.py
@@ -397,6 +397,11 @@ def get_proper_backdoor_graph(self, X, Y, inplace=False):
         ----------
         [1] Perkovic, Emilija, et al. "Complete graphical characterization and construction of adjustment sets in Markov equivalence classes of ancestral graphs." The Journal of Machine Learning Research 18.1 (2017): 8132-8193.
         """
+        if isinstance(X, str):
+            X = [X]
+        if isinstance(Y, str):
+            Y = [Y]
+
         for var in chain(X, Y):
             if var not in self.model.nodes():
                 raise ValueError(f"{var} not found in the model.")
@@ -447,6 +452,11 @@ def is_valid_adjustment_set(self, X, Y, adjustment_set):
         ----------
         [1] Perkovic, Emilija, et al. "Complete graphical characterization and construction of adjustment sets in Markov equivalence classes of ancestral graphs." The Journal of Machine Learning Research 18.1 (2017): 8132-8193.
         """
+        if isinstance(X, str):
+            X = [X]
+        if isinstance(Y, str):
+            Y = [Y]
+
         backdoor_graph = self.get_proper_backdoor_graph(X, Y, inplace=False)
         for x, y in zip(X, Y):
             if backdoor_graph.is_dconnected(start=x, end=y, observed=adjustment_set):
diff --git a/pgmpy/models/LinearGaussianBayesianNetwork.py b/pgmpy/models/LinearGaussianBayesianNetwork.py
index 3b265d7b4..2b4a5bd18 100644
--- a/pgmpy/models/LinearGaussianBayesianNetwork.py
+++ b/pgmpy/models/LinearGaussianBayesianNetwork.py
@@ -165,17 +165,19 @@ def get_random_cpds(self, loc=0, scale=1, inplace=False, seed=None):
         seed: int
             The seed for the random number generator.
         """
-        rng = np.random.default_rng(seed=seed)
+        # We want to provide a different seed for each cpd, therefore we force it to be integer and increment in a loop.
+        seed = seed if seed else 42
 
         cpds = []
-        for var in self.nodes():
+        for i, var in enumerate(self.nodes()):
             parents = self.get_parents(var)
             cpds.append(
-                LinearGaussianCPD(
+                LinearGaussianCPD.get_random(
                     variable=var,
-                    beta=rng.normal(loc=loc, scale=scale, size=(len(parents) + 1)),
-                    std=abs(rng.normal(loc=loc, scale=scale)),
                     evidence=parents,
+                    loc=loc,
+                    scale=scale,
+                    seed=(seed + i),
                 )
             )
         if inplace:


Test patch:
diff --git a/pgmpy/tests/test_estimators/test_PC.py b/pgmpy/tests/test_estimators/test_PC.py
index 177b2a350..d117de72e 100644
--- a/pgmpy/tests/test_estimators/test_PC.py
+++ b/pgmpy/tests/test_estimators/test_PC.py
@@ -638,8 +638,10 @@ def test_pc_asia(self):
         )
 
     def test_temporal_pc_cancer(self):
-        asia_model = get_example_model("cancer")
-        data = BayesianModelSampling(asia_model).forward_sample(size=int(5e4), seed=42)
+        cancer_model = get_example_model("cancer")
+        data = BayesianModelSampling(cancer_model).forward_sample(
+            size=int(5e4), seed=42
+        )
         est = PC(data)
         background = ExpertKnowledge(  # e.g. we only know "Pollution", "Smoker", "Cancer" can be the causes of others
             temporal_order=[["Pollution", "Smoker", "Cancer"], ["Dyspnoea", "Xray"]],
diff --git a/pgmpy/tests/test_factors/test_continuous/test_Linear_Gaussian_CPD.py b/pgmpy/tests/test_factors/test_continuous/test_Linear_Gaussian_CPD.py
index 53339a497..c97391cc5 100644
--- a/pgmpy/tests/test_factors/test_continuous/test_Linear_Gaussian_CPD.py
+++ b/pgmpy/tests/test_factors/test_continuous/test_Linear_Gaussian_CPD.py
@@ -24,3 +24,7 @@ def test_str(self):
             cpd2.__str__(),
             "P(y | x1, x2, x3) = N(1.0*x1 + 4.56*x2 + 8.0*x3 + 0.67; 2)",
         )
+
+    def test_get_random(self):
+        cpd_random = LinearGaussianCPD.get_random("x", ["x1", "x2", "x3"], 0.23, 0.56)
+        self.assertIn("P(x | x1, x2, x3) = N(", cpd_random.__str__())
diff --git a/pgmpy/tests/test_inference/test_CausalInference.py b/pgmpy/tests/test_inference/test_CausalInference.py
index e1d640e30..e15940ca4 100644
--- a/pgmpy/tests/test_inference/test_CausalInference.py
+++ b/pgmpy/tests/test_inference/test_CausalInference.py
@@ -80,6 +80,15 @@ def test_proper_backdoor_graph(self):
             set([("x1", "z1"), ("z1", "z2"), ("z2", "x2"), ("y2", "z2")]),
         )
 
+    def test_proper_backdoor_graph_not_list(self):
+        bd_graph = self.infer.get_proper_backdoor_graph(X="x1", Y="y1")
+        self.assertTrue(("x1", "y1") not in bd_graph.edges())
+        self.assertEqual(len(bd_graph.edges()), 4)
+        self.assertTrue(
+            set(bd_graph.edges()),
+            set([("x1", "z1"), ("z1", "z2"), ("z2", "x2"), ("y2", "z2")]),
+        )
+
     def test_is_valid_adjustment_set(self):
         self.assertTrue(
             self.infer.is_valid_adjustment_set(
@@ -87,6 +96,12 @@ def test_is_valid_adjustment_set(self):
             )
         )
 
+        self.assertTrue(
+            self.infer.is_valid_adjustment_set(
+                X="x1", Y="y1", adjustment_set=["z1", "z2"]
+            )
+        )
+
         self.assertFalse(
             self.infer.is_valid_adjustment_set(
                 X=["x1", "x2"], Y=["y1", "y2"], adjustment_set=["z1"]