Skip to content

Commit 70a17da

Browse files
committed
COMPAT: capture chain indexing even on single-dtyped
1 parent 1264ef5 commit 70a17da

File tree

9 files changed

+115
-92
lines changed

9 files changed

+115
-92
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.

pandas/core/generic.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,11 +1088,10 @@ def _maybe_cache_changed(self, item, value):
10881088
@property
10891089
def _is_cached(self):
10901090
""" boolean : return if I am cached """
1091-
cacher = getattr(self, '_cacher', None)
1092-
return cacher is not None
1091+
return getattr(self, '_cacher', None) is not None
10931092

10941093
def _get_cacher(self):
1095-
""" return my cahcer or None """
1094+
""" return my cacher or None """
10961095
cacher = getattr(self, '_cacher', None)
10971096
if cacher is not None:
10981097
cacher = cacher[1]()
@@ -1167,13 +1166,18 @@ def _check_is_chained_assignment_possible(self):
11671166
if so, then force a setitem_copy check
11681167
11691168
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
11701172
"""
11711173
if self._is_view and self._is_cached:
11721174
ref = self._get_cacher()
11731175
if ref is not None and ref._is_mixed_type:
1174-
self._check_setitem_copy(stacklevel=5, t='referant', force=True)
1176+
self._check_setitem_copy(stacklevel=4, t='referant', force=True)
1177+
return True
11751178
elif self.is_copy:
1176-
self._check_setitem_copy(stacklevel=5, t='referant')
1179+
self._check_setitem_copy(stacklevel=4, t='referant')
1180+
return False
11771181

11781182
def _check_setitem_copy(self, stacklevel=4, t='setting', force=False):
11791183
"""

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_is_chained_assignment_possible()
645652
return
646653
except KeyError:
647654
values[self.index.get_loc(key)] = value

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:

pandas/tests/test_frame.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4438,8 +4438,8 @@ def test_repr_mixed_big(self):
44384438
biggie = DataFrame({'A': randn(200),
44394439
'B': tm.makeStringIndex(200)},
44404440
index=lrange(200))
4441-
biggie['A'][:20] = nan
4442-
biggie['B'][:20] = nan
4441+
biggie.loc[:20,'A'] = nan
4442+
biggie.loc[:20,'B'] = nan
44434443

44444444
foo = repr(biggie)
44454445

@@ -7412,19 +7412,19 @@ def test_drop(self):
74127412
assert_frame_equal(df,expected)
74137413

74147414
def test_fillna(self):
7415-
self.tsframe['A'][:5] = nan
7416-
self.tsframe['A'][-5:] = nan
7415+
self.tsframe.ix[:5,'A'] = nan
7416+
self.tsframe.ix[-5:,'A'] = nan
74177417

74187418
zero_filled = self.tsframe.fillna(0)
7419-
self.assertTrue((zero_filled['A'][:5] == 0).all())
7419+
self.assertTrue((zero_filled.ix[:5,'A'] == 0).all())
74207420

74217421
padded = self.tsframe.fillna(method='pad')
7422-
self.assertTrue(np.isnan(padded['A'][:5]).all())
7423-
self.assertTrue((padded['A'][-5:] == padded['A'][-5]).all())
7422+
self.assertTrue(np.isnan(padded.ix[:5,'A']).all())
7423+
self.assertTrue((padded.ix[-5:,'A'] == padded.ix[-5,'A']).all())
74247424

74257425
# mixed type
7426-
self.mixed_frame['foo'][5:20] = nan
7427-
self.mixed_frame['A'][-10:] = nan
7426+
self.mixed_frame.ix[5:20,'foo'] = nan
7427+
self.mixed_frame.ix[-10:,'A'] = nan
74287428
result = self.mixed_frame.fillna(value=0)
74297429
result = self.mixed_frame.fillna(method='pad')
74307430

@@ -7433,7 +7433,7 @@ def test_fillna(self):
74337433

74347434
# mixed numeric (but no float16)
74357435
mf = self.mixed_float.reindex(columns=['A','B','D'])
7436-
mf['A'][-10:] = nan
7436+
mf.ix[-10:,'A'] = nan
74377437
result = mf.fillna(value=0)
74387438
_check_mixed_float(result, dtype = dict(C = None))
74397439

@@ -7605,8 +7605,8 @@ def test_replace_inplace(self):
76057605
self.assertRaises(TypeError, self.tsframe.replace, nan)
76067606

76077607
# mixed type
7608-
self.mixed_frame['foo'][5:20] = nan
7609-
self.mixed_frame['A'][-10:] = nan
7608+
self.mixed_frame.ix[5:20,'foo'] = nan
7609+
self.mixed_frame.ix[-10:,'A'] = nan
76107610

76117611
result = self.mixed_frame.replace(np.nan, 0)
76127612
expected = self.mixed_frame.fillna(value=0)
@@ -8194,8 +8194,8 @@ def test_replace_convert(self):
81948194
assert_series_equal(expec, res)
81958195

81968196
def test_replace_mixed(self):
8197-
self.mixed_frame['foo'][5:20] = nan
8198-
self.mixed_frame['A'][-10:] = nan
8197+
self.mixed_frame.ix[5:20,'foo'] = nan
8198+
self.mixed_frame.ix[-10:,'A'] = nan
81998199

82008200
result = self.mixed_frame.replace(np.nan, -18)
82018201
expected = self.mixed_frame.fillna(value=-18)
@@ -11717,11 +11717,11 @@ def test_rename_objects(self):
1171711717
self.assertNotIn('foo', renamed)
1171811718

1171911719
def test_fill_corner(self):
11720-
self.mixed_frame['foo'][5:20] = nan
11721-
self.mixed_frame['A'][-10:] = nan
11720+
self.mixed_frame.ix[5:20,'foo'] = nan
11721+
self.mixed_frame.ix[-10:,'A'] = nan
1172211722

1172311723
filled = self.mixed_frame.fillna(value=0)
11724-
self.assertTrue((filled['foo'][5:20] == 0).all())
11724+
self.assertTrue((filled.ix[5:20,'foo'] == 0).all())
1172511725
del self.mixed_frame['foo']
1172611726

1172711727
empty_float = self.frame.reindex(columns=[])
@@ -12716,6 +12716,7 @@ def __nonzero__(self):
1271612716
self.assertTrue(r1.all())
1271712717

1271812718
def test_strange_column_corruption_issue(self):
12719+
1271912720
df = DataFrame(index=[0, 1])
1272012721
df[0] = nan
1272112722
wasCol = {}

pandas/tests/test_groupby.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1664,7 +1664,7 @@ def test_cythonized_aggers(self):
16641664
'B': ['A', 'B'] * 6,
16651665
'C': np.random.randn(12)}
16661666
df = DataFrame(data)
1667-
df['C'][2:10:2] = nan
1667+
df.loc[2:10:2,'C'] = nan
16681668

16691669
def _testit(op):
16701670
# single column

pandas/tests/test_indexing.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3079,16 +3079,26 @@ def test_setitem_cache_updating(self):
30793079

30803080
# GH 7084
30813081
# not updating cache on series setting with slices
3082+
expected = DataFrame({'A': [600, 600, 600]}, index=date_range('5/7/2014', '5/9/2014'))
30823083
out = DataFrame({'A': [0, 0, 0]}, index=date_range('5/7/2014', '5/9/2014'))
30833084
df = DataFrame({'C': ['A', 'A', 'A'], 'D': [100, 200, 300]})
30843085

30853086
#loop through df to update out
30863087
six = Timestamp('5/7/2014')
30873088
eix = Timestamp('5/9/2014')
30883089
for ix, row in df.iterrows():
3089-
out[row['C']][six:eix] = out[row['C']][six:eix] + row['D']
3090+
out.loc[six:eix,row['C']] = out.loc[six:eix,row['C']] + row['D']
3091+
3092+
assert_frame_equal(out, expected)
3093+
assert_series_equal(out['A'], expected['A'])
3094+
3095+
# try via a chain indexing
3096+
# this actually works
3097+
out = DataFrame({'A': [0, 0, 0]}, index=date_range('5/7/2014', '5/9/2014'))
3098+
for ix, row in df.iterrows():
3099+
v = out[row['C']][six:eix] + row['D']
3100+
out[row['C']][six:eix] = v
30903101

3091-
expected = DataFrame({'A': [600, 600, 600]}, index=date_range('5/7/2014', '5/9/2014'))
30923102
assert_frame_equal(out, expected)
30933103
assert_series_equal(out['A'], expected['A'])
30943104

@@ -3176,8 +3186,6 @@ def f():
31763186
indexer = df.a.str.startswith('o')
31773187
df[indexer]['c'] = 42
31783188
self.assertRaises(com.SettingWithCopyError, f)
3179-
df['c'][df.a.str.startswith('o')] = 42
3180-
assert_frame_equal(df,expected)
31813189

31823190
expected = DataFrame({'A':[111,'bbb','ccc'],'B':[1,2,3]})
31833191
df = DataFrame({'A':['aaa','bbb','ccc'],'B':[1,2,3]})
@@ -3187,6 +3195,8 @@ def f():
31873195
def f():
31883196
df.loc[0]['A'] = 111
31893197
self.assertRaises(com.SettingWithCopyError, f)
3198+
3199+
df.loc[0,'A'] = 111
31903200
assert_frame_equal(df,expected)
31913201

31923202
# make sure that is_copy is picked up reconstruction

0 commit comments

Comments
 (0)