Repository: neurostuff/PyMARE

Issue:
Make `X` optional in `Dataset` creation
It seems like, since `add_intercept` is `True` by default, you shouldn't have to provide X when you're only doing a mean analysis.

Discussion:

Patch:
diff --git a/pymare/core.py b/pymare/core.py
index 3592db2..32d441e 100644
--- a/pymare/core.py
+++ b/pymare/core.py
@@ -5,6 +5,8 @@ from functools import partial
 import numpy as np
 import pandas as pd
 
+from pymare.utils import _listify
+
 from .estimators import (
     DerSimonianLaird,
     Hedges,
@@ -65,12 +67,25 @@ class Dataset:
                 "data argument."
             )
 
+        if (X is None) and (not add_intercept):
+            raise ValueError("If no X matrix is provided, add_intercept must be True!")
+
         # Extract columns from DataFrame
         if data is not None:
             y = data.loc[:, y or "y"].values
-            v = data.loc[:, v or "v"].values
-            X_names = X or "X"
-            X = data.loc[:, X_names].values
+
+            # v is optional
+            if (v is not None) or ("v" in data.columns):
+                v = data.loc[:, v or "v"].values
+
+            # X is optional
+            if (X is not None) or ("X" in data.columns):
+                X_names = X or "X"
+                X = data.loc[:, X_names].values
+
+            # n is optional
+            if (n is not None) or ("n" in data.columns):
+                n = data.loc[:, n or "n"].values
 
         self.y = ensure_2d(y)
         self.v = ensure_2d(v)
@@ -85,12 +100,15 @@ class Dataset:
                 "No fixed predictors found. If no X matrix is "
                 "provided, add_intercept must be True!"
             )
+
         X = pd.DataFrame(X)
         if names is not None:
-            X.columns = names
+            X.columns = _listify(names)
+
         if add_intercept:
             intercept = pd.DataFrame({"intercept": np.ones(len(self.y))})
             X = pd.concat([intercept, X], axis=1)
+
         return X.values, X.columns.tolist()
 
 
diff --git a/pymare/utils.py b/pymare/utils.py
new file mode 100644
index 0000000..f7f916c
--- /dev/null
+++ b/pymare/utils.py
@@ -0,0 +1,10 @@
+"""Miscellaneous utility functions."""
+import numpy as np
+
+
+def _listify(obj):
+    """Wrap all non-list or tuple objects in a list.
+
+    This provides a simple way to accept flexible arguments.
+    """
+    return obj if isinstance(obj, (list, tuple, type(None), np.ndarray)) else [obj]


Test patch:
diff --git a/pymare/tests/test_core.py b/pymare/tests/test_core.py
index 0757f5f..381e694 100644
--- a/pymare/tests/test_core.py
+++ b/pymare/tests/test_core.py
@@ -1,6 +1,7 @@
 """Tests for pymare.core."""
 import numpy as np
 import pandas as pd
+import pytest
 
 from pymare import Dataset, meta_regression
 
@@ -20,12 +21,55 @@ def test_dataset_init(variables):
 
 def test_dataset_init_from_df(variables):
     """Test Dataset creation from a DataFrame."""
-    df = pd.DataFrame({"y": [2, 4, 6], "v_alt": [100, 100, 100], "X1": [5, 2, 1], "X7": [9, 8, 7]})
-    dataset = Dataset(v="v_alt", X=["X1", "X7"], data=df)
+    df = pd.DataFrame(
+        {
+            "y": [2, 4, 6],
+            "v_alt": [100, 100, 100],
+            "sample_size": [10, 20, 30],
+            "X1": [5, 2, 1],
+            "X7": [9, 8, 7],
+        }
+    )
+    dataset = Dataset(v="v_alt", X=["X1", "X7"], n="sample_size", data=df)
     assert dataset.X.shape == (3, 3)
     assert dataset.X_names == ["intercept", "X1", "X7"]
     assert np.array_equal(dataset.y, np.array([[2, 4, 6]]).T)
     assert np.array_equal(dataset.v, np.array([[100, 100, 100]]).T)
+    assert np.array_equal(dataset.n, np.array([[10, 20, 30]]).T)
+
+    # y is undefined
+    df = pd.DataFrame({"v": [100, 100, 100], "X": [5, 2, 1], "n": [10, 20, 30]})
+    with pytest.raises(KeyError):
+        dataset = Dataset(data=df)
+
+    # X is undefined
+    df = pd.DataFrame({"y": [2, 4, 6], "v_alt": [100, 100, 100], "n": [10, 20, 30]})
+    dataset = Dataset(v="v_alt", data=df)
+    assert dataset.X.shape == (3, 1)
+    assert dataset.X_names == ["intercept"]
+    assert np.array_equal(dataset.y, np.array([[2, 4, 6]]).T)
+    assert np.array_equal(dataset.v, np.array([[100, 100, 100]]).T)
+
+    # X is undefined, but add_intercept is False
+    df = pd.DataFrame({"y": [2, 4, 6], "v_alt": [100, 100, 100], "n": [10, 20, 30]})
+    with pytest.raises(ValueError):
+        dataset = Dataset(v="v_alt", data=df, add_intercept=False)
+
+    # v is undefined
+    df = pd.DataFrame({"y": [2, 4, 6], "X": [5, 2, 1], "n": [10, 20, 30]})
+    dataset = Dataset(data=df)
+    assert dataset.X.shape == (3, 2)
+    assert dataset.X_names == ["intercept", "X"]
+    assert dataset.v is None
+    assert np.array_equal(dataset.y, np.array([[2, 4, 6]]).T)
+
+    # v is undefined
+    df = pd.DataFrame({"y": [2, 4, 6], "X": [5, 2, 1], "v": [10, 20, 30]})
+    dataset = Dataset(data=df)
+    assert dataset.X.shape == (3, 2)
+    assert dataset.X_names == ["intercept", "X"]
+    assert dataset.n is None
+    assert np.array_equal(dataset.y, np.array([[2, 4, 6]]).T)
 
 
 def test_meta_regression_1(variables):