Repository: statsmodels/statsmodels

Issue:
ARIMA missing predict exog causes type error
I am getting a `TypeError` when forecasting on an `ARIMA` when `exog` is not None. Here is the setup for a reproducible example:

```python
from statsmodels.tsa.arima_model import ARIMA
import numpy as np
from numpy.random import RandomState
rs = RandomState(42)

hr = np.array([84.2697, 84.2697, 84.0619, 85.6542, 87.2093, 87.1246,
               86.8726, 86.7052, 87.5899, 89.1475, 89.8204, 89.8204,
               90.4375, 91.7605, 93.1081, 94.3291, 95.8003, 97.5119,
               98.7457, 98.904, 98.3437, 98.3075, 98.8313, 99.0789,
               98.8157, 98.2998, 97.7311, 97.6471, 97.7922, 97.2974,
               96.2042, 95.2318, 94.9367, 95.0867, 95.389, 95.5414,
               95.2439, 94.9415, 95.3557, 96.3423, 97.1563, 97.4026,
               96.7028, 96.5516, 97.9837, 98.9879, 97.6312, 95.4064,
               93.8603, 93.0552, 94.6012, 95.8476, 95.7692, 95.9236,
               95.7692, 95.9211, 95.8501, 94.6703, 93.0993, 91.972,
               91.7821, 91.7911, 90.807, 89.3196, 88.1511, 88.7762,
               90.2265, 90.8066, 91.2284, 92.4238, 93.243, 92.8472,
               92.5926, 91.7778, 91.2974, 91.6364, 91.2952, 91.771,
               93.2285, 93.3199, 91.8799, 91.2239, 92.4055, 93.8716,
               94.5825, 94.5594, 94.9453, 96.2412, 96.6879, 95.8295,
               94.7819, 93.4731, 92.7997, 92.963, 92.6996, 91.9648,
               91.2417, 91.9312, 93.9548, 95.3044, 95.2511, 94.5358,
               93.8093, 93.2287, 92.2065, 92.1588, 93.6376, 94.899,
               95.1592, 95.2415, 95.5414, 95.0971, 94.528, 95.5887,
               96.4715, 96.6158, 97.0769, 96.8531, 96.3947, 97.4291,
               98.1767, 97.0148, 96.044, 95.9581, 96.4814, 96.5211,
               95.3629, 93.5741, 92.077, 90.4094, 90.1751, 91.3312,
               91.2883, 89.0592, 87.052, 86.6226, 85.7889, 85.6348,
               85.3911, 83.8064, 82.8729, 82.6266, 82.645, 82.645,
               82.645, 82.645, 82.645, 82.645, 82.645, 82.645])

# define model fit
fit = ARIMA(hr, order=(2, 1, 2), exog=rs.rand(hr.shape[0], 4))
res = fit.fit(disp=0)
```

And then when forecasting, here's what I get:

```python
>>> res.forecast(steps=5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "//anaconda/lib/python2.7/site-packages/statsmodels/tsa/arima_model.py", line 1869, in forecast
    exog, method=self.model.method)
  File "//anaconda/lib/python2.7/site-packages/statsmodels/tsa/arima_model.py", line 326, in _arma_predict_out_of_sample
    exog)
  File "//anaconda/lib/python2.7/site-packages/statsmodels/tsa/arima_model.py", line 292, in _get_predict_out_of_sample
    X = lagmat(np.dot(exog, exparams), p, original='in', trim='both')
TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'
```

Is this a bug or something I'm doing wrong?


Discussion:
I don't know if there is a good place to raise an easier to understand error.

If you forecast and you have used an exog, then you also need to provide a exog for the forecast periods. Given that forecast is out of sample, there is not enough information to make a forecast.
I think the specific exception is because, exog=None, by default.

(My guess would be that the estimation exog is available and used for in-sample prediction.)
That makes sense. Would it be worth storing a class attribute, something like `self.fit_with_exog_`, in `__init__` and then test `if self.fit_with_exog_ and exog is None:` raise? I know it's clunky to save that extra attribute, but it might cause less confusion...
We wouldn't need an extra attribute to check this. We can just check whether `self.model.exog is not None`

It is possible to add this check in forecast, where we always need new exog, but not in predict where it depends on the predict dates whether we need out-of-sample exog or have them in-sample.
We also have `k-exog` as attribute.

Design: we can raise an exception if exog is None and k_exog >0 either
- in the forecast function, or
- in the final function that does the computation, which is `_get_predict_out_of_sample`

The first has the advantage that input errors are caught at the source and it is obvious what we check for. The disadvantage is that we need to add it to all forecast methods.
The second is hiding away the check but doesn't require code duplication (example for problems because of hiding input checks https://github.com/statsmodels/statsmodels/issues/3503#issuecomment-282361464 )

Note: `_get_predict_out_of_sample` has two places where exog dot params is computed, and initially I was reading the wrong one
Related check: IIRC, we don't impose that exog for forecast has the correct length, i.e. it can be longer than needed. (based on what I remember from previous bug fixes)

so we don't check shape, exog.shape[1] != k_exog should raise automatically an informative exception, but I don't know if exog.ndim = 2 is required.

Resolving patch:
diff --git a/statsmodels/tsa/arima_model.py b/statsmodels/tsa/arima_model.py
--- a/statsmodels/tsa/arima_model.py
+++ b/statsmodels/tsa/arima_model.py
@@ -1558,10 +1558,19 @@ def forecast(self, steps=1, exog=None, alpha=.05):
                 exog = exog[None, :]
             if exog.shape[0] != steps:
                 raise ValueError("new exog needed for each step")
+            if self.k_exog != exog.shape[1]:
+                raise ValueError('exog must contain the same number of '
+                                 'variables as in the estimated model.')
             # prepend in-sample exog observations
             if self.k_ar > 0:
                 exog = np.vstack((self.model.exog[-self.k_ar:, self.k_trend:],
                                   exog))
+        else:
+            if self.k_exog:
+                raise ValueError('Forecast values for exog are required when '
+                                 'the model contains exogenous regressors.')
+
+
 
         forecast = _arma_predict_out_of_sample(self.params,
                                                steps, self.resid, self.k_ar,
@@ -1873,10 +1882,18 @@ def forecast(self, steps=1, exog=None, alpha=.05):
                 exog = exog[:, None]
             if exog.shape[0] != steps:
                 raise ValueError("new exog needed for each step")
+            if self.k_exog != exog.shape[1]:
+                raise ValueError('exog must contain the same number of '
+                                 'variables as in the estimated model.')
             # prepend in-sample exog observations
             if self.k_ar > 0:
                 exog = np.vstack((self.model.exog[-self.k_ar:, self.k_trend:],
                                   exog))
+        else:
+            if self.k_exog:
+                raise ValueError('Forecast values for exog are required when '
+                                 'the model contains exogenous regressors.')
+
         forecast = _arma_predict_out_of_sample(self.params, steps, self.resid,
                                                self.k_ar, self.k_ma,
                                                self.k_trend, self.k_exog,


Test patch:
diff --git a/statsmodels/tsa/tests/test_arima.py b/statsmodels/tsa/tests/test_arima.py
--- a/statsmodels/tsa/tests/test_arima.py
+++ b/statsmodels/tsa/tests/test_arima.py
@@ -1877,7 +1877,18 @@ def test_arima_exog_predict_1d():
     x = np.random.random(100)
     mod = ARMA(y, (2, 1), x).fit(disp=-1)
     newx = np.random.random(10)
-    results = mod.forecast(steps=10, alpha=0.05, exog=newx)
+    mod.forecast(steps=10, alpha=0.05, exog=newx)
+
+    with pytest.raises(ValueError):
+        mod.forecast(steps=10, alpha=0.05, exog=newx[:5])
+
+    with pytest.raises(ValueError):
+        mod.forecast(steps=10, alpha=0.05)
+
+    too_many = pd.DataFrame(np.zeros((10, 2)),
+                            columns=['x1', 'x2'])
+    with pytest.raises(ValueError):
+        mod.forecast(steps=10, alpha=0.05, exog=too_many)
 
 
 def test_arima_1123():
@@ -2142,6 +2153,16 @@ def test_arima111_predict_exog_2127():
 
     assert_allclose(predicts, predicts_res, atol=5e-6)
 
+    # Smoke check of forecast with exog in ARIMA
+    res.forecast(steps=10, exog=np.empty(10))
+
+    with pytest.raises(ValueError):
+        res.forecast(steps=10)
+    with pytest.raises(ValueError):
+        res.forecast(steps=10, exog=np.empty((10, 2)))
+    with pytest.raises(ValueError):
+        res.forecast(steps=10, exog=np.empty(100))
+
 
 def test_ARIMA_exog_predict():
     # test forecasting and dynamic prediction with exog against Stata
@@ -2261,6 +2282,14 @@ def test_ARIMA_exog_predict():
                                  exog=exog_full.values[100:120], dynamic=True)
     assert_allclose(predict_3a, predict_3b, rtol=1e-10)
 
+    h = len(exog_full.values[197:])
+    with pytest.raises(ValueError):
+        res_002.forecast(steps=h)
+    with pytest.raises(ValueError):
+        res_002.forecast(steps=h, exog=np.empty((h, 20)))
+    with pytest.raises(ValueError):
+        res_002.forecast(steps=h, exog=np.empty(20))
+
 
 def test_arima_fit_mutliple_calls():
     y = [-1214.360173, -1848.209905, -2100.918158, -3647.483678, -4711.186773]