-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Feature: Support passing DataFrames to table.table #28830
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution. The solution is slightly more complex because pandas is an optional dependency.
Also this should get a test. Something like (unchecked - you may need to slightly adapt it to make it work):
def test_table_dataframe():
df = pd.DataFrame([[1, 2], [3, 4]], columns=['a', 'b'])
fig, ax = plt.subplots()
table = ax.table(df)
assert table[0, 0] == 'a'
assert table[0, 1] == 'b'
assert table[1, 0] == '1'
assert table[1, 1] == '2'
assert table[2, 0] == '3'
assert table[2, 1] == '4'
…andle Pandas DataFrame Objects
…y and _is_pandas_dataframe with associated test.
Pytest test_table.py::test_table_dataframeUnit Test
ResultPassed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should get a whats new note. See https://matplotlib.org/devdocs/devel/api_changes.html#what-s-new-notes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight concern about the wording in the what's new so gonna give you time to address, but otherwise I think this is a great addition!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To fix the failing tests, add pandas to the testing requirements
I think to https://github.com/matplotlib/matplotlib/blob/main/requirements/testing/minver.txt or limit the new tests to 3.10+ like the xarray tests
I'm honestly extra confused why those tests don't find pandas since it's in the extra testing
This is where I am confused also. For now, , Ill modify the the mini version file to include pandas and hopefully, that test can run correctly. |
I think I found the problem. Inside the tests.yml file, the extra-requirements line is missing from the other workflows. We could add pandas to all.txt, but I suggest against it because it is not a default dependency. So instead, Ill add the extra-requirements line to the other workflows.
For the minimum versions workflow, minver.txt doesn't add dependencies, but defines the package versions for the all.txt file. Ill revert my changes there. |
Sorry for the backtracking, but I think instead of changing the minimum version tests to run everything, you should flag this test to only run when pandas is installed. |
lib/matplotlib/table.py
Outdated
@@ -674,7 +676,7 @@ def table(ax, | |||
|
|||
Parameters | |||
---------- | |||
cellText : 2D list of str, optional | |||
cellText : 2D list of str, Pandas.DataFrame, optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we want to call out pandas data frames here. How do we do this elsewhere? I'd just state "object" and then below, state that
"""
If cellText
is not a list of texts, we attempt to access cellText.columns.to_numpy()
for the column headers,
and cellText.to_numpy()
for the table's cells.
"""
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have any other functions explicitly take DataFrames. Typically, DataFrame is covered by what we call array-like
(and internally runs via to_numpy()
. It's different here because we take the column headers as well. Due to this particular usage, we've chosen to explicitly check for DataFrames (_is_pandas_dataframe()
). I believe that duck-typing would be a bit too vague here: An object with obj.columns.to_numpy()
and obj.to_numpy()
is very particular. I'm not sure if this matches anything but pandas.DataFrame - and if there exists other such objects, I'm not clear that accepting them is appropriate. If the need for similar objects should arise, we can always revisit. For now, the explicit type is the simplest documentation and matches implementation:
cellText : 2D list of str, Pandas.DataFrame, optional | |
cellText : 2D list of str or pandas.DataFrame, optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I linked an example of another very popular package, polars, that has the same form of data frame. I'm sure there are others out there. I don't think we should be special casing pandas here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Polars is logically equivalent, but it does not have the same API. See #28830 (comment).
There are several other dataframes out their (https://data-apis.org/dataframe-api/draft/purpose_and_scope.html#history-and-dataframe-implementations), but the problem is that their API is currently not consistent. IMHO we therefore would have to special case to support multiple of them.
While I appreciate the attempt to duck-type instead of explicitly relying on types, I believe the current dataframe APIs are to inhomogeneous to support reasonable duck-typing. Any object that supports obj.columns.to_numpy()
and obj.to_numpy()
is too technical - i suspect that at least half of the pandas users would not know whether that holds for pandas.DataFrame
or not. Duck-typing would work if dataframe-like
was well-defined, but it is currently not (see the draft dataframe API standard).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think np.asanyarray(df.columns)
works on both pandas and polars, and is pretty straightforward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you really want to push for duck-typing, go for it. But I'd require reasonable type stubs (accepting Any
is not good enough) and type documentation (users should at least easily see that pandas.DataFrame as the most common dataframe type is supported).
Ideally, that claimed behavior would also be tested. (It's easy for pandas, because it's a test dependency. For others, you'd likely need a mock similar to test_unpack_to_numpy_from_torch
.)
I personally believe it's not worth the added effort and would advise against requiring this from a first-time contribution. When accepting the limited scope of pandas, this PR is almost ready apart from minor documentation issues and the type stub. I therefore suggest to take this PR as a limited but well scoped improvement. We don't paint us in any corner with that. You can generalize in a followup PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I'd require reasonable type stubs
OK, but I dont' think you will get pandas dataframes in a typestub from this PR either, will you? I'm personally pretty 👎 on losing functionality because of type stubbing issues.
@@ -744,6 +746,13 @@ def table(ax, | |||
cols = len(cellColours[0]) | |||
cellText = [[''] * cols] * rows | |||
|
|||
# Check if we have a Pandas DataFrame | |||
if _is_pandas_dataframe(cellText): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd actually make this more generic, and check if cellText
has a columns attribute and a to_numpy
method. That lets non-pandas objects also work; for instance polars is a pandas competitor, and there is no reason its dataframes could not be passed in here: https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.columns.html
https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.to_numpy.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that polars.DataFrame.columns
is of type list[str]
, so the current implementation using cellText.columns.to_numpy()
would fail. These are exactly the subtleties I did not want to go into as part of this first-time contributor PR. Let's start with pandas, which is a clean addition. We can always generalize later, which should likely conform to the dataframe API standard (note that this is still draft)
As a side-remark: The table implementation has lots of design problems. If one was serious about tables, the whole Table
would need to be rewritten/replaced. Therefore, I wouldn't spend too much effort on trying to improve implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs an update to the table stub in tables.pyi
Sure, Ill revert my changes and add a flag instead. Thank you all for the thorough review of my commits. I'm learning a lot👍🏾. About the polars package, I dont mind repeating what I did here and make another PR for it. If it is as popular as you say, I can see the value in adding support. |
When trying to commit the tables.pyi file, I get this error from pre-commit.
from this change
Im not sure how to fix this unless I outright import the pandas module. |
I believe it should work to
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bunch of hopefully small things and also this PR will need two approvals
lib/matplotlib/table.pyi
Outdated
from typing import Any, Literal | ||
from typing import Any, Literal, TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having an if TYPE_CHECKING:
in a pyi is kind of redundant, as pyi is only used for type checking.
This does introduce a type-check time requirement on pandas
, which I don't fully love, but is not that bad. The if/else here does not protect against not having Pandas (and not even sure a try/except would work in a pyi
... it would at least be unusual)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll reiterate that I don't think we should be special casing pandas at all, and we should not have a pandas dataframe as a type annotation. Is there anywhere else in the library that we do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I import the DataFrame method directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following #28726 (comment), I compared with the pandas implementation:
We should mingle columns and index into rowLabels
and colLabels
: If rowLabels
/ colLabels
are not given, use index / columns as the respective labels.
We have two options for handling these with additional explicit labels:
- explicit labels take precedence and overwrite index/columns values
- we error if both are given.
While 1) could be convenient to overwrite the labels, I'm inclined to go with 2) - in the face of ambiguity, refuse the temptation to guess. We could always expand to 1) later if the need would arise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this works, but I think we should absolutely support overwriting the pandas column and row labels b/c I'm pretty sure that would be my use case at least half the time.
I don't think 1. is guessing b/c they're explicitly passing in labels.
@story645 adding the support for row/column labels is a straight forward extension that can be added later. I thought it might be nice to get a basic version in (i) because there already has been a lot of discussion and I don't want to overburden the PR with additional tweaks, in particular since this is a first-time contribution, and (ii) because it would be nice to push this into 3.10, which we most likely will miss when doing additional changes. |
I'll go and merge as is. Titles can be handled in a followup PR. |
@anijjar thanks and congratulations on your first contribution to Matplotlib 🎉. We hope to see you again. |
PR summary
Resolves #28726 : feature request: support passing DataFrames to table.table
dawnwangcx wanted a feature to enable building tables using Pandas's DataFrame object. I implemented his solution and made a Unit test in test_table.py to confirm the DataFrame was correctly converted into a table.
PR checklist