Skip to content

Commit c81712c

Browse files
committed
API: remove the copy kw from .xs to prevent accidental / confusing setting (GH6894)
1 parent 95090fd commit c81712c

12 files changed

+129
-100
lines changed

doc/source/api.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ SQL
7878

7979
.. autosummary::
8080
:toctree: generated/
81-
81+
8282
read_sql_table
8383
read_sql_query
8484
read_sql

doc/source/release.rst

+4
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ API Changes
176176
- ``.quantile`` on a ``datetime[ns]`` series now returns ``Timestamp`` instead
177177
of ``np.datetime64`` objects (:issue:`6810`)
178178

179+
- Remove the ``copy`` keyword from ``DataFrame.xs``,``Panel.major_xs``,``Panel.minor_xs``. A view will be
180+
returned if possible, otherwise a copy will be made. Previously the user could think that ``copy=False`` would
181+
ALWAYS return a view. (:issue:`6894`)
182+
179183
Deprecations
180184
~~~~~~~~~~~~
181185

doc/source/v0.14.0.txt

+4
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ API changes
214214
(and numpy defaults)
215215
- add ``inplace`` keyword to ``Series.order/sort`` to make them inverses (:issue:`6859`)
216216

217+
- Remove the ``copy`` keyword from ``DataFrame.xs``,``Panel.major_xs``,``Panel.minor_xs``. A view will be
218+
returned if possible, otherwise a copy will be made. Previously the user could think that ``copy=False`` would
219+
ALWAYS return a view. (:issue:`6894`)
220+
217221
.. _whatsnew_0140.sql:
218222

219223
SQL

pandas/core/frame.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1564,7 +1564,7 @@ def irow(self, i, copy=False):
15641564
def icol(self, i):
15651565
return self._ixs(i, axis=1)
15661566

1567-
def _ixs(self, i, axis=0, copy=False):
1567+
def _ixs(self, i, axis=0):
15681568
"""
15691569
i : int, slice, or sequence of integers
15701570
axis : int
@@ -1588,7 +1588,10 @@ def _ixs(self, i, axis=0, copy=False):
15881588
result = self.take(i, axis=axis)
15891589
copy=True
15901590
else:
1591-
new_values, copy = self._data.fast_xs(i, copy=copy)
1591+
new_values = self._data.fast_xs(i)
1592+
1593+
# if we are a copy, mark as such
1594+
copy = isinstance(new_values,np.ndarray) and new_values.base is None
15921595
result = Series(new_values, index=self.columns,
15931596
name=self.index[i], dtype=new_values.dtype)
15941597
result._set_is_copy(self, copy=copy)

pandas/core/generic.py

+15-18
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@ def take(self, indices, axis=0, convert=True, is_copy=True):
12401240

12411241
return result
12421242

1243-
def xs(self, key, axis=0, level=None, copy=True, drop_level=True):
1243+
def xs(self, key, axis=0, level=None, copy=None, drop_level=True):
12441244
"""
12451245
Returns a cross-section (row(s) or column(s)) from the Series/DataFrame.
12461246
Defaults to cross-section on the rows (axis=0).
@@ -1254,7 +1254,7 @@ def xs(self, key, axis=0, level=None, copy=True, drop_level=True):
12541254
level : object, defaults to first n levels (n=1 or len(key))
12551255
In case of a key partially contained in a MultiIndex, indicate
12561256
which levels are used. Levels can be referred by label or position.
1257-
copy : boolean, default True
1257+
copy : boolean [deprecated]
12581258
Whether to make a copy of the data
12591259
drop_level : boolean, default True
12601260
If False, returns object with same levels as self.
@@ -1276,14 +1276,6 @@ def xs(self, key, axis=0, level=None, copy=True, drop_level=True):
12761276
b 9
12771277
c 3
12781278
Name: C
1279-
>>> s = df.xs('a', copy=False)
1280-
>>> s['A'] = 100
1281-
>>> df
1282-
A B C
1283-
a 100 5 2
1284-
b 4 0 9
1285-
c 9 7 3
1286-
12871279
12881280
>>> df
12891281
A B C D
@@ -1310,16 +1302,24 @@ def xs(self, key, axis=0, level=None, copy=True, drop_level=True):
13101302
-------
13111303
xs : Series or DataFrame
13121304
1305+
Notes
1306+
-----
1307+
xs is only for getting, not setting values.
1308+
1309+
MultiIndex Slicers is a generic way to get/set values on any level or levels
1310+
it is a superset of xs functionality, see :ref:`MultiIndex Slicers <indexing.mi_slicers>`
1311+
13131312
"""
1313+
if copy is not None:
1314+
warnings.warn("copy keyword is deprecated, "
1315+
"default is to return a copy or a view if possible")
1316+
13141317
axis = self._get_axis_number(axis)
13151318
labels = self._get_axis(axis)
13161319
if level is not None:
13171320
loc, new_ax = labels.get_loc_level(key, level=level,
13181321
drop_level=drop_level)
13191322

1320-
if not copy and not isinstance(loc, slice):
1321-
raise ValueError('Cannot retrieve view (copy=False)')
1322-
13231323
# convert to a label indexer if needed
13241324
if isinstance(loc, slice):
13251325
lev_num = labels._get_level_number(level)
@@ -1336,10 +1336,7 @@ def xs(self, key, axis=0, level=None, copy=True, drop_level=True):
13361336
return result
13371337

13381338
if axis == 1:
1339-
data = self[key]
1340-
if copy:
1341-
data = data.copy()
1342-
return data
1339+
return self[key]
13431340

13441341
self._consolidate_inplace()
13451342

@@ -1362,7 +1359,7 @@ def xs(self, key, axis=0, level=None, copy=True, drop_level=True):
13621359

13631360
if np.isscalar(loc):
13641361
from pandas import Series
1365-
new_values, copy = self._data.fast_xs(loc, copy=copy)
1362+
new_values = self._data.fast_xs(loc)
13661363

13671364
# may need to box a datelike-scalar
13681365
#

pandas/core/indexing.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,14 @@ def _get_label(self, label, axis=0):
7676
# but will fail when the index is not present
7777
# see GH5667
7878
try:
79-
return self.obj._xs(label, axis=axis, copy=False)
79+
return self.obj._xs(label, axis=axis)
8080
except:
8181
return self.obj[label]
8282
elif (isinstance(label, tuple) and
8383
isinstance(label[axis], slice)):
8484
raise IndexingError('no slices here, handle elsewhere')
8585

86-
try:
87-
return self.obj._xs(label, axis=axis, copy=False)
88-
except Exception:
89-
return self.obj._xs(label, axis=axis, copy=True)
86+
return self.obj._xs(label, axis=axis)
9087

9188
def _get_loc(self, key, axis=0):
9289
return self.obj._ixs(key, axis=axis)

pandas/core/internals.py

+10-14
Original file line numberDiff line numberDiff line change
@@ -2861,9 +2861,7 @@ def xs(self, key, axis=1, copy=True, takeable=False):
28612861

28622862
new_blocks = []
28632863
if len(self.blocks) > 1:
2864-
if not copy:
2865-
raise Exception('cannot get view of mixed-type or '
2866-
'non-consolidated DataFrame')
2864+
# we must copy here as we are mixed type
28672865
for blk in self.blocks:
28682866
newb = make_block(blk.values[slicer],
28692867
blk.items,
@@ -2884,18 +2882,16 @@ def xs(self, key, axis=1, copy=True, takeable=False):
28842882

28852883
return self.__class__(new_blocks, new_axes)
28862884

2887-
def fast_xs(self, loc, copy=False):
2885+
def fast_xs(self, loc):
28882886
"""
28892887
get a cross sectional for a given location in the
28902888
items ; handle dups
28912889
2892-
return the result and a flag if a copy was actually made
2890+
return the result, is *could* be a view in the case of a
2891+
single block
28932892
"""
28942893
if len(self.blocks) == 1:
2895-
result = self.blocks[0].values[:, loc]
2896-
if copy:
2897-
result = result.copy()
2898-
return result, copy
2894+
return self.blocks[0].values[:, loc]
28992895

29002896
items = self.items
29012897

@@ -2904,7 +2900,7 @@ def fast_xs(self, loc, copy=False):
29042900
result = self._interleave(items)
29052901
if self.ndim == 2:
29062902
result = result.T
2907-
return result[loc], True
2903+
return result[loc]
29082904

29092905
# unique
29102906
dtype = _interleaved_dtype(self.blocks)
@@ -2915,7 +2911,7 @@ def fast_xs(self, loc, copy=False):
29152911
i = items.get_loc(item)
29162912
result[i] = blk._try_coerce_result(blk.iget((j, loc)))
29172913

2918-
return result, True
2914+
return result
29192915

29202916
def consolidate(self):
29212917
"""
@@ -3829,12 +3825,12 @@ def _consolidate_check(self):
38293825
def _consolidate_inplace(self):
38303826
pass
38313827

3832-
def fast_xs(self, loc, copy=False):
3828+
def fast_xs(self, loc):
38333829
"""
38343830
fast path for getting a cross-section
3831+
return a view of the data
38353832
"""
3836-
result = self._block.values[loc]
3837-
return result, False
3833+
return self._block.values[loc]
38383834

38393835
def construction_error(tot_items, block_shape, axes, e=None):
38403836
""" raise a helpful message about our construction """

pandas/core/panel.py

+53-17
Original file line numberDiff line numberDiff line change
@@ -688,43 +688,67 @@ def _combine_panel(self, other, func):
688688

689689
return self._constructor(result_values, items, major, minor)
690690

691-
def major_xs(self, key, copy=True):
691+
def major_xs(self, key, copy=None):
692692
"""
693693
Return slice of panel along major axis
694694
695695
Parameters
696696
----------
697697
key : object
698698
Major axis label
699-
copy : boolean, default True
700-
Copy data
699+
copy : boolean [deprecated]
700+
Whether to make a copy of the data
701701
702702
Returns
703703
-------
704704
y : DataFrame
705705
index -> minor axis, columns -> items
706+
707+
Notes
708+
-----
709+
major_xs is only for getting, not setting values.
710+
711+
MultiIndex Slicers is a generic way to get/set values on any level or levels
712+
it is a superset of major_xs functionality, see :ref:`MultiIndex Slicers <indexing.mi_slicers>`
713+
706714
"""
707-
return self.xs(key, axis=self._AXIS_LEN - 2, copy=copy)
715+
if copy is not None:
716+
warnings.warn("copy keyword is deprecated, "
717+
"default is to return a copy or a view if possible")
708718

709-
def minor_xs(self, key, copy=True):
719+
return self.xs(key, axis=self._AXIS_LEN - 2)
720+
721+
def minor_xs(self, key, copy=None):
710722
"""
711723
Return slice of panel along minor axis
712724
713725
Parameters
714726
----------
715727
key : object
716728
Minor axis label
717-
copy : boolean, default True
718-
Copy data
729+
copy : boolean [deprecated]
730+
Whether to make a copy of the data
719731
720732
Returns
721733
-------
722734
y : DataFrame
723735
index -> major axis, columns -> items
736+
737+
Notes
738+
-----
739+
minor_xs is only for getting, not setting values.
740+
741+
MultiIndex Slicers is a generic way to get/set values on any level or levels
742+
it is a superset of minor_xs functionality, see :ref:`MultiIndex Slicers <indexing.mi_slicers>`
743+
724744
"""
725-
return self.xs(key, axis=self._AXIS_LEN - 1, copy=copy)
745+
if copy is not None:
746+
warnings.warn("copy keyword is deprecated, "
747+
"default is to return a copy or a view if possible")
748+
749+
return self.xs(key, axis=self._AXIS_LEN - 1)
726750

727-
def xs(self, key, axis=1, copy=True):
751+
def xs(self, key, axis=1, copy=None):
728752
"""
729753
Return slice of panel along selected axis
730754
@@ -733,24 +757,36 @@ def xs(self, key, axis=1, copy=True):
733757
key : object
734758
Label
735759
axis : {'items', 'major', 'minor}, default 1/'major'
736-
copy : boolean, default True
737-
Copy data
760+
copy : boolean [deprecated]
761+
Whether to make a copy of the data
738762
739763
Returns
740764
-------
741765
y : ndim(self)-1
766+
767+
Notes
768+
-----
769+
xs is only for getting, not setting values.
770+
771+
MultiIndex Slicers is a generic way to get/set values on any level or levels
772+
it is a superset of xs functionality, see :ref:`MultiIndex Slicers <indexing.mi_slicers>`
773+
742774
"""
775+
if copy is not None:
776+
warnings.warn("copy keyword is deprecated, "
777+
"default is to return a copy or a view if possible")
778+
743779
axis = self._get_axis_number(axis)
744780
if axis == 0:
745-
data = self[key]
746-
if copy:
747-
data = data.copy()
748-
return data
781+
return self[key]
749782

750783
self._consolidate_inplace()
751784
axis_number = self._get_axis_number(axis)
752-
new_data = self._data.xs(key, axis=axis_number, copy=copy)
753-
return self._construct_return_type(new_data)
785+
new_data = self._data.xs(key, axis=axis_number, copy=False)
786+
result = self._construct_return_type(new_data)
787+
copy = new_data.is_mixed_type
788+
result._set_is_copy(self, copy=copy)
789+
return result
754790

755791
_xs = xs
756792

pandas/tests/test_frame.py

+8-21
Original file line numberDiff line numberDiff line change
@@ -8371,12 +8371,8 @@ def test_xs(self):
83718371
expected = self.frame['A']
83728372
assert_series_equal(series, expected)
83738373

8374-
# no view by default
8375-
series[:] = 5
8376-
self.assert_((expected != 5).all())
8377-
8378-
# view
8379-
series = self.frame.xs('A', axis=1, copy=False)
8374+
# view is returned if possible
8375+
series = self.frame.xs('A', axis=1)
83808376
series[:] = 5
83818377
self.assert_((expected == 5).all())
83828378

@@ -11888,25 +11884,16 @@ def test_boolean_set_uncons(self):
1188811884
assert_almost_equal(expected, self.frame.values)
1188911885

1189011886
def test_xs_view(self):
11887+
"""
11888+
in 0.14 this will return a view if possible
11889+
a copy otherwise, but this is numpy dependent
11890+
"""
11891+
1189111892
dm = DataFrame(np.arange(20.).reshape(4, 5),
1189211893
index=lrange(4), columns=lrange(5))
1189311894

11894-
dm.xs(2, copy=False)[:] = 5
11895-
self.assert_((dm.xs(2) == 5).all())
11896-
1189711895
dm.xs(2)[:] = 10
11898-
self.assert_((dm.xs(2) == 5).all())
11899-
11900-
# prior to chained assignment (GH5390)
11901-
# this would raise, but now just returns a copy (and sets is_copy)
11902-
# TODO (?): deal with mixed-type fiasco?
11903-
# with assertRaisesRegexp(TypeError, 'cannot get view of mixed-type'):
11904-
# self.mixed_frame.xs(self.mixed_frame.index[2], copy=False)
11905-
11906-
# unconsolidated
11907-
dm['foo'] = 6.
11908-
dm.xs(3, copy=False)[:] = 10
11909-
self.assert_((dm.xs(3) == 10).all())
11896+
self.assert_((dm.xs(2) == 10).all())
1191011897

1191111898
def test_boolean_indexing(self):
1191211899
idx = lrange(3)

0 commit comments

Comments
 (0)