Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pandas/core/arrays/datetimelike.py : 23%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from datetime import datetime, timedelta
2import operator
3from typing import Any, Sequence, Type, Union, cast
4import warnings
6import numpy as np
8from pandas._libs import NaT, NaTType, Timestamp, algos, iNaT, lib
9from pandas._libs.tslibs.c_timestamp import integer_op_not_supported
10from pandas._libs.tslibs.period import DIFFERENT_FREQ, IncompatibleFrequency, Period
11from pandas._libs.tslibs.timedeltas import Timedelta, delta_to_nanoseconds
12from pandas._libs.tslibs.timestamps import RoundTo, round_nsint64
13from pandas._typing import DatetimeLikeScalar
14from pandas.compat import set_function_name
15from pandas.compat.numpy import function as nv
16from pandas.errors import AbstractMethodError, NullFrequencyError, PerformanceWarning
17from pandas.util._decorators import Appender, Substitution
18from pandas.util._validators import validate_fillna_kwargs
20from pandas.core.dtypes.common import (
21 is_categorical_dtype,
22 is_datetime64_any_dtype,
23 is_datetime64_dtype,
24 is_datetime64tz_dtype,
25 is_datetime_or_timedelta_dtype,
26 is_dtype_equal,
27 is_float_dtype,
28 is_integer_dtype,
29 is_list_like,
30 is_object_dtype,
31 is_period_dtype,
32 is_string_dtype,
33 is_timedelta64_dtype,
34 is_unsigned_integer_dtype,
35 pandas_dtype,
36)
37from pandas.core.dtypes.generic import ABCSeries
38from pandas.core.dtypes.inference import is_array_like
39from pandas.core.dtypes.missing import is_valid_nat_for_dtype, isna
41from pandas.core import missing, nanops, ops
42from pandas.core.algorithms import checked_add_with_arr, take, unique1d, value_counts
43from pandas.core.arrays.base import ExtensionArray, ExtensionOpsMixin
44import pandas.core.common as com
45from pandas.core.indexers import check_array_indexer
46from pandas.core.ops.common import unpack_zerodim_and_defer
47from pandas.core.ops.invalid import invalid_comparison, make_invalid_op
49from pandas.tseries import frequencies
50from pandas.tseries.offsets import DateOffset, Tick
53def _datetimelike_array_cmp(cls, op):
54 """
55 Wrap comparison operations to convert Timestamp/Timedelta/Period-like to
56 boxed scalars/arrays.
57 """
58 opname = f"__{op.__name__}__"
59 nat_result = opname == "__ne__"
61 @unpack_zerodim_and_defer(opname)
62 def wrapper(self, other):
64 if isinstance(other, str):
65 try:
66 # GH#18435 strings get a pass from tzawareness compat
67 other = self._scalar_from_string(other)
68 except ValueError:
69 # failed to parse as Timestamp/Timedelta/Period
70 return invalid_comparison(self, other, op)
72 if isinstance(other, self._recognized_scalars) or other is NaT:
73 other = self._scalar_type(other)
74 self._check_compatible_with(other)
76 other_i8 = self._unbox_scalar(other)
78 result = op(self.view("i8"), other_i8)
79 if isna(other):
80 result.fill(nat_result)
82 elif not is_list_like(other):
83 return invalid_comparison(self, other, op)
85 elif len(other) != len(self):
86 raise ValueError("Lengths must match")
88 else:
89 if isinstance(other, list):
90 # TODO: could use pd.Index to do inference?
91 other = np.array(other)
93 if not isinstance(other, (np.ndarray, type(self))):
94 return invalid_comparison(self, other, op)
96 if is_object_dtype(other):
97 # We have to use comp_method_OBJECT_ARRAY instead of numpy
98 # comparison otherwise it would fail to raise when
99 # comparing tz-aware and tz-naive
100 with np.errstate(all="ignore"):
101 result = ops.comp_method_OBJECT_ARRAY(
102 op, self.astype(object), other
103 )
104 o_mask = isna(other)
106 elif not type(self)._is_recognized_dtype(other.dtype):
107 return invalid_comparison(self, other, op)
109 else:
110 # For PeriodDType this casting is unnecessary
111 other = type(self)._from_sequence(other)
112 self._check_compatible_with(other)
114 result = op(self.view("i8"), other.view("i8"))
115 o_mask = other._isnan
117 if o_mask.any():
118 result[o_mask] = nat_result
120 if self._hasnans:
121 result[self._isnan] = nat_result
123 return result
125 return set_function_name(wrapper, opname, cls)
128class AttributesMixin:
129 _data: np.ndarray
131 @classmethod
132 def _simple_new(cls, values, **kwargs):
133 raise AbstractMethodError(cls)
135 @property
136 def _scalar_type(self) -> Type[DatetimeLikeScalar]:
137 """The scalar associated with this datelike
139 * PeriodArray : Period
140 * DatetimeArray : Timestamp
141 * TimedeltaArray : Timedelta
142 """
143 raise AbstractMethodError(self)
145 def _scalar_from_string(
146 self, value: str
147 ) -> Union[Period, Timestamp, Timedelta, NaTType]:
148 """
149 Construct a scalar type from a string.
151 Parameters
152 ----------
153 value : str
155 Returns
156 -------
157 Period, Timestamp, or Timedelta, or NaT
158 Whatever the type of ``self._scalar_type`` is.
160 Notes
161 -----
162 This should call ``self._check_compatible_with`` before
163 unboxing the result.
164 """
165 raise AbstractMethodError(self)
167 def _unbox_scalar(self, value: Union[Period, Timestamp, Timedelta, NaTType]) -> int:
168 """
169 Unbox the integer value of a scalar `value`.
171 Parameters
172 ----------
173 value : Union[Period, Timestamp, Timedelta]
175 Returns
176 -------
177 int
179 Examples
180 --------
181 >>> self._unbox_scalar(Timedelta('10s')) # DOCTEST: +SKIP
182 10000000000
183 """
184 raise AbstractMethodError(self)
186 def _check_compatible_with(
187 self, other: Union[Period, Timestamp, Timedelta, NaTType], setitem: bool = False
188 ) -> None:
189 """
190 Verify that `self` and `other` are compatible.
192 * DatetimeArray verifies that the timezones (if any) match
193 * PeriodArray verifies that the freq matches
194 * Timedelta has no verification
196 In each case, NaT is considered compatible.
198 Parameters
199 ----------
200 other
201 setitem : bool, default False
202 For __setitem__ we may have stricter compatiblity resrictions than
203 for comparisons.
205 Raises
206 ------
207 Exception
208 """
209 raise AbstractMethodError(self)
212class DatelikeOps:
213 """
214 Common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex.
215 """
217 @Substitution(
218 URL="https://docs.python.org/3/library/datetime.html"
219 "#strftime-and-strptime-behavior"
220 )
221 def strftime(self, date_format):
222 """
223 Convert to Index using specified date_format.
225 Return an Index of formatted strings specified by date_format, which
226 supports the same string format as the python standard library. Details
227 of the string format can be found in `python string format
228 doc <%(URL)s>`__.
230 Parameters
231 ----------
232 date_format : str
233 Date format string (e.g. "%%Y-%%m-%%d").
235 Returns
236 -------
237 ndarray
238 NumPy ndarray of formatted strings.
240 See Also
241 --------
242 to_datetime : Convert the given argument to datetime.
243 DatetimeIndex.normalize : Return DatetimeIndex with times to midnight.
244 DatetimeIndex.round : Round the DatetimeIndex to the specified freq.
245 DatetimeIndex.floor : Floor the DatetimeIndex to the specified freq.
247 Examples
248 --------
249 >>> rng = pd.date_range(pd.Timestamp("2018-03-10 09:00"),
250 ... periods=3, freq='s')
251 >>> rng.strftime('%%B %%d, %%Y, %%r')
252 Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM',
253 'March 10, 2018, 09:00:02 AM'],
254 dtype='object')
255 """
256 result = self._format_native_types(date_format=date_format, na_rep=np.nan)
257 return result.astype(object)
260class TimelikeOps:
261 """
262 Common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex.
263 """
265 _round_doc = """
266 Perform {op} operation on the data to the specified `freq`.
268 Parameters
269 ----------
270 freq : str or Offset
271 The frequency level to {op} the index to. Must be a fixed
272 frequency like 'S' (second) not 'ME' (month end). See
273 :ref:`frequency aliases <timeseries.offset_aliases>` for
274 a list of possible `freq` values.
275 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
276 Only relevant for DatetimeIndex:
278 - 'infer' will attempt to infer fall dst-transition hours based on
279 order
280 - bool-ndarray where True signifies a DST time, False designates
281 a non-DST time (note that this flag is only applicable for
282 ambiguous times)
283 - 'NaT' will return NaT where there are ambiguous times
284 - 'raise' will raise an AmbiguousTimeError if there are ambiguous
285 times.
287 .. versionadded:: 0.24.0
289 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \
290default 'raise'
291 A nonexistent time does not exist in a particular timezone
292 where clocks moved forward due to DST.
294 - 'shift_forward' will shift the nonexistent time forward to the
295 closest existing time
296 - 'shift_backward' will shift the nonexistent time backward to the
297 closest existing time
298 - 'NaT' will return NaT where there are nonexistent times
299 - timedelta objects will shift nonexistent times by the timedelta
300 - 'raise' will raise an NonExistentTimeError if there are
301 nonexistent times.
303 .. versionadded:: 0.24.0
305 Returns
306 -------
307 DatetimeIndex, TimedeltaIndex, or Series
308 Index of the same type for a DatetimeIndex or TimedeltaIndex,
309 or a Series with the same index for a Series.
311 Raises
312 ------
313 ValueError if the `freq` cannot be converted.
315 Examples
316 --------
317 **DatetimeIndex**
319 >>> rng = pd.date_range('1/1/2018 11:59:00', periods=3, freq='min')
320 >>> rng
321 DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00',
322 '2018-01-01 12:01:00'],
323 dtype='datetime64[ns]', freq='T')
324 """
326 _round_example = """>>> rng.round('H')
327 DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00',
328 '2018-01-01 12:00:00'],
329 dtype='datetime64[ns]', freq=None)
331 **Series**
333 >>> pd.Series(rng).dt.round("H")
334 0 2018-01-01 12:00:00
335 1 2018-01-01 12:00:00
336 2 2018-01-01 12:00:00
337 dtype: datetime64[ns]
338 """
340 _floor_example = """>>> rng.floor('H')
341 DatetimeIndex(['2018-01-01 11:00:00', '2018-01-01 12:00:00',
342 '2018-01-01 12:00:00'],
343 dtype='datetime64[ns]', freq=None)
345 **Series**
347 >>> pd.Series(rng).dt.floor("H")
348 0 2018-01-01 11:00:00
349 1 2018-01-01 12:00:00
350 2 2018-01-01 12:00:00
351 dtype: datetime64[ns]
352 """
354 _ceil_example = """>>> rng.ceil('H')
355 DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00',
356 '2018-01-01 13:00:00'],
357 dtype='datetime64[ns]', freq=None)
359 **Series**
361 >>> pd.Series(rng).dt.ceil("H")
362 0 2018-01-01 12:00:00
363 1 2018-01-01 12:00:00
364 2 2018-01-01 13:00:00
365 dtype: datetime64[ns]
366 """
368 def _round(self, freq, mode, ambiguous, nonexistent):
369 # round the local times
370 if is_datetime64tz_dtype(self):
371 # operate on naive timestamps, then convert back to aware
372 naive = self.tz_localize(None)
373 result = naive._round(freq, mode, ambiguous, nonexistent)
374 aware = result.tz_localize(
375 self.tz, ambiguous=ambiguous, nonexistent=nonexistent
376 )
377 return aware
379 values = self.view("i8")
380 result = round_nsint64(values, mode, freq)
381 result = self._maybe_mask_results(result, fill_value=NaT)
382 return self._simple_new(result, dtype=self.dtype)
384 @Appender((_round_doc + _round_example).format(op="round"))
385 def round(self, freq, ambiguous="raise", nonexistent="raise"):
386 return self._round(freq, RoundTo.NEAREST_HALF_EVEN, ambiguous, nonexistent)
388 @Appender((_round_doc + _floor_example).format(op="floor"))
389 def floor(self, freq, ambiguous="raise", nonexistent="raise"):
390 return self._round(freq, RoundTo.MINUS_INFTY, ambiguous, nonexistent)
392 @Appender((_round_doc + _ceil_example).format(op="ceil"))
393 def ceil(self, freq, ambiguous="raise", nonexistent="raise"):
394 return self._round(freq, RoundTo.PLUS_INFTY, ambiguous, nonexistent)
397class DatetimeLikeArrayMixin(ExtensionOpsMixin, AttributesMixin, ExtensionArray):
398 """
399 Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray
401 Assumes that __new__/__init__ defines:
402 _data
403 _freq
405 and that the inheriting class has methods:
406 _generate_range
407 """
409 @property
410 def ndim(self) -> int:
411 return self._data.ndim
413 @property
414 def shape(self):
415 return self._data.shape
417 def reshape(self, *args, **kwargs):
418 # Note: we drop any freq
419 data = self._data.reshape(*args, **kwargs)
420 return type(self)(data, dtype=self.dtype)
422 def ravel(self, *args, **kwargs):
423 # Note: we drop any freq
424 data = self._data.ravel(*args, **kwargs)
425 return type(self)(data, dtype=self.dtype)
427 @property
428 def _box_func(self):
429 """
430 box function to get object from internal representation
431 """
432 raise AbstractMethodError(self)
434 def _box_values(self, values):
435 """
436 apply box func to passed values
437 """
438 return lib.map_infer(values, self._box_func)
440 def __iter__(self):
441 return (self._box_func(v) for v in self.asi8)
443 @property
444 def asi8(self) -> np.ndarray:
445 """
446 Integer representation of the values.
448 Returns
449 -------
450 ndarray
451 An ndarray with int64 dtype.
452 """
453 # do not cache or you'll create a memory leak
454 return self._data.view("i8")
456 @property
457 def _ndarray_values(self):
458 return self._data
460 # ----------------------------------------------------------------
461 # Rendering Methods
463 def _format_native_types(self, na_rep="NaT", date_format=None):
464 """
465 Helper method for astype when converting to strings.
467 Returns
468 -------
469 ndarray[str]
470 """
471 raise AbstractMethodError(self)
473 def _formatter(self, boxed=False):
474 # TODO: Remove Datetime & DatetimeTZ formatters.
475 return "'{}'".format
477 # ----------------------------------------------------------------
478 # Array-Like / EA-Interface Methods
480 @property
481 def nbytes(self):
482 return self._data.nbytes
484 def __array__(self, dtype=None) -> np.ndarray:
485 # used for Timedelta/DatetimeArray, overwritten by PeriodArray
486 if is_object_dtype(dtype):
487 return np.array(list(self), dtype=object)
488 return self._data
490 @property
491 def size(self) -> int:
492 """The number of elements in this array."""
493 return np.prod(self.shape)
495 def __len__(self) -> int:
496 return len(self._data)
498 def __getitem__(self, key):
499 """
500 This getitem defers to the underlying array, which by-definition can
501 only handle list-likes, slices, and integer scalars
502 """
504 is_int = lib.is_integer(key)
505 if lib.is_scalar(key) and not is_int:
506 raise IndexError(
507 "only integers, slices (`:`), ellipsis (`...`), "
508 "numpy.newaxis (`None`) and integer or boolean "
509 "arrays are valid indices"
510 )
512 getitem = self._data.__getitem__
513 if is_int:
514 val = getitem(key)
515 if lib.is_scalar(val):
516 # i.e. self.ndim == 1
517 return self._box_func(val)
518 return type(self)(val, dtype=self.dtype)
520 if com.is_bool_indexer(key):
521 # first convert to boolean, because check_array_indexer doesn't
522 # allow object dtype
523 if is_object_dtype(key):
524 key = np.asarray(key, dtype=bool)
526 key = check_array_indexer(self, key)
527 if key.all():
528 key = slice(0, None, None)
529 else:
530 key = lib.maybe_booleans_to_slice(key.view(np.uint8))
531 elif isinstance(key, list) and len(key) == 1 and isinstance(key[0], slice):
532 # see https://github.com/pandas-dev/pandas/issues/31299, need to allow
533 # this for now (would otherwise raise in check_array_indexer)
534 pass
535 else:
536 key = check_array_indexer(self, key)
538 is_period = is_period_dtype(self)
539 if is_period:
540 freq = self.freq
541 else:
542 freq = None
543 if isinstance(key, slice):
544 if self.freq is not None and key.step is not None:
545 freq = key.step * self.freq
546 else:
547 freq = self.freq
548 elif key is Ellipsis:
549 # GH#21282 indexing with Ellipsis is similar to a full slice,
550 # should preserve `freq` attribute
551 freq = self.freq
553 result = getitem(key)
554 if result.ndim > 1:
555 # To support MPL which performs slicing with 2 dim
556 # even though it only has 1 dim by definition
557 return result
559 return self._simple_new(result, dtype=self.dtype, freq=freq)
561 def __setitem__(
562 self,
563 key: Union[int, Sequence[int], Sequence[bool], slice],
564 value: Union[NaTType, Any, Sequence[Any]],
565 ) -> None:
566 # I'm fudging the types a bit here. "Any" above really depends
567 # on type(self). For PeriodArray, it's Period (or stuff coercible
568 # to a period in from_sequence). For DatetimeArray, it's Timestamp...
569 # I don't know if mypy can do that, possibly with Generics.
570 # https://mypy.readthedocs.io/en/latest/generics.html
571 if lib.is_scalar(value) and not isna(value):
572 value = com.maybe_box_datetimelike(value)
574 if is_list_like(value):
575 is_slice = isinstance(key, slice)
577 if lib.is_scalar(key):
578 raise ValueError("setting an array element with a sequence.")
580 if not is_slice:
581 key = cast(Sequence, key)
582 if len(key) != len(value) and not com.is_bool_indexer(key):
583 msg = (
584 f"shape mismatch: value array of length '{len(key)}' "
585 "does not match indexing result of length "
586 f"'{len(value)}'."
587 )
588 raise ValueError(msg)
589 elif not len(key):
590 return
592 value = type(self)._from_sequence(value, dtype=self.dtype)
593 self._check_compatible_with(value, setitem=True)
594 value = value.asi8
595 elif isinstance(value, self._scalar_type):
596 self._check_compatible_with(value, setitem=True)
597 value = self._unbox_scalar(value)
598 elif is_valid_nat_for_dtype(value, self.dtype):
599 value = iNaT
600 else:
601 msg = (
602 f"'value' should be a '{self._scalar_type.__name__}', 'NaT', "
603 f"or array of those. Got '{type(value).__name__}' instead."
604 )
605 raise TypeError(msg)
607 key = check_array_indexer(self, key)
608 self._data[key] = value
609 self._maybe_clear_freq()
611 def _maybe_clear_freq(self):
612 # inplace operations like __setitem__ may invalidate the freq of
613 # DatetimeArray and TimedeltaArray
614 pass
616 def astype(self, dtype, copy=True):
617 # Some notes on cases we don't have to handle here in the base class:
618 # 1. PeriodArray.astype handles period -> period
619 # 2. DatetimeArray.astype handles conversion between tz.
620 # 3. DatetimeArray.astype handles datetime -> period
621 from pandas import Categorical
623 dtype = pandas_dtype(dtype)
625 if is_object_dtype(dtype):
626 return self._box_values(self.asi8)
627 elif is_string_dtype(dtype) and not is_categorical_dtype(dtype):
628 return self._format_native_types()
629 elif is_integer_dtype(dtype):
630 # we deliberately ignore int32 vs. int64 here.
631 # See https://github.com/pandas-dev/pandas/issues/24381 for more.
632 values = self.asi8
634 if is_unsigned_integer_dtype(dtype):
635 # Again, we ignore int32 vs. int64
636 values = values.view("uint64")
638 if copy:
639 values = values.copy()
640 return values
641 elif (
642 is_datetime_or_timedelta_dtype(dtype)
643 and not is_dtype_equal(self.dtype, dtype)
644 ) or is_float_dtype(dtype):
645 # disallow conversion between datetime/timedelta,
646 # and conversions for any datetimelike to float
647 msg = f"Cannot cast {type(self).__name__} to dtype {dtype}"
648 raise TypeError(msg)
649 elif is_categorical_dtype(dtype):
650 return Categorical(self, dtype=dtype)
651 else:
652 return np.asarray(self, dtype=dtype)
654 def view(self, dtype=None):
655 if dtype is None or dtype is self.dtype:
656 return type(self)(self._data, dtype=self.dtype)
657 return self._data.view(dtype=dtype)
659 # ------------------------------------------------------------------
660 # ExtensionArray Interface
662 def unique(self):
663 result = unique1d(self.asi8)
664 return type(self)(result, dtype=self.dtype)
666 def _validate_fill_value(self, fill_value):
667 """
668 If a fill_value is passed to `take` convert it to an i8 representation,
669 raising ValueError if this is not possible.
671 Parameters
672 ----------
673 fill_value : object
675 Returns
676 -------
677 fill_value : np.int64
679 Raises
680 ------
681 ValueError
682 """
683 if isna(fill_value):
684 fill_value = iNaT
685 elif isinstance(fill_value, self._recognized_scalars):
686 self._check_compatible_with(fill_value)
687 fill_value = self._scalar_type(fill_value)
688 fill_value = self._unbox_scalar(fill_value)
689 else:
690 raise ValueError(
691 f"'fill_value' should be a {self._scalar_type}. Got '{fill_value}'."
692 )
693 return fill_value
695 def take(self, indices, allow_fill=False, fill_value=None):
696 if allow_fill:
697 fill_value = self._validate_fill_value(fill_value)
699 new_values = take(
700 self.asi8, indices, allow_fill=allow_fill, fill_value=fill_value
701 )
703 return type(self)(new_values, dtype=self.dtype)
705 @classmethod
706 def _concat_same_type(cls, to_concat):
707 dtypes = {x.dtype for x in to_concat}
708 assert len(dtypes) == 1
709 dtype = list(dtypes)[0]
711 values = np.concatenate([x.asi8 for x in to_concat])
712 return cls(values, dtype=dtype)
714 def copy(self):
715 values = self.asi8.copy()
716 return type(self)._simple_new(values, dtype=self.dtype, freq=self.freq)
718 def _values_for_factorize(self):
719 return self.asi8, iNaT
721 @classmethod
722 def _from_factorized(cls, values, original):
723 return cls(values, dtype=original.dtype)
725 def _values_for_argsort(self):
726 return self._data
728 @Appender(ExtensionArray.shift.__doc__)
729 def shift(self, periods=1, fill_value=None, axis=0):
730 if not self.size or periods == 0:
731 return self.copy()
733 if is_valid_nat_for_dtype(fill_value, self.dtype):
734 fill_value = NaT
735 elif not isinstance(fill_value, self._recognized_scalars):
736 # only warn if we're not going to raise
737 if self._scalar_type is Period and lib.is_integer(fill_value):
738 # kludge for #31971 since Period(integer) tries to cast to str
739 new_fill = Period._from_ordinal(fill_value, freq=self.freq)
740 else:
741 new_fill = self._scalar_type(fill_value)
743 # stacklevel here is chosen to be correct when called from
744 # DataFrame.shift or Series.shift
745 warnings.warn(
746 f"Passing {type(fill_value)} to shift is deprecated and "
747 "will raise in a future version, pass "
748 f"{self._scalar_type.__name__} instead.",
749 FutureWarning,
750 stacklevel=7,
751 )
752 fill_value = new_fill
754 fill_value = self._unbox_scalar(fill_value)
756 new_values = self._data
758 # make sure array sent to np.roll is c_contiguous
759 f_ordered = new_values.flags.f_contiguous
760 if f_ordered:
761 new_values = new_values.T
762 axis = new_values.ndim - axis - 1
764 new_values = np.roll(new_values, periods, axis=axis)
766 axis_indexer = [slice(None)] * self.ndim
767 if periods > 0:
768 axis_indexer[axis] = slice(None, periods)
769 else:
770 axis_indexer[axis] = slice(periods, None)
771 new_values[tuple(axis_indexer)] = fill_value
773 # restore original order
774 if f_ordered:
775 new_values = new_values.T
777 return type(self)._simple_new(new_values, dtype=self.dtype)
779 # ------------------------------------------------------------------
780 # Additional array methods
781 # These are not part of the EA API, but we implement them because
782 # pandas assumes they're there.
784 def searchsorted(self, value, side="left", sorter=None):
785 """
786 Find indices where elements should be inserted to maintain order.
788 Find the indices into a sorted array `self` such that, if the
789 corresponding elements in `value` were inserted before the indices,
790 the order of `self` would be preserved.
792 Parameters
793 ----------
794 value : array_like
795 Values to insert into `self`.
796 side : {'left', 'right'}, optional
797 If 'left', the index of the first suitable location found is given.
798 If 'right', return the last such index. If there is no suitable
799 index, return either 0 or N (where N is the length of `self`).
800 sorter : 1-D array_like, optional
801 Optional array of integer indices that sort `self` into ascending
802 order. They are typically the result of ``np.argsort``.
804 Returns
805 -------
806 indices : array of ints
807 Array of insertion points with the same shape as `value`.
808 """
809 if isinstance(value, str):
810 value = self._scalar_from_string(value)
812 if not (isinstance(value, (self._scalar_type, type(self))) or isna(value)):
813 raise ValueError(f"Unexpected type for 'value': {type(value)}")
815 self._check_compatible_with(value)
816 if isinstance(value, type(self)):
817 value = value.asi8
818 else:
819 value = self._unbox_scalar(value)
821 return self.asi8.searchsorted(value, side=side, sorter=sorter)
823 def repeat(self, repeats, *args, **kwargs):
824 """
825 Repeat elements of an array.
827 See Also
828 --------
829 numpy.ndarray.repeat
830 """
831 nv.validate_repeat(args, kwargs)
832 values = self._data.repeat(repeats)
833 return type(self)(values.view("i8"), dtype=self.dtype)
835 def value_counts(self, dropna=False):
836 """
837 Return a Series containing counts of unique values.
839 Parameters
840 ----------
841 dropna : bool, default True
842 Don't include counts of NaT values.
844 Returns
845 -------
846 Series
847 """
848 from pandas import Series, Index
850 if dropna:
851 values = self[~self.isna()]._data
852 else:
853 values = self._data
855 cls = type(self)
857 result = value_counts(values, sort=False, dropna=dropna)
858 index = Index(
859 cls(result.index.view("i8"), dtype=self.dtype), name=result.index.name
860 )
861 return Series(result.values, index=index, name=result.name)
863 def map(self, mapper):
864 # TODO(GH-23179): Add ExtensionArray.map
865 # Need to figure out if we want ExtensionArray.map first.
866 # If so, then we can refactor IndexOpsMixin._map_values to
867 # a standalone function and call from here..
868 # Else, just rewrite _map_infer_values to do the right thing.
869 from pandas import Index
871 return Index(self).map(mapper).array
873 # ------------------------------------------------------------------
874 # Null Handling
876 def isna(self):
877 return self._isnan
879 @property # NB: override with cache_readonly in immutable subclasses
880 def _isnan(self):
881 """
882 return if each value is nan
883 """
884 return self.asi8 == iNaT
886 @property # NB: override with cache_readonly in immutable subclasses
887 def _hasnans(self):
888 """
889 return if I have any nans; enables various perf speedups
890 """
891 return bool(self._isnan.any())
893 def _maybe_mask_results(self, result, fill_value=iNaT, convert=None):
894 """
895 Parameters
896 ----------
897 result : a ndarray
898 fill_value : object, default iNaT
899 convert : str, dtype or None
901 Returns
902 -------
903 result : ndarray with values replace by the fill_value
905 mask the result if needed, convert to the provided dtype if its not
906 None
908 This is an internal routine.
909 """
911 if self._hasnans:
912 if convert:
913 result = result.astype(convert)
914 if fill_value is None:
915 fill_value = np.nan
916 result[self._isnan] = fill_value
917 return result
919 def fillna(self, value=None, method=None, limit=None):
920 # TODO(GH-20300): remove this
921 # Just overriding to ensure that we avoid an astype(object).
922 # Either 20300 or a `_values_for_fillna` would avoid this duplication.
923 if isinstance(value, ABCSeries):
924 value = value.array
926 value, method = validate_fillna_kwargs(value, method)
928 mask = self.isna()
930 if is_array_like(value):
931 if len(value) != len(self):
932 raise ValueError(
933 f"Length of 'value' does not match. Got ({len(value)}) "
934 f" expected {len(self)}"
935 )
936 value = value[mask]
938 if mask.any():
939 if method is not None:
940 if method == "pad":
941 func = missing.pad_1d
942 else:
943 func = missing.backfill_1d
945 values = self._data
946 if not is_period_dtype(self):
947 # For PeriodArray self._data is i8, which gets copied
948 # by `func`. Otherwise we need to make a copy manually
949 # to avoid modifying `self` in-place.
950 values = values.copy()
952 new_values = func(values, limit=limit, mask=mask)
953 if is_datetime64tz_dtype(self):
954 # we need to pass int64 values to the constructor to avoid
955 # re-localizing incorrectly
956 new_values = new_values.view("i8")
957 new_values = type(self)(new_values, dtype=self.dtype)
958 else:
959 # fill with value
960 new_values = self.copy()
961 new_values[mask] = value
962 else:
963 new_values = self.copy()
964 return new_values
966 # ------------------------------------------------------------------
967 # Frequency Properties/Methods
969 @property
970 def freq(self):
971 """
972 Return the frequency object if it is set, otherwise None.
973 """
974 return self._freq
976 @freq.setter
977 def freq(self, value):
978 if value is not None:
979 value = frequencies.to_offset(value)
980 self._validate_frequency(self, value)
982 self._freq = value
984 @property
985 def freqstr(self):
986 """
987 Return the frequency object as a string if its set, otherwise None
988 """
989 if self.freq is None:
990 return None
991 return self.freq.freqstr
993 @property # NB: override with cache_readonly in immutable subclasses
994 def inferred_freq(self):
995 """
996 Tryies to return a string representing a frequency guess,
997 generated by infer_freq. Returns None if it can't autodetect the
998 frequency.
999 """
1000 if self.ndim != 1:
1001 return None
1002 try:
1003 return frequencies.infer_freq(self)
1004 except ValueError:
1005 return None
1007 @property # NB: override with cache_readonly in immutable subclasses
1008 def _resolution(self):
1009 return frequencies.Resolution.get_reso_from_freq(self.freqstr)
1011 @property # NB: override with cache_readonly in immutable subclasses
1012 def resolution(self):
1013 """
1014 Returns day, hour, minute, second, millisecond or microsecond
1015 """
1016 return frequencies.Resolution.get_str(self._resolution)
1018 @classmethod
1019 def _validate_frequency(cls, index, freq, **kwargs):
1020 """
1021 Validate that a frequency is compatible with the values of a given
1022 Datetime Array/Index or Timedelta Array/Index
1024 Parameters
1025 ----------
1026 index : DatetimeIndex or TimedeltaIndex
1027 The index on which to determine if the given frequency is valid
1028 freq : DateOffset
1029 The frequency to validate
1030 """
1031 if is_period_dtype(cls):
1032 # Frequency validation is not meaningful for Period Array/Index
1033 return None
1035 inferred = index.inferred_freq
1036 if index.size == 0 or inferred == freq.freqstr:
1037 return None
1039 try:
1040 on_freq = cls._generate_range(
1041 start=index[0], end=None, periods=len(index), freq=freq, **kwargs
1042 )
1043 if not np.array_equal(index.asi8, on_freq.asi8):
1044 raise ValueError
1045 except ValueError as e:
1046 if "non-fixed" in str(e):
1047 # non-fixed frequencies are not meaningful for timedelta64;
1048 # we retain that error message
1049 raise e
1050 # GH#11587 the main way this is reached is if the `np.array_equal`
1051 # check above is False. This can also be reached if index[0]
1052 # is `NaT`, in which case the call to `cls._generate_range` will
1053 # raise a ValueError, which we re-raise with a more targeted
1054 # message.
1055 raise ValueError(
1056 f"Inferred frequency {inferred} from passed values "
1057 f"does not conform to passed frequency {freq.freqstr}"
1058 )
1060 # monotonicity/uniqueness properties are called via frequencies.infer_freq,
1061 # see GH#23789
1063 @property
1064 def _is_monotonic_increasing(self):
1065 return algos.is_monotonic(self.asi8, timelike=True)[0]
1067 @property
1068 def _is_monotonic_decreasing(self):
1069 return algos.is_monotonic(self.asi8, timelike=True)[1]
1071 @property
1072 def _is_unique(self):
1073 return len(unique1d(self.asi8)) == len(self)
1075 # ------------------------------------------------------------------
1076 # Arithmetic Methods
1077 _create_comparison_method = classmethod(_datetimelike_array_cmp)
1079 # pow is invalid for all three subclasses; TimedeltaArray will override
1080 # the multiplication and division ops
1081 __pow__ = make_invalid_op("__pow__")
1082 __rpow__ = make_invalid_op("__rpow__")
1083 __mul__ = make_invalid_op("__mul__")
1084 __rmul__ = make_invalid_op("__rmul__")
1085 __truediv__ = make_invalid_op("__truediv__")
1086 __rtruediv__ = make_invalid_op("__rtruediv__")
1087 __floordiv__ = make_invalid_op("__floordiv__")
1088 __rfloordiv__ = make_invalid_op("__rfloordiv__")
1089 __mod__ = make_invalid_op("__mod__")
1090 __rmod__ = make_invalid_op("__rmod__")
1091 __divmod__ = make_invalid_op("__divmod__")
1092 __rdivmod__ = make_invalid_op("__rdivmod__")
1094 def _add_datetimelike_scalar(self, other):
1095 # Overridden by TimedeltaArray
1096 raise TypeError(f"cannot add {type(self).__name__} and {type(other).__name__}")
1098 _add_datetime_arraylike = _add_datetimelike_scalar
1100 def _sub_datetimelike_scalar(self, other):
1101 # Overridden by DatetimeArray
1102 assert other is not NaT
1103 raise TypeError(f"cannot subtract a datelike from a {type(self).__name__}")
1105 _sub_datetime_arraylike = _sub_datetimelike_scalar
1107 def _sub_period(self, other):
1108 # Overridden by PeriodArray
1109 raise TypeError(f"cannot subtract Period from a {type(self).__name__}")
1111 def _add_offset(self, offset):
1112 raise AbstractMethodError(self)
1114 def _add_delta(self, other):
1115 """
1116 Add a timedelta-like, Tick or TimedeltaIndex-like object
1117 to self, yielding an int64 numpy array
1119 Parameters
1120 ----------
1121 delta : {timedelta, np.timedelta64, Tick,
1122 TimedeltaIndex, ndarray[timedelta64]}
1124 Returns
1125 -------
1126 result : ndarray[int64]
1128 Notes
1129 -----
1130 The result's name is set outside of _add_delta by the calling
1131 method (__add__ or __sub__), if necessary (i.e. for Indexes).
1132 """
1133 if isinstance(other, (Tick, timedelta, np.timedelta64)):
1134 new_values = self._add_timedeltalike_scalar(other)
1135 elif is_timedelta64_dtype(other):
1136 # ndarray[timedelta64] or TimedeltaArray/index
1137 new_values = self._add_delta_tdi(other)
1139 return new_values
1141 def _add_timedeltalike_scalar(self, other):
1142 """
1143 Add a delta of a timedeltalike
1144 return the i8 result view
1145 """
1146 if isna(other):
1147 # i.e np.timedelta64("NaT"), not recognized by delta_to_nanoseconds
1148 new_values = np.empty(self.shape, dtype="i8")
1149 new_values[:] = iNaT
1150 return new_values
1152 inc = delta_to_nanoseconds(other)
1153 new_values = checked_add_with_arr(self.asi8, inc, arr_mask=self._isnan).view(
1154 "i8"
1155 )
1156 new_values = self._maybe_mask_results(new_values)
1157 return new_values.view("i8")
1159 def _add_delta_tdi(self, other):
1160 """
1161 Add a delta of a TimedeltaIndex
1162 return the i8 result view
1163 """
1164 if len(self) != len(other):
1165 raise ValueError("cannot add indices of unequal length")
1167 if isinstance(other, np.ndarray):
1168 # ndarray[timedelta64]; wrap in TimedeltaIndex for op
1169 from pandas.core.arrays import TimedeltaArray
1171 other = TimedeltaArray._from_sequence(other)
1173 self_i8 = self.asi8
1174 other_i8 = other.asi8
1175 new_values = checked_add_with_arr(
1176 self_i8, other_i8, arr_mask=self._isnan, b_mask=other._isnan
1177 )
1178 if self._hasnans or other._hasnans:
1179 mask = (self._isnan) | (other._isnan)
1180 new_values[mask] = iNaT
1181 return new_values.view("i8")
1183 def _add_nat(self):
1184 """
1185 Add pd.NaT to self
1186 """
1187 if is_period_dtype(self):
1188 raise TypeError(
1189 f"Cannot add {type(self).__name__} and {type(NaT).__name__}"
1190 )
1192 # GH#19124 pd.NaT is treated like a timedelta for both timedelta
1193 # and datetime dtypes
1194 result = np.zeros(self.shape, dtype=np.int64)
1195 result.fill(iNaT)
1196 return type(self)(result, dtype=self.dtype, freq=None)
1198 def _sub_nat(self):
1199 """
1200 Subtract pd.NaT from self
1201 """
1202 # GH#19124 Timedelta - datetime is not in general well-defined.
1203 # We make an exception for pd.NaT, which in this case quacks
1204 # like a timedelta.
1205 # For datetime64 dtypes by convention we treat NaT as a datetime, so
1206 # this subtraction returns a timedelta64 dtype.
1207 # For period dtype, timedelta64 is a close-enough return dtype.
1208 result = np.zeros(self.shape, dtype=np.int64)
1209 result.fill(iNaT)
1210 return result.view("timedelta64[ns]")
1212 def _sub_period_array(self, other):
1213 """
1214 Subtract a Period Array/Index from self. This is only valid if self
1215 is itself a Period Array/Index, raises otherwise. Both objects must
1216 have the same frequency.
1218 Parameters
1219 ----------
1220 other : PeriodIndex or PeriodArray
1222 Returns
1223 -------
1224 result : np.ndarray[object]
1225 Array of DateOffset objects; nulls represented by NaT.
1226 """
1227 if not is_period_dtype(self):
1228 raise TypeError(
1229 f"cannot subtract {other.dtype}-dtype from {type(self).__name__}"
1230 )
1232 if self.freq != other.freq:
1233 msg = DIFFERENT_FREQ.format(
1234 cls=type(self).__name__, own_freq=self.freqstr, other_freq=other.freqstr
1235 )
1236 raise IncompatibleFrequency(msg)
1238 new_values = checked_add_with_arr(
1239 self.asi8, -other.asi8, arr_mask=self._isnan, b_mask=other._isnan
1240 )
1242 new_values = np.array([self.freq.base * x for x in new_values])
1243 if self._hasnans or other._hasnans:
1244 mask = (self._isnan) | (other._isnan)
1245 new_values[mask] = NaT
1246 return new_values
1248 def _addsub_object_array(self, other: np.ndarray, op):
1249 """
1250 Add or subtract array-like of DateOffset objects
1252 Parameters
1253 ----------
1254 other : np.ndarray[object]
1255 op : {operator.add, operator.sub}
1257 Returns
1258 -------
1259 result : same class as self
1260 """
1261 assert op in [operator.add, operator.sub]
1262 if len(other) == 1:
1263 return op(self, other[0])
1265 warnings.warn(
1266 "Adding/subtracting array of DateOffsets to "
1267 f"{type(self).__name__} not vectorized",
1268 PerformanceWarning,
1269 )
1271 # For EA self.astype('O') returns a numpy array, not an Index
1272 left = self.astype("O")
1274 res_values = op(left, np.array(other))
1275 kwargs = {}
1276 if not is_period_dtype(self):
1277 kwargs["freq"] = "infer"
1278 try:
1279 res = type(self)._from_sequence(res_values, **kwargs)
1280 except ValueError:
1281 # e.g. we've passed a Timestamp to TimedeltaArray
1282 res = res_values
1283 return res
1285 def _time_shift(self, periods, freq=None):
1286 """
1287 Shift each value by `periods`.
1289 Note this is different from ExtensionArray.shift, which
1290 shifts the *position* of each element, padding the end with
1291 missing values.
1293 Parameters
1294 ----------
1295 periods : int
1296 Number of periods to shift by.
1297 freq : pandas.DateOffset, pandas.Timedelta, or str
1298 Frequency increment to shift by.
1299 """
1300 if freq is not None and freq != self.freq:
1301 if isinstance(freq, str):
1302 freq = frequencies.to_offset(freq)
1303 offset = periods * freq
1304 result = self + offset
1305 return result
1307 if periods == 0:
1308 # immutable so OK
1309 return self.copy()
1311 if self.freq is None:
1312 raise NullFrequencyError("Cannot shift with no freq")
1314 start = self[0] + periods * self.freq
1315 end = self[-1] + periods * self.freq
1317 # Note: in the DatetimeTZ case, _generate_range will infer the
1318 # appropriate timezone from `start` and `end`, so tz does not need
1319 # to be passed explicitly.
1320 return self._generate_range(start=start, end=end, periods=None, freq=self.freq)
1322 @unpack_zerodim_and_defer("__add__")
1323 def __add__(self, other):
1325 # scalar others
1326 if other is NaT:
1327 result = self._add_nat()
1328 elif isinstance(other, (Tick, timedelta, np.timedelta64)):
1329 result = self._add_delta(other)
1330 elif isinstance(other, DateOffset):
1331 # specifically _not_ a Tick
1332 result = self._add_offset(other)
1333 elif isinstance(other, (datetime, np.datetime64)):
1334 result = self._add_datetimelike_scalar(other)
1335 elif lib.is_integer(other):
1336 # This check must come after the check for np.timedelta64
1337 # as is_integer returns True for these
1338 if not is_period_dtype(self):
1339 raise integer_op_not_supported(self)
1340 result = self._time_shift(other)
1342 # array-like others
1343 elif is_timedelta64_dtype(other):
1344 # TimedeltaIndex, ndarray[timedelta64]
1345 result = self._add_delta(other)
1346 elif is_object_dtype(other):
1347 # e.g. Array/Index of DateOffset objects
1348 result = self._addsub_object_array(other, operator.add)
1349 elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
1350 # DatetimeIndex, ndarray[datetime64]
1351 return self._add_datetime_arraylike(other)
1352 elif is_integer_dtype(other):
1353 if not is_period_dtype(self):
1354 raise integer_op_not_supported(self)
1355 result = self._addsub_int_array(other, operator.add)
1356 else:
1357 # Includes Categorical, other ExtensionArrays
1358 # For PeriodDtype, if self is a TimedeltaArray and other is a
1359 # PeriodArray with a timedelta-like (i.e. Tick) freq, this
1360 # operation is valid. Defer to the PeriodArray implementation.
1361 # In remaining cases, this will end up raising TypeError.
1362 return NotImplemented
1364 if is_timedelta64_dtype(result) and isinstance(result, np.ndarray):
1365 from pandas.core.arrays import TimedeltaArray
1367 return TimedeltaArray(result)
1368 return result
1370 def __radd__(self, other):
1371 # alias for __add__
1372 return self.__add__(other)
1374 @unpack_zerodim_and_defer("__sub__")
1375 def __sub__(self, other):
1377 # scalar others
1378 if other is NaT:
1379 result = self._sub_nat()
1380 elif isinstance(other, (Tick, timedelta, np.timedelta64)):
1381 result = self._add_delta(-other)
1382 elif isinstance(other, DateOffset):
1383 # specifically _not_ a Tick
1384 result = self._add_offset(-other)
1385 elif isinstance(other, (datetime, np.datetime64)):
1386 result = self._sub_datetimelike_scalar(other)
1387 elif lib.is_integer(other):
1388 # This check must come after the check for np.timedelta64
1389 # as is_integer returns True for these
1390 if not is_period_dtype(self):
1391 raise integer_op_not_supported(self)
1392 result = self._time_shift(-other)
1394 elif isinstance(other, Period):
1395 result = self._sub_period(other)
1397 # array-like others
1398 elif is_timedelta64_dtype(other):
1399 # TimedeltaIndex, ndarray[timedelta64]
1400 result = self._add_delta(-other)
1401 elif is_object_dtype(other):
1402 # e.g. Array/Index of DateOffset objects
1403 result = self._addsub_object_array(other, operator.sub)
1404 elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
1405 # DatetimeIndex, ndarray[datetime64]
1406 result = self._sub_datetime_arraylike(other)
1407 elif is_period_dtype(other):
1408 # PeriodIndex
1409 result = self._sub_period_array(other)
1410 elif is_integer_dtype(other):
1411 if not is_period_dtype(self):
1412 raise integer_op_not_supported(self)
1413 result = self._addsub_int_array(other, operator.sub)
1414 else:
1415 # Includes ExtensionArrays, float_dtype
1416 return NotImplemented
1418 if is_timedelta64_dtype(result) and isinstance(result, np.ndarray):
1419 from pandas.core.arrays import TimedeltaArray
1421 return TimedeltaArray(result)
1422 return result
1424 def __rsub__(self, other):
1425 if is_datetime64_any_dtype(other) and is_timedelta64_dtype(self.dtype):
1426 # ndarray[datetime64] cannot be subtracted from self, so
1427 # we need to wrap in DatetimeArray/Index and flip the operation
1428 if lib.is_scalar(other):
1429 # i.e. np.datetime64 object
1430 return Timestamp(other) - self
1431 if not isinstance(other, DatetimeLikeArrayMixin):
1432 # Avoid down-casting DatetimeIndex
1433 from pandas.core.arrays import DatetimeArray
1435 other = DatetimeArray(other)
1436 return other - self
1437 elif (
1438 is_datetime64_any_dtype(self.dtype)
1439 and hasattr(other, "dtype")
1440 and not is_datetime64_any_dtype(other.dtype)
1441 ):
1442 # GH#19959 datetime - datetime is well-defined as timedelta,
1443 # but any other type - datetime is not well-defined.
1444 raise TypeError(
1445 f"cannot subtract {type(self).__name__} from {type(other).__name__}"
1446 )
1447 elif is_period_dtype(self.dtype) and is_timedelta64_dtype(other):
1448 # TODO: Can we simplify/generalize these cases at all?
1449 raise TypeError(f"cannot subtract {type(self).__name__} from {other.dtype}")
1450 elif is_timedelta64_dtype(self.dtype):
1451 if lib.is_integer(other) or is_integer_dtype(other):
1452 # need to subtract before negating, since that flips freq
1453 # -self flips self.freq, messing up results
1454 return -(self - other)
1456 return (-self) + other
1458 return -(self - other)
1460 def __iadd__(self, other): # type: ignore
1461 result = self + other
1462 self[:] = result[:]
1464 if not is_period_dtype(self):
1465 # restore freq, which is invalidated by setitem
1466 self._freq = result._freq
1467 return self
1469 def __isub__(self, other): # type: ignore
1470 result = self - other
1471 self[:] = result[:]
1473 if not is_period_dtype(self):
1474 # restore freq, which is invalidated by setitem
1475 self._freq = result._freq
1476 return self
1478 # --------------------------------------------------------------
1479 # Reductions
1481 def _reduce(self, name, axis=0, skipna=True, **kwargs):
1482 op = getattr(self, name, None)
1483 if op:
1484 return op(skipna=skipna, **kwargs)
1485 else:
1486 return super()._reduce(name, skipna, **kwargs)
1488 def min(self, axis=None, skipna=True, *args, **kwargs):
1489 """
1490 Return the minimum value of the Array or minimum along
1491 an axis.
1493 See Also
1494 --------
1495 numpy.ndarray.min
1496 Index.min : Return the minimum value in an Index.
1497 Series.min : Return the minimum value in a Series.
1498 """
1499 nv.validate_min(args, kwargs)
1500 nv.validate_minmax_axis(axis)
1502 result = nanops.nanmin(self.asi8, skipna=skipna, mask=self.isna())
1503 if isna(result):
1504 # Period._from_ordinal does not handle np.nan gracefully
1505 return NaT
1506 return self._box_func(result)
1508 def max(self, axis=None, skipna=True, *args, **kwargs):
1509 """
1510 Return the maximum value of the Array or maximum along
1511 an axis.
1513 See Also
1514 --------
1515 numpy.ndarray.max
1516 Index.max : Return the maximum value in an Index.
1517 Series.max : Return the maximum value in a Series.
1518 """
1519 # TODO: skipna is broken with max.
1520 # See https://github.com/pandas-dev/pandas/issues/24265
1521 nv.validate_max(args, kwargs)
1522 nv.validate_minmax_axis(axis)
1524 mask = self.isna()
1525 if skipna:
1526 values = self[~mask].asi8
1527 elif mask.any():
1528 return NaT
1529 else:
1530 values = self.asi8
1532 if not len(values):
1533 # short-circuit for empty max / min
1534 return NaT
1536 result = nanops.nanmax(values, skipna=skipna)
1537 # Don't have to worry about NA `result`, since no NA went in.
1538 return self._box_func(result)
1540 def mean(self, skipna=True):
1541 """
1542 Return the mean value of the Array.
1544 .. versionadded:: 0.25.0
1546 Parameters
1547 ----------
1548 skipna : bool, default True
1549 Whether to ignore any NaT elements.
1551 Returns
1552 -------
1553 scalar
1554 Timestamp or Timedelta.
1556 See Also
1557 --------
1558 numpy.ndarray.mean : Returns the average of array elements along a given axis.
1559 Series.mean : Return the mean value in a Series.
1561 Notes
1562 -----
1563 mean is only defined for Datetime and Timedelta dtypes, not for Period.
1564 """
1565 if is_period_dtype(self):
1566 # See discussion in GH#24757
1567 raise TypeError(
1568 f"mean is not implemented for {type(self).__name__} since the "
1569 "meaning is ambiguous. An alternative is "
1570 "obj.to_timestamp(how='start').mean()"
1571 )
1573 mask = self.isna()
1574 if skipna:
1575 values = self[~mask]
1576 elif mask.any():
1577 return NaT
1578 else:
1579 values = self
1581 if not len(values):
1582 # short-circuit for empty max / min
1583 return NaT
1585 result = nanops.nanmean(values.view("i8"), skipna=skipna)
1586 # Don't have to worry about NA `result`, since no NA went in.
1587 return self._box_func(result)
1590DatetimeLikeArrayMixin._add_comparison_ops()
1592# -------------------------------------------------------------------
1593# Shared Constructor Helpers
1596def validate_periods(periods):
1597 """
1598 If a `periods` argument is passed to the Datetime/Timedelta Array/Index
1599 constructor, cast it to an integer.
1601 Parameters
1602 ----------
1603 periods : None, float, int
1605 Returns
1606 -------
1607 periods : None or int
1609 Raises
1610 ------
1611 TypeError
1612 if periods is None, float, or int
1613 """
1614 if periods is not None:
1615 if lib.is_float(periods):
1616 periods = int(periods)
1617 elif not lib.is_integer(periods):
1618 raise TypeError(f"periods must be a number, got {periods}")
1619 return periods
1622def validate_endpoints(closed):
1623 """
1624 Check that the `closed` argument is among [None, "left", "right"]
1626 Parameters
1627 ----------
1628 closed : {None, "left", "right"}
1630 Returns
1631 -------
1632 left_closed : bool
1633 right_closed : bool
1635 Raises
1636 ------
1637 ValueError : if argument is not among valid values
1638 """
1639 left_closed = False
1640 right_closed = False
1642 if closed is None:
1643 left_closed = True
1644 right_closed = True
1645 elif closed == "left":
1646 left_closed = True
1647 elif closed == "right":
1648 right_closed = True
1649 else:
1650 raise ValueError("Closed has to be either 'left', 'right' or None")
1652 return left_closed, right_closed
1655def validate_inferred_freq(freq, inferred_freq, freq_infer):
1656 """
1657 If the user passes a freq and another freq is inferred from passed data,
1658 require that they match.
1660 Parameters
1661 ----------
1662 freq : DateOffset or None
1663 inferred_freq : DateOffset or None
1664 freq_infer : bool
1666 Returns
1667 -------
1668 freq : DateOffset or None
1669 freq_infer : bool
1671 Notes
1672 -----
1673 We assume at this point that `maybe_infer_freq` has been called, so
1674 `freq` is either a DateOffset object or None.
1675 """
1676 if inferred_freq is not None:
1677 if freq is not None and freq != inferred_freq:
1678 raise ValueError(
1679 f"Inferred frequency {inferred_freq} from passed "
1680 "values does not conform to passed frequency "
1681 f"{freq.freqstr}"
1682 )
1683 elif freq is None:
1684 freq = inferred_freq
1685 freq_infer = False
1687 return freq, freq_infer
1690def maybe_infer_freq(freq):
1691 """
1692 Comparing a DateOffset to the string "infer" raises, so we need to
1693 be careful about comparisons. Make a dummy variable `freq_infer` to
1694 signify the case where the given freq is "infer" and set freq to None
1695 to avoid comparison trouble later on.
1697 Parameters
1698 ----------
1699 freq : {DateOffset, None, str}
1701 Returns
1702 -------
1703 freq : {DateOffset, None}
1704 freq_infer : bool
1705 """
1706 freq_infer = False
1707 if not isinstance(freq, DateOffset):
1708 # if a passed freq is None, don't infer automatically
1709 if freq != "infer":
1710 freq = frequencies.to_offset(freq)
1711 else:
1712 freq_infer = True
1713 freq = None
1714 return freq, freq_infer