timeseries.py 9.3 KB

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