Julia Exercises Solutions

Author

Haden Bunn

1 Julia Exercises

This document contains exercises that are intended to reinforce the concepts presented in Module 1. Each exercise should take <5 minutes to complete and there may be more than one way to approach each problem.

1.1 Exercise 1

  • Create a variable x and assign it a value of 1.5 as a String.
x = "1.5"
"1.5"
  • Convert the value of x to a numeric type that will preserve the decimal value, then assign the result to a variable y.
y = parse(Float64, x)
1.5
  • Create another variable z and assign it the numeric value 1.5.
z = 1.5
1.5
  • Is the value of y equal to z?
y == z
true
  • Is the value of y identical to z?
y === z
true

1.2 Exercise 2

  • How many operations are included in the logical expression: false || true && true && false || true.
4
4
  • Predict the return value of the expression if it were entered into the REPL.
true
true
  • Write out the individual operations in the order they would be evaluated.
true && true
true && false
false || false
false || true
true
  • Modify the original expression to obtain the opposite result (e.g., false -> true) by adding ≤3 characters.
false || true && true && false || !true
!(false || true && true && false || true)
false

1.3 Exercise 3

  • What are the 4 basic (primitive) data types in Julia?
1::Int
1.0::Float64
'c'::Char
true::Bool
true
  • Write two expressions that confirm 1.0 is both a Number and Float64 by returning a boolean value of true.
1.0 isa Number
1.0 isa Float64
true
  • Explain why 1.0 is both a Number and a Float64.

    • 1.0 is, by definition, a floating point number.
    • Float64 is a subtype of the AbstractType, Number, thus, 1.0 is also a Number.
  • Explain the difference between Missing and missing.

    • Missing is a type used to represent missing values.
    • missing is an instance of the type Missing.
  • Write an expression that evaluates whether nothing and missing are equivalent.

isequal(nothing, missing)
false

1.4 Exercise 4

  • The expression below creates a string of 1000 characters randomly sampled with replacement from the range “A-Z”.
s = join(rand('A':'Z', 1000))
"KCJQXYCXPDERBAXSPZMCXBPDDCQRUZTIMJNAKKWSKGYKGUYPBRAXYKNHEEAOAJZULHOGHJAKMHFPSRTXZBZNHSCHWMSVCFZCMSKRUYCLSIXXINPDUDEKCAUQVGVLJHYRHRBJZIBILQGDMWWHRYJKPIYRVGULFJRVLGVJTUQQOROTZJVSXSVNJYVEBZNLIFVJSJPCYVJFXPNBOAGKBVRHZLPEMPXRIHRATOWLEGGHIKOXOVSYDGLRSPQCBONRSZBLWPLADWBANVFLBJS" ⋯ 459 bytes ⋯ "WUCOXRUUDLZJWMYUCKFHOIJGLOJIKLPDXIFQLPWEXYTNPYKJHIJVOWWWMJIBACSSTZEQLBECDJIWKWTAUKFSKJZMRCHSQFBWEDFQRUDCOFXZGRIWTLRGOZNKSCPSMSGXBFLALARMNVTEAPZTDUFEUYVDAIDQBLJOIXVFRUKRBCHRCFQASKEJQLPDNHHJUGCIZWPIHUWXHJAYAAXHTMSWDEIUXDATLKPTPFUDBSGPSRHZFQSNVBGJHQXHNTEEDFUGTJTGAWYHKVOSYZ"
  • Find the index of the first 'A' in s. If the search returns nothing, choose a different character until the function returns a valid index. If changed, be sure to use the new character instead of 'A' and 'a' (e.g., 'B', 'b') for the remaining prompts in this exercise.
findfirst('A', s)
14
  • Modify the search so that it is case-insensitive, then test it using a lowercase 'a'.
findfirst('a', lowercase(s))
14
  • Find the index for the last 'A' in s.
findlast('A', s)
991
  • Write an expression that returns the characters in s that occur between the first and last indices identified above.
chop(s, head = findfirst('A', s), tail = 1000 - findlast('A', s))
"XSPZMCXBPDDCQRUZTIMJNAKKWSKGYKGUYPBRAXYKNHEEAOAJZULHOGHJAKMHFPSRTXZBZNHSCHWMSVCFZCMSKRUYCLSIXXINPDUDEKCAUQVGVLJHYRHRBJZIBILQGDMWWHRYJKPIYRVGULFJRVLGVJTUQQOROTZJVSXSVNJYVEBZNLIFVJSJPCYVJFXPNBOAGKBVRHZLPEMPXRIHRATOWLEGGHIKOXOVSYDGLRSPQCBONRSZBLWPLADWBANVFLBJSWVGCDDGWPLCSKP" ⋯ 436 bytes ⋯ "EULLRRNMLWUCOXRUUDLZJWMYUCKFHOIJGLOJIKLPDXIFQLPWEXYTNPYKJHIJVOWWWMJIBACSSTZEQLBECDJIWKWTAUKFSKJZMRCHSQFBWEDFQRUDCOFXZGRIWTLRGOZNKSCPSMSGXBFLALARMNVTEAPZTDUFEUYVDAIDQBLJOIXVFRUKRBCHRCFQASKEJQLPDNHHJUGCIZWPIHUWXHJAYAAXHTMSWDEIUXDATLKPTPFUDBSGPSRHZFQSNVBGJHQXHNTEEDFUGTJTGA"
  • Write an expression that checks whether s contains a string "AA".
occursin("AA", s)
true

1.5 Exercise 5

  • Which of Julia’s four basic data structures (Tuple, NamedTuple, Dictionary, and Array) are mutable? Which are indexable?

    • Arrays and dictionaries are mutable.
    • Tuples, named tuples, and arrays are indexable.
  • Create a Dictionary, d1, with Integer keys 1, 2, 3, corresponding to values: "my string", 99, and a tuple ("x", "y", "z").

d1 = Dict(1 => "my string", 2 => 99, 3 => ("x", "y", "z"))
Dict{Int64, Any} with 3 entries:
  2 => 99
  3 => ("x", "y", "z")
  1 => "my string"
  • Unpack the second and third elements of the tuple in d1 into two variables, a and b, then use those variables to create a NamedTuple, ntp.
_, a, b = d1[3]
ntp = (; a, b)
(a = "y", b = "z")
  • Change the value of ntp.b to 5; make sure the update is reflected in ntp.
ntp = merge(ntp, (; b = 5))
(a = "y", b = 5)
  • Create another Dictionary, d2, from ntp where each key is of type Symbol and each value is of type Any.
d2 = Dict{Symbol,Any}(pairs(ntp))
Dict{Symbol, Any} with 2 entries:
  :a => "y"
  :b => 5
  • Write an expression that checks if key c exists in d2 where c::Symbol; the expression should return a boolean value.
haskey(d2, :c)
false
  • Add a key, c, to d2, assign it a tuple containing a single value, 1, then verify the type of value stored in c using the appropriate function.
d2[:c] = (1,)
typeof(d2[:c])
Tuple{Int64}

1.6 Exercise 6

  • Create a 3x3 matrix, X with values ranging from 100 to 900.
X = collect(reshape(100:100:900, 3, 3))
3×3 Matrix{Int64}:
 100  400  700
 200  500  800
 300  600  900
  • There are at least 4 methods of indexing into X that will return a scalar value of 100; list 4 of them below.
X[begin]
X[1]
X[1, 1]
X[X.==100][1]
100
  • Retrieve all values in the first row of X as both a column vector and a row vector.
X[1, :]
X[[1], :]
1×3 Matrix{Int64}:
 100  400  700
  • Replace the values in the second row of X with the values 2, 5, 8 (if you get an error here, read it carefully and apply the suggested fix).
X .= 0
zeros(Int, 3, 3) == X
true
  • Modify the elements of X so that the expression below evaluates true.
# Answer here
zeros(Int, 3, 3) == X # check after modifying X
true
  • Consider the 4x2 matrix below, recreate this matrix by vertically concatenating 4 separate arrays.
# 5  10
# 15 20
# 25 30
# 35 40

[
    [5 10]
    [15 20]
    [25 30]
    [35 40]
]
4×2 Matrix{Int64}:
  5  10
 15  20
 25  30
 35  40
  • Recreate the same 4x2 matrix above by horizontally concatenating 2 Ranges.
[5:10:35 10:10:40]
4×2 Matrix{Int64}:
  5  10
 15  20
 25  30
 35  40

1.7 Exercise 7

  • Create a variable, x and assign it a value missing. Write a simple if statement that will print the value of x to the REPL if it is missing.
x = missing

if ismissing(x)
    println("x = $x")
end
x = missing
  • Rewrite the simple if statement above using a short-circuiting logical operator.
ismissing(x) && println("x = $x")
!ismissing(x) || println("x = $x")
x = missing
x = missing
  • Consider the if statement below. Rewrite the expression using the ternary operator (<cond> ? <true statement> : <false statement>).
#=
if a < 5
    println("$a < 5")
else
    println("$a ≥ 10")
end
=#

a = 10
a < 5 ? println("$a < 5") : println("$a ≥ 10")
10 ≥ 10
  • The if-else statement above was changed to an if-elseif statement. Rewrite the expression using the ternary operator.
#=
if a < 5
    println("$a < 5")
elseif a < 10
    println("$a < 10")
else
    println("$a ≥ 10")
end
=#

a = 10
a < 5 ? println("$a < 5") : a < 10 ? println("$a < 10") : println("$a ≥ 10")
10 ≥ 10

1.8 Exercise 8

  • Create an empty vector, v, of Int64 values and a counter, i = 0. Using a while loop, increase the value of i by 2 until i > 20 and store the result of each iteration in v.
v = Int64[]
i = 0

while i  20
    global i += 2
    push!(v, i)
end
  • Create an empty Dictionary, d, then use a for loop to populate it with the keys and values of the NamedTuple, ntp, below.
ntp = (; a = 100, b = "mystring", c = false)
d = Dict()

for (k, v) in zip(keys(ntp), values(ntp))
    d[k] = v
end
  • Consider the comprehension below. In a few words, explain what is happening inside the comprehension.

    • The anonymous function x^2+1 is being applied to each element in the range 1:20 to create a column vector, A, with 20 elements.
A = [x^2 + 1 for x = 1:20]
20-element Vector{Int64}:
   2
   5
  10
  17
  26
  37
  50
  65
  82
 101
 122
 145
 170
 197
 226
 257
 290
 325
 362
 401
  • Create an uninitialized 4x5 Array, B, of Int64 values, then use a nested for loop to fill B, row-wise, with the elements of A.
B = Array{Int64}(undef, 4, 5)

n = 0
for i = 1:4, j = 1:5
    n += 1
    global B[i, j] = A[n]
end
  • Repeat the step above, but this time, fill B column-wise with the elements of A.
n = 0
for j = 1:5, i = 1:4
    n += 1
    global B[i, j] = A[n]
end
  • Create a third 4x5 Array, C, that is type Int64 and contains only zeros. Use a single (non-nested) for loop to fill C, column-wise with the elements of A.
C = zeros(Int64, 4, 5)
for i in eachindex(A)
    global C[i] = A[i]
end
  • Perform an element-wise comparison of the values in B and C to check for equality; how would you interpret the resulting BitMatrix?
B .== C
4×5 BitMatrix:
 1  1  1  1  1
 1  1  1  1  1
 1  1  1  1  1
 1  1  1  1  1

1.9 Exercise 9

Creatinine Clearance

The Cockcroft-Gault formula provides an estimate of glomerular filtration rate (GFR) by calculating serum creatinine clearance (CrCL). While imperfect, CrCL is the standard metric used to guide dose adjustments for renal impairment.

The goal of this exercise is to implement the CG formula as a function and apply it across a range of values. A commonly used version of the formula is given below.

CrCL (mL/min) = (140-age)/scr * tbw/72 * 0.85^isfemale

Where:

  • age (years)
  • scr (serum creatinine, mg/dL)
  • tbw (total body weight, kg)
  • isfemale (female sex, true/false)
  • Create a function, crcl1, using inline assignment and positional arguments.
crcl1(age, scr, tbw, isfemale) = (140 - age) / scr * tbw / 72 * 0.85^isfemale
crcl1 (generic function with 1 method)
  • Test crcl1 for a 41 y/o male patient weighing 95.6 kg with a SCr of 1.2 mg/dL
crcl1(41, 1.2, 95.6, false)
109.54166666666666
  • Test crcl1 for the same patient, but pass in, "41", for age. Read the error message carefully; if you did not know the cause up front, could you interpret the message and identify the problem?
crcl1("41", 1.2, 95.6, false)
MethodError: no method matching -(::Int64, ::String)

Closest candidates are:
  -(::Real, ::Complex{Bool})
   @ Base complex.jl:321
  -(::Integer, ::Rational)
   @ Base rational.jl:350
  -(::Union{Int16, Int32, Int64, Int8}, ::BigInt)
   @ Base gmp.jl:558
  ...

Stacktrace:
 [1] crcl1(age::String, scr::Float64, tbw::Float64, isfemale::Bool)
   @ Main.Notebook ~/run/_work/PumasTutorials.jl/PumasTutorials.jl/tutorials/LearningPaths/01-LP/01-Module/mod1-exercises-solutions.qmd:482
 [2] top-level scope
   @ ~/run/_work/PumasTutorials.jl/PumasTutorials.jl/tutorials/LearningPaths/01-LP/01-Module/mod1-exercises-solutions.qmd:497
  • Test crcl1 for the same patient, but pass in missing for isfemale. Did you expect an error message, or a missing return value? Why?
crcl1(41, 1.2, 95.6, missing)
missing
  • Create a function, crcl2, using the function keyword and kwargs. Set the default value for isfemale to false. Before calculating CrCL, the function should check whether the value of each continuous variable is a Number; if not, it should return a missing value. (hint: there are several ways to write the check; consider reviewing, ?isa and ?all)
function crcl2(; age, scr, tbw, isfemale = false)

    all(x -> isa(x, Number), [age, scr, tbw]) || return missing

    return (140 - age) / scr * tbw / 72 * 0.85^isfemale

end
crcl2 (generic function with 1 method)
  • Test crcl2 for the same patient, making sure to test the “normal” case, then a case where age, scr, or tbw is not a Number.
crcl2(; age = 41, scr = 1.2, tbw = 95.6)
crcl2(; age = missing, scr = 1.2, tbw = 95.6)
missing
  • Create a function crcl3 using kwargs and type assertion for each argument. Set the default value for isfemale to false.
function crcl3(; age::Real, scr::Real, tbw::Real, isfemale::Bool = false)
    return (140 - age) / scr * tbw / 72 * 0.85^isfemale
end
crcl3 (generic function with 1 method)
  • Test crcl3 for the same patient using both a “normal” and “error” case.
crcl3(; age = 41, scr = 1.2, tbw = 95.6)
crcl3(; age = missing, scr = 1.2, tbw = 95.6)
TypeError: in keyword argument age, expected Real, got a value of type Missing
Stacktrace:
 [1] top-level scope
   @ ~/run/_work/PumasTutorials.jl/PumasTutorials.jl/tutorials/LearningPaths/01-LP/01-Module/mod1-exercises-solutions.qmd:544
Error-handling

Note: It is good practice to include basic error-handling in your code, but the way you define “basic” will evolve as you become more comfortable with Julia. Simply returning a missing value for common errors as in crcl2, is a valid approach, especially if you’re working alone and your code is well-documented. In larger projects, type assertion (e.g., crcl3) and more advanced error-handling workflows (e.g., try-catch, throw, @assert) become more important. When starting out, you can worry less about error handling since the code you write will naturally become more elegant (and less error-prone) over time.

  • Use a comprehension and crcl3 to calculate CrCL across a range of scr values (0.5:0.05:1.25) for a 30 y/o female weighing 145 lbs.
[crcl3(; age = 30, scr, tbw = 145 / 2.2, isfemale = true) for scr = 0.5:0.05:1.25]
16-element Vector{Float64}:
 171.18055555555554
 155.61868686868684
 142.65046296296296
 131.6773504273504
 122.27182539682539
 114.12037037037035
 106.98784722222221
 100.69444444444443
  95.1003086419753
  90.09502923976609
  85.59027777777777
  81.51455026455027
  77.80934343434342
  74.42632850241546
  71.32523148148148
  68.47222222222223
  • Explore the code below, then try to articulate what is happening with each component. If you encounter a function or symbol that is unfamiliar, check the help menu, ?. The goal here is to provide a few examples for generating and manipulating data based up on the techniques outlined so far.

    • The Distributions package is included in the Pumas application and provides access to the Normal and Bernoulli distributions.
    • patients is an empty Dictionary that will be used to store information about each individual patient once it’s created in the for loop.
    • The for loop will create 10 patients based upon the length of range 1:10.
    • Characteristics like age and tbw are randomly sampled from a distribution (uniform, normal, bernoulli).
    • input is a NamedTuple that is used to store patient characteristics for use elsewhere in the loop.
    • output is the result of the crcl3 function; recall that crcl3 accepts 4 kwargs corresponding the keys in input. The splatting operator ... “opens” input and matches its keys to the appropriate kwarg.
    • The final expression inside the for loop creates a key i in patients; the value is a NamedTuple of the input and output for crcl3.
    • patients[1] is accessing the first patient’s data; note this is not indexing, the keys in patients are integers.
    • _in is a vector of NamedTuples, each corresponding to an input. The underscore _ in _in is common style convention denoting a “temporary” or “intermediate” variable.
    • _weights is a vector of individual weights.
    • The mean expression uses a generator to calculate mean body weight.
# load package needed for Normal, Bernoulli distributions
using Distributions

# a data structure for storing "patients"
patients = Dict()

# a loop to create 10 patients with randomly sampled characteristics 
for i = 1:10
    # random input
    age = rand(19:92)
    scr = rand(Normal(0.9, 0.2))
    tbw = rand(Normal(72, 20))
    isfemale = rand(Bernoulli())

    # save the input in a data structure for later
    input = (; age, scr, tbw, isfemale)

    # generate output
    output = crcl3(; input...)

    # save both as a entry in patients with a corresponding key
    patients[i] = (; input, output)
end

patients[1]     # key, *not* index

_in = [v.input for v in values(patients)]

_weights = [i.tbw for i in _in]

mean((i.tbw for i in _in))
75.52446003937078

1.10 Exercise 10

  • Consider the attempt at defining two methods for a function, m, below. Execute the code in the begin block and then programmatically check the number of methods associated with m.
begin
    m(; x::Real) = x + 3
    m(; x::AbstractString) = parse(Float64, x) + 3
end

methods(m)
# 1 method for generic function m from Main.Notebook:
  • What happens if you execute the call below? Why?

    • Methods are created when the same function is defined using a different number or type of positional arguments.
    • m has no positional arguments, so the second definition simply overwrites the first, since the “number” of positional argument remains zero.
    • The MethodError occurred because m was expecting an argument of type AbstractString.
m(x = 3)
TypeError: in keyword argument x, expected AbstractString, got a value of type Int64
Stacktrace:
 [1] top-level scope
   @ ~/run/_work/PumasTutorials.jl/PumasTutorials.jl/tutorials/LearningPaths/01-LP/01-Module/mod1-exercises-solutions.qmd:654
  • Redefine m to create the two intended methods above. How many total methods will m have? Why? What happens if you try to set m = nothing? Why?

    • m will have 3 methods, the first was defined above with a single kwarg, x.
    • The two remaining methods have a single positional argument, y; one method accepts Real values and the other accepts AbstractStrings
m(y::Real) = y + 3
m(y::AbstractString) = parse(Float64, y) + 3
m (generic function with 3 methods)
  • Define a vector, x = [1,2,3]. Then, define a function that adds 3 to each element of x and returns the sum of its elements. The function should modify x in-place and follow the style convention for mutating functions.
x = [1, 2, 3]
function add3!(y)
    y .+= 3
    return sum(y)
end
add3! (generic function with 1 method)
  • Define a second version of your function above that performs the same operations without modifying the input outside of the function.
function add3(y)
    y = y .+ 3
    return sum(y)
end
add3 (generic function with 1 method)