11 - interaktive websider
Vi husker fra sist hvordan vi importerte pakken "gapminder":
import pandas as pd
import requests
# URL of the CSV file
url = "https://titlon.uit.no/hht/data/gapminder.csv"
#sometimes this doesn't work on mac
#g = pd.read_csv(url)#reading data
# Local file path to save the downloaded CSV
local_file_path = "gapminder.csv"
# Download the CSV file using requests
response = requests.get(url)
with open(local_file_path, 'wb') as f:
f.write(response.content)
# Read the locally saved CSV file into a Pandas DataFrame
g = pd.read_csv(local_file_path)
g
Vi plottet så dataene, etter å ha konverter BNP til logartime:
import numpy as np
from matplotlib import pyplot as plt
fig,ax=plt.subplots()
#adding axis lables:
ax.set_ylabel('Forventet antall leveår')
ax.set_xlabel('BNP per innbygger')
#plotting the function:
ax.scatter(np.log(g['gdp_cap']), g['life_exp'], label='Observasjoner')
ax.legend(loc='lower right',frameon=False)
Vi ønsker imidlertid å lage en litt mer interessant fremstilling enn dette ved å
Det første vi må gjøre for å få til det er å lage en datasettet som inneholder de dataene vi trenger for å få til dette. Vi starter med å lage oppslag som definerer norske navn og fargekode. Disse brukes så med pandas apply
-funksjon til å definere en kolonne continent_no
med norske navn og en colors
-kolonne:
#creating a dictionary of continents with norwegian translation:
continents={'Africa': 'Afrika', 'Americas': 'Sør/Nord-Amerika', 'Asia': 'Asia', 'Europe': 'Europa', 'Oceania': 'Oseania'}
#assigning a color hex-code to each continent
colors={'Africa': '#E14827', 'Americas': '#84E127', 'Asia': '#2792E1', 'Europe': '#BC27E1', 'Oceania': '#E04A6C'}
g['continent_no']=g['continent'].apply(lambda cont: continents[cont])
g['colors']=g['continent'].apply(lambda cont: colors[cont])
g
#E14827 er for eksempel koden for en rødfarge.
Punkt 3. var at størrelsen på prikkene skal indikere land. Da trenger vi en kolonne som angir størrelse i pikser, som python forstår, i stedet for faktisk befolkning. Variasjonen i befolkning er også så stor at vi tar roten av den faktiske befolkningen for at det skal se penere ut. Formelen for størrelse på prikkene settes derfor til pop**0.5/300
, der pop
er befolkning i millioner.
g['size']=g['population'].apply(lambda pop: pop**0.5/300)
display(g[g['country']=='India'])
g
Vi ser over at for India blir for eksempel størrelsen da 111 piksler.
Til sist var målsettingen i punkt 4 å lage interaktivitet ved å få hvert datapunkt til å vise ytterligere informasjon når vi holder pekeren over. Vi starter med å definerer variablene som skal poppe opp. For å få norsk tusenskilletegn, med mellomrom, kan vi gjøre slik:
'{:,}'.format(int(23153161365)).replace(',',' ')
Vi kan nå, med norsk tusenskilletegn, definere BNP i norske kroner (vekslingskurs 8.9NOK/USD), forventet livslengde og befolkning i millioner, i heltall.
g['gdp_capNOK']=g['gdp_cap'].apply(lambda gdp: '{:,}'.format(int(8.9*gdp)).replace(',',' '))
g['life_exp_rnd']=g['life_exp'].apply(lambda l: int(l))
g['pop_mill_str']=g['population'].apply(lambda pop: '{:,}'.format(int(pop/1000000)))
g
Vi skal bruke en pakke som heter bokeh
til å lage en interaktiv figur og nettside, av datasettet vi nettopp laget ferdig. Vi starter med å definere figuren. I Eksempel 7 lager vi en figur ved å kalle på figurfunksjonen i bokeh.
De første fire argumentene definerer tittel og navn på aksene.
x_axis_type = "log"
har samme effekt som å konvertere BNP til log i 10 - statsmodels, men med den forskjell at det er benevningen på aksen og ikke selve variabelen som konverteres.
Så defineres at det at grafen skal ha verktøytips ("tooltips"). Det vil si at det popper opp en forklaring når vi holder musen over et område, som nevnt i punkt 4. . Denne funksjonaliteten defineres med argumentet tools = "hoover"
.
I det neste argumentet defineres hva som skal vises som tips. Pekeren skal åpne en boks med en liten liste med navn på land, BNP/Innbygger, forventet levealder og befolkning. Dette oppnås med argumentet tooltips
. Hvert element i listen er en tuple
med tekst som skal vises, og hilken kolonne dataene skal hentes fra. For eksempel betyr ("Land","@country")
at på den første linjen skal teksten "Land" vises, og informasjonen skal hentes fra kolonnen "country" i datasettet.
Til sist settes størrelsen på plottet
from bokeh.plotting import figure
#creating figure:
p = figure(
title = "Levealder og BNP per innbygger",
x_axis_label = 'BNP per innbygger (NOK)',
y_axis_label = 'Forventet antall leveår',
x_axis_type = "log",
tools="hover",
tooltips = [
("Land","@country"),
("BNP/innbygger","@gdp_capNOK"),
("Forventet alder","@life_exp_rnd"),
("Befolkning","@pop_mill_str")
],
height = 580,
width = 980)
Vi kan nå definere hva slags plott vi skal ha. Vi skal ha et scatter-plott som i 10 - statsmodels, men dette blir altså interaktivt. Vi bruker da funksjonen scatter
.
Det første som må angis i scatter
er datasettet, som altså er g
her. Så angis at vi skal ha gdp_cap
langs x-aksen og life_exp
langs y-aksen.
Til sist angis størrelse, farge og hvilen kollonne som definerer fargeetikettnavn. Til sist angis at prikkene skal være 20% gjennomsiktig.
from bokeh.io import show, output_notebook
from bokeh.plotting import output_file
p.scatter(
source=g,
x= 'gdp_cap',
y= 'life_exp',
size='size',
color= 'colors',
legend_field='continent_no',
alpha= 0.8
)
#Creating the graph and saving as html
output_notebook()
output_file("BNP_levealder.html")
show(p)
Du kan nå selv teste vertktøytipsfunksonen ved å holde pekeren en av sirklene i figuren.
Som vi ser over kalles følgende tre funksjoner, etter å ha definert plottet:
output_notebook()
gir beskjed om at grafen skal vises i jupyteroutput_file("BNP_levealder.html")
angir at html-filen som skal lagres skal hete "BNP_levealder.html"show(p)
genererer html-filen og visningOm du ønsker å legge denne grafen inn i en annen webside kan du for eksempel bruke iframes. Her er et eksmpel på hvordan du gjør det fra W3Schools
Men det er grunn til ikke å være helt fornøyd med resultatet. For det første bør benvenelsen på x-aksen være mer lesevennlig. Vi endrer derfor denne fra vitenskapelig format til 1k, 10k og 100k, der k=1000. Etikettene ligger delvis over plottene, så vi flytter den til "top_left". I tillegg ser ikke linjene pene ut på en nettside, så vi fjerner de fleste av dem. Det kan vi gjøre med følgende kode:
#formatting:
p.xaxis.major_label_overrides = { 1000: '1k', 10000: '10k', 100000: '100k' }
p.legend.location = "top_left"
p.legend[0].border_line_alpha=0
p.outline_line_alpha=0
p.grid[0].grid_line_alpha=0
p.grid[1].grid_line_alpha=0
output_notebook()
output_file("BNP_levealder.html")
show(p)
import pandas as pd
import requests
# URL of the CSV file
url = r"https://titlon.uit.no/hht/data/valgkretser/data.csv"
#sometimes this doesn't work on mac
#g = pd.read_csv(url)#reading data
# Local file path to save the downloaded CSV
local_file_path = "data.csv"
# Download the CSV file using requests
response = requests.get(url)
with open(local_file_path, 'wb') as f:
f.write(response.content)
data=pd.read_csv(
local_file_path,
encoding="latin-1",
delimiter=";")
data
Som vi ser, inneholder datasettet ligningstall fra 2015, utdanningsnivå og politisk opplsutning ved valget 2019. "Rødhet" er differansen på hvor mange som har stemt Arbeiderpartiet, Sosialistisk Venstreparti og Rødt. HFrp angir hvor mange som stemte på Høyre og Frp. "Mynter" er et tall som er proporsjonalt med gjennomsnittlig inntekt. Vi kommer tilbake til hva det skal brukes til.
De siste to variabelene er Lat og Lon. De angir hvor på kartet vi skal sette markøren for hver bydel. Som du ser ligger alle plasseringene på nesten 70 grader nord.
For å importere kartdata bruker vi en pakke som heter "geopandas". Den kan lese kartfiler. Det finnes ulike formater for kartfiler, og geopandas leser de fleste. I dette tilfellet bruker vi kartdata som ligger på nettadressen https://titlon.uit.no/hht/data/valgkretser.zip
, som er fra Tromsø kommune. Disse dataene angir grensene for valgkretsene/bydelene i Tromsø.
Laster ned kartdata:
import geopandas as gpd
#loading the geographical data
#and converting to a coordinate system that folium understands:
# URL of the CSV file
url = r"https://titlon.uit.no/hht/data/valgkretser.zip"
#sometimes this doesn't work on mac
#g = pd.read_csv(url)#reading data
# Local file path to save the downloaded CSV
local_file_path = "valgkretser.zip"
# Download the CSV file using requests
response = requests.get(url)
with open(local_file_path, 'wb') as f:
f.write(response.content)
Henter data:
geodata = gpd.read_file("valgkretser.zip")
#Need to specify which cooridinate system to use
geodata=geodata.to_crs('epsg:4326')
geodata
Over ser vi hvordan et kartdatasett ser ut. De viktigste variablene her er "VKRETS_V_1" som angir navnet på bydelen og "geometry" som angir grensene til valgkretsen. Vi ser ikke all dataen som ligger i hver celle under "geometry", men i hver celle er det et "polygon" (flerkant") som angir en lang rekke punkter. Under vises de første 300 tegnene av Sentrum bydel. Vi ser at polygonet er en rekke kombinasjoner av lengde og breddegrad, som tilsammen tegner et området på et kart.
str(geodata['geometry'][geodata['VKRETS_V_1']=='Sentrum'].iloc[0])[0:300]
Setningen geodata=geodata.to_crs('epsg:4326')
sørger for at koordinatsystemet som tromsø kommune bruker, konverteres til det som brukes av folium
, som er kartpakken vi skal benytte her.
For å kunne slå sammen kart-dataene med de skatte- og valgdataene, må vi angi hvilken variabel som identifiserer bydelene i begge datasettene. Disse to variablene må ha samme navn, så vi endrer navn på 'VKRETS_V_1' i kartdataene til 'Bydel':
geodata=geodata.rename(columns={'VKRETS_V_1':'Bydel'})
Vi kan nå slå sammen dataene. Kollonenene 'Bydel' i hvert datasett angir hvilke rader som skal slås sammen. Det er endel felter i geodataene som vi ikke trenger, så vil filterer til slutt ut de variablene vi vil ha med:
#merging the demographic data into the geodata:
geodata_merged=geodata.merge(data,on='Bydel')
#Keeping only the relevant fields
geodata_merged.keys()
geodata_merged=geodata_merged[['Bydel', 'Inntekt', 'Formue', 'Skatt',
'Alder', 'Skattepliktige', 'Andel med høyere utdanning', 'Rødhet',
'ApSVR', 'HFrp', 'Mynter', 'Lat', 'Lon',
'GEOMETRITY', 'geometry']]
geodata_merged
Å lage en nettside med et kart, gjør vi ganske enkelt ved bruk av en pakke som heter folium
. Den følgende koden lager en nettside med et interaktivt kart (du kan navigere i det) med senter [69.67, 18.98] og zoom 10. Nettsiden lagres som 'tromso.html' og siden skal åpnes i en ny fane, når du kjører koden under.
import webbrowser
import folium
#Creating the map
tromso=folium.Map(location=[69.67, 18.98],zoom_start=10)
#Creating the html file
tromso.save('tromso.html')
#Open the html in new tab
webbrowser.open('tromso.html')
Det som gjenstår nå, er å fylle kartet med innhold. Vi begynner med å bruke geodata-settet til å fargelegge bydelene etter politisk tilhørighet. Flere stemmer til Frp og Høyre skal gi en mer blå farge, og stemmer til SV, Ap og Rødt skal gi mer
Vi har nå et kart, som vi nå skal fylle med informasjonen vi ordnet i forrige avsnitt. webbrowser.open('tromso.html')
åpner html-filen i en ny fane.
Følgende kode bruker funksjonen Choropleth
til å legge et lag over eksisterende kart med kommunegrensene fra
f=folium.Choropleth(
geo_data=geodata_merged,
name="choropleth",
columns=["Bydel", "Rødhet"],
data=geodata_merged,
key_on="feature.properties.Bydel",
fill_color="RdBu",
nan_fill_color='white',
fill_opacity=0.5,
legend_name="Valgresultat oppslutning H+Frp minus Ap+SV+Rødt"
).add_to(tromso)
folium.LayerControl().add_to(tromso)
#Saving a new html wtih colour overlay
tromso.save('tromso.html')
Om du laster inn websiden på nytt, vil du nå se at bydelene har fått farger etter stemmegivgning
Vi skal nå legge til markører for hver bydel, og et verktøytips til hver markør. Verktøytips (tooltip) er en liten tekstboks som dukker opp når vi holder pekere over markøren. Verktøytipset skal inneholde følgende informasjon:
Tallene må formateres for å se pene ut og være formatert som prosenttall i vertktøytipset, så vi ordner det først:
#cleaning data
import math
geodata_merged['Andel med høyere utdanning%'] = geodata_merged['Andel med høyere utdanning'].apply(
lambda x: f"{int(x)}%"
if not math.isnan(x) else 'NA')
geodata_merged['ApSVR%'] = geodata_merged['ApSVR'].apply(
lambda x: f"{int(x*100)}%"
if not math.isnan(x) else 'NA')
geodata_merged['HFrp%'] = geodata_merged['HFrp'].apply(
lambda x: f"{int(x*100)}%"
if not math.isnan(x) else 'NA')
geodata_merged[['Bydel', 'Andel med høyere utdanning%', 'ApSVR%', 'HFrp%']]
Vi itererer så gjennom alle bydelene, og legger til en markør som skal vise vertøytipset.
Først i iterasjonen finner vi breddegraden ("Lat") og lengdegraden ("lon") i geodata_merged-tabellen. Så lager vi en tekst-streng i html-format, som har fem linjer med informasjon som vises når musa beveger seg over markøren.
Vi setter markøren i folium
-pakken ved hjelp av Marker
-klassen, der det første argumentet er koordinatene på kartet, det andre argumentet er verktøytipset, altså tekstrengen som ble laget. Tredje argument angir at markøren skal være rød.
for i,r in geodata_merged.iterrows():
#obtaining coordinates:
lat=float(r['Lat'])
lon=float(r['Lon'])
#tooltips text:
t=(f"<b>{r['Bydel']}</b><br>"
f"Inntekt: { r['Inntekt'] }<br>"
f"Alder: { r['Alder'] }<br>"
f"Høyere utdanning: { r['Andel med høyere utdanning%'] }<br>"
f"Ap+SV+Rødt: { r['ApSVR%'] }<br>"
f"H+FrP: { r['HFrp%'] }")
#setting the marker:
folium.Marker(
[lat,lon-0.01],
tooltip=t,
icon=folium.Icon(color='red')
).add_to(tromso)
#saving the map as a html file:
tromso.save('./tromso.html')
Vi skal nå lage en stabel med mynter som skal representere gjennomsnittsinntekten I mappen img/coins/ ligger det bilder av én til ti mynter stablet i høyden. Vi illustrerer inntekt ved å velge bilde etter antall mynter angitt i kollonnen 'Mynter' i datasettet. Dette er gjort i eksemplet under:
for i,r in geodata_merged.iterrows():
#obtaining coordinates:
lat=float(r['Lat'])
lon=float(r['Lon'])
#creating pin for wealth. The height of the stack of coins is determined by the
# picture file f"./img/{int(r['Mynter'])}coins.png":
icon=folium.features.CustomIcon(f"img/coins/{int(r['Mynter'])}coins.png")
folium.Marker([lat,lon+0.01],tooltip=f"Formue:{r['Formue']}",icon=icon).add_to(tromso)
#saving the map as a html file:
tromso.save('./tromso.html')
webbrowser.open('tromso.html')
Lag en interaktiv graf. Du kan bruke dataene fra oppgaven i kap. 10 om du vil
Finn data fra fylker og plott det i et interaktivt kart. Du kan bruke eksemplet under om du vil. Jobben din er å finne hvilke deler av koden som må endres, og hvordan.
import geopandas as gpd
#loading the geographical data
#and converting to a coordinate system that folium understands:
# URL of the CSV file
url = r"https://titlon.uit.no/hht/data/fylker_komprimert.json"
#sometimes this doesn't work on mac
#g = pd.read_csv(url)#reading data
# Local file path to save the downloaded CSV
local_file_path = "fylker_komprimert.json"
# Download the CSV file using requests
response = requests.get(url)
with open(local_file_path, 'wb') as f:
f.write(response.content)
import geopandas as gpd
import numpy as np
import string
#loading the geographical data
#and converting to a coordinate system that folium understands:
geodata = gpd.read_file(local_file_path)
#Need to specify which cooridinate system to use
geodata = geodata[['navn', 'geometry']]
geodata['Fylke'] = geodata['navn'].apply(lambda x: eval(x)[0]['navn'])
#Adding some random data:
geodata['Farge'] = np.random.rand(len(geodata))
s = string.ascii_letters
geodata['Tilfeldig streng'] = geodata['navn'].apply(lambda x: ''.join([s[i] for i in np.random.choice(len(s),8)]))
geodata['Tilfeldig kvm. pris'] = np.array(30000+np.random.rand(len(geodata))*40000, dtype = int)
geodata = geodata[['Fylke','Farge', 'Tilfeldig streng', 'Tilfeldig kvm. pris', 'geometry']]
#Adding cooridnates for symbols that shall be placed in the counties:
coordinates = {
"Rogaland": (59.1000, 5.7333),
"Vestfold og Telemark": (59.2800, 9.1100),
"Nordland": (66.8309, 13.7467),
"Agder": (58.3405, 7.9715),
"Innlandet": (61.5000, 10.0000),
"Møre og Romsdal": (62.7100, 7.3600),
"Vestland": (60.3913, 5.3221),
"Oslo": (59.9139, 10.7522),
"Viken": (60.0, 9.5),
"Trøndelag": (63.4305, 10.3951),
"Troms og Finnmark": (70.2500, 25.0000)
}
geodata['lat'] = geodata['Fylke'].map(lambda fylke: coordinates[fylke][0] if fylke in coordinates else None)
geodata['lon'] = geodata['Fylke'].map(lambda fylke: coordinates[fylke][1] if fylke in coordinates else None)
geodata
import webbrowser
import folium
#Creating the map
norge=folium.Map(location=[65, 14],zoom_start=4.5)
f=folium.Choropleth(
geo_data=geodata.to_json(),
name="choropleth",
columns=["Fylke", "Farge"],
data=geodata,
key_on="feature.properties.Fylke",
fill_color="RdBu",
nan_fill_color='white',
fill_opacity=0.5,
legend_name="Tilfeldig farge"
).add_to(norge)
folium.LayerControl().add_to(norge)
#norge.save('norge.html')
#webbrowser.open('norge.html')
for i,r in geodata.iterrows():
#obtaining coordinates:
lat=float(r['lat'])
lon=float(r['lon'])
#tooltips text:
t=(f"<b>{r['Fylke']}</b><br>"
f"Tilfeldig streng: { r['Tilfeldig streng'] }<br>"
)
#setting the marker:
folium.Marker(
[lat,lon-0.01],
tooltip=t,
icon=folium.Icon(color='blue')
).add_to(norge)
#norge.save('norge.html')
#webbrowser.open('norge.html')
for i,r in geodata.iterrows():
#obtaining coordinates:
lat=float(r['lat'])
lon=float(r['lon'])
#creating pin for wealth. The height of the stack of coins is determined by the
# picture file f"./img/{int(r['Mynter'])}coins.png":
icon=folium.features.CustomIcon(f"img/house.png")
folium.Marker([lat,lon+0.4],tooltip=f"Kvm.pris:{r['Tilfeldig kvm. pris']}",icon=icon).add_to(norge)
norge.save('norge.html')
webbrowser.open('norge.html')