Repository: pandas-dev/pandas

Issue:
Allow IntervalIndex constructor closed parameter to override inferred closed
#### Code Sample, a copy-pastable example if possible

The `closed` parameter of `IntervalIndex` is currently a bit inconsistent, and it's behavior is input dependent.  By my count, there are 3 different behaviors:

**1.  `closed` parameter overrides the inferred `closed` when the input data is empty or purely NA**
```python
In [2]: pd.IntervalIndex([], closed='both')
Out[2]:
IntervalIndex([]
              closed='both',
              dtype='interval[int64]')

In [3]: pd.IntervalIndex([np.nan, np.nan], closed='neither')
Out[3]:
IntervalIndex([nan, nan]
              closed='neither',
              dtype='interval[float64]')
```

**2. `closed` is ignored when the input data is an `IntervalIndex`:**
```python
In [4]: ii = pd.interval_range(0, 3, closed='both')

In [5]: ii
Out[5]:
IntervalIndex([[0, 1], [1, 2], [2, 3]]
              closed='both',
              dtype='interval[int64]')

In [6]: pd.IntervalIndex(ii, closed='neither')
Out[6]:
IntervalIndex([[0, 1], [1, 2], [2, 3]]
              closed='both',
              dtype='interval[int64]')
```

**3. `closed` raises a `ValueError` when a list-like of `Interval` objects with a conflicting value for `closed` is passed as input data:**
```python
In [7]: ivs = [pd.Interval(0, 1, closed='both'), pd.Interval(1, 2, closed='both')]

In [8]: pd.IntervalIndex(ivs, closed='neither')
---------------------------------------------------------------------------
ValueError: conflicting values for closed: constructor got 'neither', inferred from data 'both'
```
xref #18421 where I implemented this behavior; I'm no longer sure it's the best approach to take.


#### Problem description
This behavior is inconsistent, and could lead to confusion.  As it's currently implemented, the `closed` parameter is useless in the majority of cases, either doing nothing or raising an error.

#### Expected Output
I'd expect there to be consistency as to how the `closed` parameter is handled.  My preference would be to have it override the inferred `closed` if explicitly passed by the user.  Note that this is consistent with how the `dtype` parameter is used in constructors throughout the codebase.

xref #19371 (may render this issue moot)


Discussion:
I a nice pattern would be to default the constructor to ``closed=None`` and just infer

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
@@ -233,7 +233,7 @@ def __new__(cls, data, closed=None, dtype=None, copy=False,
         if isinstance(data, IntervalIndex):
             left = data.left
             right = data.right
-            closed = data.closed
+            closed = closed or data.closed
         else:
 
             # don't allow scalars
@@ -241,16 +241,8 @@ def __new__(cls, data, closed=None, dtype=None, copy=False,
                 cls._scalar_data_error(data)
 
             data = maybe_convert_platform_interval(data)
-            left, right, infer_closed = intervals_to_interval_bounds(data)
-
-            if (com._all_not_none(closed, infer_closed) and
-                    closed != infer_closed):
-                # GH 18421
-                msg = ("conflicting values for closed: constructor got "
-                       "'{closed}', inferred from data '{infer_closed}'"
-                       .format(closed=closed, infer_closed=infer_closed))
-                raise ValueError(msg)
-
+            left, right, infer_closed = intervals_to_interval_bounds(
+                data, validate_closed=closed is None)
             closed = closed or infer_closed
 
         return cls._simple_new(left, right, closed, name, copy=copy,


Test patch:
diff --git a/pandas/tests/indexes/interval/test_construction.py b/pandas/tests/indexes/interval/test_construction.py
--- a/pandas/tests/indexes/interval/test_construction.py
+++ b/pandas/tests/indexes/interval/test_construction.py
@@ -317,13 +317,7 @@ def test_generic_errors(self, constructor):
         pass
 
     def test_constructor_errors(self, constructor):
-        # mismatched closed inferred from intervals vs constructor.
-        ivs = [Interval(0, 1, closed='both'), Interval(1, 2, closed='both')]
-        msg = 'conflicting values for closed'
-        with tm.assert_raises_regex(ValueError, msg):
-            constructor(ivs, closed='neither')
-
-        # mismatched closed within intervals
+        # mismatched closed within intervals with no constructor override
         ivs = [Interval(0, 1, closed='right'), Interval(2, 3, closed='left')]
         msg = 'intervals must all be closed on the same side'
         with tm.assert_raises_regex(ValueError, msg):
@@ -341,6 +335,24 @@ def test_constructor_errors(self, constructor):
         with tm.assert_raises_regex(TypeError, msg):
             constructor([0, 1])
 
+    @pytest.mark.parametrize('data, closed', [
+        ([], 'both'),
+        ([np.nan, np.nan], 'neither'),
+        ([Interval(0, 3, closed='neither'),
+          Interval(2, 5, closed='neither')], 'left'),
+        ([Interval(0, 3, closed='left'),
+          Interval(2, 5, closed='right')], 'neither'),
+        (IntervalIndex.from_breaks(range(5), closed='both'), 'right')])
+    def test_override_inferred_closed(self, constructor, data, closed):
+        # GH 19370
+        if isinstance(data, IntervalIndex):
+            tuples = data.to_tuples()
+        else:
+            tuples = [(iv.left, iv.right) if notna(iv) else iv for iv in data]
+        expected = IntervalIndex.from_tuples(tuples, closed=closed)
+        result = constructor(data, closed=closed)
+        tm.assert_index_equal(result, expected)
+
 
 class TestFromIntervals(TestClassConstructors):
     """