Basis¶
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. Je maakt ook met een f-string een vraag waarin het dagnummer staat.
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 |
|
|
|
|
|
|
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
Gebruik bij deze geneste lus het lusrecept uit lectures/4a_lussen. Noteer bij vraag 4 wanneer de binnenste lus klaar is met de dagen van het huidige fonds en wanneer de buitenste lus klaar is met alle fondsen.
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.
Een vraag maken met het dagnummer¶
Bij het invoeren van dagkoersen wil je het dagnummer in een vraag opnemen. Met een f-string maak je een string waarin Python waarden invult. Dat heet interpolatie. Zet een f vóór de aanhalingstekens en de variabele tussen accolades.
day = 3
message = f"Prijs voor dag {day}: "
assert message == "Prijs voor dag 3: "
print(message)
De toekenning maakt eerst de string; print(message) drukt die daarna af:
Prijs voor dag 3:
Wil je alleen iets afdrukken, dan kun je ook meerdere argumenten aan print geven:
day = 3
print("Prijs voor dag", day, ":")
Dat drukt Prijs voor dag 3 : af. print zet spaties tussen de argumenten. Een f-string is dus een hulpmiddel om een string te maken; afdrukken is een afzonderlijke handeling.
Opdracht: een vraag voor de dagkoers¶
Maak de string
messagezelf met een f-string. Zet het dagnummerdayerin. Voor dag3ismessageprecies"Prijs voor dag 3: ", inclusief de spatie na de dubbele punt.Druk daarna
messageaf en voer de controlecel uit.Verander
dayin11, voer je oplossing opnieuw uit en pas de verwachte string in de controle aan.
De assertion controleert de stringwaarde. Vergelijk de afgedrukte vraag ook zelf: staat het gekozen dagnummer erin?
day = 3
# Maak hier message met een f-string en druk de string af.
assert message == "Prijs voor dag 3: "
Kolommen uitlijnen (optioneel)¶
Wil je nette kolommen afdrukken, dan kun je deze opmaak overnemen of opzoeken. Achter de dubbele punt staat hoe breed het veld minimaal is en hoe het wordt uitgelijnd:
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 lijnt rechts uit in een veld van vier tekens; >6 doet hetzelfde in een veld van zes tekens. Je hoeft deze codes niet te onthouden of zelf kolombreedtes te ontwerpen. Afdrukken met meerdere argumenten en andere spaties 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.
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¶
Bij deze opgave horen twaalf verplichte taken: één opdracht en elf stappen. De opdracht staat hierboven, onder De gegevens: daar maak je zelf de vraag naar de dagkoers. Maak die eerst, en werk daarna de stappen af.
In de stappen schrijf je 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 |
|
drukt de prijzen af als tabel |
1 |
2 |
|
de gemiddelde prijs |
2 |
3 |
|
de standaardafwijking |
3 |
4 |
|
de laagste prijs en de dag |
4 |
5 |
|
de hoogste prijs en de dag |
5 |
6 |
|
de beste koop- en verkoopdag |
6 |
7 |
|
een fonds aan het raster toevoegen |
7 |
8 |
|
het raster afdrukken |
8 |
9 |
|
de gemiddelde koers per fonds |
8 |
10 |
|
het raster omzetten naar tekens |
8 |
11 |
|
het menu |
- |
Onder elke stap staan twee cellen: één waarin je je functie schrijft, en één met controles. Assertions controleren de genoemde gevallen. Bij de printfuncties vergelijk je de afgedrukte uitvoer zelf met het verwachte resultaat; de aanroep alleen controleert die uitvoer niet automatisch.
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.
In input(f"Getal {ix}: ") maakt de f-string op dezelfde manier een string. Hier krijgt input die string als vraagtekst, in plaats van dat print hem afdrukt.
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.
Dit is één mogelijke presentatie van de tabel. Elke dag hoort bij zijn prijs; de precieze spaties mogen verschillen. Meerdere argumenten bij print zijn geldig.
Dag Prijs
--- -----
0 20
1 10
2 30
Voer deze aanroep uit en vergelijk de uitvoer met de inhoud hierboven:
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. Gebruik vraag 4 van het lusrecept: de binnenste lus is klaar bij het einde van range(ix1, len(prices)); de buitenste lus is klaar na alle mogelijke koopdagen.
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.
Dit is één mogelijke presentatie van het raster met twee fondsen. De dag- en fondsnummers horen bij de juiste prijzen; de precieze spaties mogen verschillen. Meerdere argumenten bij print zijn geldig.
fonds 0 1 2
0 20 10 30
1 15 25 35
Wil je de kolommen netjes onder elkaar, dan kun je de opmaak uit Kolommen uitlijnen (optioneel) hierboven overnemen. Dat hoeft niet.
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.
Voer deze aanroep uit en vergelijk de inhoud en de rij- en kolomverbanden zelf:
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]. Gebruik vraag 4 van het lusrecept: de binnenste lus stopt na de laatste dag van de huidige rij; de buitenste lus stopt na het laatste fonds.
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."""
...
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.