Thanks to visit codestin.com
Credit goes to github.com

Skip to content

ENH: add variable epoch #15008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/api/api_changes_3.3/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,8 @@ experimental and may change in the future.
``testing.compare.make_external_conversion_command``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
... is deprecated.

`.epoch2num` and `.num2epoch` are deprecated
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These are unused and can be easily reproduced by other date tools.
`.get_epoch` will return Matplotlib's epoch.
22 changes: 22 additions & 0 deletions doc/users/next_whats_new/2020-04-09-datetime-epoch-change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Dates now use a modern epoch
----------------------------

Matplotlib converts dates to days since an epoch using `.dates.date2num` (via
`matplotlib.units`). Previously, an epoch of ``0000-12-31T00:00:00`` was used
so that ``0001-01-01`` was converted to 1.0. An epoch so distant in the
past meant that a modern date was not able to preserve microseconds because
2000 years times the 2^(-52) resolution of a 64-bit float gives 14
microseconds.

Here we change the default epoch to the more reasonable UNIX default of
``1970-01-01T00:00:00`` which for a modern date has 0.35 microsecond
resolution. (Finer resolution is not possible because we rely on
`datetime.datetime` for the date locators). Access to the epoch is provided
by `~.dates.get_epoch`, and there is a new :rc:`date.epoch` rcParam. The user
may also call `~.dates.set_epoch`, but it must be set *before* any date
conversion or plotting is used.

If you have data stored as ordinal floats in the old epoch, a simple
conversion (using the new epoch) is::

new_ordinal = old_ordinal + mdates.date2num(np.datetime64('0000-12-31'))
155 changes: 155 additions & 0 deletions examples/ticks_and_spines/date_precision_and_epochs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
=========================
Date Precision and Epochs
=========================

Matplotlib can handle `.datetime` objects and `numpy.datetime64` objects using
a unit converter that recognizes these dates and converts them to floating
point numbers.

Before Matplotlib 3.3, the default for this conversion returns a float that was
days since "0000-12-31T00:00:00". As of Matplotlib 3.3, the default is
days from "1970-01-01T00:00:00". This allows more resolution for modern
dates. "2020-01-01" with the old epoch converted to 730120, and a 64-bit
floating point number has a resolution of 2^{-52}, or approximately
14 microseconds, so microsecond precision was lost. With the new default
epoch "2020-01-01" is 10957.0, so the achievable resolution is 0.21
microseconds.

"""
import datetime
import numpy as np

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.dates as mdates


def _reset_epoch_for_tutorial():
"""
Users (and downstream libraries) should not use the private method of
resetting the epoch.
"""
mdates._reset_epoch_test_example()


#############################################################################
# Datetime
# --------
#
# Python `.datetime` objects have microsecond resolution, so with the
# old default matplotlib dates could not round-trip full-resolution datetime
# objects.

old_epoch = '0000-12-31T00:00:00'
new_epoch = '1970-01-01T00:00:00'

_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
mdates.set_epoch(old_epoch) # old epoch (pre MPL 3.3)

date1 = datetime.datetime(2000, 1, 1, 0, 10, 0, 12,
tzinfo=datetime.timezone.utc)
mdate1 = mdates.date2num(date1)
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
date2 = mdates.num2date(mdate1)
print('After Roundtrip: ', date2)

#############################################################################
# Note this is only a round-off error, and there is no problem for
# dates closer to the old epoch:

date1 = datetime.datetime(10, 1, 1, 0, 10, 0, 12,
tzinfo=datetime.timezone.utc)
mdate1 = mdates.date2num(date1)
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
date2 = mdates.num2date(mdate1)
print('After Roundtrip: ', date2)

#############################################################################
# If a user wants to use modern dates at microsecond precision, they
# can change the epoch using `~.set_epoch`. However, the epoch has to be
# set before any date operations to prevent confusion between different
# epochs. Trying to change the epoch later will raise a `RuntimeError`.

try:
mdates.set_epoch(new_epoch) # this is the new MPL 3.3 default.
except RuntimeError as e:
print('RuntimeError:', str(e))

#############################################################################
# For this tutorial, we reset the sentinel using a private method, but users
# should just set the epoch once, if at all.

_reset_epoch_for_tutorial() # Just being done for this tutorial.
mdates.set_epoch(new_epoch)

date1 = datetime.datetime(2020, 1, 1, 0, 10, 0, 12,
tzinfo=datetime.timezone.utc)
mdate1 = mdates.date2num(date1)
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
date2 = mdates.num2date(mdate1)
print('After Roundtrip: ', date2)

#############################################################################
# datetime64
# ----------
#
# `numpy.datetime64` objects have microsecond precision for a much larger
# timespace than `.datetime` objects. However, currently Matplotlib time is
# only converted back to datetime objects, which have microsecond resolution,
# and years that only span 0000 to 9999.

_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
mdates.set_epoch(new_epoch)

date1 = np.datetime64('2000-01-01T00:10:00.000012')
mdate1 = mdates.date2num(date1)
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
date2 = mdates.num2date(mdate1)
print('After Roundtrip: ', date2)

#############################################################################
# Plotting
# --------
#
# This all of course has an effect on plotting. With the old default epoch
# the times were rounded, leading to jumps in the data:

_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
mdates.set_epoch(old_epoch)

x = np.arange('2000-01-01T00:00:00.0', '2000-01-01T00:00:00.000100',
dtype='datetime64[us]')
y = np.arange(0, len(x))
fig, ax = plt.subplots(constrained_layout=True)
ax.plot(x, y)
ax.set_title('Epoch: ' + mdates.get_epoch())
plt.setp(ax.xaxis.get_majorticklabels(), rotation=40)
plt.show()

#############################################################################
# For a more recent epoch, the plot is smooth:

_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
mdates.set_epoch(new_epoch)

fig, ax = plt.subplots(constrained_layout=True)
ax.plot(x, y)
ax.set_title('Epoch: ' + mdates.get_epoch())
plt.setp(ax.xaxis.get_majorticklabels(), rotation=40)
plt.show()

_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.

#############################################################################
# ------------
#
# References
# """"""""""
#
# The use of the following functions, methods and classes is shown
# in this example:

matplotlib.dates.num2date
matplotlib.dates.date2num
matplotlib.dates.set_epoch
1 change: 0 additions & 1 deletion lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1809,7 +1809,6 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False,
self.xaxis_date(tz)
if ydate:
self.yaxis_date(tz)

ret = self.plot(x, y, fmt, **kwargs)

self._request_autoscale_view()
Expand Down
Loading