test_period.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600
  1. # Arithmetic tests for DataFrame/Series/Index/Array classes that should
  2. # behave identically.
  3. # Specifically for Period dtype
  4. import operator
  5. import numpy as np
  6. import pytest
  7. from pandas._libs.tslibs import (
  8. IncompatibleFrequency,
  9. Period,
  10. Timestamp,
  11. to_offset,
  12. )
  13. from pandas.errors import PerformanceWarning
  14. import pandas as pd
  15. from pandas import (
  16. PeriodIndex,
  17. Series,
  18. Timedelta,
  19. TimedeltaIndex,
  20. period_range,
  21. )
  22. import pandas._testing as tm
  23. from pandas.core import ops
  24. from pandas.core.arrays import TimedeltaArray
  25. from pandas.tests.arithmetic.common import (
  26. assert_invalid_addsub_type,
  27. assert_invalid_comparison,
  28. get_upcast_box,
  29. )
  30. # ------------------------------------------------------------------
  31. # Comparisons
  32. class TestPeriodArrayLikeComparisons:
  33. # Comparison tests for PeriodDtype vectors fully parametrized over
  34. # DataFrame/Series/PeriodIndex/PeriodArray. Ideally all comparison
  35. # tests will eventually end up here.
  36. @pytest.mark.parametrize("other", ["2017", Period("2017", freq="D")])
  37. def test_eq_scalar(self, other, box_with_array):
  38. idx = PeriodIndex(["2017", "2017", "2018"], freq="D")
  39. idx = tm.box_expected(idx, box_with_array)
  40. xbox = get_upcast_box(idx, other, True)
  41. expected = np.array([True, True, False])
  42. expected = tm.box_expected(expected, xbox)
  43. result = idx == other
  44. tm.assert_equal(result, expected)
  45. def test_compare_zerodim(self, box_with_array):
  46. # GH#26689 make sure we unbox zero-dimensional arrays
  47. pi = period_range("2000", periods=4)
  48. other = np.array(pi.to_numpy()[0])
  49. pi = tm.box_expected(pi, box_with_array)
  50. xbox = get_upcast_box(pi, other, True)
  51. result = pi <= other
  52. expected = np.array([True, False, False, False])
  53. expected = tm.box_expected(expected, xbox)
  54. tm.assert_equal(result, expected)
  55. @pytest.mark.parametrize(
  56. "scalar",
  57. [
  58. "foo",
  59. Timestamp("2021-01-01"),
  60. Timedelta(days=4),
  61. 9,
  62. 9.5,
  63. 2000, # specifically don't consider 2000 to match Period("2000", "D")
  64. False,
  65. None,
  66. ],
  67. )
  68. def test_compare_invalid_scalar(self, box_with_array, scalar):
  69. # GH#28980
  70. # comparison with scalar that cannot be interpreted as a Period
  71. pi = period_range("2000", periods=4)
  72. parr = tm.box_expected(pi, box_with_array)
  73. assert_invalid_comparison(parr, scalar, box_with_array)
  74. @pytest.mark.parametrize(
  75. "other",
  76. [
  77. pd.date_range("2000", periods=4).array,
  78. pd.timedelta_range("1D", periods=4).array,
  79. np.arange(4),
  80. np.arange(4).astype(np.float64),
  81. list(range(4)),
  82. # match Period semantics by not treating integers as Periods
  83. [2000, 2001, 2002, 2003],
  84. np.arange(2000, 2004),
  85. np.arange(2000, 2004).astype(object),
  86. pd.Index([2000, 2001, 2002, 2003]),
  87. ],
  88. )
  89. def test_compare_invalid_listlike(self, box_with_array, other):
  90. pi = period_range("2000", periods=4)
  91. parr = tm.box_expected(pi, box_with_array)
  92. assert_invalid_comparison(parr, other, box_with_array)
  93. @pytest.mark.parametrize("other_box", [list, np.array, lambda x: x.astype(object)])
  94. def test_compare_object_dtype(self, box_with_array, other_box):
  95. pi = period_range("2000", periods=5)
  96. parr = tm.box_expected(pi, box_with_array)
  97. other = other_box(pi)
  98. xbox = get_upcast_box(parr, other, True)
  99. expected = np.array([True, True, True, True, True])
  100. expected = tm.box_expected(expected, xbox)
  101. result = parr == other
  102. tm.assert_equal(result, expected)
  103. result = parr <= other
  104. tm.assert_equal(result, expected)
  105. result = parr >= other
  106. tm.assert_equal(result, expected)
  107. result = parr != other
  108. tm.assert_equal(result, ~expected)
  109. result = parr < other
  110. tm.assert_equal(result, ~expected)
  111. result = parr > other
  112. tm.assert_equal(result, ~expected)
  113. other = other_box(pi[::-1])
  114. expected = np.array([False, False, True, False, False])
  115. expected = tm.box_expected(expected, xbox)
  116. result = parr == other
  117. tm.assert_equal(result, expected)
  118. expected = np.array([True, True, True, False, False])
  119. expected = tm.box_expected(expected, xbox)
  120. result = parr <= other
  121. tm.assert_equal(result, expected)
  122. expected = np.array([False, False, True, True, True])
  123. expected = tm.box_expected(expected, xbox)
  124. result = parr >= other
  125. tm.assert_equal(result, expected)
  126. expected = np.array([True, True, False, True, True])
  127. expected = tm.box_expected(expected, xbox)
  128. result = parr != other
  129. tm.assert_equal(result, expected)
  130. expected = np.array([True, True, False, False, False])
  131. expected = tm.box_expected(expected, xbox)
  132. result = parr < other
  133. tm.assert_equal(result, expected)
  134. expected = np.array([False, False, False, True, True])
  135. expected = tm.box_expected(expected, xbox)
  136. result = parr > other
  137. tm.assert_equal(result, expected)
  138. class TestPeriodIndexComparisons:
  139. # TODO: parameterize over boxes
  140. def test_pi_cmp_period(self):
  141. idx = period_range("2007-01", periods=20, freq="M")
  142. per = idx[10]
  143. result = idx < per
  144. exp = idx.values < idx.values[10]
  145. tm.assert_numpy_array_equal(result, exp)
  146. # Tests Period.__richcmp__ against ndarray[object, ndim=2]
  147. result = idx.values.reshape(10, 2) < per
  148. tm.assert_numpy_array_equal(result, exp.reshape(10, 2))
  149. # Tests Period.__richcmp__ against ndarray[object, ndim=0]
  150. result = idx < np.array(per)
  151. tm.assert_numpy_array_equal(result, exp)
  152. # TODO: moved from test_datetime64; de-duplicate with version below
  153. def test_parr_cmp_period_scalar2(self, box_with_array):
  154. pi = period_range("2000-01-01", periods=10, freq="D")
  155. val = pi[3]
  156. expected = [x > val for x in pi]
  157. ser = tm.box_expected(pi, box_with_array)
  158. xbox = get_upcast_box(ser, val, True)
  159. expected = tm.box_expected(expected, xbox)
  160. result = ser > val
  161. tm.assert_equal(result, expected)
  162. val = pi[5]
  163. result = ser > val
  164. expected = [x > val for x in pi]
  165. expected = tm.box_expected(expected, xbox)
  166. tm.assert_equal(result, expected)
  167. @pytest.mark.parametrize("freq", ["M", "2M", "3M"])
  168. def test_parr_cmp_period_scalar(self, freq, box_with_array):
  169. # GH#13200
  170. base = PeriodIndex(["2011-01", "2011-02", "2011-03", "2011-04"], freq=freq)
  171. base = tm.box_expected(base, box_with_array)
  172. per = Period("2011-02", freq=freq)
  173. xbox = get_upcast_box(base, per, True)
  174. exp = np.array([False, True, False, False])
  175. exp = tm.box_expected(exp, xbox)
  176. tm.assert_equal(base == per, exp)
  177. tm.assert_equal(per == base, exp)
  178. exp = np.array([True, False, True, True])
  179. exp = tm.box_expected(exp, xbox)
  180. tm.assert_equal(base != per, exp)
  181. tm.assert_equal(per != base, exp)
  182. exp = np.array([False, False, True, True])
  183. exp = tm.box_expected(exp, xbox)
  184. tm.assert_equal(base > per, exp)
  185. tm.assert_equal(per < base, exp)
  186. exp = np.array([True, False, False, False])
  187. exp = tm.box_expected(exp, xbox)
  188. tm.assert_equal(base < per, exp)
  189. tm.assert_equal(per > base, exp)
  190. exp = np.array([False, True, True, True])
  191. exp = tm.box_expected(exp, xbox)
  192. tm.assert_equal(base >= per, exp)
  193. tm.assert_equal(per <= base, exp)
  194. exp = np.array([True, True, False, False])
  195. exp = tm.box_expected(exp, xbox)
  196. tm.assert_equal(base <= per, exp)
  197. tm.assert_equal(per >= base, exp)
  198. @pytest.mark.parametrize("freq", ["M", "2M", "3M"])
  199. def test_parr_cmp_pi(self, freq, box_with_array):
  200. # GH#13200
  201. base = PeriodIndex(["2011-01", "2011-02", "2011-03", "2011-04"], freq=freq)
  202. base = tm.box_expected(base, box_with_array)
  203. # TODO: could also box idx?
  204. idx = PeriodIndex(["2011-02", "2011-01", "2011-03", "2011-05"], freq=freq)
  205. xbox = get_upcast_box(base, idx, True)
  206. exp = np.array([False, False, True, False])
  207. exp = tm.box_expected(exp, xbox)
  208. tm.assert_equal(base == idx, exp)
  209. exp = np.array([True, True, False, True])
  210. exp = tm.box_expected(exp, xbox)
  211. tm.assert_equal(base != idx, exp)
  212. exp = np.array([False, True, False, False])
  213. exp = tm.box_expected(exp, xbox)
  214. tm.assert_equal(base > idx, exp)
  215. exp = np.array([True, False, False, True])
  216. exp = tm.box_expected(exp, xbox)
  217. tm.assert_equal(base < idx, exp)
  218. exp = np.array([False, True, True, False])
  219. exp = tm.box_expected(exp, xbox)
  220. tm.assert_equal(base >= idx, exp)
  221. exp = np.array([True, False, True, True])
  222. exp = tm.box_expected(exp, xbox)
  223. tm.assert_equal(base <= idx, exp)
  224. @pytest.mark.parametrize("freq", ["M", "2M", "3M"])
  225. def test_parr_cmp_pi_mismatched_freq(self, freq, box_with_array):
  226. # GH#13200
  227. # different base freq
  228. base = PeriodIndex(["2011-01", "2011-02", "2011-03", "2011-04"], freq=freq)
  229. base = tm.box_expected(base, box_with_array)
  230. msg = rf"Invalid comparison between dtype=period\[{freq}\] and Period"
  231. with pytest.raises(TypeError, match=msg):
  232. base <= Period("2011", freq="A")
  233. with pytest.raises(TypeError, match=msg):
  234. Period("2011", freq="A") >= base
  235. # TODO: Could parametrize over boxes for idx?
  236. idx = PeriodIndex(["2011", "2012", "2013", "2014"], freq="A")
  237. rev_msg = r"Invalid comparison between dtype=period\[A-DEC\] and PeriodArray"
  238. idx_msg = rev_msg if box_with_array in [tm.to_array, pd.array] else msg
  239. with pytest.raises(TypeError, match=idx_msg):
  240. base <= idx
  241. # Different frequency
  242. msg = rf"Invalid comparison between dtype=period\[{freq}\] and Period"
  243. with pytest.raises(TypeError, match=msg):
  244. base <= Period("2011", freq="4M")
  245. with pytest.raises(TypeError, match=msg):
  246. Period("2011", freq="4M") >= base
  247. idx = PeriodIndex(["2011", "2012", "2013", "2014"], freq="4M")
  248. rev_msg = r"Invalid comparison between dtype=period\[4M\] and PeriodArray"
  249. idx_msg = rev_msg if box_with_array in [tm.to_array, pd.array] else msg
  250. with pytest.raises(TypeError, match=idx_msg):
  251. base <= idx
  252. @pytest.mark.parametrize("freq", ["M", "2M", "3M"])
  253. def test_pi_cmp_nat(self, freq):
  254. idx1 = PeriodIndex(["2011-01", "2011-02", "NaT", "2011-05"], freq=freq)
  255. per = idx1[1]
  256. result = idx1 > per
  257. exp = np.array([False, False, False, True])
  258. tm.assert_numpy_array_equal(result, exp)
  259. result = per < idx1
  260. tm.assert_numpy_array_equal(result, exp)
  261. result = idx1 == pd.NaT
  262. exp = np.array([False, False, False, False])
  263. tm.assert_numpy_array_equal(result, exp)
  264. result = pd.NaT == idx1
  265. tm.assert_numpy_array_equal(result, exp)
  266. result = idx1 != pd.NaT
  267. exp = np.array([True, True, True, True])
  268. tm.assert_numpy_array_equal(result, exp)
  269. result = pd.NaT != idx1
  270. tm.assert_numpy_array_equal(result, exp)
  271. idx2 = PeriodIndex(["2011-02", "2011-01", "2011-04", "NaT"], freq=freq)
  272. result = idx1 < idx2
  273. exp = np.array([True, False, False, False])
  274. tm.assert_numpy_array_equal(result, exp)
  275. result = idx1 == idx2
  276. exp = np.array([False, False, False, False])
  277. tm.assert_numpy_array_equal(result, exp)
  278. result = idx1 != idx2
  279. exp = np.array([True, True, True, True])
  280. tm.assert_numpy_array_equal(result, exp)
  281. result = idx1 == idx1
  282. exp = np.array([True, True, False, True])
  283. tm.assert_numpy_array_equal(result, exp)
  284. result = idx1 != idx1
  285. exp = np.array([False, False, True, False])
  286. tm.assert_numpy_array_equal(result, exp)
  287. @pytest.mark.parametrize("freq", ["M", "2M", "3M"])
  288. def test_pi_cmp_nat_mismatched_freq_raises(self, freq):
  289. idx1 = PeriodIndex(["2011-01", "2011-02", "NaT", "2011-05"], freq=freq)
  290. diff = PeriodIndex(["2011-02", "2011-01", "2011-04", "NaT"], freq="4M")
  291. msg = rf"Invalid comparison between dtype=period\[{freq}\] and PeriodArray"
  292. with pytest.raises(TypeError, match=msg):
  293. idx1 > diff
  294. result = idx1 == diff
  295. expected = np.array([False, False, False, False], dtype=bool)
  296. tm.assert_numpy_array_equal(result, expected)
  297. # TODO: De-duplicate with test_pi_cmp_nat
  298. @pytest.mark.parametrize("dtype", [object, None])
  299. def test_comp_nat(self, dtype):
  300. left = PeriodIndex([Period("2011-01-01"), pd.NaT, Period("2011-01-03")])
  301. right = PeriodIndex([pd.NaT, pd.NaT, Period("2011-01-03")])
  302. if dtype is not None:
  303. left = left.astype(dtype)
  304. right = right.astype(dtype)
  305. result = left == right
  306. expected = np.array([False, False, True])
  307. tm.assert_numpy_array_equal(result, expected)
  308. result = left != right
  309. expected = np.array([True, True, False])
  310. tm.assert_numpy_array_equal(result, expected)
  311. expected = np.array([False, False, False])
  312. tm.assert_numpy_array_equal(left == pd.NaT, expected)
  313. tm.assert_numpy_array_equal(pd.NaT == right, expected)
  314. expected = np.array([True, True, True])
  315. tm.assert_numpy_array_equal(left != pd.NaT, expected)
  316. tm.assert_numpy_array_equal(pd.NaT != left, expected)
  317. expected = np.array([False, False, False])
  318. tm.assert_numpy_array_equal(left < pd.NaT, expected)
  319. tm.assert_numpy_array_equal(pd.NaT > left, expected)
  320. class TestPeriodSeriesComparisons:
  321. def test_cmp_series_period_series_mixed_freq(self):
  322. # GH#13200
  323. base = Series(
  324. [
  325. Period("2011", freq="A"),
  326. Period("2011-02", freq="M"),
  327. Period("2013", freq="A"),
  328. Period("2011-04", freq="M"),
  329. ]
  330. )
  331. ser = Series(
  332. [
  333. Period("2012", freq="A"),
  334. Period("2011-01", freq="M"),
  335. Period("2013", freq="A"),
  336. Period("2011-05", freq="M"),
  337. ]
  338. )
  339. exp = Series([False, False, True, False])
  340. tm.assert_series_equal(base == ser, exp)
  341. exp = Series([True, True, False, True])
  342. tm.assert_series_equal(base != ser, exp)
  343. exp = Series([False, True, False, False])
  344. tm.assert_series_equal(base > ser, exp)
  345. exp = Series([True, False, False, True])
  346. tm.assert_series_equal(base < ser, exp)
  347. exp = Series([False, True, True, False])
  348. tm.assert_series_equal(base >= ser, exp)
  349. exp = Series([True, False, True, True])
  350. tm.assert_series_equal(base <= ser, exp)
  351. class TestPeriodIndexSeriesComparisonConsistency:
  352. """Test PeriodIndex and Period Series Ops consistency"""
  353. # TODO: needs parametrization+de-duplication
  354. def _check(self, values, func, expected):
  355. # Test PeriodIndex and Period Series Ops consistency
  356. idx = PeriodIndex(values)
  357. result = func(idx)
  358. # check that we don't pass an unwanted type to tm.assert_equal
  359. assert isinstance(expected, (pd.Index, np.ndarray))
  360. tm.assert_equal(result, expected)
  361. s = Series(values)
  362. result = func(s)
  363. exp = Series(expected, name=values.name)
  364. tm.assert_series_equal(result, exp)
  365. def test_pi_comp_period(self):
  366. idx = PeriodIndex(
  367. ["2011-01", "2011-02", "2011-03", "2011-04"], freq="M", name="idx"
  368. )
  369. per = idx[2]
  370. f = lambda x: x == per
  371. exp = np.array([False, False, True, False], dtype=np.bool_)
  372. self._check(idx, f, exp)
  373. f = lambda x: per == x
  374. self._check(idx, f, exp)
  375. f = lambda x: x != per
  376. exp = np.array([True, True, False, True], dtype=np.bool_)
  377. self._check(idx, f, exp)
  378. f = lambda x: per != x
  379. self._check(idx, f, exp)
  380. f = lambda x: per >= x
  381. exp = np.array([True, True, True, False], dtype=np.bool_)
  382. self._check(idx, f, exp)
  383. f = lambda x: x > per
  384. exp = np.array([False, False, False, True], dtype=np.bool_)
  385. self._check(idx, f, exp)
  386. f = lambda x: per >= x
  387. exp = np.array([True, True, True, False], dtype=np.bool_)
  388. self._check(idx, f, exp)
  389. def test_pi_comp_period_nat(self):
  390. idx = PeriodIndex(
  391. ["2011-01", "NaT", "2011-03", "2011-04"], freq="M", name="idx"
  392. )
  393. per = idx[2]
  394. f = lambda x: x == per
  395. exp = np.array([False, False, True, False], dtype=np.bool_)
  396. self._check(idx, f, exp)
  397. f = lambda x: per == x
  398. self._check(idx, f, exp)
  399. f = lambda x: x == pd.NaT
  400. exp = np.array([False, False, False, False], dtype=np.bool_)
  401. self._check(idx, f, exp)
  402. f = lambda x: pd.NaT == x
  403. self._check(idx, f, exp)
  404. f = lambda x: x != per
  405. exp = np.array([True, True, False, True], dtype=np.bool_)
  406. self._check(idx, f, exp)
  407. f = lambda x: per != x
  408. self._check(idx, f, exp)
  409. f = lambda x: x != pd.NaT
  410. exp = np.array([True, True, True, True], dtype=np.bool_)
  411. self._check(idx, f, exp)
  412. f = lambda x: pd.NaT != x
  413. self._check(idx, f, exp)
  414. f = lambda x: per >= x
  415. exp = np.array([True, False, True, False], dtype=np.bool_)
  416. self._check(idx, f, exp)
  417. f = lambda x: x < per
  418. exp = np.array([True, False, False, False], dtype=np.bool_)
  419. self._check(idx, f, exp)
  420. f = lambda x: x > pd.NaT
  421. exp = np.array([False, False, False, False], dtype=np.bool_)
  422. self._check(idx, f, exp)
  423. f = lambda x: pd.NaT >= x
  424. exp = np.array([False, False, False, False], dtype=np.bool_)
  425. self._check(idx, f, exp)
  426. # ------------------------------------------------------------------
  427. # Arithmetic
  428. class TestPeriodFrameArithmetic:
  429. def test_ops_frame_period(self):
  430. # GH#13043
  431. df = pd.DataFrame(
  432. {
  433. "A": [Period("2015-01", freq="M"), Period("2015-02", freq="M")],
  434. "B": [Period("2014-01", freq="M"), Period("2014-02", freq="M")],
  435. }
  436. )
  437. assert df["A"].dtype == "Period[M]"
  438. assert df["B"].dtype == "Period[M]"
  439. p = Period("2015-03", freq="M")
  440. off = p.freq
  441. # dtype will be object because of original dtype
  442. exp = pd.DataFrame(
  443. {
  444. "A": np.array([2 * off, 1 * off], dtype=object),
  445. "B": np.array([14 * off, 13 * off], dtype=object),
  446. }
  447. )
  448. tm.assert_frame_equal(p - df, exp)
  449. tm.assert_frame_equal(df - p, -1 * exp)
  450. df2 = pd.DataFrame(
  451. {
  452. "A": [Period("2015-05", freq="M"), Period("2015-06", freq="M")],
  453. "B": [Period("2015-05", freq="M"), Period("2015-06", freq="M")],
  454. }
  455. )
  456. assert df2["A"].dtype == "Period[M]"
  457. assert df2["B"].dtype == "Period[M]"
  458. exp = pd.DataFrame(
  459. {
  460. "A": np.array([4 * off, 4 * off], dtype=object),
  461. "B": np.array([16 * off, 16 * off], dtype=object),
  462. }
  463. )
  464. tm.assert_frame_equal(df2 - df, exp)
  465. tm.assert_frame_equal(df - df2, -1 * exp)
  466. class TestPeriodIndexArithmetic:
  467. # ---------------------------------------------------------------
  468. # __add__/__sub__ with PeriodIndex
  469. # PeriodIndex + other is defined for integers and timedelta-like others
  470. # PeriodIndex - other is defined for integers, timedelta-like others,
  471. # and PeriodIndex (with matching freq)
  472. def test_parr_add_iadd_parr_raises(self, box_with_array):
  473. rng = period_range("1/1/2000", freq="D", periods=5)
  474. other = period_range("1/6/2000", freq="D", periods=5)
  475. # TODO: parametrize over boxes for other?
  476. rng = tm.box_expected(rng, box_with_array)
  477. # An earlier implementation of PeriodIndex addition performed
  478. # a set operation (union). This has since been changed to
  479. # raise a TypeError. See GH#14164 and GH#13077 for historical
  480. # reference.
  481. msg = r"unsupported operand type\(s\) for \+: .* and .*"
  482. with pytest.raises(TypeError, match=msg):
  483. rng + other
  484. with pytest.raises(TypeError, match=msg):
  485. rng += other
  486. def test_pi_sub_isub_pi(self):
  487. # GH#20049
  488. # For historical reference see GH#14164, GH#13077.
  489. # PeriodIndex subtraction originally performed set difference,
  490. # then changed to raise TypeError before being implemented in GH#20049
  491. rng = period_range("1/1/2000", freq="D", periods=5)
  492. other = period_range("1/6/2000", freq="D", periods=5)
  493. off = rng.freq
  494. expected = pd.Index([-5 * off] * 5)
  495. result = rng - other
  496. tm.assert_index_equal(result, expected)
  497. rng -= other
  498. tm.assert_index_equal(rng, expected)
  499. def test_pi_sub_pi_with_nat(self):
  500. rng = period_range("1/1/2000", freq="D", periods=5)
  501. other = rng[1:].insert(0, pd.NaT)
  502. assert other[1:].equals(rng[1:])
  503. result = rng - other
  504. off = rng.freq
  505. expected = pd.Index([pd.NaT, 0 * off, 0 * off, 0 * off, 0 * off])
  506. tm.assert_index_equal(result, expected)
  507. def test_parr_sub_pi_mismatched_freq(self, box_with_array, box_with_array2):
  508. rng = period_range("1/1/2000", freq="D", periods=5)
  509. other = period_range("1/6/2000", freq="H", periods=5)
  510. rng = tm.box_expected(rng, box_with_array)
  511. other = tm.box_expected(other, box_with_array2)
  512. msg = r"Input has different freq=[HD] from PeriodArray\(freq=[DH]\)"
  513. with pytest.raises(IncompatibleFrequency, match=msg):
  514. rng - other
  515. @pytest.mark.parametrize("n", [1, 2, 3, 4])
  516. def test_sub_n_gt_1_ticks(self, tick_classes, n):
  517. # GH 23878
  518. p1_d = "19910905"
  519. p2_d = "19920406"
  520. p1 = PeriodIndex([p1_d], freq=tick_classes(n))
  521. p2 = PeriodIndex([p2_d], freq=tick_classes(n))
  522. expected = PeriodIndex([p2_d], freq=p2.freq.base) - PeriodIndex(
  523. [p1_d], freq=p1.freq.base
  524. )
  525. tm.assert_index_equal((p2 - p1), expected)
  526. @pytest.mark.parametrize("n", [1, 2, 3, 4])
  527. @pytest.mark.parametrize(
  528. "offset, kwd_name",
  529. [
  530. (pd.offsets.YearEnd, "month"),
  531. (pd.offsets.QuarterEnd, "startingMonth"),
  532. (pd.offsets.MonthEnd, None),
  533. (pd.offsets.Week, "weekday"),
  534. ],
  535. )
  536. def test_sub_n_gt_1_offsets(self, offset, kwd_name, n):
  537. # GH 23878
  538. kwds = {kwd_name: 3} if kwd_name is not None else {}
  539. p1_d = "19910905"
  540. p2_d = "19920406"
  541. freq = offset(n, normalize=False, **kwds)
  542. p1 = PeriodIndex([p1_d], freq=freq)
  543. p2 = PeriodIndex([p2_d], freq=freq)
  544. result = p2 - p1
  545. expected = PeriodIndex([p2_d], freq=freq.base) - PeriodIndex(
  546. [p1_d], freq=freq.base
  547. )
  548. tm.assert_index_equal(result, expected)
  549. # -------------------------------------------------------------
  550. # Invalid Operations
  551. @pytest.mark.parametrize(
  552. "other",
  553. [
  554. # datetime scalars
  555. Timestamp("2016-01-01"),
  556. Timestamp("2016-01-01").to_pydatetime(),
  557. Timestamp("2016-01-01").to_datetime64(),
  558. # datetime-like arrays
  559. pd.date_range("2016-01-01", periods=3, freq="H"),
  560. pd.date_range("2016-01-01", periods=3, tz="Europe/Brussels"),
  561. pd.date_range("2016-01-01", periods=3, freq="S")._data,
  562. pd.date_range("2016-01-01", periods=3, tz="Asia/Tokyo")._data,
  563. # Miscellaneous invalid types
  564. 3.14,
  565. np.array([2.0, 3.0, 4.0]),
  566. ],
  567. )
  568. def test_parr_add_sub_invalid(self, other, box_with_array):
  569. # GH#23215
  570. rng = period_range("1/1/2000", freq="D", periods=3)
  571. rng = tm.box_expected(rng, box_with_array)
  572. msg = "|".join(
  573. [
  574. r"(:?cannot add PeriodArray and .*)",
  575. r"(:?cannot subtract .* from (:?a\s)?.*)",
  576. r"(:?unsupported operand type\(s\) for \+: .* and .*)",
  577. r"unsupported operand type\(s\) for [+-]: .* and .*",
  578. ]
  579. )
  580. assert_invalid_addsub_type(rng, other, msg)
  581. with pytest.raises(TypeError, match=msg):
  582. rng + other
  583. with pytest.raises(TypeError, match=msg):
  584. other + rng
  585. with pytest.raises(TypeError, match=msg):
  586. rng - other
  587. with pytest.raises(TypeError, match=msg):
  588. other - rng
  589. # -----------------------------------------------------------------
  590. # __add__/__sub__ with ndarray[datetime64] and ndarray[timedelta64]
  591. def test_pi_add_sub_td64_array_non_tick_raises(self):
  592. rng = period_range("1/1/2000", freq="Q", periods=3)
  593. tdi = TimedeltaIndex(["-1 Day", "-1 Day", "-1 Day"])
  594. tdarr = tdi.values
  595. msg = r"Cannot add or subtract timedelta64\[ns\] dtype from period\[Q-DEC\]"
  596. with pytest.raises(TypeError, match=msg):
  597. rng + tdarr
  598. with pytest.raises(TypeError, match=msg):
  599. tdarr + rng
  600. with pytest.raises(TypeError, match=msg):
  601. rng - tdarr
  602. msg = r"cannot subtract PeriodArray from TimedeltaArray"
  603. with pytest.raises(TypeError, match=msg):
  604. tdarr - rng
  605. def test_pi_add_sub_td64_array_tick(self):
  606. # PeriodIndex + Timedelta-like is allowed only with
  607. # tick-like frequencies
  608. rng = period_range("1/1/2000", freq="90D", periods=3)
  609. tdi = TimedeltaIndex(["-1 Day", "-1 Day", "-1 Day"])
  610. tdarr = tdi.values
  611. expected = period_range("12/31/1999", freq="90D", periods=3)
  612. result = rng + tdi
  613. tm.assert_index_equal(result, expected)
  614. result = rng + tdarr
  615. tm.assert_index_equal(result, expected)
  616. result = tdi + rng
  617. tm.assert_index_equal(result, expected)
  618. result = tdarr + rng
  619. tm.assert_index_equal(result, expected)
  620. expected = period_range("1/2/2000", freq="90D", periods=3)
  621. result = rng - tdi
  622. tm.assert_index_equal(result, expected)
  623. result = rng - tdarr
  624. tm.assert_index_equal(result, expected)
  625. msg = r"cannot subtract .* from .*"
  626. with pytest.raises(TypeError, match=msg):
  627. tdarr - rng
  628. with pytest.raises(TypeError, match=msg):
  629. tdi - rng
  630. @pytest.mark.parametrize("pi_freq", ["D", "W", "Q", "H"])
  631. @pytest.mark.parametrize("tdi_freq", [None, "H"])
  632. def test_parr_sub_td64array(self, box_with_array, tdi_freq, pi_freq):
  633. box = box_with_array
  634. xbox = box if box not in [pd.array, tm.to_array] else pd.Index
  635. tdi = TimedeltaIndex(["1 hours", "2 hours"], freq=tdi_freq)
  636. dti = Timestamp("2018-03-07 17:16:40") + tdi
  637. pi = dti.to_period(pi_freq)
  638. # TODO: parametrize over box for pi?
  639. td64obj = tm.box_expected(tdi, box)
  640. if pi_freq == "H":
  641. result = pi - td64obj
  642. expected = (pi.to_timestamp("S") - tdi).to_period(pi_freq)
  643. expected = tm.box_expected(expected, xbox)
  644. tm.assert_equal(result, expected)
  645. # Subtract from scalar
  646. result = pi[0] - td64obj
  647. expected = (pi[0].to_timestamp("S") - tdi).to_period(pi_freq)
  648. expected = tm.box_expected(expected, box)
  649. tm.assert_equal(result, expected)
  650. elif pi_freq == "D":
  651. # Tick, but non-compatible
  652. msg = (
  653. "Cannot add/subtract timedelta-like from PeriodArray that is "
  654. "not an integer multiple of the PeriodArray's freq."
  655. )
  656. with pytest.raises(IncompatibleFrequency, match=msg):
  657. pi - td64obj
  658. with pytest.raises(IncompatibleFrequency, match=msg):
  659. pi[0] - td64obj
  660. else:
  661. # With non-Tick freq, we could not add timedelta64 array regardless
  662. # of what its resolution is
  663. msg = "Cannot add or subtract timedelta64"
  664. with pytest.raises(TypeError, match=msg):
  665. pi - td64obj
  666. with pytest.raises(TypeError, match=msg):
  667. pi[0] - td64obj
  668. # -----------------------------------------------------------------
  669. # operations with array/Index of DateOffset objects
  670. @pytest.mark.parametrize("box", [np.array, pd.Index])
  671. def test_pi_add_offset_array(self, box):
  672. # GH#18849
  673. pi = PeriodIndex([Period("2015Q1"), Period("2016Q2")])
  674. offs = box(
  675. [
  676. pd.offsets.QuarterEnd(n=1, startingMonth=12),
  677. pd.offsets.QuarterEnd(n=-2, startingMonth=12),
  678. ]
  679. )
  680. expected = PeriodIndex([Period("2015Q2"), Period("2015Q4")]).astype(object)
  681. with tm.assert_produces_warning(PerformanceWarning):
  682. res = pi + offs
  683. tm.assert_index_equal(res, expected)
  684. with tm.assert_produces_warning(PerformanceWarning):
  685. res2 = offs + pi
  686. tm.assert_index_equal(res2, expected)
  687. unanchored = np.array([pd.offsets.Hour(n=1), pd.offsets.Minute(n=-2)])
  688. # addition/subtraction ops with incompatible offsets should issue
  689. # a PerformanceWarning and _then_ raise a TypeError.
  690. msg = r"Input cannot be converted to Period\(freq=Q-DEC\)"
  691. with pytest.raises(IncompatibleFrequency, match=msg):
  692. with tm.assert_produces_warning(PerformanceWarning):
  693. pi + unanchored
  694. with pytest.raises(IncompatibleFrequency, match=msg):
  695. with tm.assert_produces_warning(PerformanceWarning):
  696. unanchored + pi
  697. @pytest.mark.parametrize("box", [np.array, pd.Index])
  698. def test_pi_sub_offset_array(self, box):
  699. # GH#18824
  700. pi = PeriodIndex([Period("2015Q1"), Period("2016Q2")])
  701. other = box(
  702. [
  703. pd.offsets.QuarterEnd(n=1, startingMonth=12),
  704. pd.offsets.QuarterEnd(n=-2, startingMonth=12),
  705. ]
  706. )
  707. expected = PeriodIndex([pi[n] - other[n] for n in range(len(pi))])
  708. expected = expected.astype(object)
  709. with tm.assert_produces_warning(PerformanceWarning):
  710. res = pi - other
  711. tm.assert_index_equal(res, expected)
  712. anchored = box([pd.offsets.MonthEnd(), pd.offsets.Day(n=2)])
  713. # addition/subtraction ops with anchored offsets should issue
  714. # a PerformanceWarning and _then_ raise a TypeError.
  715. msg = r"Input has different freq=-1M from Period\(freq=Q-DEC\)"
  716. with pytest.raises(IncompatibleFrequency, match=msg):
  717. with tm.assert_produces_warning(PerformanceWarning):
  718. pi - anchored
  719. with pytest.raises(IncompatibleFrequency, match=msg):
  720. with tm.assert_produces_warning(PerformanceWarning):
  721. anchored - pi
  722. def test_pi_add_iadd_int(self, one):
  723. # Variants of `one` for #19012
  724. rng = period_range("2000-01-01 09:00", freq="H", periods=10)
  725. result = rng + one
  726. expected = period_range("2000-01-01 10:00", freq="H", periods=10)
  727. tm.assert_index_equal(result, expected)
  728. rng += one
  729. tm.assert_index_equal(rng, expected)
  730. def test_pi_sub_isub_int(self, one):
  731. """
  732. PeriodIndex.__sub__ and __isub__ with several representations of
  733. the integer 1, e.g. int, np.int64, np.uint8, ...
  734. """
  735. rng = period_range("2000-01-01 09:00", freq="H", periods=10)
  736. result = rng - one
  737. expected = period_range("2000-01-01 08:00", freq="H", periods=10)
  738. tm.assert_index_equal(result, expected)
  739. rng -= one
  740. tm.assert_index_equal(rng, expected)
  741. @pytest.mark.parametrize("five", [5, np.array(5, dtype=np.int64)])
  742. def test_pi_sub_intlike(self, five):
  743. rng = period_range("2007-01", periods=50)
  744. result = rng - five
  745. exp = rng + (-five)
  746. tm.assert_index_equal(result, exp)
  747. def test_pi_add_sub_int_array_freqn_gt1(self):
  748. # GH#47209 test adding array of ints when freq.n > 1 matches
  749. # scalar behavior
  750. pi = period_range("2016-01-01", periods=10, freq="2D")
  751. arr = np.arange(10)
  752. result = pi + arr
  753. expected = pd.Index([x + y for x, y in zip(pi, arr)])
  754. tm.assert_index_equal(result, expected)
  755. result = pi - arr
  756. expected = pd.Index([x - y for x, y in zip(pi, arr)])
  757. tm.assert_index_equal(result, expected)
  758. def test_pi_sub_isub_offset(self):
  759. # offset
  760. # DateOffset
  761. rng = period_range("2014", "2024", freq="A")
  762. result = rng - pd.offsets.YearEnd(5)
  763. expected = period_range("2009", "2019", freq="A")
  764. tm.assert_index_equal(result, expected)
  765. rng -= pd.offsets.YearEnd(5)
  766. tm.assert_index_equal(rng, expected)
  767. rng = period_range("2014-01", "2016-12", freq="M")
  768. result = rng - pd.offsets.MonthEnd(5)
  769. expected = period_range("2013-08", "2016-07", freq="M")
  770. tm.assert_index_equal(result, expected)
  771. rng -= pd.offsets.MonthEnd(5)
  772. tm.assert_index_equal(rng, expected)
  773. @pytest.mark.parametrize("transpose", [True, False])
  774. def test_pi_add_offset_n_gt1(self, box_with_array, transpose):
  775. # GH#23215
  776. # add offset to PeriodIndex with freq.n > 1
  777. per = Period("2016-01", freq="2M")
  778. pi = PeriodIndex([per])
  779. expected = PeriodIndex(["2016-03"], freq="2M")
  780. pi = tm.box_expected(pi, box_with_array, transpose=transpose)
  781. expected = tm.box_expected(expected, box_with_array, transpose=transpose)
  782. result = pi + per.freq
  783. tm.assert_equal(result, expected)
  784. result = per.freq + pi
  785. tm.assert_equal(result, expected)
  786. def test_pi_add_offset_n_gt1_not_divisible(self, box_with_array):
  787. # GH#23215
  788. # PeriodIndex with freq.n > 1 add offset with offset.n % freq.n != 0
  789. pi = PeriodIndex(["2016-01"], freq="2M")
  790. expected = PeriodIndex(["2016-04"], freq="2M")
  791. pi = tm.box_expected(pi, box_with_array)
  792. expected = tm.box_expected(expected, box_with_array)
  793. result = pi + to_offset("3M")
  794. tm.assert_equal(result, expected)
  795. result = to_offset("3M") + pi
  796. tm.assert_equal(result, expected)
  797. # ---------------------------------------------------------------
  798. # __add__/__sub__ with integer arrays
  799. @pytest.mark.parametrize("int_holder", [np.array, pd.Index])
  800. @pytest.mark.parametrize("op", [operator.add, ops.radd])
  801. def test_pi_add_intarray(self, int_holder, op):
  802. # GH#19959
  803. pi = PeriodIndex([Period("2015Q1"), Period("NaT")])
  804. other = int_holder([4, -1])
  805. result = op(pi, other)
  806. expected = PeriodIndex([Period("2016Q1"), Period("NaT")])
  807. tm.assert_index_equal(result, expected)
  808. @pytest.mark.parametrize("int_holder", [np.array, pd.Index])
  809. def test_pi_sub_intarray(self, int_holder):
  810. # GH#19959
  811. pi = PeriodIndex([Period("2015Q1"), Period("NaT")])
  812. other = int_holder([4, -1])
  813. result = pi - other
  814. expected = PeriodIndex([Period("2014Q1"), Period("NaT")])
  815. tm.assert_index_equal(result, expected)
  816. msg = r"bad operand type for unary -: 'PeriodArray'"
  817. with pytest.raises(TypeError, match=msg):
  818. other - pi
  819. # ---------------------------------------------------------------
  820. # Timedelta-like (timedelta, timedelta64, Timedelta, Tick)
  821. # TODO: Some of these are misnomers because of non-Tick DateOffsets
  822. def test_parr_add_timedeltalike_minute_gt1(self, three_days, box_with_array):
  823. # GH#23031 adding a time-delta-like offset to a PeriodArray that has
  824. # minute frequency with n != 1. A more general case is tested below
  825. # in test_pi_add_timedeltalike_tick_gt1, but here we write out the
  826. # expected result more explicitly.
  827. other = three_days
  828. rng = period_range("2014-05-01", periods=3, freq="2D")
  829. rng = tm.box_expected(rng, box_with_array)
  830. expected = PeriodIndex(["2014-05-04", "2014-05-06", "2014-05-08"], freq="2D")
  831. expected = tm.box_expected(expected, box_with_array)
  832. result = rng + other
  833. tm.assert_equal(result, expected)
  834. result = other + rng
  835. tm.assert_equal(result, expected)
  836. # subtraction
  837. expected = PeriodIndex(["2014-04-28", "2014-04-30", "2014-05-02"], freq="2D")
  838. expected = tm.box_expected(expected, box_with_array)
  839. result = rng - other
  840. tm.assert_equal(result, expected)
  841. msg = "|".join(
  842. [
  843. r"bad operand type for unary -: 'PeriodArray'",
  844. r"cannot subtract PeriodArray from timedelta64\[[hD]\]",
  845. ]
  846. )
  847. with pytest.raises(TypeError, match=msg):
  848. other - rng
  849. @pytest.mark.parametrize("freqstr", ["5ns", "5us", "5ms", "5s", "5T", "5h", "5d"])
  850. def test_parr_add_timedeltalike_tick_gt1(self, three_days, freqstr, box_with_array):
  851. # GH#23031 adding a time-delta-like offset to a PeriodArray that has
  852. # tick-like frequency with n != 1
  853. other = three_days
  854. rng = period_range("2014-05-01", periods=6, freq=freqstr)
  855. first = rng[0]
  856. rng = tm.box_expected(rng, box_with_array)
  857. expected = period_range(first + other, periods=6, freq=freqstr)
  858. expected = tm.box_expected(expected, box_with_array)
  859. result = rng + other
  860. tm.assert_equal(result, expected)
  861. result = other + rng
  862. tm.assert_equal(result, expected)
  863. # subtraction
  864. expected = period_range(first - other, periods=6, freq=freqstr)
  865. expected = tm.box_expected(expected, box_with_array)
  866. result = rng - other
  867. tm.assert_equal(result, expected)
  868. msg = "|".join(
  869. [
  870. r"bad operand type for unary -: 'PeriodArray'",
  871. r"cannot subtract PeriodArray from timedelta64\[[hD]\]",
  872. ]
  873. )
  874. with pytest.raises(TypeError, match=msg):
  875. other - rng
  876. def test_pi_add_iadd_timedeltalike_daily(self, three_days):
  877. # Tick
  878. other = three_days
  879. rng = period_range("2014-05-01", "2014-05-15", freq="D")
  880. expected = period_range("2014-05-04", "2014-05-18", freq="D")
  881. result = rng + other
  882. tm.assert_index_equal(result, expected)
  883. rng += other
  884. tm.assert_index_equal(rng, expected)
  885. def test_pi_sub_isub_timedeltalike_daily(self, three_days):
  886. # Tick-like 3 Days
  887. other = three_days
  888. rng = period_range("2014-05-01", "2014-05-15", freq="D")
  889. expected = period_range("2014-04-28", "2014-05-12", freq="D")
  890. result = rng - other
  891. tm.assert_index_equal(result, expected)
  892. rng -= other
  893. tm.assert_index_equal(rng, expected)
  894. def test_parr_add_sub_timedeltalike_freq_mismatch_daily(
  895. self, not_daily, box_with_array
  896. ):
  897. other = not_daily
  898. rng = period_range("2014-05-01", "2014-05-15", freq="D")
  899. rng = tm.box_expected(rng, box_with_array)
  900. msg = "|".join(
  901. [
  902. # non-timedelta-like DateOffset
  903. "Input has different freq(=.+)? from Period.*?\\(freq=D\\)",
  904. # timedelta/td64/Timedelta but not a multiple of 24H
  905. "Cannot add/subtract timedelta-like from PeriodArray that is "
  906. "not an integer multiple of the PeriodArray's freq.",
  907. ]
  908. )
  909. with pytest.raises(IncompatibleFrequency, match=msg):
  910. rng + other
  911. with pytest.raises(IncompatibleFrequency, match=msg):
  912. rng += other
  913. with pytest.raises(IncompatibleFrequency, match=msg):
  914. rng - other
  915. with pytest.raises(IncompatibleFrequency, match=msg):
  916. rng -= other
  917. def test_pi_add_iadd_timedeltalike_hourly(self, two_hours):
  918. other = two_hours
  919. rng = period_range("2014-01-01 10:00", "2014-01-05 10:00", freq="H")
  920. expected = period_range("2014-01-01 12:00", "2014-01-05 12:00", freq="H")
  921. result = rng + other
  922. tm.assert_index_equal(result, expected)
  923. rng += other
  924. tm.assert_index_equal(rng, expected)
  925. def test_parr_add_timedeltalike_mismatched_freq_hourly(
  926. self, not_hourly, box_with_array
  927. ):
  928. other = not_hourly
  929. rng = period_range("2014-01-01 10:00", "2014-01-05 10:00", freq="H")
  930. rng = tm.box_expected(rng, box_with_array)
  931. msg = "|".join(
  932. [
  933. # non-timedelta-like DateOffset
  934. "Input has different freq(=.+)? from Period.*?\\(freq=H\\)",
  935. # timedelta/td64/Timedelta but not a multiple of 24H
  936. "Cannot add/subtract timedelta-like from PeriodArray that is "
  937. "not an integer multiple of the PeriodArray's freq.",
  938. ]
  939. )
  940. with pytest.raises(IncompatibleFrequency, match=msg):
  941. rng + other
  942. with pytest.raises(IncompatibleFrequency, match=msg):
  943. rng += other
  944. def test_pi_sub_isub_timedeltalike_hourly(self, two_hours):
  945. other = two_hours
  946. rng = period_range("2014-01-01 10:00", "2014-01-05 10:00", freq="H")
  947. expected = period_range("2014-01-01 08:00", "2014-01-05 08:00", freq="H")
  948. result = rng - other
  949. tm.assert_index_equal(result, expected)
  950. rng -= other
  951. tm.assert_index_equal(rng, expected)
  952. def test_add_iadd_timedeltalike_annual(self):
  953. # offset
  954. # DateOffset
  955. rng = period_range("2014", "2024", freq="A")
  956. result = rng + pd.offsets.YearEnd(5)
  957. expected = period_range("2019", "2029", freq="A")
  958. tm.assert_index_equal(result, expected)
  959. rng += pd.offsets.YearEnd(5)
  960. tm.assert_index_equal(rng, expected)
  961. def test_pi_add_sub_timedeltalike_freq_mismatch_annual(self, mismatched_freq):
  962. other = mismatched_freq
  963. rng = period_range("2014", "2024", freq="A")
  964. msg = "Input has different freq(=.+)? from Period.*?\\(freq=A-DEC\\)"
  965. with pytest.raises(IncompatibleFrequency, match=msg):
  966. rng + other
  967. with pytest.raises(IncompatibleFrequency, match=msg):
  968. rng += other
  969. with pytest.raises(IncompatibleFrequency, match=msg):
  970. rng - other
  971. with pytest.raises(IncompatibleFrequency, match=msg):
  972. rng -= other
  973. def test_pi_add_iadd_timedeltalike_M(self):
  974. rng = period_range("2014-01", "2016-12", freq="M")
  975. expected = period_range("2014-06", "2017-05", freq="M")
  976. result = rng + pd.offsets.MonthEnd(5)
  977. tm.assert_index_equal(result, expected)
  978. rng += pd.offsets.MonthEnd(5)
  979. tm.assert_index_equal(rng, expected)
  980. def test_pi_add_sub_timedeltalike_freq_mismatch_monthly(self, mismatched_freq):
  981. other = mismatched_freq
  982. rng = period_range("2014-01", "2016-12", freq="M")
  983. msg = "Input has different freq(=.+)? from Period.*?\\(freq=M\\)"
  984. with pytest.raises(IncompatibleFrequency, match=msg):
  985. rng + other
  986. with pytest.raises(IncompatibleFrequency, match=msg):
  987. rng += other
  988. with pytest.raises(IncompatibleFrequency, match=msg):
  989. rng - other
  990. with pytest.raises(IncompatibleFrequency, match=msg):
  991. rng -= other
  992. @pytest.mark.parametrize("transpose", [True, False])
  993. def test_parr_add_sub_td64_nat(self, box_with_array, transpose):
  994. # GH#23320 special handling for timedelta64("NaT")
  995. pi = period_range("1994-04-01", periods=9, freq="19D")
  996. other = np.timedelta64("NaT")
  997. expected = PeriodIndex(["NaT"] * 9, freq="19D")
  998. obj = tm.box_expected(pi, box_with_array, transpose=transpose)
  999. expected = tm.box_expected(expected, box_with_array, transpose=transpose)
  1000. result = obj + other
  1001. tm.assert_equal(result, expected)
  1002. result = other + obj
  1003. tm.assert_equal(result, expected)
  1004. result = obj - other
  1005. tm.assert_equal(result, expected)
  1006. msg = r"cannot subtract .* from .*"
  1007. with pytest.raises(TypeError, match=msg):
  1008. other - obj
  1009. @pytest.mark.parametrize(
  1010. "other",
  1011. [
  1012. np.array(["NaT"] * 9, dtype="m8[ns]"),
  1013. TimedeltaArray._from_sequence(["NaT"] * 9),
  1014. ],
  1015. )
  1016. def test_parr_add_sub_tdt64_nat_array(self, box_with_array, other):
  1017. pi = period_range("1994-04-01", periods=9, freq="19D")
  1018. expected = PeriodIndex(["NaT"] * 9, freq="19D")
  1019. obj = tm.box_expected(pi, box_with_array)
  1020. expected = tm.box_expected(expected, box_with_array)
  1021. result = obj + other
  1022. tm.assert_equal(result, expected)
  1023. result = other + obj
  1024. tm.assert_equal(result, expected)
  1025. result = obj - other
  1026. tm.assert_equal(result, expected)
  1027. msg = r"cannot subtract .* from .*"
  1028. with pytest.raises(TypeError, match=msg):
  1029. other - obj
  1030. # some but not *all* NaT
  1031. other = other.copy()
  1032. other[0] = np.timedelta64(0, "ns")
  1033. expected = PeriodIndex([pi[0]] + ["NaT"] * 8, freq="19D")
  1034. expected = tm.box_expected(expected, box_with_array)
  1035. result = obj + other
  1036. tm.assert_equal(result, expected)
  1037. result = other + obj
  1038. tm.assert_equal(result, expected)
  1039. result = obj - other
  1040. tm.assert_equal(result, expected)
  1041. with pytest.raises(TypeError, match=msg):
  1042. other - obj
  1043. # ---------------------------------------------------------------
  1044. # Unsorted
  1045. def test_parr_add_sub_index(self):
  1046. # Check that PeriodArray defers to Index on arithmetic ops
  1047. pi = period_range("2000-12-31", periods=3)
  1048. parr = pi.array
  1049. result = parr - pi
  1050. expected = pi - pi
  1051. tm.assert_index_equal(result, expected)
  1052. def test_parr_add_sub_object_array(self):
  1053. pi = period_range("2000-12-31", periods=3, freq="D")
  1054. parr = pi.array
  1055. other = np.array([Timedelta(days=1), pd.offsets.Day(2), 3])
  1056. with tm.assert_produces_warning(PerformanceWarning):
  1057. result = parr + other
  1058. expected = PeriodIndex(
  1059. ["2001-01-01", "2001-01-03", "2001-01-05"], freq="D"
  1060. )._data.astype(object)
  1061. tm.assert_equal(result, expected)
  1062. with tm.assert_produces_warning(PerformanceWarning):
  1063. result = parr - other
  1064. expected = PeriodIndex(["2000-12-30"] * 3, freq="D")._data.astype(object)
  1065. tm.assert_equal(result, expected)
  1066. class TestPeriodSeriesArithmetic:
  1067. def test_parr_add_timedeltalike_scalar(self, three_days, box_with_array):
  1068. # GH#13043
  1069. ser = Series(
  1070. [Period("2015-01-01", freq="D"), Period("2015-01-02", freq="D")],
  1071. name="xxx",
  1072. )
  1073. assert ser.dtype == "Period[D]"
  1074. expected = Series(
  1075. [Period("2015-01-04", freq="D"), Period("2015-01-05", freq="D")],
  1076. name="xxx",
  1077. )
  1078. obj = tm.box_expected(ser, box_with_array)
  1079. if box_with_array is pd.DataFrame:
  1080. assert (obj.dtypes == "Period[D]").all()
  1081. expected = tm.box_expected(expected, box_with_array)
  1082. result = obj + three_days
  1083. tm.assert_equal(result, expected)
  1084. result = three_days + obj
  1085. tm.assert_equal(result, expected)
  1086. def test_ops_series_period(self):
  1087. # GH#13043
  1088. ser = Series(
  1089. [Period("2015-01-01", freq="D"), Period("2015-01-02", freq="D")],
  1090. name="xxx",
  1091. )
  1092. assert ser.dtype == "Period[D]"
  1093. per = Period("2015-01-10", freq="D")
  1094. off = per.freq
  1095. # dtype will be object because of original dtype
  1096. expected = Series([9 * off, 8 * off], name="xxx", dtype=object)
  1097. tm.assert_series_equal(per - ser, expected)
  1098. tm.assert_series_equal(ser - per, -1 * expected)
  1099. s2 = Series(
  1100. [Period("2015-01-05", freq="D"), Period("2015-01-04", freq="D")],
  1101. name="xxx",
  1102. )
  1103. assert s2.dtype == "Period[D]"
  1104. expected = Series([4 * off, 2 * off], name="xxx", dtype=object)
  1105. tm.assert_series_equal(s2 - ser, expected)
  1106. tm.assert_series_equal(ser - s2, -1 * expected)
  1107. class TestPeriodIndexSeriesMethods:
  1108. """Test PeriodIndex and Period Series Ops consistency"""
  1109. def _check(self, values, func, expected):
  1110. idx = PeriodIndex(values)
  1111. result = func(idx)
  1112. tm.assert_equal(result, expected)
  1113. ser = Series(values)
  1114. result = func(ser)
  1115. exp = Series(expected, name=values.name)
  1116. tm.assert_series_equal(result, exp)
  1117. def test_pi_ops(self):
  1118. idx = PeriodIndex(
  1119. ["2011-01", "2011-02", "2011-03", "2011-04"], freq="M", name="idx"
  1120. )
  1121. expected = PeriodIndex(
  1122. ["2011-03", "2011-04", "2011-05", "2011-06"], freq="M", name="idx"
  1123. )
  1124. self._check(idx, lambda x: x + 2, expected)
  1125. self._check(idx, lambda x: 2 + x, expected)
  1126. self._check(idx + 2, lambda x: x - 2, idx)
  1127. result = idx - Period("2011-01", freq="M")
  1128. off = idx.freq
  1129. exp = pd.Index([0 * off, 1 * off, 2 * off, 3 * off], name="idx")
  1130. tm.assert_index_equal(result, exp)
  1131. result = Period("2011-01", freq="M") - idx
  1132. exp = pd.Index([0 * off, -1 * off, -2 * off, -3 * off], name="idx")
  1133. tm.assert_index_equal(result, exp)
  1134. @pytest.mark.parametrize("ng", ["str", 1.5])
  1135. @pytest.mark.parametrize(
  1136. "func",
  1137. [
  1138. lambda obj, ng: obj + ng,
  1139. lambda obj, ng: ng + obj,
  1140. lambda obj, ng: obj - ng,
  1141. lambda obj, ng: ng - obj,
  1142. lambda obj, ng: np.add(obj, ng),
  1143. lambda obj, ng: np.add(ng, obj),
  1144. lambda obj, ng: np.subtract(obj, ng),
  1145. lambda obj, ng: np.subtract(ng, obj),
  1146. ],
  1147. )
  1148. def test_parr_ops_errors(self, ng, func, box_with_array):
  1149. idx = PeriodIndex(
  1150. ["2011-01", "2011-02", "2011-03", "2011-04"], freq="M", name="idx"
  1151. )
  1152. obj = tm.box_expected(idx, box_with_array)
  1153. msg = "|".join(
  1154. [
  1155. r"unsupported operand type\(s\)",
  1156. "can only concatenate",
  1157. r"must be str",
  1158. "object to str implicitly",
  1159. ]
  1160. )
  1161. with pytest.raises(TypeError, match=msg):
  1162. func(obj, ng)
  1163. def test_pi_ops_nat(self):
  1164. idx = PeriodIndex(
  1165. ["2011-01", "2011-02", "NaT", "2011-04"], freq="M", name="idx"
  1166. )
  1167. expected = PeriodIndex(
  1168. ["2011-03", "2011-04", "NaT", "2011-06"], freq="M", name="idx"
  1169. )
  1170. self._check(idx, lambda x: x + 2, expected)
  1171. self._check(idx, lambda x: 2 + x, expected)
  1172. self._check(idx, lambda x: np.add(x, 2), expected)
  1173. self._check(idx + 2, lambda x: x - 2, idx)
  1174. self._check(idx + 2, lambda x: np.subtract(x, 2), idx)
  1175. # freq with mult
  1176. idx = PeriodIndex(
  1177. ["2011-01", "2011-02", "NaT", "2011-04"], freq="2M", name="idx"
  1178. )
  1179. expected = PeriodIndex(
  1180. ["2011-07", "2011-08", "NaT", "2011-10"], freq="2M", name="idx"
  1181. )
  1182. self._check(idx, lambda x: x + 3, expected)
  1183. self._check(idx, lambda x: 3 + x, expected)
  1184. self._check(idx, lambda x: np.add(x, 3), expected)
  1185. self._check(idx + 3, lambda x: x - 3, idx)
  1186. self._check(idx + 3, lambda x: np.subtract(x, 3), idx)
  1187. def test_pi_ops_array_int(self):
  1188. idx = PeriodIndex(
  1189. ["2011-01", "2011-02", "NaT", "2011-04"], freq="M", name="idx"
  1190. )
  1191. f = lambda x: x + np.array([1, 2, 3, 4])
  1192. exp = PeriodIndex(
  1193. ["2011-02", "2011-04", "NaT", "2011-08"], freq="M", name="idx"
  1194. )
  1195. self._check(idx, f, exp)
  1196. f = lambda x: np.add(x, np.array([4, -1, 1, 2]))
  1197. exp = PeriodIndex(
  1198. ["2011-05", "2011-01", "NaT", "2011-06"], freq="M", name="idx"
  1199. )
  1200. self._check(idx, f, exp)
  1201. f = lambda x: x - np.array([1, 2, 3, 4])
  1202. exp = PeriodIndex(
  1203. ["2010-12", "2010-12", "NaT", "2010-12"], freq="M", name="idx"
  1204. )
  1205. self._check(idx, f, exp)
  1206. f = lambda x: np.subtract(x, np.array([3, 2, 3, -2]))
  1207. exp = PeriodIndex(
  1208. ["2010-10", "2010-12", "NaT", "2011-06"], freq="M", name="idx"
  1209. )
  1210. self._check(idx, f, exp)
  1211. def test_pi_ops_offset(self):
  1212. idx = PeriodIndex(
  1213. ["2011-01-01", "2011-02-01", "2011-03-01", "2011-04-01"],
  1214. freq="D",
  1215. name="idx",
  1216. )
  1217. f = lambda x: x + pd.offsets.Day()
  1218. exp = PeriodIndex(
  1219. ["2011-01-02", "2011-02-02", "2011-03-02", "2011-04-02"],
  1220. freq="D",
  1221. name="idx",
  1222. )
  1223. self._check(idx, f, exp)
  1224. f = lambda x: x + pd.offsets.Day(2)
  1225. exp = PeriodIndex(
  1226. ["2011-01-03", "2011-02-03", "2011-03-03", "2011-04-03"],
  1227. freq="D",
  1228. name="idx",
  1229. )
  1230. self._check(idx, f, exp)
  1231. f = lambda x: x - pd.offsets.Day(2)
  1232. exp = PeriodIndex(
  1233. ["2010-12-30", "2011-01-30", "2011-02-27", "2011-03-30"],
  1234. freq="D",
  1235. name="idx",
  1236. )
  1237. self._check(idx, f, exp)
  1238. def test_pi_offset_errors(self):
  1239. idx = PeriodIndex(
  1240. ["2011-01-01", "2011-02-01", "2011-03-01", "2011-04-01"],
  1241. freq="D",
  1242. name="idx",
  1243. )
  1244. ser = Series(idx)
  1245. msg = (
  1246. "Cannot add/subtract timedelta-like from PeriodArray that is not "
  1247. "an integer multiple of the PeriodArray's freq"
  1248. )
  1249. for obj in [idx, ser]:
  1250. with pytest.raises(IncompatibleFrequency, match=msg):
  1251. obj + pd.offsets.Hour(2)
  1252. with pytest.raises(IncompatibleFrequency, match=msg):
  1253. pd.offsets.Hour(2) + obj
  1254. with pytest.raises(IncompatibleFrequency, match=msg):
  1255. obj - pd.offsets.Hour(2)
  1256. def test_pi_sub_period(self):
  1257. # GH#13071
  1258. idx = PeriodIndex(
  1259. ["2011-01", "2011-02", "2011-03", "2011-04"], freq="M", name="idx"
  1260. )
  1261. result = idx - Period("2012-01", freq="M")
  1262. off = idx.freq
  1263. exp = pd.Index([-12 * off, -11 * off, -10 * off, -9 * off], name="idx")
  1264. tm.assert_index_equal(result, exp)
  1265. result = np.subtract(idx, Period("2012-01", freq="M"))
  1266. tm.assert_index_equal(result, exp)
  1267. result = Period("2012-01", freq="M") - idx
  1268. exp = pd.Index([12 * off, 11 * off, 10 * off, 9 * off], name="idx")
  1269. tm.assert_index_equal(result, exp)
  1270. result = np.subtract(Period("2012-01", freq="M"), idx)
  1271. tm.assert_index_equal(result, exp)
  1272. exp = TimedeltaIndex([np.nan, np.nan, np.nan, np.nan], name="idx")
  1273. result = idx - Period("NaT", freq="M")
  1274. tm.assert_index_equal(result, exp)
  1275. assert result.freq == exp.freq
  1276. result = Period("NaT", freq="M") - idx
  1277. tm.assert_index_equal(result, exp)
  1278. assert result.freq == exp.freq
  1279. def test_pi_sub_pdnat(self):
  1280. # GH#13071, GH#19389
  1281. idx = PeriodIndex(
  1282. ["2011-01", "2011-02", "NaT", "2011-04"], freq="M", name="idx"
  1283. )
  1284. exp = TimedeltaIndex([pd.NaT] * 4, name="idx")
  1285. tm.assert_index_equal(pd.NaT - idx, exp)
  1286. tm.assert_index_equal(idx - pd.NaT, exp)
  1287. def test_pi_sub_period_nat(self):
  1288. # GH#13071
  1289. idx = PeriodIndex(
  1290. ["2011-01", "NaT", "2011-03", "2011-04"], freq="M", name="idx"
  1291. )
  1292. result = idx - Period("2012-01", freq="M")
  1293. off = idx.freq
  1294. exp = pd.Index([-12 * off, pd.NaT, -10 * off, -9 * off], name="idx")
  1295. tm.assert_index_equal(result, exp)
  1296. result = Period("2012-01", freq="M") - idx
  1297. exp = pd.Index([12 * off, pd.NaT, 10 * off, 9 * off], name="idx")
  1298. tm.assert_index_equal(result, exp)
  1299. exp = TimedeltaIndex([np.nan, np.nan, np.nan, np.nan], name="idx")
  1300. tm.assert_index_equal(idx - Period("NaT", freq="M"), exp)
  1301. tm.assert_index_equal(Period("NaT", freq="M") - idx, exp)