_timeseries.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. # TODO: Use the fact that axis can have units to simplify the process
  2. import functools
  3. from matplotlib import pylab
  4. import numpy as np
  5. from pandas._libs.tslibs.frequencies import (
  6. FreqGroup, get_base_alias, get_freq, is_subperiod, is_superperiod)
  7. from pandas._libs.tslibs.period import Period
  8. import pandas.compat as compat
  9. from pandas.core.dtypes.generic import (
  10. ABCDatetimeIndex, ABCPeriodIndex, ABCTimedeltaIndex)
  11. from pandas.io.formats.printing import pprint_thing
  12. from pandas.plotting._converter import (
  13. TimeSeries_DateFormatter, TimeSeries_DateLocator,
  14. TimeSeries_TimedeltaFormatter)
  15. import pandas.tseries.frequencies as frequencies
  16. from pandas.tseries.offsets import DateOffset
  17. # ---------------------------------------------------------------------
  18. # Plotting functions and monkey patches
  19. def tsplot(series, plotf, ax=None, **kwargs):
  20. import warnings
  21. """
  22. Plots a Series on the given Matplotlib axes or the current axes
  23. Parameters
  24. ----------
  25. axes : Axes
  26. series : Series
  27. Notes
  28. _____
  29. Supports same kwargs as Axes.plot
  30. .. deprecated:: 0.23.0
  31. Use Series.plot() instead
  32. """
  33. warnings.warn("'tsplot' is deprecated and will be removed in a "
  34. "future version. Please use Series.plot() instead.",
  35. FutureWarning, stacklevel=2)
  36. # Used inferred freq is possible, need a test case for inferred
  37. if ax is None:
  38. import matplotlib.pyplot as plt
  39. ax = plt.gca()
  40. freq, series = _maybe_resample(series, ax, kwargs)
  41. # Set ax with freq info
  42. _decorate_axes(ax, freq, kwargs)
  43. ax._plot_data.append((series, plotf, kwargs))
  44. lines = plotf(ax, series.index._mpl_repr(), series.values, **kwargs)
  45. # set date formatter, locators and rescale limits
  46. format_dateaxis(ax, ax.freq, series.index)
  47. return lines
  48. def _maybe_resample(series, ax, kwargs):
  49. # resample against axes freq if necessary
  50. freq, ax_freq = _get_freq(ax, series)
  51. if freq is None: # pragma: no cover
  52. raise ValueError('Cannot use dynamic axis without frequency info')
  53. # Convert DatetimeIndex to PeriodIndex
  54. if isinstance(series.index, ABCDatetimeIndex):
  55. series = series.to_period(freq=freq)
  56. if ax_freq is not None and freq != ax_freq:
  57. if is_superperiod(freq, ax_freq): # upsample input
  58. series = series.copy()
  59. series.index = series.index.asfreq(ax_freq, how='s')
  60. freq = ax_freq
  61. elif _is_sup(freq, ax_freq): # one is weekly
  62. how = kwargs.pop('how', 'last')
  63. series = getattr(series.resample('D'), how)().dropna()
  64. series = getattr(series.resample(ax_freq), how)().dropna()
  65. freq = ax_freq
  66. elif is_subperiod(freq, ax_freq) or _is_sub(freq, ax_freq):
  67. _upsample_others(ax, freq, kwargs)
  68. else: # pragma: no cover
  69. raise ValueError('Incompatible frequency conversion')
  70. return freq, series
  71. def _is_sub(f1, f2):
  72. return ((f1.startswith('W') and is_subperiod('D', f2)) or
  73. (f2.startswith('W') and is_subperiod(f1, 'D')))
  74. def _is_sup(f1, f2):
  75. return ((f1.startswith('W') and is_superperiod('D', f2)) or
  76. (f2.startswith('W') and is_superperiod(f1, 'D')))
  77. def _upsample_others(ax, freq, kwargs):
  78. legend = ax.get_legend()
  79. lines, labels = _replot_ax(ax, freq, kwargs)
  80. _replot_ax(ax, freq, kwargs)
  81. other_ax = None
  82. if hasattr(ax, 'left_ax'):
  83. other_ax = ax.left_ax
  84. if hasattr(ax, 'right_ax'):
  85. other_ax = ax.right_ax
  86. if other_ax is not None:
  87. rlines, rlabels = _replot_ax(other_ax, freq, kwargs)
  88. lines.extend(rlines)
  89. labels.extend(rlabels)
  90. if (legend is not None and kwargs.get('legend', True) and
  91. len(lines) > 0):
  92. title = legend.get_title().get_text()
  93. if title == 'None':
  94. title = None
  95. ax.legend(lines, labels, loc='best', title=title)
  96. def _replot_ax(ax, freq, kwargs):
  97. data = getattr(ax, '_plot_data', None)
  98. # clear current axes and data
  99. ax._plot_data = []
  100. ax.clear()
  101. _decorate_axes(ax, freq, kwargs)
  102. lines = []
  103. labels = []
  104. if data is not None:
  105. for series, plotf, kwds in data:
  106. series = series.copy()
  107. idx = series.index.asfreq(freq, how='S')
  108. series.index = idx
  109. ax._plot_data.append((series, plotf, kwds))
  110. # for tsplot
  111. if isinstance(plotf, compat.string_types):
  112. from pandas.plotting._core import _plot_klass
  113. plotf = _plot_klass[plotf]._plot
  114. lines.append(plotf(ax, series.index._mpl_repr(),
  115. series.values, **kwds)[0])
  116. labels.append(pprint_thing(series.name))
  117. return lines, labels
  118. def _decorate_axes(ax, freq, kwargs):
  119. """Initialize axes for time-series plotting"""
  120. if not hasattr(ax, '_plot_data'):
  121. ax._plot_data = []
  122. ax.freq = freq
  123. xaxis = ax.get_xaxis()
  124. xaxis.freq = freq
  125. if not hasattr(ax, 'legendlabels'):
  126. ax.legendlabels = [kwargs.get('label', None)]
  127. else:
  128. ax.legendlabels.append(kwargs.get('label', None))
  129. ax.view_interval = None
  130. ax.date_axis_info = None
  131. def _get_ax_freq(ax):
  132. """
  133. Get the freq attribute of the ax object if set.
  134. Also checks shared axes (eg when using secondary yaxis, sharex=True
  135. or twinx)
  136. """
  137. ax_freq = getattr(ax, 'freq', None)
  138. if ax_freq is None:
  139. # check for left/right ax in case of secondary yaxis
  140. if hasattr(ax, 'left_ax'):
  141. ax_freq = getattr(ax.left_ax, 'freq', None)
  142. elif hasattr(ax, 'right_ax'):
  143. ax_freq = getattr(ax.right_ax, 'freq', None)
  144. if ax_freq is None:
  145. # check if a shared ax (sharex/twinx) has already freq set
  146. shared_axes = ax.get_shared_x_axes().get_siblings(ax)
  147. if len(shared_axes) > 1:
  148. for shared_ax in shared_axes:
  149. ax_freq = getattr(shared_ax, 'freq', None)
  150. if ax_freq is not None:
  151. break
  152. return ax_freq
  153. def _get_freq(ax, series):
  154. # get frequency from data
  155. freq = getattr(series.index, 'freq', None)
  156. if freq is None:
  157. freq = getattr(series.index, 'inferred_freq', None)
  158. ax_freq = _get_ax_freq(ax)
  159. # use axes freq if no data freq
  160. if freq is None:
  161. freq = ax_freq
  162. # get the period frequency
  163. if isinstance(freq, DateOffset):
  164. freq = freq.rule_code
  165. else:
  166. freq = get_base_alias(freq)
  167. freq = frequencies.get_period_alias(freq)
  168. return freq, ax_freq
  169. def _use_dynamic_x(ax, data):
  170. freq = _get_index_freq(data)
  171. ax_freq = _get_ax_freq(ax)
  172. if freq is None: # convert irregular if axes has freq info
  173. freq = ax_freq
  174. else: # do not use tsplot if irregular was plotted first
  175. if (ax_freq is None) and (len(ax.get_lines()) > 0):
  176. return False
  177. if freq is None:
  178. return False
  179. if isinstance(freq, DateOffset):
  180. freq = freq.rule_code
  181. else:
  182. freq = get_base_alias(freq)
  183. freq = frequencies.get_period_alias(freq)
  184. if freq is None:
  185. return False
  186. # hack this for 0.10.1, creating more technical debt...sigh
  187. if isinstance(data.index, ABCDatetimeIndex):
  188. base = get_freq(freq)
  189. x = data.index
  190. if (base <= FreqGroup.FR_DAY):
  191. return x[:1].is_normalized
  192. return Period(x[0], freq).to_timestamp(tz=x.tz) == x[0]
  193. return True
  194. def _get_index_freq(data):
  195. freq = getattr(data.index, 'freq', None)
  196. if freq is None:
  197. freq = getattr(data.index, 'inferred_freq', None)
  198. if freq == 'B':
  199. weekdays = np.unique(data.index.dayofweek)
  200. if (5 in weekdays) or (6 in weekdays):
  201. freq = None
  202. return freq
  203. def _maybe_convert_index(ax, data):
  204. # tsplot converts automatically, but don't want to convert index
  205. # over and over for DataFrames
  206. if isinstance(data.index, ABCDatetimeIndex):
  207. freq = getattr(data.index, 'freq', None)
  208. if freq is None:
  209. freq = getattr(data.index, 'inferred_freq', None)
  210. if isinstance(freq, DateOffset):
  211. freq = freq.rule_code
  212. if freq is None:
  213. freq = _get_ax_freq(ax)
  214. if freq is None:
  215. raise ValueError('Could not get frequency alias for plotting')
  216. freq = get_base_alias(freq)
  217. freq = frequencies.get_period_alias(freq)
  218. data = data.to_period(freq=freq)
  219. return data
  220. # Patch methods for subplot. Only format_dateaxis is currently used.
  221. # Do we need the rest for convenience?
  222. def format_timedelta_ticks(x, pos, n_decimals):
  223. """
  224. Convert seconds to 'D days HH:MM:SS.F'
  225. """
  226. s, ns = divmod(x, 1e9)
  227. m, s = divmod(s, 60)
  228. h, m = divmod(m, 60)
  229. d, h = divmod(h, 24)
  230. decimals = int(ns * 10**(n_decimals - 9))
  231. s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s))
  232. if n_decimals > 0:
  233. s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals)
  234. if d != 0:
  235. s = '{:d} days '.format(int(d)) + s
  236. return s
  237. def _format_coord(freq, t, y):
  238. return "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)
  239. def format_dateaxis(subplot, freq, index):
  240. """
  241. Pretty-formats the date axis (x-axis).
  242. Major and minor ticks are automatically set for the frequency of the
  243. current underlying series. As the dynamic mode is activated by
  244. default, changing the limits of the x axis will intelligently change
  245. the positions of the ticks.
  246. """
  247. # handle index specific formatting
  248. # Note: DatetimeIndex does not use this
  249. # interface. DatetimeIndex uses matplotlib.date directly
  250. if isinstance(index, ABCPeriodIndex):
  251. majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True,
  252. minor_locator=False,
  253. plot_obj=subplot)
  254. minlocator = TimeSeries_DateLocator(freq, dynamic_mode=True,
  255. minor_locator=True,
  256. plot_obj=subplot)
  257. subplot.xaxis.set_major_locator(majlocator)
  258. subplot.xaxis.set_minor_locator(minlocator)
  259. majformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True,
  260. minor_locator=False,
  261. plot_obj=subplot)
  262. minformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True,
  263. minor_locator=True,
  264. plot_obj=subplot)
  265. subplot.xaxis.set_major_formatter(majformatter)
  266. subplot.xaxis.set_minor_formatter(minformatter)
  267. # x and y coord info
  268. subplot.format_coord = functools.partial(_format_coord, freq)
  269. elif isinstance(index, ABCTimedeltaIndex):
  270. subplot.xaxis.set_major_formatter(
  271. TimeSeries_TimedeltaFormatter())
  272. else:
  273. raise TypeError('index type not supported')
  274. pylab.draw_if_interactive()