Tilbake
8.3

8.3 DRILL — Fillesing til nøstet dict med filtrering

Full drill på L-sjangeren: les→bygg nøstet dict→filtrer→skriv, med roterende datainnhold.

75 min
10 oppgaver
DRILLFillesing til nøstet dict med filtrering
Din fremgang i kapitlet
0 / 10 oppgaver

Forkunnskaper — sist du var her

Drillen forutsetter kap. 8.1 (lesing) og kap. 8.2 (skriving og filtrering). Her er de tre grepene du kommer til å bruke i hver eneste oppgave, oppfrisket med kjørt utskrift.

1. Lesing til nøstet ordbok. Strip før split, konverter med én gang, return utenfor løkka.

def les_stasjoner(filnavn):
    data = {}
    with open(filnavn) as infile:
        for line in infile:
            navn, temp, nedbor = line.strip().split(';')
            data[navn] = {'temp': float(temp), 'nedbor': int(nedbor)}
    return data

print(len(les_stasjoner('stasjoner.txt')))

Utskrift:

4

2. Den indre ordboka må opprettes når flere linjer deler samme ytre nøkkel:

data = {}
for stasjon in ['Vest', 'Aust', 'Vest']:
    if stasjon not in data:
        data[stasjon] = {'antall': 0}
    data[stasjon]['antall'] += 1
print(data)

Utskrift:

{'Vest': {'antall': 2}, 'Aust': {'antall': 1}}

3. Filtrering bygger en ny ordbok — aldri ny = data:

def storre_enn(d, grense):
    ny = {}
    for k, v in d.items():
        if v > grense:
            ny[k] = v
    return ny

d = {'Kvitheia': 1180, 'Sandvollen': 760}
print(storre_enn(d, 1000))
print(d)

Utskrift:

{'Kvitheia': 1180}
{'Kvitheia': 1180, 'Sandvollen': 760}

Er noe av dette uklart, les de to forrige kapitlene først — drillen forklarer ikke konstruksjonene på nytt. Feilmeldingene som dukker opp underveis, er katalogisert i kap. 2.2.

📜Løsningsoppskriften for sjanger L — seks steg

Les oppgaveteksten, og gjør så dette, i rekkefølge. Hvert steg svarer til ett spørsmål du besvarer ved å se på fila som er gjengitt i oppgaven.

1. Hva er den ytre nøkkelen? Det som identifiserer raden: stasjonsnavn, ID, land, kandidatnummer. Er det bare én linje per nøkkel, kan hele den indre ordboka settes inn i én tilordning. Har samme nøkkel flere linjer, trenger du et nøkkelnivå mer (typisk dato) og en if nøkkel not in data:-sjekk.
2. Er det en header-linje? Står det kolonnenavn på den første linja, hopper du over den med infile.readline() før løkka — eller bruker den som kilde til feltnavnene. Er det ingen header, skal readline() ikke være der.
3. Hva er separatoren? Semikolon gir .split(';'), komma gir .split(','), mellomrom eller TAB gir bar .split(), faste kolonner gir line[a:b]. Les det ut av fila; ikke gjett.
4. Hvilke felt er tall, og hvilke er tekst? Konverter tallfeltene med int eller float med én gang, inne i løkka, og la tekstfeltene være. Da inneholder strukturen ferdige verdier.
5. Del (b) — skriving: with open(filnavn, 'w') as outfile:, én write per linje, hver linje bygget som f-streng og avsluttet med '\n'. Skriver du en header, må leseren senere hoppe over den.
6. Del (c) — filtrering: ny = {}, løkke over .items(), if-betingelse, innsetting, og return ny utenfor løkka. Argumentet skal være urørt.

Skjelettet er alltid dette, og det er verdt å skrive ned først, før du fyller inn detaljene:

def les(filnavn):
    data = {}
    with open(filnavn) as infile:
        for line in infile:
            felt = line.strip().split(';')
            data[felt[0]] = {}
    return data

print(len(les('stasjoner.txt')))

Utskrift:

4

Skjelettet gir uttelling i seg selv. Fyller du inn de indre feltene feil, mister du ett eller to poeng — glemmer du skjelettet, mister du oppgaven.

Det gjennomarbeidede caset (~20 min)

Under står en komplett, tredelt sjanger-L-oppgave av samme type og vekt som på et virkelig sett, med sensorkommentar ved hvert steg. Les den som en modell: det er nøyaktig denne formen svarene dine skal ha.

Oppgaven (10 poeng). Fila vind.txt har en header-linje og deretter én linje per måling, med feltene stasjon;dato;vind;retning der vindstyrken er i meter per sekund. Samme stasjon har flere målinger.

Innholdet i vind.txt:

stasjon;dato;vind;retning
Kvitheia;2025-01-04;12.4;SV
Kvitheia;2025-01-05;7.1;NV
Ospedalen;2025-01-04;18.9;S
Ospedalen;2025-01-05;21.3;SV
Nordvika;2025-01-04;5.2;N

a) (4 p) Skriv les_vind(filnavn) som returnerer en nøstet ordbok med stasjon som ytre nøkkel, dato som neste, og {'vind': ..., 'retning': ...} innerst.

b) (3 p) Skriv skriv_sterk_vind(data, filnavn, grense) som lagrer alle målingene over en gitt vindstyrke til fil, med header-linja stasjon;dato;vind.

c) (3 p) Skriv snitt_per_stasjon(data) som returnerer en ny ordbok fra stasjonsnavn til gjennomsnittlig vindstyrke, uten å endre argumentet.

Denne fila kan du lage selv på to sekunder — det er dataene løsningen under leser.

✏️Modellbesvarelse del (a): les til nøstet ordbok

Del (a), 4 poeng: les_vind(filnavn) skal returnere en nøstet ordbok med stasjon → dato → {'vind': ..., 'retning': ...}.

Gå gjennom oppskriften. Ytre nøkkel er stasjonen, og samme stasjon har flere linjer — altså trengs et nøkkelnivå til (datoen) og en not in-sjekk. Det er header-linje, så readline() skal med. Separatoren er semikolon. vind er tall, retning er tekst.

def les_vind(filnavn):
    data = {}
    with open(filnavn) as infile:
        infile.readline()
        for line in infile:
            stasjon, dato, vind, retning = line.strip().split(';')
            if stasjon not in data:
                data[stasjon] = {}
            data[stasjon][dato] = {'vind': float(vind), 'retning': retning}
    return data


data = les_vind('vind.txt')
print(data['Kvitheia'])
print(data['Ospedalen']['2025-01-05']['vind'])
print(len(data))

Utskrift:

{'2025-01-04': {'vind': 12.4, 'retning': 'SV'}, '2025-01-05': {'vind': 7.1, 'retning': 'NV'}}
21.3
3

Sensorkommentar, steg for steg:

- data = {} og with open(filnavn) as infile:1 p. Rene rutinelinjer, men de gis poeng, og de er de eneste du kan skrive før du har forstått fila.
- infile.readline() før løkka — 0,5 p. Glemmer du den, får du ValueError: could not convert string to float: 'vind', og du mister dette halve poenget. Resten av oppgaven står.
- line.strip().split(';')1 p. Riktig separator er det halve; .strip() er nødvendig her fordi retning er tekstfeltet til slutt på linja, og uten strip blir verdien 'SV\n'.
- if stasjon not in data: data[stasjon] = {}1 p. Dette er kjernen. Skriver du i stedet data[stasjon][dato] = ... direkte, krasjer programmet med KeyError på første linje.
- Innsetting på tre nivåer med float(vind) og retning som tekst — 0,5 p.
- return data utenfor løkka — inngår i det første poenget, men står return inne i løkka, mister du hele strukturpoenget: funksjonen returnerer etter første måling.

Hva som er godt nok: hadde du skrevet data[stasjon][dato] = float(vind) og droppet retningen, ville du fått 3 av 4 — strukturen er riktig, ett felt er glemt.

✏️Modellbesvarelse del (b) og (c): skriv til fil og aggreger

Del (b), 3 poeng: skriv_sterk_vind(data, filnavn, grense) skal lagre målingene over grense til fil med header. Del (c), 3 poeng: snitt_per_stasjon(data) skal returnere en ny ordbok med gjennomsnittlig vindstyrke per stasjon.

Del (b). To løkker, fordi strukturen har to nivåer: en over stasjonene og en over datoene under hver stasjon. Betingelsen står innerst, der målingen er.

def skriv_sterk_vind(data, filnavn, grense):
    with open(filnavn, 'w') as outfile:
        outfile.write('stasjon;dato;vind\n')
        for stasjon, dager in data.items():
            for dato, verdier in dager.items():
                if verdier['vind'] > grense:
                    outfile.write(f'{stasjon};{dato};{verdier["vind"]:.1f}\n')

Del (c). Aggregeringsmønsteret: for hver ytre nøkkel, summer over de indre og del på antall. len(dager) er antall målinger for den stasjonen.

def snitt_per_stasjon(data):
    ny = {}
    for stasjon, dager in data.items():
        total = 0
        for dato in dager:
            total += dager[dato]['vind']
        ny[stasjon] = total / len(dager)
    return ny

Kjørt sammen med les_vind fra del (a), med grensen 10,0 m/s:

data = les_vind('vind.txt')
skriv_sterk_vind(data, 'sterk.txt', 10.0)
with open('sterk.txt') as infile:
    for line in infile:
        print(line, end='')
snitt = snitt_per_stasjon(data)
for stasjon in snitt:
    print(f'{stasjon:11s}{snitt[stasjon]:6.2f}')

Utskrift:

stasjon;dato;vind
Kvitheia;2025-01-04;12.4
Ospedalen;2025-01-04;18.9
Ospedalen;2025-01-05;21.3
Kvitheia     9.75
Ospedalen   20.10
Nordvika     5.20

Sensorkommentar del (b): with open(filnavn, 'w') (1 p) · header skrevet én gang før løkkene (0,5 p) · to nøstede løkker som når ned til målingene (1 p) · f-streng som slutter på '\n' (0,5 p). Glemmer du '\n', ligger alle målingene på én linje — det koster det halve poenget, ikke mer.

Sensorkommentar del (c): ny = {} og return ny utenfor løkka (1 p) · summeringsløkke over de indre nøklene (1 p) · deling på len(dager), altså antall målinger for den stasjonen (1 p). Den vanligste feilen her er å dele på len(data), antall stasjoner — det gir tre i stedet for to, og koster ett poeng.

Merk hva som ikke kreves: ingen main-blokk, ingen brukerinput, ingen kommentarer utover eventuelle antakelser. Tolv linjer kode for ti poeng.

Oppgaver: samme mønster, ny innpakning (~30 min for oppgave 1–5)

— naturlig pausepunkt —

Alle oppgavene under er på eksamensnivå. Datainnholdet roterer bevisst, og separatoren og header-linja varierer. Bruk oppskriftens seks spørsmål på hver oppgave før du skriver en linje kode. Filene kan du lage selv i en tekstredigerer — innholdet står i oppgaven.

📝Oppgave 1
Eksamensnivå, sjanger L

Et folkeregisterutdrag ligger i personer.txt med header-linje og semikolon som separator.

Innholdet i personer.txt:

id;navn;postnr;fodselsaar
1042;Ingrid Hauge;5003;1998
1043;Mathias Lind;7014;1985
1044;Sofie Berge;5003;2001
1045;Jonas Rud;9012;1972

a) Skriv les_personer(filnavn) som returnerer en nøstet ordbok med id-en som heltallsnøkkel og en indre ordbok med feltnavnene fra header-linja. Navnet skal være tekst, postnummer og fødselsår heltall.

b) Skriv ut den indre ordboka for id 1044, navnet og fødselsåret til 1043, og antall personer.

📝Oppgave 2
Eksamensnivå, sjanger L

Fila land.txt er kommaseparert og har ingen header. Feltene er land,befolkning,bnp_per_innbygger.

Innholdet i land.txt:

Alveria,4820000,52100
Brenland,1290000,38400
Corvia,17650000,9800
Dunmark,610000,71200

a) Skriv les_land(filnavn) som returnerer en nøstet ordbok fra landnavn til {'folketall': ..., 'bnp': ...} med heltall som verdier.

b) Skriv rikeste(data) som returnerer navnet på landet med høyest BNP per innbygger.

c) Vis den indre ordboka for Corvia, resultatet av rikeste og antall land.

📝Oppgave 3

(Midtveisnivå, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) Programmet leser land.txt fra forrige oppgave. Hva skriver det ut? Skriv utskriften nøyaktig, med mellomrom og linjeskift slik terminalen viser den.

data = {}
with open('land.txt') as infile:
    for line in infile:
        felt = line.strip().split(',')
        data[felt[0]] = int(felt[1])

n = 0
for navn in data:
    if data[navn] > 1000000:
        n += 1
        print(navn, end=' ')
print()
print(n, len(data))

📝Oppgave 4
Eksamensnivå, sjanger L

Bruk vind.txt og les_vind fra det gjennomarbeidede caset.

a) Skriv maks_per_stasjon(data) som returnerer en ny ordbok fra stasjonsnavn til den høyeste vindstyrken som er målt på stasjonen. Argumentet skal ikke endres.

b) Vis resultatet for hele fila.

📝Oppgave 5
Eksamensnivå, sjanger L

Gitt en ordbok fra person-id til {'navn': ..., 'fodselsaar': ...}, som den les_personer fra oppgave 1 lager.

a) Skriv skriv_unge(data, filnavn, aar) som lagrer alle personer født etter aar til fil, med header-linja id;navn og deretter én semikolonseparert linje per person.

b) Kall funksjonen med aar = 1990 på Ingrid Hauge (id 1042, født 1998), Mathias Lind (1043, 1985) og Sofie Berge (1044, 2001), og les fila tilbake.

Oppgave 6–10: tredelte oppgaver og de vanskeligere variantene (~25 min)

— naturlig pausepunkt —

De neste fem oppgavene ligner mest på det du møter til slutt i et sett: flere deler som bygger på hverandre, én «finn feilen», og én kolonnefil. Har du de fem første inne, er dette samme mønster med flere nivåer.

📝Oppgave 6
Eksamensnivå, sjanger L

Fila karakterer.txt har header-linje og feltene kandidat;emne;karakter;poeng. Samme kandidat kan ha flere emner.

Innholdet i karakterer.txt:

kandidat;emne;karakter;poeng
K1201;IN1900;B;71
K1202;IN1900;D;48
K1201;MAT1100;C;60
K1203;IN1900;A;88

a) Skriv les_karakterer(filnavn) som returnerer en nøstet ordbok med kandidatnummer som ytre nøkkel, emnekode som neste, og {'karakter': ..., 'poeng': ...} innerst, der poengsummen er et heltall.

b) Skriv bestatt(data, grense) som returnerer en ny ordbok med bare de resultatene som har minst grense poeng. Kandidater uten kvalifiserte resultater skal ikke være med i det hele tatt.

c) Skriv skriv(data, filnavn) som lagrer en slik struktur til fil med linjer kandidat;emne;karakter, og kjør hele kjeden med grensen 60.

📝Oppgave 7
Eksamensnivå, sjanger O

Programmet skal lese vind.txt fra caset til en ordbok med stasjon og dato som nøkler og vindstyrken som verdi. Det krasjer på første linje.

data = {}
with open('vind.txt') as infile:
    for line in infile:
        felt = line.split()
        data[felt[0]][felt[1]] = float(felt[2])
print(data)

a) Hvilken feilmelding kommer, og hvorfor?

b) Programmet har tre feil. Nevn alle tre, og skriv den rettede koden.

📝Oppgave 8

(Bør kjenne til, sjanger L — fast-bredde-felt.) Fila kolonner.txt har ingen separator; feltene står i faste kolonner. Navnet er i posisjon 0–11, nedbøren i 12–17 og temperaturen i 18–24. Hver linje er 25 tegn.

Innholdet i kolonner.txt:

Kvitheia      1180    7.9
Sandvollen     760    5.4
Ospedalen     1420    8.6

a) Skriv koden som leser fila til en nøstet ordbok fra stasjonsnavn til {'nedbor': ..., 'temp': ...}, med heltall og flyttall.

b) Skriv ut den indre ordboka for Sandvollen og summen av nedbøren for Ospedalen og Kvitheia.

📝Oppgave 9
Eksamensnivå, sjanger L

Bruk personer.txt fra oppgave 1.

a) Skriv per_postnr(filnavn) som returnerer en ordbok fra postnummer (som tekst) til en liste med navnene på personene som bor der.

b) Vis hele ordboka og antall personer i postnummer 5003.

📝Oppgave 10
Eksamensnivå, sjanger L

Fila avlop.txt har header-linje og feltene stasjon;dato;ph;temp. Samme stasjon har flere prøver.

Innholdet i avlop.txt:

stasjon;dato;ph;temp
Vest;2025-05-02;7.4;11.2
Vest;2025-05-03;6.9;12.0
Aust;2025-05-02;8.1;10.4

a) Skriv les_avlop(filnavn) som returnerer en nøstet ordbok stasjon → dato → {'ph': ..., 'temp': ...} med flyttall.

b) Skriv sure_dogn(data, grense) som returnerer en ny struktur med bare prøvene der pH er lavere enn grense, uten å endre argumentet.

c) Skriv skriv_snitt(data, filnavn) som lagrer gjennomsnittlig pH per stasjon til fil, med header-linja stasjon;snitt_ph og to desimaler.

d) Kjør kjeden med grensen 7,0 og vis resultatene.

API- og konstruksjonsliste

Begrepsbank

Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.

Sjanger L

Fillesing til nøstet ordbok: oppgaven der du leser en tekstfil linje for linje og bygger en datastruktur. Den forekommer i 8 av 8 gjennomgåtte avsluttende sett og teller 8–12 poeng, oftest delt i tre: (a) les til ordbok, (b) skriv struktur til fil, (c) filtrer og returner en ny ordbok. Innpakningen roterer år for år, programmeringsøvelsen er den samme.

Steg 1 — velg den ytre nøkkelen

Det feltet som identifiserer raden: stasjonsnavn, ID, land, kandidatnummer. Har hver nøkkel én linje, settes hele den indre ordboka inn i én tilordning. Har samme nøkkel flere linjer, trengs et nøkkelnivå til (typisk dato) og en if nøkkel not in data:-sjekk. Dette valget avgjør hele resten av koden.

Steg 2 — header eller ikke

Er den første linja kolonnenavn, hoppes den over med infile.readline() før løkka — eller den brukes som kilde til feltnavnene med readline().strip().split(sep)[1:]. Er det ingen header, skal readline() ikke stå der; ellers mister du den første dataraden i stillhet.

Steg 3 — velg separator

Semikolon gir .split(';'), komma gir .split(','), mellomrom eller TAB gir bar .split(), faste kolonner gir line[a:b]. Separatoren leses ut av fila slik den er gjengitt i oppgaveteksten. Feil valg gir IndexError med én gang, fordi felt[1] da ikke finnes.

Steg 4 — typekonvertering

Konverter tallfeltene med int eller float med én gang, inne i innlesingsløkka, og la tekstfeltene være tekst. Da inneholder strukturen ferdige verdier. Glemt konvertering gir ingen feilmelding, bare gale resultater: + på to tallstrenger skjøter dem sammen.

Steg 5 — skriving til fil
with open(filnavn, 'w') as outfile:, header én gang før løkka, deretter én write per linje der linja er bygget som f-streng og slutter på '\n'. write legger aldri til linjeskift, og godtar bare strenger. Skriver du header, må leseren senere hoppe over den.
Steg 6 — filtrering uten mutasjon
ny = {} → løkke over .items()if-betingelse → innsetting → return ny utenfor løkka. I en nøstet struktur står if ytre not in ny: etter betingelsen, slik at ytre nøkler uten treff ikke havner i resultatet med tom indre ordbok. Skriv aldri ny = data.
Aggregeringsmønsteret

Ett tall per ytre nøkkel, regnet ut fra det indre nivået: total = 0 øverst i den ytre løkka, total += … i den indre, og ny[ytre] = total / len(indre) etter den indre løkka. To feil koster poeng: å nullstille total utenfor den ytre løkka (verdier lekker mellom nøklene), og å dele på len(data) i stedet for len(indre).

«Beste så langt»-mønsteret

Å finne største eller minste verdi uten max: en variabel som holder den beste kandidaten, oppdatert ved hver sammenligning. beste = '' betyr «ingen kandidat ennå» og gjør at den første oppføringen alltid vinner første runde. Nullstillingen hører øverst i den ytre løkka når resultatet skal beregnes per nøkkel.

Grupperingsmønsteret

Når flere rader skal samles under samme nøkkel som en liste: if nøkkel not in data: data[nøkkel] = [] og deretter data[nøkkel].append(verdi). Tilordning i stedet for .append overskriver, så bare den siste raden overlever. Mønsteret gjenkjennes på oppgaveformuleringen «alle X som hører til Y».

Repetisjon: oppskriften på ett kort

Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.

Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.