Skip to content

Improve formatting of Table/Column.info() - #12022

Merged
taldcroft merged 1 commit into
astropy:mainfrom
eerovaher:table-stats-format
Sep 2, 2021
Merged

taldcroft merged 1 commit into
astropy:mainfrom
eerovaher:table-stats-format

Conversation

@eerovaher

@eerovaher eerovaher commented Aug 6, 2021 •

Copy link
Copy Markdown
Member

Description

Suppose you happen to have a Table like this:

import numpy as np

from astropy.table import Table


arr = np.arange(15, dtype=np.int32).reshape(5, 3)
t = Table(arr, names=('a', 'b', 'c'))
t['a'].format = '%.3f'
t['b'].format = '.2f'
t['c'].format = '{:.1f}'

Currently Table.info('stats') ignores the column formats:

>>> t.info('stats')
<Table length=5>
name mean        std        min max
---- ---- ----------------- --- ---
   a  6.0 4.242640687119285   0  12
   b  7.0 4.242640687119285   1  13
   c  8.0 4.242640687119285   2  14

With the changes I've made to the code the column formats would be applied to the statistics too:

>>> t.info('stats')
<Table length=5>
name  mean  std   min   max  
---- ----- ----- ----- ------
   a 6.000 4.243 0.000 12.000
   b  7.00  4.24  1.00  13.00
   c   8.0   4.2   2.0   14.0

I am opening this pull request as a draft for initial evaluation and to present a proof of concept.

EDIT:

As a result of discussion this pull request does not allow the output of Table.info('stats') to be formatted using the format attribute of the table columns, but it will apply a better default formatting to numerical values instead.

EDIT2 (AFTER MERGING):

Fixes #6962

Checklist for package maintainer(s)

This checklist is meant to remind the package maintainer(s) who will review this pull request of some common things to look for. This list is not exhaustive.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see "When to rebase and squash commits".
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the Extra CI label.
  • Is a change log needed? If yes, did the change log check pass? If no, add the no-changelog-entry-needed label.
  • Is a milestone set? Milestone must be set but astropy-bot check might be missing; do not let the green checkmark fool you.
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate backport-X.Y.x label(s) before merge.

@github-actions github-actions Bot added the utils label Aug 6, 2021
@github-actions

github-actions Bot commented Aug 6, 2021

Copy link
Copy Markdown
Contributor

👋 Thank you for your draft pull request! Do you know that you can use [ci skip] or [skip ci] in your commit messages to skip running continuous integration tests until you are ready?

@eerovaher eerovaher mentioned this pull request Aug 6, 2021
9 tasks done
@pllim pllim added this to the v5.0 milestone Aug 6, 2021

@nstarman nstarman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@eerovaher I really like this idea and it might actually be worth pushing further: v5.0 is coming up and now is the time to change the default behavior. So let's make respecting the format the new default!

Comment thread astropy/utils/data_info.py Outdated


def data_info_factory(names, funcs):
def data_info_factory(names, funcs, apply_format=False):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's make apply_format=True the default!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This code is used to enable two options for info(), which are 'attributes' and 'stats'. The default option is 'attributes', which prints the dtype, unit, format and description attributes of the columns. It doesn't make much sense to try to apply the column format to those attributes, so it is not clear that setting apply_format=True by default would be useful.

Furthermore, setting the default value to True might cause problems to anyone who might be using this factory to enable more options.

@taldcroft

Copy link
Copy Markdown
Member

I'm slightly ambivalent about this since you have functions like std where a reasonable precision for the values may not be reasonable for the std. E.g. if the data values have a mean of 1_000_000 but are very close with a std of 0.01.

What about formatting floating point outputs with 6g by default? I think in 99% of cases this is fine and will give a clean readable output.

Beyond that there are some technical problems with the current implementation.

  • Integer values with a format like 05d
  • You cannot really apply simple text-based logic like if fmt.startswith('{').

Perhaps the right thing (if we really want to inherit the column formatting, which I'm not convinced of) would be to mimic the pprint logic and do a try/except for each of these possibilities in order:

# From pprint.py
def _possible_string_format_functions(format_):
    """Iterate through possible string-derived format functions.

    A string can either be a format specifier for the format built-in,
    a new-style format string, or an old-style format string.
    """
    yield lambda format_, val: format(val, format_)
    yield lambda format_, val: format_.format(val)
    yield lambda format_, val: format_ % val

Then if all of those fail, fall through to no formatting. This will allow for integer formats that don't apply to floating point info values.

@eerovaher

Copy link
Copy Markdown
Member Author

I have to start by clarifying that calling t.info() only prints a summary of the table, it's return value is always None. The code examples I had provided were misleading because they included print(t.info()) and I have corrected them.

I'm slightly ambivalent about this since you have functions like std where a reasonable precision for the values may not be reasonable for the std. E.g. if the data values have a mean of 1_000_000 but are very close with a std of 0.01.

If the user has specified a column format that makes the values indistinguishable then it should be expected that the standard deviation would appear to be zero. If the user actually wants to use the standard deviation for something then they can still compute it and the formatting does not affect the value they obtain.

What about formatting floating point outputs with 6g by default? I think in 99% of cases this is fine and will give a clean readable output.

I see no reason why the user shouldn't be able to specify the formatting of the stats and using the column format would be a very natural way of achieving that. That being said the mean and standard deviation of an integer column are floats, so the column format might not be applicable. In that case falling back to a sensible default like 6g would indeed be a good idea.

Beyond that there are some technical problems with the current implementation.

* Integer values with a format like `05d`

* You cannot really apply simple text-based logic like `if fmt.startswith('{')`.

Perhaps the right thing (if we really want to inherit the column formatting, which I'm not convinced of) would be to mimic the pprint logic and do a try/except for each of these possibilities in order:

# From pprint.py
def _possible_string_format_functions(format_):
    """Iterate through possible string-derived format functions.

    A string can either be a format specifier for the format built-in,
    a new-style format string, or an old-style format string.
    """
    yield lambda format_, val: format(val, format_)
    yield lambda format_, val: format_.format(val)
    yield lambda format_, val: format_ % val

I don't understand why it is necessary to try these different options, but maybe I'm just not familiar enough with string formatting. Can you provide an example that does not work with the text-based logic?

Then if all of those fail, fall through to no formatting. This will allow for integer formats that don't apply to floating point info values.

What I would propose is that the stats of a column should be formatted using the column format if it is specified. If the format is not applicable or it is not specified then applying a sensible default should be attempted. If that fails then then the code should fall back to current behavior.

@taldcroft

Copy link
Copy Markdown
Member

I don't understand why it is necessary to try these different options, but maybe I'm just not familiar enough with string formatting. Can you provide an example that does not work with the text-based logic?

In [21]: t = simple_table()                                                                                                                          

In [22]: t['b'].info.format = '${:.2f}'                                                                                                              

In [23]: t                                                                                                                                           
Out[23]: 
<Table length=3>
  a      b     c  
int64 float64 str1
----- ------- ----
    1   $1.00    c
    2   $2.00    d
    3   $3.00    e

@taldcroft

Copy link
Copy Markdown
Member

Another thing to remember is that this is user-customizable for any functions, so applying the column format won't necessarily be appropriate in those cases (as indeed it is already not appropriate for 'attributes').

I'm not too keen on the behavior of this simple convenience function getting complicated with the various fall-throughs (like going to 6g for int columns where the func(col) is float, but going to str(val) where that doesn't apply).

I also think this is a bit of the tail wagging the dog, since the PR was mostly driven by the need to contrive a doc example that looks tidy and passes tests. But the table in the doc example would not normally require format attributes to be set, so putting them in just so the stats are formatted is not something a user would really do.

@taldcroft

Copy link
Copy Markdown
Member

If the user has specified a column format that makes the values indistinguishable then it should be expected that the standard deviation would appear to be zero.

An important thing in astropy is to understand that there are a huge world of use-cases and it is important to try avoiding making assumptions based on what you believe is sensible or expected. In particular if someone chooses to format floats to the nearest int .0f because the table needs to be compact, they may still want to see the std to full precision.

The worst case is to give an answer that appears to be definitive (e.g. std=0) but requires understanding of astropy details ("oh, the .0f format was applied to the real answer of 0.489232"). The user might have read the table in from an ECSV file they didn't create and not even be aware of the formatting.

@eerovaher

Copy link
Copy Markdown
Member Author

There are two questions that need a definitive answer.

  1. Should it be possible for the user to specify the formatting of the stats? This does not necessarily mean that the output of the 'stats' option would have to be formatted (although this PR originally proposed that it should be). It would also be possible to create a new 'formatted_stats' option so that the user would have full control over whether or not they want to apply column formats, and implementing a separate option for formatted output would not be any more complicated than changing the 'stats' option would be.
  2. Should the default format for the stats be something different from what it is now? This is an example of how that might look like:
t.info('stats')    # with default format {:g}
<Table length=5>
name mean   std   min max
---- ---- ------- --- ---
   a    6 4.24264   0  12
   b    7 4.24264   1  13
   c    8 4.24264   2  14

If 1. is considered to be worthwhile then it can be implemented. 2. can be implemented with or without 1.

@taldcroft

Copy link
Copy Markdown
Member

My opinion is that this is not something where we really need the flexibility / control implied by (1). For reference there is no way to configure the output format of pandas describe().

In [12]: tp.describe()                                                                                 
Out[12]: 
               a          b
count   3.000000   3.000000
mean   16.666667  16.666667
std    24.542480  24.542480
min     2.000000   2.000000
25%     2.500000   2.500000
50%     3.000000   3.000000
75%    24.000000  24.000000
max    45.000000  45.000000

So IMO just a simple change of formatting all float outputs with {:g} would be an improvement over the current behavior and would be sufficient for the goals of info().

Others may disagree and a free discussion here is a good thing.

`Table.info()` and `Column.info()` now attempt to format values as
`f'{val:g}'`.
@eerovaher eerovaher changed the title Make Table.info('stat') respect column format Improve formatting of Table/Column.info() Aug 18, 2021
@eerovaher
eerovaher marked this pull request as ready for review August 18, 2021 23:29
@eerovaher

Copy link
Copy Markdown
Member Author

Who is supposed to review this?

@taldcroft

Copy link
Copy Markdown
Member

@eerovaher - I'll review, though not today since I am completely tied up. Sorry for the delay, I didn't realize that there was an update. (Probably a good idea to @ mention someone if you make some progress on a PR, new commits may not get the attention of reviewers).

@pllim
pllim requested a review from taldcroft September 1, 2021 14:52

@taldcroft taldcroft left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome, thanks @eerovaher !

@taldcroft
taldcroft merged commit 06fbc15 into astropy:main Sep 2, 2021
@eerovaher
eerovaher deleted the table-stats-format branch September 16, 2021 14:20
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.

Info "stats" looks horrible (since numpy 1.14)

4 participants