Skip to content

Commit f400014

Browse files
committed
Merge pull request #7950 from jreback/setting_with_copy
COMPAT: raise SettingWithCopy in even more situations when a view is at hand
2 parents 123a555 + 70a17da commit f400014

14 files changed

+237
-151
lines changed

doc/source/indexing.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1481,7 +1481,8 @@ which can take the values ``['raise','warn',None]``, where showing a warning is
14811481
'three', 'two', 'one', 'six'],
14821482
'c' : np.arange(7)})
14831483
1484-
# passed via reference (will stay)
1484+
# This will show the SettingWithCopyWarning
1485+
# but the frame values will be set
14851486
dfb['c'][dfb.a.str.startswith('o')] = 42
14861487
14871488
This however is operating on a copy and will not work.

doc/source/v0.15.0.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ API changes
114114
df
115115
df.dtypes
116116

117-
- ``SettingWithCopy`` raise/warnings (according to the option ``mode.chained_assignment``) will now be issued when setting a value on a sliced mixed-dtype DataFrame using chained-assignment. (:issue:`7845`)
117+
- ``SettingWithCopy`` raise/warnings (according to the option ``mode.chained_assignment``) will now be issued when setting a value on a sliced mixed-dtype DataFrame using chained-assignment. (:issue:`7845`, :issue:`7950`)
118118

119119
.. code-block:: python
120120

pandas/core/generic.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,8 +1088,14 @@ def _maybe_cache_changed(self, item, value):
10881088
@property
10891089
def _is_cached(self):
10901090
""" boolean : return if I am cached """
1091+
return getattr(self, '_cacher', None) is not None
1092+
1093+
def _get_cacher(self):
1094+
""" return my cacher or None """
10911095
cacher = getattr(self, '_cacher', None)
1092-
return cacher is not None
1096+
if cacher is not None:
1097+
cacher = cacher[1]()
1098+
return cacher
10931099

10941100
@property
10951101
def _is_view(self):
@@ -1154,8 +1160,35 @@ def _set_is_copy(self, ref=None, copy=True):
11541160
else:
11551161
self.is_copy = None
11561162

1157-
def _check_setitem_copy(self, stacklevel=4, t='setting'):
1163+
def _check_is_chained_assignment_possible(self):
1164+
"""
1165+
check if we are a view, have a cacher, and are of mixed type
1166+
if so, then force a setitem_copy check
1167+
1168+
should be called just near setting a value
1169+
1170+
will return a boolean if it we are a view and are cached, but a single-dtype
1171+
meaning that the cacher should be updated following setting
11581172
"""
1173+
if self._is_view and self._is_cached:
1174+
ref = self._get_cacher()
1175+
if ref is not None and ref._is_mixed_type:
1176+
self._check_setitem_copy(stacklevel=4, t='referant', force=True)
1177+
return True
1178+
elif self.is_copy:
1179+
self._check_setitem_copy(stacklevel=4, t='referant')
1180+
return False
1181+
1182+
def _check_setitem_copy(self, stacklevel=4, t='setting', force=False):
1183+
"""
1184+
1185+
Parameters
1186+
----------
1187+
stacklevel : integer, default 4
1188+
the level to show of the stack when the error is output
1189+
t : string, the type of setting error
1190+
force : boolean, default False
1191+
if True, then force showing an error
11591192
11601193
validate if we are doing a settitem on a chained copy.
11611194
@@ -1177,7 +1210,7 @@ def _check_setitem_copy(self, stacklevel=4, t='setting'):
11771210
11781211
"""
11791212

1180-
if self.is_copy:
1213+
if force or self.is_copy:
11811214

11821215
value = config.get_option('mode.chained_assignment')
11831216
if value is None:

pandas/core/indexing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ def can_do_equal_len():
472472
if isinstance(value, ABCPanel):
473473
value = self._align_panel(indexer, value)
474474

475+
# check for chained assignment
476+
self.obj._check_is_chained_assignment_possible()
477+
475478
# actually do the set
476479
self.obj._data = self.obj._data.setitem(indexer=indexer, value=value)
477480
self.obj._maybe_update_cacher(clear=True)

pandas/core/series.py

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -587,61 +587,68 @@ def _get_values(self, indexer):
587587
return self.values[indexer]
588588

589589
def __setitem__(self, key, value):
590-
try:
591-
self._set_with_engine(key, value)
592-
return
593-
except (SettingWithCopyError):
594-
raise
595-
except (KeyError, ValueError):
596-
values = self.values
597-
if (com.is_integer(key)
598-
and not self.index.inferred_type == 'integer'):
599590

600-
values[key] = value
591+
def setitem(key, value):
592+
try:
593+
self._set_with_engine(key, value)
601594
return
602-
elif key is Ellipsis:
603-
self[:] = value
595+
except (SettingWithCopyError):
596+
raise
597+
except (KeyError, ValueError):
598+
values = self.values
599+
if (com.is_integer(key)
600+
and not self.index.inferred_type == 'integer'):
601+
602+
values[key] = value
603+
return
604+
elif key is Ellipsis:
605+
self[:] = value
606+
return
607+
elif _is_bool_indexer(key):
608+
pass
609+
elif com.is_timedelta64_dtype(self.dtype):
610+
# reassign a null value to iNaT
611+
if isnull(value):
612+
value = tslib.iNaT
613+
614+
try:
615+
self.index._engine.set_value(self.values, key, value)
616+
return
617+
except (TypeError):
618+
pass
619+
620+
self.loc[key] = value
604621
return
605-
elif _is_bool_indexer(key):
606-
pass
607-
elif com.is_timedelta64_dtype(self.dtype):
608-
# reassign a null value to iNaT
609-
if isnull(value):
610-
value = tslib.iNaT
611-
612-
try:
613-
self.index._engine.set_value(self.values, key, value)
614-
return
615-
except (TypeError):
616-
pass
617-
618-
self.loc[key] = value
619-
return
620622

621-
except TypeError as e:
622-
if isinstance(key, tuple) and not isinstance(self.index,
623-
MultiIndex):
624-
raise ValueError("Can only tuple-index with a MultiIndex")
623+
except TypeError as e:
624+
if isinstance(key, tuple) and not isinstance(self.index,
625+
MultiIndex):
626+
raise ValueError("Can only tuple-index with a MultiIndex")
625627

626-
# python 3 type errors should be raised
627-
if 'unorderable' in str(e): # pragma: no cover
628-
raise IndexError(key)
628+
# python 3 type errors should be raised
629+
if 'unorderable' in str(e): # pragma: no cover
630+
raise IndexError(key)
629631

630-
if _is_bool_indexer(key):
631-
key = _check_bool_indexer(self.index, key)
632-
try:
633-
self.where(~key, value, inplace=True)
634-
return
635-
except (InvalidIndexError):
636-
pass
632+
if _is_bool_indexer(key):
633+
key = _check_bool_indexer(self.index, key)
634+
try:
635+
self.where(~key, value, inplace=True)
636+
return
637+
except (InvalidIndexError):
638+
pass
639+
640+
self._set_with(key, value)
637641

638-
self._set_with(key, value)
642+
# do the setitem
643+
cacher_needs_updating = self._check_is_chained_assignment_possible()
644+
setitem(key, value)
645+
if cacher_needs_updating:
646+
self._maybe_update_cacher()
639647

640648
def _set_with_engine(self, key, value):
641649
values = self.values
642650
try:
643651
self.index._engine.set_value(values, key, value)
644-
self._check_setitem_copy()
645652
return
646653
except KeyError:
647654
values[self.index.get_loc(key)] = value

pandas/io/tests/test_json/test_pandas.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,13 @@ def test_frame_from_json_nones(self):
305305
# infinities get mapped to nulls which get mapped to NaNs during
306306
# deserialisation
307307
df = DataFrame([[1, 2], [4, 5, 6]])
308-
df[2][0] = np.inf
308+
df.loc[0,2] = np.inf
309309
unser = read_json(df.to_json())
310310
self.assertTrue(np.isnan(unser[2][0]))
311311
unser = read_json(df.to_json(), dtype=False)
312312
self.assertTrue(np.isnan(unser[2][0]))
313313

314-
df[2][0] = np.NINF
314+
df.loc[0,2] = np.NINF
315315
unser = read_json(df.to_json())
316316
self.assertTrue(np.isnan(unser[2][0]))
317317
unser = read_json(df.to_json(),dtype=False)

pandas/io/tests/test_pytables.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,8 +1278,8 @@ def test_append_with_data_columns(self):
12781278
# data column selection with a string data_column
12791279
df_new = df.copy()
12801280
df_new['string'] = 'foo'
1281-
df_new['string'][1:4] = np.nan
1282-
df_new['string'][5:6] = 'bar'
1281+
df_new.loc[1:4,'string'] = np.nan
1282+
df_new.loc[5:6,'string'] = 'bar'
12831283
_maybe_remove(store, 'df')
12841284
store.append('df', df_new, data_columns=['string'])
12851285
result = store.select('df', [Term('string=foo')])
@@ -1317,14 +1317,14 @@ def check_col(key,name,size):
13171317
with ensure_clean_store(self.path) as store:
13181318
# multiple data columns
13191319
df_new = df.copy()
1320-
df_new.loc[:,'A'].iloc[0] = 1.
1321-
df_new.loc[:,'B'].iloc[0] = -1.
1320+
df_new.ix[0,'A'] = 1.
1321+
df_new.ix[0,'B'] = -1.
13221322
df_new['string'] = 'foo'
1323-
df_new['string'][1:4] = np.nan
1324-
df_new['string'][5:6] = 'bar'
1323+
df_new.loc[1:4,'string'] = np.nan
1324+
df_new.loc[5:6,'string'] = 'bar'
13251325
df_new['string2'] = 'foo'
1326-
df_new['string2'][2:5] = np.nan
1327-
df_new['string2'][7:8] = 'bar'
1326+
df_new.loc[2:5,'string2'] = np.nan
1327+
df_new.loc[7:8,'string2'] = 'bar'
13281328
_maybe_remove(store, 'df')
13291329
store.append(
13301330
'df', df_new, data_columns=['A', 'B', 'string', 'string2'])

pandas/tests/test_format.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,8 +1348,8 @@ def test_to_string(self):
13481348
'B': tm.makeStringIndex(200)},
13491349
index=lrange(200))
13501350

1351-
biggie['A'][:20] = nan
1352-
biggie['B'][:20] = nan
1351+
biggie.loc[:20,'A'] = nan
1352+
biggie.loc[:20,'B'] = nan
13531353
s = biggie.to_string()
13541354

13551355
buf = StringIO()
@@ -1597,8 +1597,8 @@ def test_to_html(self):
15971597
'B': tm.makeStringIndex(200)},
15981598
index=lrange(200))
15991599

1600-
biggie['A'][:20] = nan
1601-
biggie['B'][:20] = nan
1600+
biggie.loc[:20,'A'] = nan
1601+
biggie.loc[:20,'B'] = nan
16021602
s = biggie.to_html()
16031603

16041604
buf = StringIO()
@@ -1624,8 +1624,8 @@ def test_to_html_filename(self):
16241624
'B': tm.makeStringIndex(200)},
16251625
index=lrange(200))
16261626

1627-
biggie['A'][:20] = nan
1628-
biggie['B'][:20] = nan
1627+
biggie.loc[:20,'A'] = nan
1628+
biggie.loc[:20,'B'] = nan
16291629
with tm.ensure_clean('test.html') as path:
16301630
biggie.to_html(path)
16311631
with open(path, 'r') as f:

0 commit comments

Comments
 (0)