Repository: pandas-dev/pandas

Issue:
ENH: Add set_closed method to IntervalIndex
xref https://github.com/pandas-dev/pandas/pull/21584#discussion_r198317876

This method would allow for changing the `closed` value of an existing `IntervalIndex`.

Example usage:
```python
In [2]: index = pd.interval_range(0, 3, closed='both')

In [3]: index
Out[3]:
IntervalIndex([[0, 1], [1, 2], [2, 3]]
              closed='both',
              dtype='interval[int64]')

In [4]: index.set_closed('neither')
Out[4]:
IntervalIndex([(0, 1), (1, 2), (2, 3)]
              closed='neither',
              dtype='interval[int64]')
```


Discussion:

Resolving patch:
diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py
--- a/pandas/core/indexes/interval.py
+++ b/pandas/core/indexes/interval.py
@@ -180,6 +180,7 @@ class IntervalIndex(IntervalMixin, Index):
     from_tuples
     get_indexer
     get_loc
+    set_closed
 
     Examples
     ---------
@@ -708,6 +709,41 @@ def closed(self):
         """
         return self._closed
 
+    def set_closed(self, closed):
+        """
+        Return an IntervalIndex identical to the current one, but closed on the
+        specified side
+
+        .. versionadded:: 0.24.0
+
+        Parameters
+        ----------
+        closed : {'left', 'right', 'both', 'neither'}
+            Whether the intervals are closed on the left-side, right-side, both
+            or neither.
+
+        Returns
+        -------
+        new_index : IntervalIndex
+
+        Examples
+        --------
+        >>>  index = pd.interval_range(0, 3)
+        >>>  index
+        IntervalIndex([(0, 1], (1, 2], (2, 3]]
+              closed='right',
+              dtype='interval[int64]')
+        >>>  index.set_closed('both')
+        IntervalIndex([[0, 1], [1, 2], [2, 3]]
+              closed='both',
+              dtype='interval[int64]')
+        """
+        if closed not in _VALID_CLOSED:
+            msg = "invalid option for 'closed': {closed}"
+            raise ValueError(msg.format(closed=closed))
+
+        return self._shallow_copy(closed=closed)
+
     @property
     def length(self):
         """


Test patch:
diff --git a/pandas/tests/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py
--- a/pandas/tests/indexes/interval/test_interval.py
+++ b/pandas/tests/indexes/interval/test_interval.py
@@ -977,3 +977,20 @@ def test_to_tuples_na(self, tuples, na_tuple):
             assert all(isna(x) for x in result_na)
         else:
             assert isna(result_na)
+
+    @pytest.mark.parametrize('new_closed', [
+        'left', 'right', 'both', 'neither'])
+    def test_set_closed(self, name, closed, new_closed):
+        # GH 21670
+        index = interval_range(0, 5, closed=closed, name=name)
+        result = index.set_closed(new_closed)
+        expected = interval_range(0, 5, closed=new_closed, name=name)
+        tm.assert_index_equal(result, expected)
+
+    @pytest.mark.parametrize('bad_closed', ['foo', 10, 'LEFT', True, False])
+    def test_set_closed_errors(self, bad_closed):
+        # GH 21670
+        index = interval_range(0, 5)
+        msg = "invalid option for 'closed': {closed}".format(closed=bad_closed)
+        with tm.assert_raises_regex(ValueError, msg):
+            index.set_closed(bad_closed)