Een vaste grootte instellen voor punten in de legenda

Ik ben wat spreidingsdiagrammen aan het maken en ik wil de grootte van de punten in de legenda instellen op een vaste, gelijke waarde.

Op dit moment heb ik dit:

import matplotlib.pyplot as plt
import numpy as np
def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))
# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]
plt.figure()
plt.scatter(x1, y1, marker='o', label='first', s=20., c='b')
plt.scatter(x2, y2, marker='o', label='second', s=35., c='r')
# Plot legend.
plt.legend(loc="lower left", markerscale=2., scatterpoints=1, fontsize=10)
plt.show()

die dit produceert:

voer hier de afbeeldingsbeschrijving in

De afmetingen van de punten in de legenda zijn geschaald, maar niet hetzelfde. Hoe kan ik de grootte van de punten in de legenda op een gelijke waarde instellen zonder de grootte in de scatter-plot te beïnvloeden?


Antwoord 1, autoriteit 100%

Ik heb de broncode van matplotlibbekeken. Slecht nieuws is dat er geen eenvoudige manier lijkt te zijn om punten van gelijke grootte in de legenda in te stellen. Vooral met scatterplots is het lastig (fout: zie de update hieronder). Er zijn in wezen twee alternatieven:

  1. Wijzig de maplotlib-code
  2. Voeg een transformatie toe aan de PathCollection-objecten die de punten in de afbeelding vertegenwoordigen. De transformatie (schaling) moet rekening houden met de oorspronkelijke grootte.

Geen van beide is erg leuk, hoewel #1 gemakkelijker lijkt te zijn. De scatterplots zijn in dit opzicht bijzonder uitdagend.

Ik heb echter een hack die waarschijnlijk doet wat je wilt:

import matplotlib.pyplot as plt
import numpy as np
def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))
# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]
plt.figure()
plt.plot(x1, y1, 'o', label='first', markersize=np.sqrt(20.), c='b')
plt.plot(x2, y2, 'o', label='second', markersize=np.sqrt(35.), c='r')
# Plot legend.
lgnd = plt.legend(loc="lower left", numpoints=1, fontsize=10)
#change the marker size manually for both lines
lgnd.legendHandles[0]._legmarker.set_markersize(6)
lgnd.legendHandles[1]._legmarker.set_markersize(6)
plt.show()

Dit geeft:

voer hier de afbeeldingsbeschrijving in

Dat lijkt te zijn wat je wilde.

De wijzigingen:

  • scatteris veranderd in een plot, waardoor de schaal van de marker verandert (vandaar de sqrt) en het onmogelijk wordt om de grootte van de marker te veranderen ( als dat de bedoeling was)
  • de grootte van de markering is handmatig gewijzigd in 6 punten voor beide markeringen in de legenda

Zoals je kunt zien, maakt dit gebruik van verborgen underscore-eigenschappen (_legmarker) en is het bug-lelijk. Het kan bij elke update in matplotlibkapot gaan.

Bijwerken

Ha, ik heb het gevonden. Een betere hack:

import matplotlib.pyplot as plt
import numpy as np
def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))
# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]
plt.figure()
plt.scatter(x1, y1, marker='o', label='first', s=20., c='b')
plt.scatter(x2, y2, marker='o', label='second', s=35., c='r')
# Plot legend.
lgnd = plt.legend(loc="lower left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.show()

Nu doet de _sizes(een andere underscore-eigenschap) het. Het is niet nodig om de bron aan te raken, ook al is dit nogal een hack. Maar nu kun je alles gebruiken wat scatterbiedt.

voer hier de afbeeldingsbeschrijving in


Antwoord 2, autoriteit 56%

Vergelijkbaar met het antwoord, ervan uitgaande dat u alle markeringen met dezelfde grootte wilt:

lgnd = plt.legend(loc="lower left", scatterpoints=1, fontsize=10)
for handle in lgnd.legendHandles:
    handle.set_sizes([6.0])

Met MatPlotlib 2.0.0


Antwoord 3, autoriteit 16%

U kunt een Line2D-object maken dat lijkt op de door u gekozen markeringen, behalve met een andere markeringsgrootte naar keuze, en die gebruiken om de legenda te construeren. Dit is fijn omdat er geen object in je assen hoeft te worden geplaatst (waardoor mogelijk een resize-gebeurtenis wordt geactiveerd), en het vereist geen gebruik van verborgen attributen. Het enige echte nadeel is dat je de legenda expliciet moet samenstellen uit lijsten met objecten en labels, maar dit is een goed gedocumenteerde matplotlib-functie, dus het voelt redelijk veilig om te gebruiken.

from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
import numpy as np
def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))
# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]
plt.figure()
plt.scatter(x1, y1, marker='o', label='first', s=20., c='b')
plt.scatter(x2, y2, marker='o', label='second', s=35., c='r')
# Create dummy Line2D objects for legend
h1 = Line2D([0], [0], marker='o', markersize=np.sqrt(20), color='b', linestyle='None')
h2 = Line2D([0], [0], marker='o', markersize=np.sqrt(20), color='r', linestyle='None')
# Set axes limits
plt.gca().set_xlim(-0.2, 1.2)
plt.gca().set_ylim(-0.2, 1.2)
# Plot legend.
plt.legend([h1, h2], ['first', 'second'], loc="lower left", markerscale=2,
           scatterpoints=1, fontsize=10)
plt.show()

resulterende figuur


Antwoord 4, autoriteit 11%

Ik had niet veel succes met het gebruik van de oplossing van @DrV, hoewel mijn gebruiksscenario misschien uniek is. Vanwege de dichtheid van punten, gebruik ik de kleinste markeringsgrootte, dat wil zeggen plt.plot(x, y, '.', ms=1, ...), en ik wil dat de legendasymbolen groter zijn .

Ik heb de aanbeveling opgevolgd die ik vond op de matplotlib-forums:

  1. plot de gegevens (geen labels)
  2. limiet voor record-assen (xlimits = plt.xlim())
  3. plot valse gegevens ver weg van echte gegevens met bij de legenda passende symboolkleuren en -formaten
  4. aslimieten herstellen (plt.xlim(xlimits))
  5. legenda maken

Dit is hoe het is geworden (hiervoor zijn de puntjes eigenlijk minder belangrijk dan de lijnen):
voer hier de afbeeldingsbeschrijving in

Hopelijk helpt dit iemand anders.


Antwoord 5, autoriteit 8%

Nog een alternatief hier. Dit heeft het voordeel dat het geen “private” methoden zou gebruiken en zelfs werkt met andere objecten dan de scatters die in de legenda aanwezig zijn. De sleutel is om de scatter PathCollectiontoe te wijzen aan een HandlerPathCollectionmet een update-functie erop ingesteld.

def update(handle, orig):
    handle.update_from(orig)
    handle.set_sizes([64])
plt.legend(handler_map={PathCollection : HandlerPathCollection(update_func=update)})

Compleet codevoorbeeld:

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(42)
from matplotlib.collections import PathCollection
from matplotlib.legend_handler import HandlerPathCollection, HandlerLine2D
colors = ["limegreen", "crimson", "indigo"]
markers = ["o", "s", r"$\clubsuit$"]
labels = ["ABC", "DEF", "XYZ"]
plt.plot(np.linspace(0,1,8), np.random.rand(8), marker="o", markersize=22, label="A line")
for i,(c,m,l) in enumerate(zip(colors,markers,labels)):
    plt.scatter(np.random.rand(8),np.random.rand(8), 
                c=c, marker=m, s=10+np.exp(i*2.9), label=l)
def updatescatter(handle, orig):
    handle.update_from(orig)
    handle.set_sizes([64])
def updateline(handle, orig):
    handle.update_from(orig)
    handle.set_markersize(8)
plt.legend(handler_map={PathCollection : HandlerPathCollection(update_func=updatescatter),
                        plt.Line2D : HandlerLine2D(update_func = updateline)})
plt.show()

voer hier de afbeeldingsbeschrijving in

Other episodes