test_converter.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. from datetime import (
  2. date,
  3. datetime,
  4. )
  5. import subprocess
  6. import sys
  7. import numpy as np
  8. import pytest
  9. import pandas._config.config as cf
  10. from pandas._libs.tslibs import to_offset
  11. from pandas import (
  12. Index,
  13. Period,
  14. PeriodIndex,
  15. Series,
  16. Timestamp,
  17. arrays,
  18. date_range,
  19. )
  20. import pandas._testing as tm
  21. from pandas.plotting import (
  22. deregister_matplotlib_converters,
  23. register_matplotlib_converters,
  24. )
  25. from pandas.tseries.offsets import (
  26. Day,
  27. Micro,
  28. Milli,
  29. Second,
  30. )
  31. try:
  32. from pandas.plotting._matplotlib import converter
  33. except ImportError:
  34. # try / except, rather than skip, to avoid internal refactoring
  35. # causing an improper skip
  36. pass
  37. pytest.importorskip("matplotlib.pyplot")
  38. dates = pytest.importorskip("matplotlib.dates")
  39. @pytest.mark.single_cpu
  40. def test_registry_mpl_resets():
  41. # Check that Matplotlib converters are properly reset (see issue #27481)
  42. code = (
  43. "import matplotlib.units as units; "
  44. "import matplotlib.dates as mdates; "
  45. "n_conv = len(units.registry); "
  46. "import pandas as pd; "
  47. "pd.plotting.register_matplotlib_converters(); "
  48. "pd.plotting.deregister_matplotlib_converters(); "
  49. "assert len(units.registry) == n_conv"
  50. )
  51. call = [sys.executable, "-c", code]
  52. subprocess.check_output(call)
  53. def test_timtetonum_accepts_unicode():
  54. assert converter.time2num("00:01") == converter.time2num("00:01")
  55. class TestRegistration:
  56. @pytest.mark.single_cpu
  57. def test_dont_register_by_default(self):
  58. # Run in subprocess to ensure a clean state
  59. code = (
  60. "import matplotlib.units; "
  61. "import pandas as pd; "
  62. "units = dict(matplotlib.units.registry); "
  63. "assert pd.Timestamp not in units"
  64. )
  65. call = [sys.executable, "-c", code]
  66. assert subprocess.check_call(call) == 0
  67. def test_registering_no_warning(self):
  68. plt = pytest.importorskip("matplotlib.pyplot")
  69. s = Series(range(12), index=date_range("2017", periods=12))
  70. _, ax = plt.subplots()
  71. # Set to the "warn" state, in case this isn't the first test run
  72. register_matplotlib_converters()
  73. ax.plot(s.index, s.values)
  74. plt.close()
  75. def test_pandas_plots_register(self):
  76. plt = pytest.importorskip("matplotlib.pyplot")
  77. s = Series(range(12), index=date_range("2017", periods=12))
  78. # Set to the "warn" state, in case this isn't the first test run
  79. with tm.assert_produces_warning(None) as w:
  80. s.plot()
  81. try:
  82. assert len(w) == 0
  83. finally:
  84. plt.close()
  85. def test_matplotlib_formatters(self):
  86. units = pytest.importorskip("matplotlib.units")
  87. # Can't make any assertion about the start state.
  88. # We we check that toggling converters off removes it, and toggling it
  89. # on restores it.
  90. with cf.option_context("plotting.matplotlib.register_converters", True):
  91. with cf.option_context("plotting.matplotlib.register_converters", False):
  92. assert Timestamp not in units.registry
  93. assert Timestamp in units.registry
  94. def test_option_no_warning(self):
  95. pytest.importorskip("matplotlib.pyplot")
  96. ctx = cf.option_context("plotting.matplotlib.register_converters", False)
  97. plt = pytest.importorskip("matplotlib.pyplot")
  98. s = Series(range(12), index=date_range("2017", periods=12))
  99. _, ax = plt.subplots()
  100. # Test without registering first, no warning
  101. with ctx:
  102. ax.plot(s.index, s.values)
  103. # Now test with registering
  104. register_matplotlib_converters()
  105. with ctx:
  106. ax.plot(s.index, s.values)
  107. plt.close()
  108. def test_registry_resets(self):
  109. units = pytest.importorskip("matplotlib.units")
  110. dates = pytest.importorskip("matplotlib.dates")
  111. # make a copy, to reset to
  112. original = dict(units.registry)
  113. try:
  114. # get to a known state
  115. units.registry.clear()
  116. date_converter = dates.DateConverter()
  117. units.registry[datetime] = date_converter
  118. units.registry[date] = date_converter
  119. register_matplotlib_converters()
  120. assert units.registry[date] is not date_converter
  121. deregister_matplotlib_converters()
  122. assert units.registry[date] is date_converter
  123. finally:
  124. # restore original stater
  125. units.registry.clear()
  126. for k, v in original.items():
  127. units.registry[k] = v
  128. class TestDateTimeConverter:
  129. @pytest.fixture
  130. def dtc(self):
  131. return converter.DatetimeConverter()
  132. def test_convert_accepts_unicode(self, dtc):
  133. r1 = dtc.convert("2000-01-01 12:22", None, None)
  134. r2 = dtc.convert("2000-01-01 12:22", None, None)
  135. assert r1 == r2, "DatetimeConverter.convert should accept unicode"
  136. def test_conversion(self, dtc):
  137. rs = dtc.convert(["2012-1-1"], None, None)[0]
  138. xp = dates.date2num(datetime(2012, 1, 1))
  139. assert rs == xp
  140. rs = dtc.convert("2012-1-1", None, None)
  141. assert rs == xp
  142. rs = dtc.convert(date(2012, 1, 1), None, None)
  143. assert rs == xp
  144. rs = dtc.convert("2012-1-1", None, None)
  145. assert rs == xp
  146. rs = dtc.convert(Timestamp("2012-1-1"), None, None)
  147. assert rs == xp
  148. # also testing datetime64 dtype (GH8614)
  149. rs = dtc.convert("2012-01-01", None, None)
  150. assert rs == xp
  151. rs = dtc.convert("2012-01-01 00:00:00+0000", None, None)
  152. assert rs == xp
  153. rs = dtc.convert(
  154. np.array(["2012-01-01 00:00:00+0000", "2012-01-02 00:00:00+0000"]),
  155. None,
  156. None,
  157. )
  158. assert rs[0] == xp
  159. # we have a tz-aware date (constructed to that when we turn to utc it
  160. # is the same as our sample)
  161. ts = Timestamp("2012-01-01").tz_localize("UTC").tz_convert("US/Eastern")
  162. rs = dtc.convert(ts, None, None)
  163. assert rs == xp
  164. rs = dtc.convert(ts.to_pydatetime(), None, None)
  165. assert rs == xp
  166. rs = dtc.convert(Index([ts - Day(1), ts]), None, None)
  167. assert rs[1] == xp
  168. rs = dtc.convert(Index([ts - Day(1), ts]).to_pydatetime(), None, None)
  169. assert rs[1] == xp
  170. def test_conversion_float(self, dtc):
  171. rtol = 0.5 * 10**-9
  172. rs = dtc.convert(Timestamp("2012-1-1 01:02:03", tz="UTC"), None, None)
  173. xp = converter.mdates.date2num(Timestamp("2012-1-1 01:02:03", tz="UTC"))
  174. tm.assert_almost_equal(rs, xp, rtol=rtol)
  175. rs = dtc.convert(
  176. Timestamp("2012-1-1 09:02:03", tz="Asia/Hong_Kong"), None, None
  177. )
  178. tm.assert_almost_equal(rs, xp, rtol=rtol)
  179. rs = dtc.convert(datetime(2012, 1, 1, 1, 2, 3), None, None)
  180. tm.assert_almost_equal(rs, xp, rtol=rtol)
  181. @pytest.mark.parametrize(
  182. "values",
  183. [
  184. [date(1677, 1, 1), date(1677, 1, 2)],
  185. [datetime(1677, 1, 1, 12), datetime(1677, 1, 2, 12)],
  186. ],
  187. )
  188. def test_conversion_outofbounds_datetime(self, dtc, values):
  189. # 2579
  190. rs = dtc.convert(values, None, None)
  191. xp = converter.mdates.date2num(values)
  192. tm.assert_numpy_array_equal(rs, xp)
  193. rs = dtc.convert(values[0], None, None)
  194. xp = converter.mdates.date2num(values[0])
  195. assert rs == xp
  196. @pytest.mark.parametrize(
  197. "time,format_expected",
  198. [
  199. (0, "00:00"), # time2num(datetime.time.min)
  200. (86399.999999, "23:59:59.999999"), # time2num(datetime.time.max)
  201. (90000, "01:00"),
  202. (3723, "01:02:03"),
  203. (39723.2, "11:02:03.200"),
  204. ],
  205. )
  206. def test_time_formatter(self, time, format_expected):
  207. # issue 18478
  208. result = converter.TimeFormatter(None)(time)
  209. assert result == format_expected
  210. @pytest.mark.parametrize("freq", ("B", "ms", "s"))
  211. def test_dateindex_conversion(self, freq, dtc):
  212. rtol = 10**-9
  213. dateindex = date_range("2020-01-01", periods=10, freq=freq)
  214. rs = dtc.convert(dateindex, None, None)
  215. xp = converter.mdates.date2num(dateindex._mpl_repr())
  216. tm.assert_almost_equal(rs, xp, rtol=rtol)
  217. @pytest.mark.parametrize("offset", [Second(), Milli(), Micro(50)])
  218. def test_resolution(self, offset, dtc):
  219. # Matplotlib's time representation using floats cannot distinguish
  220. # intervals smaller than ~10 microsecond in the common range of years.
  221. ts1 = Timestamp("2012-1-1")
  222. ts2 = ts1 + offset
  223. val1 = dtc.convert(ts1, None, None)
  224. val2 = dtc.convert(ts2, None, None)
  225. if not val1 < val2:
  226. raise AssertionError(f"{val1} is not less than {val2}.")
  227. def test_convert_nested(self, dtc):
  228. inner = [Timestamp("2017-01-01"), Timestamp("2017-01-02")]
  229. data = [inner, inner]
  230. result = dtc.convert(data, None, None)
  231. expected = [dtc.convert(x, None, None) for x in data]
  232. assert (np.array(result) == expected).all()
  233. class TestPeriodConverter:
  234. @pytest.fixture
  235. def pc(self):
  236. return converter.PeriodConverter()
  237. @pytest.fixture
  238. def axis(self):
  239. class Axis:
  240. pass
  241. axis = Axis()
  242. axis.freq = "D"
  243. return axis
  244. def test_convert_accepts_unicode(self, pc, axis):
  245. r1 = pc.convert("2012-1-1", None, axis)
  246. r2 = pc.convert("2012-1-1", None, axis)
  247. assert r1 == r2
  248. def test_conversion(self, pc, axis):
  249. rs = pc.convert(["2012-1-1"], None, axis)[0]
  250. xp = Period("2012-1-1").ordinal
  251. assert rs == xp
  252. rs = pc.convert("2012-1-1", None, axis)
  253. assert rs == xp
  254. rs = pc.convert([date(2012, 1, 1)], None, axis)[0]
  255. assert rs == xp
  256. rs = pc.convert(date(2012, 1, 1), None, axis)
  257. assert rs == xp
  258. rs = pc.convert([Timestamp("2012-1-1")], None, axis)[0]
  259. assert rs == xp
  260. rs = pc.convert(Timestamp("2012-1-1"), None, axis)
  261. assert rs == xp
  262. rs = pc.convert("2012-01-01", None, axis)
  263. assert rs == xp
  264. rs = pc.convert("2012-01-01 00:00:00+0000", None, axis)
  265. assert rs == xp
  266. rs = pc.convert(
  267. np.array(
  268. ["2012-01-01 00:00:00", "2012-01-02 00:00:00"],
  269. dtype="datetime64[ns]",
  270. ),
  271. None,
  272. axis,
  273. )
  274. assert rs[0] == xp
  275. def test_integer_passthrough(self, pc, axis):
  276. # GH9012
  277. rs = pc.convert([0, 1], None, axis)
  278. xp = [0, 1]
  279. assert rs == xp
  280. def test_convert_nested(self, pc, axis):
  281. data = ["2012-1-1", "2012-1-2"]
  282. r1 = pc.convert([data, data], None, axis)
  283. r2 = [pc.convert(data, None, axis) for _ in range(2)]
  284. assert r1 == r2
  285. class TestTimeDeltaConverter:
  286. """Test timedelta converter"""
  287. @pytest.mark.parametrize(
  288. "x, decimal, format_expected",
  289. [
  290. (0.0, 0, "00:00:00"),
  291. (3972320000000, 1, "01:06:12.3"),
  292. (713233432000000, 2, "8 days 06:07:13.43"),
  293. (32423432000000, 4, "09:00:23.4320"),
  294. ],
  295. )
  296. def test_format_timedelta_ticks(self, x, decimal, format_expected):
  297. tdc = converter.TimeSeries_TimedeltaFormatter
  298. result = tdc.format_timedelta_ticks(x, pos=None, n_decimals=decimal)
  299. assert result == format_expected
  300. @pytest.mark.parametrize("view_interval", [(1, 2), (2, 1)])
  301. def test_call_w_different_view_intervals(self, view_interval, monkeypatch):
  302. # previously broke on reversed xlmits; see GH37454
  303. class mock_axis:
  304. def get_view_interval(self):
  305. return view_interval
  306. tdc = converter.TimeSeries_TimedeltaFormatter()
  307. monkeypatch.setattr(tdc, "axis", mock_axis())
  308. tdc(0.0, 0)
  309. @pytest.mark.parametrize("year_span", [11.25, 30, 80, 150, 400, 800, 1500, 2500, 3500])
  310. # The range is limited to 11.25 at the bottom by if statements in
  311. # the _quarterly_finder() function
  312. def test_quarterly_finder(year_span):
  313. vmin = -1000
  314. vmax = vmin + year_span * 4
  315. span = vmax - vmin + 1
  316. if span < 45:
  317. pytest.skip("the quarterly finder is only invoked if the span is >= 45")
  318. nyears = span / 4
  319. (min_anndef, maj_anndef) = converter._get_default_annual_spacing(nyears)
  320. result = converter._quarterly_finder(vmin, vmax, to_offset("QE"))
  321. quarters = PeriodIndex(
  322. arrays.PeriodArray(np.array([x[0] for x in result]), dtype="period[Q]")
  323. )
  324. majors = np.array([x[1] for x in result])
  325. minors = np.array([x[2] for x in result])
  326. major_quarters = quarters[majors]
  327. minor_quarters = quarters[minors]
  328. check_major_years = major_quarters.year % maj_anndef == 0
  329. check_minor_years = minor_quarters.year % min_anndef == 0
  330. check_major_quarters = major_quarters.quarter == 1
  331. check_minor_quarters = minor_quarters.quarter == 1
  332. assert np.all(check_major_years)
  333. assert np.all(check_minor_years)
  334. assert np.all(check_major_quarters)
  335. assert np.all(check_minor_quarters)