has2k1 / plotnine

A Grammar of Graphics for Python
https://plotnine.org
MIT License
4.07k stars 225 forks source link

How to draw subplots? #46

Open anhqle opened 7 years ago

anhqle commented 7 years ago

plotnine is made using matplotlib as the back-end, so I'm guessing there must be a way to draw subplots (without using faceting).

Is there a way to do so? I'd be happy to contribute to the documentation if someone points out a solution.

has2k1 commented 7 years ago

You cannot draw subplots. It will be implemented when Matplotlib gets a better layout manager. At the moment, it would be hard to implement.

anhqle commented 7 years ago

Thanks for the quick reply! Is there not even any workaround, such as assigning plotnine plots to variables and print them out in a grid?

has2k1 commented 7 years ago

There is no work around. Subplot layouts in Matplotlib are not a flexible as they could be, the solution is a better layout manager.

jklymak commented 7 years ago

@has2k1 I've been working on the layout manager, but I'm not clear what flexibility you are looking for. Since I'm looking for use-cases, can you elaborate (either here or in https://github.com/matplotlib/matplotlib/issues/1109)? Thanks!

has2k1 commented 7 years ago

@jklymak, you declared your interest in tacking the layout issue within a day of me finally fiddling with Tillstens examples and trying to figure out what it would take to come up with a workable MEP. What I suggested as a plausible way forward has at the minimum (points 1 and 2) the flexibility that would be useful to plotnine.

From the your updates in that thread, I got more assured that the direction you were taking had the main concerns of this project covered. Nonetheless, I intended on working on top of your exploration to make sure of that. That would allow me to raise more substantive concerns. So far I have been reluctant to do that as I think the code you have is still in a lot of flux.

I will create a notebook to track the requirements of this project and test implementations using the constraint layout.

jklymak commented 7 years ago

@has2k1

Sure - you can check out my draft MEP at https://github.com/jklymak/mplconstrainedlayout/blob/master/DraftMEP.ipynb

Its pretty rudimentary. OTOH, I've actually implemented most of it.

gokceneraslan commented 6 years ago

Is the layout manager in matplotlib v2.2 sufficient for proper subplots in plotnine?

has2k1 commented 6 years ago

@gokceneraslan, the layout manager is still experimental and I expect it to be lacking in some ways, the solution to which is to use it in an experimental branch in plotnine and make upstream contributions where necessary.

louis-vines commented 6 years ago

Sorry to wade in on this but this is the main piece of ggplot2 functionality I'm missing with plotnine and I'm using a lot more python these days. Is there any progress on this or is it still blocked by the matplotlib layout manager?

has2k1 commented 6 years ago

There is now an experimental constraint layout manager in Matplotlib. It is not yet ready for prime time.

jklymak commented 6 years ago

What is missing from the layout manager?

has2k1 commented 6 years ago

@jklymak, These are my notes since when it was merged.


I have not yet followed up on them.

dschneiderch commented 5 years ago

once there is a viable plotting manager, I find https://github.com/wilkelab/cowplot indispensable when working with ggplot in R so you might take some inspiration from that project for a feature set.

has2k1 commented 5 years ago

@dschneiderch, such a plotting manager would be very helpful, but it is hard to build since matplotlib is not made of nice components that can be easily moved around.

dschneiderch commented 5 years ago

That's too bad. It seems though that some basic arranging should be possible with matplotlib.gridspec, no? I was looking over these answers at SO, but admittedly haven't quite figured it out yet. https://stackoverflow.com/questions/34028255/set-height-and-width-of-figure-created-with-plt-subplots-in-matplotlib https://stackoverflow.com/questions/47535866/how-to-iteratively-populate-matplotlib-gridspec-with-a-multipart-seaborn-plot

has2k1 commented 5 years ago

Thanks for those links. Yes, the basic stuff would be possible with gridspec and then the rest next to impossible. The problem is with plot labels, facets and legends i.e the items that fall outside the main plot area.

I will try to wrap my head around the axis copying technique together with gridspecs and see how much mileage we can get from that.

Other links https://stackoverflow.com/questions/6309472/matplotlib-can-i-create-axessubplot-objects-then-add-them-to-a-figure-instance/46906599#46906599

ghost commented 4 years ago

Any updates on this issue and the layout manager?

has2k1 commented 4 years ago

Any updates on this issue and the layout manager?

Such a layout manager is not possible, someone familiar with Matplotlib way more than me confirmed. So we have to find a definitely lesser (but still hard) solution.

peacej commented 4 years ago

Such a feature would be hugely helpful, given how much people use cowplot/gridExtra for R ggplot2. I understand that it's hard...

has2k1 commented 3 years ago

@ponnhide, Thanks for effort. I will certainly look at it more carefully. I thought about something similar (encapsulating the axes ...) but I had no idea how well it could work. The results are promising.

kendavidn commented 2 years ago

Thank you @ponnhide and @has2k1 for your amazing work. A friend and I are actually following this issue closely, and plan to switch our analysis workflow from R to Python once something like patchworklib is implemented for plotnine!

(We're academics and often need to make complex multi-panel figures for papers)

louis-vines commented 2 years ago

@ponnhide i've been following this thread since Aug 2018. Thank you so much for this contribution! I am looking forward to playing with this next week...

ponnhide commented 2 years ago

Hi, I'm the developer of patchworklib. This package provides the function to arrange multiple plotnine, matplotlib, and seaborn plots quickly using only the | and / as the patchwork of the R library.

When I posted here previously, I realized the package still had many bugs and problems, so I removed my posts once. Now, in most cases, patchworklib can properly handle plotnine plots as subplots, and you can align them with other plotnine, matplotlib, and seaborn plots. The following code is the simple example script to arrange multiple plotnine plots using patchworklib.

import patchworklib as pw
from plotnine import *
from plotnine.data import *
g1 = (ggplot(mtcars) + geom_point(aes("mpg", "disp")))
g2 = (ggplot(mtcars) + geom_boxplot(aes("gear", "disp", group="gear")))
g3 = (ggplot(mtcars, aes('wt', 'mpg', color='factor(gear)')) + geom_point() + stat_smooth(method='lm') + facet_wrap('~gear'))
g4 = (ggplot(data=diamonds) + geom_bar(mapping=aes(x="cut", fill="clarity"), position="dodge"))

g1 = pw.load_ggplot(g1, figsize=(2,3))
g2 = pw.load_ggplot(g2, figsize=(2,3))
g3 = pw.load_ggplot(g3, figsize=(3,3))
g4 = pw.load_ggplot(g4, figsize=(5,2))
g1234 = (g1|g2|g3)/g4
g1234.savefig()

download

If you are interested in patchworklib, please try to use the following example codes on Google colab.

If you face some problems, please let me know.

benjaminleroy commented 2 years ago

Hi there,

Super appreciative of plotnine and impressed by the work done in patchworklib. @wangmallory and I have also started working on a python package similar to cowplot and gridExtra (though we also plan to do some patchwork stuff as well). In the backend we convert everything to svg objects (which may be seen a a bit little more hacky) but does means we avoid some limitations of matplotlib. 

We just completed our first MVP and welcome people also checking the package (documentation can be found here).

Here’s a following example of simple approach

import cowpatch as cow #our package
import plotnine as p9
import plotnine.data as p9_data
import numpy as np

# creation of some some ggplot objects
g0 = p9.ggplot(p9_data.mpg) +\
    p9.geom_bar(p9.aes(x="hwy")) +\
    p9.labs(title = 'Plot 0')

g1 = p9.ggplot(p9_data.mpg) +\
    p9.geom_point(p9.aes(x="hwy", y = "displ")) +\
    p9.labs(title = 'Plot 1')

g2 = p9.ggplot(p9_data.mpg) +\
    p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\
    p9.labs(title = 'Plot 2')

vis_patch = cow.patch(g0, g1, g2)
vis_patch += cow.layout(design = np.array([[0,1],
                                           [0,2]]),
                        rel_heights = [1,2])
vis_patch.show(width = 11, height = 7)

ponnhide commented 2 years ago

Please let me report here. I have now updated patchworklib to support plotnine v0.9.0 and the inheritance of the plotnine theme (The previous version of patchworklib cannot handle style settings of plotnine properly). Now, you can freely arrange multiple plotnine plots using patchworklib as follows.

import patchworklib as pw
from plotnine import *
from plotnine.data import *
g1 = pw.load_ggplot(ggplot(mtcars) 
                    + geom_point(aes("mpg", "disp"))
                    + theme(text=element_text(size=14), axis_title=element_text(size=18)),
                    figsize=(2,3))
g2 = pw.load_ggplot(ggplot(mtcars) 
                    + geom_boxplot(aes("gear", "disp", group="gear"))
                    + theme(text=element_text(size=14), axis_title=element_text(size=18)), 
                    figsize=(2,3))
g3 = pw.load_ggplot(ggplot(mtcars, aes('wt', 'mpg', color='factor(gear)')) 
                    + geom_point() + stat_smooth(method='lm') 
                    + facet_wrap('~gear')
                    + theme(text=element_text(size=14), axis_title=element_text(size=18)), 
                    figsize=(3,3))
g4 = pw.load_ggplot(ggplot(data=diamonds) 
                    + geom_bar(mapping=aes(x="cut", fill="clarity"), position="dodge")
                    + theme(text=element_text(size=14), axis_title=element_text(size=20, color="blue"), 
                            legend_text=element_text(size=14), legend_title=element_text(size=18)), 
                    figsize=(5,2))
g1234 = (g1|g2|g3)/g4
g1234.savefig()

Furthermore, by using the newest version of patchworklib and plotnine, you can draw a scatter plot with marginal distributions as follows.

import patchworklib as pw
from plotnine import *
from plotnine.data import *

g1 = pw.load_ggplot(ggplot(mpg, aes(x='cty', color='drv', fill='drv')) +
                    geom_density(aes(y=after_stat('count')), alpha=0.1) +
                    scale_color_discrete(guide=False) +
                    theme(axis_ticks_major_x=element_blank(),
                          axis_text_x =element_blank(),
                          axis_title_x=element_blank(),
                          axis_text_y =element_text(size=12),
                          axis_title_y=element_text(size=14),
                          legend_position="none"),
                    figsize=(4,1))

g2 = pw.load_ggplot(ggplot(mpg, aes(x='hwy', color='drv', fill='drv')) +
                    geom_density(aes(y=after_stat('count')), alpha=0.1) +
                    coord_flip() +
                    theme(axis_ticks_major_y=element_blank(),
                          axis_text_y =element_blank(),
                          axis_title_y=element_blank(),
                          axis_text_x =element_text(size=12),
                          axis_title_x=element_text(size=14)
                         ),
                    figsize=(1,4))

g3 = pw.load_ggplot(ggplot(mpg) +
                    geom_point(aes(x="cty", y="hwy", color="drv")) +
                    scale_color_discrete(guide=False) +
                    theme(axis_text =element_text(size=12),
                          axis_title=element_text(size=14)
                         ),
                    figsize=(4,4))

pw.param["margin"] = 0.2
(g1/(g3|g2)[g3]).savefig() #By specifying g3 in (g3|g2), g1 is positioned exactly on g3. 

If you face any problems, please let me know by creating issues on Github of patchworklib.

Ubadub commented 8 months ago

@has2k1 any update on including this in a future version of plotnine? I see you dropped it as a goal from 0.10.0.

@ponnhide I believe your solution does not work with the latest version(s) of plotnine, as reported at your repo.

has2k1 commented 8 months ago

@has2k1 any update on including this in a future version of plotnine? I see you dropped it as a goal from 0.10.0.

Putting a version number was too ambitious. This is a target we are already carefully working towards.

lvlh2 commented 3 months ago

I managed to figure out a way to utilize plt.subplots() in matplotlib to arrange subplots in plotnine. The idea basically came from using imread and imshow, and it seems that the result is not bad:

import matplotlib.pyplot as plt
from plotnine import (
    aes,
    facet_wrap,
    geom_bar,
    geom_boxplot,
    geom_point,
    geom_smooth,
    ggplot,
)
from plotnine.data import diamonds, mtcars

p1 = ggplot(mtcars) + geom_point(aes('mpg', 'disp'))

p2 = ggplot(mtcars) + geom_boxplot(aes('gear', 'disp', group='gear'))

p3 = (
    ggplot(mtcars, aes('wt', 'mpg', color='factor(gear)'))
    + geom_point()
    + geom_smooth(method='lm')
    + facet_wrap('gear')
)

p4 = ggplot(data=diamonds) + geom_bar(
    mapping=aes(x="cut", fill="clarity"), position="dodge"
)

for i, pic in enumerate([p1, p2, p3, p4], start=1):
    pic.save(f'pic_{i}.png', width=8, height=6, dpi=300)

fig, axes = plt.subplots(2, 2, figsize=(10, 8), dpi=300)

for i, ax in enumerate(axes.flat, start=1):
    img = plt.imread(f'pic_{i}.png')
    ax.imshow(img)
    ax.set_axis_off()

fig.tight_layout()

fig.savefig('subplots.png')

subplots

stephematician commented 1 month ago

What's the current status?

Anything else?

ponnhide commented 1 month ago

@stephematician, I have updated Patchworklib to support the newest version of plotnine. At least, it seems to work for basic examples. https://colab.research.google.com/drive/1zK_3_bFRYDIO-xLIvo1kzQxXGSk0ptuP?usp=sharing

It probably can't fully support plotnine yet, but it should help to a certain extent.

khaeru commented 1 month ago

Does it make sense to have a portion of the plotnine test suite to stabilize API features needed by patchworklib or others? For me it would be great to be able to rely on continued compatibility of the two packages.

A strategy I've observed and tried before:

  1. Use Pytest mark features to declare and use a custom test mark like @pytest.mark.for_patchworklib.
  2. For the main test suite/GitHub Actions job/check, use -m "not for_patchworklib" to exclude tests that are marked this way. This way, failures of these tests are not necessarily an obstacle to merging PRs.
  3. Configure an additional GHA job to check only tests with the mark: -m for_patchworklib. This could be run less often than the main job, and be for information only.

The added tests can be developed in different ways, e.g.:

If it helps I could make a PR to illustrate this approach.