Python del 18: Histogram
Denne gangen skal vi se på hvordan vi kan bruke Python til å tegne den vanskeligste diagramtypen i Matematikk 2P: Histogram.
Vi husker at et histogram er et søylediagram hvor hver av søylene representerer et måleintervall og bredden på måleintervallet bestemmer bredden på søylene. Samtidig velger vi høyde på søylene slik at arealet til søylene blir proporsjonalt med antall målinger i intervallet.
Enkelt histogram
Vi tegner histogrammer med den samme funksjonen som vi bruker til å tegne søylediagrammer: bar. Når vi skal tegne histogrammer må vi ha fire argumenter:
- En sortert liste med de nedre klassegrensensene
- En liste med søylehøyder
- En liste med søylebredder
- Argumentet align=”edge” som sørger for at søylene starter på den nedre klassegrensen
In [1]: plt.bar([0, 2, 3, 5, 7], [1, 2, 3, 4, 5], [2, 1, 2, 2, 1], align="edge") Out[1]:
Vi kan også styre fargene til histogrammet.
color
-argumentet angir fargen på søyleneedgecolor
-argumentet angir fargen på kantene til søylene
In [2]: plt.bar([0, 2, 3, 5, 7], [1, 2, 3, 4, 5], [2, 1, 2, 2, 1], align='edge', color='lightblue', edgecolor='blue') Out[2]:
Vi har tidligere sett på hvilke farger vi kan velge.
Eksempel: Befolkning i norge
På SSB sine hjemmesider kan vi finne følgende tabell over den norske befolkningen sin aldersfordeling:
0 år | 53134 |
1-5 år | 290630 |
6-12 år | 448573 |
13-15 år | 192510 |
16-19 år | 252943 |
20-44 år | 1792220 |
45-66 år | 1511702 |
67-79 år | 613250 |
80-89 år | 190309 |
90 år eller eldre | 46098 |
Vi ønsker å fremstille dette som et histogram.
In [3]: %matplotlib In [4]: grenser = [0, 1, 6, 13, 16, 20, 45, 67, 80, 90, 110] In [5]: grupperteFrekvenser = [53134, 290630, 448573, 192510, 252943, 1792220, 1511702, 613250, 190309, 46098] In [6]: bredder = [x-y for x, y in zip(grenser[1:], grenser[:-1])]
Den siste kodelinjen gjør mye
grenser[1:]
ignorerer det første elementet i grenser-listen og gir oss derfor bare de øvre grensene.grenser[:-1]
ignorerer det siste elementet, og gir oss derfor de nedre grensene.zip
-funksjonen gjør at vi går gjennom et element fra de to underlistene samtidig. Disse blir tilordnet hendholdsvis x- og y- variablelen og den nye listen består av resultatet av utregningen x-y. Det vi gjør er altså det samme som vektorsubtraksjon.
In [7]: høyder = [x/y for x, y in zip(grupperteFrekvenser,bredder)]
Her regner vi elementvis ut frekvens/bredde.
Til slutt legger vi til forklarende tekst:
In [8]: plt.bar(grenser[:-1],høyder,bredder,align="edge", color='lightblue', edgecolor='blue') In [9]: plt.title("Norges befolkning") In [10]: plt.xlabel("alder") In [11]: plt.ylabel("personer")
Funksjon for å tegne histogram
Vi ser fra det forrige eksemplet at vi trenger to biter med informasjon for å tegne et histogram: grenser og frekvenser. Ut i fra dette kan vi regne ut søylehøydene og søylebreddene. Derfor passer det å lage en funksjon som tegner et histogram ut i fra de to opprinnelige parameterne.
def tegnHistogram(grenser, grupperteFrekvenser,tittel="Histogram", xlabel ="", ylabel=""): bredder = [x-y for x,y in zip(grenser[1:], grenser[:-1])] høyder = [x/y for x, y in zip(grupperteFrekvenser,bredder)] plt.bar(grenser[:-1], høyder, bredder, align="edge", color='lightblue', edgecolor='blue') plt.title(tittel) plt.show()
De siste tre parametrene styrer den forklarende teksten. Hvis vi ville kunne vi også lagt til parametre for farger.
Vi tester funksjonen med de samme dataene som i det forrige eksemplet:
In [13]: tegnHistogram(grenser, grupperteFrekvenser, "Norges befolkning","alder","personer")
Endre histogramstil
Vi kan også endre stilen på histogrammer
In [14]: with plt.xkcd(): ...: tegnHistogram(grenser, grupperteFrekvenser, "Norges befolkning","alder","personer")
Ofte får vi dataene som enkeltverdier og må gruppere dataene selv. Dette kan vi lett gjøre ved hjelp av for-løkker.
For å finne ut om en verdi er inni et intervall bruker vi listefiltrering: [ element for element in liste if logisk test]
Vi går gjennom elementene i listen og tar bare med de elementene som oppfyller den logiske testen.
For å trekke ut alle elementer mellom to verdier må vi da skrive: [ verdi for verdi in liste if a <= verdi < b ]
Hvor a og b er to tall. I den logiske testen brukte vi to nye operatorer: <=
(mindre enn eller lik) og <
(mindre). Denne testen returnerer sann dersom verdi
er større enn eller lik a
, og samtidig mindre enn b
.
Dette uttrykket trekker ut alle verdier fra en liste mellom 0 og 2:
In [15]: observasjoner = [0, 0, 2, 6, 7, 2, 1, 4, 1, 7, 2, 9, 4, 10, 9, 3, 7, 9, 2, 1] In [16]: [verdi for verdi in observasjoner if 0 <= verdi <2] Out[16]: [0, 0, 1, 1, 1]
hvis vi er interessert i antall observasjoner i listen bruker vi funksjonen len
på den filtrerte listen:
In [17]: len([0,0,1,1,1,]) Out[17]: 5
eller direkte:
In [18]: len([verdi for verdi in observasjoner if 0 <= verdi <2]) Out[18]: 5
For å gruppere alle tallene legger vi dette uttrykket i en for
-løkke hvor vi går gjennom alle nerdre og øvre grenser:
In [19]: nedreGrenser = [0,2,5,8] In [20]: øvreGrenser = [2,5,8,10] In [21]: grupperteFrekvenser = [] In [22]: for nedreGrense, øvreGrense in zip(nedreGrenser, øvreGrenser): ...: grupperteFrekvenser.append(len([verdi for verdi in observasjoner if nedreGrense <= verdi < øvreGrense])) In [23]: grupperteFrekvenser Out[23]: [5, 7, 4, 3]
Vi ser at vi har to lister, øvre og nedre grenser, vi kan koble disse sammen til en liste, grenser siden vi kan hente ut listene over øvre og nedre grenser som underlister.
Til slutt kan vi samle filtreringskoden som en funksjon:
def grupperFrekvenser(grenser, observasjoner): gruppert = [] for nedreGrense, øvreGrense in zip(grenser[:-1],grenser[1:]): gruppert.append(len([x for x in observasjoner if nedreGrense <= x < øvreGrense])) return gruppert
Tester denne funksjonen:
In [25]: observasjoner = [0, 0, 2, 6, 7, 2, 1, 4, 1, 7, 2, 9, 4, 10, 9, 3, 7, 9, 2, 1] In [26]: grenser = [0, 2, 5, 8, 10] In [27]: grupperteFrekvenser = grupperFrekvenser(grenser, observasjoner) In [28]: grupperteFrekvenser Out[28]: [5, 7, 4, 3]
Når vi har gruppert observasjonene kan vi tegne dem som et histogram:
In [29]: %matplotlib inline In [30]: tegnHistogram(grenser, grupperteFrekvenser)
Vi har til nå sett på grupperte frekvenstabeller. Disse har to attributter: grenser og frekvenser.
I tillegg har vi gjort en del beregninger: vi har regnet ut bredder, høyder, øvre- og nedre grenser. Disse beregningene blir bare riktige hvis vi gjør dem på grensene og frekvensene til en gruppert frekvenstabell. Derfor passer det bra å samle attributtene og metodene i en klasse som vi kaller frekvenstabell:
import matplotlib.pyplot as plt class gruppertFrekvenstabell: def __init__(self,grenser, frekvenser,beskrivelse=""): # konstruktør self.grenser = grenser self.frekvenser = frekvenser self.beskrivelse = beskrivelse def hentNedreGrenser(self): # funksjon som returnerer de nedre grensene return self.grenser[:-1] def hentØvreGrenser(self): # funksjon som returnerer de øvre grensene return self.grenser [1:] def hentFrekvenser(self): # funksjon som returnerer klassefrekvenser return self.frekvenser def hentBredder(self): # funksjon som returnerer klassebreddene return [x-y for x,y in zip(self.grenser[1:], self.grenser[:-1])] def hentHøyder(self): # funksjon som returnerer klassehøydene return [x/y for x, y in zip(self.frekvenser,self.hentBredder())] def histogram(self): # funksjon som representerer den grupperteFrekvenstabellen som et histogram plt.bar(self.hentNedreGrenser(),self.hentHøyder(),self.hentBredder(),align="edge", color='lightblue', edgecolor='blue') plt.title(self.beskrivelse) plt.show()
Vi ser at de fleste metodene bare returnerer enkle beregninger og attributtene. Dette er fordi vi ikke ønsker å bruke objektatributter direkte utenfor objektene selv. Fordelen med å gjøre det slik er at vi kan endre på den indre strukturen til klassen uten å endre resten av programmet vårt. For eksempel kan vi endre på klassen vår slik at vi lagrer grenser og frekvenser som en ordliste uten at vi trenger å endre på måten vi henter ut verdiene.
Vi legger klassedefinisjonen inn et Python-program og kjører dette. Etterpå kan vi teste den nye klassen i konsollvinduet:
In [32]: gf1 = gruppertFrekvenstabell(grenser,grupperteFrekvenser,"Tilfeldig generert histogram") In [33]: gf1.histogram()
Vi ønsker å samle alt som har med grupperte frekvenstabeller i denne klassen. I den forbindelse vil vi at grupperFrekvenser-funksjonen skal kobles til gruppertFrekvenstabell-klassen.
grupperFrekvenser tar inn grenser og data og returnerer grenser og grupperteFrekvenser, det vil si de to listene vi har koblet sammen til et gruppertFrekvenstabell–objekt.
Vi kan ikke gjøre denne funksjonen til en vanlig metode, siden vi har ikke noe gruppertFrekvens-objekt å kalle før vi har kalt grupperFrekvenser. Vi kan i stedet gjøre denne funksjonen til en statisk metode. En statisk metode kaller vi med: Klassenavn.statiskMetode()
Vi gjør funksjonen til en statisk metode kalt lagFraObservasjoner som returnerer et gruppertFrekvenstabell-objekt.
Legger denne koden til på slutten av klassedefinisjonen vår:
@staticmethod def lagFraObservasjoner(grenser, observasjoner, beskrivelse = ""): gruppert = [] for nedreGrense, øvreGrense in zip(grenser[:-1],grenser[1:]): gruppert.append(len([x for x in observasjoner if nedreGrense <= x < øvreGrense])) return gruppertFrekvenstabell(grenser, gruppert, beskrivelse)
@staticmetod sier at den neste metoden skal være statisk.
Vi kjører programmet vårt med klassedefinisjonen på nytt og tester om den nye statiske metoden virker:
In [35]: gf2 = gruppertFrekvenstabell.lagFraObservasjoner(grenser,observasjoner,"Gruppert frekvenstabell fra observasjoner") In [36]: gf2.histogram()
En metode som lager et objekt kaller vi for en factory-metode. Siden det bare er mulig å ha en konstruktør, men det ofte er mange måter å lage et objekt på, må vi av og til bruke slike metoder.
Yorumlar
Yorum Gönder