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

Skip to content

Improve error messages for unit conversion #13005

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
Jan 12, 2019

Conversation

timhoffm
Copy link
Member

PR Summary

Fixes #12990. The call stack is a bit lengthy, but that's because the error happens several levels down and at least the reason gets clear.

Assigning categoricals to floats.

from matplotlib import pyplot as plt
xs = [1, 2, 3]
ys = [50, 10, 20]
ticks = ['banana', 'apple', 'pear']
plt.bar(xs, ys)
plt.xticks(ticks)

results in

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/dev/matplotlib/lib/matplotlib/axis.py in convert_units(self, x)
   1489         try:
-> 1490             ret = self.converter.convert(x, self.units, self)
   1491         except Exception as e:

~/dev/matplotlib/lib/matplotlib/category.py in convert(value, unit, axis)
     45             raise ValueError(
---> 46                 'Missing unit for StrCategoryConverter. This might be caused'
     47                 'by unintendedly mixing categorical and numeric data.')

ValueError: Missing unit for StrCategoryConverter. This might be causedby unintendedly mixing categorical and numeric data.

The above exception was the direct cause of the following exception:

ConversionError                           Traceback (most recent call last)
<ipython-input-2-8d8628e715b5> in <module>()
      4 ticks = ['banana', 'apple', 'pear']
      5 plt.bar(xs, ys)
----> 6 plt.xticks(ticks)

~/dev/matplotlib/lib/matplotlib/pyplot.py in xticks(ticks, labels, **kwargs)
   1518         labels = ax.get_xticklabels()
   1519     elif labels is None:
-> 1520         locs = ax.set_xticks(ticks)
   1521         labels = ax.get_xticklabels()
   1522     else:

~/dev/matplotlib/lib/matplotlib/axes/_base.py in set_xticks(self, ticks, minor)
   3303             Default is ``False``.
   3304         """
-> 3305         ret = self.xaxis.set_ticks(ticks, minor=minor)
   3306         self.stale = True
   3307         return ret

~/dev/matplotlib/lib/matplotlib/axis.py in set_ticks(self, ticks, minor)
   1689         """
   1690         # XXX if the user changes units, the information will be lost here
-> 1691         ticks = self.convert_units(ticks)
   1692         if len(ticks) > 1:
   1693             xleft, xright = self.get_view_interval()

~/dev/matplotlib/lib/matplotlib/axis.py in convert_units(self, x)
   1491         except Exception as e:
   1492             raise munits.ConversionError('Failed to convert value(s) to axis '
-> 1493                                          'units: %r' % x) from e
   1494         return ret
   1495 

ConversionError: Failed to convert value(s) to axis units: ['banana', 'apple', 'pear']

Assigning categoricals to dates.

from matplotlib import pyplot as plt
xs = [datetime(2015, 1, 1), datetime(2016, 1, 1), datetime(2017, 1, 1)]
ys = [50, 10, 20]
ticks = ['banana', 'apple', 'pear']
plt.bar(xs, ys)
plt.xticks(ticks)

results in

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
~/dev/matplotlib/lib/matplotlib/axis.py in convert_units(self, x)
   1489         try:
-> 1490             ret = self.converter.convert(x, self.units, self)
   1491         except Exception as e:

~/dev/matplotlib/lib/matplotlib/dates.py in convert(value, unit, axis)
   1805         """
-> 1806         return date2num(value)
   1807 

~/dev/matplotlib/lib/matplotlib/dates.py in date2num(d)
    421             return d
--> 422         return _to_ordinalf_np_vectorized(d)
    423 

~/miniconda3/envs/mpl/lib/python3.6/site-packages/numpy/lib/function_base.py in __call__(self, *args, **kwargs)
   2754 
-> 2755         return self._vectorize_call(func=func, args=vargs)
   2756 

~/miniconda3/envs/mpl/lib/python3.6/site-packages/numpy/lib/function_base.py in _vectorize_call(self, func, args)
   2824         else:
-> 2825             ufunc, otypes = self._get_ufunc_and_otypes(func=func, args=args)
   2826 

~/miniconda3/envs/mpl/lib/python3.6/site-packages/numpy/lib/function_base.py in _get_ufunc_and_otypes(self, func, args)
   2784             inputs = [arg.flat[0] for arg in args]
-> 2785             outputs = func(*inputs)
   2786 

~/dev/matplotlib/lib/matplotlib/dates.py in _to_ordinalf(dt)
    225 
--> 226     base = float(dt.toordinal())
    227 

AttributeError: 'numpy.str_' object has no attribute 'toordinal'

The above exception was the direct cause of the following exception:

ConversionError                           Traceback (most recent call last)
<ipython-input-3-e7444cbe73e7> in <module>()
      4 ticks = ['banana', 'apple', 'pear']
      5 plt.bar(xs, ys)
----> 6 plt.xticks(ticks)

~/dev/matplotlib/lib/matplotlib/pyplot.py in xticks(ticks, labels, **kwargs)
   1518         labels = ax.get_xticklabels()
   1519     elif labels is None:
-> 1520         locs = ax.set_xticks(ticks)
   1521         labels = ax.get_xticklabels()
   1522     else:

~/dev/matplotlib/lib/matplotlib/axes/_base.py in set_xticks(self, ticks, minor)
   3303             Default is ``False``.
   3304         """
-> 3305         ret = self.xaxis.set_ticks(ticks, minor=minor)
   3306         self.stale = True
   3307         return ret

~/dev/matplotlib/lib/matplotlib/axis.py in set_ticks(self, ticks, minor)
   1689         """
   1690         # XXX if the user changes units, the information will be lost here
-> 1691         ticks = self.convert_units(ticks)
   1692         if len(ticks) > 1:
   1693             xleft, xright = self.get_view_interval()

~/dev/matplotlib/lib/matplotlib/axis.py in convert_units(self, x)
   1491         except Exception as e:
   1492             raise munits.ConversionError('Failed to convert value(s) to axis '
-> 1493                                          'units: %r' % x) from e
   1494         return ret
   1495 

ConversionError: Failed to convert value(s) to axis units: ['banana', 'apple', 'pear']

@timhoffm timhoffm added this to the v3.0.3 milestone Dec 17, 2018
"""
if unit is None:
raise ValueError(
'Missing unit for StrCategoryConverter. This might be caused'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what's missing here is the mapping from category to float. Worried about using unit in a user facing error because unit is a kwarg that doesn't have to be passed in for categoricals to work.

Copy link
Member Author

@timhoffm timhoffm Dec 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worried about using unit in a user facing error because unit is a kwarg that doesn't have to be passed in for categoricals to work.

That's why I'm casting the error message to "Failed to convert..." using raise ... from.

So what's missing here is the mapping from category to float.

Not sure what that means. Moving the example from pyplot to ax to make it a little more obvious:

ax = plt.gca()
ax.bar([1, 2, 3], [50, 10, 20])
ax.set_xticks(['banana', 'apple', 'pear'])

We are trying to string positions (not labels) to a float axis. IMHO there's nothing missing here and this should error out. Ideally, the error message would be something like "Attemtping to set categorical ticks to a non-categorical axis".

But there's no good place to raise that.

The original stacktrace before this PR was:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-2-13c713392566> in <module>()
      2 ax = plt.gca()
      3 ax.bar([1, 2, 3], [50, 10, 20])
----> 4 ax.set_xticks(['banana', 'apple', 'pear'])

~/dev/matplotlib/lib/matplotlib/axes/_base.py in set_xticks(self, ticks, minor)
   3303             Default is ``False``.
   3304         """
-> 3305         ret = self.xaxis.set_ticks(ticks, minor=minor)
   3306         self.stale = True
   3307         return ret

~/dev/matplotlib/lib/matplotlib/axis.py in set_ticks(self, ticks, minor)
   1686         """
   1687         # XXX if the user changes units, the information will be lost here
-> 1688         ticks = self.convert_units(ticks)
   1689         if len(ticks) > 1:
   1690             xleft, xright = self.get_view_interval()

~/dev/matplotlib/lib/matplotlib/axis.py in convert_units(self, x)
   1488             return x
   1489 
-> 1490         ret = self.converter.convert(x, self.units, self)
   1491         return ret
   1492 

~/dev/matplotlib/lib/matplotlib/category.py in convert(value, unit, axis)
     51 
     52         # force an update so it also does type checking
---> 53         unit.update(values)
     54 
     55         str2idx = np.vectorize(unit._mapping.__getitem__,

AttributeError: 'NoneType' object has no attribute 'update'

So, you can raise

  1. in set_ticks(self, ticks, minor)
    Advantage: You know you are setting ticks, so you could issue "Attemtping to set categorical ticks to a non-categorical axis".
    Disadvantage: You need to explicitly check ticks and the unit of Axis.
  2. in convert_units(self, x)
    Advantage: You can investigate self.units and generate a more appropriate message related to categoricals.
    Disadvantage: You don't know that you are setting ticks here. You would have to explicitly check which converter you are using.
  3. in convert(value, unit, axis)
    Advantage: You know that unit must not be None so the check is trivial.
    Disadvantage: You don't know anything about categorical axes anymore and therefore can only state that unit is missing and add a guess why that might be the case.

In that situation I decided to raise on 3. add add context information in 2. using the raise .. from.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against the error, I worry that the specific word unit will lead users down a google rabbit hole that's misguided. Basically, there's an xunit/yunit kwarg:
https://matplotlib.org/gallery/units/bar_demo2.html#sphx-glr-gallery-units-bar-demo2-py

axs[1, 0].bar(cms, cms, bottom=bottom, width=width, xunits=inch, yunits=cm)

So I worry an error message with the word unit might make users think the fix is to pass in something, when really the issue is that the axis input data is not a string so the axis is not registered as string units. I wonder if the following is enough:

'Axis does not support StrCategoryConvertor; axis data values must be strings'

Like you, I want to help people catch that the problem is the mismatch between the plot data and the tick data.

Copy link
Member Author

@timhoffm timhoffm Jan 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased to

            raise ValueError(
                'Missing category information for StrCategoryConverter; '
                'this might be caused by unintendedly mixing categorical and '
                'numeric data')

I don't want to mention an Axis as we don't know the call context here. In the end, this is also why we can only guess the reason for the failure but not make a clear statement what to change.

@@ -49,6 +49,10 @@ def default_units(x, axis):
from matplotlib import cbook


class ConversionError(ValueError):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit bikesheddy but it looks more like a TypeError than a ValueError to me.

if unit is None:
raise ValueError(
'Missing unit for StrCategoryConverter. This might be caused'
'by unintendedly mixing categorical and numeric data.')
Copy link
Contributor

@anntzer anntzer Dec 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor preference for error messages that are single sentence and without a final dot, as that's what we mostly have in the codebase (so replace the middle dot by a semicolon).
Also a missing space after "caused".

@timhoffm timhoffm force-pushed the unit-conversion-error-message branch 4 times, most recently from 4cebc36 to e38410c Compare January 6, 2019 22:56
@timhoffm
Copy link
Member Author

Travis CI failure is due to #13137 and unrelated to this PR.

By me, this is ready to go in.

@@ -23,24 +23,30 @@
class StrCategoryConverter(units.ConversionInterface):
@staticmethod
def convert(value, unit, axis):
"""Converts strings in value to floats using
"""Convert strings in value to floats using
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just inherit the docstring from units.ConversionInterface perhaps? (and possibly edit that docstring).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can reasonably use an inherited docstring here. Using the conversion interface for string-index mapping is a bit of a stretch and justifies explicit description.

  • type of value is a string, which is rather the exception for ConversionInterface
  • mark unit as UnitData
  • note that axis is unused.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

@timhoffm timhoffm force-pushed the unit-conversion-error-message branch from e38410c to c9a0ac7 Compare January 12, 2019 16:14
Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyone can merge (including tim) when ci passes.

@timhoffm
Copy link
Member Author

Self-merging based on the reviews. The Travis CI failure is still due to #13137 (unrelated to this PR).

@timhoffm timhoffm merged commit c8933e0 into matplotlib:master Jan 12, 2019
@timhoffm timhoffm deleted the unit-conversion-error-message branch January 12, 2019 19:58
meeseeksmachine pushed a commit to meeseeksmachine/matplotlib that referenced this pull request Jan 12, 2019
@tacaswell tacaswell modified the milestones: v3.0.3, v3.1 Jan 14, 2019
@tacaswell
Copy link
Member

Re-milestoned to 3.1 so we do not have to deal with backporting the f string and because there is a subtle API change (the type of the raise exception) in this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants