JuliaKurs23/chapters/6_ArraysEtcP1.qmd

348 lines
8.5 KiB
Plaintext
Raw Normal View History

2024-05-12 19:50:45 +02:00
---
engine: julia
---
2023-05-12 20:42:26 +02:00
2024-05-13 00:38:46 +02:00
```{julia}
#| error: false
#| echo: false
#| output: false
using InteractiveUtils
```
2024-05-12 19:50:45 +02:00
# Container
2023-05-12 20:42:26 +02:00
Julia bietet eine große Auswahl von Containertypen mit weitgehend ähnlichem Interface an.
Wir stellen hier `Tuple`, `Range` und `Dict` vor, im nächsten Kapitel dann `Array`, `Vector` und `Matrix`.
Diese Container sind:
- **iterierbar:** Man kann über die Elemente des Containers iterieren:
```julia
for x ∈ container ... end
```
- **indizierbar:** Man kann auf Elemente über ihren Index zugreifen:
```julia
x = container[i]
```
und einige sind auch
- **mutierbar**: Man kann Elemente hinzufügen, entfernen und ändern.
Weiterhin gibt es eine Reihe gemeinsamer Funktionen, z.B.
- `length(container)` --- Anzahl der Elemente
- `eltype(container)` --- Typ der Elemente
- `isempty(container)` --- Test, ob Container leer ist
- `empty!(container)` --- leert Container (nur wenn mutierbar)
## Tupeln
Ein Tupel ist ein nicht mutierbarer Container von Elementen. Es ist also nicht möglich, neue Elemente dazuzufügen oder den Wert eines Elements zu ändern.
```{julia}
t = (33, 4.5, "Hello")
@show t[2] # indizierbar
for i ∈ t println(i) end # iterierbar
```
Ein Tupel ist ein **inhomogener** Typ. Jedes Element hat seinen eigenen Typ und das zeigt sich auch im Typ des Tupels:
```{julia}
typeof(t)
```
Man verwendet Tupel gerne als Rückgabewerte von Funktionen, um mehr als ein Objekt zurückzulieferen.
```{julia}
# Ganzzahldivision und Rest:
# Quotient und Rest werden den Variablen q und r zugewiesen
q, r = divrem(71, 6)
@show q r;
```
Wie man hier sieht, kann man in bestimmten Konstrukten die Klammern auch weglassen.
Dieses *implict tuple packing/unpacking* verwendet man auch gerne in Mehrfachzuweisungen:
```{julia}
x, y, z = 12, 17, 203
```
```{julia}
y
```
Manche Funktionen bestehen auf Tupeln als Argument oder geben immer Tupeln zurück. Dann braucht man manchmal ein Tupel aus einem Element.
Das notiert man so:
```{julia}
x = (13,) # ein 1-Element-Tupel
```
Das Komma - und nicht die Klammern -- macht das Tupel.
```{julia}
x= (13) # kein Tupel
```
## Ranges
Wir haben *range*-Objekte schon in numerischen `for`-Schleifen verwendet.
```{julia}
r = 1:1000
typeof(r)
```
Es gibt verschiedene *range*-Typen. Wie man sieht, sind es über den Zahlentyp parametrisierte Typen und `UnitRange` ist z.B. ein *range* mit der Schrittweite 1. Ihre Konstruktoren heißen in der Regel `range()`.
Der Doppelpunkt ist eine spezielle Syntax.
- `a:b` wird vom Parser umgesetzt zu `range(a, b)`
- `a:b:c` wird umgesetzt zu `range(a, c, step=b)`
*Ranges* sind offensichtlich iterierbar, nicht mutierbar aber indizierbar.
```{julia}
(3:100)[20] # das zwanzigste Element
```
Wir erinnern an die Semantik der `for`-Schleife: `for i in 1:1000` heißt **nicht**:
- 'Die Schleifenvariable `i` wird bei jedem Durchlauf um eins erhöht' **sondern**
- 'Der Schleifenvariable werden nacheinander die Werte 1,2,3,...,1000 aus dem Container zugewiesen'.
Allerdings wäre es sehr ineffektiv, diesen Container tatsächlich explizit anzulegen.
- _Ranges_ sind "lazy" Vektoren, die nie wirklich irgendwo als konkrete Liste abgespeichert werden. Das macht sie als Iteratoren in `for`-Schleifen so nützlich: speichersparend und schnell.
- Sie sind 'Rezepte' oder Generatoren, die auf die Abfrage 'Gib mir dein nächstes Element!' antworten.
- Tatsächlich ist der Muttertyp `AbstractRange` ein Subtyp von `AbstractVector`.
Das Macro `@allocated` gibt aus, wieviel Bytes an Speicher bei der Auswertung eines Ausdrucks alloziert wurden.
```{julia}
2024-05-27 21:56:40 +02:00
@allocated r = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
2023-05-12 20:42:26 +02:00
```
```{julia}
2024-05-27 21:56:40 +02:00
@allocated r = 1:20
2023-05-12 20:42:26 +02:00
```
Zum Umwandeln in einen 'richtigen' Vektor dient die Funktion `collect()`.
```{julia}
collect(20:-3:1)
```
Recht nützlich, z.B. beim Vorbereiten von Daten zum Plotten, ist der *range*-Typ `LinRange`.
```{julia}
LinRange(2, 50, 300)
```
`LinRange(start, stop, n)` erzeugt eine äquidistante Liste von `n` Werten von denen der erste und der letzte die vorgegebenen Grenzen sind.
Mit `collect()` kann man bei Bedarf auch daraus den entsprechenden Vektor gewinnen.
## Dictionaries
- _Dictionaries_ (deutsch: "assoziative Liste" oder "Zuordnungstabelle" oder ...) sind spezielle Container.
- Einträge in einem Vektor `v` sind durch einen Index 1,2,3.... addressierbar: `v[i]`
- Einträge in einem _dictionary_ sind durch allgemeinere _keys_ addressierbar.
- Ein _dictionary_ ist eine Ansammlung von _key-value_-Paaren.
- Damit haben _dictionaries_ in Julia den parametrisierten Typ `Dict{S,T}`, wobei `S` der Typ der _keys_ und `T` der Typ der _values_ ist
Man kann sie explizit anlegen:
```{julia}
# Einwohner 2020 in Millionen, Quelle: wikipedia
EW = Dict("Berlin" => 3.66, "Hamburg" => 1.85,
"München" => 1.49, "Köln" => 1.08)
```
```{julia}
typeof(EW)
```
und mit den _keys_ indizieren:
```{julia}
EW["Berlin"]
```
Das Abfragen eines nicht existierenden _keys_ ist natürlich ein Fehler.
```{julia}
EW["Leipzig"]
```
Man kann ja auch vorher mal anfragen...
```{julia}
haskey(EW, "Leipzig")
```
... oder die Funktion `get(dict, key, default)` benutzen, die bei nicht existierendem Key keinen Fehler wirft sondern das 3. Argument zurückgibt.
```{julia}
@show get(EW, "Leipzig", -1) get(EW, "Berlin", -1);
```
Man kann sich auch alle `keys` und `values` als spezielle Container geben lassen.
```{julia}
keys(EW)
```
```{julia}
values(EW)
```
Man kann über die `keys` iterieren...
```{julia}
for i in keys(EW)
n = EW[i]
println("Die Stadt $i hat $n Millionen Einwohner.")
end
```
odere gleich über `key-value`-Paare.
```{julia}
for (stadt, ew) ∈ EW
println("$stadt : $ew Mill.")
end
```
### Erweitern und Modifizieren
Man kann in ein `Dict` zusätzliche `key-value`-Paare eintragen...
```{julia}
EW["Leipzig"] = 0.52
EW["Dresden"] = 0.52
EW
```
und einen `value` ändern.
```{julia}
# Oh, das war bei Leipzig die Zahl von 2010, nicht 2020
EW["Leipzig"] = 0.597
EW
```
Ein Paar kann über seinen `key` auch gelöscht werden.
```{julia}
delete!(EW, "Dresden")
```
Zahlreiche Funktionen können mit `Dicts` wie mit anderen Containern arbeiten.
```{julia}
maximum(values(EW))
```
### Anlegen eines leeren Dictionaries
Ohne Typspezifikation ...
```{julia}
d1 = Dict()
```
und mit Typspezifikation:
```{julia}
d2 = Dict{String, Int}()
```
### Umwandlung in Vektoren: `collect()`
- `keys(dict)` und `values(dict)` sind spezielle Datentypen.
- Die Funktion `collect()` macht daraus eine Liste vom Typ `Vector`.
- `collect(dict)` liefert eine Liste vom Typ `Vector{Pair{S,T}}`
```{julia}
collect(EW)
```
```{julia}
collect(keys(EW)), collect(values(EW))
```
### Geordnetes Iterieren über ein Dictionary
Wir sortieren die Keys. Als Strings werden sie alphabetisch sortiert. Mit dem `rev`-Parameter wird rückwärts sortiert.
```{julia}
for k in sort(collect(keys(EW)), rev = true)
n = EW[k]
println("$k hat $n Millionen Einw. ")
end
```
Wir sortieren `collect(dict)`. Das ist ein Vektor von Paaren. Mit `by` definieren wir, wonach zu sortieren ist: nach dem 2. Element des Paares.
```{julia}
for (k,v) in sort(collect(EW), by = pair -> last(pair), rev=false)
println("$k hat $v Mill. EW")
end
```
### Eine Anwendung von Dictionaries: Zählen von Häufigkeiten
Wir machen 'experimentelle Stochastik' mit 2 Würfeln:
Gegeben sei `l`, eine Liste mit den Ergebnissen von 100 000 Pasch-Würfen, also 100 000 Zahlen zwischen 2 und 12.
Wie häufig sind die Zahlen 2 bis 12?
Wir (lassen) würfeln:
```{julia}
l = rand(1:6, 100_000) .+ rand(1:6, 100_000)
```
Wir zählen mit Hilfe eines Dictionaries die Häufigkeiten der Ereignisse. Dazu nehmen wir das Ereignis als `key` und seine Häufigkeit als `value`.
```{julia}
# In diesem Fall könnte man das auch mit einem einfachen Vektor
# lösen. Eine bessere Illustration wäre z.B. Worthäufigkeit in
# einem Text. Dann ist i keine ganze Zahl sondern ein Wort=String
d = Dict{Int,Int}() # das Dict zum 'reinzählen'
for i in l # für jedes i wird d[i] erhöht.
d[i] = get(d, i, 0) + 1
end
d
```
Das Ergebnis:
```{julia}
using Plots
plot(collect(keys(d)), collect(values(d)), seriestype=:scatter)
```
##### Das Erklär-Bild dazu:
[https://math.stackexchange.com/questions/1204396/why-is-the-sum-of-the-rolls-of-two-dices-a-binomial-distribution-what-is-define](https://math.stackexchange.com/questions/1204396/why-is-the-sum-of-the-rolls-of-two-dices-a-binomial-distribution-what-is-define)