test_converter.py 13 KB

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