{"id":415,"date":"2021-06-10T09:50:04","date_gmt":"2021-06-10T09:50:04","guid":{"rendered":"http:\/\/www.reades.com\/?p=415"},"modified":"2021-06-10T09:50:04","modified_gmt":"2021-06-10T09:50:04","slug":"seaborn-plots-with-2-legends","status":"publish","type":"post","link":"http:\/\/www.reades.com\/wp\/?p=415","title":{"rendered":"Seaborn Plots with 2 Legends"},"content":{"rendered":"<p>\t\t\t\tPosted here because I will inevitably forget this painfully worked-out answer for having legends for two different types of plots in Seaborn&#8230;<\/p>\n<p><code><span class=\"hljs-keyword\">import<\/span> numpy <span class=\"hljs-keyword\">as<\/span> np<br \/>\n<span class=\"hljs-keyword\">import<\/span> pandas <span class=\"hljs-keyword\">as<\/span> pd<br \/>\n<span class=\"hljs-keyword\">import<\/span> seaborn <span class=\"hljs-keyword\">as<\/span> sns<br \/>\n<span class=\"hljs-keyword\">import<\/span> matplotlib.pyplot <span class=\"hljs-keyword\">as<\/span> plt<\/p>\n<p><span class=\"hljs-comment\"># We will need to access some of these matplotlib classes directly<\/span><br \/>\n<span class=\"hljs-keyword\">from<\/span> matplotlib.lines <span class=\"hljs-keyword\">import<\/span> Line2D <span class=\"hljs-comment\"># For points and lines<\/span><br \/>\n<span class=\"hljs-keyword\">from<\/span> matplotlib.patches <span class=\"hljs-keyword\">import<\/span> Patch <span class=\"hljs-comment\">#\u00a0For KDE and other plots<\/span><br \/>\n<span class=\"hljs-keyword\">from<\/span> matplotlib.legend <span class=\"hljs-keyword\">import<\/span> Legend<br \/>\n<span class=\"hljs-keyword\">from<\/span> matplotlib <span class=\"hljs-keyword\">import<\/span> cm<\/p>\n<p><span class=\"hljs-comment\"># Initialise random number generator<\/span><br \/>\nrng = np.random.default_rng(seed=<span class=\"hljs-number\">42<\/span>)<\/p>\n<p><span class=\"hljs-comment\"># Generate sample of 25 numbers<\/span><br \/>\nn = <span class=\"hljs-number\">25<\/span><\/p>\n<p>clusters = []<\/p>\n<p><span class=\"hljs-keyword\">for<\/span> c <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-built_in\">range<\/span>(<span class=\"hljs-number\">0<\/span>,<span class=\"hljs-number\">3<\/span>):<br \/>\n<span class=\"hljs-comment\">\u00a0 \u00a0 # Crude way to get different distributions<\/span><br \/>\n<span class=\"hljs-comment\">\u00a0 \u00a0 #\u00a0for each cluster<\/span><br \/>\np = rng.integers(low=<span class=\"hljs-number\">1<\/span>, high=<span class=\"hljs-number\">6<\/span>, size=<span class=\"hljs-number\">4<\/span>)<br \/>\ndf = pd.DataFrame({<br \/>\n<span class=\"hljs-string\">'x'<\/span>: rng.normal(p[<span class=\"hljs-number\">0<\/span>], p[<span class=\"hljs-number\">1<\/span>], n),<br \/>\n<span class=\"hljs-string\">'y'<\/span>: rng.normal(p[<span class=\"hljs-number\">2<\/span>], p[<span class=\"hljs-number\">3<\/span>], n),<br \/>\n<span class=\"hljs-string\">'name'<\/span>: <span class=\"hljs-string\">f\"Cluster <span class=\"hljs-subst\">{c+<span class=\"hljs-number\">1<\/span>}<\/span>\"<\/span><br \/>\n})<br \/>\nclusters.append(df)<\/p>\n<p><span class=\"hljs-comment\"># Flatten to a single data frame<\/span><br \/>\nclusters = pd.concat(clusters)<\/p>\n<p><span class=\"hljs-comment\"># Now do the same for data to feed into<\/span><br \/>\n<span class=\"hljs-comment\"># the second (scatter) plot... <\/span><br \/>\nn = <span class=\"hljs-number\">8<\/span><br \/>\npoints = []<\/p>\n<p><span class=\"hljs-keyword\">for<\/span> c <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-built_in\">range<\/span>(<span class=\"hljs-number\">0<\/span>,<span class=\"hljs-number\">2<\/span>):<\/p>\n<p>p = rng.integers(low=<span class=\"hljs-number\">1<\/span>, high=<span class=\"hljs-number\">6<\/span>, size=<span class=\"hljs-number\">4<\/span>)<\/p>\n<p>df = pd.DataFrame({<br \/>\n<span class=\"hljs-string\">'x'<\/span>: rng.normal(p[<span class=\"hljs-number\">0<\/span>], p[<span class=\"hljs-number\">1<\/span>], n),<br \/>\n<span class=\"hljs-string\">'y'<\/span>: rng.normal(p[<span class=\"hljs-number\">2<\/span>], p[<span class=\"hljs-number\">3<\/span>], n),<br \/>\n<span class=\"hljs-string\">'name'<\/span>: <span class=\"hljs-string\">f\"Group <span class=\"hljs-subst\">{c+<span class=\"hljs-number\">1<\/span>}<\/span>\"<\/span><br \/>\n})<br \/>\npoints.append(df)<\/p>\n<p>points = pd.concat(points)<\/p>\n<p><span class=\"hljs-comment\"># And create the figure<\/span><br \/>\nf, ax = plt.subplots(figsize=(<span class=\"hljs-number\">8<\/span>,<span class=\"hljs-number\">8<\/span>))<\/p>\n<p><span class=\"hljs-comment\"># The KDE-plot generates a Legend 'as usual'<\/span><br \/>\nk = sns.kdeplot(<br \/>\ndata=clusters,<br \/>\nx=<span class=\"hljs-string\">'x'<\/span>, y=<span class=\"hljs-string\">'y'<\/span>,<br \/>\nhue=<span class=\"hljs-string\">'name'<\/span>,<br \/>\nshade=<span class=\"hljs-literal\">True<\/span>,<br \/>\nthresh=<span class=\"hljs-number\">0.05<\/span>,<br \/>\nn_levels=<span class=\"hljs-number\">2<\/span>,<br \/>\nalpha=<span class=\"hljs-number\">0.2<\/span>,<br \/>\nax=ax,<br \/>\n)<\/p>\n<p><span class=\"hljs-comment\"># Notice that we access this legend via the<\/span><br \/>\n<span class=\"hljs-comment\"># axis to turn off the frame, set the title, <\/span><br \/>\n<span class=\"hljs-comment\"># and adjust the patch alpha level so that<\/span><br \/>\n<span class=\"hljs-comment\"># it closely matches the alpha of the KDE-plot<\/span><br \/>\nax.get_legend().set_frame_on(<span class=\"hljs-literal\">False<\/span>)<br \/>\nax.get_legend().set_title(<span class=\"hljs-string\">\"Clusters\"<\/span>)<br \/>\n<span class=\"hljs-keyword\">for<\/span> lh <span class=\"hljs-keyword\">in<\/span> ax.get_legend().get_patches():<br \/>\nlh.set_alpha(<span class=\"hljs-number\">0.2<\/span>)<\/p>\n<p><span class=\"hljs-comment\"># You would probably want to sort your data <\/span><br \/>\n<span class=\"hljs-comment\"># frame or set the hue and style order in order<\/span><br \/>\n<span class=\"hljs-comment\"># to ensure consistency for your own application<\/span><br \/>\n<span class=\"hljs-comment\"># but this works for demonstration purposes<\/span><br \/>\ngroups = points.name.unique()<br \/>\nmarkers = [<span class=\"hljs-string\">'o'<\/span>, <span class=\"hljs-string\">'v'<\/span>, <span class=\"hljs-string\">'s'<\/span>, <span class=\"hljs-string\">'X'<\/span>, <span class=\"hljs-string\">'D'<\/span>, <span class=\"hljs-string\">'&lt;'<\/span>, <span class=\"hljs-string\">'&gt;'<\/span>]<br \/>\ncolors = cm.get_cmap(<span class=\"hljs-string\">'Dark2'<\/span>).colors<\/p>\n<p><span class=\"hljs-comment\"># Generate the scatterplot: notice that Legend is<\/span><br \/>\n<span class=\"hljs-comment\"># off (otherwise this legend would overwrite the <\/span><br \/>\n<span class=\"hljs-comment\"># first one) and that we're setting the hue, style,<\/span><br \/>\n<span class=\"hljs-comment\"># markers, and palette using the 'name' parameter <\/span><br \/>\n<span class=\"hljs-comment\"># from the data frame and the number of groups in <\/span><br \/>\n<span class=\"hljs-comment\"># the data.<\/span><br \/>\np = sns.scatterplot(<br \/>\ndata=points,<br \/>\nx=<span class=\"hljs-string\">\"x\"<\/span>,<br \/>\ny=<span class=\"hljs-string\">\"y\"<\/span>,<br \/>\nhue=<span class=\"hljs-string\">'name'<\/span>,<br \/>\nstyle=<span class=\"hljs-string\">'name'<\/span>,<br \/>\nmarkers=markers[:<span class=\"hljs-built_in\">len<\/span>(groups)],<br \/>\npalette=colors[:<span class=\"hljs-built_in\">len<\/span>(groups)],<br \/>\nlegend=<span class=\"hljs-literal\">False<\/span>,<br \/>\ns=<span class=\"hljs-number\">30<\/span>,<br \/>\nalpha=<span class=\"hljs-number\">1.0<\/span><br \/>\n)<\/p>\n<p><span class=\"hljs-comment\"># Here's the 'magic' -- we use zip to link together <\/span><br \/>\n<span class=\"hljs-comment\"># the group name, the color, and the marker style. You<\/span><br \/>\n<span class=\"hljs-comment\"># *cannot* retreive the marker style from the scatterplot<\/span><br \/>\n<span class=\"hljs-comment\"># since that information is lost when rendered as a <\/span><br \/>\n<span class=\"hljs-comment\"># PathCollection (as far as I can tell). Anyway, this allows<\/span><br \/>\n<span class=\"hljs-comment\"># us to loop over each group in the second data frame and <\/span><br \/>\n<span class=\"hljs-comment\"># generate a 'fake' Line2D plot (with zero elements and no<\/span><br \/>\n<span class=\"hljs-comment\"># line-width in our case) that we can add to the legend. If<\/span><br \/>\n<span class=\"hljs-comment\"># you were overlaying a line plot or a second plot that uses<\/span><br \/>\n<span class=\"hljs-comment\"># patches you'd have to tweak this accordingly.<\/span><br \/>\npatches = []<br \/>\n<span class=\"hljs-keyword\">for<\/span> x <span class=\"hljs-keyword\">in<\/span> <span class=\"hljs-built_in\">zip<\/span>(groups, colors[:<span class=\"hljs-built_in\">len<\/span>(groups)], markers[:<span class=\"hljs-built_in\">len<\/span>(groups)]):<br \/>\npatches.append(Line2D([<span class=\"hljs-number\">0<\/span>],[<span class=\"hljs-number\">0<\/span>], linewidth=<span class=\"hljs-number\">0.0<\/span>, linestyle=<span class=\"hljs-string\">''<\/span>,<br \/>\ncolor=x[<span class=\"hljs-number\">1<\/span>], markerfacecolor=x[<span class=\"hljs-number\">1<\/span>],<br \/>\nmarker=x[<span class=\"hljs-number\">2<\/span>], label=x[<span class=\"hljs-number\">0<\/span>], alpha=<span class=\"hljs-number\">1.0<\/span>))<\/p>\n<p><span class=\"hljs-comment\"># And add these patches (with their group labels) to the new<\/span><br \/>\n<span class=\"hljs-comment\"># legend item and place it on the plot.<\/span><br \/>\nleg = Legend(ax, patches, labels=groups,<br \/>\nloc=<span class=\"hljs-string\">'upper left'<\/span>, frameon=<span class=\"hljs-literal\">False<\/span>, title=<span class=\"hljs-string\">'Groups'<\/span>)<br \/>\nax.add_artist(leg);<\/p>\n<p><span class=\"hljs-comment\"># Done<\/span><br \/>\nplt.show();<br \/>\n<\/code><\/p>\n<figure id=\"attachment_416\" aria-describedby=\"caption-attachment-416\" style=\"width: 560px\" class=\"wp-caption aligncenter\"><a href=\"http:\/\/www.reades.com\/wp-content\/uploads\/2021\/06\/2-Legends.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-416\" src=\"http:\/\/www.reades.com\/wp-content\/uploads\/2021\/06\/2-Legends-1024x1024.png\" alt=\"\" width=\"560\" height=\"560\" \/><\/a><figcaption id=\"caption-attachment-416\" class=\"wp-caption-text\">A Seaborn plot showing 2 legends for different types of plots.<\/figcaption><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>Posted here because I will inevitably forget this painfully worked-out answer for having legends for two different types of plots in Seaborn&#8230; import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt # We will need to access some of these matplotlib classes directly from matplotlib.lines import Line2D # [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,12],"tags":[],"class_list":["post-415","post","type-post","status-publish","format-standard","hentry","category-coding","category-tutorials"],"_links":{"self":[{"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=\/wp\/v2\/posts\/415","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=415"}],"version-history":[{"count":0,"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=\/wp\/v2\/posts\/415\/revisions"}],"wp:attachment":[{"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=415"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=415"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.reades.com\/wp\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=415"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}