Julia: Tutorial & Code-Collection¶

Source: https://github.com/markomlikota/CodingSoftware

MIT License, © Marko Mlikota, https://markomlikota.github.io

Futher Object-Types¶

Knowing the object-types we discussed so far suffices for most (standard) tasks.

However, there are a few more object-types in Julia worth knowing about.

In [1]:
# Let's install the new packages needed in this section:
Pkg.add("DataFrames")
   Resolving package versions...
   Installed InvertedIndices ───────────── v1.3.0
   Installed DataValueInterfaces ───────── v1.0.0
   Installed SentinelArrays ────────────── v1.4.1
   Installed TableTraits ───────────────── v1.0.1
   Installed PooledArrays ──────────────── v1.4.3
   Installed InlineStrings ─────────────── v1.4.0
   Installed Crayons ───────────────────── v4.1.1
   Installed Tables ────────────────────── v1.11.1
   Installed DataFrames ────────────────── v1.6.1
   Installed IteratorInterfaceExtensions ─ v1.0.0
   Installed StringManipulation ────────── v0.3.4
   Installed PrettyTables ──────────────── v2.3.1
    Updating `/opt/conda/julia/environments/v1.10/Project.toml`
  [a93c6f00] + DataFrames v1.6.1
    Updating `/opt/conda/julia/environments/v1.10/Manifest.toml`
  [a8cc5b0e] + Crayons v4.1.1
  [a93c6f00] + DataFrames v1.6.1
  [e2d170a0] + DataValueInterfaces v1.0.0
  [842dd82b] + InlineStrings v1.4.0
  [41ab1584] + InvertedIndices v1.3.0
  [82899510] + IteratorInterfaceExtensions v1.0.0
  [2dfb63ee] + PooledArrays v1.4.3
  [08abe8d2] + PrettyTables v2.3.1
  [91c51154] + SentinelArrays v1.4.1
  [892a3eda] + StringManipulation v0.3.4
  [3783bdb8] + TableTraits v1.0.1
  [bd369af6] + Tables v1.11.1
Precompiling project...
  ✓ IteratorInterfaceExtensions
  ✓ DataValueInterfaces
  ✓ InvertedIndices
  ✓ PooledArrays
  ✓ TableTraits
  ✓ InlineStrings
  ✓ SentinelArrays
  ✓ Crayons
  ✓ Tables
  ✓ StringManipulation
  ✓ CategoricalArrays → CategoricalArraysSentinelArraysExt
  ✓ PrettyTables
  ✓ DataFrames
  ✓ Latexify → DataFramesExt
  14 dependencies successfully precompiled in 45 seconds. 193 already precompiled. 1 skipped during auto due to previous errors.

Expressions & Symbols¶

In [2]:
# Julia has object types "Symbol" and "Expr" (expression),
# whereby symbols are essentially building blocks of expressions.
# They allow us to use strings to execute commands and, more generally,
# to define and call objects.
In [3]:
# Construction and evaluation of expressions:


# Simple construction of expressions:
exA = :((4+5)/2)
# or:
exA = Meta.parse("(4+5)/2")

# Construction of longer expressions:
exB = quote
    x = 4
    y = 5
    (x+y)/2
end

# It is of type "Expr":
typeof(exA)

# Evaluate expression:
eval(exA)
Out[3]:
4.5
In [4]:
# We can let our expressions depend on objects we create.
# This can be done in two ways.
# When we write the object name with a dollar sign in front,
# we include the current value of that object in our expression.
# When we write the object name without a dollar sign in front,
# we link our expression to them so that the object's value 
# at the time of execution matters.

a = 3
b = 8 
exA = :($a + b)  # or:
exA = Meta.parse("$a + b")

println("The expression 'exA' reads: ", exA)
# (value of b doesn't matter; b doesn't even need to exist when defining exA)

a = 2
b = 7
eval(exA)
The expression 'exA' reads: 3 + b
Out[4]:
10
In [5]:
# Using expressions, we can define objects:

exA = Meta.parse("x_4 = 3.74")
eval(exA)

# Sidenote: to define strings, need to use triple quotation marks
exB = Meta.parse(""" s_4 = "3.74" """)
eval(exB)

# This is useful if e.g. we create many variables x_1, x_2, x_3, ... in a loop;
# we would type something like
ii = 4 # this is the iterator in the loop
value = 3.74
Meta.parse(string("x_",ii," = ",value))
Out[5]:
:(x_4 = 3.74)
In [6]:
# A symbol allows us to call objects using strings:

sym1 = Symbol("x","_",4)

typeof(sym1)

eval(sym1)

# This is useful if we call many variables x_1, x_2, ... in a loop;
# we'd type something like
eval(Symbol("x","_",ii))


# Such "dynamic" object-creation and -calling is referred to as "metaprogramming".
Out[6]:
3.74

Tuples¶

In [7]:
# A tuple (like a vector) is an ordered collection of elements (of any type)
# that (unlike a vector) cannot be changed once they are defined.
# Using tuples rather than vectors for immutable collections of elements
# tends to lead to more efficient code.

ta = ("hello", 2025, true, 2, 5//8)

# We can turn a vector into a tuple:
vx = ["hello",2025]
tuple(vx...)

# A tuple is indexed just like a vector:
ta[2]
ta[end]
ta[2:4]

# We cannot change its elements ...:
ta[2] = 2024

# ... but we can overwrite the object:
ta = (2,3,4)

# We can apply functions to a tuple:
sqrt.(ta) 

# We can also create a vector of tuples:
vt = Vector{Tuple}(undef,1)
vt[1] = ("hello", 2025)
push!(vt,("hi", 2024))
MethodError: no method matching setindex!(::Tuple{String, Int64, Bool, Int64, Rational{Int64}}, ::Int64, ::Int64)

Stacktrace:
 [1] top-level scope
   @ In[7]:18

Dataframes¶

In [8]:
# Dataframes are essentially like matrices with named columns/rows.
# There are many pre-defined commands to manipulate them (see package documentation).
# I find them useful for reading from and writing to csv or excel files 
# as well as for printing information as nice tables in the REPL.
# (By habit, I stick to dataframes in R for raw data manipulation.)

using DataFrames

# Create an empty data frame:
dfData1 = DataFrame(Stat = String[], b_ = Float64[], k_ = Float64[])

# Add a row at the end:
push!(dfData1, ("Mean",0.67,1.34)) 

# Add (squeeze in) a row at position 1:
insert!(dfData1, 1, ("Median",0.65,1.33))

# Add a column called "Periods":
dfData1[!, "Periods"] = ["period1","period1"]
Out[8]:
2-element Vector{String}:
 "period1"
 "period1"
In [9]:
# To access elements, index dataframe just like a matrix: e.g.
dfData1[1,2:3]

# Access column names:
names(dfData1)

# Rename columns:
rename!(dfData1, names(dfData1) .=> Symbol.(["stats","b_", "k_","t"]))
Out[9]:
2×4 DataFrame
Rowstatsb_k_t
StringFloat64Float64String
1Median0.651.33period1
2Mean0.671.34period1
In [10]:
# Convert dataframe to matrix:

Matrix{Float64}(dfData1[:,2:3])
Out[10]:
2×2 Matrix{Float64}:
 0.65  1.33
 0.67  1.34
In [11]:
# Convert matrix to dataframe:

mData = [2.0 3 4; 3 4 5]

dfData = DataFrame(mData, :auto)
# with labelled columns:
vLabels = ["a","b","c"]
dfData = DataFrame(mData, Symbol.(vLabels))
Out[11]:
2×3 DataFrame
Rowabc
Float64Float64Float64
12.03.04.0
23.04.05.0

Custom Types (Structures)¶

In [12]:
# We can also define own object-types, so-called "structures" or "structs".
# For example:

struct mystruct3
    year::Int
    quarter::Int
    region::String
    consumption::Float64
    gdp::Float64
end

# Just like when defining functions, typing "::" and the type is not necessary;
# the struct will default to type "Any". 

# By default, a struct is immutable. 
# To define a mutable struct, type "mutable struct mystruct3".
In [13]:
# Create an object of this type:
strCountry1 = mystruct3(2025, 4, "Europe", 0.74, 0.89)

# Access the field "gdp" of this object:
strCountry1.gdp

# Remind yourself which fields are available:
fieldnames(mystruct3)
Out[13]:
(:year, :quarter, :region, :consumption, :gdp)
In [14]:
# Create a 10x1 vector with elements of type "mystruct3":
vStrCountries = Vector{mystruct3}(undef,10) 

# Fill in first element:
vStrCountries[1] = mystruct3(2025, 4, "Europe", 0.74, 0.89)
Out[14]:
mystruct3(2025, 4, "Europe", 0.74, 0.89)

Try Exercises 8.a.i - 8.a.iv