Skip to content

Commit 250574e

Browse files
jschendeljreback
authored andcommitted
ENH: Add dtype parameter to IntervalIndex constructors and deprecate from_intervals (#19339)
1 parent e865b77 commit 250574e

File tree

11 files changed

+432
-306
lines changed

11 files changed

+432
-306
lines changed

doc/source/api.rst

-1
Original file line numberDiff line numberDiff line change
@@ -1617,7 +1617,6 @@ IntervalIndex Components
16171617
IntervalIndex.from_arrays
16181618
IntervalIndex.from_tuples
16191619
IntervalIndex.from_breaks
1620-
IntervalIndex.from_intervals
16211620
IntervalIndex.contains
16221621
IntervalIndex.left
16231622
IntervalIndex.right

doc/source/whatsnew/v0.23.0.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,8 @@ Other Enhancements
207207
:func:`pandas.api.extensions.register_index_accessor`, accessor for libraries downstream of pandas
208208
to register custom accessors like ``.cat`` on pandas objects. See
209209
:ref:`Registering Custom Accessors <developer.register-accessors>` for more (:issue:`14781`).
210-
211-
212210
- ``IntervalIndex.astype`` now supports conversions between subtypes when passed an ``IntervalDtype`` (:issue:`19197`)
211+
- :class:`IntervalIndex` and its associated constructor methods (``from_arrays``, ``from_breaks``, ``from_tuples``) have gained a ``dtype`` parameter (:issue:`19262`)
213212

214213
.. _whatsnew_0230.api_breaking:
215214

@@ -329,6 +328,7 @@ Deprecations
329328
- ``Series.valid`` is deprecated. Use :meth:`Series.dropna` instead (:issue:`18800`).
330329
- :func:`read_excel` has deprecated the ``skip_footer`` parameter. Use ``skipfooter`` instead (:issue:`18836`)
331330
- The ``is_copy`` attribute is deprecated and will be removed in a future version (:issue:`18801`).
331+
- ``IntervalIndex.from_intervals`` is deprecated in favor of the :class:`IntervalIndex` constructor (:issue:`19263`)
332332

333333

334334
.. _whatsnew_0230.prior_deprecations:

pandas/core/indexes/base.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None,
200200
# interval
201201
if is_interval_dtype(data) or is_interval_dtype(dtype):
202202
from .interval import IntervalIndex
203-
return IntervalIndex(data, dtype=dtype, name=name, copy=copy)
203+
closed = kwargs.get('closed', None)
204+
return IntervalIndex(data, dtype=dtype, name=name, copy=copy,
205+
closed=closed)
204206

205207
# index-like
206208
elif isinstance(data, (np.ndarray, Index, ABCSeries)):
@@ -313,8 +315,7 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None,
313315
return Float64Index(subarr, copy=copy, name=name)
314316
elif inferred == 'interval':
315317
from .interval import IntervalIndex
316-
return IntervalIndex.from_intervals(subarr, name=name,
317-
copy=copy)
318+
return IntervalIndex(subarr, name=name, copy=copy)
318319
elif inferred == 'boolean':
319320
# don't support boolean explicitly ATM
320321
pass

pandas/core/indexes/category.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ def __array__(self, dtype=None):
341341
def astype(self, dtype, copy=True):
342342
if is_interval_dtype(dtype):
343343
from pandas import IntervalIndex
344-
return IntervalIndex.from_intervals(np.array(self))
344+
return IntervalIndex(np.array(self))
345345
elif is_categorical_dtype(dtype):
346346
# GH 18630
347347
dtype = self.dtype._update_dtype(dtype)

pandas/core/indexes/interval.py

+74-43
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" define the IntervalIndex """
22

33
import numpy as np
4+
import warnings
45

56
from pandas.core.dtypes.missing import notna, isna
67
from pandas.core.dtypes.generic import ABCDatetimeIndex, ABCPeriodIndex
@@ -151,6 +152,10 @@ class IntervalIndex(IntervalMixin, Index):
151152
Name to be stored in the index.
152153
copy : boolean, default False
153154
Copy the meta-data
155+
dtype : dtype or None, default None
156+
If None, dtype will be inferred
157+
158+
..versionadded:: 0.23.0
154159
155160
Attributes
156161
----------
@@ -167,7 +172,6 @@ class IntervalIndex(IntervalMixin, Index):
167172
from_arrays
168173
from_tuples
169174
from_breaks
170-
from_intervals
171175
contains
172176
173177
Examples
@@ -181,8 +185,7 @@ class IntervalIndex(IntervalMixin, Index):
181185
182186
It may also be constructed using one of the constructor
183187
methods: :meth:`IntervalIndex.from_arrays`,
184-
:meth:`IntervalIndex.from_breaks`, :meth:`IntervalIndex.from_intervals`
185-
and :meth:`IntervalIndex.from_tuples`.
188+
:meth:`IntervalIndex.from_breaks`, and :meth:`IntervalIndex.from_tuples`.
186189
187190
See further examples in the doc strings of ``interval_range`` and the
188191
mentioned constructor methods.
@@ -211,8 +214,7 @@ class IntervalIndex(IntervalMixin, Index):
211214

212215
_mask = None
213216

214-
def __new__(cls, data, closed=None,
215-
name=None, copy=False, dtype=None,
217+
def __new__(cls, data, closed=None, name=None, copy=False, dtype=None,
216218
fastpath=False, verify_integrity=True):
217219

218220
if fastpath:
@@ -245,19 +247,28 @@ def __new__(cls, data, closed=None,
245247

246248
closed = closed or infer_closed
247249

248-
return cls._simple_new(left, right, closed, name,
249-
copy=copy, verify_integrity=verify_integrity)
250+
return cls._simple_new(left, right, closed, name, copy=copy,
251+
dtype=dtype, verify_integrity=verify_integrity)
250252

251253
@classmethod
252-
def _simple_new(cls, left, right, closed=None, name=None,
253-
copy=False, verify_integrity=True):
254+
def _simple_new(cls, left, right, closed=None, name=None, copy=False,
255+
dtype=None, verify_integrity=True):
254256
result = IntervalMixin.__new__(cls)
255257

256-
if closed is None:
257-
closed = 'right'
258+
closed = closed or 'right'
258259
left = _ensure_index(left, copy=copy)
259260
right = _ensure_index(right, copy=copy)
260261

262+
if dtype is not None:
263+
# GH 19262: dtype must be an IntervalDtype to override inferred
264+
dtype = pandas_dtype(dtype)
265+
if not is_interval_dtype(dtype):
266+
msg = 'dtype must be an IntervalDtype, got {dtype}'
267+
raise TypeError(msg.format(dtype=dtype))
268+
elif dtype.subtype is not None:
269+
left = left.astype(dtype.subtype)
270+
right = right.astype(dtype.subtype)
271+
261272
# coerce dtypes to match if needed
262273
if is_float_dtype(left) and is_integer_dtype(right):
263274
right = right.astype(left.dtype)
@@ -304,7 +315,7 @@ def _shallow_copy(self, left=None, right=None, **kwargs):
304315
# only single value passed, could be an IntervalIndex
305316
# or array of Intervals
306317
if not isinstance(left, IntervalIndex):
307-
left = type(self).from_intervals(left)
318+
left = self._constructor(left)
308319

309320
left, right = left.left, left.right
310321
else:
@@ -322,7 +333,7 @@ def _validate(self):
322333
Verify that the IntervalIndex is valid.
323334
"""
324335
if self.closed not in _VALID_CLOSED:
325-
raise ValueError("invalid options for 'closed': {closed}"
336+
raise ValueError("invalid option for 'closed': {closed}"
326337
.format(closed=self.closed))
327338
if len(self.left) != len(self.right):
328339
raise ValueError('left and right must have the same length')
@@ -356,7 +367,7 @@ def _engine(self):
356367

357368
@property
358369
def _constructor(self):
359-
return type(self).from_intervals
370+
return type(self)
360371

361372
def __contains__(self, key):
362373
"""
@@ -402,7 +413,8 @@ def contains(self, key):
402413
return False
403414

404415
@classmethod
405-
def from_breaks(cls, breaks, closed='right', name=None, copy=False):
416+
def from_breaks(cls, breaks, closed='right', name=None, copy=False,
417+
dtype=None):
406418
"""
407419
Construct an IntervalIndex from an array of splits
408420
@@ -417,6 +429,10 @@ def from_breaks(cls, breaks, closed='right', name=None, copy=False):
417429
Name to be stored in the index.
418430
copy : boolean, default False
419431
copy the data
432+
dtype : dtype or None, default None
433+
If None, dtype will be inferred
434+
435+
..versionadded:: 0.23.0
420436
421437
Examples
422438
--------
@@ -430,18 +446,17 @@ def from_breaks(cls, breaks, closed='right', name=None, copy=False):
430446
interval_range : Function to create a fixed frequency IntervalIndex
431447
IntervalIndex.from_arrays : Construct an IntervalIndex from a left and
432448
right array
433-
IntervalIndex.from_intervals : Construct an IntervalIndex from an array
434-
of Interval objects
435449
IntervalIndex.from_tuples : Construct an IntervalIndex from a
436450
list/array of tuples
437451
"""
438452
breaks = maybe_convert_platform_interval(breaks)
439453

440454
return cls.from_arrays(breaks[:-1], breaks[1:], closed,
441-
name=name, copy=copy)
455+
name=name, copy=copy, dtype=dtype)
442456

443457
@classmethod
444-
def from_arrays(cls, left, right, closed='right', name=None, copy=False):
458+
def from_arrays(cls, left, right, closed='right', name=None, copy=False,
459+
dtype=None):
445460
"""
446461
Construct an IntervalIndex from a a left and right array
447462
@@ -458,6 +473,10 @@ def from_arrays(cls, left, right, closed='right', name=None, copy=False):
458473
Name to be stored in the index.
459474
copy : boolean, default False
460475
copy the data
476+
dtype : dtype or None, default None
477+
If None, dtype will be inferred
478+
479+
..versionadded:: 0.23.0
461480
462481
Examples
463482
--------
@@ -471,22 +490,23 @@ def from_arrays(cls, left, right, closed='right', name=None, copy=False):
471490
interval_range : Function to create a fixed frequency IntervalIndex
472491
IntervalIndex.from_breaks : Construct an IntervalIndex from an array of
473492
splits
474-
IntervalIndex.from_intervals : Construct an IntervalIndex from an array
475-
of Interval objects
476493
IntervalIndex.from_tuples : Construct an IntervalIndex from a
477494
list/array of tuples
478495
"""
479496
left = maybe_convert_platform_interval(left)
480497
right = maybe_convert_platform_interval(right)
481498

482-
return cls._simple_new(left, right, closed, name=name,
483-
copy=copy, verify_integrity=True)
499+
return cls._simple_new(left, right, closed, name=name, copy=copy,
500+
dtype=dtype, verify_integrity=True)
484501

485502
@classmethod
486-
def from_intervals(cls, data, name=None, copy=False):
503+
def from_intervals(cls, data, closed=None, name=None, copy=False,
504+
dtype=None):
487505
"""
488506
Construct an IntervalIndex from a 1d array of Interval objects
489507
508+
.. deprecated:: 0.23.0
509+
490510
Parameters
491511
----------
492512
data : array-like (1-dimensional)
@@ -496,6 +516,10 @@ def from_intervals(cls, data, name=None, copy=False):
496516
Name to be stored in the index.
497517
copy : boolean, default False
498518
by-default copy the data, this is compat only and ignored
519+
dtype : dtype or None, default None
520+
If None, dtype will be inferred
521+
522+
..versionadded:: 0.23.0
499523
500524
Examples
501525
--------
@@ -521,16 +545,14 @@ def from_intervals(cls, data, name=None, copy=False):
521545
IntervalIndex.from_tuples : Construct an IntervalIndex from a
522546
list/array of tuples
523547
"""
524-
if isinstance(data, IntervalIndex):
525-
left, right, closed = data.left, data.right, data.closed
526-
name = name or data.name
527-
else:
528-
data = maybe_convert_platform_interval(data)
529-
left, right, closed = intervals_to_interval_bounds(data)
530-
return cls.from_arrays(left, right, closed, name=name, copy=False)
548+
msg = ('IntervalIndex.from_intervals is deprecated and will be '
549+
'removed in a future version; use IntervalIndex(...) instead')
550+
warnings.warn(msg, FutureWarning, stacklevel=2)
551+
return cls(data, closed=closed, name=name, copy=copy, dtype=dtype)
531552

532553
@classmethod
533-
def from_tuples(cls, data, closed='right', name=None, copy=False):
554+
def from_tuples(cls, data, closed='right', name=None, copy=False,
555+
dtype=None):
534556
"""
535557
Construct an IntervalIndex from a list/array of tuples
536558
@@ -545,10 +567,14 @@ def from_tuples(cls, data, closed='right', name=None, copy=False):
545567
Name to be stored in the index.
546568
copy : boolean, default False
547569
by-default copy the data, this is compat only and ignored
570+
dtype : dtype or None, default None
571+
If None, dtype will be inferred
572+
573+
..versionadded:: 0.23.0
548574
549575
Examples
550576
--------
551-
>>> pd.IntervalIndex.from_tuples([(0, 1), (1,2)])
577+
>>> pd.IntervalIndex.from_tuples([(0, 1), (1, 2)])
552578
IntervalIndex([(0, 1], (1, 2]],
553579
closed='right', dtype='interval[int64]')
554580
@@ -559,8 +585,6 @@ def from_tuples(cls, data, closed='right', name=None, copy=False):
559585
right array
560586
IntervalIndex.from_breaks : Construct an IntervalIndex from an array of
561587
splits
562-
IntervalIndex.from_intervals : Construct an IntervalIndex from an array
563-
of Interval objects
564588
"""
565589
if len(data):
566590
left, right = [], []
@@ -571,15 +595,22 @@ def from_tuples(cls, data, closed='right', name=None, copy=False):
571595
if isna(d):
572596
lhs = rhs = np.nan
573597
else:
574-
lhs, rhs = d
598+
try:
599+
# need list of length 2 tuples, e.g. [(0, 1), (1, 2), ...]
600+
lhs, rhs = d
601+
except ValueError:
602+
msg = ('IntervalIndex.from_tuples requires tuples of '
603+
'length 2, got {tpl}').format(tpl=d)
604+
raise ValueError(msg)
605+
except TypeError:
606+
msg = ('IntervalIndex.from_tuples received an invalid '
607+
'item, {tpl}').format(tpl=d)
608+
raise TypeError(msg)
575609
left.append(lhs)
576610
right.append(rhs)
577611

578-
# TODO
579-
# if we have nulls and we previous had *only*
580-
# integer data, then we have changed the dtype
581-
582-
return cls.from_arrays(left, right, closed, name=name, copy=False)
612+
return cls.from_arrays(left, right, closed, name=name, copy=False,
613+
dtype=dtype)
583614

584615
def to_tuples(self, na_tuple=True):
585616
"""
@@ -921,7 +952,7 @@ def get_loc(self, key, method=None):
921952
Examples
922953
---------
923954
>>> i1, i2 = pd.Interval(0, 1), pd.Interval(1, 2)
924-
>>> index = pd.IntervalIndex.from_intervals([i1, i2])
955+
>>> index = pd.IntervalIndex([i1, i2])
925956
>>> index.get_loc(1)
926957
0
927958
@@ -937,7 +968,7 @@ def get_loc(self, key, method=None):
937968
relevant intervals.
938969
939970
>>> i3 = pd.Interval(0, 2)
940-
>>> overlapping_index = pd.IntervalIndex.from_intervals([i2, i3])
971+
>>> overlapping_index = pd.IntervalIndex([i2, i3])
941972
>>> overlapping_index.get_loc(1.5)
942973
array([0, 1], dtype=int64)
943974
"""

pandas/core/reshape/tile.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,7 @@ def _format_labels(bins, precision, right=True,
348348
# account that we are all right closed
349349
v = adjust(labels[0].left)
350350

351-
i = IntervalIndex.from_intervals(
352-
[Interval(v, labels[0].right, closed='right')])
351+
i = IntervalIndex([Interval(v, labels[0].right, closed='right')])
353352
labels = i.append(labels[1:])
354353

355354
return labels

pandas/tests/categorical/test_constructors.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,7 @@ def test_constructor_unsortable(self):
7676
def test_constructor_interval(self):
7777
result = Categorical([Interval(1, 2), Interval(2, 3), Interval(3, 6)],
7878
ordered=True)
79-
ii = IntervalIndex.from_intervals([Interval(1, 2),
80-
Interval(2, 3),
81-
Interval(3, 6)])
79+
ii = IntervalIndex([Interval(1, 2), Interval(2, 3), Interval(3, 6)])
8280
exp = Categorical(ii, ordered=True)
8381
tm.assert_categorical_equal(result, exp)
8482
tm.assert_index_equal(result.categories, ii)

0 commit comments

Comments
 (0)