Graafikute kujundamine Pythonis paketiga Matplotlib

Valter Kiisk
TÜ Füüsika Instituut
Viimati muudetud: 21.11.2020
In [29]:
# üldised seadistused ja vahendid
import numpy as np
from numpy import sqrt, exp, sin, cos, log, pi, arange, linspace, geomspace, meshgrid, polyfit
from numpy.random import uniform, randn, rand, seed
from scipy.special import erf

Matplotlib on põhjalik objekt-orienteeritud teek mitmesuguste graafikute kujundamiseks Pythonis. Lihtsamad vahendid asuvad moodulis matplotlib.pyplot, millele importimisel antakse tüüpiliselt nimeks plt. Joonist tervikuna kirjeldab klass matplotlib.figure.Figure ja selle saab kõige mugavamalt käsuga plt.figure. Viimasel on ka hulk nimelisi argumente, millest olulisimad on joonise mõõdud tollides (figsize) ja punktide (pikslite) arv tolli kohta (dpi). Joonisele paigutatakse üks või mitu teljestikku (Axes). Vajadusel saab nende asukohta täpselt kontrollida (Figure.add_axes), aga enamasti lastakse need automaatselt paigutada tabelisse, milles on teatud arv ridu ja veerge. Näiteks käsuga Figure.add_subplot(2, 3, 1) antakse mõista, et joonisele kavatsetakse paigutada kokku kuni 6 graafikut kahes reas ja kolmes veerus ning luuakse ja tagastatakse 1. teljestik (vaikimisi Cartesiuse ristteljestik). Kui kõik arvud on 10-st väiksemad, tohib selle käsu anda ka kujul Figure.add_subplot(231). Seega ainult ühe teljestiku puhul oleks vastav käsk add_subplot(111). Seejärel andmete kandmine teljestikule toimub käsuga Axes.plot, millele tuleb anda argumentidena andmemassiivid. Käsu plot korduva väljakutsega saab ühele graafikule lisada rohkem kui ühe andmeseeria. Lisaparameetritena saab näidata iga andmeseeria jaoks ka nime ja kujunduse. Seeriate eristamiseks üksteisest on põhimõtteliselt hulk võimalusi: ühelt poolt joone värv (color), stiil (linestyle) ja paksus (linewidth), teiselt poolt sümboli värv (markeredgecolor ja markerfacecolor), kuju (marker) ja suurus (markersize ja markeredgewidth). Lihtsamate juhtude jaoks on olemas spetsiaalsed lühikesed koodid. Näiteks koodiga r- tehakse punast värvi (red) pidevjoon, koodiga bo-- tehakse sinised (blue) mummud, mis on kriipsjoonega ühendatud, jne (vt põhjalikumat kirjeldust). Andmeseeria tähis ja nimi kantakse legendile, mis tekib käsuga Axes.legend. Käsk Axes.grid lisab ruudustikujooned (grid lines). Viimaks graafik kuvatakse käsuga plt.show(). Seega piisavalt üldine objekt-orienteeritud kood ristteljestikuga graafiku kujundamiseks on umbes järgmine:

In [21]:
import matplotlib.pyplot as plt

x = linspace(0, 4*pi, 200)
fig = plt.figure(figsize=(6,3), dpi=100)
ax = fig.add_subplot(111)
ax.plot(x, sin(x), 'r-', label='sin(x)')
ax.plot(x, cos(x), 'b--', label='cos(x)')
ax.set_xlim(0, 4*pi)
ax.set_title('harmooniline võnkumine')
ax.set_xlabel('aeg')
ax.set_ylabel('signaal')
ax.legend(loc='lower left')
ax.grid(color='gray', linestyle=':')
plt.show()

Vaikimisi graafik tekib otse Jupyteri töölehel (vajadusel saab seda nõuda direktiiviga %matplotlib inline). Direktiiviga %matplotlib notebook tekib interaktiivne graafik, kus saab graafikut hiirega liigutada, suurendada, salvestada jms. Alternatiivselt võib graafikuga toimetada eraldi aknas (näiteks %matplotlib tk, kui GUI raamistikuks valida Tk). Mainitud direktiivid (mis algavad protsendimärgiga) ei ole seotud Pythoniga, vaid on IPython'i "maagilised käsud". Märgime, et Jupyteri töölehel (st veebilehel) kuvatava graafiku suurust ei määra otseselt mitte joonise nõutud suurus tollides, vaid pikslite koguarv (=pikkusmõõt×punktitihedus), monitori punktitihedus ja veebilehe suurendusaste.

Moodulisse matplotlib.pyplot on koondatud komplekt lihtsate nimedega funktsioone (plot, legend, xlabel jne), mis ei vaja ilmutatud kujul joonisele ega teljestikule viitavate muutujate (nagu fig ja ax) kasutamist. Näiteks käsuga plt.plot lisatakse andmeseeria aktiivse joonise aktiivsele teljestikule või kui viimased puuduvad, siis need luuakse automaatselt. Pärast graafiku kuvamist hakkavad järgmised käsud kujundama juba uut graafikut jne. Kui teha need funktsioonid otseselt kättesaadavaks, saame efektiivselt Matlab'ile sarnase lihtsa käsustiku:

In [22]:
from matplotlib.pyplot import *

figure(figsize=(6,3), dpi=100)
subplot(111)
plot(x, sin(x), 'r-', label='sin(x)')
plot(x, cos(x), 'b--', label='cos(x)')
xlim(0, 4*pi)
title('harmooniline võnkumine')
xlabel('aeg')
ylabel('signaal')
legend(loc='lower left')
grid(color='gray', linestyle=':')
show()

Veel enne esimeste graafikute tegemist võiks paika panna mõned üldised vormindussätted, näiteks joonise vaikesuurus, lahutusvõime, šrifti suurus vms, nii et neid ei peaks iga joonise tegemisel eraldi määrama. Vaikeseadistustele pääseb ligi objekti matplotlib.rcParams vahendusel, aga mugavam on kasutada käsku pyplot.style.use, millele stiilisätted tuleb anda sõnastikuna (võti-väärtus paarid loogeliste sulgude vahel). Parameetrite täielik nimekiri on siin.

In [23]:
style.use({
    #'figure.figsize': (6,4), # joonise suurus tollides
    'figure.dpi': 120, # mitu punkti tolli kohta
    #'font.size': 12, # teksti suurus punktides
    'figure.titlesize': 'medium', # siin ja edaspidi suhteline suurus
    #'axes.titlesize': 'medium',
    'legend.fontsize': 'small',
    #'figure.facecolor': 'white', # joonise tausta värv
    #'lines.linewidth': 2, # graafiku joone jämedus
    #'lines.markeredgewidth': 0,
    #'lines.markersize': 5, # sümboli suurus
    #'axes.prop_cycle': cycler('color', 'rbgm'), # andmeseeriate värvused
    #'patch.facecolor': 'black', # noolte jms värvus
    #'patch.edgecolor': 'black',
    #'patch.linewidth': 0.8, # noolte jms joone jämedus
    #'mathtext.fontset': 'cm' # matemaatikašrift
})

Mitut graafikut sisaldava joonise konstrueerimiseks tuleb käsku Figure.add_subplot() või lihtsalt plt.subplot() välja kutsuda mitu korda. Vaikimisi tekib ristkoordinaadistik, polaarkoordinaadistik luuakse parameetriga projection='polar' või polar=True.

In [24]:
θ = linspace(0, 2*pi, 300)
r1 = cos(3*θ)**2
r2 = 2 - sin(6*θ) - 0.5 * cos(30*θ)

figure(figsize=(7,3))
subplot(121, polar=True)
plot(θ, r1, 'b-')
ylim(0, 1)
subplot(122, polar=True)
plot(θ, r2, 'b-')
ylim(0, 3.5)
subplots_adjust(wspace=0.3)
show()

Käsk pyplot.subplots teeb korraga valmis nii joonise kui ka hulga teljestikke (jällegi $m\times n$ maatriksis) ja tagastab viidad nimetatud objektidele. Seega ilmutatult objekt-orienteeritud kood eelneva joonise tegemiseks on selline:

In [7]:
fig, (ax1, ax2) = subplots(1, 2, figsize=(7,3), subplot_kw=dict(polar=True))
ax1.plot(θ, r1, 'b-')
ax1.set_ylim(0, 1)
ax2.plot(θ, r2, 'b-')
ax2.set_ylim(0, 3.5)
subplots_adjust(wspace=0.3)
show()

Lihtsamatel juhtudel võib kasutada käsku plt.polar, mis teeb valmis polaarkoordinaadistiku ja lisab sellele kohe ka andmeseeria (nagu plot).

In [8]:
polar(θ, r2, 'b-')
ylim(0, 3.5)
show()

Olgu meil eksperimentaalselt mõõdetud sõltuvus, mida teoreetiliselt peaks kirjeldama astme-, eksponent- või logaritmfunktsioon. Selle veenvaks demonstreerimiseks tuleks graafik (st selle üks või mõlemad teljed) viia logaritmilisse skaalasse, nii et see sõltuvus muutuks lineaarseks:

In [25]:
# "eksperimentaalsed" andmepunktid
x1 = [1, 2, 3, 4, 5]
y1 = [2.5, 8, 20, 56, 173]

# teoreetiline sõltvus (pidevjoon) peenema sammuga
x2 = arange(0, 5.5, 0.1)
y2 = exp(x2)

figure(figsize=(7,3))
subplot(121)
plot(x1, y1, "bo", label="katse")
plot(x2, y2, "r-", label="teooria")
grid()
legend()
subplot(122)
plot(x1, y1, "bo")
plot(x2, y2, "r-")
yscale('log')
grid()
show()

Automaatne skaalakriipsude (ticks) valik ei pruugi olla alati optimaalne. Näiteks äsja saadud graafikutel võiks skaalakriipsud olla kohati tihedama sammuga. Kõige konkreetsem lahendus on kõikide kriipsude (ja vajadusel ka nende tähiste) asukohad jada või massiivina ette anda käskudega plt.xticks ja plt.yticks. Samas moodulist matplotlib.ticker leiab ka poolautomaatseid algoritme.

In [26]:
from matplotlib.ticker import MultipleLocator

figure(figsize=(7,3))
subplot(121)
plot(x1, y1, 'bo')
plot(x2, y2, 'r-')
gca().xaxis.set_major_locator( MultipleLocator(1) )
gca().yaxis.set_major_locator( MultipleLocator(50) )
grid()

subplot(122)
plot(x1, y1, 'bo')
plot(x2, y2, 'r-')
yscale('log')
xticks( arange(0, 6, 1) )
asukohad = 1, 3, 10, 30, 100
yticks( asukohad, asukohad )  # kriipsud ja nende tähised
grid()
show()

Kui on tarvis sõltumatu muutuja (harilikult x-teljel) näidata logaritmilises skaalas, osutub käepäraseks funktsioon numpy.geomspace(a,b,n), mis tekitab $n$-elemendilise arvuvektori alates $a$ kuni $b$, kus arvud paiknevad ühtlase sammuga logaritmilisel skaalal.

In [27]:
from scipy.constants import micro, c, h, k

# x on lainepikkus või lainepikkuste vektor [µm], T on temperatuur [K]
def planck(x, T):
    x = x * micro
    a = exp(h * c / (x * k * T)) - 1
    return 2 * pi * h * c**2 / x**5 / a * micro

x = geomspace(0.1, 100, 200) # 0.1 kuni 100 mikromeetrit, 200 punkti

gca().set_prop_cycle( cycler('color', 'bgr') )
for T in (400, 1000, 3000):
    plot(x, planck(x, T), '-', label='T=%.0f K' % T)

xlabel('lainepikkus (µm)')
ylabel('kiirguse intensiivsus\nW m$^{-2}$ µm$^{-1}$')
ylim(ymin=1)
xscale('log')
yscale('log')
legend()
grid()
show()

Käsk plt.gca() (get current axes) tagastab viida aktiivsele teljestikule. (On olemas ka käsk gcf(), mis tagastab viida joonisele.)

Eespool on juba graafikute annoteerimiseks ja ilustamiseks kasutatud käske title, xlabel, ylabel, legend ja grid. Graafikul toodud sõltuvuste illustreerimiseks kasutatakse sageli vertikaalseid või horisontaalseid abijooni, mida saab luua käskudega axhline ja axvline. Käskudega text ja annotate saab meelevaldsesse kohta graafikul lisada mitmesuguseid spetsiaalseid annotatsioone. Seejuures igasugune tekstielement suudab renderdada ka matemaatilisi sümboleid ja valemeid — vastavas sõne-tüüpi argumendis tuleb LaTeX-koodis avaldis kirjutada kahe dollarimärgi vahele. Matplotlib ise tunneb teatud alamhulka LaTeX'i käsustikust, aga süsteemi saab seadistada ka nii, et kasutatakse välist, täismahulist LaTeX'i installatsiooni (rcParams['text.usetex']).

Kui graafiku vormistamisega juba niipalju vaeva nähakse, on harilikult eesmärk graafik salvestada (et kasutada seda artiklis, esitluses, vms). Kui graafik on interaktiivne, on seal olemas ka salvestamise nupp. Teine variant on kasutada käsku savefig. Failitüüp valitakse automaatselt failinime laiendi järgi. Olemas on nii vektorgraafika (näiteks PDF) kui ka rastergraafika (PNG) formaadid. Parameetriga bbox_inches='tight' saab kõrvaldada tühja valge ala graafiku ümbert.

In [28]:
doppler = lambda x: exp(-4 * log(2) * x**2 )
lorentz = lambda x: 1 / (4 * x**2 + 1.0)
x = linspace(-2, 2, 200)

plot(x, doppler(x), 'r-', label='doppler')
plot(x, lorentz(x), 'b-', label='lorentz')
xlim(-2, 2)
ylim(0, 1)
xlabel('sagedus $\omega$')
ylabel('signaal')
axhline( 0.5, color='black', linewidth=0.8, linestyle='--')
axvline(-0.5, color='black', linewidth=0.8, linestyle='--')
axvline( 0.5, color='black', linewidth=0.8, linestyle='--')

text(-0.55, 0.51, 'poolkõrgus', fontsize='small', va='bottom', ha='right')
annotate('', xy=(0.5, 0.4), xytext=(-0.5, 0.4), arrowprops=dict(arrowstyle='<|-|>'))
text(0, 0.38, 'FWHM=1', fontsize='small', va='top', ha='center')
annotate('$(4+\omega^2)^{-1}$', xy=(0.69, 0.34), xytext=(1, 0.63),
             va='center', ha='center', arrowprops=dict(arrowstyle='-|>'))
annotate('$e^{-4\ln(2)\omega^2}$', xy=(0.94, 0.082), xytext=(1.25, 0.4),
             va='center', ha='center', arrowprops=dict(arrowstyle='-|>'))
legend()
savefig('test.pdf', bbox_inches='tight')
show()

Selles näites text ja annotate kasutasid graafiku enda koordinaate, mis oli antud juhul igati mõistlik. Mõnikord on vaja tekst paigutada graafikule kindlasse kohta telgede suhtes, näiteks andes koordinaadid murdosana vastava telje pikkusest. Seda saab teha käsuga annotate, lisades nimelise parameetri xycoords='axes fraction'. Alati kui mingeid tüüplahendusi piisavalt sageli tarvis läheb, võib ehitada vastava tööriista. Anname sellele nimeks axtext:

In [30]:
def axtext(x, y, s, **kwargs):
    annotate(s, (x, y), xycoords='axes fraction',
             backgroundcolor='white', **kwargs)

n = 20
x = linspace(0, 5, n)

seed(42)
y = 3 * x + 7 + 2 * randn(n)
a, b = polyfit(x, y, 1)
plot(x, y, 'bo')
plot(x, a * x + b, 'r-')
axtext(0.05, 0.95, '$y=%.3fx+%.3f$' % (a, b), va='top')
grid()
show()

Teaduslikes publikatsioonides on tavapärane, et sekundaarse tähtsusega graafik asetatakse primaarse teljestiku sisse. Täpse paigutusega teljestikke saab käsuga plt.axes (või Figure.add_axes), millele jadana (left, bottom, width, height) antakse teljestiku paigutus. Need on jällegi murdarvud, nii et joonise vastav mõõt (laius või kõrgus) on täpselt 1.

In [31]:
x = linspace(-6, 6, 200)
D = exp(-x**2/2) / sqrt(2*pi)
F = (1 + erf(x/sqrt(2))) / 2

axes( (0.15, 0.10, 0.80, 0.85) )  # left, bottom, width, height
plot(x, F, 'r-')
xlim(-6, 4)
ylim(ymin=0)
xlabel('juhuslik muutuja')
ylabel('kumulatiivne tõenäosus')
title('normaaljaotus')
axes( (0.27, 0.48, 0.32, 0.43) )  # left, bottom, width, height
plot(x, D, 'b-')
xlim(-3, 3)
ylim(ymin=0)
ylabel('tõenäosustihedus')
show()