Repository: statsmodels/statsmodels

Issue:
Wrap x-12 arima
See about wrapping some x-12 arima functionality.



Discussion:

Resolving patch:
diff --git a/statsmodels/tools/sm_exceptions.py b/statsmodels/tools/sm_exceptions.py
--- a/statsmodels/tools/sm_exceptions.py
+++ b/statsmodels/tools/sm_exceptions.py
@@ -17,7 +17,26 @@ class PerfectSeparationError(Exception):
 
 class MissingDataError(Exception):
     pass
+
+
+class X13NotFoundError(Exception):
+    pass
+
+
+class X13Error(Exception):
+    pass
+
+
 # Warning
+
+class X13Warning(Warning):
+    pass
+
+
+class IOWarning(RuntimeWarning):
+    pass
+
+
 class ModuleUnavailableWarning(Warning):
     pass
 
diff --git a/statsmodels/tsa/api.py b/statsmodels/tsa/api.py
--- a/statsmodels/tsa/api.py
+++ b/statsmodels/tsa/api.py
@@ -13,3 +13,5 @@
 from .base import datetools
 from .seasonal import seasonal_decompose
 from ..graphics import tsaplots as graphics
+from .x13 import x13_arima_select_order
+from .x13 import x13_arima_analysis
diff --git a/statsmodels/tsa/x13.py b/statsmodels/tsa/x13.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/tsa/x13.py
@@ -0,0 +1,634 @@
+"""
+Run x12/x13-arima specs in a subprocess from Python and curry results back
+into python.
+
+Notes
+-----
+Many of the functions are called x12. However, they are also intended to work
+for x13. If this is not the case, it's a bug.
+"""
+from __future__ import print_function
+import os
+import subprocess
+import tempfile
+import re
+from warnings import warn
+
+import pandas as pd
+
+from statsmodels.compat.python import iteritems
+from statsmodels.tools.tools import Bunch
+from statsmodels.tools.sm_exceptions import (X13NotFoundError,
+                                             IOWarning, X13Error,
+                                             X13Warning)
+
+__all__ = ["x13_arima_select_order", "x13_arima_analysis"]
+
+_binary_names = ('x13as.exe', 'x13as', 'x12a.exe', 'x12a')
+
+
+class _freq_to_period:
+    def __getitem__(self, key):
+        if key.startswith('M'):
+            return 12
+        elif key.startswith('Q'):
+            return 4
+
+
+_freq_to_period = _freq_to_period()
+
+_period_to_freq = {12 : 'M', 4 : 'Q'}
+_log_to_x12 = {True : 'log', False : 'none', None : 'auto'}
+_bool_to_yes_no = lambda x : 'yes' if x else 'no'
+
+
+def _find_x12(x12path=None, prefer_x13=True):
+    """
+    If x12path is not given, then either x13as[.exe] or x12a[.exe] must
+    be found on the PATH. Otherwise, the environmental variable X12PATH or
+    X13PATH must be defined. If prefer_x13 is True, only X13PATH is searched
+    for. If it is false, only X12PATH is searched for.
+    """
+    global _binary_names
+    if x12path is not None and x12path.endswith(_binary_names):
+        # remove binary from path if given
+        x12path = os.path.dirname(x12path)
+
+    if not prefer_x13:  # search for x12 first
+        _binary_names = _binary_names[::-1]
+        if x12path is None:
+            x12path = os.getenv("X12PATH", "")
+        if not x12path:
+            x12path = os.getenv("X13PATH", "")
+    elif x12path is None:
+        x12path = os.getenv("X13PATH", "")
+        if not x12path:
+            x12path = os.getenv("X12PATH", "")
+
+    for binary in _binary_names:
+        x12 = os.path.join(x12path, binary)
+        try:
+            subprocess.check_call(x12, stdout=subprocess.PIPE,
+                                  stderr=subprocess.PIPE)
+            return x12
+        except OSError:
+            pass
+
+    else:
+        return False
+
+
+def _check_x12(x12path=None):
+    x12path = _find_x12(x12path)
+    if not x12path:
+        raise X13NotFoundError("x12a and x13as not found on path. Give the "
+                               "path, put them on PATH, or set the "
+                               "X12PATH or X13PATH environmental variable.")
+    return x12path
+
+
+def _clean_order(order):
+    """
+    Takes something like (1 1 0)(0 1 1) and returns a arma order, sarma
+    order tuple. Also accepts (1 1 0) and return arma order and (0, 0, 0)
+    """
+    order = re.findall("\([0-9 ]*?\)", order)
+    clean = lambda x : tuple(map(int, re.sub("[()]", "", x).split(" ")))
+    if len(order) > 1:
+        order, sorder = map(clean, order)
+    else:
+        order = clean(order[0])
+        sorder = (0, 0, 0)
+
+    return order, sorder
+
+
+def run_spec(x12path, specpath, outname=None, meta=False, datameta=False):
+
+    if meta and datameta:
+        raise ValueError("Cannot specify both meta and datameta.")
+    if meta:
+        args = [x12path, "-m " + specpath]
+    elif datameta:
+        args = [x12path, "-d " + specpath]
+    else:
+        args = [x12path, specpath]
+
+    if outname:
+        args += [outname]
+
+    return subprocess.Popen(args, stdout=subprocess.PIPE,
+                            stderr=subprocess.STDOUT)
+
+
+def _make_automdl_options(maxorder, maxdiff, diff):
+    options = "\n"
+    options += "maxorder = ({0} {1})\n".format(maxorder[0], maxorder[1])
+    if maxdiff is not None:  # maxdiff always takes precedence
+        options += "maxdiff = ({0} {1})\n".format(maxdiff[0], maxdiff[1])
+    else:
+        options += "diff = ({0} {1})\n".format(diff[0], diff[1])
+    return options
+
+
+def _make_var_names(exog):
+    if hasattr(exog, "name"):
+        var_names = exog.name
+    elif hasattr(exog, "columns"):
+        var_names = exog.columns
+    else:
+        raise ValueError("exog is not a Series or DataFrame or is unnamed.")
+    try:
+        var_names = " ".join(var_names)
+    except TypeError:  # cannot have names that are numbers, pandas default
+        from statsmodels.base.data import _make_exog_names
+        if exog.ndim == 1:
+            var_names = "x1"
+        else:
+            var_names = " ".join(_make_exog_names(exog))
+    return var_names
+
+
+def _make_regression_options(trading, exog):
+    if not trading and exog is None:  # start regression spec
+        return ""
+
+    reg_spec = "regression{\n"
+    if trading:
+        reg_spec += "    variables = (td)\n"
+    if exog is not None:
+        var_names = _make_var_names(exog)
+        reg_spec += "    user = ({0})\n".format(var_names)
+        reg_spec += "    data = ({0})\n".format("\n".join(map(str,
+                                                exog.values.ravel().tolist())))
+
+    reg_spec += "}\n"  # close out regression spec
+    return reg_spec
+
+
+def _make_forecast_options(forecast_years):
+    if forecast_years is None:
+        return ""
+    forecast_spec = "forecast{\n"
+    forecast_spec += "maxlead = ({0})\n}}\n".format(forecast_years)
+    return forecast_spec
+
+
+def _check_errors(errors):
+    errors = errors[errors.find("spc:")+4:].strip()
+    if errors and 'ERROR' in errors:
+        raise X13Error(errors)
+    elif errors and 'WARNING' in errors:
+        warn(errors, X13Warning)
+
+
+def _convert_out_to_series(x, dates, name):
+    """
+    Convert x to a DataFrame where x is a string in the format given by
+    x-13arima-seats output.
+    """
+    from StringIO import StringIO
+    from pandas import read_table
+    out = read_table(StringIO(x), skiprows=2, header=None)
+    return out.set_index(dates).rename(columns={1 : name})[name]
+
+
+def _open_and_read(fname):
+    # opens a file, reads it, and make sure it's closed
+    with open(fname, 'r') as fin:
+        fout = fin.read()
+    return fout
+
+
+class Spec(object):
+    @property
+    def spec_name(self):
+        return self.__class__.__name__.replace("Spec", "")
+
+    def create_spec(self, **kwargs):
+        spec = """{name} {{
+        {options}
+        }}
+        """
+        return spec.format(name=self.spec_name,
+                           options=self.options)
+
+    def set_options(self, **kwargs):
+        options = ""
+        for key, value in kwargs.iteritems():
+            options += "{0}={1}\n".format(key, value)
+            self.__dict__.update({key : value})
+        self.options = options
+
+
+class SeriesSpec(Spec):
+    """
+    Parameters
+    ----------
+    data
+    appendbcst : bool
+    appendfcst : bool
+    comptype
+    compwt
+    decimals
+    modelspan
+    name
+    period
+    precision
+    to_print
+    to_save
+    span
+    start
+    title
+    type
+
+    Notes
+    -----
+    Rarely used arguments
+
+    divpower
+    missingcode
+    missingval
+    saveprecision
+    trimzero
+
+    """
+    def __init__(self, data, name='Unnamed Series', appendbcst=False,
+                 appendfcst=False,
+                 comptype=None, compwt=1, decimals=0, modelspan=(),
+                 period=12, precision=0, to_print=[], to_save=[], span=(),
+                 start=(1, 1), title='', series_type=None, divpower=None,
+                 missingcode=-99999, missingval=1000000000):
+
+        appendbcst, appendfcst = map(_bool_to_yes_no, [appendbcst,
+                                                       appendfcst,
+                                                       ])
+
+        series_name = "\"{0}\"".format(name[:64])  # trim to 64 characters
+        title = "\"{0}\"".format(title[:79])  # trim to 79 characters
+        self.set_options(data=data, appendbcst=appendbcst,
+                         appendfcst=appendfcst, period=period, start=start,
+                         title=title, name=series_name,
+                         )
+
+
+def pandas_to_series_spec(x):
+    # from statsmodels.tools.data import _check_period_index
+    # check_period_index(x)
+    if hasattr(x, 'columns'):  # convert to series
+        if len(x.columns) > 1:
+            raise ValueError("Does not handle DataFrame with more than one "
+                             "column")
+        x = x[x.columns[0]]
+
+    data = "({0})".format("\n".join(map(str, x.values.tolist())))
+
+    # get periodicity
+    # get start / first data
+    # give it a title
+    try:
+        period = _freq_to_period[x.index.freqstr]
+    except (AttributeError, ValueError):
+        from pandas.tseries.api import infer_freq
+        period = _freq_to_period[infer_freq(x.index)]
+    start_date = x.index[0]
+    if period == 12:
+        year, stperiod = start_date.year, start_date.month
+    elif period == 4:
+        year, stperiod = start_date.year, start_date.quarter
+    else:  # pragma: no cover
+        raise ValueError("Only monthly and quarterly periods are supported."
+                         " Please report or send a pull request if you want "
+                         "this extended.")
+
+    if hasattr(x, 'name'):
+        name = x.name or "Unnamed Series"
+    else:
+        name = 'Unnamed Series'
+    series_spec = SeriesSpec(data=data, name=name, period=period,
+                             title=name, start="{0}.{1}".format(year,
+                                                                stperiod))
+    return series_spec
+
+
+def x13_arima_analysis(endog, maxorder=(2, 1), maxdiff=(2, 1), diff=None,
+                       exog=None, log=None, outlier=True, trading=False,
+                       forecast_years=None, retspec=False,
+                       speconly=False, start=None, freq=None,
+                       print_stdout=False, x12path=None, prefer_x13=True):
+    """
+    Perform x13-arima analysis for monthly or quarterly data.
+
+    Parameters
+    ----------
+    endog : array-like, pandas.Series
+        The series to model. It is best to use a pandas object with a
+        DatetimeIndex or PeriodIndex. However, you can pass an array-like
+        object. If your object does not have a dates index then ``start`` and
+        ``freq`` are not optional.
+    maxorder : tuple
+        The maximum order of the regular and seasonal ARMA polynomials to
+        examine during the model identification. The order for the regular
+        polynomial must be greater than zero and no larger than 4. The
+        order for the seaonal polynomial may be 1 or 2.
+    maxdiff : tuple
+        The maximum orders for regular and seasonal differencing in the
+        automatic differencing procedure. Acceptable inputs for regular
+        differencing are 1 and 2. The maximum order for seasonal differencing
+        is 1. If ``diff`` is specified then ``maxdiff`` should be None.
+        Otherwise, ``diff`` will be ignored. See also ``diff``.
+    diff : tuple
+        Fixes the orders of differencing for the regular and seasonal
+        differencing. Regular differencing may be 0, 1, or 2. Seasonal
+        differencing may be 0 or 1. ``maxdiff`` must be None, otherwise
+        ``diff`` is ignored.
+    exog : array-like
+        Exogenous variables.
+    log : bool or None
+        If None, it is automatically determined whether to log the series or
+        not. If False, logs are not taken. If True, logs are taken.
+    outlier : bool
+        Whether or not outliers are tested for and corrected, if detected.
+    trading : bool
+        Whether or not trading day effects are tested for.
+    forecast_years : int
+        Number of forecasts produced. The default is one year.
+    retspec : bool
+        Whether to return the created specification file. Can be useful for
+        debugging.
+    speconly : bool
+        Whether to create the specification file and then return it without
+        performing the analysis. Can be useful for debugging.
+    start : str, datetime
+        Must be given if ``endog`` does not have date information in its index.
+        Anything accepted by pandas.DatetimeIndex for the start value.
+    freq : str
+        Must be givein if ``endog`` does not have date information in its
+        index. Anything accapted by pandas.DatetimeIndex for the freq value.
+    print_stdout : bool
+        The stdout from X12/X13 is suppressed. To print it out, set this
+        to True. Default is False.
+    x12path : str or None
+        The path to x12 or x13 binary. If None, the program will attempt
+        to find x13as or x12a on the PATH or by looking at X13PATH or
+        X12PATH depending on the value of prefer_x13.
+    prefer_x13 : bool
+        If True, will look for x13as first and will fallback to the X13PATH
+        environmental variable. If False, will look for x12a first and will
+        fallback to the X12PATH environmental variable. If x12path points
+        to the path for the X12/X13 binary, it does nothing.
+
+
+    Returns
+    -------
+    res : Bunch
+        A bunch object with the following attributes:
+
+        - results : str
+          The full output from the X12/X13 run.
+        - seasadj : pandas.Series
+          The final seasonally adjusted ``endog``
+        - trend : pandas.Series
+          The trend-cycle component of ``endog``
+        - irregular : pandas.Series
+          The final irregular component of ``endog``
+        - stdout : str
+          The captured stdout produced by x12/x13.
+        - spec : str, optional
+          Returned if ``retspec`` is True. The only thing returned if
+          ``speconly`` is True.
+
+    Notes
+    -----
+    This works by creating a specification file, writing it to a temporary
+    directory, invoking X12/X13 in a subprocess, and reading the output
+    directory, invoking exog12/X13 in a subprocess, and reading the output
+    back in.
+    """
+    x12path = _check_x12(x12path)
+
+    if not isinstance(endog, (pd.DataFrame, pd.Series)):
+        if start is None or freq is None:
+            raise ValueError("start and freq cannot be none if endog is not "
+                             "a pandas object")
+        endog = pd.Series(endog, index=pd.DatetimeIndex(start=start,
+                                                        periods=len(endog),
+                                                        freq=freq))
+    spec_obj = pandas_to_series_spec(endog)
+    spec = spec_obj.create_spec()
+    spec += "transform{{function={0}}}\n".format(_log_to_x12[log])
+    if outlier:
+        spec += "outlier{}\n"
+    options = _make_automdl_options(maxorder, maxdiff, diff)
+    spec += "automdl{{{0}}}\n".format(options)
+    spec += _make_regression_options(trading, exog)
+    spec += _make_forecast_options(forecast_years)
+    spec += "x11{ save=(d11 d12 d13) }"
+    if speconly:
+        return spec
+    # write it to a tempfile
+    # TODO: make this more robust - give the user some control?
+    ftempin = tempfile.NamedTemporaryFile(delete=False, suffix='.spc')
+    ftempout = tempfile.NamedTemporaryFile(delete=False)
+    try:
+        ftempin.write(spec)
+        ftempin.close()
+        ftempout.close()
+        # call x12 arima
+        p = run_spec(x12path, ftempin.name[:-4], ftempout.name)
+        p.wait()
+        stdout = p.stdout.read()
+        if print_stdout:
+            print(p.stdout.read())
+        # check for errors
+        errors = _open_and_read(ftempout.name + '.err')
+        _check_errors(errors)
+
+        # read in results
+        results = _open_and_read(ftempout.name + '.out')
+        seasadj = _open_and_read(ftempout.name + '.d11')
+        trend = _open_and_read(ftempout.name + '.d12')
+        irregular = _open_and_read(ftempout.name + '.d13')
+    finally:
+        try:  # sometimes this gives a permission denied error?
+            #   not sure why. no process should have these open
+            os.remove(ftempin.name)
+            os.remove(ftempout.name)
+        except:
+            if os.path.exists(ftempin.name):
+                warn("Failed to delete resource {0}".format(ftempin.name),
+                     IOWarning)
+            if os.path.exists(ftempout.name):
+                warn("Failed to delete resource {0}".format(ftempout.name),
+                     IOWarning)
+
+    seasadj = _convert_out_to_series(seasadj, endog.index, 'seasadj')
+    trend = _convert_out_to_series(trend, endog.index, 'trend')
+    irregular = _convert_out_to_series(irregular, endog.index, 'irregular')
+
+    # NOTE: there isn't likely anything in stdout that's not in results
+    #       so may be safe to just suppress and remove it
+    if not retspec:
+        res = X13ArimaAnalysisResult(observed=endog, results=results,
+                                     seasadj=seasadj, trend=trend,
+                                     irregular=irregular, stdout=stdout)
+    else:
+        res = X13ArimaAnalysisResult(observed=endog, results=results,
+                                     seasadj=seasadj, trend=trend,
+                                     irregular=irregular, stdout=stdout,
+                                     spec=spec)
+    return res
+
+
+def x13_arima_select_order(endog, maxorder=(2, 1), maxdiff=(2, 1), diff=None,
+                           exog=None, log=None, outlier=True, trading=False,
+                           forecast_years=None,
+                           start=None, freq=None, print_stdout=False,
+                           x12path=None, prefer_x13=True):
+    """
+    Perform automatic seaonal ARIMA order identification using x12/x13 ARIMA.
+
+    Parameters
+    ----------
+    endog : array-like, pandas.Series
+        The series to model. It is best to use a pandas object with a
+        DatetimeIndex or PeriodIndex. However, you can pass an array-like
+        object. If your object does not have a dates index then ``start`` and
+        ``freq`` are not optional.
+    maxorder : tuple
+        The maximum order of the regular and seasonal ARMA polynomials to
+        examine during the model identification. The order for the regular
+        polynomial must be greater than zero and no larger than 4. The
+        order for the seaonal polynomial may be 1 or 2.
+    maxdiff : tuple
+        The maximum orders for regular and seasonal differencing in the
+        automatic differencing procedure. Acceptable inputs for regular
+        differencing are 1 and 2. The maximum order for seasonal differencing
+        is 1. If ``diff`` is specified then ``maxdiff`` should be None.
+        Otherwise, ``diff`` will be ignored. See also ``diff``.
+    diff : tuple
+        Fixes the orders of differencing for the regular and seasonal
+        differencing. Regular differencing may be 0, 1, or 2. Seasonal
+        differencing may be 0 or 1. ``maxdiff`` must be None, otherwise
+        ``diff`` is ignored.
+    exog : array-like
+        Exogenous variables.
+    log : bool or None
+        If None, it is automatically determined whether to log the series or
+        not. If False, logs are not taken. If True, logs are taken.
+    outlier : bool
+        Whether or not outliers are tested for and corrected, if detected.
+    trading : bool
+        Whether or not trading day effects are tested for.
+    forecast_years : int
+        Number of forecasts produced. The default is one year.
+    start : str, datetime
+        Must be given if ``endog`` does not have date information in its index.
+        Anything accepted by pandas.DatetimeIndex for the start value.
+    freq : str
+        Must be givein if ``endog`` does not have date information in its
+        index. Anything accapted by pandas.DatetimeIndex for the freq value.
+    print_stdout : bool
+        The stdout from X12/X13 is suppressed. To print it out, set this
+        to True. Default is False.
+    x12path : str or None
+        The path to x12 or x13 binary. If None, the program will attempt
+        to find x13as or x12a on the PATH or by looking at X13PATH or X12PATH
+        depending on the value of prefer_x13.
+    prefer_x13 : bool
+        If True, will look for x13as first and will fallback to the X13PATH
+        environmental variable. If False, will look for x12a first and will
+        fallback to the X12PATH environmental variable. If x12path points
+        to the path for the X12/X13 binary, it does nothing.
+
+    Returns
+    -------
+    results : Bunch
+        A bunch object that has the following attributes:
+
+        - order : tuple
+          The regular order
+        - sorder : tuple
+          The seasonal order
+        - include_mean : bool
+          Whether to include a mean or not
+        - results : str
+          The full results from the X12/X13 analysis
+        - stdout : str
+          The captured stdout from the X12/X13 analysis
+
+    Notes
+    -----
+    This works by creating a specification file, writing it to a temporary
+    directory, invoking X12/X13 in a subprocess, and reading the output back
+    in.
+    """
+    results = x13_arima_analysis(endog, x12path=x12path, exog=exog, log=log,
+                                 outlier=outlier, trading=trading,
+                                 forecast_years=forecast_years,
+                                 maxorder=maxorder, maxdiff=maxdiff, diff=diff,
+                                 start=start, freq=freq, prefer_x13=prefer_x13)
+    model = re.search("(?<=Final automatic model choice : ).*",
+                      results.results)
+    order = model.group()
+    if re.search("Mean is not significant", results.results):
+        include_mean = False
+    elif re.search("Constant", results.results):
+        include_mean = True
+    else:
+        include_mean = False
+    order, sorder = _clean_order(order)
+    res = Bunch(order=order, sorder=sorder, include_mean=include_mean,
+                results=results.results, stdout=results.stdout)
+    return res
+
+
+class X13ArimaAnalysisResult(object):
+    def __init__(self, **kwargs):
+        for key, value in iteritems(kwargs):
+            setattr(self, key, value)
+
+    def plot(self):
+        from statsmodels.graphics.utils import _import_mpl
+        plt = _import_mpl()
+        fig, axes = plt.subplots(4, 1, sharex=True)
+        self.observed.plot(ax=axes[0], legend=False)
+        axes[0].set_ylabel('Observed')
+        self.seasadj.plot(ax=axes[1], legend=False)
+        axes[1].set_ylabel('Seas. Adjusted')
+        self.trend.plot(ax=axes[2], legend=False)
+        axes[2].set_ylabel('Trend')
+        self.irregular.plot(ax=axes[3], legend=False)
+        axes[3].set_ylabel('Irregular')
+
+        fig.tight_layout()
+        return fig
+
+
+if __name__ == "__main__":
+    import numpy as np
+    from statsmodels.tsa.arima_process import ArmaProcess
+    np.random.seed(123)
+    ar = [1, .35, .8]
+    ma = [1, .8]
+    arma = ArmaProcess(ar, ma, nobs=100)
+    assert arma.isstationary()
+    assert arma.isinvertible()
+    y = arma.generate_sample()
+    dates = pd.date_range("1/1/1990", periods=len(y), freq='M')
+    ts = pd.TimeSeries(y, index=dates)
+
+    xpath = "/home/skipper/src/x12arima/x12a"
+
+    try:
+        results = x13_arima_analysis(xpath, ts)
+    except:
+        print("Caught exception")
+
+    results = x13_arima_analysis(xpath, ts, log=False)
+
+    # import pandas as pd
+    # seas_y = pd.read_csv("usmelec.csv")
+    # seas_y = pd.TimeSeries(seas_y["usmelec"].values,
+    #                        index=pd.DatetimeIndex(seas_y["date"], freq="MS"))
+    # results = x13_arima_analysis(xpath, seas_y)


Test patch:
diff --git a/statsmodels/tsa/tests/test_x13.py b/statsmodels/tsa/tests/test_x13.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/tsa/tests/test_x13.py
@@ -0,0 +1,53 @@
+from nose import SkipTest
+from numpy.testing import assert_
+
+from statsmodels.tsa.base.datetools import dates_from_range
+from statsmodels.tsa.x13 import _find_x12, x13_arima_select_order
+
+x13path = _find_x12()
+
+if x13path is False:
+    _have_x13 = False
+else:
+    _have_x13 = True
+
+class TestX13(object):
+    @classmethod
+    def setupClass(cls):
+        if not _have_x13:
+            raise SkipTest('X13/X12 not available')
+
+        import pandas as pd
+        from statsmodels.datasets import macrodata, co2
+        dta = macrodata.load_pandas().data
+        dates = dates_from_range('1959Q1', '2009Q3')
+        index = pd.DatetimeIndex(dates)
+        dta.index = index
+        cls.quarterly_data = dta.dropna()
+
+        dta = co2.load_pandas().data
+        dta['co2'] = dta.co2.interpolate()
+        cls.monthly_data = dta.resample('M')
+
+        cls.monthly_start_data = dta.resample('MS')
+
+    def test_x13_arima_select_order(self):
+        res = x13_arima_select_order(self.monthly_data)
+        assert_(isinstance(res.order, tuple))
+        assert_(isinstance(res.sorder, tuple))
+        res = x13_arima_select_order(self.monthly_start_data)
+        assert_(isinstance(res.order, tuple))
+        assert_(isinstance(res.sorder, tuple))
+        res = x13_arima_select_order(self.monthly_data.co2)
+        assert_(isinstance(res.order, tuple))
+        assert_(isinstance(res.sorder, tuple))
+        res = x13_arima_select_order(self.monthly_start_data.co2)
+        assert_(isinstance(res.order, tuple))
+        assert_(isinstance(res.sorder, tuple))
+
+        res = x13_arima_select_order(self.quarterly_data[['realgdp']])
+        assert_(isinstance(res.order, tuple))
+        assert_(isinstance(res.sorder, tuple))
+        res = x13_arima_select_order(self.quarterly_data.realgdp)
+        assert_(isinstance(res.order, tuple))
+        assert_(isinstance(res.sorder, tuple))