Lab9

Grunnleggende oppgaver (Nivå E):

Nivå A-D:

Nivå E
Terningkast

I filen, dice_throw.py skal du skrive to funksjoner:

  1. Skriv en funksjon throw_n_2(n) som har som parameter et heltall n og simulerer \(n\) kast av to terninger (bruk modulen random fra python sitt standardbibliotek) og legger til summen av de to i en liste. Returner den listen (som nå inneholder \(n\) elementer med tall mellom 2 og 12).

  2. Skriv så en funksjon print_histo(throws) som tar inn en slik liste throws og printer ut et enkelt histogram hvor ‘*’ står for 1% (runde av til nærmeste heltall når prosentandelen av et utfall er et blandet tall). Bruk collections.Counter fra modulen collections i funksjonen. Et kall til print_histo(throw_n_2(100)) skal for eksempel se noenlunde slik ut (Pass på formateringen av tallene og mellomrom; bruk tab, “\t”, mellom tallene og stjernene):

2 	***
3 	****
4 	******
5 	*****
6 	***************
7 	**********************
8 	********************
9 	*************
10 	********
11 	**
12 	**

Ikke bruk random.seed() i denne oppgaven slik at autotestene kan fungere rett.

Nivå E
Fredag 13.

I filen friday_13th.py skriv en funksjon first_friday_13th_after(date) som har som parameter et datetime -objekt. Funksjonenen returnerer et nytt datetime-objekt, som befinner seg på første fredag den 13. etter den gitte datoen. Dersom input-datoen selv er på en fredag den 13., er det neste datoen som skal returneres.

Denne oppgaven skal løses ved hjelp av datetime -modulen fra python sitt standardbibliotek.

Test koden din ved å legge til disse linjene nederst i filen:

print("Tester first_friday_13th_after... ", end="")
# Test 1
result = first_friday_13th_after(datetime(2022, 10, 24))
assert((2023, 1, 13) == (result.year, result.month, result.day))
# Test 2
result = first_friday_13th_after(datetime(2023, 1, 13))
assert((2023, 10, 13) == (result.year, result.month, result.day))
# Test 3
result = first_friday_13th_after(datetime(1950, 1, 1))
assert((1950, 1, 13) == (result.year, result.month, result.day))
print("OK")

  • Se eksempel på bruk av datetime i kursnotater om standardbibliotektet og se eventuelt også den offisielle dokumentasjonen for datetime.
  • Kan være lurt å ha en hjelpefunksjon som sjekker om et gitt tidspunkt er på en fredag den trettende.
  • Forsøk å øke datoen med én dag helt til du treffer en dag som er på en fredag den trettende.

Nivå E
Egen modul

Til denne oppgaven oppfordrer vi deg til å løse den uten å bruke en python-interpreter. Her skal du løse oppgaven for hånd i for eksempel en teksteditor som notepad der du ikke kan kjøre programmet ditt. Hensikten med dette er å forberede deg til eksamen hvor du ikke vil kunne kjøre koden din.

Denne oppgaven vil ikke bli rettet på CodeGrade, men vil i stedet bli rettet med skriftlige refleksjoner sånn som i lab 5.

Del A

I denne oppgaven skal vi lage vår egen modul data_normalization i filen data_normalization.py, samt et eksempel-program i filen data_normalization_example.py. La innholdet i filen data_normalization_example.py være nøyaktig:

import data_normalization

data = [1, 2, 3, 4, 5, 6, 7, 8, 9]


# Normalizing the data to [0, 1] range
a = data_normalization.norm_0_1(data)

# Normalizing the data to [-1, 1] range
b = data_normalization.norm_neg1_1(data)

assert a == [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]

assert b == [-1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0]

Funksjonen norm_0_1(x) normaliserer verdiene i listen x til å ligge i området mellom 0 og 1. Funksjonen først finner minimums- og maksimumsverdien i listen, og deretter normaliserer den hvert element i listen til å være i området mellom 0 og 1. Dette gjøres ved å trekke fra minimumsverdien, og deretter dividere med differansen mellom maksimums- og minimumsverdien.

$$ {x_i - \min(x) \over \max(x) - \min(x)} $$

Funksjonen norm_neg1_1(x) normaliserer verdiene i listen x til å ligge i området mellom -1 og 1. Funksjonen finner også minimums- og maksimumsverdien i listen, men deretter normaliserer den hvert element i listen til å være i området mellom -1 og 1. Dette gjøres ved å trekke fra minimumsverdien, multiplisere med 2 og deretter dividere med differansen mellom maksimums- og minimumsverdien. Til slutt trekkes det fra 1, slik at verdiene ligger i området mellom -1 og 1.

$$ {2(x_i - \min(x)) \over \max(x) - \min(x)} - 1$$

Oppgaven går ut på å skrive modulen data_normalization slik at funksjonskallene over fungerer.

Del B

I tillegg til at modulen skal kunne importeres uten at det blir noen utskrift til skjermen, skal det også være mulig å kjøre data_normalization.py direkte for å få en interaktiv modus hvor brukeren kan skrive inn tallene selv. Her kan vi bruke __name__.

En eksempelkjøring av data_normalization (fet/grønn tekst skrevet av brukeren):

Normaliser data.
Oppgi data du vil normalisere, separert av mellomrom:
1 2 3 4 5 6 7 8 9

norm_0_1: [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
norm_neg1_1: [-1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0]

Ytterligere krav:

  • Alle funksjonene skal ha en liste med flyttall som parameter (lister med int er også OK, siden int er en undertype av float).
  • Alle funksjonene skal være ikke-destruktive.
  • Alle tall skal behandles som flyttall, og returverdien skal alltid være et liste med flyttall.
Nivå D
Pi med tilfeldige tall

I denne oppgaven skal vi bruke modulen random fra python sitt standardbibliotek for å beregne verdien av \(\pi\). Selvfølgelig finnes en tilnærming for denne verdien allerede (f. eks. math.pi), men la oss forestille oss at vi nå ikke hadde denne beregningen fra før.

Den beste tilnærmingen vi har til π i skrivende stund inneholder over \(10^{12}\) siffer. I python er tilnærmingen av gitt ved math.pi begrenset til de første 15 siffrene av pi, men det finnes også andre moduler som gir en bedre tilnærming; men da må vi slutte å bruke float, og heller bruke en annen datatype som er i stand til å gi oss høyere presisjon.

\(\pi\) er definert som en sirkels omkrets delt på sin diameter. Uten å vite nøyaktig hvilken verdi π har, kan matematikere bevise at denne verdien, om vi en vakker dag skulle få vite hva den er, kan brukes for å regne ut en sirkel sitt areal:

$$ A = \pi \cdot r^2 $$

I formelen over er \(r\) sirkelens radius.

Tenk deg nå at vi har en sirkel med sentrum i (0, 0) og radius \(r = 1\). Vi har også et kvadrat med sidelengde \(s = 2\), som går mellom punktene (-1, -1) og (1, 1), som illustrert i bildet under.

Illustrasjon av sirkel i rektangel

Vi legger verdiene for r og s inn i formelene for areal av henholdsvis sirkler og kvadrat, og får følgende:

$$A_{\bigcirc} = \pi \cdot r^2 = \pi \cdot 1^2 = \pi$$ $$A_{\square} = s^2 = 2^2 = 4$$

Siden arealet av sirkelen tydelig er mindre enn arealet av kvadratet, vet vi allerede nå at π må være mindre enn 4. Men vi kan gjøre en bedre tilnærming enn som så.

Dersom vi velger et helt tilfeldig punkt inne i kvadratet, vil punktet med viss sannsynlighet \(p\) havne inne i sirkelen. Merk at denne sannsynligheten \(p\) er gitt ved forholdet mellom arealet av sirkelen og kvadratet:

$$ p = \frac{A_{\bigcirc}}{A_{\square}} = \frac{\pi}{4}$$

Det følger at hvis vi klarer å beregne hva \(p\) er for noe, kan vi bare gange tallet med 4 for å finne \(\pi\). Det leder oss til følgende algoritme:

  1. Gjett et tilfeldig punkt mellom (-1, -1) og (1, 1)
  2. Sjekk om punktet er innenfor eller utenfor sirkelen
  3. Gjenta steg 1-2 tilstrekkelig mange ganger, og tell opp hvor ofte det tilfeldige tallet havnet inne i eller utenfor sirkelen. Da bli forholdet mellom antall treff i sirkel og totalt antall genererte punkter tilnærmet lik \(p\).
  4. Gang opp \(p\) med 4 for å finne en tilnærmet verdi for π.

I random_pi.py skriv en funksjon estimate_pi(n). La funksjonen beregne \(\pi\) ved å generere n tilfeldige koordinater mellom (-1, -1) og (1, 1) og telle opp hvor mange av dem som havnet innefor sirkelen og hvor mange som havnet utenfor.

For å finne et tilfeldig verdi mellom -1 og 1:

  • random.random() gir oss et tilfeldig flyttall mellom 0 og 1
  • gang tallet med 2 og trekk fra 1; da får vi et tilfeldig tall mellom -1 og 1. For å finne ut om et punkt er inne i en sirkel, holder det å sjekke om avstanden mellom punktet og sirkelens sentrum er større enn sirkelens radius. Til dette kan vi bruke pytagoras. Se oppgaven avstand mellom to punkter.

Nivå C
Kvittering

I filen kvittering.py skal du bruke modulen decimals. til å skrive et program som produserer en kvittering fra en butikk.

Koden din vil sannsynligvis ikke fungere hvis du ikke bruker decimals-modulen for å lagre tall, siden avrundingsfeil er veldig viktig å unngå, samtidig som vi skal bruke desimal-tall.

Ta utgangspunt i koden nedenfor:

def receipt_content(prices_filename, cash_register_filename):
    """Construct contents of a receipt of the cash register events,
    given the store prices."""

    # din kode her


def receipt(prices_filename, cash_register_filename):
    """Construct a receipt of the cash register events,
    given the store prices."""

    # get receipt content from receipt_content()
    purchases_returns, total, vat, payment, change = receipt_content(
        prices_filename, cash_register_filename
    )

    # the formatted lines of the receipt
    receipt_lines = [f"{'Nr.':>4}  {'Item':18}  {'Price':>10}"]

    for nr, item, price in purchases_returns:
        receipt_lines.append(f"{nr:4d}  {item:18}  {price:10.2f}")

    receipt_lines.append(f"Total: {total}")
    receipt_lines.append(f"Of which VAT: {vat:.2f}")
    receipt_lines.append(f"Payment: {payment}")
    receipt_lines.append(f"Change {change}")

    # add some dividers
    max_len = max(len(line) for line in receipt_lines)
    divider = "-" * max_len
    receipt_lines.insert(1, divider)
    receipt_lines.insert(-4, divider)
    receipt_lines.insert(-2, divider)

    return "\n".join(receipt_lines)

Du skal skrive koden til funksjonen receipt_content() som beregner inneholdet i kvitteringen utifra en fil med butikkens priser og en fil med hendelsene ved kassen. Argumentet prices_filename er en streng med navnet til filen som inneholder prisene. Argumentet cash_register_filename er en streng med navnet til filen som inneholder hendelsene ved kassen.

Funksjonen receipt_content() skal returnere en tupel som inneholder følgende (i denne rekkefølgen):

  1. En liste med tupler som inneholder (i følgende rekkefølge) antall, produkt og total pris, først for alle ting som har blitt kjøpt, i alfabetisk orden, og så for alle ting som har blitt returnert, i alfabetisk orden (se eksempelkvittering nedenfor). For de returnerte produktene blir det negative tall.
  2. Den totale prisen.
  3. Hvor mye av den totale prisen som er mva.
  4. Hvor mye som har blitt betalt.
  5. Hvor mye som blir betalt tilbake (her blir det ikke-positive tall).

Filen med butikkens priser er formattert slik som eksempelfilen prices.txt (som du kan bruke til å teste programmet ditt). Først står produkt, så semikolon (;), og så prisen. Dette er egentlig en CSV-fil som bruker semikolon som skilletegn.

Filen med hendelsene ved kassen er formattert slik som eksempelfilen cash_register.txt (som du kan bruke til å teste programmet ditt). Først står hva som skjer (kjøp, retur eller betaling), så semikolon (;), og så produkten/verdien. Dette er også egentlig en CSV-fil som bruker semikolon som skilletegn.

Funksjonen receipt() skal du ikke endre. Den bruker receipt_content() til å beregne inneholdet i kvitteringen og så produserer receipt() en pen kvittering utifra det innholdet. Du skal bare skrive kode til receipt_content().

Om din kode til receipt_content() er riktig så skal denne testen passere:

print("Tester receipt... ", end="")
expected_value = """\
 Nr.  Item                     Price
------------------------------------
   2  apple                    10.00
   1  chips                    24.30
   1  dish soap                26.20
   1  frozen pizza             54.40
   1  peanuts                  18.50
   1  toilet paper             34.00
   3  tomato                   30.00
  -1  pocket book            -149.00
  -1  toothpaste              -13.70
------------------------------------
Total: 34.70
Of which VAT: 6.94
------------------------------------
Payment: 100.00
Change -65.30"""
assert(expected_value == receipt("prices.txt", "cash_register.txt"))
print("OK")

  • Du må bruke biblioteket decimals for at beregningene skal bli riktige (prøv hvordan kvitteringen blir om du bruker float).

  • Decimals aksepterer både float og string for å lage decimals. Prøv hvordan kvitteringen blir om du gir decimals en float. Hvorfor skjer dette?

  • Det er mulig at betaling skjer flere ganger underveis ved kassen.

  • Du kan anta at alle betalinger ved kassen har positiv verdi.

  • Du kan anta at det ikke er noen feil i filene som inneholder prisene og hendelsene ved kassen.

  • Det er mulig at den totale prisen blir negativt (om kunden for eksempel bare returnerer ting).

  • Gitt en pris inklusive mva så multipliserer du prisen med 0.2 for å finne ut hvor mye av prisen som er mva.

  • Det kan være lurt å bruke dictionaries til prislisten og det som blitt kjøpt og det som blitt returnert.

  • Du må ikke skrive all kode innen funksjonen receipt_content(). Du kan dele opp koden i flere funksjoner på en måte du synes føles naturlig.

Nivå C
Kombinere CSV-filer

I denne oppgaven skal vi kombinere flere CSV-filer som ligger i en mappe til én stor CSV-fil. Hver av CSV-filene har det samme formatet, som også er det formatet den kombinerte CSV-filen skal ha:

uibid,karakter,kommentar
abc101,A,"Veldig bra, fra start til slutt"
abc102,B,"Denne kandidaten kan sin INF100, men bommer litt i oppgave 2 og 3"
abc103,C,"Denne kandidaten valgte å kun svare på partallsoppgavene"

I csv_combiner.py skriv en funksjon combine_csv_in_dir(dirpath, result_path) som har to parametre:

  • dirpath er en sti til en mappe som inneholder en rekke CSV-filer som skal kombineres. Det er kun filene som slutter på .csv i mappen som skal inkluderes, andre filtyper skal vi overse.
  • result_path er en sti til en CSV-fil som skal opprettes, som inneholder de kombinerte dataene fra alle CSV-filene.

For å teste funksjonen kan du laste ned samples.zip og pakke ut innholdet i samme mappe hvor du også finner csv_combiner.py. Innholdet skal ligge i en mappe som heter samples. Du kan så teste funksjonen din med denne koden:

print("Tester combine_csv_in_dir... ", end="")
# Mappen samples må ligge i samme mappe som denne filen
dirpath = os.path.join(os.path.dirname(__file__), "samples")
combine_csv_in_dir(dirpath, "combined_table.csv")
with open("combined_table.csv", "rt", encoding='utf-8') as f:
   content = f.read()
   assert("""\
uibid,karakter,kommentar
abc104,C,hei
abc105,D,"med komma, her er jeg"
abc106,E,tittit
abc101,A,Her er min kommentar
abc102,B,"Jeg er glad, men her er komma"
abc103,C,Katching
""" == content or """\
uibid,karakter,kommentar
abc101,A,Her er min kommentar
abc102,B,"Jeg er glad, men her er komma"
abc103,C,Katching
abc104,C,hei
abc105,D,"med komma, her er jeg"
abc106,E,tittit
""" == content)
print("OK")

Merk: csv-biblioteket sin standard oppførsel når du lagrer 2D-lister som CSV er at det kun benyttes hermetegn dersom det er nødvendig. Det er nødvendig med hermetegn dersom en celle inneholder skilletegn (komma), linjeskift eller hermetegn. Dersom cellen ikke inneholder noen av de tre tegnene, vil det ikke inkluderes hermetegn i filen. Denne oppførselen kan endres ved å kalle på write_csv_file -funksjonen i kursnotatene med argumentene quoting=csv.QUOTE_NONNUMERIC eller quoting=csv.QUOTE_ALL (se også offisiell dokumentasjon).

Dette betyr at selv om det kanskje er hermetegn i input-filen, vil disse ikke nødvendigvis bli med i resultat-filen. Assert-setningene over viser resultatet slik det blir med standard-innstillingene til csv-biblioteket.

  • CSV-filene i dette eksempelet er best lest med csv-biblioteket. Det blir fort komplisert å tolke dem selv, siden det kan være komma-tegn i kommentar-feltet. Bruk gjerne funksjonene fra kursnotatene om csv-modulen i notater om standardbiblioteket.

  • Les om os -modulen i kursnotater om standardbiblioteket, og legg spesielt merke til os.walk -funksjonen.

  • Bruk gjerne en hjelpefunksjon merge_table_into(master_table, new_table) som tar som input en 2D-liste master_table som skal muteres, og en 2D-liste new_table som inneholder det nye innholdet som skal legges til. For hver rad i new_table (bortsett fra første rad), kopier raden inn i master_table.

  • Opprett først en 2D-liste for resultat-tabellen vår, som initielt inneholder én rad (overskriftene). På slutten skal vi konvertere denne listen til CSV.

  • Bruk os.walk eller os.listdir for å gå igjennom alle filene i mappen gitt ved dirpath (os.walk vil også gå inn i undermapper, og du trenger en nøstet løkke inne i os.walk for å gå gjennom listen med filer; ellers fungerer de nokså likt). For hver fil som ender på .csv (bruk f. eks. .endswith -metoden på strenger).

  • Husk å bruke os.path.join -funksjonen for å omgjøre filnavn til filstier. Se eksempler fra kursnotatene.

  • For hver .csv -fil du finner, omgjør den til en 2D-liste, og legg til radene i resultat-tabellen (bruk hjelpefunksjonen beskrevet over).

Nivå A
Værmelding

I filen forecast.py skriv en funksjon weather_in_bergen_next_hour() uten parametre som returnerer en streng som sammenfatter været i Bergen den neste timen. Funksjonen skal hente vær-data fra meterologisk instutt, og bruke informasjon fra feltet «symbol_code» om den nærmeste timen.

Eksempelkjøring: print("Været i Bergen neste time:", weather_in_bergen_next_hour())

Været i Bergen neste time: cloudy

For å løse denne oppgaven, skal vi

  1. Finne ut hvilken url vi kan bruke for å få informasjon om været fra meterologisk institutt
  2. Bruke requests -pakken for å laste ned informasjonen fra meterlogisk institutt inn i python-programmet
  3. Bruke json -pakken for å konvertere den nedlastede informasjonen til et oppslagsverk, og til slutt
  4. Slå opp på riktig sted i oppslagsverket og returnere denne informasjonen.

Steg A: nettadresse fra meterologisk institutt

Meterologisk instutt tilbyr massevis av vær-informasjon gratis (under lisensen CC-BY 4.0) fra sine nettsider. Slå opp på nettsiden https://api.met.no/weatherapi/locationforecast/2.0/ og finn OPENAPI UI fannen (Øverst i siden ved siden av DOCUMENTATION). Les om GET /compact under data.

  • Du vil under «parameters» se to felter lat og lon hvor du kan fylle ut henholdsvis breddegrad og lengdegrad for hvilken posisjon du ønsker.

For å finne breddegrad og lengdegrad for et gitt sted, kan du på til https://maps.google.com og trykke på kartet. Da vil koordinatene til det gitte stedet vises.

  • Når du har fylt ut breddegrad og lengdegrad, trykk på «Try it out!»
  • Kopier med deg internettadressen under «Request URL». Dette er adressen til siden vi skal laste ned i programmet vårt (prøv den direkte i nettleseren også og se hva du får).
  • Undersøk innholdet under «Response Body». Dette er den strengen vi får når vi laster ned innholdet fra den overnevnte nettadressen. Hvor ligger informasjonen vi er ute etter? Husk, vi ser etter verdier knyttet til nøkkelen «symbol_code».

Steg B: bruk requests for å laste ned informasjon

Bruk nettadressen vi fant i forrige steg som url. Merk at vilkårene til Meterologisk institutt sier at vi må identifisere hvem vi er i User-Agent -feltet i «header». Det er tilstrekkelig å bruke "inf100.ii.uib.no <ditt uib-brukernavn>" som verdi for dette feltet. Se eksempel i kursnotater om eksterne pakker.

Steg C: bruk JSON for å konvertere til oppslagsverk

Se eksempel i kursnotater om moduler

Steg D: returner verdien knyttet til nøkkelen «symbol code» i oppslagsverket

Det gjelder å holde tungen rett i munnen når du skal slå opp i oppslagsverket. En strategi kan være å begynne med å returnere d["properties"] og så spesifisere videre ved å returnere d["properties"]["timeseries"] og så videre, helt til riktig verdi blir returnert.

Det er ikke nødvendig å finne nøyaktig riktig time for å få oppgaven godkjent, det holder å returnere den første timen du får informasjon om (selv om den var i fortiden)

Steg E (frivillig): finn nøyaktig riktig time å rapportere for

For å rapportere for nøyaktig riktig time, må du matche den timen du får fra meterologisk instiutt med tiden akkurat nå. Les deg opp på datetime -modulen og se om du kan bruke informasjonen fra meterologisk institutt for å finne den nærmeste timen å rapportere fra.

Steg F (frivillig): mer omfattende værmelding

Kan du finne mer informasjon å rapportere? Nedbørsmengder? Vind? Sannsynlighet for nedbør (nå må du over på GET /complete hos meterologisk institutt)?