Referanse fra merknad og programområde tilbake til deres tidligere navn - Utdanningsdirektoratet/Grep_SPARQL GitHub Wiki
Bakgrunn
I Grep har vi en hovedregel om at hvis et element skifter navn, så endrer vi også koden. Med andre ord – vi oppretter da et nytt element med det nye navnet (og evt endret innhold), og setter det gamle til status "utgått" med en erstatter/erstattes-av-referanse. Men det finnes unntak fra denne regelen. Det forekommer nemlig at enkelte elementer kan skifte navn ("tittel") men beholde koden. Da opprettes det et element av typen "tidligere_navn" (se full liste på https://data.udir.no/kl06/v201906/tidligere-navn) som inneholder det gamle navnet. Dette kan kun forekomme på følgende typer elementer:
- Utdanningsprogram (3 forekomster. Disse har referanse tilbake til tidl. navn)
- Programområde (1 forekomst)
- Merknad (8 forekomster)
Eksempler:
- HS (Utdanningsprogram). Tidl. navn: tidligere-navn/tidligere_navn_1
- HSHUD3---- (Programområde). Tidl. navn: tidligere_navn_4
- VMM15 (Merknad). Tidl. navn: tidligere_navn_5
Så til selve problemstillingen
Det er kun utdanningsprogram som har referanse tilbake til det tidligere navnet i selve elementet. For de andre typene (programområde og merknad) må du slå opp de ulike tidligere_navn-elementene og se hva de er tidligere navn for og hva det tidligere navnet var. Du må med andre ord spørre baklengs, og det er det vi tenker å vise i denne saken.
Løsning
- Se Python-eksempel nederst på siden
La oss ta utgangspunkt i et eksempel der elementet vi skal se på er gitt, nemlig merknaden VMM15 i json-visning eller VMM15 i SPARQL-visning. Det vi ønsker i første omgang er å se om VMM15 forekommer i listen over tidligere navn, altså hva de er tidligere navn for. Da kan vi kjøre følgende spørring (for tips om hvordan du kjører SPARQL-spørringer, se denne veiledningen):
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
PREFIX d: <http://psi.udir.no/kl06/>
select * where {
d:VMM15 ^u:tidligere-navn-for ?tidligereNavn .
}
Da får vi dette resultatet:
Først en kjapp forklaring på denne spørringen: Alle SPARQL-spørringer består av tre ledd: Subjekt, predikat, objekt (og i den rekkefølgen). I spørringen over er "d:VMM15" subjekt, "^tidligere-navn-for" er predikat, og variabelen "?tidligereNavn" er objektet vi er på jakt etter.
Caret-tegnat ("^") foran predikatet i spørringen er en indikator for at vi spør reversert (baklengs). Det vi egentlig gjør, er å spørre: Hvilke subjekter i databasen har prediktatet "u:tidligere-navn" der "VMM15" er objekt. Det er bare det at vi har satt "VMM15" på objekt-plassen, for det er den vi kjenner.
Vi kan spørre riktig vei slik:
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
PREFIX d: <http://psi.udir.no/kl06/>
select * where {
?tidligereNavn u:tidligere-navn-for d:VMM15 .
}
og vi får det samme svaret for variabelen ?tidligereNavn som i forrige spørring. Med andre ord – "tidligere_navn_5" er "u:tidligere-navn-for" "d:VMM15".
Grunnen til at vi ønsker å sette d:VMM15 på subjekt-plassen i spørringen, er at det er denne som i første omgang er kjent, og den inngår kanskje i en større spørring der "d:VMM15" er subjekt. Det er også nødvendig å spørre baklengs hvis "d:VMM15" bare er en forekomst blant flere i variabelen "?s" (for subjekt) i spørringen nedenfor:
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
PREFIX d: <http://psi.udir.no/kl06/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
select * where {
?s ^u:tidligere-navn-for ?o ;
rdf:type u:merknad.
}
Som gir dette resultatet, der d:VMM15 er med i mengden av ?s:
Med andre ord – det vi spør om, er følgende: Hvilke ?s av typen u:merknad har noen ?o som igjen er tidligere-navn for ?s.
Når gjaldt det gamle navnet?
Et typisk eksempel på bruk av denne informasjonen kan være å passe på å få den varianten av tittelen på f.eks. en vitnemålsmerknad som gjaldt på et gitt tidspunkt. Denne tidsinformasjonen ligger i tidligere_navn-elementet, og la oss først se det i json:
Her ser vi at "tidligere_navn_5" er "tidligere-navn-for" "VMM15", og at denne koblingen (mellom tidl.navn og merknad) er gyldig fra 1.8.2006 til 31.12.2024.
Vi ser også av json-represenatasjonen at dato-informasjonen under egenskapen "gyldighet" er et objekt i objekt, noe som fører til såkalte blanke noder i SPARQL (se mer om bNoder her). I praksis blir det bare litt ekstra "lirking" for å få tak i objekt i objekt-informasjonen:
Vi har forflatet dette JSON-objektet:
"gyldighet" {
"gyldig-fra": "2006-08-01T00:00:00"
},
{
"gyldig-til": "2024-12-31T00:00:00"
}
til at det ser slik ut ut i graf-databasen:
Subjekt | Predikat | Objekt |
---|---|---|
d:tidligere_navn_5 | u:gyldighet-tidligere-navn-for-VMM15 | _:genid-8cd0b7bdb8c548ed9b6800e9dc613d422528108-b0 |
Gyldighetsinformasjonen ligger skjult under den blanke noden i Objekt, og siden en blank node ikke er en URI vi kan slå opp direkte, kan vi likevel se hva som er bak den ved å spørre skik:
PREFIX d: <http://psi.udir.no/kl06/>
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
select * where {
d:tidligere_navn_5 u:gyldighet-tidligere-navn-for-VMM15 ?o .
?o ?p2 ?o2
}
Som gir:
Men for alle praktiske formål, er det i utgangspunktet tungvint at predikatet har en gitt VMM-kode (u:gyldighet-tidligere-navn-for-VMM15). Vi gjør derfor moen knep for å generalisere:
PREFIX d: <http://psi.udir.no/kl06/>
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
select * where {
d:tidligere_navn_5 ?p ?o ;
u:tidligere-navn-for ?tidlNavnFor .
?tidlNavnFor u:kode ?kode .
FILTER regex(str(?p), ?kode)
?o ?p2 ?o2
}
Det vi gjør her er, i stedet for å skrive u:gyldighet-tidligere-navn-for-VMM15 i predikatet for d:tidligere_navn_5, bare skrive en variabel ?p (for predikat). Så lirker vi fram koden til hva det er tidligere navn for, og legger det i et regex-filter for ?p. Vi vil kun ha ?p som inneholder ?kode (i vårt eksempel "VMM15"). Dermed representerer ?p det som i den forrige spørringen var u:gyldighet-tidligere-navn-for-VMM15.
Ved hjelp av denne metoden kan vi hente fram all gyldighetsinformasjon for alle tidligere titler (og vi forholder oss for enkelhets skyld til merknader):
PREFIX d: <http://psi.udir.no/kl06/>
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
select * where {
?s a u:tidligere_navn ;
?p ?o ;
u:tidligere-navn-for ?tidlNavnFor .
?tidlNavnFor a u:merknad;
u:kode ?kode .
FILTER regex(str(?p), ?kode)
?o ?p2 ?o2
}
Og vi får dette som resultat (utsnitt):
Eller slik for å få hver ?s på én linje (vi skiller ut gyldig-fra/til til hver sin variabel):
PREFIX d: <http://psi.udir.no/kl06/>
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
select ?s ?tidlNavnFor ?gyldigFra ?gyldigTil where {
?s a u:tidligere_navn ;
?p ?o ;
u:tidligere-navn-for ?tidlNavnFor .
?tidlNavnFor a u:merknad;
u:kode ?kode .
FILTER regex(str(?p), ?kode)
?o u:gyldig-fra ?gyldigFra ;
u:gyldig-til ?gyldigTil .
}
Som gir:
Eksempel med OPTIONAL
I eksempelet nedenfor lister vi opp alle merknader, og vi tar med tidligere navn med gyldighetsinformasjon der dette finnes. Da bruker vi operatoren OPTIONAL i SPARQL:
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
PREFIX d: <http://psi.udir.no/kl06/>
select ?s ?tittel ?tidlNavn ?tidlNavnTittel
(SUBSTR(STR(?TidlNavnGF), 1, 10) AS ?TidlNavnGyldigFra)
(SUBSTR(STR(?TidlNavnGT), 1, 10) AS ?TidlNavnGyldigTil)
where {
?s a u:merknad ;
u:tittel ?tittel .
# d:FAM32 u:tittel ?tittel . # Evt. bytte de to foregående linjene med denne hvis merknaden er gitt
OPTIONAL {
?s ^u:tidligere-navn-for ?tidlNavn ;
u:kode ?kode .
?tidlNavn u:tittel ?tidlNavnTittel ;
?p ?o . # se regex-filter nedenfor (kode skal være med i ?p)
?o u:gyldig-fra ?TidlNavnGF ;
u:gyldig-til ?TidlNavnGT .
FILTER regex(str(?p), ?kode)
FILTER(LANG(?tidlNavnTittel)="default")
}
FILTER(LANG(?tittel)="default")
}
som gir dette resultatet (utsnitt):
Med andre ord – noen har og andre har ikke tidligere navn-informasjon
Løsning ved hjelp av Python
Nedenfor følger et eksempel på et scenario der vi ønsker å liste opp alle merknader men kun returnere titler slik de var på en gitt dato (01.01.2007). Jeg som skriver dette har begrenset kunnskap om Python, men jeg ba CharGPT om hjelp til å lage et script som gjør følgende:
Henter data: Scriptet henter data fra to REST-endepunkter: ett for "merknader" og ett for "tidligere navn".
Oppretter oppslag: For hver merknad lages et oppslagsobjekt som inneholder:
- kode: En unik identifikator.
- tittel: Standardtittelen som hentes fra merknaden.
- flag_gammelt_navn: Et flagg som settes til False som standard.
- Oppdaterer basert på gyldighetsperiode:
Scriptet går gjennom alle tidligere navn og:
- henter koden fra feltet url-data i objektet tidligere-navn-for
- sjekker om gyldighetsperioden (fra "gyldig-fra" til "gyldig-til") inkluderer datoen 1. januar 2007
- dersom datoen ligger innenfor gyldighetsperioden, oppdateres merknadens tittel med tittelen fra tidligere navn, og flagget settes til True.
- returnerer resultatet
Til slutt konverteres alle dataene til JSON-format og skrives ut. Dette gjør det enkelt å se hvilke merknader som har fått oppdatert tittel basert på tidligere navn.
Med andre ord, scriptet sjekker om et tidligere navn var gyldig på en bestemt dato (1. januar 2007) og oppdaterer tittelen på den tilhørende merknaden dersom det er tilfellet, før det returnerer alt i JSON-format.
import requests
import datetime
import json
BASE_URL = "https://data.udir.no/kl06/v201906"
MERKNADER_URL = f"{BASE_URL}/merknader"
TIDLIGERE_NAVN_URL = f"{BASE_URL}/tidligere-navn"
# Datoen vi vil sjekke gyldighet mot
CHECK_DATE = datetime.date(2007, 1, 1)
def get_json_list(url):
"""Henter JSON fra gitt URL og antar at svaret er en liste."""
resp = requests.get(url)
resp.raise_for_status()
return resp.json()
def get_default_title(json_obj):
"""
Henter ut tittel fra et JSON-objekt.
Dersom 'tittel' er en streng, returneres den direkte,
ellers sjekkes en liste etter et objekt med spraak 'default'.
"""
tittel = json_obj.get('tittel')
if isinstance(tittel, str):
return tittel
elif isinstance(tittel, list):
for t in tittel:
if t.get('spraak') == 'default':
return t.get('verdi')
return None
def is_valid_for_date(gyldigFra, gyldigTil, date_to_check):
"""Returnerer True hvis date_to_check er innenfor [gyldigFra, gyldigTil]."""
from_str = gyldigFra.replace('Z', '')
to_str = gyldigTil.replace('Z', '')
from_date = datetime.datetime.fromisoformat(from_str).date()
to_date = datetime.datetime.fromisoformat(to_str).date()
return from_date <= date_to_check <= to_date
def extract_code_from_url(url):
"""Ekstraherer koden fra en URL, dvs. den siste delen etter skråstrek."""
return url.rstrip('/').split('/')[-1]
def main():
# 1. Hent alle merknader
merknader_list = get_json_list(MERKNADER_URL)
# 2. Hent alle "tidligere navn"
tidligere_navn_list = get_json_list(TIDLIGERE_NAVN_URL)
# Lag et oppslagsverk for merknader (key = kode) med standardtittel
merknader_dict = {}
for merknad in merknader_list:
kode = merknad.get('kode')
merknader_dict[kode] = {
'kode': kode,
'title': get_default_title(merknad), # standard "ny" tittel
'flag_gammelt_navn': False # settes til True om vi finner et gyldig "tidligere navn"
}
# 3. Gå gjennom alle "tidligere navn" og sjekk gyldighet
for tn in tidligere_navn_list:
ref_obj = tn.get('tidligere-navn-for')
if not isinstance(ref_obj, dict):
continue
# Hent koden fra "url-data"
rest_url = ref_obj.get('url-data')
if not rest_url:
continue
ref_kode = extract_code_from_url(rest_url)
# Hent gyldighetsdata fra objektet "gyldighet"
gyldighet = ref_obj.get('gyldighet', {})
gyldigFra = gyldighet.get('gyldig-fra')
gyldigTil = gyldighet.get('gyldig-til')
# Sjekk om 1.1.2007 faller innenfor gyldighetsintervallet
if gyldigFra and gyldigTil and is_valid_for_date(gyldigFra, gyldigTil, CHECK_DATE):
if ref_kode in merknader_dict:
old_title = get_default_title(ref_obj)
if old_title:
merknader_dict[ref_kode]['title'] = old_title
merknader_dict[ref_kode]['flag_gammelt_navn'] = True
# 4. Returner resultatet som JSON
# Her konverterer vi dictionary til en liste med objekter før vi dumper det som JSON
result = list(merknader_dict.values())
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
Kjøring av scriptet gir et resultat som ser slik ut (utsnitt):