diff --git a/doc/whats-new.rst b/doc/whats-new.rst index a1b3e5416ca..e9a7e010d51 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -27,6 +27,11 @@ Enhancements Bug fixes ~~~~~~~~~ +- Improved error handling and documentation for `.expand_dims()` + read-only view. + +- Add a DataArray.broadcast_like() method. + .. _whats-new.0.12.3: v0.12.3 (10 July 2019) diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index beaf148df6b..e4f2734df1f 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -15,7 +15,7 @@ utils) from .accessor_dt import DatetimeAccessor from .accessor_str import StringAccessor -from .alignment import align, reindex_like_indexers +from .alignment import align, broadcast, reindex_like_indexers from .common import AbstractArray, DataWithCoords from .coordinates import ( DataArrayCoordinates, LevelCoordinatesSource, assert_coordinate_consistent, @@ -1156,6 +1156,57 @@ def interp(self, coords: Optional[Mapping[Hashable, Any]] = None, **coords_kwargs) return self._from_temp_dataset(ds) + def broadcast_like(self, + other: Union['DataArray', Dataset]) -> 'DataArray': + """Broadcast a DataArray to the shape of another DataArray or Dataset + + xarray objects are broadcast against each other in arithmetic + operations, so this method is not be necessary for most uses. + + If no change is needed, the input data is returned to the output + without being copied. + + If new coords are added by the broadcast, their values are + NaN filled. + + Parameters + ---------- + other : Dataset or DataArray + + Returns + ------- + new_da: xr.DataArray + + Examples + -------- + + >>> arr1 + + array([[0.824093, 0.769792, 0.571621], + [0.310378, 0.480418, 0.062015]]) + Coordinates: + * x (x) >> arr2 + + array([[0.852992, 0.106589], + [0.087549, 0.563304], + [0.675744, 0.285752]]) + Coordinates: + * x (x) >> arr1.broadcast_like(arr2) + + array([[0.852992, 0.106589, nan], + [0.087549, 0.563304, nan], + [0.675744, 0.285752, nan]]) + Coordinates: + * x (x) object 'a' 'b' 'c' + * y (y) object 'a' 'b' 'c' + + """ + return broadcast(self, other)[1] + def interp_like(self, other: Union['DataArray', Dataset], method: str = 'linear', assume_sorted: bool = False, kwargs: Optional[Mapping[str, Any]] = None) -> 'DataArray': @@ -1272,7 +1323,9 @@ def expand_dims(self, dim: Union[None, Hashable, Sequence[Hashable], Mapping[Hashable, Any]] = None, axis=None, **dim_kwargs: Any) -> 'DataArray': """Return a new object with an additional axis (or axes) inserted at - the corresponding position in the array shape. + the corresponding position in the array shape. The new object is a + view into the underlying array, not a copy. + If dim is already a scalar coordinate, it will be promoted to a 1D coordinate consisting of a single value. diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 060c80b6722..71197026aa2 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -2516,7 +2516,8 @@ def swap_dims(self, dims_dict, inplace=None): def expand_dims(self, dim=None, axis=None, **dim_kwargs): """Return a new object with an additional axis (or axes) inserted at - the corresponding position in the array shape. + the corresponding position in the array shape. The new object is a + view into the underlying array, not a copy. If dim is already a scalar coordinate, it will be promoted to a 1D coordinate consisting of a single value. diff --git a/xarray/core/indexing.py b/xarray/core/indexing.py index 02953b74fa4..e262d9ee24b 100644 --- a/xarray/core/indexing.py +++ b/xarray/core/indexing.py @@ -1177,7 +1177,15 @@ def __getitem__(self, key): def __setitem__(self, key, value): array, key = self._indexing_array_and_key(key) - array[key] = value + try: + array[key] = value + except ValueError: + # More informative exception if read-only view + if not array.flags.writeable and not array.flags.owndata: + raise ValueError("Assignment destination is a view. " + "Do you want to .copy() array first?") + else: + raise class DaskIndexingAdapter(ExplicitlyIndexedNDArrayMixin): diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index 30385779e5f..b6ec889f5f8 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -438,6 +438,15 @@ def test_broadcast_equals(self): assert not a.broadcast_equals(c) assert not c.broadcast_equals(a) + def test_broadcast_like(self): + arr1 = DataArray(np.ones((2, 3)), dims=['x', 'y'], + coords={'x': ['a', 'b'], 'y': ['a', 'b', 'c']}) + arr2 = DataArray(np.ones((3, 2)), dims=['x', 'y'], + coords={'x': ['a', 'b', 'c'], 'y': ['a', 'b']}) + broad1 = xr.broadcast(arr1, arr2)[1] + broad2 = arr1.broadcast_like(arr2) + assert broad1.identical(broad2) + def test_getitem(self): # strings pull out dataarrays assert_identical(self.dv, self.ds['foo']) diff --git a/xarray/tests/test_indexing.py b/xarray/tests/test_indexing.py index c044d2ed1f3..64eee80d4eb 100644 --- a/xarray/tests/test_indexing.py +++ b/xarray/tests/test_indexing.py @@ -144,6 +144,16 @@ def test_indexer(data, x, expected_pos, expected_idx=None): [True, True, True, True, False, False, False, False], pd.MultiIndex.from_product([[1, 2], [-1, -2]])) + def test_read_only_view(self): + from collections import OrderedDict + arr = DataArray(np.random.rand(3, 3), + coords={'x': np.arange(3), 'y': np.arange(3)}, + dims=('x', 'y')) # Create a 2D DataArray + arr = arr.expand_dims(OrderedDict([('z', 3)]), -1) # New dimension 'z' + arr['z'] = np.arange(3) # New coords to dimension 'z' + with pytest.raises(ValueError, match='Do you want to .copy()'): + arr.loc[0, 0, 0] = 999 + class TestLazyArray: def test_slice_slice(self):