= 1 # Assign an integer to x
x 2x # The result of 2 * x
2
Julia is a high-level, high-performance programming language primarily designed for numerical and scientific computing. Its syntax is familiar to users of other technical computing environments, while its flexibility and performance make it an excellent choice for a wide range of applications. In this section, we will look at a few simple examples to illustrate some core features of Julia and demonstrate its intuitive and powerful design.
In Julia, you can assign values to variables directly:
You can also perform mathematical operations directly on variables:
Julia allows you to use Unicode characters in your code, which makes it more expressive:
Julia even allows using emojis for variable names:
2.8284271247461903
Visit the list of Unicode Input for more examples.
In Julia, you can define a function using the function
keyword:
f (generic function with 1 method)
To evaluate a function, simply call it with an argument:
Julia also supports defintion of functions in assignement form, which are often used for short operations:
Julia also supports anonymous functions (functions without a name):
In some cases, you need to be cautious about operator precedence:
In Julia, functions can have side effects, meaning they modify variables or objects outside the scope of the function. Here’s an example:
Let’s consider the following vector:
You can access an element of the vector like this:
To update an element, simply reassign it:
If you mutate data inside a function, it will have side effects. For example, consider this function:
function f(x, y)
x[1] = 42 # Mutates x
y = 7 + sum(x) # New binding for y, no mutation
return y
end
a = [4, 5, 6]
b = 3
println("f($a, $b) = ", f(a, b)) # f modifies 'a' but not 'b'
println("a = ", a, " # a[1] is changed to 42 by f")
println("b = ", b, " # b remains unchanged")
f([4, 5, 6], 3) = 60
a = [42, 5, 6] # a[1] is changed to 42 by f
b = 3 # b remains unchanged
When a function has side effects, it’s a good practice to use the !
symbol at the end of the function’s name. This is called the bang convention, and it signals that the function mutates its arguments:
When you pass a slice of an array to a function in Julia, the slice is actually a copy, so modifying it does not alter the original array:
x = [1, 2, 3, 4]
println("x[2] before slice modification: ", x[2])
put_at_second_place!(x[1:3], 15) # Safe to modify the slice
println("x[2] after slice modification: ", x[2]) # Original array remains unchanged
x[2] before slice modification: 2
x[2] after slice modification: 2
When working with slices, remember that they are copies in Julia. Modifying a slice will not impact the original array, which helps prevent unintentional changes to your data.
Julia supports multiple methods for the same function name, which allows for more flexible and dynamic behavior. Here’s an example:
You can define several methods for the same function with different types:
Calling the function:
If you call Σ
with arguments that don’t match the types, Julia will throw an error:
MethodError: no method matching Σ(::Int64, ::Float64) The function `Σ` exists, but no method is defined for this combination of argument types. Closest candidates are: Σ(::Float64, ::Float64) @ Main In[17]:1 Stacktrace: [1] top-level scope @ In[19]:1
You can define more methods that work with different types:
Julia will select the appropriate method based on the argument types:
In Julia, iterators allow you to loop through collections in a memory-efficient way. Here’s an example of using 1:5
as an iterator:
This prints the numbers from 1 to 5. You can also iterate through ranges and collections:
Julia’s Iterators
package allows for lazy collections, where values are computed on demand. Here’s an example:
using Base.Iterators: cycle
round = 1
for i in cycle([1, 2, 3])
println(i)
if i == 3
if round == 2
break
else
round += 1
end
end
end
1
2
3
1
2
3
This loops over the values 1, 2, and 3, repeating as a cycle.
Julia has type stability for fast compilation and execution. When writing functions, it’s important to ensure that the type of the return value can be determined without ambiguity.
Example of type instability:
function f(x)
if x > 0
return 1
else
return 0.0
end
end
println("The value 2 of type ", typeof( 2), " produces an output of type ", typeof(f( 2)))
println("The value -2 of type ", typeof(-2), " produces an output of type ", typeof(f(-2)))
The value 2 of type Int64 produces an output of type Int64
The value -2 of type Int64 produces an output of type Float64
Julia is dynamically typed, but ensuring type stability within functions helps the compiler optimize code for better performance.
For better performance, always try to ensure type stability in your functions. This can be achieved by making the return type predictable, from the types of input variables and not their values.
We propose a first exercise about simple linear regression. The data are excerpted from this example and saved into data.csv. We propose an ordinary least squares formulation which is a type of linear least squares method for choosing the unknown parameters in a linear regression model by the principle of least squares: minimizing the sum of the squares of the differences between the observed dependent variable (values of the variable being observed) in the input dataset and the output of the (linear) function of the independent variable.
Given a set of m data points y_{1}, y_{2}, \dots, y_{m}, consisting of experimentally measured values taken at m values x_{1}, x_{2}, \dots, x_{m} of an independent variable (x_i may be scalar or vector quantities), and given a model function y=f(x,\beta), with \beta =(\beta_{1},\beta_{2},\dots ,\beta_{n}), it is desired to find the parameters \beta_j such that the model function “best” fits the data. In linear least squares, linearity is meant to be with respect to parameters \beta_j, so f(x, \beta) = \sum_{j=1}^n \beta_j\, \varphi_j(x). In general, the functions \varphi_j may be nonlinear. However, we consider linear regression, that is f(x, \beta) = \beta_1 + \beta_2 x. Ideally, the model function fits the data exactly, so y_i = f(x_i, \beta) for all i=1, 2, \dots, m. This is usually not possible in practice, as there are more data points than there are parameters to be determined. The approach chosen then is to find the minimal possible value of the sum of squares of the residuals r_i(\beta) = y_i - f(x_i, \beta), \quad i=1, 2, \dots, m so to minimize the function S(\beta) = \sum_{i=1}^m r_i^2(\beta). In the linear least squares case, the residuals are of the form r(\beta) = y - X\, \beta with y = (y_i)_{1\le i\le m} \in \mathbb{R}^m and X = (X_{ij})_{1\le i\le m, 1\le j\le n} \in \mathrm{M}_{mn}(\mathbb{R}), where X_{ij} = \varphi_j(x_i). Since we consider linear regression, the i-th row of the matrix X is given by X_{i[:]} = [1 \quad x_i]. The objective function may be written S(\beta) = {\Vert y - X\, \beta \Vert}^2 where the norm is the usual 2-norm. The solution to the linear least squares problem \underset{\beta \in \mathbb{R}^n}{\mathrm{minimize}}\, {\Vert y - X\, \beta \Vert}^2 is computed by solving the normal equation X^\top X\, \beta = X^\top y, where X^\top denotes the transpose of X.
To answer the questions you need to import the following packages.
You also need to download the csv file. Click on the following image.
DataFrames.jl
and CSV.jl
, load the dataset from data/introduction/data.csv and save the result into a variable named dataset
.Row | Time | Mass |
---|---|---|
Int64 | Int64 | |
1 | 5 | 40 |
2 | 7 | 120 |
3 | 12 | 180 |
4 | 16 | 210 |
5 | 20 | 240 |
Do not hesitate to visit the documentation of CSV.jl
and DataFrames.jl
.
Plot.jl
, plot the data.Use names(dataset)
to get the list of data names. If Time
is a name you can access to the associated data by dataset.Time
.
Base.\
.Use ones(m)
to generate a vector of 1 of length m.
2-element Vector{Float64}:
11.506493506493449
12.207792207792208
plot!
function. See the basic concepts for plotting.