Repository: pandas-dev/pandas (python)

Issue:
DOC: Clarify how groupby forms groups for mixed-value columns
### Pandas version checks

- [X] I have checked that the issue still exists on the latest versions of the docs on `main` [here](https://pandas.pydata.org/docs/dev/)


### Location of the documentation

https://pandas.pydata.org/docs/dev/reference/api/pandas.DataFrame.groupby.html#pandas.DataFrame.groupby
https://pandas.pydata.org/docs/dev/user_guide/groupby.html#

### Documentation problem

In my broad view of things, groupby can work on any (combination of) hashables, but there can be some unintuitive situations that should be mentioned in the docs.

E.g., the following should and does work (work as in there is no exception, but the results are not necessarily as expected):
```
>>> df = pd.DataFrame({'a': [False, 1, True, 'string'], 'b': [1, 2, 3, 4]})
        a  b
0   False  1
1       1  2
2    True  3
3  string  4

# let's groupby on 'a':
>>> df.groupby('a').describe()
           b
       count mean       std  min   25%  50%   75%  max
a
False    1.0  1.0       NaN  1.0  1.00  1.0  1.00  1.0
1        2.0  2.5  0.707107  2.0  2.25  2.5  2.75  3.0
string   1.0  4.0       NaN  4.0  4.00  4.0  4.00  4.0
```
We can see that the value `1` and `True` were grouped together. From a Python background, that is not unexpected, as `1 == True` evaluates as `True`, i.e. `1` and `True` are equal and thus in the same group.

While this data layout may be questionable, it certainly happens in reality. I find myself often using (abusing?) `groupby()` on multiple cols to identify unique combinations of values, and in this case, I would not want `1` to be considered equal to `True` (or `0` equal to `False` and whichever other 'convenience' conversions Python does).


### Suggested fix for documentation


I think the docs could benefit from mentioning this pitfall. The suggested workaround that I see would be to first convert the columns to strings before the `groupby`:
```
>>> df.groupby(df['a'].apply(str)).describe()
           b
       count mean std  min  25%  50%  75%  max
a
1        1.0  2.0 NaN  2.0  2.0  2.0  2.0  2.0
False    1.0  1.0 NaN  1.0  1.0  1.0  1.0  1.0
True     1.0  3.0 NaN  3.0  3.0  3.0  3.0  3.0
string   1.0  4.0 NaN  4.0  4.0  4.0  4.0  4.0
```


Pull request:
DOC: Add clarification to groupby docs regarding hashes and equality
closes #57526

Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 7802ef4798..157b87c93e 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -245,6 +245,7 @@ Removal of prior version deprecations/changes
 
 Performance improvements
 ~~~~~~~~~~~~~~~~~~~~~~~~
+- :meth:`RangeIndex.append` returns a :class:`RangeIndex` instead of a :class:`Index` when appending values that could continue the :class:`RangeIndex` (:issue:`57467`)
 - :meth:`Series.str.extract` returns a :class:`RangeIndex` columns instead of an :class:`Index` column when possible (:issue:`57542`)
 - Performance improvement in :class:`DataFrame` when ``data`` is a ``dict`` and ``columns`` is specified (:issue:`24368`)
 - Performance improvement in :meth:`DataFrame.join` for sorted but non-unique indexes (:issue:`56941`)
diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py
index 09d635b53c..0781a86e5d 100644
--- a/pandas/core/indexes/range.py
+++ b/pandas/core/indexes/range.py
@@ -989,7 +989,10 @@ class RangeIndex(Index):
         indexes = [RangeIndex(3), RangeIndex(4, 6)] -> Index([0,1,2,4,5], dtype='int64')
         """
         if not all(isinstance(x, RangeIndex) for x in indexes):
-            return super()._concat(indexes, name)
+            result = super()._concat(indexes, name)
+            if result.dtype.kind == "i":
+                return self._shallow_copy(result._values)
+            return result
 
         elif len(indexes) == 1:
             return indexes[0]
diff --git a/pandas/core/shared_docs.py b/pandas/core/shared_docs.py
index 787a03471c..06621f7127 100644
--- a/pandas/core/shared_docs.py
+++ b/pandas/core/shared_docs.py
@@ -178,6 +178,12 @@ See the `user guide
 <https://pandas.pydata.org/pandas-docs/stable/groupby.html>`__ for more
 detailed usage and examples, including splitting an object into groups,
 iterating through groups, selecting a group, aggregation, and more.
+
+The implementation of groupby is hash-based, meaning in particular that
+objects that compare as equal will be considered to be in the same group.
+An exception to this is that pandas has special handling of NA values:
+any NA values will be collapsed to a single group, regardless of how
+they compare. See the user guide linked above for more details.
 """
 
 _shared_docs["melt"] = """


Test patch:
diff --git a/pandas/tests/indexes/ranges/test_range.py b/pandas/tests/indexes/ranges/test_range.py
index 898548d1cc..8c24ce5d69 100644
--- a/pandas/tests/indexes/ranges/test_range.py
+++ b/pandas/tests/indexes/ranges/test_range.py
@@ -608,6 +608,20 @@ class TestRangeIndex:
         tm.assert_index_equal(result, expected)
 
 
+def test_append_non_rangeindex_return_rangeindex():
+    ri = RangeIndex(1)
+    result = ri.append(Index([1]))
+    expected = RangeIndex(2)
+    tm.assert_index_equal(result, expected, exact=True)
+
+
+def test_append_non_rangeindex_return_index():
+    ri = RangeIndex(1)
+    result = ri.append(Index([1, 3, 4]))
+    expected = Index([0, 1, 3, 4])
+    tm.assert_index_equal(result, expected, exact=True)
+
+
 def test_reindex_returns_rangeindex():
     ri = RangeIndex(2, name="foo")
     result, result_indexer = ri.reindex([1, 2, 3])