Repository: pandas-dev/pandas
Pull request: ENH: support non default indexes in writing to Parquet
- [x] closes #18581
- [x] tests added / passed
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
Support non-default indexes in to_parquet
Calling `to_parquet` on a DataFrame with a non-default index results in the error below:
```python-traceback
ValueError: parquet does not support serializing a non-default index for the index; you can .reset_index() to make the index into column(s)
```
While, you can work-around this by calling `reset_index()`  as the message says, this loses the information about what columns made up the index so means you can't round-trip a DataFrame with a non-default index.


#### Output of ``pd.show_versions()``

<details>

```
INSTALLED VERSIONS
------------------
commit: None
python: 3.6.3.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 142 Stepping 9, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.21.0
pytest: 3.2.5
pip: 9.0.1
setuptools: 37.0.0
Cython: 0.27.3
numpy: 1.13.3
scipy: 1.0.0
pyarrow: 0.7.1
xarray: None
IPython: 6.2.1
sphinx: 1.6.5
patsy: 0.4.1
dateutil: 2.6.1
pytz: 2017.3
blosc: 1.4.4
```
</details>

Hints from issue discussion:
If I remove the checking code (this is with pyarrow 0.7.1)

```
In [2]: df = pd.DataFrame({'A': list('abc')}, index=[2, 3, 4])

In [3]: df
Out[3]: 
   A
2  a
3  b
4  c

In [4]: df.to_parquet('foo.parquet')

In [5]: pd.read_parquet('foo.parquet')
Out[5]: 
   A
2  a
3  b
4  c
```

```
diff --git a/pandas/io/parquet.py b/pandas/io/parquet.py
index 4a13d2c9d..b7cce3ae7 100644
--- a/pandas/io/parquet.py
+++ b/pandas/io/parquet.py
@@ -147,28 +147,6 @@ def to_parquet(df, path, engine='auto', compression='snappy', **kwargs):
 
     valid_types = {'string', 'unicode'}
 
-    # validate index
-    # --------------
-
-    # validate that we have only a default index
-    # raise on anything else as we don't serialize the index
-
-    if not isinstance(df.index, Int64Index):
-        raise ValueError("parquet does not support serializing {} "
-                         "for the index; you can .reset_index()"
-                         "to make the index into column(s)".format(
-                             type(df.index)))
-
-    if not df.index.equals(RangeIndex.from_range(range(len(df)))):
-        raise ValueError("parquet does not support serializing a "
-                         "non-default index for the index; you "
-                         "can .reset_index() to make the index "
-                         "into column(s)")
-
-    if df.index.name is not None:
-        raise ValueError("parquet does not serialize index meta-data on a "
-                         "default index")
-
     # validate columns
     # ----------------
 
```

We support ``pyarrow >= 0.4.1``, I don't remember exactly when index support was added (and had a bug or 2), but we could check conditionally (as we have other compat code for pyarrow < 0.5.0, and < 0.6.0 for other items). alternatively could bump minimum to 0.6.0 is ok too.

@dhirschfeld would love a PR.

cc @cpcloud @wesm 
Seems a simple fix! Will see about putting in a PR shortly...

Commit message: move whatsnew note to parquet section

Patch to review:
diff --git a/ci/requirements-2.7.sh b/ci/requirements-2.7.sh
index e3bd5e46026c5..9e59f3e5eeb55 100644
--- a/ci/requirements-2.7.sh
+++ b/ci/requirements-2.7.sh
@@ -4,4 +4,4 @@ source activate pandas
 
 echo "install 27"
 
-conda install -n pandas -c conda-forge feather-format pyarrow=0.4.1 fastparquet
+conda install -n pandas -c conda-forge feather-format pyarrow=0.7.0 fastparquet
diff --git a/ci/requirements-3.5.sh b/ci/requirements-3.5.sh
index d694ad3679ac1..a1ed467b36ff9 100644
--- a/ci/requirements-3.5.sh
+++ b/ci/requirements-3.5.sh
@@ -8,4 +8,4 @@ echo "install 35"
 conda remove -n pandas python-dateutil --force
 pip install python-dateutil
 
-conda install -n pandas -c conda-forge feather-format pyarrow=0.5.0
+conda install -n pandas -c conda-forge feather-format pyarrow=0.7.0
diff --git a/doc/source/install.rst b/doc/source/install.rst
index 979d5afd0a04f..641a07ed22810 100644
--- a/doc/source/install.rst
+++ b/doc/source/install.rst
@@ -229,7 +229,7 @@ Optional Dependencies
 * `xarray <http://xarray.pydata.org>`__: pandas like handling for > 2 dims, needed for converting Panels to xarray objects. Version 0.7.0 or higher is recommended.
 * `PyTables <http://www.pytables.org>`__: necessary for HDF5-based storage. Version 3.0.0 or higher required, Version 3.2.1 or higher highly recommended.
 * `Feather Format <https://github.com/wesm/feather>`__: necessary for feather-based storage, version 0.3.1 or higher.
-* `Apache Parquet <https://parquet.apache.org/>`__, either `pyarrow <http://arrow.apache.org/docs/python/>`__ (>= 0.4.1) or `fastparquet <https://fastparquet.readthedocs.io/en/latest/necessary>`__ (>= 0.0.6) for parquet-based storage. The `snappy <https://pypi.python.org/pypi/python-snappy>`__ and `brotli <https://pypi.python.org/pypi/brotlipy>`__ are available for compression support.
+* `Apache Parquet <https://parquet.apache.org/>`__, either `pyarrow <http://arrow.apache.org/docs/python/>`__ (>= 0.7.0) or `fastparquet <https://fastparquet.readthedocs.io/en/latest/necessary>`__ (>= 0.1.0) for parquet-based storage. The `snappy <https://pypi.python.org/pypi/python-snappy>`__ and `brotli <https://pypi.python.org/pypi/brotlipy>`__ are available for compression support.
 * `SQLAlchemy <http://www.sqlalchemy.org>`__: for SQL database support. Version 0.8.1 or higher recommended. Besides SQLAlchemy, you also need a database specific driver. You can find an overview of supported drivers for each SQL dialect in the `SQLAlchemy docs <http://docs.sqlalchemy.org/en/latest/dialects/index.html>`__. Some common drivers are:
 
   * `psycopg2 <http://initd.org/psycopg/>`__: for PostgreSQL
diff --git a/doc/source/io.rst b/doc/source/io.rst
index 54e7a11c5f2b1..2cbd64bf5186b 100644
--- a/doc/source/io.rst
+++ b/doc/source/io.rst
@@ -4504,11 +4504,8 @@ dtypes, including extension dtypes such as datetime with tz.
 
 Several caveats.
 
-- The format will NOT write an ``Index``, or ``MultiIndex`` for the
-  ``DataFrame`` and will raise an error if a non-default one is provided. You
-  can ``.reset_index()`` to store the index or ``.reset_index(drop=True)`` to
-  ignore it.
 - Duplicate column names and non-string columns names are not supported
+- Index level names, if specified, must be strings
 - Categorical dtypes can be serialized to parquet, but will de-serialize as ``object`` dtype.
 - Non supported types include ``Period`` and actual python object types. These will raise a helpful error message
   on an attempt at serialization.
diff --git a/doc/source/whatsnew/v0.21.1.txt b/doc/source/whatsnew/v0.21.1.txt
index 31902c98d0b6c..1b709cfd4aa90 100644
--- a/doc/source/whatsnew/v0.21.1.txt
+++ b/doc/source/whatsnew/v0.21.1.txt
@@ -61,6 +61,9 @@ New features
 Improvements to the Parquet IO functionality
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
+- :func:`DataFrame.to_parquet` will now write non-default indexes when the
+  underlying engine supports it. The indexes will be preserved when reading
+  back in with :func:`read_parquet` (:issue:`18581`).
 - :func:`read_parquet` now allows to specify the columns to read from a parquet file (:issue:`18154`)
 - :func:`read_parquet` now allows to specify kwargs which are passed to the respective engine (:issue:`18216`)
 
@@ -91,8 +94,6 @@ Performance Improvements
 
 Bug Fixes
 ~~~~~~~~~
--
-
 
 Conversion
 ^^^^^^^^^^
diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt
index 02cb5aa870c9b..650832beadae3 100644
--- a/doc/source/whatsnew/v0.22.0.txt
+++ b/doc/source/whatsnew/v0.22.0.txt
@@ -329,4 +329,4 @@ Other
 ^^^^^
 
 - Improved error message when attempting to use a Python keyword as an identifier in a ``numexpr`` backed query (:issue:`18221`)
--
+-
\ No newline at end of file
diff --git a/pandas/io/parquet.py b/pandas/io/parquet.py
index 7827c3ae04d4d..8f07b14fc4a3d 100644
--- a/pandas/io/parquet.py
+++ b/pandas/io/parquet.py
@@ -3,7 +3,8 @@
 from warnings import catch_warnings
 from distutils.version import LooseVersion
 from pandas import DataFrame, RangeIndex, Int64Index, get_option
-from pandas.compat import range
+from pandas.compat import string_types
+from pandas.core.common import AbstractMethodError
 from pandas.io.common import get_filepath_or_buffer
 
 
@@ -34,82 +35,155 @@ def get_engine(engine):
         return FastParquetImpl()
 
 
-class PyArrowImpl(object):
+class BaseImpl(object):
+
+    api = None  # module
+
+    @staticmethod
+    def validate_dataframe(df):
+        if not isinstance(df, DataFrame):
+            raise ValueError("to_parquet only supports IO with DataFrames")
+        # must have value column names (strings only)
+        if df.columns.inferred_type not in {'string', 'unicode'}:
+            raise ValueError("parquet must have string column names")
+        # index level names must be strings
+        valid_names = all(
+            isinstance(name, string_types)
+            for name in df.index.names
+            if name is not None
+        )
+        if not valid_names:
+            raise ValueError("Index level names must be strings")
+
+    def write(self, df, path, compression, **kwargs):
+        raise AbstractMethodError(self)
+
+    def read(self, path, columns=None, **kwargs):
+        raise AbstractMethodError(self)
+
+
+class PyArrowImpl(BaseImpl):
 
     def __init__(self):
         # since pandas is a dependency of pyarrow
         # we need to import on first use
-
         try:
             import pyarrow
             import pyarrow.parquet
         except ImportError:
-            raise ImportError("pyarrow is required for parquet support\n\n"
-                              "you can install via conda\n"
-                              "conda install pyarrow -c conda-forge\n"
-                              "\nor via pip\n"
-                              "pip install -U pyarrow\n")
-
-        if LooseVersion(pyarrow.__version__) < LooseVersion('0.4.1'):
-            raise ImportError("pyarrow >= 0.4.1 is required for parquet"
-                              "support\n\n"
-                              "you can install via conda\n"
-                              "conda install pyarrow -c conda-forge\n"
-                              "\nor via pip\n"
-                              "pip install -U pyarrow\n")
-
-        self._pyarrow_lt_050 = (LooseVersion(pyarrow.__version__) <
-                                LooseVersion('0.5.0'))
-        self._pyarrow_lt_060 = (LooseVersion(pyarrow.__version__) <
-                                LooseVersion('0.6.0'))
+            raise ImportError(
+                "pyarrow is required for parquet support\n\n"
+                "you can install via conda\n"
+                "conda install pyarrow -c conda-forge\n"
+                "\nor via pip\n"
+                "pip install -U pyarrow\n"
+            )
+        if LooseVersion(pyarrow.__version__) < '0.4.1':
+            raise ImportError(
+                "pyarrow >= 0.4.1 is required for parquet support\n\n"
+                "you can install via conda\n"
+                "conda install pyarrow -c conda-forge\n"
+                "\nor via pip\n"
+                "pip install -U pyarrow\n"
+            )
+        self._pyarrow_lt_070 = (
+            LooseVersion(pyarrow.__version__) < LooseVersion('0.7.0')
+        )
         self.api = pyarrow
 
     def write(self, df, path, compression='snappy',
               coerce_timestamps='ms', **kwargs):
+        self.validate_dataframe(df)
+        if self._pyarrow_lt_070:
+            self._validate_write_lt_070(
+                df, path, compression, coerce_timestamps, **kwargs
+            )
         path, _, _ = get_filepath_or_buffer(path)
-        if self._pyarrow_lt_060:
-            table = self.api.Table.from_pandas(df, timestamps_to_ms=True)
-            self.api.parquet.write_table(
-                table, path, compression=compression, **kwargs)
-
-        else:
-            table = self.api.Table.from_pandas(df)
-            self.api.parquet.write_table(
-                table, path, compression=compression,
-                coerce_timestamps=coerce_timestamps, **kwargs)
+        table = self.api.Table.from_pandas(df)
+        self.api.parquet.write_table(
+            table, path, compression=compression,
+            coerce_timestamps=coerce_timestamps, **kwargs)
 
     def read(self, path, columns=None, **kwargs):
         path, _, _ = get_filepath_or_buffer(path)
-        return self.api.parquet.read_table(path, columns=columns,
-                                           **kwargs).to_pandas()
-
-
-class FastParquetImpl(object):
+        parquet_file = self.api.parquet.ParquetFile(path)
+        if self._pyarrow_lt_070:
+            parquet_file.path = path
+            return self._read_lt_070(parquet_file, columns, **kwargs)
+        kwargs['use_pandas_metadata'] = True
+        return parquet_file.read(columns=columns, **kwargs).to_pandas()
+
+    def _validate_write_lt_070(self, df, path, compression='snappy',
+                               coerce_timestamps='ms', **kwargs):
+        # Compatibility shim for pyarrow < 0.7.0
+        # TODO: Remove in pandas 0.22.0
+        from pandas.core.indexes.multi import MultiIndex
+        if isinstance(df.index, MultiIndex):
+            msg = (
+                "Mulit-index DataFrames are only supported "
+                "with pyarrow >= 0.7.0"
+            )
+            raise ValueError(msg)
+        # Validate index
+        if not isinstance(df.index, Int64Index):
+            msg = (
+                "parquet does not support serializing {} for the index;"
+                "you can .reset_index() to make the index into column(s)"
+            )
+            raise ValueError(msg.format(type(df.index)))
+        if not df.index.equals(RangeIndex(len(df))):
+            raise ValueError(
+                "parquet does not support serializing a non-default index "
+                "for the index; you can .reset_index() to make the index "
+                "into column(s)"
+            )
+        if df.index.name is not None:
+            raise ValueError(
+                "parquet does not serialize index meta-data "
+                "on a default index"
+            )
+
+    def _read_lt_070(self, parquet_file, columns, **kwargs):
+        # Compatibility shim for pyarrow < 0.7.0
+        # TODO: Remove in pandas 0.22.0
+        from itertools import chain
+        import json
+        if columns is not None:
+            metadata = json.loads(parquet_file.metadata.metadata[b'pandas'])
+            columns = set(chain(columns, metadata['index_columns']))
+        kwargs['columns'] = columns
+        kwargs['path'] = parquet_file.path
+        return self.api.parquet.read_table(**kwargs).to_pandas()
+
+
+class FastParquetImpl(BaseImpl):
 
     def __init__(self):
         # since pandas is a dependency of fastparquet
         # we need to import on first use
-
         try:
             import fastparquet
         except ImportError:
-            raise ImportError("fastparquet is required for parquet support\n\n"
-                              "you can install via conda\n"
-                              "conda install fastparquet -c conda-forge\n"
-                              "\nor via pip\n"
-                              "pip install -U fastparquet")
-
-        if LooseVersion(fastparquet.__version__) < LooseVersion('0.1.0'):
-            raise ImportError("fastparquet >= 0.1.0 is required for parquet "
-                              "support\n\n"
-                              "you can install via conda\n"
-                              "conda install fastparquet -c conda-forge\n"
-                              "\nor via pip\n"
-                              "pip install -U fastparquet")
-
+            raise ImportError(
+                "fastparquet is required for parquet support\n\n"
+                "you can install via conda\n"
+                "conda install fastparquet -c conda-forge\n"
+                "\nor via pip\n"
+                "pip install -U fastparquet"
+            )
+        if LooseVersion(fastparquet.__version__) < '0.1.0':
+            raise ImportError(
+                "fastparquet >= 0.1.0 is required for parquet "
+                "support\n\n"
+                "you can install via conda\n"
+                "conda install fastparquet -c conda-forge\n"
+                "\nor via pip\n"
+                "pip install -U fastparquet"
+            )
         self.api = fastparquet
 
     def write(self, df, path, compression='snappy', **kwargs):
+        self.validate_dataframe(df)
         # thriftpy/protocol/compact.py:339:
         # DeprecationWarning: tostring() is deprecated.
         # Use tobytes() instead.
@@ -120,7 +194,8 @@ def write(self, df, path, compression='snappy', **kwargs):
 
     def read(self, path, columns=None, **kwargs):
         path, _, _ = get_filepath_or_buffer(path)
-        return self.api.ParquetFile(path).to_pandas(columns=columns, **kwargs)
+        parquet_file = self.api.ParquetFile(path)
+        return parquet_file.to_pandas(columns=columns, **kwargs)
 
 
 def to_parquet(df, path, engine='auto', compression='snappy', **kwargs):
@@ -141,43 +216,7 @@ def to_parquet(df, path, engine='auto', compression='snappy', **kwargs):
     kwargs
         Additional keyword arguments passed to the engine
     """
-
     impl = get_engine(engine)
-
-    if not isinstance(df, DataFrame):
-        raise ValueError("to_parquet only support IO with DataFrames")
-
-    valid_types = {'string', 'unicode'}
-
-    # validate index
-    # --------------
-
-    # validate that we have only a default index
-    # raise on anything else as we don't serialize the index
-
-    if not isinstance(df.index, Int64Index):
-        raise ValueError("parquet does not support serializing {} "
-                         "for the index; you can .reset_index()"
-                         "to make the index into column(s)".format(
-                             type(df.index)))
-
-    if not df.index.equals(RangeIndex.from_range(range(len(df)))):
-        raise ValueError("parquet does not support serializing a "
-                         "non-default index for the index; you "
-                         "can .reset_index() to make the index "
-                         "into column(s)")
-
-    if df.index.name is not None:
-        raise ValueError("parquet does not serialize index meta-data on a "
-                         "default index")
-
-    # validate columns
-    # ----------------
-
-    # must have value column names (strings only)
-    if df.columns.inferred_type not in valid_types:
-        raise ValueError("parquet must have string column names")
-
     return impl.write(df, path, compression=compression, **kwargs)
 
 
diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py
index 8e6f8998f5eeb..b06398412900b 100644
--- a/pandas/tests/io/test_parquet.py
+++ b/pandas/tests/io/test_parquet.py
@@ -6,6 +6,7 @@
 from warnings import catch_warnings
 
 import numpy as np
+from numpy.random import randn
 import pandas as pd
 from pandas.compat import PY3, is_platform_windows
 from pandas.io.parquet import (to_parquet, read_parquet, get_engine,
@@ -43,23 +44,7 @@ def engine(request):
 def pa():
     if not _HAVE_PYARROW:
         pytest.skip("pyarrow is not installed")
-    return 'pyarrow'
-
-
-@pytest.fixture
-def pa_lt_070():
-    if not _HAVE_PYARROW:
-        pytest.skip("pyarrow is not installed")
-    if LooseVersion(pyarrow.__version__) >= LooseVersion('0.7.0'):
-        pytest.skip("pyarrow is >= 0.7.0")
-    return 'pyarrow'
-
-
-@pytest.fixture
-def pa_ge_070():
-    if not _HAVE_PYARROW:
-        pytest.skip("pyarrow is not installed")
-    if LooseVersion(pyarrow.__version__) < LooseVersion('0.7.0'):
+    if LooseVersion(pyarrow.__version__) < '0.7.0':
         pytest.skip("pyarrow is < 0.7.0")
     return 'pyarrow'
 
@@ -68,6 +53,8 @@ def pa_ge_070():
 def fp():
     if not _HAVE_FASTPARQUET:
         pytest.skip("fastparquet is not installed")
+    if LooseVersion(fastparquet.__version__) < '0.1.0':
+        pytest.skip("fastparquet is < 0.1.0")
     return 'fastparquet'
 
 
@@ -207,15 +194,14 @@ def test_cross_engine_fp_pa(df_cross_compat, pa, fp):
 class Base(object):
 
     def check_error_on_write(self, df, engine, exc):
-        # check that we are raising the exception
-        # on writing
-
+        # check that we are raising the exception on writing
         with pytest.raises(exc):
             with tm.ensure_clean() as path:
                 to_parquet(df, path, engine, compression=None)
 
     def check_round_trip(self, df, engine, expected=None,
-                         write_kwargs=None, read_kwargs=None):
+                         write_kwargs=None, read_kwargs=None,
+                         check_names=True):
         if write_kwargs is None:
             write_kwargs = {}
         if read_kwargs is None:
@@ -226,7 +212,7 @@ def check_round_trip(self, df, engine, expected=None,
 
             if expected is None:
                 expected = df
-            tm.assert_frame_equal(result, expected)
+            tm.assert_frame_equal(result, expected, check_names=check_names)
 
             # repeat
             to_parquet(df, path, engine, **write_kwargs)
@@ -234,7 +220,7 @@ def check_round_trip(self, df, engine, expected=None,
 
             if expected is None:
                 expected = df
-            tm.assert_frame_equal(result, expected)
+            tm.assert_frame_equal(result, expected, check_names=check_names)
 
 
 class TestBasic(Base):
@@ -273,33 +259,6 @@ def test_columns_dtypes_invalid(self, engine):
                       datetime.datetime(2011, 1, 1, 1, 1)]
         self.check_error_on_write(df, engine, ValueError)
 
-    def test_write_with_index(self, engine):
-
-        df = pd.DataFrame({'A': [1, 2, 3]})
-        self.check_round_trip(df, engine, write_kwargs={'compression': None})
-
-        # non-default index
-        for index in [[2, 3, 4],
-                      pd.date_range('20130101', periods=3),
-                      list('abc'),
-                      [1, 3, 4],
-                      pd.MultiIndex.from_tuples([('a', 1), ('a', 2),
-                                                 ('b', 1)]),
-                      ]:
-
-            df.index = index
-            self.check_error_on_write(df, engine, ValueError)
-
-        # index with meta-data
-        df.index = [0, 1, 2]
-        df.index.name = 'foo'
-        self.check_error_on_write(df, engine, ValueError)
-
-        # column multi-index
-        df.index = [0, 1, 2]
-        df.columns = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)]),
-        self.check_error_on_write(df, engine, ValueError)
-
     @pytest.mark.parametrize('compression', [None, 'gzip', 'snappy', 'brotli'])
     def test_compression(self, engine, compression):
 
@@ -323,6 +282,70 @@ def test_read_columns(self, engine):
                               write_kwargs={'compression': None},
                               read_kwargs={'columns': ['string']})
 
+    def test_write_with_index(self, engine):
+        check_names = engine != 'fastparquet'
+
+        df = pd.DataFrame({'A': [1, 2, 3]})
+        self.check_round_trip(df, engine, write_kwargs={'compression': None})
+
+        indexes = [
+            [2, 3, 4],
+            pd.date_range('20130101', periods=3),
+            list('abc'),
+            [1, 3, 4],
+        ]
+        # non-default index
+        for index in indexes:
+            df.index = index
+            self.check_round_trip(
+                df, engine,
+                write_kwargs={'compression': None},
+                check_names=check_names,
+            )
+        if engine != 'fastparquet':
+            # Not suppoprted in fastparquet as of 0.1.3
+            index = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)])
+            df.index = index
+            self.check_round_trip(
+                df, engine,
+                write_kwargs={'compression': None},
+            )
+        # index with meta-data
+        df.index = [0, 1, 2]
+        df.index.name = 'foo'
+        self.check_round_trip(
+            df, engine,
+            write_kwargs={'compression': None}
+        )
+
+        # column multi-index
+        df.index = [0, 1, 2]
+        df.columns = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)]),
+        self.check_error_on_write(df, engine, ValueError)
+
+    def test_multiindex_with_columns(self, engine):
+        if engine == 'fastparquet':
+            msg = "fastparquet doesn't support mulit-indexes as of 0.1.3"
+            pytest.xfail(msg)
+
+        dates = pd.date_range('01-Jan-2018', '01-Dec-2018', freq='MS')
+        df = pd.DataFrame(randn(2 * len(dates), 3), columns=list('ABC'))
+        index1 = pd.MultiIndex.from_product(
+            [['Level1', 'Level2'], dates],
+            names=['level', 'date']
+        )
+        index2 = index1.copy(names=None)
+        for index in [index1, index2]:
+            df.index = index
+            with tm.ensure_clean() as path:
+                df.to_parquet(path, engine)
+                result = read_parquet(path, engine)
+                expected = df
+                tm.assert_frame_equal(result, expected)
+                result = read_parquet(path, engine, columns=['A', 'B'])
+                expected = df[['A', 'B']]
+                tm.assert_frame_equal(result, expected)
+
 
 class TestParquetPyArrow(Base):
 
@@ -352,14 +375,12 @@ def test_basic_subset_columns(self, pa, df_full):
                               read_kwargs={'columns': ['string', 'int']})
 
     def test_duplicate_columns(self, pa):
-
         # not currently able to handle duplicate columns
         df = pd.DataFrame(np.arange(12).reshape(4, 3),
                           columns=list('aaa')).copy()
         self.check_error_on_write(df, pa, ValueError)
 
     def test_unsupported(self, pa):
-
         # period
         df = pd.DataFrame({'a': pd.period_range('2013', freq='M', periods=3)})
         self.check_error_on_write(df, pa, ValueError)
@@ -373,23 +394,13 @@ def test_unsupported(self, pa):
         df = pd.DataFrame({'a': ['a', 1, 2.0]})
         self.check_error_on_write(df, pa, ValueError)
 
-    def test_categorical(self, pa_ge_070):
-        pa = pa_ge_070
-
-        # supported in >= 0.7.0
+    def test_categorical(self, pa):
         df = pd.DataFrame({'a': pd.Categorical(list('abc'))})
 
         # de-serialized as object
         expected = df.assign(a=df.a.astype(object))
         self.check_round_trip(df, pa, expected)
 
-    def test_categorical_unsupported(self, pa_lt_070):
-        pa = pa_lt_070
-
-        # supported in >= 0.7.0
-        df = pd.DataFrame({'a': pd.Categorical(list('abc'))})
-        self.check_error_on_write(df, pa, NotImplementedError)
-
 
 class TestParquetFastParquet(Base):
 


Code review comments:
diff_hunk: @@ -4,4 +4,4 @@ source activate pandas
 
 echo "install 27"
 
-conda install -n pandas -c conda-forge feather-format pyarrow=0.4.1 fastparquet
+conda install -n pandas -c conda-forge feather-format pyarrow=0.7.0 fastparquet
path: ci/requirements-2.7.sh
text: @user1:
let's keep the old version here? (so we actually test the older version, now we dropped bumping the version in this PR)