Repository: statsmodels/statsmodels

Issue:
BUG: AdditiveGamSmoother.transform fails for 1-D arrays
#### Describe the bug

Passing a 1-D numpy array, or a pandas DataFrame or Series as the `x` parameter to `BSplines` succeeds in returning a `BSplines` instance, but then fails to transform the same `x` when passed as the argument to `BSplines.transform`

#### Code Sample

```python
import numpy as np
import pandas as pd
from statsmodels.gam.smooth_basis import BSplines

x = np.linspace(0, 1, 100)
df = pd.DataFrame(dict(x1=x, x2=x))

splines = BSplines(x, [6], [3])
splines.transform(x)

df_splines = BSplines(df, [6, 6], [3, 3])
df_splines.transform(df)

series_splines = BSplines(df["x1"], [6], [3])
series_splines.transform(df["x1"])
```
- A 1-D ndarray raises `IndexError: too many indices for array`
- A DataFrame raises `TypeError: '(slice(None, None, None), 0)' is an invalid key`
- A Series raises `ValueError: Can only tuple-index with a MultiIndex`

#### Expected Output

Transformed arrays

#### Output of ``import statsmodels.api as sm; sm.show_versions()``

<details>
INSTALLED VERSIONS
------------------
Python: 3.7.5.final.0

statsmodels
===========

Installed: 0.11.1

Required Dependencies
=====================

cython: 0.29.21
numpy: 1.18.5
scipy: 1.5.0
pandas: 0.25.3
    dateutil: 2.8.1
patsy: 0.5.1

Optional Dependencies
=====================

matplotlib: 3.2.2
    backend: module://ipykernel.pylab.backend_inline 
cvxopt: Not installed
joblib: 0.16.0

Developer Tools
================

IPython: 7.16.1
    jinja2: 2.11.2
sphinx: 3.1.2
    pygments: 2.6.1
pytest: 5.4.3
virtualenv: 16.7.8

</details>



Discussion:

Resolving patch:
diff --git a/statsmodels/gam/smooth_basis.py b/statsmodels/gam/smooth_basis.py
--- a/statsmodels/gam/smooth_basis.py
+++ b/statsmodels/gam/smooth_basis.py
@@ -616,7 +616,7 @@ class UnivariateCubicCyclicSplines(UnivariateGamSmoother):
     x : ndarray, 1-D
         underlying explanatory variable for smooth terms.
     df : int
-        numer of basis functions or degrees of freedom
+        number of basis functions or degrees of freedom
     degree : int
         degree of the spline
     include_intercept : bool
@@ -792,6 +792,8 @@ def transform(self, x_new):
         basis : ndarray
             design matrix for the spline basis for given ``x_new``.
         """
+        if x_new.ndim == 1 and self.k_variables == 1:
+            x_new = x_new.reshape(-1, 1)
         exog = np.hstack(list(self.smoothers[i].transform(x_new[:, i])
                          for i in range(self.k_variables)))
         return exog
@@ -839,10 +841,13 @@ class BSplines(AdditiveGamSmoother):
         underlying explanatory variable for smooth terms.
         If 2-dimensional, then observations should be in rows and
         explanatory variables in columns.
-    df :  int
-        numer of basis functions or degrees of freedom
-    degree : int
-        degree of the spline
+    df :  {int, array_like[int]}
+        number of basis functions or degrees of freedom; should be equal
+        in length to the number of columns of `x`; may be an integer if
+        `x` has one column or is 1-D.
+    degree : {int, array_like[int]}
+        degree(s) of the spline; the same length and type rules apply as
+        to `df`
     include_intercept : bool
         If False, then the basis functions are transformed so that they
         do not include a constant. This avoids perfect collinearity if
@@ -907,8 +912,14 @@ class BSplines(AdditiveGamSmoother):
     """
     def __init__(self, x, df, degree, include_intercept=False,
                  constraints=None, variable_names=None, knot_kwds=None):
-        self.degrees = degree
-        self.dfs = df
+        if isinstance(degree, int):
+            self.degrees = np.array([degree], dtype=int)
+        else:
+            self.degrees = degree
+        if isinstance(df, int):
+            self.dfs = np.array([df], dtype=int)
+        else:
+            self.dfs = df
         self.knot_kwds = knot_kwds
         # TODO: move attaching constraints to super call
         self.constraints = constraints


Test patch:
diff --git a/statsmodels/gam/tests/test_smooth_basis.py b/statsmodels/gam/tests/test_smooth_basis.py
--- a/statsmodels/gam/tests/test_smooth_basis.py
+++ b/statsmodels/gam/tests/test_smooth_basis.py
@@ -6,10 +6,12 @@
 
 """
 
+import pytest
 import numpy as np
 from numpy.testing import assert_allclose, assert_equal
 from statsmodels.gam.smooth_basis import (UnivariatePolynomialSmoother,
-                                          PolynomialSmoother)
+                                          PolynomialSmoother,
+                                          BSplines)
 
 
 def test_univariate_polynomial_smoother():
@@ -27,3 +29,19 @@ def test_multivariate_polynomial_basis():
     for i, deg in enumerate(degrees):
         uv_basis = UnivariatePolynomialSmoother(x[:, i], degree=deg).basis
         assert_allclose(mps.smoothers[i].basis, uv_basis)
+
+
+@pytest.mark.parametrize(
+    "x, df, degree",
+    [
+        (
+            np.c_[np.linspace(0, 1, 100), np.linspace(0, 10, 100)],
+            [5, 6],
+            [3, 5]
+        ),
+        (np.linspace(0, 1, 100), 6, 3),
+    ]
+)
+def test_bsplines(x, df, degree):
+    bspline = BSplines(x, df, degree)
+    bspline.transform(x)