Tilbake
7.4

7.4 DRILL — Ledd 3: analysér — finn maks/beste, tell og gruppér

Kjernen i programmeringsdelen: gå gjennom tabellen/ordboken og finn beste/vinner, tell forekomster, og gruppér — både med eksplisitt løkke og med snarvei.

75 min
12 oppgaver
DRILLLedd 3analysérfinn maks/bestetell
Din fremgang i kapitlet
0 / 12 oppgaver
Forkunnskaper — sist du var her

Analysen bygger på tre grep du allerede har. Her er de, ferdig oppfrisket.

1. Gå gjennom radene i en tabell (kap. 3.2) — og hopp over overskriftsraden:

tabell = [['navn', 'pris'], ['girservice', 690], ['dekkskift', 250]]
for rad in tabell[1:]:
    print(rad[0], rad[1])

Utskrift:

girservice 690
dekkskift 250

2. Kolonnen funnet ved navn (kap. 3.2):

tabell = [['mekaniker', 'type', 'minutter', 'pris']]
print(tabell[0].index('pris'))

Utskrift:

3

3. Telleren med get-default (kap. 3.5):

d = {}
for navn in ['Rune', 'Selma', 'Rune']:
    d[navn] = d.get(navn, 0) + 1
print(d)

Utskrift:

{'Rune': 2, 'Selma': 1}

I tillegg forutsetter kapitlet kap. 7.2 og kap. 7.3, som leverte den ferdige tabellen analysen jobber på.

Løkke 1 — finn den beste raden (~14 min)

«Hvem er raskest?», «hvilket produkt er dyrest?», «hvilket parti leder?» — alle tre er den samme løkka, og den er verdt å kunne skrive uten å tenke.

📜Kodemønster: finn beste

Fem linjer, og de er de samme uansett domene:

beste = None
beste_verdi = None
for rad in tabell[1:]:
    v = rad[k]
    if beste_verdi is None or v > beste_verdi:
        beste = rad
        beste_verdi = v
return beste

Blokken over er mønsteret; den ferdige funksjonen står i eksempel 1.

Fire ting å legge merke til:

1. To variabler, ikke én: raden du skal returnere, og verdien du sammenligner med. Uten den andre må du hente beste[k] i hver runde.
2. None som startverdi på begge. Da vinner den første raden alltid, uansett hvilke tall dataene inneholder.
3. beste_verdi is None or … — venstre side sjekkes først, så v > None blir aldri forsøkt. Python stopper å regne på et or-uttrykk så snart en side er sann.
4. tabell[1:] når tabellen har overskriftsrad. Glemmer du det, sammenligner du et tall med teksten 'pris'.

Skal du finne den minste, snur du > til <. Ingenting annet endres.

`beste_verdi is None` som startbetingelse

Testen som lar den første raden alltid vinne, uten at du må behandle den som et spesialtilfelle.

Alternativene har hver sin svakhet:

- beste_verdi = 0 — ingen rad vinner hvis alle verdiene er negative, og funksjonen returnerer None uten å si fra.
- beste_verdi = tabell[1][k] — krasjer med IndexError på en tabell som bare har overskriftsraden.
- beste_verdi = -1000000 — virker som regel, men er en gjetning om dataene, og den kan slå feil.

None virker i alle tre tilfellene og koster ingenting. Bruk is None og ikke == None — begge fungerer, men is er formen Python-miljøet bruker, og den er raskere å lese.

Returnér raden eller bare feltet

Les oppgaveteksten to ganger her. «Returnerer den dyreste raden» og «returnerer navnet på den dyreste» er to forskjellige bestillinger, og forskjellen er ett tegn i koden:

- return beste gir hele raden: ['Selma', 'ramme', 120, 1450]
- return beste[0] gir navnet: 'Selma'

Sier oppgaven ingenting, er hele raden det tryggeste — den som kaller, kan alltid plukke ut feltet selv, men kan ikke få tilbake det som er kastet.

Skal du gi både raden og verdien, returnerer du to verdier: return beste, beste_verdi, og den som kaller pakker ut med rad, verdi = finn_beste(tabell).

✏️Eksempel 1: det dyreste oppdraget på sykkelverkstedet
«Filen oppdrag.txt har overskriftsraden mekaniker;type;minutter;pris. Skriv dyreste_oppdrag(tabell) som returnerer hele raden for det dyreste oppdraget. Kolonnen skal finnes ved navn.»

Slik ser fila ut:

Innholdet i oppdrag.txt:

mekaniker;type;minutter;pris
Rune;girservice;45;690
Selma;dekkskift;20;250
Rune;bremsejustering;30;390
Selma;girservice;50;690
Tuva;dekkskift;25;250
Selma;ramme;120;1450

Innlesingen er den fra kap. 7.2, og analysen tar tabellen som parameter:

def dyreste_oppdrag(tabell):
    k = tabell[0].index('pris')
    beste = None
    beste_verdi = None
    for rad in tabell[1:]:
        if beste_verdi is None or rad[k] > beste_verdi:
            beste = rad
            beste_verdi = rad[k]
    return beste

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
print(dyreste_oppdrag(tabell))

Utskrift:

['Selma', 'ramme', 120, 1450]

Slik ville sensor sett på det. Deloppgaven er typisk 10 %, og fem ting gir uttelling hver for seg:

GrepHvorfor det teller
tabell[0].index('pris')kolonnen ved navn — fullscore-kravet fra kap. 7.6
tabell[1:]overskriftsraden holdt utenfor sammenligningen
beste_verdi is Nonevirker også på negative verdier og på tom tabell
beste = rad og beste_verdi = rad[k]begge oppdateres i samme if
return bestefunksjonen returnerer, den skriver ikke ut

Flere korrekte løsninger: max(tabell[1:], key=lambda rad: rad[k]) gir det samme på én linje, og den kommer i løkke 2. Begge gir full pott.
Kantfeilen: to oppdrag til 1450 kroner ville gitt den første av dem, fordi > er streng. Vil du ha den siste, bruker du >=. Ingen av delene er galt — men skriv én setning om valget hvis oppgaven ikke sier noe.

📝Oppgave 1

(Innstegsoppgave, sjanger B — kodesporing, altså «hva skriver dette programmet ut?».) Hva skriver programmet ut?

tall = [-8, -3, -19]
beste = 0
for t in tall:
    if t > beste:
        beste = t
print(beste)

📝Oppgave 2
Eksamensnivå, sjanger G
«Skriv korteste_oppdrag(tabell) som returnerer navnet på mekanikeren som har utført oppdraget med færrest minutter. Både mekanikerkolonnen og minuttkolonnen skal finnes ved navn.»

Tabellen har overskriftsrad.

Løkke 2 — snarveien (~12 min)

I dette faget er de innebygde funksjonene ikke juks. De offisielle løsningsforslagene bruker dem fritt, og en løsning med max gir nøyaktig samme uttelling som en med løkke.

Poenget med å lære begge er praktisk: løkka er lettere å skrive riktig for hånd når du er sliten, og snarveien er lettere å skrive riktig når du er trygg.

`max(liste)` og `min(liste)`

Gir det største og minste elementet i lista. På ei liste av tall er det rett fram.

priser = [690, 250, 390, 1450]
print(max(priser), min(priser))

Utskrift:

1450 250

På en 2D-liste sammenligner de radene som lister: første element først, deretter det andre, og så videre. max(tabell) gir altså raden med det alfabetisk siste navnet, ikke den dyreste. Det er nesten aldri det du vil ha — og derfor finnes key.

Begge krasjer med ValueError på ei tom liste.

`key`-parameteren i `max` og `min`
max(liste, key=f) sammenligner ikke elementene selv, men det f gir for hvert element. Elementet som vinner, returneres helt — bare rangeringen bestemmes av nøkkelen.

tabell = [['Rune', 45, 690], ['Selma', 20, 250], ['Selma', 120, 1450]]
print(max(tabell, key=lambda rad: rad[2]))

Utskrift:

['Selma', 120, 1450]

lambda rad: rad[2] er en navnløs funksjon: «gitt en rad, gi meg element nummer 2». Du kan skrive det samme med def og sende funksjonsnavnet inn — begge er riktige, lambdaen er bare kortere.

Merk at kolonnenummeret må være et tall når lambdaen skrives, så finn det med .index(...) først og bruk variabelen.

`sum(liste)` og `len(liste)`
sum legger sammen alle tallene i lista, len teller elementene. Sammen gir de gjennomsnittet.

priser = [690, 250, 390, 1450]
print(sum(priser))
print(sum(priser) / len(priser))

Utskrift:

2780
695.0

sum virker bare på tall — er kolonnen fortsatt tekst, får du en TypeError. Det er feilkode #1, glemt konvertering, som viser seg her i stedet for i innlesingen.

For å bruke dem på en tabellkolonne må du trekke kolonnen ut først: [rad[k] for rad in tabell[1:]].

✏️Eksempel 2: den samme analysen, to former

Løs tre spørsmål om oppdrag.txt både med eksplisitt løkke og med snarvei: dyreste oppdrag, samlet omsetning og gjennomsnittlig varighet.

Dyreste oppdrag. Løkka til venstre i eksempel 1; her er snarveien:

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
k = tabell[0].index('pris')
print(max(tabell[1:], key=lambda rad: rad[k]))

Utskrift:

['Selma', 'ramme', 120, 1450]

Samlet omsetning — begge former:

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
k = tabell[0].index('pris')

total = 0
for rad in tabell[1:]:
    total = total + rad[k]
print(total)

print(sum([rad[k] for rad in tabell[1:]]))

Utskrift:

3720
3720

Gjennomsnittlig varighet — begge former:

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
k = tabell[0].index('minutter')

total = 0
antall = 0
for rad in tabell[1:]:
    total = total + rad[k]
    antall = antall + 1
print(total / antall)

minutter = [rad[k] for rad in tabell[1:]]
print(sum(minutter) / len(minutter))

Utskrift:

48.333333333333336
48.333333333333336

Slik ville sensor sett på det: de tre parene er sidestilt. Begge gir full pott, og det står uttrykkelig i de offisielle løsningsforslagene at flere korrekte løsninger godtas.

Det praktiske rådet er likevel dette: skriv snarveien når du er trygg på den, og løkka når du er i tvil. En lambda med en indeksfeil i er vanskelig å finne på papir; en løkke på fire linjer kan du spore med fingeren.

Kantfeilen som gjelder begge: en tom tabell gir ValueError fra max og ZeroDivisionError fra snittet. Løkkevarianten gir None og en divisjon på null — altså samme problem, bare med et annet navn. Tomtilfellet må håndteres uansett hvilken form du velger, og det gjøres i løkke 6.

📝Oppgave 3
Eksamensnivå, sjanger G

Bruk tabellen fra oppdrag.txt.

a) Skriv én linje som gir raden for det korteste oppdraget, med min og en key.

b) Skriv én linje som gir den samlede tiden i minutter.

c) Forklar med én setning hvorfor min(tabell[1:]) uten key gir feil svar.

📝Oppgave 4
Eksamensnivå, sjanger B

Hva skriver programmet ut? Skriv begge linjene.

rader = [['Nordre', 4], ['Sentrum', 11], ['Ostre', 2]]
print(max(rader))
print(max(rader, key=lambda rad: rad[1]))

Løkke 3 — tell og summér per nøkkel (~14 min)

«Hvor mange oppdrag har hver mekaniker?» og «hvor mye har hver mekaniker omsatt for?» er det samme mønsteret med ett tegn til forskjell.

— naturlig pausepunkt etter denne løkka —

📜Kodemønster: teller og summering med ordbok

Fire linjer, og forskjellen mellom å telle og å summere er hva du legger til:

d = {}
for rad in tabell[1:]:
    d[rad[k]] = d.get(rad[k], 0) + 1
return d

Blokken over er telleren. Bytt + 1 med + rad[j], og du har summeringen i stedet.

Hvorfor d.get(rad[k], 0) og ikke d[rad[k]]: oppslag på en nøkkel som ikke finnes gir KeyError. get gir 0 i stedet, og det er nøyaktig riktig startverdi for både en teller og en sum.

Merk at get ikke setter nøkkelen inn i ordboka — det gjør tilordningen på venstre side. Det er derfor mønsteret må skrives som en tilordning, ikke som et uttrykk.

Telleren `d[k] = d.get(k, 0) + 1`

Bygger {nøkkel: antall} i én gjennomgang av dataene.

d = {}
for navn in ['Rune', 'Selma', 'Rune', 'Selma', 'Tuva', 'Selma']:
    d[navn] = d.get(navn, 0) + 1
print(d)

Utskrift:

{'Rune': 2, 'Selma': 3, 'Tuva': 1}

Legg merke til rekkefølgen i utskriften: nøklene kommer i den rekkefølgen de ble satt inn første gang. Skal svaret være sortert, bruker du sorted(d) når du går gjennom den.

Feilen å unngå er d[navn] += 1 uten get: den krasjer med KeyError første gang navnet dukker opp, fordi det ikke finnes noen gammel verdi å legge 1 til.

Summeringen `d[k] = d.get(k, 0) + verdi`

Samme mønster, men verdien fra raden legges til i stedet for 1. Bygger {nøkkel: sum}.

rader = [['Rune', 690], ['Selma', 250], ['Rune', 390]]
d = {}
for rad in rader:
    d[rad[0]] = d.get(rad[0], 0) + rad[1]
print(d)

Utskrift:

{'Rune': 1080, 'Selma': 250}

Test på oppgaveteksten: «hvor mange» peker på telleren, «hvor mye til sammen» peker på summeringen. Det er verdt å lese setningen to ganger — de to gir helt forskjellige tall, og begge ser riktige ut.

Skal du ha snittet per nøkkel, trenger du begge ordbøkene: summen delt på antallet.

Betinget telling

Skal bare noen av radene telles, står testen inne i løkka, før tellingen:

for rad in tabell[1:]:
    if rad[k_type] == 'girservice':
        d[rad[k_navn]] = d.get(rad[k_navn], 0) + 1

Blokken over er et utdrag fra analysefunksjonen.

Formuleringen i oppgaveteksten er som regel «… som har utført minst ett girservice-oppdrag» eller «… bare oppdrag som tok over en time».

Vær nøye med hva som skal med i ordboka når betingelsen aldri slår til: med denne formen kommer mekanikeren rett og slett ikke med i det hele tatt. Vil du ha alle med, også med 0, må du legge dem inn først med en egen løkke.

`.count(v)` og `v in liste`

To snarveier når du bare skal telle én bestemt verdi, ikke bygge en hel ordbok.

typer = ['girservice', 'dekkskift', 'girservice', 'ramme']
print(typer.count('girservice'))
print('ramme' in typer)
print(typer.count('bremser'))

Utskrift:

2
True
0

.count(v) gir 0 for noe som ikke finnes — den krasjer aldri. in gir True eller False.

Begge virker på ei flat liste, ikke på en 2D-tabell. Skal du telle i en kolonne, trekker du kolonnen ut først: [rad[k] for rad in tabell[1:]].count('girservice').

✏️Eksempel 3: tell og summér på verkstedet
«Skriv oppdrag_per_mekaniker(tabell) som returnerer {mekaniker: antall}, og omsetning_per_mekaniker(tabell) som returnerer {mekaniker: sum pris}. Skriv i tillegg antall_av_type(tabell, type) som teller hvor mange oppdrag som er av en bestemt type.»
Telleren:

def oppdrag_per_mekaniker(tabell):
    k = tabell[0].index('mekaniker')
    d = {}
    for rad in tabell[1:]:
        d[rad[k]] = d.get(rad[k], 0) + 1
    return d

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
print(oppdrag_per_mekaniker(tabell))

Utskrift:

{'Rune': 2, 'Selma': 3, 'Tuva': 1}

Summeringen — ett tegn til forskjell:

def omsetning_per_mekaniker(tabell):
    k_navn = tabell[0].index('mekaniker')
    k_pris = tabell[0].index('pris')
    d = {}
    for rad in tabell[1:]:
        d[rad[k_navn]] = d.get(rad[k_navn], 0) + rad[k_pris]
    return d

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
print(omsetning_per_mekaniker(tabell))

Utskrift:

{'Rune': 1080, 'Selma': 2390, 'Tuva': 250}

Selma har tre oppdrag og 2390 kroner; Rune har to oppdrag og 1080.

Tellingen av én type, begge former:

def antall_av_type(tabell, type_navn):
    k = tabell[0].index('type')
    antall = 0
    for rad in tabell[1:]:
        if rad[k] == type_navn:
            antall = antall + 1
    return antall

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
print(antall_av_type(tabell, 'girservice'))

k = tabell[0].index('type')
print([rad[k] for rad in tabell[1:]].count('girservice'))

Utskrift:

2
2

Slik ville sensor sett på det: at antall_av_type tar typen som parameter er verdt poeng i seg selv — da kan den kalles for alle typene i en senere deloppgave, i stedet for at du skriver én funksjon per type.

De to formene er sidestilt. Begge gir full pott.

Kantfeilen: antall_av_type(tabell, 'Girservice') med stor forbokstav gir 0. Skal søket være ufølsomt for store og små bokstaver, sammenligner du rad[k].lower() == type_navn.lower(). Sier ikke oppgaven noe, skriv én setning om at du antar eksakt treff.

📝Oppgave 5
Eksamensnivå, sjanger G
«Filen avviksmeldinger.txt har overskriftsraden bydel;type;dager. Skriv meldinger_per_bydel(tabell) som returnerer {bydel: antall}, og dager_per_bydel(tabell) som returnerer {bydel: sum dager}

Begge kolonnene skal finnes ved navn.

📝Oppgave 6
Eksamensnivå, sjanger G
«Skriv lange_oppdrag_per_mekaniker(tabell, grense) som returnerer {mekaniker: antall}, men teller bare oppdrag som tok flere minutter enn grense

Test med grensen 30.

Løkke 4 — gruppér (~14 min)

Den tredje analyseformen: i stedet for ett tall per nøkkel skal du ta vare på alle radene som hører til nøkkelen. Da er verdien i ordboka ei liste.

📜Kodemønster: gruppering `{nøkkel: [elementer]}`

Fem linjer, og den tredje er den som glipper oftest:

d = {}
for rad in tabell[1:]:
    if rad[k] not in d:
        d[rad[k]] = []
    d[rad[k]].append(rad)
return d

Blokken over er mønsteret; den ferdige funksjonen står i eksempel 4.

Den indre lista må finnes før du legger noe i den. d[rad[k]].append(...) slår først opp d[rad[k]], og et oppslag på en ukjent nøkkel gir KeyError. Testen if … not in d er derfor ikke pynt, den er nødvendig.

Hva du legger inn, styres av oppgaveteksten: hele raden (rad), ett felt (rad[j]), eller en kombinasjon. «Alle rettene som bruker ingrediensen» betyr rettens navn, ikke hele oppskriften.

`d.setdefault(k, [])`

Gjør de to første linjene i grupperingen på én: slår opp nøkkelen, og setter inn standardverdien først hvis den mangler. Returnerer verdien i begge tilfeller.

d = {}
d.setdefault('Rune', []).append('girservice')
d.setdefault('Rune', []).append('bremser')
d.setdefault('Selma', []).append('dekkskift')
print(d)

Utskrift:

{'Rune': ['girservice', 'bremser'], 'Selma': ['dekkskift']}

Formen er sidestilt med den eksplisitte if … not in d-varianten, og begge gir full pott.

Merk forskjellen fra get: setdefault setter nøkkelen inn i ordboka, get gjør det ikke. Det er nettopp derfor setdefault virker for gruppering og get for telling.

Hva legger du i gruppen

Tre varianter, og oppgaveteksten avgjør hvilken:

- d[nøkkel].append(rad) — hele raden. Nyttig når en senere deloppgave skal regne på gruppen.
- d[nøkkel].append(rad[j]) — ett felt. «Alle typene hver mekaniker har jobbet med.»
- d[nøkkel].append(rad[j]) med duplikatsjekk — når oppgaven sier «uten gjentakelser».

Den siste er et eksplisitt fullscore-krav i flere sett, og den drilles i kap. 7.6. Formen er if rad[j] not in d[nøkkel]: før append.

Les returtypen i oppgaveteksten nøye: {mekaniker: ['girservice', 'ramme']} og {mekaniker: [['Selma', 'ramme', 120, 1450]]} ser like ut i beskrivelsen, men er helt forskjellige strukturer.

✏️Eksempel 4: hvilke oppdragstyper har hver mekaniker
«Skriv typer_per_mekaniker(tabell) som returnerer {mekaniker: [type, type, …]} med alle oppdragstypene mekanikeren har utført. Skriv deretter rader_per_type(tabell) som grupperer hele radene på oppdragstype.»
Gruppering av ett felt:

def typer_per_mekaniker(tabell):
    k_navn = tabell[0].index('mekaniker')
    k_type = tabell[0].index('type')
    d = {}
    for rad in tabell[1:]:
        if rad[k_navn] not in d:
            d[rad[k_navn]] = []
        d[rad[k_navn]].append(rad[k_type])
    return d

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
print(typer_per_mekaniker(tabell))

Utskrift:

{'Rune': ['girservice', 'bremsejustering'], 'Selma': ['dekkskift', 'girservice', 'ramme'], 'Tuva': ['dekkskift']}

Gruppering av hele rader, med setdefault:

def rader_per_type(tabell):
    k = tabell[0].index('type')
    d = {}
    for rad in tabell[1:]:
        d.setdefault(rad[k], []).append(rad)
    return d

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
d = rader_per_type(tabell)
print(d['girservice'])
print(len(d['dekkskift']))

Utskrift:

[['Rune', 'girservice', 45, 690], ['Selma', 'girservice', 50, 690]]
2

Hvorfor gruppering er verdt å kunne: når radene først er samlet per nøkkel, er de neste spørsmålene ett kall hver.

grupper = {'girservice': [['Rune', 'girservice', 45, 690], ['Selma', 'girservice', 50, 690]],
           'dekkskift': [['Selma', 'dekkskift', 20, 250], ['Tuva', 'dekkskift', 25, 250]]}
for type_navn in sorted(grupper):
    minutter = [rad[2] for rad in grupper[type_navn]]
    print(type_navn.ljust(14), len(minutter), sum(minutter) / len(minutter))

Utskrift:

dekkskift      2 22.5
girservice     2 47.5

Slik ville sensor sett på det: den ene linja if rad[k] not in d: d[rad[k]] = [] er verdt poengene sine alene, fordi koden krasjer uten den. Sensor ser etter at du vet at den indre lista må opprettes.

setdefault gir samme uttelling. Velg den du husker riktig — den eksplisitte formen er lettere å feilsøke, den korte er raskere å skrive.

Kantfeilen: d[rad[k]] = rad i stedet for .append(rad) gir ingen feilmelding, men bare den siste raden per nøkkel. Det er den samme fellen som i kap. 7.2 — en ordbok har én verdi per nøkkel.

📝Oppgave 7
Eksamensnivå, sjanger G
«Bruk tabellen fra avviksmeldinger.txt. Skriv bydeler_per_type(tabell) som returnerer {type: [bydel, bydel, …]} — hvilke bydeler som har meldt inn hver type avvik.»
📝Oppgave 8
Eksamensnivå, sjanger G
«Skriv snitt_per_type(tabell) som returnerer {type: gjennomsnittlig varighet} for oppdragene på verkstedet. Bruk rader_per_type fra eksempel 4 i stedet for å gå gjennom tabellen på nytt.»

Rund av til én desimal i utskriften, ikke i ordboka.

Løkke 5 — analysér ordboka (~12 min)

Deloppgavene kommer ofte parvis: først «bygg ordboka», så «finn den beste nøkkelen i den». Den andre er «finn beste» én gang til, bare over nøkler i stedet for rader.

Finn beste nøkkel i en ordbok

Samme mønster som «finn beste rad», men løkka går over nøklene og verdien hentes med oppslag:

d = {'Rune': 1080, 'Selma': 2390, 'Tuva': 250}
beste = None
beste_verdi = None
for navn in d:
    if beste_verdi is None or d[navn] > beste_verdi:
        beste = navn
        beste_verdi = d[navn]
print(beste, beste_verdi)

Utskrift:

Selma 2390

Snarveien er max(d, key=d.get): gå gjennom nøklene, og bruk d.get som nøkkelfunksjon. Den gir nøkkelen, ikke verdien — vil du ha begge, henter du d[beste] etterpå.

for navn in d: går over nøklene, ikke verdiene. for verdi in d.values(): går over verdiene, men da mister du hvilken nøkkel de hørte til.

`sorted(d)` når rekkefølgen skal være fast

Nøklene i en ordbok kommer i den rekkefølgen de ble satt inn. Det er forutsigbart, men sjelden det du vil vise en bruker.

d = {'Selma': 3, 'Rune': 2, 'Tuva': 1}
print(list(d))
print(sorted(d))

Utskrift:

['Selma', 'Rune', 'Tuva']
['Rune', 'Selma', 'Tuva']

sorted(d) gir nøklene som ei sortert liste, uten å endre ordboka. Bruk den i alle utskriftsløkker — da blir svaret ditt det samme hver gang, og det er lettere for sensor å lese.

Skal du sortere på verdien i stedet, er formen sorted(d, key=d.get), og den kommer i kap. 7.5.

✏️Eksempel 5: fra ordbok til svar
«Bruk omsetning_per_mekaniker fra eksempel 3. Skriv beste_mekaniker(tabell) som returnerer navnet på den med høyest omsetning, og skriv ut hele oversikten sortert alfabetisk.»
Analysen av ordboka, med gjenbruk:

def omsetning_per_mekaniker(tabell):
    k_navn = tabell[0].index('mekaniker')
    k_pris = tabell[0].index('pris')
    d = {}
    for rad in tabell[1:]:
        d[rad[k_navn]] = d.get(rad[k_navn], 0) + rad[k_pris]
    return d

def beste_mekaniker(tabell):
    d = omsetning_per_mekaniker(tabell)
    beste = None
    beste_verdi = None
    for navn in d:
        if beste_verdi is None or d[navn] > beste_verdi:
            beste = navn
            beste_verdi = d[navn]
    return beste

tabell = [['mekaniker', 'type', 'minutter', 'pris'],
          ['Rune', 'girservice', 45, 690],
          ['Selma', 'dekkskift', 20, 250],
          ['Rune', 'bremsejustering', 30, 390],
          ['Selma', 'girservice', 50, 690],
          ['Tuva', 'dekkskift', 25, 250],
          ['Selma', 'ramme', 120, 1450]]
print(beste_mekaniker(tabell))

Utskrift:

Selma

Snarveien for det samme:

d = {'Rune': 1080, 'Selma': 2390, 'Tuva': 250}
print(max(d, key=d.get))

Utskrift:

Selma

Oversikten, sortert:

d = {'Rune': 1080, 'Selma': 2390, 'Tuva': 250}
for navn in sorted(d):
    print(navn.ljust(8) + str(d[navn]).rjust(6))

Utskrift:

Rune      1080
Selma     2390
Tuva       250

Slik ville sensor sett på det: beste_mekaniker som kaller omsetning_per_mekaniker er nøyaktig den kjeden deloppgavene er bygget for. En besvarelse som går gjennom tabellen på nytt inne i beste_mekaniker gir riktig svar, men taper gjenbrukspoengene — og den er lengre å skrive.

Antakelsen å skrive ned: ved lik omsetning returnerer denne løsningen den som ble sett først i ordboka. Én setning om det, så er valget dokumentert.

Kantfeilen: en tom ordbok gir None fra løkkevarianten og ValueError fra max. Løkka er altså litt snillere her — men begge trenger et bevisst valg, og det er tema i neste løkke.

📝Oppgave 9
Eksamensnivå, sjanger G
«Du har ordboka {bydel: sum dager} fra oppgave 5. Skriv tregeste_bydel(ordbok) som returnerer navnet på bydelen med flest dager til sammen, og total_dager(ordbok) som returnerer summen over alle bydelene.»

Begge funksjonene skal ta ordboka som parameter, ikke tabellen.

📝Oppgave 10
Eksamensnivå, sjanger G
«Skriv snitt_per_mekaniker(tabell) som returnerer {mekaniker: gjennomsnittspris}. Bygg den av to ordbøker: én med antall oppdrag og én med samlet omsetning, begge gjenbrukt fra eksempel 3.»

Løs den uten å gå gjennom tabellen mer enn de to gangene de to hjelpefunksjonene gjør det.

Løkke 6 — analysen som tåler en tom tabell (~9 min)

Den siste biten før robusthetskapitlet: hva skal en analysefunksjon svare når det ikke finnes noe å analysere?

Tomtilfellet i en analysefunksjon

Fire spørsmål, ett svar hver, og alle fire skal besvares i koden eller i én setning:

SituasjonRimelig returverdi
tom tabell i «finn beste»None
tom tabell i en teller{} — den tomme ordboka, som kommer av seg selv
«ikke funnet» i et søkNone eller -1, avhengig av om du returnerer en verdi eller en posisjon
tom liste i et gjennomsnittNone — en divisjon på null krasjer

Mønsteret med None som startverdi gir de to første gratis: løkka kjører aldri, og funksjonen returnerer None og {} helt av seg selv. Det er en av grunnene til at mønsteret er skrevet slik.
Snarveiene gjør det ikke: max([]) og sum([]) / len([]) krasjer begge. Bruker du dem, må tomtesten skrives eksplisitt.

✏️Eksempel 6: den samme funksjonen, tre datasett

Test dyreste_oppdrag fra eksempel 1 på tre tabeller: en vanlig, en med bare overskriftsrad, og en der alle prisene er negative (en tabell over rabatter).

Vanlig tabell — svaret er den dyreste raden:

def dyreste_oppdrag(tabell):
    k = tabell[0].index('pris')
    beste = None
    beste_verdi = None
    for rad in tabell[1:]:
        if beste_verdi is None or rad[k] > beste_verdi:
            beste = rad
            beste_verdi = rad[k]
    return beste

print(dyreste_oppdrag([['type', 'pris'], ['ramme', 1450], ['dekk', 250]]))

Utskrift:

['ramme', 1450]

Bare overskriftsrad — løkka kjører null ganger, og None faller ut av seg selv:

def dyreste_oppdrag(tabell):
    k = tabell[0].index('pris')
    beste = None
    beste_verdi = None
    for rad in tabell[1:]:
        if beste_verdi is None or rad[k] > beste_verdi:
            beste = rad
            beste_verdi = rad[k]
    return beste

print(dyreste_oppdrag([['type', 'pris']]))

Utskrift:

None

Bare negative verdier — her ville beste_verdi = 0 gitt None, mens None gir riktig svar:

def dyreste_oppdrag(tabell):
    k = tabell[0].index('pris')
    beste = None
    beste_verdi = None
    for rad in tabell[1:]:
        if beste_verdi is None or rad[k] > beste_verdi:
            beste = rad
            beste_verdi = rad[k]
    return beste

print(dyreste_oppdrag([['type', 'pris'], ['rabatt A', -120], ['rabatt B', -45]]))

Utskrift:

['rabatt B', -45]

Snarveien på det tomme tilfellet:

tabell = [['type', 'pris']]
if len(tabell) <= 1:
    print(None)
else:
    print(max(tabell[1:], key=lambda rad: rad[1]))

Utskrift:

None

Uten testen ville max krasjet med ValueError: max() arg is an empty sequence.

Slik ville sensor sett på det: at løkkevarianten håndterer to av de tre tilfellene uten en eneste ekstra linje, er nettopp grunnen til at mønsteret er verdt å lære slik det står. Sensor ser etter None som startverdi og is None i testen — det er to konkrete tegn på at kandidaten kan kantetilfellene.

📝Oppgave 11
Eksamensnivå, sjanger G
«Skriv snitt_pris(tabell) som returnerer gjennomsnittsprisen for oppdragene. Funksjonen skal returnere None hvis tabellen ikke har noen datarader, og prisen skal finnes ved navn.»

Skriv i tillegg én setning om hvorfor None er et bedre svar enn 0 her.

📝Oppgave 12
Eksamensnivå, sjanger G

Bruk tabellen fra avviksmeldinger.txt med kolonnene bydel, type og dager.

a) Skriv verste_type(tabell) som returnerer den avvikstypen som har flest meldinger. Bruk telleren og «finn beste nøkkel».

b) Skriv raskest_behandlet(tabell) som returnerer hele raden for meldingen med færrest dager.

c) Skriv typer_per_bydel(tabell) som returnerer {bydel: [type, …]} uten duplikater.

Alle tre skal finne kolonnene ved navn og tåle en tabell uten datarader.

API- og konstruksjonsliste

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 Norges teknisk-naturvitenskapelige universitet. Dette er ikke offisielt studiemateriell. Les mer.