Skip to content

Commit 9e71a9d

Browse files
Merge main
2 parents 088e5de + 8b3bd48 commit 9e71a9d

File tree

15 files changed

+721
-583
lines changed

15 files changed

+721
-583
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ Other enhancements
6464
- :meth:`Series.nlargest` uses a 'stable' sort internally and will preserve original ordering.
6565
- :class:`ArrowDtype` now supports ``pyarrow.JsonType`` (:issue:`60958`)
6666
- :class:`DataFrameGroupBy` and :class:`SeriesGroupBy` methods ``sum``, ``mean``, ``median``, ``prod``, ``min``, ``max``, ``std``, ``var`` and ``sem`` now accept ``skipna`` parameter (:issue:`15675`)
67+
- :class:`Easter` has gained a new constructor argument ``method`` which specifies the method used to calculate Easter — for example, Orthodox Easter (:issue:`61665`)
68+
- :class:`Holiday` has gained the constructor argument and field ``exclude_dates`` to exclude specific datetimes from a custom holiday calendar (:issue:`54382`)
6769
- :class:`Rolling` and :class:`Expanding` now support ``nunique`` (:issue:`26958`)
6870
- :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`)
6971
- :func:`read_parquet` accepts ``to_pandas_kwargs`` which are forwarded to :meth:`pyarrow.Table.to_pandas` which enables passing additional keywords to customize the conversion to pandas, such as ``maps_as_pydicts`` to read the Parquet map data type as python dictionaries (:issue:`56842`)
@@ -716,6 +718,7 @@ Numeric
716718
- Bug in :meth:`DataFrame.cov` raises a ``TypeError`` instead of returning potentially incorrect results or other errors (:issue:`53115`)
717719
- Bug in :meth:`DataFrame.quantile` where the column type was not preserved when ``numeric_only=True`` with a list-like ``q`` produced an empty result (:issue:`59035`)
718720
- Bug in :meth:`Series.dot` returning ``object`` dtype for :class:`ArrowDtype` and nullable-dtype data (:issue:`61375`)
721+
- Bug in :meth:`Series.std` and :meth:`Series.var` when using complex-valued data (:issue:`61645`)
719722
- Bug in ``np.matmul`` with :class:`Index` inputs raising a ``TypeError`` (:issue:`57079`)
720723

721724
Conversion

pandas/_libs/tslibs/offsets.pyi

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,13 @@ class FY5253Quarter(FY5253Mixin):
230230
variation: Literal["nearest", "last"] = ...,
231231
) -> None: ...
232232

233-
class Easter(SingleConstructorOffset): ...
233+
class Easter(SingleConstructorOffset):
234+
def __init__(
235+
self,
236+
n: int = ...,
237+
normalize: bool = ...,
238+
method: int = ...,
239+
) -> None: ...
234240

235241
class _CustomBusinessMonth(BusinessMixin):
236242
def __init__(

pandas/_libs/tslibs/offsets.pyx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4520,6 +4520,12 @@ cdef class Easter(SingleConstructorOffset):
45204520
The number of years represented.
45214521
normalize : bool, default False
45224522
Normalize start/end dates to midnight before generating date range.
4523+
method : int, default 3
4524+
The method used to calculate the date of Easter. Valid options are:
4525+
- 1 (EASTER_JULIAN): Original calculation in Julian calendar
4526+
- 2 (EASTER_ORTHODOX): Original method, date converted to Gregorian calendar
4527+
- 3 (EASTER_WESTERN): Revised method, in Gregorian calendar
4528+
These constants are defined in the `dateutil.easter` module.
45234529
45244530
See Also
45254531
--------
@@ -4532,15 +4538,32 @@ cdef class Easter(SingleConstructorOffset):
45324538
Timestamp('2022-04-17 00:00:00')
45334539
"""
45344540

4541+
_attributes = tuple(["n", "normalize", "method"])
4542+
4543+
cdef readonly:
4544+
int method
4545+
4546+
from dateutil.easter import EASTER_WESTERN
4547+
4548+
def __init__(self, n=1, normalize=False, method=EASTER_WESTERN):
4549+
BaseOffset.__init__(self, n, normalize)
4550+
4551+
self.method = method
4552+
4553+
if method < 1 or method > 3:
4554+
raise ValueError(f"Method must be 1<=method<=3, got {method}")
4555+
45354556
cpdef __setstate__(self, state):
4557+
from dateutil.easter import EASTER_WESTERN
45364558
self.n = state.pop("n")
45374559
self.normalize = state.pop("normalize")
4560+
self.method = state.pop("method", EASTER_WESTERN)
45384561

45394562
@apply_wraps
45404563
def _apply(self, other: datetime) -> datetime:
45414564
from dateutil.easter import easter
45424565

4543-
current_easter = easter(other.year)
4566+
current_easter = easter(other.year, method=self.method)
45444567
current_easter = datetime(
45454568
current_easter.year, current_easter.month, current_easter.day
45464569
)
@@ -4555,7 +4578,7 @@ cdef class Easter(SingleConstructorOffset):
45554578

45564579
# NOTE: easter returns a datetime.date so we have to convert to type of
45574580
# other
4558-
new = easter(other.year + n)
4581+
new = easter(other.year + n, method=self.method)
45594582
new = datetime(
45604583
new.year,
45614584
new.month,
@@ -4573,7 +4596,7 @@ cdef class Easter(SingleConstructorOffset):
45734596

45744597
from dateutil.easter import easter
45754598

4576-
return date(dt.year, dt.month, dt.day) == easter(dt.year)
4599+
return date(dt.year, dt.month, dt.day) == easter(dt.year, method=self.method)
45774600

45784601

45794602
# ----------------------------------------------------------------------

pandas/core/nanops.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,11 @@ def nanvar(
10141014
avg = _ensure_numeric(values.sum(axis=axis, dtype=np.float64)) / count
10151015
if axis is not None:
10161016
avg = np.expand_dims(avg, axis)
1017-
sqr = _ensure_numeric((avg - values) ** 2)
1017+
if values.dtype.kind == "c":
1018+
# Need to use absolute value for complex numbers.
1019+
sqr = _ensure_numeric(abs(avg - values) ** 2)
1020+
else:
1021+
sqr = _ensure_numeric((avg - values) ** 2)
10181022
if mask is not None:
10191023
np.putmask(sqr, mask, 0)
10201024
result = sqr.sum(axis=axis, dtype=np.float64) / d

pandas/tests/extension/base/setitem.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,11 +440,11 @@ def test_delitem_series(self, data):
440440
tm.assert_series_equal(ser, expected)
441441

442442
def test_setitem_invalid(self, data, invalid_scalar):
443-
msg = "" # messages vary by subclass, so we do not test it
444-
with pytest.raises((ValueError, TypeError), match=msg):
443+
# messages vary by subclass, so we do not test it
444+
with pytest.raises((ValueError, TypeError), match=None):
445445
data[0] = invalid_scalar
446446

447-
with pytest.raises((ValueError, TypeError), match=msg):
447+
with pytest.raises((ValueError, TypeError), match=None):
448448
data[:] = invalid_scalar
449449

450450
def test_setitem_2d_values(self, data):

pandas/tests/reductions/test_reductions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,12 @@ def test_var_masked_array(self, ddof, exp):
780780
assert result == result_numpy_dtype
781781
assert result == exp
782782

783+
def test_var_complex_array(self):
784+
# GH#61645
785+
ser = Series([-1j, 0j, 1j], dtype=complex)
786+
assert ser.var(ddof=1) == 1.0
787+
assert ser.std(ddof=1) == 1.0
788+
783789
@pytest.mark.parametrize("dtype", ("m8[ns]", "M8[ns]", "M8[ns, UTC]"))
784790
def test_empty_timeseries_reductions_return_nat(self, dtype, skipna):
785791
# covers GH#11245

pandas/tests/tseries/holiday/test_holiday.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,104 @@ def test_holidays_with_timezone_specified_but_no_occurences():
353353
expected_results.index = expected_results.index.as_unit("ns")
354354

355355
tm.assert_equal(test_case, expected_results)
356+
357+
358+
def test_holiday_with_exclusion():
359+
# GH 54382
360+
start = Timestamp("2020-05-01")
361+
end = Timestamp("2025-05-31")
362+
exclude = DatetimeIndex([Timestamp("2022-05-30")]) # Queen's platinum Jubilee
363+
364+
queens_jubilee_uk_spring_bank_holiday: Holiday = Holiday(
365+
"Queen's Jubilee UK Spring Bank Holiday",
366+
month=5,
367+
day=31,
368+
offset=DateOffset(weekday=MO(-1)),
369+
exclude_dates=exclude,
370+
)
371+
372+
result = queens_jubilee_uk_spring_bank_holiday.dates(start, end)
373+
expected = DatetimeIndex(
374+
[
375+
Timestamp("2020-05-25"),
376+
Timestamp("2021-05-31"),
377+
Timestamp("2023-05-29"),
378+
Timestamp("2024-05-27"),
379+
Timestamp("2025-05-26"),
380+
],
381+
dtype="datetime64[ns]",
382+
)
383+
tm.assert_index_equal(result, expected)
384+
385+
386+
def test_holiday_with_multiple_exclusions():
387+
start = Timestamp("2025-01-01")
388+
end = Timestamp("2065-12-31")
389+
exclude = DatetimeIndex(
390+
[
391+
Timestamp("2025-01-01"),
392+
Timestamp("2042-01-01"),
393+
Timestamp("2061-01-01"),
394+
]
395+
) # Yakudoshi new year
396+
397+
yakudoshi_new_year: Holiday = Holiday(
398+
"Yakudoshi New Year", month=1, day=1, exclude_dates=exclude
399+
)
400+
401+
result = yakudoshi_new_year.dates(start, end)
402+
expected = DatetimeIndex(
403+
[
404+
Timestamp("2026-01-01"),
405+
Timestamp("2027-01-01"),
406+
Timestamp("2028-01-01"),
407+
Timestamp("2029-01-01"),
408+
Timestamp("2030-01-01"),
409+
Timestamp("2031-01-01"),
410+
Timestamp("2032-01-01"),
411+
Timestamp("2033-01-01"),
412+
Timestamp("2034-01-01"),
413+
Timestamp("2035-01-01"),
414+
Timestamp("2036-01-01"),
415+
Timestamp("2037-01-01"),
416+
Timestamp("2038-01-01"),
417+
Timestamp("2039-01-01"),
418+
Timestamp("2040-01-01"),
419+
Timestamp("2041-01-01"),
420+
Timestamp("2043-01-01"),
421+
Timestamp("2044-01-01"),
422+
Timestamp("2045-01-01"),
423+
Timestamp("2046-01-01"),
424+
Timestamp("2047-01-01"),
425+
Timestamp("2048-01-01"),
426+
Timestamp("2049-01-01"),
427+
Timestamp("2050-01-01"),
428+
Timestamp("2051-01-01"),
429+
Timestamp("2052-01-01"),
430+
Timestamp("2053-01-01"),
431+
Timestamp("2054-01-01"),
432+
Timestamp("2055-01-01"),
433+
Timestamp("2056-01-01"),
434+
Timestamp("2057-01-01"),
435+
Timestamp("2058-01-01"),
436+
Timestamp("2059-01-01"),
437+
Timestamp("2060-01-01"),
438+
Timestamp("2062-01-01"),
439+
Timestamp("2063-01-01"),
440+
Timestamp("2064-01-01"),
441+
Timestamp("2065-01-01"),
442+
],
443+
dtype="datetime64[ns]",
444+
)
445+
tm.assert_index_equal(result, expected)
446+
447+
448+
def test_exclude_date_value_error():
449+
msg = "exclude_dates must be None or of type DatetimeIndex."
450+
451+
with pytest.raises(ValueError, match=msg):
452+
exclude = [
453+
Timestamp("2025-06-10"),
454+
Timestamp("2026-06-10"),
455+
]
456+
Holiday("National Ice Tea Day", month=6, day=10, exclude_dates=exclude)

pandas/tests/tseries/offsets/test_easter.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
from datetime import datetime
99

10+
from dateutil.easter import (
11+
EASTER_ORTHODOX,
12+
EASTER_WESTERN,
13+
)
1014
import pytest
1115

1216
from pandas.tests.tseries.offsets.common import assert_offset_equal
@@ -32,3 +36,115 @@ class TestEaster:
3236
)
3337
def test_offset(self, offset, date, expected):
3438
assert_offset_equal(offset, date, expected)
39+
40+
@pytest.mark.parametrize(
41+
"offset,date,expected",
42+
[
43+
(Easter(method=EASTER_WESTERN), datetime(2010, 1, 1), datetime(2010, 4, 4)),
44+
(
45+
Easter(method=EASTER_WESTERN),
46+
datetime(2010, 4, 5),
47+
datetime(2011, 4, 24),
48+
),
49+
(
50+
Easter(2, method=EASTER_WESTERN),
51+
datetime(2010, 1, 1),
52+
datetime(2011, 4, 24),
53+
),
54+
(
55+
Easter(method=EASTER_WESTERN),
56+
datetime(2010, 4, 4),
57+
datetime(2011, 4, 24),
58+
),
59+
(
60+
Easter(2, method=EASTER_WESTERN),
61+
datetime(2010, 4, 4),
62+
datetime(2012, 4, 8),
63+
),
64+
(
65+
-Easter(method=EASTER_WESTERN),
66+
datetime(2011, 1, 1),
67+
datetime(2010, 4, 4),
68+
),
69+
(
70+
-Easter(method=EASTER_WESTERN),
71+
datetime(2010, 4, 5),
72+
datetime(2010, 4, 4),
73+
),
74+
(
75+
-Easter(2, method=EASTER_WESTERN),
76+
datetime(2011, 1, 1),
77+
datetime(2009, 4, 12),
78+
),
79+
(
80+
-Easter(method=EASTER_WESTERN),
81+
datetime(2010, 4, 4),
82+
datetime(2009, 4, 12),
83+
),
84+
(
85+
-Easter(2, method=EASTER_WESTERN),
86+
datetime(2010, 4, 4),
87+
datetime(2008, 3, 23),
88+
),
89+
],
90+
)
91+
def test_western_easter_offset(self, offset, date, expected):
92+
assert_offset_equal(offset, date, expected)
93+
94+
@pytest.mark.parametrize(
95+
"offset,date,expected",
96+
[
97+
(
98+
Easter(method=EASTER_ORTHODOX),
99+
datetime(2010, 1, 1),
100+
datetime(2010, 4, 4),
101+
),
102+
(
103+
Easter(method=EASTER_ORTHODOX),
104+
datetime(2010, 4, 5),
105+
datetime(2011, 4, 24),
106+
),
107+
(
108+
Easter(2, method=EASTER_ORTHODOX),
109+
datetime(2010, 1, 1),
110+
datetime(2011, 4, 24),
111+
),
112+
(
113+
Easter(method=EASTER_ORTHODOX),
114+
datetime(2010, 4, 4),
115+
datetime(2011, 4, 24),
116+
),
117+
(
118+
Easter(2, method=EASTER_ORTHODOX),
119+
datetime(2010, 4, 4),
120+
datetime(2012, 4, 15),
121+
),
122+
(
123+
-Easter(method=EASTER_ORTHODOX),
124+
datetime(2011, 1, 1),
125+
datetime(2010, 4, 4),
126+
),
127+
(
128+
-Easter(method=EASTER_ORTHODOX),
129+
datetime(2010, 4, 5),
130+
datetime(2010, 4, 4),
131+
),
132+
(
133+
-Easter(2, method=EASTER_ORTHODOX),
134+
datetime(2011, 1, 1),
135+
datetime(2009, 4, 19),
136+
),
137+
(
138+
-Easter(method=EASTER_ORTHODOX),
139+
datetime(2010, 4, 4),
140+
datetime(2009, 4, 19),
141+
),
142+
(
143+
-Easter(2, method=EASTER_ORTHODOX),
144+
datetime(2010, 4, 4),
145+
datetime(2008, 4, 27),
146+
),
147+
],
148+
)
149+
def test_orthodox_easter_offset(self, offset, date, expected):
150+
assert_offset_equal(offset, date, expected)

pandas/tests/tseries/offsets/test_offsets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def test_offset_freqstr(self, offset_types):
239239
offset = _create_offset(offset_types)
240240

241241
freqstr = offset.freqstr
242-
if freqstr not in ("<Easter>", "<DateOffset: days=1>", "LWOM-SAT"):
242+
if freqstr not in ("<Easter: method=3>", "<DateOffset: days=1>", "LWOM-SAT"):
243243
code = _get_offset(freqstr)
244244
assert offset.rule_code == code
245245

0 commit comments

Comments
 (0)