Basis

Deze pagina bevat uitvoerbare code.

TR investeringen BV

TR Investeringen BV belooft zijn klanten de beste dag om te kopen en de beste dag om te verkopen. In deze opgave bouw je het programma waarmee ze dat doen: een menu waarin je een lijst met dagkoersen invoert en er van alles over uitrekent. Daarna gaan we van één fonds naar meerdere fondsen tegelijk, en dan heb je geen lijst meer nodig maar een 2D-lijst.

Je oefent hier met lussen, met geneste lussen en met het verwerken van gebruikersinvoer.

Waarschuwing

Je mag geen gebruik maken van de ingebouwde functies sum, min of max in deze opgave. Schrijf in plaats daarvan bij het bouwen van de verschillende menu-opties hulpfuncties die de gegevens op de juiste manier kunnen verwerken. Als je deze functies echt wilt gebruiken, schrijf dan je eigen versies hiervan.

De gegevens

Eén fonds is een lijst met een prijs per dag. De index is de dag, het element is de prijs.

index

element

Dag

Prijs

0

20

1

10

2

30

In Python: prices = [20, 10, 30].

Meerdere fondsen tegelijk vormen een 2D-lijst: één rij per fonds, één kolom per dag. Dit raster volgt twee fondsen over drie dagen.

dag 0

dag 1

dag 2

fonds 0

20

10

30

fonds 1

15

25

35

Zo werk je met dat raster. Eerst de rij, dan de kolom:

grid = [[20, 10, 30], [15, 25, 35]]

# de koers van fonds 1 op dag 2
assert grid[1][2] == 35

# het aantal fondsen en het aantal dagen
assert len(grid) == 2
assert len(grid[0]) == 3

# elke positie in het raster langslopen
for fund in range(len(grid)):
    for day in range(len(grid[0])):
        print(grid[fund][day], end=" ")
    print()

De uitvoer van die geneste lus is:

20 10 30
15 25 35

Meer dan één waarde teruggeven

Een aantal stappen hieronder geeft twee of drie waarden tegelijk terug. Dat mag in Python:

def f(x):
    return x, (x + 4)

Deze functie roep je zo aan:

a, b = f(38)

a krijgt de waarde 38 en b de waarde 42.

Netjes uitlijnen met een f-string

Dit is optioneel, maar als je nette kolommen wilt afdrukken helpt een f-string. Achter de dubbele punt zet je hoe breed het veld moet zijn en hoe het uitgelijnd wordt:

day = 3
price = 42.0

print(f"{day:>4} : € {price:>6}")
print(f"{11:>4} : € {27042:>6}")

Dat geeft:

   3 : €   42.0
  11 : €  27042

>4 betekent: rechts uitlijnen in een veld van vier tekens breed. Met >7.2f krijg je bovendien altijd twee cijfers achter de komma. Afdrukken zonder je druk te maken over het formaat is ook goed.

De standaardafwijking berekenen

Gebruik deze formule voor de standaardafwijking. Lav is de gemiddelde waarde van de elementen van lijst L. Je mag ervan uitgaan dat L nooit leeg is.

\[ \sqrt \frac{\Sigma_{i} (\text{L[i]} - \text{L}_{av} )^2}{\text{len(L)}} \]

Ben je niet bekend met de Σ-notatie: die betekent dat je alle waardes bij elkaar optelt. Vraag om hulp als je er niet uitkomt!

Voor de wortel heb je geen extra module nodig: x ** 0.5 is de wortel van x.

De stappen

Je schrijft elf functies. De eerste zes werken op één lijst prijzen, de volgende vier op het koersenraster, en de laatste is het menuprogramma dat ze allemaal aanroept.

Stap

Functie

Wat ze doet

Menu-optie

1

print_prices(prices)

drukt de prijzen af als tabel

1

2

mean(prices)

de gemiddelde prijs

2

3

std_dev(prices)

de standaardafwijking

3

4

find_min(prices)

de laagste prijs en de dag

4

5

find_max(prices)

de hoogste prijs en de dag

5

6

best_trade(prices)

de beste koop- en verkoopdag

6

7

add_fund(grid, prices)

een fonds aan het raster toevoegen

7

8

print_grid(grid)

het raster afdrukken

8

9

mean_per_fund(grid)

de gemiddelde koers per fonds

8

10

chart(grid, limit)

het raster omzetten naar tekens

8

11

main()

het menu

-

Onder elke stap staan twee cellen: één waarin je je functie schrijft, en één met tests. Slagen die tests zonder foutmelding, dan klopt je functie.

Begincode

In deze opgave gebruik je een lus voor gebruikersinteractie. Hier is een voorbeeldprogramma van zo’n lus; in stap 11 schrijf je je eigen versie.

#
# Voorbeeldprogramma voor een lus voor gebruikersinteractie
#


def menu():
    """Drukt het menu af."""
    print()
    print("(0) Doorgaan!")
    print("(1) Nieuwe lijst invoeren")
    print("(2) Volgende element voorspellen")
    print("(9) Stoppen! (einde)")
    print()


def predict(numbers):
    """Negeert het argument en geeft terug wat het volgende element had moeten zijn."""
    return 42


def read_numbers():
    """Vraagt de gebruiker om getallen, één voor één, en geeft ze terug als lijst."""
    n = int(input("Hoeveel getallen wil je invoeren? "))
    numbers = []
    for ix in range(n):
        numbers = numbers + [float(input(f"Getal {ix}: "))]
    return numbers


def main():
    """De lus voor gebruikersinteractie."""
    numbers = [30, 10, 20]  # een beginlijst

    while True:  # de lus voor gebruikersinteractie
        print()
        print("De lijst is", numbers)
        menu()
        choice = input("Maak een keuze: ")

        # choice blijft een string. Daardoor levert rare invoer hier nooit een
        # foutmelding op: hij valt gewoon in de else hieronder.
        if choice == "9":  # we willen stoppen
            break  # de hele while-lus afbreken

        elif choice == "0":  # we willen doorgaan
            continue  # terug naar het begin van de while-lus

        elif choice == "1":  # een nieuwe lijst invoeren
            numbers = read_numbers()

        elif choice == "2":  # het volgende element voorspellen en toevoegen
            n = predict(numbers)
            print("Het volgende element is", n)
            print("Het wordt toegevoegd aan je lijst...")
            numbers = numbers + [n]

        else:
            print(choice, " ?      Dat staat niet op het menu!")

    print()
    print("Tot gisteren!")

Twee dingen om op te merken. De menukeuze blijft een string; hij wordt niet door int() gehaald. Daardoor levert onzinnige invoer geen foutmelding op, maar valt ze vanzelf in de else. En read_numbers vraagt eerst hoeveel getallen er komen, en daarna de getallen één voor één, zodat je ze meteen als getal binnenkrijgt en niet als tekst. Verder mag je ervan uitgaan dat de gebruiker daar echt getallen intypt.

Stap 1: print_prices(prices)

Druk de prijzen af als een tabel met een kolom Dag en een kolom Prijs.

Hint. Loop met range(len(prices)) over de dagen, zodat je zowel het dagnummer als de prijs hebt.

Deze tests horen te slagen:

print_prices([20, 10, 30])
# jouw oplossing


def print_prices(prices):
    """Drukt de prijzen af als tabel met een kolom Dag en een kolom Prijs."""
    ...
print_prices([20, 10, 30])

Stap 2: mean(prices)

Bereken de gemiddelde prijs: de som van alle prijzen gedeeld door het aantal prijzen.

Hint. Begin met een teller op 0, tel er in een lus elke prijs bij op, en deel daarna door len(prices).

Deze tests horen te slagen:

assert mean([20, 10, 30]) == 20.0
assert mean([5]) == 5.0
assert mean([1, 2]) == 1.5
# jouw oplossing


def mean(prices):
    """Geeft de gemiddelde prijs van de niet-lege lijst prices terug."""
    ...
assert mean([20, 10, 30]) == 20.0
assert mean([5]) == 5.0
assert mean([1, 2]) == 1.5

Stap 3: std_dev(prices)

Bereken de standaardafwijking volgens de formule hierboven.

Hint. Roep eerst je eigen mean aan. Tel daarna in een lus de kwadraten van de afwijkingen op, deel door len(prices) en neem de wortel met ** 0.5.

Deze tests horen te slagen:

assert std_dev([20, 10, 30]) == 8.16496580927726
assert std_dev([5]) == 0.0
assert std_dev([10, 10, 10]) == 0.0
# jouw oplossing


def std_dev(prices):
    """Geeft de standaardafwijking van de niet-lege lijst prices terug."""
    ...
assert std_dev([20, 10, 30]) == 8.16496580927726
assert std_dev([5]) == 0.0
assert std_dev([10, 10, 10]) == 0.0

Stap 4: find_min(prices)

Geef de laagste prijs terug én de dag waarop die viel, in die volgorde.

Hint. Begin met de prijs van dag 0 als voorlopig minimum en dag 0 als voorlopige dag. Loop met range(len(prices)) en pas beide aan zodra je iets lagers ziet.

Deze tests horen te slagen:

assert find_min([20, 10, 30]) == (10, 1)
assert find_min([7]) == (7, 0)
assert find_min([4, 4, 9]) == (4, 0)
# jouw oplossing


def find_min(prices):
    """Geeft de laagste prijs terug en de dag waarop die viel."""
    ...
assert find_min([20, 10, 30]) == (10, 1)
assert find_min([7]) == (7, 0)
assert find_min([4, 4, 9]) == (4, 0)

Stap 5: find_max(prices)

Geef de hoogste prijs terug én de dag waarop die viel, in die volgorde.

Hint. Dezelfde vorm als stap 4, met > in plaats van <.

Deze tests horen te slagen:

assert find_max([20, 10, 30]) == (30, 2)
assert find_max([7]) == (7, 0)
assert find_max([9, 4, 9]) == (9, 0)
# jouw oplossing


def find_max(prices):
    """Geeft de hoogste prijs terug en de dag waarop die viel."""
    ...
assert find_max([20, 10, 30]) == (30, 2)
assert find_max([7]) == (7, 0)
assert find_max([9, 4, 9]) == (9, 0)

Stap 6: best_trade(prices)

Geef de koopdag, de verkoopdag en de grootste winst terug. De verkoopdag mag niet vóór de koopdag liggen.

Vergelijk alle paren van een koopdag en een verkoopdag met elkaar en onthoud de beste. Valt er niets te verdienen, geef dan winst 0 terug met dag 0 als koop- en verkoopdag.

Hint. Dit is het All Pairs-algoritme uit het college. Neem min_diff erbij en vervang het verschil door de winst prices[ix2] - prices[ix1], en < door >. Dat de binnenste lus bij ix1 begint, is precies de eis dat je pas verkoopt nadat je hebt gekocht.

Deze tests horen te slagen:

assert best_trade([20, 10, 30]) == (1, 2, 20)
assert best_trade([40, 80, 10, 30, 27, 52, 5, 15]) == (2, 5, 42)
assert best_trade([30, 20, 10]) == (0, 0, 0)
assert best_trade([7]) == (0, 0, 0)
# jouw oplossing


def best_trade(prices):
    """Geeft de koopdag, de verkoopdag en de grootste winst terug."""
    ...
assert best_trade([20, 10, 30]) == (1, 2, 20)
assert best_trade([40, 80, 10, 30, 27, 52, 5, 15]) == (2, 5, 42)
assert best_trade([30, 20, 10]) == (0, 0, 0)
assert best_trade([7]) == (0, 0, 0)

Van één fonds naar meerdere

Tot hier ging alles over één lijst. TR Investeringen volgt natuurlijk meer dan één fonds, en dan wordt de lijst een raster: één rij per fonds, één kolom per dag. De vier stappen hieronder bouwen dat raster op, drukken het af, rekenen erover en tekenen het.

Merk op dat je mean uit stap 2 gewoon opnieuw gebruikt. Een rij van het raster ís een lijst prijzen. De geneste lus zit in het doorlopen van het raster, niet in het rekenwerk.

Stap 7: add_fund(grid, prices)

Geef een nieuw koersenraster terug met de prijzen van een extra fonds erbij als laatste rij.

Elke rij van het raster heeft evenveel dagen. Is het raster nog leeg, dan bepaalt het nieuwe fonds het aantal dagen; anders neem je alleen de eerste dagen over. Je mag ervan uitgaan dat een nieuw fonds minstens zoveel dagen heeft als het raster.

Let op: het raster dat je meekrijgt verandert niet. Je geeft een nieuw raster terug.

Hint. Bepaal eerst het aantal dagen: is het raster leeg, dan is dat len(prices), anders len(grid[0]). Bouw daarna de nieuwe rij op met new_row = new_row + [prices[day]] en plak die aan het raster met grid + [new_row].

Deze tests horen te slagen:

assert add_fund([], [20, 10, 30]) == [[20, 10, 30]]
assert add_fund([[20, 10, 30]], [15, 25, 35]) == [[20, 10, 30], [15, 25, 35]]
assert add_fund([[1, 2]], [7, 8, 9]) == [[1, 2], [7, 8]]

# het raster dat je meegeeft blijft ongewijzigd
grid = [[20, 10, 30]]
add_fund(grid, [15, 25, 35])
assert grid == [[20, 10, 30]]
# jouw oplossing


def add_fund(grid, prices):
    """Geeft een nieuw koersenraster terug met prices erbij als extra fonds."""
    ...
assert add_fund([], [20, 10, 30]) == [[20, 10, 30]]
assert add_fund([[20, 10, 30]], [15, 25, 35]) == [[20, 10, 30], [15, 25, 35]]
assert add_fund([[1, 2]], [7, 8, 9]) == [[1, 2], [7, 8]]

# het raster dat je meegeeft blijft ongewijzigd
grid = [[20, 10, 30]]
add_fund(grid, [15, 25, 35])
assert grid == [[20, 10, 30]]

Stap 8: print_grid(grid)

Druk het raster af met een regel per fonds, een kolomkop met de dagnummers en een regelkop met het fondsnummer.

Voor het raster met twee fondsen ziet dat er zo uit:

 fonds       0       1       2
     0      20      10      30
     1      15      25      35

Hint. Twee geneste lussen: de buitenste over range(len(grid)), de binnenste over range(len(grid[0])). Gebruik print(..., end="") zodat je op dezelfde regel blijft, en een lege print() aan het eind van elke rij.

Deze tests horen te slagen:

print_grid([[20, 10, 30], [15, 25, 35]])
# jouw oplossing


def print_grid(grid):
    """Drukt het koersenraster af, met een regel per fonds en de dagen als kolommen."""
    ...
print_grid([[20, 10, 30], [15, 25, 35]])

Stap 9: mean_per_fund(grid)

Geef een lijst terug met de gemiddelde koers per fonds, in de volgorde van de rijen.

Hint. Eén lus over range(len(grid)) is genoeg. grid[fund] is de hele rij van dat fonds, en dat is een lijst prijzen: geef die aan je eigen mean.

Deze tests horen te slagen:

assert mean_per_fund([[20, 10, 30], [15, 25, 35]]) == [20.0, 25.0]
assert mean_per_fund([[5, 5]]) == [5.0]
# jouw oplossing


def mean_per_fund(grid):
    """Geeft een lijst terug met de gemiddelde koers per fonds, op volgorde van de rijen."""
    ...
assert mean_per_fund([[20, 10, 30], [15, 25, 35]]) == [20.0, 25.0]
assert mean_per_fund([[5, 5]]) == [5.0]

Stap 10: chart(grid, limit)

Geef een nieuw raster van tekens terug: op elke positie een # als de koers minstens limit is, en anders een ..

Zo verandert een raster van getallen in een raster van tekens. In week 6 doe je precies dit met een afbeelding: elke pixel is een getal, en je zet het om naar iets anders.

Druk je zo’n tekenraster af met print_grid, dan zie je in één oogopslag waar de koersen boven de grens lagen.

Hint. Twee geneste lussen, net als bij print_grid, maar nu bouw je op: new_row = new_row + ["#"] en new_grid = new_grid + [new_row].

Deze tests horen te slagen:

assert chart([[20, 10, 30], [15, 25, 35]], 20) == [["#", ".", "#"], [".", "#", "#"]]
assert chart([[1]], 0) == [["#"]]
assert chart([[1]], 2) == [["."]]
# jouw oplossing


def chart(grid, limit):
    """Geeft een nieuw raster van tekens terug: # waar de koers minstens limit is, anders een punt."""
    ...
assert chart([[20, 10, 30], [15, 25, 35]], 20) == [["#", ".", "#"], [".", "#", "#"]]
assert chart([[1]], 0) == [["#"]]
assert chart([[1]], 2) == [["."]]

Stap 11: main()

Schrijf het menuprogramma. Het toont dit menu, wacht op een keuze, voert die uit en toont daarna het menu opnieuw. Bij keuze 9 stopt het.

(0) Voer een nieuwe lijst prijzen in
(1) Druk de huidige lijst af
(2) Bepaal de gemiddelde prijs
(3) Bepaal de standaardafwijking
(4) Bepaal het minimum en de bijbehorende dag
(5) Bepaal het maximum en de bijbehorende dag
(6) Je TR-investeringsplan
(7) Voeg de huidige lijst toe als extra fonds
(8) Toon het koersenraster
(9) Stoppen

Maak je keuze:

Optie 8 gebruikt drie van je functies achter elkaar: eerst print_grid, dan mean_per_fund, en dan print_grid(chart(grid, limit)) met een grens die je zelf kiest.

Bij een keuze die niet op het menu staat, druk je een waarschuwing af en toon je het menu opnieuw.

Hint. Neem de begincode hierboven als vertrekpunt. Houd twee dingen bij: de huidige lijst prices en het raster grid, dat begint als []. Vergeet niet dat add_fund een nieuw raster teruggeeft, dus je moet het resultaat opvangen: grid = add_fund(grid, prices).

# jouw oplossing


def main():
    """De lus voor gebruikersinteractie van het TRI-programma."""
    ...

Bonusopgave: creatieve menu-opties

Als je wilt kan je extra menu-opties toevoegen (met andere numerieke labels) die de lijst of het raster op een andere, eigen ontworpen manier verwerken. Dit hoeft niet serieus te zijn! In ieder geval worden de opties beoordeeld op wat ze doen en hoe makkelijk ze te gebruiken zijn.

Tot slot

Je hebt nu een koersenraster gebouwd, doorlopen en omgerekend. Stel dat er één koers verkeerd is ingevoerd: fonds 1 op dag 2 moet geen 35 zijn maar 33. De rest van het raster klopt.

De uitkomst is bereikbaar. Je schrijft een geneste lus die alle posities langsloopt en per positie een waarde kiest, precies zoals chart dat doet, en je krijgt een nieuw raster terug dat op één plek van het oude verschilt.

Eén vakje van een raster veranderen terwijl de rest blijft staan, is met het gereedschap van deze week niet uit te drukken. Wat je opschrijft is niet die ene correctie, maar de opbouw van een compleet nieuw raster. En omdat de functie een nieuw raster teruggeeft, moet de aanroeper het opvangen, net als bij add_fund. Bij één functie valt dat mee. Bij een programma waarin een handvol functies elk iets aan hetzelfde raster doen, bepaalt het de vorm van het hele programma.

Dat is een echte prijs, en de vraag is of hij nodig is. In week 7 komt daar een nieuw vermogen voor: mutatie, een lijst veranderen die er al is. Game of Life is daar de aanleiding, want dat is een raster dat generatie na generatie wordt bijgewerkt.