Repository: pydata/xarray
Issue: TypeError with to_dataframe and 0d string/object coordinates

```
xray.Dataset({'x': pd.Index(['bar'])}).isel(x=0).to_dataframe()
```

results in:

```
TypeError                                 Traceback (most recent call last)
<ipython-input-261-6b1470f49b2d> in <module>()
      1 obj = xray.Dataset({'x': pd.Index(['bar'])}).isel(x=0)
----> 2 obj.to_dataframe()

/Users/shoyer/dev/xray/xray/core/dataset.pyc in to_dataframe(self)
   1520         this dataset's indices.
   1521         """
-> 1522         return self._to_dataframe(self.dims)
   1523 
   1524     @classmethod

/Users/shoyer/dev/xray/xray/core/dataset.pyc in _to_dataframe(self, ordered_dims)
   1507         columns = [k for k in self if k not in self.dims]
   1508         orig_arrays = (self._arrays[k] for k in columns)
-> 1509         broadcast_arrays = variable.broadcast_variables(*orig_arrays)
   1510         data = [arr.transpose(*ordered_dims).values.reshape(-1)
   1511                 for arr in broadcast_arrays]

/Users/shoyer/dev/xray/xray/core/variable.pyc in broadcast_variables(*variables)
    891     and broadcast data.
    892     """
--> 893     variables = _broadcast_compatible_variables(*variables)
    894     broadcast_data = np.broadcast_arrays(*[v.values for v in variables])
    895     for var, data in zip(variables, broadcast_data):

/Users/shoyer/dev/xray/xray/core/variable.pyc in _broadcast_compatible_variables(*variables)
    875     broadcast_vars = []
    876     for var in variables:
--> 877         data = var.values[(Ellipsis,) + (None,) * (len(all_dims) - var.ndim)]
    878         var_dims = set(var.dims)
    879         dims = var.dims + tuple(d for d in all_dims if d not in var_dims)

TypeError: string indices must be integers, not tuple
```


Changed files:
xray/test/test_dataset.py, xray/test/test_utils.py, xray/core/variable.py, xray/test/test_variable.py, xray/core/utils.py, xray/core/dataset.py

diff --git a/xray/core/dataset.py b/xray/core/dataset.py
index d5bfb003a..441928047 100644
--- a/xray/core/dataset.py
+++ b/xray/core/dataset.py
@@ -1504,10 +1504,8 @@ class Dataset(Mapping, common.ImplementsDatasetReduce):
 
     def _to_dataframe(self, ordered_dims):
         columns = [k for k in self if k not in self.dims]
-        orig_arrays = (self._arrays[k] for k in columns)
-        broadcast_arrays = variable.broadcast_variables(*orig_arrays)
-        data = [arr.transpose(*ordered_dims).values.reshape(-1)
-                for arr in broadcast_arrays]
+        data = [self._arrays[k].set_dims(ordered_dims).values.reshape(-1)
+                for k in columns]
         index = self.coords.to_index(ordered_dims)
         return pd.DataFrame(OrderedDict(zip(columns, data)), index=index)
 
diff --git a/xray/core/utils.py b/xray/core/utils.py
index 2305d3f0c..1b5ba5848 100644
--- a/xray/core/utils.py
+++ b/xray/core/utils.py
@@ -70,6 +70,28 @@ def array_equiv(arr1, arr2):
     return ((arr1 == arr2) | (isnull(arr1) & isnull(arr2))).all()
 
 
+def as_shape(array, shape):
+    """Expand a numpy.ndarray to a new shape according to broadcasting rules
+    """
+    array = np.asarray(array)
+    if len(shape) < array.ndim:
+        raise ValueError('new shape must be greater than or equal to the '
+                         'number of array dimensions')
+
+    # based on numpy.lib.stride_tricks.broadcast_arrays
+    strides = [0] * (len(shape) - array.ndim) + list(array.strides)
+    for n, (new_size, old_size) in enumerate(zip(shape[-array.ndim:],
+                                                 array.shape)):
+        if new_size != old_size:
+            axis = array.ndim + n
+            if old_size != 1:
+                raise ValueError('shape mismatch: new shape differs from the '
+                                 'original shape on axis %r, but the original '
+                                 'shape is not 1 on that axis' % axis)
+            strides[axis] = 0
+    return np.lib.stride_tricks.as_strided(array, shape=shape, strides=strides)
+
+
 def safe_cast_to_index(array):
     """Given an array, safely cast it to a pandas.Index.
 
diff --git a/xray/core/variable.py b/xray/core/variable.py
index 329b22e23..0fbf040f3 100644
--- a/xray/core/variable.py
+++ b/xray/core/variable.py
@@ -537,6 +537,40 @@ class Variable(common.AbstractArray):
         dims = dict(zip(self.dims, self.shape))
         return common.squeeze(self, dims, dim)
 
+    def set_dims(self, dims):
+        """Return a new variable with expanded dimensions.
+
+        When possible, this operation does not copy this variable's data.
+
+        Parameters
+        ----------
+        dims : str or sequence of str or dict
+            Dimensions to include on the new variable. If a dict, values are
+            used to provide the sizes of new dimensions; otherwise, new
+            dimensions are inserted with length 1.
+
+        Returns
+        -------
+        Variable
+        """
+        if isinstance(dims, basestring):
+            dims = [dims]
+
+        if not utils.is_dict_like(dims):
+            dims_map = dict(zip(self.dims, self.shape))
+            dims = OrderedDict((d, dims_map.get(d, 1)) for d in dims)
+
+        missing_dims = set(self.dims) - set(dims)
+        if missing_dims:
+            raise ValueError('new dimensions must be a superset of existing '
+                             'dimensions')
+
+        self_dims = set(self.dims)
+        exp_dims = tuple(d for d in dims if d not in self_dims) + self.dims
+        exp_data = utils.as_shape(self, [dims[d] for d in exp_dims])
+        expanded_var = Variable(exp_dims, exp_data, self.attrs, self.encoding)
+        return expanded_var.transpose(*dims)
+
     def reduce(self, func, dim=None, axis=None, keep_attrs=False,
                **kwargs):
         """Reduce this array by applying `func` along some dimension(s).
@@ -871,15 +905,7 @@ def _broadcast_compatible_variables(*variables):
                 raise ValueError('operands cannot be broadcast together '
                                  'with mismatched lengths for dimension %r: %s'
                                  % (d, (all_dims[d], s)))
-
-    broadcast_vars = []
-    for var in variables:
-        data = var.values[(Ellipsis,) + (None,) * (len(all_dims) - var.ndim)]
-        var_dims = set(var.dims)
-        dims = var.dims + tuple(d for d in all_dims if d not in var_dims)
-        expanded_var = Variable(dims, data, var.attrs, var.encoding)
-        broadcast_vars.append(expanded_var.transpose(*all_dims))
-    return tuple(broadcast_vars)
+    return tuple(var.set_dims(all_dims) for var in variables)
 
 
 def _set_data_directly(var, values):
diff --git a/xray/test/test_dataset.py b/xray/test/test_dataset.py
index b70f5d6a1..cb2abf93d 100644
--- a/xray/test/test_dataset.py
+++ b/xray/test/test_dataset.py
@@ -1010,6 +1010,20 @@ class TestDataset(TestCase):
         expected = Dataset()
         self.assertDatasetIdentical(expected, actual)
 
+        # regression test for GH278
+        ds = Dataset({'x': pd.Index(['bar']), 'a': ('y', [1])}).isel(x=0)
+        # use .loc to ensure consistent results on Python 3
+        actual = ds.to_dataframe().loc[:, ['a', 'x']]
+        expected = pd.DataFrame([[1, 'bar']], index=pd.Index([0], name='y'),
+                                columns=['a', 'x'])
+        assert expected.equals(actual), (expected, actual)
+
+        ds = Dataset({'x': [0], 'y': [1]})
+        actual = ds.to_dataframe()
+        idx = pd.MultiIndex.from_arrays([[0], [1]], names=['x', 'y'])
+        expected = pd.DataFrame([[]], index=idx)
+        assert expected.equals(actual), (expected, actual)
+
     def test_pickle(self):
         data = create_test_data()
         roundtripped = pickle.loads(pickle.dumps(data))
diff --git a/xray/test/test_utils.py b/xray/test/test_utils.py
index 063b75c5f..d64cb582e 100644
--- a/xray/test/test_utils.py
+++ b/xray/test/test_utils.py
@@ -32,6 +32,28 @@ class TestArrayEquiv(TestCase):
             utils.array_equiv(0, np.array(1, dtype=object)))
 
 
+class TestAsShape(TestCase):
+    def test_expand(self):
+        for array, shape in [
+                (np.arange(3), (3,)),
+                (np.arange(3), (1, 3,)),
+                (np.arange(3), (2, 3,)),
+                (np.arange(3), (1, 2, 3,)),
+                (1, (3, 2, 1,)),
+                (np.empty((3, 4), order='C'), (2, 3, 4)),
+                (np.empty((3, 4), order='F'), (2, 3, 4)),
+                ]:
+            _, expected = np.broadcast_arrays(np.empty(shape), array)
+            actual = utils.as_shape(array, shape)
+            self.assertArrayEqual(expected, actual)
+
+    def test_errors(self):
+        with self.assertRaisesRegexp(ValueError, 'shape must be greater'):
+            utils.as_shape(np.arange(3), ())
+        with self.assertRaisesRegexp(ValueError, 'shape mismatch'):
+            utils.as_shape(np.arange(3), (2,))
+
+
 class TestDictionaries(TestCase):
     def setUp(self):
         self.x = {'a': 'A', 'b': 'B'}
diff --git a/xray/test/test_variable.py b/xray/test/test_variable.py
index 96ab9770c..e8ddb57f9 100644
--- a/xray/test/test_variable.py
+++ b/xray/test/test_variable.py
@@ -508,6 +508,27 @@ class TestVariable(TestCase, VariableSubclassTestCases):
         with self.assertRaisesRegexp(ValueError, 'not found in array dim'):
             v.get_axis_num('foobar')
 
+    def test_set_dims(self):
+        v = Variable(['x'], [0, 1])
+        actual = v.set_dims(['x', 'y'])
+        expected = Variable(['x', 'y'], [[0], [1]])
+        self.assertVariableIdentical(actual, expected)
+
+        actual = v.set_dims(['y', 'x'])
+        self.assertVariableIdentical(actual, expected.T)
+
+        actual = v.set_dims(OrderedDict([('x', 2), ('y', 2)]))
+        expected = Variable(['x', 'y'], [[0, 0], [1, 1]])
+        self.assertVariableIdentical(actual, expected)
+
+        v = Variable(['foo'], [0, 1])
+        actual = v.set_dims('foo')
+        expected = v
+        self.assertVariableIdentical(actual, expected)
+
+        with self.assertRaisesRegexp(ValueError, 'must be a superset'):
+            v.set_dims(['z'])
+
     def test_broadcasting_math(self):
         x = np.random.randn(2, 3)
         v = Variable(['a', 'b'], x)