9. Visualisering med Plotly#

Kapitlet bruker følgende datafiler:
oda-master-thesis.csv
folkebibl-2020-kommuner.csv


I dette kapitlet går vi igjennom hvordan visualisere aggregerte mønstre i form av grafer, og gir en inngang til enkle statistiske oversikter.

Det finnes flere Python-moduler som kan brukes for å generere diagramer (stolpediagram, kakediagram, osv.). Noen av de meste kjente er Matplotlib[1], Seaborn[2] og Bokeh[3]. Her skal vi bruke Plotly. Det er også mulig å lage og eksportere csv-filer som kan brukes i eksterne visualiseringsprogrammer. Dette er alt fra verktøy som gir tilgang til hundrevis av tilpassbare måter å visualisere data som hos RawGraphs, til verkøy som er laget for spesifikke typer nettverksanalyser og -visualiseringer, som GephiLite.

En fordel med Plotly er at det lar deg lage interaktive diagramer direkte i Jupyter Notebooks.

Innlasting av modulene og tilrettelegging#

Data som brukes med Plotly kommer fra en Pandas dataframe. Vi må derfor importere både Plotly og Pandas.

1import pandas as pd
2import plotly.express as px
3import plotly.io as pio 
4pio.renderers.default = 'notebook'

Plotly Express er en del av Plotly. Det er vanlig å referere til modulen som px, på samme måte som det er vanlig å referere til Pandas som pd. De to siste linjene, 3 og 4 trenges i visse situasjoner for at plottene skal vises. Vi skal nå gå gjennom enkle diagrammer som vi kan generere med Plotly

Stolpediagram#

I forrige kapittel så vi på et eksempel av data om masteroppgaver ved OsloMet. Vi starter ved å opprette dataframen fra csv-filen i data-katalogen, og ber den vise oss hvilke kolonner datasettet består av:

df_oda = pd.read_csv('data/oda-master-thesis.csv', sep = ',')
df_oda.columns
Index(['Title', 'Identifier', 'Creators', 'Supervisors', 'Lang', 'Subjects',
       'SubjectsVDP', 'Date', 'Institute', 'FacultyShort', 'FacultyLong',
       'InstituteShort', 'InstituteLong', 'MasterProg', 'LangNorm'],
      dtype='object')

For helt grunnleggende statistikk er det viktig å kunne telle antall forekomster av verdier i diverse kolonner. Nedenfor bruker vi funksjonen .value_counts() til å telle antall oppgaver hvert institutt har i datasettet.

1df_oda['InstituteShort'].value_counts()
SHA    521
SF     499
HHS    478
GFU    275
YLU    263
EST    185
AV     167
IT     133
ABI    129
BLU    102
PD      93
BE      84
FYS     78
IST     70
NVH     42
JM      23
EO       7
Name: InstituteShort, dtype: int64

Vårt første plott blir et stolpediagram med disse fordelingen av språk på tvers av alle oppavene. For å bruke dette i Plotly må vi først gjør om resultatet av tellingen (.value_counts()) til en dataframe.

1df_temp = df_oda['LangNorm'].value_counts().to_frame()
2df_temp
LangNorm
nor 2443
eng 695
oth 11

I cellen nedenfor gjør vi om den gamle rad-indeksen (kolonnen med språkene) til en vanlig kolonne og gir deretter kolonnene nye navn. Denne eksisterer nå som en ny dataframe.

1df_temp.reset_index(inplace = True)
2
3col_map = {
4    'index' : 'Språk',
5    'LangNorm' : 'Antall'
6}
7
8df_temp.rename(columns = col_map, inplace = True)
9df_temp
Språk Antall
0 nor 2443
1 eng 695
2 oth 11

Den resulterende dataframen, df_temp, kan nå brukes for å lage stolpediagrammet. I cellen nedenfor opprettes en objektvariabel fig som tilordnes resultatet av .bar()-funskjonen fra Plotly Express. fig representerer et objekt av typen Figure, som har mange funksjoner (også kalt metoder). Metoden show() som brukes til slutt er den som driver mekanismen som viser diagrammet i tegneområdet. Metoden .bar() som produserer og returnerer fig-objektet kan ta mange parametre. De viktigste listes nedenfor:

* dataframen i eksempelet: `df_temp`
* tittelen gitt til horisontalaksen (x) i eksempelet: 'Språk'
* tittelen gitt til vertikalaksen (y) i eksempelet: 'Antall'
1import plotly.io as pio 
2pio.renderers.default = 'notebook'
3fig = px.bar(
4    df_temp,
5    x = 'Språk',
6    y = 'Antall'
7)
8
9fig.show()

Dette resulteter i stolpediagrammet på figuren over.

✍️ Oppgave: Gjør det samme som over i din egen notebook. Bytt gjerne ut “Språk” med en annen verdi som du tenker det kan være hensiktsmessig å telle.

Hvis vi lar musepekeren hvile til høyre over diagramet, vises en knapperad som lar oss manipulere diagramet (figuren nedenfor). Her kan vi lagre diagrammet som et bildefil i PNG-formatet, zoome inn og ut, osv.

Plotly_knapper

Endre tittel og farger#

Vi kan legge til en tittel på diagrammet ved å bruke title-parameteren til .bar()-funskjonen (Det samme kan vi gjøre hos de andre av Plotly sine graf-funksjoner).

Det er stor fleksibitet i hvilke farger som kan brukes. Dette oppgis med color_discrete_sequence-parameteren. Det finne flere ferdige fargekombinasjoner, f.eks. px.colors.qualitative.T10. Noen er vist på figuren .

–> Se Plotly dokumentasjon for en fullstendig liste.

plotly-colour-palettes

Alternativt kan vi bruke fargenavn fra CSS, AliceBlue, AntiqueWhite, osv. Eller heksadesimalkoder, f.eks. #00FFFF.

Se f.eks. W3Schools for en oversikt over koder.

Flere, “grunnleggende” farger kan angis ved navn.

1colours = ['green']
2
3colours
['green']

Når vi legger inn koder slik, selv om det bare er ett element som skal fargelegges (som ovenfor), må disse legges inn i en Python-liste. Hvis stolpene skal være grønne og vi har tre stolper kan vi skrive ['green', 'green', 'green'].

1colours = ['green', 'green', 'green']
2colours
['green', 'green', 'green']

Men vi kan også gjøre dette på en annen måte om vi skal ha samme farge for alle kolonner. Legg merke til cellen nedenfor. Her brukes *-operatoren til å mangfoldiggjøre listen ['green'] et gitt antall ganger (her 3).

1['green'] * 3
['green', 'green', 'green']

Dette gir oss muligheten til enkelt å bruke len()-funksjon (dataframens lengde) for å fargelegge alle rader i dataframen likt.

1colours = ['green']
2colours * len(df_temp)
['green', 'green', 'green']

Og slik brukes det i selve plottet:

1fig = px.bar(
2    df_temp,
3    x = 'Språk',
4    y = 'Antall',
5    color_discrete_sequence = ['green'] * len(df_temp),
6    title = 'Fordeling av OsloMet masteroppgavene etter språk'
7)
8
9fig.show()

Kakediagram#

Fordeling av språk kan også vises som et kakediagram ved å bruke .pie()-funksjonen.

Dataframen som brukes er den samme som ovenfor. Istedenfor x og y brukes parameteren names for kategoriene og values for fordelingen.

Her ønsker vi i tillegg vise språkenes fulle navn (Dette kan også gjøres i stolpediagrammet, bare prøv!). Til dette anvender vi dict’en value_map.

 1df_temp = df_oda['LangNorm'].value_counts().to_frame()
 2df_oda
 3df_temp.reset_index(inplace = True)
 4
 5col_map = {
 6    'index' : 'Språk',
 7    'LangNorm' : 'Antall'
 8}
 9
10df_temp.rename(columns = col_map, inplace = True)
11
12value_map = {
13    'nor' : 'Norsk',
14    'eng' : 'Engelsk',
15    'oth' : 'Annet'
16}
17
18df_temp['Språk'] = df_temp['Språk'].map(value_map)
19
20df_temp
Språk Antall
0 Norsk 2443
1 Engelsk 695
2 Annet 11

Nedenfor ser vi også bruk av parametrene til .update_layout()-metoden, som kan brukes for å midtstille overskriften (title_x) og endre skriftstørrelse til overskriften (title_font_size).

 1fig = px.pie(
 2    df_temp, 
 3    names = 'Språk',
 4    values = 'Antall'
 5)
 6
 7fig.update_layout(
 8    showlegend = False
 9)
10
11fig.update_traces(
12    textposition = 'outside',
13    textinfo = 'percent+label'
14)
15
16fig.show()

Resultatet ser vi på figuren over.

Hvis musepekeren hviler over et kakestykke vises ekstra opplysninger om kakestykke: Som vi ser på figuren nedenfor, kategorien eng og absoluttallet 2443. Prøv også å flytte markøren over diagrammet over.

plotly-pie-hover

Vi kan legge inn opplysninger i selve kaken ved å bruke .update_traces()-metoden (se linje 11 i koden over). Vi har fjernet noen av parametrene fra før for å spare plass. Hvis kategoriene legges i kaken kan vi vurdere å fjerne forklaringen til diagrammet (eng. “legend”). Dette gjøres ved å sette parameteren showlegend til False.

Merk at annet språk (oth) vises så vidt ikke på figuren nedenfor (en stripe grønt), grunnet størrelsen. Fargene som brukes her er standardfargene til Plotly.

plotly-pie-no-legend-result

Gruppert stolpediagram#

For å sammenligne språkfordelingen på fakultetene, bruker vi .groupby()-metoden i Pandas. For å visualisere slike analyser kan vi bruke grupperte eller stablete stolpediagram.

I cellen nedenfor grupperer vi språkkodene etter fakultet. Settingen normalize = True medfører at tallene vises som relativtall. Resultatet gjøres om til en dataframe.

1df_temp = df_oda.groupby('FacultyShort')['LangNorm'].value_counts(normalize = True).to_frame()
2
3df_temp
LangNorm
FacultyShort LangNorm
HV nor 0.829448
eng 0.166871
oth 0.003681
LUI nor 0.887324
eng 0.112676
SAM nor 0.754650
eng 0.240035
oth 0.005314
TKD nor 0.575758
eng 0.420202
oth 0.004040

Som du ser, har vi her en multiindex, en indeks med flere nivåer (her: to). Legg også merke til at value_counts()-funksjonen produserer en tallkolonne (på multiindeksens øverste nivå), som heter det samme som kolonnen på annet nivå, noe som må “brytes opp”. Dette gjør vi ved å endre førstenivåkolonnens navn til Prosent:

1col_map = {
2    'LangNorm' : 'Prosent'
3}
4df_temp.rename(columns = col_map, inplace = True)
5
6df_temp
Prosent
FacultyShort LangNorm
HV nor 0.829448
eng 0.166871
oth 0.003681
LUI nor 0.887324
eng 0.112676
SAM nor 0.754650
eng 0.240035
oth 0.005314
TKD nor 0.575758
eng 0.420202
oth 0.004040

rename()-funksjonen forholder seg til det øverste nivået når det lager Prosent-kolonnen. Nedenfor prosentuerer vi denne i praksis, simpelthen ved å gange verdiene med 100:

1df_temp['Prosent'] = df_temp['Prosent'] * 100
2
3df_temp
Prosent
FacultyShort LangNorm
HV nor 82.944785
eng 16.687117
oth 0.368098
LUI nor 88.732394
eng 11.267606
SAM nor 75.465013
eng 24.003543
oth 0.531444
TKD nor 57.575758
eng 42.020202
oth 0.404040

Tilbake til multiindeksen:

1df_temp.index
MultiIndex([( 'HV', 'nor'),
            ( 'HV', 'eng'),
            ( 'HV', 'oth'),
            ('LUI', 'nor'),
            ('LUI', 'eng'),
            ('SAM', 'nor'),
            ('SAM', 'eng'),
            ('SAM', 'oth'),
            ('TKD', 'nor'),
            ('TKD', 'eng'),
            ('TKD', 'oth')],
           names=['FacultyShort', 'LangNorm'])

I følgende celle brukes reset_index for å gjøre om multiindeksen til vanlige kolonner, og innføre et default-tallindeks.

1df_temp.reset_index(inplace = True)
2df_temp
FacultyShort LangNorm Prosent
0 HV nor 82.944785
1 HV eng 16.687117
2 HV oth 0.368098
3 LUI nor 88.732394
4 LUI eng 11.267606
5 SAM nor 75.465013
6 SAM eng 24.003543
7 SAM oth 0.531444
8 TKD nor 57.575758
9 TKD eng 42.020202
10 TKD oth 0.404040

Til slutt omdøper vi kolonnen FacultyShort til Fakultet og kolonnen LangNorm til Språk. Dette gjør det enklere å holde styr på hva de ulike kolonnene består av.

1col_map = {
2    'FacultyShort' : 'Fakultet',
3    'LangNorm' : 'Språk'
4}
5
6df_temp.rename(columns = col_map, inplace = True)
7
8df_temp
Fakultet Språk Prosent
0 HV nor 82.944785
1 HV eng 16.687117
2 HV oth 0.368098
3 LUI nor 88.732394
4 LUI eng 11.267606
5 SAM nor 75.465013
6 SAM eng 24.003543
7 SAM oth 0.531444
8 TKD nor 57.575758
9 TKD eng 42.020202
10 TKD oth 0.404040

For å lage en gruppert stolpediagram må vi legge til noen parameter til .bar()-funskjonen vi brukte tidligere. Vi må legge til barmode-parameteren med group som verdi (linje 6). Selve gruppene legges inn som x-akse. Kategoriene som grupperer, her språk, legges inn i color-parameteren.

 1fig = px.bar(
 2    df_temp,
 3    x = 'Fakultet',
 4    y = 'Prosent',
 5    color = 'Språk',
 6    barmode = 'group',
 7    color_discrete_sequence = px.colors.qualitative.T10,
 8    title = 'Språk av OsloMet masteroppgavene etter fakultet',
 9)
10
11fig.show()

✍️ Oppgave: Gjør det samme som beskrevet over, men forsøk å gruppere etter institutt og ikke fakultet.

Stablet stolpediagram#

Den eneste endring som er nødvendig for å endre fra gruppert til stablet stolpediagram er å endre verdien av barmode-parameteret til relative (linje 6).

Hvis du klikker på en kategori i forklaringen, fjernes katagorien fra visningen. Klikk en gang til for å hente den tilbake. For eksempel, hvis vi kun vil vise andelen som er på engelsk kan vi klikke på nor og oth.

 1fig = px.bar(
 2    df_temp,
 3    x = 'Fakultet',
 4    y = 'Prosent',
 5    color = 'Språk',
 6    barmode = 'relative',
 7    color_discrete_sequence = ['green', 'yellow', 'red'], #px.colors.qualitative.T10,
 8    title = 'Språk av OsloMet masteroppgavene etter fakultet',
 9)
10
11fig.show()

Linjediagram#

Linjediagrammer er ofte brukt med tidsdata, slik at X-aksen representerer tiden. Hvordan har fordelingen av språk endret seg over tid? Som før lager vi en dataframe med dataene vi ønsker å bruke i visualiseringen. Mye av dette ligner det forrige eksemplet, men her grupperer vi på dato heller enn på fakultet ( se linje 1 i cellen nedenfor.).

 1df_temp = df_oda.groupby('Date')['LangNorm'].\
 2value_counts(normalize = True).to_frame()
 3
 4col_map = {
 5    'LangNorm' : 'Prosent'
 6}
 7df_temp.rename(columns = col_map, inplace = True)
 8df_temp.reset_index(inplace = True)
 9df_temp['Prosent'] = df_temp['Prosent'] * 100
10
11df_temp.head()
Date LangNorm Prosent
0 2003 nor 100.000000
1 2005 eng 90.909091
2 2005 nor 9.090909
3 2006 eng 75.000000
4 2006 nor 25.000000

For at data skal vises riktig, må verdiene i Date-kolonnen (som vi ønsker å omdøpe til År) endres fra tekststrenger til datoer. Dette gjøre vha .to_datatime()-funksjonen fra Pandas (linje 7). Datoer kan skrives på mange ulike måter. Vi kan bruke format-parameteren til å angi formatet. Formatet %Y betyr at kun årstallet vises.

1col_map = {
2    'Date' : 'År',
3    'LangNorm' : 'Språk'
4}
5df_temp.rename(columns = col_map, inplace = True)
6
7df_temp['År'] = pd.to_datetime(df_temp['År'],  format = '%Y')
8
9df_temp.head()
År Språk Prosent
0 2003-01-01 nor 100.000000
1 2005-01-01 eng 90.909091
2 2005-01-01 nor 9.090909
3 2006-01-01 eng 75.000000
4 2006-01-01 nor 25.000000

⚠️ Merk! Legg merke til at Pandas har lagt 01-01 til året for å gjøre det om til en dato.

For å vise et linjediagram bruker vi line()-funksjon i Plotly. Bruk av color-parameteren gir oss en linje for hver kategori (linje 4). markers-parameter bestemmer om punktene skal markeres på linjen (linje 5).

1fig = px.line(df_temp, 
2    x= 'År', 
3    y= 'Prosent',
4    color = 'Språk', # Hva er det fargene skiller på
5    markers = True
6)
7
8fig.show()

Resultatet ser du på plottet over. Legg merke til at vi mangler data for noen tidspunkter. Dette er et eksempel på at når vi jobber med å få frem mønstre i data vil vi også oppdage noe det kan være interessantå dykke dypere ned i.

Hva skjedde i 2005? Her bør vi se i grunnlagsdataene. Vi lager et filter for å kun se rader fra 2005 (linje 1). Vi avgrenser visningen til kolonnene for tittel, årstall, språk og institutt (linje 3-8).

 1filter1 = df_oda['Date'] == 2005
 2
 3cols = [
 4    'Title',
 5    'Date',
 6    'Lang',
 7    'InstituteLong'
 8]
 9
10df_oda[filter1][cols]
Title Date Lang InstituteLong
2095 Langtidssosialhjelpsmottakere : veien til sosi... 2005 nb Institutt for sosialfag
2664 Evaluation of file access control implementations 2005 en Institutt for informasjonsteknologi
2677 Predicting TCP congestion through active and p... 2005 en Institutt for informasjonsteknologi
2679 High speed network sampling 2005 en Institutt for informasjonsteknologi
2687 Administration of remote computer networks 2005 en Institutt for informasjonsteknologi
2707 Retrivability of data in ad-hoc backup 2005 en Institutt for informasjonsteknologi
2710 Virtual user simulation 2005 en Institutt for informasjonsteknologi
2716 User survey of practices using cfengine 2005 en Institutt for informasjonsteknologi
2718 Passive traffic characterization and analysis ... 2005 en Institutt for informasjonsteknologi
2754 Traffic classification with passive measurement 2005 en Institutt for informasjonsteknologi
2763 Comparison of NFS, Samba and AFS 2005 en Institutt for informasjonsteknologi

Over ser vi at vi har kun 11 oppgaver fra 2005. 10 var skrevet på engelsk og alle var skrevet på instituttet for informasjonsteknologi. Hva dette skyldes kan ikke dataene vi har alene svare på.

Spredningsdiagram#

Her bruker vi data fra statistikk for norske folkebibliotek. Vi er interessert i å vise forholdet mellom folketall og besøkstall. Er det slik at bibliotek i kommuner med høyere folketall har mer besøk?

1df_bib = pd.read_csv('data/folkebibl-2020-kommuner.csv', sep = ',')
2df_bib.columns, df_bib.shape
(Index(['Antall kommuner i fylket', 'Fylkesnr.', 'Kommunenr.', 'Kommune',
        '0-14 år per 1.1.2020', '15 år og eldre per 1.1.2020',
        'Totalt per 1.1.2020', 'Besøk totalt', 'Besøk i betjent åpningstid',
        'Arrangement skoler - totalt',
        ...
        'Kombinasjons-bibliotek med annen skole/\r\ninstitusjon',
        'Utlånssteder utenfor biblioteket', 'Leseplasser ved hoved-biblioteket',
        'Leseplasser ved avdelingene', 'Nye lokaler', 'Antall mobile enheter',
        'Antall stoppesteder for mobile enheter',
        'Regnskap, lønn og sosiale utgifter ', 'Regnskap, kjøp av medier',
        ' Regnskap lønn- og medieutgifter totalt '],
       dtype='object', length=212),
 (355, 212))

Over viser vi (et utdrag av) kolonnenavnene og størrelsen på datatabellen. CSV-filen med bibliotekstatistikk har over 200 kolonner med statistikk.

Vi ser så nærmere på hva som er i dette datasettet ved å be den vise frem filen som en dataframe:

1df_bib
Antall kommuner i fylket Fylkesnr. Kommunenr. Kommune 0-14 år per 1.1.2020 15 år og eldre per 1.1.2020 Totalt per 1.1.2020 Besøk totalt Besøk i betjent åpningstid Arrangement skoler - totalt ... Kombinasjons-bibliotek med annen skole/\r\ninstitusjon Utlånssteder utenfor biblioteket Leseplasser ved hoved-biblioteket Leseplasser ved avdelingene Nye lokaler Antall mobile enheter Antall stoppesteder for mobile enheter Regnskap, lønn og sosiale utgifter Regnskap, kjøp av medier Regnskap lønn- og medieutgifter totalt
0 NaN 30.0 3024.0 Bærum 24 806 102 925 127 731 307 528 307 145 46.0 ... 0.0 2.0 209 380 Nei 0.0 0.0 32 743 318 3 551 352 36 294 670
1 NaN 30.0 3005.0 Drammen 17 439 83 947 101 386 275 962 NaN 31.0 ... 0.0 0.0 NaN NaN Nei 0.0 0.0 20 736 256 1 543 435 22 279 691
2 NaN 30.0 3025.0 Asker 18 067 76 374 94 441 268 127 266 115 183.0 ... NaN 1.0 238 122 Nei NaN NaN 23 056 421 2 375 880 25 432 301
3 NaN 30.0 3030.0 Lillestrøm 15 999 69 984 85 983 171 063 160 234 55.0 ... 0.0 0.0 200 168 Nei 0.0 0.0 16 607 879 1 789 507 18 397 386
4 NaN 30.0 3004.0 Fredrikstad 13 566 68 819 82 385 175 412 175 412 29.0 ... 1.0 NaN 136 72 Ja 0.0 0.0 17 111 866 1 459 072 18 570 938
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
350 NaN 54.0 5415.0 Loabák - Lavangen 180 854 1 034 NaN NaN 1.0 ... 0.0 0.0 10 0 Nei 0.0 0.0 339 325 38 595 377 920
351 NaN 54.0 5433.0 Hasvik 148 857 1 005 740 548 0.0 ... NaN NaN 8 NaN Nei NaN NaN 247 509 60 000 307 509
352 NaN 54.0 5440.0 Berlevåg 106 851 957 794 794 0.0 ... 1.0 0.0 10 0 Nei 0.0 0.0 396 268 32 051 428 319
353 NaN 54.0 5442.0 Unjárga - Nesseby 129 797 926 808 808 2.0 ... NaN NaN 16 NaN Nei 1.0 82.0 584 066 53 000 637 066
354 NaN 54.0 5432.0 Loppa 85 803 888 1 007 NaN NaN ... NaN 3.0 10 NaN Nei NaN NaN 750 000 50 000 800 000

355 rows × 212 columns

Her er vi interessert i kolonnen Besøk totalt og Totalt per 1.1.2020. Imidlertid kan det virke som det som står i kolonnen “Besøk totalt” er tekst. En indikasjon på dette er at det er et mellomrom mellom de tre første tallene og de tre siste.

Kolonnen Totalt per 1.1.2020 må vaskes før vi kan bruke den videre.

Vi vasker direkte i notebooken (men dette kan også gjøres i OpenRefine). Vi fjerner først mellomrom fra tallene (eks. 307 528 til 307528). Nå som mellomrom er borte kan vi endre datatype i kolonnen til å være tall og ikke tekst. Til dette bruker vi i begge cellene, ovenfor linje 8 og nedenfor linje 5, Pandas-funksjonen to_numeric. Vi omdøper også Totalt per 1.1.2020 til Folketall:

 1# definer funksjonen vi skal anvende
 2def vaskMellomRom(verdi):
 3    return verdi.replace(' ', '')
 4
 5#fjern mellomrom i tall. (typisk dataproblem)
 6df_bib['Totalt per 1.1.2020'] = df_bib['Totalt per 1.1.2020'].apply(vaskMellomRom)
 7
 8# Go kolonne nytt navn
 9col_map = {
10    'Totalt per 1.1.2020' : 'Folketall'
11}
12df_bib.rename(columns = col_map, inplace = True)
13
14# Endre datatype
15df_bib['Folketall'] = pd.to_numeric(df_bib['Folketall'])

Her brukte vi metoden Series.apply(), som anvender en funksjon (her vaskMellomRom) på hver av innførslene. Se på syntaksen på linje 6: apply() bruker funksjonens navn alene (uten parentes og argument). Bak kulissene brukes hver av innførslene, etter tur, som argument til funksjonen, og vasket verdi returneres.

✍️ Oppgave: Gjør det samme som over i din egen notebook. Sjekk så df_bib for at endringene er blitt gjort og for å få en oversikt over hvordan dataframen nå ser ut.

En annen måte å fjerne mellomrom på er gjort med den andre kolonnen i cellen under. Her skjer mer bak kulissene, men produserer samme resultat; mellomrom fjernes.

1df_bib['Besøk totalt'].fillna('', inplace = True)
2
3df_bib['Besøk totalt'] = df_bib['Besøk totalt'].apply(lambda x: x.replace(' ', ''))
4
5df_bib['Besøk totalt'] = pd.to_numeric(df_bib['Besøk totalt'])

Utgangspunktet for diagrammet kodes i cellen nedenfor. På linje 3 oppretter vi en ny kolonne som inneholder besøk per person. Vi viser data for de ti bibliotek med høyest besøkstall.

1cols = ['Kommune', 'Folketall', 'Besøk totalt', 'Besøk per person']
2
3df_bib['Besøk per person'] = df_bib['Besøk totalt'] / df_bib['Folketall']
4
5df_bib.sort_values('Besøk totalt', ascending = False)[cols].head(10)
Kommune Folketall Besøk totalt Besøk per person
51 Oslo 693494 2441381.0 3.520407
145 Stavanger 143574 1039369.0 7.239256
168 Bergen 283929 733405.0 2.583058
237 Trondheim 205163 669357.0 3.262562
120 Kristiansand 111633 314976.0 2.821531
0 Bærum 127731 307528.0 2.407622
316 Tromsø 76974 300944.0 3.909684
1 Drammen 101386 275962.0 2.721895
2 Asker 94441 268127.0 2.839095
275 Bodø 52357 253000.0 4.832210

Nedenfor bruker vi scatter-funksjonen fra Plotly for å lage spredningsdiagrammet. Igjen, parametre for tittel osv. er utelatt for å fremheve de viktigste parametrene. Vi bruker hover_name-parameteren for å legge ekstra data til boksen som vises når vi sette musepekeren over et punkt på diagrammet.

 1#Vil vise sammenheng i småsteder
 2
 3fig = px.scatter(
 4    df_bib,
 5    x = 'Folketall',
 6    y = 'Besøk totalt',
 7    hover_name = 'Kommune'
 8)
 9
10fig.show()

Ovenfor vises resultatet. De alle fleste bibliotek er klumpet nederst til venstre i diagrammet.

Vi kan bruke et filter for å fjerne de fire største komunnene. På linje 2 i kodecellen nedenfor oppretter vi en liste med de kommunene som vi ikke vil ha. På linje 4 bruker vi .isin()-metoden for å lage et filter som kun tar med disse fire kommune. Når vi bruker filteret på dataframe (linje 7), setter vi ~-tegnet foran for å si at vi vil ha alt som ikke matcher filteret.

 1#Vil vise sammenheng i småsteder
 2fjern = ['Oslo', 'Bergen', 'Trondheim', 'Stavanger']
 3
 4filter1 = df_bib['Kommune'].isin(fjern)
 5
 6fig = px.scatter(
 7    df_bib[~filter1],
 8    x = 'Folketall',
 9    y = 'Besøk totalt',
10    hover_name = 'Kommune'
11)
12
13fig.show()

Resultatet ser du ovenfor.

Boksplot#

Blir bibliotek i større kommuner oftere besøkt (per innbygger) enn bibliotek i mindre kommuner? Eller er det kanskje omvendt? Èn måte å finne ut av det, er å dele kommunene i kvantiler: Altså et antall like store grupper bibliotek, i stigende antall innbyggere, og se gruppenes fordelinger av relative besøkstall (herunder kalt besøksrate) opp i mot hverandre. Linje 1 nedenfor deler vi kommunene i 4 folketallskvantiler, slik at de minste kommunene havner i det nederste kvantilet (0), de litt større i neste kvantilet (1), osv. Linje 2 legger til en kolonne i data-framen (kalt kvantil) hvor hver kommune får “sitt” kvantil (0, 1, 2 eller 3) tilordnet.

1kvantiler = pd.qcut(df_bib['Folketall'], 4, labels=False)
2df_bib = df_bib.assign(kvantil=kvantiler.values)

Nå er vi klare til å tegne boksplottet for hvert kvantil. Boksplot er en god måte å vise relative tendenser på, samt å isolere “outliers”.

 1fig = px.box(
 2    df_bib,
 3    x = 'kvantil',
 4    y = 'Besøk per person',
 5    hover_name = 'Kommune',
 6    color='kvantil',
 7)
 8fig.update_traces(quartilemethod="linear") # or "inclusive", or "linear" by default
 9
10fig.show()

Hold musepekeren over boksene. Et nøye blikk på boksene kan tyde på en svak tendens til at større kommuner har flere besøk per innbygger. Men er dette en virkelig trend? i statistikkspråk: Er den signifikant? eller med andre ord: Hva er sannsynligheten for at utviklingen vi ser er tilfeldig? Boksene vi ser tyder på at fordelingen er meget skjev. Altså: besøk-ratene i alle kvantilene klumper seg ikke likt til høyre og til venstre for et gjennomsnitt.

Hvis vi vil undersøke disse aggregerte mønstrene nærmere med utgangspunkt i statistiske analyser kan vi gjøre følgende beskrevet under:

Cellen nedenfor genererer en tetthetfordelingsplot, et slags glattet histogram, for hver av kvantilene. Her bruker vi figure_factory-modulen til Plotly.

 1import plotly.figure_factory as ff
 2
 3
 4kv0 = list(df_bib[df_bib['kvantil']==0]['Besøk per person'].fillna(0))
 5kv1 = list(df_bib[df_bib['kvantil']==1]['Besøk per person'].fillna(0))
 6kv2 = list(df_bib[df_bib['kvantil']==2]['Besøk per person'].fillna(0))
 7kv3 = list(df_bib[df_bib['kvantil']==3]['Besøk per person'].fillna(0))
 8
 9hist_data = [kv0, kv1, kv2, kv3]
10
11group_labels = ['kv0', 'kv1', 'kv2', 'kv3']
12colors = ['green', 'blue', 'red', 'black']
13
14# Create distplot with curve_type set to 'normal'
15fig = ff.create_distplot(hist_data, group_labels, show_hist=False, colors=colors)
16
17# Add title
18fig.update_layout(title_text='Besøksrate fire kvantiler')
19fig.show()

Ved å se på figuren over er det lett å miste håpet om at folketallet er forklarende til forskjell i besøksraten. Det er også lett å se at hovedtyngdene av kvantilene har en tilnærmet normal fordeling, hvor de lange halene representerer relativt få verdier. Da vi også har relativt mange datapunkter, drister vi oss til å kjøre en T-test mellom kv0 og kv3, som er kvantilene hvis gjennomsnitt ligger lengst unna hverandre i plottet.

En T-test tester om to datasett “kommer fra” den samme fordelingen (hypotesen h0) eller ikke (h1). Vi avviser stort sett h0 (default-hypotesen) hvis p-verdien er mindre enn 0,05.

1from scipy.stats import ttest_ind
2kv0 = list(df_bib[df_bib['kvantil']==0]['Besøk per person'].fillna(0))
3kv3 = list(df_bib[df_bib['kvantil']==3]['Besøk per person'].fillna(0))
4ttest_ind(kv0, kv3, equal_var=False)
Ttest_indResult(statistic=-0.5339022621071996, pvalue=0.5944288128684567)

Med en så stor p-verdi er vi langt fra å kunne avvise h0, dvs foreslå at besøksraten har noe med folketallet å gjøre.

Du har nå fått en smakebit på hvordan undersøke sammenheng mellom variabler i aggregerte mønstre nærmere. Merk at vi ikke skal gå nærmere inn på slike statistiske analyser i dette kapitlet.

Ordsky#

Det siste vi skal ta for oss er en metode som gir deg en alternativ oversikt til en lang liste over ordfrekvenser. Her er istedet størrelsen på ordet en indikasjon på forekomst, mens andre variabler som farge og plassering er arbitrære.

For å generere en ordky bruker vi wordcloud-modulen. For å installere moduelen åpner vi Anaconda Prompt i Anaconda. Skriv inn kommandoen pip install wordcloud.

I cellen nedenfor importeres modulen og en del variabler derfra. Deretter lager vi teksten som skal vises som et ordsky. Vi bruker emne-kolonnen fra ODA-datasettet om masteroppgavene. Først erstatter vi manglende verdier med blank (linje 2). Deretter oppretter vi en tom variabel, text (linje 4) som skal senere “fylles inn” med emneordene etter hvert som de hentes ut fra dataframens rader. For å avgrense til masteroppgaver skrevet ved ABI, lager vi et filter (linje 6). Vi går gjennom alle radene (oppgavene) som oppfyller filteret, og henter innholdet i Subjects-kolonnen (linje 9). Hvis en oppgave har flere emner, er emnene adskilt med |-tegnet. Vi bruker .replace()-metoden til å erstatte tegnet med et mellomrom (linje 8). På linje 11 sammenføyes emne(ne) bakerst i teksten. vi tilføyer en ekstra mellomrom for at siste ord i nåværende ikke sammenføyes direkte med førsteord i neste.

 1from wordcloud import WordCloud, ImageColorGenerator, STOPWORDS
 2df_oda['Subjects'].fillna('', inplace = True)
 3
 4text = ''
 5
 6filter1 = df_oda['InstituteShort'] == 'ABI'
 7
 8for idx, row in df_oda[filter1].iterrows():
 9    subject = row['Subjects'].replace('|', ' ')
10    
11    text += subject+ " "

Koden som lager ordskyet er vist under. I linjer 3 til 8 sette vi parameter til ordskyet. Teksten som vi har generert over brukes for å opprette ordskyen i linje 10.

 1stopwords = set(STOPWORDS)
 2
 3wc= WordCloud(background_color="black", 
 4              random_state=1,
 5              stopwords = stopwords,
 6              max_words = 200, 
 7              width = 1500, 
 8              height = 1500)
 9
10wc.generate(text)
<wordcloud.wordcloud.WordCloud at 0x7fc0bd091990>

Vi bruker Plotly for å vises bildet. Parametere i linjene 3-9 brukes for å fjerne aksene.

 1fig = px.imshow(wc)
 2
 3fig.update_xaxes(
 4    showticklabels=False
 5)
 6
 7fig.update_yaxes(
 8    showticklabels=False
 9)
10
11fig.show()

Resultatet vises ovenfor.

Du har nå gått gjennom en innføring i det å visualisere aggregerte mønstre gjennom ulike grafer. Dette har vi gjort med utgangspunkt i datasett som allerede eksisterer som CSV-filer.