Repository: pandas-dev/pandas

Issue:
BUG?: .at not working on object indexes containing some integers
Version 0.22.0

#### Problem description
Using the .at - Method on an Index which contains Integers as well as str/objects raises an Error. This used to be possible using the ``.get_value()``-Method. As ``.at`` is the designated successor (#15269) the same behaviour should be supported.
I also noticed that ``.get_value`` is approx. twice as fast as ``.at``. Is there a specific reason to stick with ``.at``?  (see again #15269 for a speed comparison)

#### Code Sample
```python
import pandas as pd
import numpy as np

data = np.random.randn(10, 5)
df = pd.DataFrame(data, columns=['a', 'b', 'c', 1, 2])
df.at[0, 1]
```

Raises:
```python
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/thielc/anaconda3/lib/python3.6/site-packages/pandas/core/indexing.py", line 1868, in __getitem__
    key = self._convert_key(key)
  File "/home/thielc/anaconda3/lib/python3.6/site-packages/pandas/core/indexing.py", line 1915, in _convert_key
    raise ValueError("At based indexing on an non-integer "
ValueError: At based indexing on an non-integer index can only have non-integer indexers
```




Discussion:
@c-thiel : Thanks for reporting this!  I'm a little unclear as to what's being reported.  Is this a regression from a previous version (you said this used to be possible), or is this a general API inconsistency that you're referring to?

That aside, I do agree that the behavior does look strange.
I don't think it is a regression from a previous version (test with 0.16, already broken there), but it is possible with `get_value`. However, this is deprecated, and we say people should use `.at` instead. So then `.at` should work correctly on this as well.

The problem comes from here:

https://github.com/pandas-dev/pandas/blob/572476f0a3652222c17458d418a107554580eaa5/pandas/core/indexing.py#L1907-L1916

where we do this check. But I agree the check looks to strict, as a mixed object index can indeed contain integers as well.

Welcome to try to fix this (eg try removing this check, and see if some tests fail due to that).

yeah I think prob ok to remove the else check; this ultimately goes thru ``.loc`` so indexing verfication can occur there 
@c-thiel indexing with mixed dtype indexes is simply going to be slow generally.
@jorisvandenbossche Yes, this is what I was reffering to.

@jreback : Regarding Performance, ``at`` is still much faster than ``loc``, especially when setting values. The setting-performance for single values is the main reason for me using the set_value and get_value functions. But also the ``get_value``, ``set_value`` functions are twice as fast as ``at`` :
```python
import pandas as pd
import numpy as np
import time

c = ['a', 'b', 'c', 'd', 'e']
data = np.random.rand(10000, 5)
df = pd.DataFrame(data, columns=c)
rows = np.random.randint(0, 9999, (100000,))
columns = np.random.choice(c, (100000,))

t = time.time()
for row, column in zip(rows, columns):
    a = df.get_value(row, column)    
print(f'get_value: {time.time()-t}')

t = time.time()
for row, column in zip(rows, columns):
    a = df.at[row, column]    
print(f'at: {time.time()-t}')

t = time.time()
for row, column in zip(rows, columns):
    a = df.loc[row, column]    
print(f'loc: {time.time()-t}')

t = time.time()
for row, column in zip(rows, columns):
    df.at[row, column] = 4
print(f'set at: {time.time()-t}')

t = time.time()
for row, column in zip(rows, columns):
    df.loc[row, column] = 5
print(f'set loc: {time.time()-t}')

t = time.time()
for row, column in zip(rows, columns):
    df.set_value(row, column, 4)
print(f'set_value: {time.time()-t}')
```
```
get_value: 0.257692813873291
at: 0.52744460105896
loc: 0.7349758148193359
set at: 0.687880277633667
set loc: 11.664336204528809
set_value: 0.3008086681365967
```

@c-thiel setting individual values in a loop is non-idiomatic. set_value/get_value were deprecated because they didn't properly handle *any* edge cases nor had any type safetly whatsoever. Correct is much much better then wrong but *slightly* faster.

Resolving patch:
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -3125,8 +3125,8 @@ def get_value(self, series, key):
                 iloc = self.get_loc(key)
                 return s[iloc]
             except KeyError:
-                if (len(self) > 0 and
-                        self.inferred_type in ['integer', 'boolean']):
+                if (len(self) > 0
+                        and (self.holds_integer() or self.is_boolean())):
                     raise
                 elif is_integer(key):
                     return s[key]
@@ -3139,7 +3139,7 @@ def get_value(self, series, key):
             return self._engine.get_value(s, k,
                                           tz=getattr(series.dtype, 'tz', None))
         except KeyError as e1:
-            if len(self) > 0 and self.inferred_type in ['integer', 'boolean']:
+            if len(self) > 0 and (self.holds_integer() or self.is_boolean()):
                 raise
 
             try:
diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py
--- a/pandas/core/indexing.py
+++ b/pandas/core/indexing.py
@@ -2354,7 +2354,7 @@ def _convert_key(self, key, is_setter=False):
                     raise ValueError("At based indexing on an integer index "
                                      "can only have integer indexers")
             else:
-                if is_integer(i):
+                if is_integer(i) and not ax.holds_integer():
                     raise ValueError("At based indexing on an non-integer "
                                      "index can only have non-integer "
                                      "indexers")


Test patch:
diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py
--- a/pandas/tests/indexing/test_indexing.py
+++ b/pandas/tests/indexing/test_indexing.py
@@ -610,6 +610,22 @@ def test_index_contains(self, index, val):
     def test_index_not_contains(self, index, val):
         assert val not in index
 
+    @pytest.mark.parametrize("index,val", [
+        (Index([0, 1, '2']), 0),
+        (Index([0, 1, '2']), '2'),
+    ])
+    def test_mixed_index_contains(self, index, val):
+        # GH 19860
+        assert val in index
+
+    @pytest.mark.parametrize("index,val", [
+        (Index([0, 1, '2']), '1'),
+        (Index([0, 1, '2']), 2),
+    ])
+    def test_mixed_index_not_contains(self, index, val):
+        # GH 19860
+        assert val not in index
+
     def test_index_type_coercion(self):
 
         with catch_warnings(record=True):
@@ -710,6 +726,22 @@ def test_float_index_at_iat(self):
         for i in range(len(s)):
             assert s.iat[i] == i + 1
 
+    def test_mixed_index_assignment(self):
+        # GH 19860
+        s = Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 1, 2])
+        s.at['a'] = 11
+        assert s.iat[0] == 11
+        s.at[1] = 22
+        assert s.iat[3] == 22
+
+    def test_mixed_index_no_fallback(self):
+        # GH 19860
+        s = Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 1, 2])
+        with pytest.raises(KeyError):
+            s.at[0]
+        with pytest.raises(KeyError):
+            s.at[4]
+
     def test_rhs_alignment(self):
         # GH8258, tests that both rows & columns are aligned to what is
         # assigned to. covers both uniform data-type & multi-type cases
diff --git a/pandas/tests/indexing/test_scalar.py b/pandas/tests/indexing/test_scalar.py
--- a/pandas/tests/indexing/test_scalar.py
+++ b/pandas/tests/indexing/test_scalar.py
@@ -170,3 +170,33 @@ def test_at_with_tz(self):
 
         result = df.at[0, 'date']
         assert result == expected
+
+    def test_mixed_index_at_iat_loc_iloc_series(self):
+        # GH 19860
+        s = Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 1, 2])
+        for el, item in s.iteritems():
+            assert s.at[el] == s.loc[el] == item
+        for i in range(len(s)):
+            assert s.iat[i] == s.iloc[i] == i + 1
+
+        with pytest.raises(KeyError):
+            s.at[4]
+        with pytest.raises(KeyError):
+            s.loc[4]
+
+    def test_mixed_index_at_iat_loc_iloc_dataframe(self):
+        # GH 19860
+        df = DataFrame([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]],
+                       columns=['a', 'b', 'c', 1, 2])
+        for rowIdx, row in df.iterrows():
+            for el, item in row.iteritems():
+                assert df.at[rowIdx, el] == df.loc[rowIdx, el] == item
+
+        for row in range(2):
+            for i in range(5):
+                assert df.iat[row, i] == df.iloc[row, i] == row * 5 + i
+
+        with pytest.raises(KeyError):
+            df.at[0, 3]
+        with pytest.raises(KeyError):
+            df.loc[0, 3]