Matplotlib-Tutorial: Mehrfache Plots und Doppelachsen

multiple plots

Inzwischen haben wir in den vergangenen Kapiteln des Matplotlib-Tutorials viele Beispiele gegeben wie Graphen gezeichnet werden können. Ein häufig gestellte Frage ist, wie man mehrere Plots in einem Graphen unterbringen kann.

Im einfachsten Fall heisst das, dass wir eine Kurve haben und wir eine weitere Kurve darüber legen. Der interessantere Fall ist jedoch, wenn zwei Plots in einem Fenster gewünscht werden. In einem Fenster bedeutet, dass es zwei Unter-Plots geben soll, d.h. dass diese nicht übereinander gezeichnet werden. Die Idee ist, mehr als einen Graphen in einem Fenster zu haben und jeder Graüh erscheint in seinem eigenen Unter-Plot.

Wir stellen zwei verschiedene Wege vor, wie dies erreicht werden kann:

Wir sind der Meinung, dass gridspec die beste Option ist, weil es einfacher in der Anwendung ist, wenn das Layout komplexer wird!



Arbeiten mit mehreren Abbildungen und Achsen


subplot und die Parameter:

subplot(nrows, ncols, plot_number)

Wenn ein Unter-Plot auf eine Abbildung angewendet wird, so wird die Abbldung theoretisch aufgeteilt in 'nrows' * 'ncols' Unter-Achsen. Der Parameter "plot_number" bezeichnet den Unter-Plot, den der Funktionsaufruf erstellen muss. "plot_number" kann einen Wert zwischen 1 und dem maximum von 'nrows' * 'ncols' annehmen.

Wenn der Wert der drei Parameter kleiner als 10 ist, kann die Funktion mit einem Integer Wert aufgerufen werden, wobei die Hunderter "nrows", die Zehner "ncols" und die Einheiten "plot_number" repräsentieren. Das bedeutet: Statt subplot(2, 3, 4) kann subplot(234) geschrieben werden.


Im folgenden Beispiel, "aktivieren" wir zwei Unter-Plots in einem theoretischen 2x2 Gitter:

%matplotlib inline
import matplotlib.pyplot as plt
python_course_green = "#476042"
plt.figure(figsize=(6, 4))
plt.subplot(221) # equivalent to: plt.subplot(2, 2, 1)
plt.text(0.5, # x coordinate, 0 leftmost positioned, 1 rightmost
         0.5, # y coordinate, 0 topmost positioned, 1 bottommost
         'subplot(2,2,1)', # the text which will be printed
         horizontalalignment='center', # shortcut 'ha' 
         verticalalignment='center', # shortcut 'va'
         fontsize=20, # can be named 'font' as well
         alpha=.5 # float (0.0 transparent through 1.0 opaque)
         )
plt.subplot(224, axisbg=python_course_green)
plt.text(0.5, 0.5, 
         'subplot(2,2,4)', 
         ha='center', va='center',
         fontsize=20, 
         color="y")
Wir erhalten die folgende Ausgabe:
<matplotlib.text.Text at 0x10bc0a5c0>

Für unsere Absichten benötigen wir keine Ticks auf den Achsen. Wir können sie loswerden, indem wir ein leeres Tupel setzen und folgenden Code ergänzen: plt.xticks(()) plt.yticks(())

import matplotlib.pyplot as plt
python_course_green = "#476042"
plt.figure(figsize=(6, 4))
plt.subplot(221) # equivalent to: plt.subplot(2, 2, 1)
plt.xticks(())
plt.yticks(())
plt.text(0.5, # x coordinate, 0 leftmost positioned, 1 rightmost
         0.5, # y coordinate, 0 topmost positioned, 1 bottommost
         'subplot(2,2,1)', # the text which will be printed
         horizontalalignment='center', # shortcut 'ha' 
         verticalalignment='center', # shortcut 'va'
         fontsize=20, # can be named 'font' as well
         alpha=.5 # float (0.0 transparent through 1.0 opaque)
         )
plt.subplot(224, axisbg=python_course_green)
plt.xticks(())
plt.yticks(())
plt.text(0.5, 0.5, 
         'subplot(2,2,4)', 
         ha='center', va='center',
         fontsize=20, 
         color="y")
Wir erhalten die folgende Ergebnisse:
<matplotlib.text.Text at 0x10c9d79b0>

Der vorige Ansatz ist durchaus akzeptabel. Jedoch ist guter Stil, wenn eine objektorientierter Ansatz verfolgt wird, indem man Instanzen der Figure-Klasse verwendet. Wir demonstrieren dies, indem wir das vorige Beispiel umschreiben. In diesem Fall müssen wir die "add_subplot"-Methode auf das Figure-Objekt anwenden.

Wir empfehlen dazu die Kapitel zu OOP in unserem Python-Tutorial zu lesen, wenn Sie nicht mit objektorientierter Programmierung vertraut sind:

Die überarbeitete Version des Codes sieht wie folgt aus:

import matplotlib.pyplot as plt
python_course_green = "#476042"
fig = plt.figure(figsize=(6, 4))
sub1 = fig.add_subplot(221) # equivalent to: plt.subplot(2, 2, 1)
sub1.text(0.5, # x coordinate, 0 leftmost positioned, 1 rightmost
          0.5, # y coordinate, 0 topmost positioned, 1 bottommost
          'subplot(2,2,1)', # the text which will be printed
          horizontalalignment='center', # shortcut 'ha' 
          verticalalignment='center', # shortcut 'va'
          fontsize=20, # can be named 'font' as well
          alpha=.5 # float (0.0 transparent through 1.0 opaque)
          )
sub2 = fig.add_subplot(224, axisbg=python_course_green)
sub2.text(0.5, 0.5, 
          'subplot(2,2,4)', 
          ha='center', va='center',
          fontsize=20, 
          color="y")
Der obige Python-Code liefert folgendes Ergebnis:
<matplotlib.text.Text at 0x10ca6d5c0>

Lass uns die Ticks wieder loswerden. Dieses Mal können wir nicht plt.xticks(()) und plt.yticks(()) benutzen. Wir müssen die Methoden set_xticks(()) und set_yticks(()) stattdessen benutzen.

import matplotlib.pyplot as plt
python_course_green = "#476042"
fig = plt.figure(figsize=(6, 4))
sub1 = fig.add_subplot(221) # equivalent to: plt.subplot(2, 2, 1)
sub1.set_xticks([]) 
sub1.set_yticks([]) 
sub1.text(0.5, # x coordinate, 0 leftmost positioned, 1 rightmost
          0.5, # y coordinate, 0 topmost positioned, 1 bottommost
          'subplot(2,2,1)', # the text which will be printed
          horizontalalignment='center', # shortcut 'ha' 
          verticalalignment='center', # shortcut 'va'
          fontsize=20, # can be named 'font' as well
          alpha=.5 # float (0.0 transparent through 1.0 opaque)
          )
sub2 = fig.add_subplot(224, axisbg=python_course_green)
sub2.set_xticks([])
sub2.set_yticks([]) 
sub2.text(0.5, 0.5, 
          'subplot(2,2,4)', 
          ha='center', va='center',
          fontsize=20, 
          color="y")
Führt man obigen Code aus, erhält man folgendes Ergebnis:
<matplotlib.text.Text at 0x10cbf7a20>

Wenn alle Unter-Plots des 2x2-Gitters aktiviert sind, sieht es wie folgt aus:

import matplotlib.pyplot as plt
python_course_green = "#476042"
fig = plt.figure(figsize=(6, 4))
sub1 = plt.subplot(2, 2, 1)
sub1.set_xticks(())
sub1.set_yticks(())
sub1.text(0.5, 0.5, 'subplot(2,2,1)', ha='center', va='center',
        size=20, alpha=.5)
sub2 = plt.subplot(2, 2, 2)
sub2.set_xticks(())
sub2.set_yticks(())
sub2.text(0.5, 0.5, 'subplot(2,2,2)', ha='center', va='center',
        size=20, alpha=.5)
sub3 = plt.subplot(2, 2, 3)
sub3.set_xticks(())
sub3.set_yticks(())
sub3.text(0.5, 0.5, 'subplot(2,2,3)', ha='center', va='center',
        size=20, alpha=.5)
sub4 = plt.subplot(2, 2, 4, axisbg=python_course_green)
sub4.set_xticks(())
sub4.set_yticks(())
sub4.text(0.5, 0.5, 'subplot(2,2,4)', ha='center', va='center',
        size=20, alpha=.5, color="y")
fig.tight_layout()
plt.show()

Das vorige Beispiel zeigt lediglich, wie man das Unter-Plot-Design erstellen kann. Normalerweise möchsten Sie ein Python-Programm mit Matplotlib schreiben und die Unter-Plot-Funktion benutzen, um mehrere Graphen darzustellen. Wir demostrieren nun, wie man das vorige Unter-Plot-Design mit einigen Graphen bestückt:

import numpy as np
from numpy import e, pi, sin, exp, cos
import matplotlib.pyplot as plt
def f(t):
    return exp(-t) * cos(2*pi*t)
def fp(t):
    return -2*pi * exp(-t) * sin(2*pi*t) - e**(-t)*cos(2*pi*t)
def g(t):
    return sin(t) * cos(1/(t+0.1))
def g(t):
    return sin(t) * cos(1/(t))
python_course_green = "#476042"
fig = plt.figure(figsize=(6, 4))
t = np.arange(-5.0, 1.0, 0.1)
sub1 = fig.add_subplot(221) # instead of plt.subplot(2, 2, 1)
sub1.set_title('The function f') # non OOP: plt.title('The function f')
sub1.plot(t, f(t))
sub2 = fig.add_subplot(222, axisbg="lightgrey")
sub2.set_title('fp, the derivation of f')
sub2.plot(t, fp(t))
t = np.arange(-3.0, 2.0, 0.02)
sub3 = fig.add_subplot(223)
sub3.set_title('The function g')
sub3.plot(t, g(t))
t = np.arange(-0.2, 0.2, 0.001)
sub4 = fig.add_subplot(224, axisbg="lightgrey")
sub4.set_title('A closer look at g')
sub4.set_xticks([-0.2, -0.1, 0, 0.1, 0.2])
sub4.set_yticks([-0.15, -0.1, 0, 0.1, 0.15])
sub4.plot(t, g(t))
plt.plot(t, g(t))
plt.tight_layout()
plt.show()

Weiteres Beispiel:

import  matplotlib.pyplot as plt
X = [ (2,1,1), (2,3,4), (2,3,5), (2,3,6) ]
for nrows, ncols, plot_number in X:
    plt.subplot(nrows, ncols, plot_number)

Das folgende Beispiel zeigt nichts besonderes. Wir entfernen die xticks und experimentieren etwas mit der Größe der Abbildung und der Unter-Plots. Dafür führen wir nun den Schlüsselwort-Parameter figsize für "figure" ein. Und die Funktion "subplot_adjust" mit dessen Schlüsselwort-Parametern bottom, left, top und right:

import  matplotlib.pyplot as plt
fig =plt.figure(figsize=(6,4))
fig.subplots_adjust(bottom=0.025, left=0.025, top = 0.975, right=0.975)
X = [ (2,1,1), (2,3,4), (2,3,5), (2,3,6) ]
for nrows, ncols, plot_number in X:
    sub = fig.add_subplot(nrows, ncols, plot_number)
    sub.set_xticks([])
    sub.set_yticks([])

Alternative Lösung:

Um die ersten drei Elemente des 2x3-Gitters zu verbinden, können wir auch eine Tupel-Schreibweise verwenden. In unserem Fall (1,3) in (2,3,(1,3)) um festzulegen, dass die ersten drei Elemente des theoretischen 2x3-Gitters verbunden werden sollen:

import  matplotlib.pyplot as plt
fig =plt.figure(figsize=(6,4))
fig.subplots_adjust(bottom=0.025, left=0.025, top = 0.975, right=0.975)
X = [ (2,3,(1,3)), (2,3,4), (2,3,5), (2,3,6) ]
for nrows, ncols, plot_number in X:
    sub = fig.add_subplot(nrows, ncols, plot_number)
    sub.set_xticks([])
    sub.set_yticks([])

Übung


Wie können Sie ein Unter-Plot-Design eines 3x2-Gitters erstellen, bei dem die erste Spalte verbunden ist?



Lösung:

import  matplotlib.pyplot as plt
X = [ (1,2,1), (3,2,2), (3,2,4), (3,2,6) ]
for nrows, ncols, plot_number in X:
    plt.subplot(nrows, ncols, plot_number)
    plt.xticks([])
    plt.yticks([])


Übung

Erstellen Sie ein Unter-Plot-Layout für das folgende Design:

subplot layout



Lösung:

import  matplotlib.pyplot as plt
X = [  (4,2,1),(4,2,2), (4,2,3), (4,2,5), (4,2,(4,6)), (4,1,4)]
plt.subplots_adjust(bottom=0, left=0, top = 0.975, right=1)
for nrows, ncols, plot_number in X:
    plt.subplot(nrows, ncols, plot_number)
    plt.xticks([])
    plt.yticks([])
plt.show()

Unter-Plots mit gridspec

'matplotlib.gridspec' beinhaltet eine Klasse GridSpec. Diese kann als Alternative zu subplot verwendet werden um die Geometrie der Unter-Plots zu spezifizieren. Die Grund-Idee hinter GridSpec ist ein "Gitter". Ein Gitter wird wurd mit der Anzahl von Zeilen und Spalten erstellt. Anschließend muss definiert werden, wieviel des Gitters ein Unter-Plot einnehmen soll.

Das folgende Beispiel zeigt den trivialsten oder einfachsten Fall, d.h. ein 1x1-Gitter:

import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure()
gs = GridSpec(1, 1)
ax = fig.add_subplot(gs[0,0])
plt.show()

Wir könnten noch einige der Parameter aus GridSpec benutzen, z.B. können wir definieren, dass der Graph erst 20% von unten und 15% von links der verfügbaren Abbildungs-Fläche beginnen soll:

import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure()
gs = GridSpec(1, 1, 
              bottom=0.2,
              left=0.15,
              top=0.8)
ax = fig.add_subplot(gs[0,0])
plt.show()

Das nächste Beispiel zeigt ein komplexeres Beispiel mit einem aufwändigeren Gitter-Design:

import matplotlib.gridspec as gridspec
import matplotlib.pyplot as pl
pl.figure(figsize=(6, 4))
G = gridspec.GridSpec(3, 3)
axes_1 = pl.subplot(G[0, :])
pl.xticks(())
pl.yticks(())
pl.text(0.5, 0.5, 'Axes 1', ha='center', va='center', size=24, alpha=.5)
axes_2 = pl.subplot(G[1, :-1])
pl.xticks(())
pl.yticks(())
pl.text(0.5, 0.5, 'Axes 2', ha='center', va='center', size=24, alpha=.5)
axes_3 = pl.subplot(G[1:, -1])
pl.xticks(())
pl.yticks(())
pl.text(0.5, 0.5, 'Axes 3', ha='center', va='center', size=24, alpha=.5)
axes_4 = pl.subplot(G[-1, 0])
pl.xticks(())
pl.yticks(())
pl.text(0.5, 0.5, 'Axes 4', ha='center', va='center', size=24, alpha=.5)
axes_5 = pl.subplot(G[-1, -2])
pl.xticks(())
pl.yticks(())
pl.text(0.5, 0.5, 'Axes 5', ha='center', va='center', size=24, alpha=.5)
pl.tight_layout()
pl.show()

Die Gitter-Spezifikation aus dem vorigen Beispiel verwenden wir nun, um sie mit Graphen aus einigen Funktionen zu bestücken:

import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import numpy as np
pl.figure(figsize=(6, 4))
G = gridspec.GridSpec(3, 3)
X = np.linspace(0, 2 * np.pi, 50, endpoint=True)
F1 = 2.8 * np.cos(X)
F2 = 5 * np.sin(X)
F3 = 0.3 * np.sin(X)
axes_1 = plt.subplot(G[0, :])
axes_1.plot(X, F1, 'r-', X, F2)
axes_2 = plt.subplot(G[1, :-1])
axes_2.plot(X, F3)
axes_3 = plt.subplot(G[1:, -1])
axes_3.plot([1,2,3,4], [1,10,100,1000], 'b-')
axes_4 = plt.subplot(G[-1, 0])
axes_4.plot([1,2,3,4], [47, 11, 42, 60], 'r-')
axes_5 = plt.subplot(G[-1, -2])
axes_5.plot([1,2,3,4], [7, 5, 4, 3.8])
plt.tight_layout()
plt.show()



Arbeiten mit Objekten

Matplotlib wurde programmiert und gestaltet - wie Python selbst - in einer komplett objektorientierten Ausrichtung. Die bis jetzt gezeigten Beispiel waren sehr einfach. Und um Sie einfach wie möglich zu halten, wir z.B. nicht mit figure-Objekten gearbeitet. Trotzdem werden die Objekte automatisch angelegt. Der Vorteil kommt dann zum vorschein, wenn mehr als eine Abbildung verwendet werden, oder wenn eine Abbildung mehrere Unter-Plots beinhaltet.

Im folgenden Beispiel erstellen wir einen Plot auf einem strikt objektorientiertem Weg. Wir beginnen mit der Erstellung eines neuen figure-Objektes. Wir speichern eine Referenz in der Variable fig. Diese benutzen wir um mit add_axes aus der figure-Klasse neue axis-Instanzen zu erstellen:

import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
X = np.arange(0,10)
Y = np.random.randint(1,20, size=10)
left, bottom, width, height = 0.1, 0.1, 0.8, 0.8
axes = fig.add_axes([left, bottom, width, height])
axes.plot(X, Y, 'g')
plt.show(fig)
#axes.set_xlabel('x')
#axes.set_ylabel('y')
#axes.set_title('title');

Ohne explizite Instanzen zu verwenden, sieht der Code folgendermaßen aus:

import numpy as np
import matplotlib.pyplot as plt
X = np.arange(0,10)
Y = np.random.randint(1,20, size=10)
plt.plot(X, Y, 'g')
plt.show()

Ein Plot innerhalb eines anderen Plots

import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
X = [1, 2, 3, 4, 5, 6, 7]
Y = [1, 3, 4, 2, 5, 8, 6]
axes1 = fig.add_axes([0.1, 0.1, 0.9, 0.9]) # main axes
axes2 = fig.add_axes([0.2, 0.6, 0.4, 0.3]) # inset axes
# main figure
axes1.plot(X, Y, 'r')
axes1.set_xlabel('x')
axes1.set_ylabel('y')
axes1.set_title('title')
# insert
axes2.plot(Y, X, 'g')
axes2.set_xlabel('y')
axes2.set_ylabel('x')
axes2.set_title('title inside');



Setzen des Plotbereichs

Es gibt die Möglichkeit den Bereich der Achsen zu konfigurieren. Dafür verwendet man die Methoden set_ylim und set_xlim des Achsen-Objektes. Mit axis.('tight') erstellen wir automatisch "eng anliegende" Achs-Bereiche:

import numpy as np
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(10, 4))
x = np.arange(0, 5, 0.25)
axes[0].plot(x, x**2, x, x**3)
axes[0].set_title("default axes ranges")
axes[1].plot(x, x**2, x, x**3)
axes[1].axis('tight')
axes[1].set_title("tight axes")
axes[2].plot(x, x**2, x, x**3)
axes[2].set_ylim([0, 60])
axes[2].set_xlim([2, 5])
axes[2].set_title("custom axes range");



Logarithmische Darstellung

Es gibt auch die Möglichkeit eine logarithmische Darstellung für eine oder beide Achsen einzustellen. Die Funktionalität ist tatsächlich nur eine einzige Applikation eines allgemeineren Transformations-Systems in Matpltolib. Jede der Achsen-Darstellung wird durch eine separate set_xscale und set_yscale Methode gesetzt. Diese nehmen einen Parameter entgegen (in diesem Fall mit dem Wert "log"):

import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
x = np.arange(0, 5, 0.25)
ax.plot(x, x**2, x, x**3)
ax.set_yscale("log")
plt.show()
import numpy as np
import matplotlib.pyplot as plt
fig, ax1 = plt.subplots()
x = np.arange(1,7,0.1)
ax1.plot(x, 2 * np.pi * x, lw=2, color="blue")
ax1.set_ylabel(r"Circumference $(cm)$", fontsize=16, color="blue")
for label in ax1.get_yticklabels():
    label.set_color("blue")
    
ax2 = ax1.twinx()
ax2.plot(x, np.pi * x ** 2, lw=2, color="darkgreen")
ax2.set_ylabel(r"area $(cm^2)$", fontsize=16, color="darkgreen")
for label in ax2.get_yticklabels():
    label.set_color("darkgreen")

Die folgenden Themen stehen nicht im direkten Zusammenhang mit Unter-Plots. Trotzdem wollen wir Sie hier erwähnen um die Einführung der Basis-Möglichkeiten von Matplotlib abzurunden. Das erste zeigt, wie Gitter-Linien definiert werden. Das zweite, sehr wichtige Thema, befasst sich mit der Speicherung von Plots in Bild-Dateien.



Gitter-Linien

import numpy as np
import matplotlib.pyplot as plt
def f(t):
    return np.exp(-t) * np.cos(2*np.pi*t)
def g(t):
    return np.sin(t) * np.cos(1/(t+0.1))
t1 = np.arange(0.0, 5.0, 0.1)
t2 = np.arange(0.0, 5.0, 0.02)
plt.subplot(212)
plt.plot(t1, g(t1), 'ro', t2, f(t2), 'k')
plt.grid(color='b', alpha=0.5, linestyle='dashed', linewidth=0.5)
plt.show()



Abbildungen speichern

Die savefig-Methode kann verwendet werden, um Abbildungen als Datei zu speichern:

fig.savefig("filename.png")

Optional kann die DPI und das Ausgabe-Format festgelegt werden:

fig.savefig("filename.png", dpi=200)

Ausgabe-Formate können die folgenden sein: PNG, JPG, EPS, SVG, PGF und PDF.