-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy path02_maths_vector_stuff.qmd
More file actions
380 lines (251 loc) · 12.6 KB
/
02_maths_vector_stuff.qmd
File metadata and controls
380 lines (251 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
---
title: "Tutorial 2: Using Julia for Maths and Vector Stuff"
date: last-modified
author: "Danet and Becks, based on originals by Delmas and Griffiths"
format:
html:
embed-resources: true
title-block-banner: true
engine: julia
---
```{julia}
#| echo: false
using DataFrames, Plots, Random, StatsPlots
```
This second document follows on from Tutorial 1: "Getting started" and assumes that you're still working in your active project.
There is also a section at the end with some "Quick tips".
## Basic Maths {#sec-basic-maths}
As you probably can guess, the REPL is an interface onto a large calculator. Julia does all the things R does... and you can find the basic maths operations defined [The Julia Manual](https://docs.julialang.org/en/v1/manual/mathematical-operations/index.html)
sums
```{julia}
#| output: false
1+1
```
exponents
```{julia}
#| output: false
10^3
```
sequences in a vector
```{julia}
# From 0, by 1, to 10...
x = collect(0:1:10)
# see it
x
```
multiply vector by scalar value
```{julia}
x*10
```
## Getting Help {#sec-getting-help}
Before we move on, lets talk about the help files and how to access them. As in R, the help files associated with a given package, function or command can be accessed using `?` followed by the function name (e.g. type `? pi` in the REPL).
Similar to when you entered Julia's package manager (using `]`) you'll notice that the `?` command causes a change in the REPL with `help?> `replacing `julia>` as the prompt. This informs you that you've entered the help mode. As an exercise, use the help mode to find the difference between `print` and `println`.
## Preamble to Objects like scalars, vectors and arrays
Before we start creating arrays and matrices, we'd like to demonstrate how you allocate numbers and strings to objects in Julia and check an object's type. We'd also like to highlight some simple mathematical operations.
### Allocating data to objects
Allocating in Julia is useful as it means that variables can be stored and used elsewhere. You allocate numbers to objects using the following:
First note that we use the `=` in `Julia`, not the `<-` as in R.
```{julia}
#| output: false
# allocate an Integer number to a variable
n = 5
```
Julia, like other languages, has some built in values, like `pi`. We can allocate these to variable names we will use. Notice that Julia converts pi into the unicode symbol!
::: {.callout-tip collapse="true"}
## Capitalising on unicode
`Julia` has built in unicode support that allows you to use mathematical symbols (and emojis!). This is vary useful when describing models and variables as you don't have to specify a 'wordy' variable but rather the actual mathematical symbol. Making for cleaner and more 'readable' code!
To 'access' different unicode symbols start by typing `\` and the standard name of the symbol you wish to use
:::
```{julia}
# allocate a pre-defined number of importance to a variable
# note that pi is converted to π
pi_sum = pi
pi_sum
```
We can use these unicode symbols (and emojis!), as a variable and assign them a value - meaning that 🐺 is totally a usable variable name!
```{julia}
🐺 = 4
🐺
```
You can also assign multiple values to separate variables in a concise manner. Julia can manage something like this:
```{julia}
αi, βi, γi = 1.3, 2.1, exp(39)
# confirm...
αi, βi, γi
```
#### Allocating strings
Of course you can also allocate strings of text to objects. You must use the `""` and not `''` to define strings.
```{julia}
sob = "School of Biosciences"
```
You can combine strings and numbers to print like this. Note how you use $object.name within the text string you are writing... and this works for objects that are text or numeric.
```{julia}
println("The favourite number in $sob is $n")
```
### Identifying the Type of object you've made
Julia is very specific about types of objects. Most programming languages are. One way to learn about them is to look at what is made when you make things in different ways.
```{julia}
typeof(n), typeof(sob), typeof(pi)
```
Julia is like R and Python in that it can infer the type of object (Integer, Float, etc) on the left hand side of the equals sign - you don't have to justify it like you do in C. However, you can declare the type if needed e.g.
```{julia}
pi_custom = Float64(3.141592)
```
For those of you that are interested, a floating-point object (a Float) is a number that has a decimal place. An Int object is an integer, a number without a decimal place, whereas an Irrational object is a specific type of Float used only for representing some irrational numbers of special significance (e.g. π and γ). The 64 purely refers to 64-bit which is the type of processor your computer uses, most modern computers are 64-bit.
Occasionally it will be valuable to convert an object from one type to another. For example, `n` is currently an Integer (Int64), and we might want it to be Float (Float64). To be clear, this is a distinction between `5` and `5.0`!
```{julia}
typeof(n)
```
```{julia}
n2 = convert(Float64, n)
typeof(n2)
```
::: {.callout-tip collapse="true"}
## Multiple dispatch using types
The strict type system of Julia means that it is possible to define multiple 'methods' for functions depending on the type of the input object. Which allows you to have function behave different for different input types. [This](https://www.moll.dev/projects/effective-multi-dispatch/) blog post provides a nice overview of multiple dispatch using Pokemon types but the take-home message here is that it is important that you specify the correct type of an object and that often times when you run into an error it is because of that... This may seem annoying at first but it does mean that in the long run your code is much 'safer' because you won't (unknowingly) be converting and combining objects that are of different types unless you specifically specify it.
:::
## Understanding Arrays, Vectors and Sequences.
As you saw above, we created a sequence of numbers using `collect(0:1:10)`. Let's look at what type of object this is:
```{julia}
typeof(x)
```
This is a vector. Let's step back to see the difference between arrays and vectors. Arrays, for the R users, are best thought of as `lists` - they are storage boxes for any type of variables and can contain collections of various types. The general way to create an array, in this case and empty one, is the `[ ]`.
```{julia}
empty_array = []
```
We will first create an array with the same values as `x` and then see how `collect` is the function that converts this to a vector, and actually lets us see the numbers too!
First, `range` can be used to make an array. This is very similar to `seq()` in `R` and has the two variations - `by` and `length` that the `R` function has. The difference is that `by` is replaced by the argument `step`. Note how a very concise summary of this array is presented using information in square brackets `[ ]`:
```{julia}
x_array1 = range(start = 1, step = 1, stop = 10)
x_array2 = range(start = 1, stop = 10, length = 5)
x_array1, x_array2
```
You can also now see that creating arrays is possible with `[ ]` and the use of the `:` :
```{julia}
x_array3 = [1:1:10]
```
Quite often, you want to either see the values, or specifically be using a vector. To do this, you can use the function `collect()`:
```{julia}
collect(x_array1)
```
## Indices of Arrays.
You should recall from **R** that values in arrays and vectors and dataframes have _addresses_ that we call indices. Julia works with indexing very similarly.
Let's make a simple array of 5 numbers and another simple array of five words. Note that the `[]` array function is a but like the `c()` function in **R**.
```{julia}
ar = [6,7,8,9,10]
br = ["Pint", "of", "Moonshine", "please"]
```
You can get any address in these using... square brackets!
```{julia}
ar[2] # gets the number 7!
```
```{julia}
br[3] # gets the word Moonshine
```
If you want two addresses in a sequence, you can just provide the sequence:
```{julia}
ar[2:3]
```
But if you want non-adjacent values, you need to provide the 'list of indices' as an array, which results in the use of `[[ ]]`.
```{julia}
ar[[2,4]]
```
Note this would be like using in R `ar[c(2,4)]`.
Another nice indexing feature is that you can simply specify `end` as a means to index the final element. This is quite useful when you *e.g.,* want to pull the final output of a series without needing to know how long the series is.
```{julia}
ar[end]
```
## Broadcasting: something VERY special
Broadcasting allows you to apply a function, like a `log()` or `exp()`, in an element-wise manner to an array (in other words apply the function to every element of an array).
We saw above that we can create a vector using `collect()` and multiply this by a scalar
```{julia}
# sequences in a vector
# From 0, by 1, to 10...
x = collect(0:1:10)
# see it
x
```
```{julia}
# multply scaler x vector.
x*10
```
You can work directly with arrays and pre-built functions to do things like this. To do-so, we combine the function with the (dot) `.` operator. Let's work with `x_array1` from above. Note how broadcasting the function across the array returns a vector.
```{julia}
# Look at the help file for exp10
exp_array1 = exp10.(x_array1)
```
If you try to do this without the (dot) `.` operator what happens?
```{julia}
# look at the help file for log - what is the default!?
log_array1 = log.(x_array1)
```
Did you check the help file for `log`? Is it the same default as we find in **R**?
## Matrices
Sometimes we'll be interested in a 2-dimensional or higher version of the array/vector, and this is a matrix. Making a matrix in Julia uses the `[ ]` again, an separates rows of numbers with the `;`
```{julia}
mat = [1 2 3; 4 5 6]
```
Note how there are NO commas between the numbers in each row! This is read as 'rows are separated by ; and columns by spaces'!
You can also 'pre-fill' a matrix with zeros. This is good practice in loops and programming as pre-filling and replacing variables in a matrix is more efficient than creating the matrix on the fly. Here we demonstrate how to pre-fil a vector, matrix and high dimension array! Matrices can have more than two dimensions!
```{julia}
vec0 = zeros(2) # 2 zeros allocated in a vector
```
```{julia}
mat0 = zeros(2,3) # zeros allocated to 2 rows and 3 columns!
```
```{julia}
arr0 = zeros(2,3,4) # 2 rows, 3 columns and 4 dimensions!
```
Accessing values in a matrix follows the same convention as with the vector. The convention is `[row, column]`
```{julia}
mat[1,2] # value in the first row and second column
```
```{julia}
mat[1:2, 3] # rows 1 AND 2 in the 3rd column
```
Finally, to get a row or column, you need to know that we need a placeholder for the _missing_ bit of what you are asking for. If we want the second row, we ask for row 2, and stick the `:` placeholder in the column spot:
```{julia}
mat[2,:]
```
For a column, we reverse this.
```{julia}
mat[:,2]
```
### A quick interlude on types
As discussed earlier **Julia** is able to infer the type of an object based on the input. So the `mat` object we created earlier will be a matrix of integers i.e. `Matrix{Int64}`.
```{julia}
typeof(mat)
```
So what happens if we want to replace one of the elements with a Float?
```{julia}
#| eval: false
mat[1,2] = 1.5
```
This is because we are trying to add a Float to an object that is of the type Integer. What happens if we convert `mat` to be a matrix of floats and then try again?
```{julia}
mat = convert(Matrix{Float64}, mat)
mat[1,2] = 1.5
```
## Dictionaries
Dictionaries are another way to collect information in **Julia**, these look-up tables allow you to organise information (the **key**) with corresponding data (**value**). When we create a dictionary we specify it as 'value' `=>` 'key'. Dictionaries are useful if you need to store a collection of parameters or outputs, especially because the values for each pair can be of a different type.
```{julia}
parameters = Dict{Symbol,Any}(
:growth_rate => 0.4,
:response => :logistic,
:carry_capacity => 0.28,
)
```
We can also add an 'entry' to our dictionary very easily (or change the value)
```{julia}
parameters[:abundance] = collect(1:10)
```
### Looking things up
To get a value, if you have the key:
```{julia}
parameters[:growth_rate]
```
You can also get all values using `values()` (and the same for keys using `keys()`). Note that these functions are iterators and they have one job: to iterate through a dictionary value by value (or key by key) so if we want to turn these into an array we need to also call `collect()`
```{julia}
param_vals = collect(values(parameters))
param_keys = collect(keys(parameters))
```