using Plots
plotly();9 Function transformations
In this section we will use this add-on package:
Thinking of functions as objects themselves that can be manipulated—rather than just blackboxes for evaluation—is a major abstraction of calculus. The main operations to come: the limit of a function, the derivative of a function, and the integral of a function all operate on functions. Hence the idea of an operator. Here we discuss manipulations of functions from pre-calculus that have proven to be useful abstractions.
9.1 The algebra of functions
We can talk about the algebra of functions. For example, the sum of functions \(f\) and \(g\) would be a function whose value at \(x\) was just \(f(x) + g(x)\). More formally, we would have:
\[ (f + g)(x) = f(x) + g(x), \]
We have given meaning to a new function \(f+g\) by defining what is does to \(x\) with the rule on the right hand side. Similarly, we can define operations for subtraction, multiplication, addition, and powers.
These mathematical concepts aren’t defined for function instances in base Julia. For example, a common mistake in computing \(\sin^2(1)\) would be to try that syntax exactly:
sin^2(1)MethodError: no method matching ^(::typeof(sin), ::Int64) The function `^` exists, but no method is defined for this combination of argument types. Closest candidates are: ^(::Irrational{:ℯ}, ::Integer) @ Base mathconstants.jl:139 ^(::Irrational{:ℯ}, ::Number) @ Base mathconstants.jl:139 ^(::BigFloat, ::Union{Int16, Int32, Int64, Int8}) @ Base mpfr.jl:797 ... Stacktrace: [1] top-level scope @ ~/julia/CalculusWithJuliaNotes/quarto/precalc/transformations.qmd:37
The error message might seem cryptic but it comes from there not being a power operation (^) for sin defined. Similarly, out of the box there are not operations for +, -, *, or / defined for functions.1
To define such a function, we would specify what it does to each value of x, along the lines of:
fplusg(x) = f(x) + g(x)fplusg (generic function with 1 method)
9.1.1 Composition of functions
As seen, just like with numbers, it can make sense mathematically to define addition, subtraction, multiplication and division of functions. Unlike numbers though, we can also define a new operation on functions called composition that involves chaining the output of one function to the input of another. Composition is a common practice in life, where the result of some act is fed into another process. For example, making a pie from scratch involves first making a crust, then composing this with a filling. A better abstraction might be how we “surf” the web. The output of one search leads us to another search whose output then is a composition.
Mathematically, a composition of univariate functions \(f\) and \(g\) is written \(f \circ g\) and defined by what it does to a value in the domain of \(g\) by:
\[ (f \circ g)(x) = f(g(x)). \]
The output of \(g\) becomes the input of \(f\).
Composition depends on the order of things. There is no guarantee that \(f \circ g\) should be the same as \(g \circ f\). (Putting on socks then shoes is quite different from putting on shoes then socks.) Mathematically, we can see this quite clearly with the functions \(f(x) = x^2\) and \(g(x) = \sin(x)\). Algebraically we have:
\[ (f \circ g)(x) = \sin(x)^2, \quad (g \circ f)(x) = \sin(x^2). \]
Though they may be typographically similar don’t be fooled, the following graph shows that the two functions aren’t even close except for \(x\) near \(0\) (for example, one composition is always non-negative, whereas the other is not):
f(x) = x^2
g(x) = sin(x)
fg(x) = f(g(x))
gf(x) = g(f(x))
plot(fg, -2, 2, label="f∘g")
plot!(gf, label="g∘f")Starting with two functions and composing them requires nothing more than a solid grasp of knowing the rules of function evaluation. If \(f(x)\) is defined by some rule involving \(x\), then \(f(g(x))\) just replaces each \(x\) in the rule with a \(g(x)\).
Composition of two functions does have an infix operator, ∘, entered as \circ[tab]. This mirrors the mathematical usage of this syntax, though the order of operations are such that calling the composed function on a value requires an extra set of parentheses: (f∘g)(x), as the expression f∘g(x) evaluates g(x) before the composition.
So if \(f(x) = x^2 + 2x - 1\) and \(g(x) = e^x - x\) then \(f \circ g\) would be (before any simplification)
\[ (f \circ g)(x) = (e^x - x)^2 + 2(e^x - x) - 1. \]
It can be helpful to think of the argument to \(f\) as a “box” that gets filled in by \(g(x)\):
\[ \begin{align*} g(x) &=e^x - x\\ f(\square) &= (\square)^2 + 2(\square) - 1\\ f(g(x)) &= (g(x))^2 + 2(g(x)) - 1 = (e^x - x)^2 + 2(e^x - x) - 1. \end{align*} \]
Here we look at a few compositions:
The function \(h(x) = \sqrt{1 - x^2}\) can be seen as \(f\circ g\) with \(f(x) = \sqrt{x}\) and \(g(x) = 1-x^2\).
The function \(h(x) = \sin(x/3 + x^2)\) can be viewed as \(f\circ g\) with \(f(x) = \sin(x)\) and \(g(x) = x/3 + x^2\).
The function \(h(x) = e^{-1/2 \cdot x^2}\) can be viewed as \(f\circ g\) with \(f(x) = e^{-x}\) and \(g(x) = (1/2) \cdot x^2\).
Decomposing a function into a composition of functions is not unique, other compositions could have been given above. For example, the last function is also \(f(x) = e^{-x/2}\) composed with \(g(x) = x^2\).
The real value of composition is to break down more complicated things into a sequence of easier steps. This is good mathematics, but also good practice more generally. For example, when we approach a problem with the computer, we generally use a smallish set of functions and piece them together (that is, compose them) to find a solution.
9.1.2 Shifting and scaling graphs
It is very useful to mentally categorize functions within families. The difference between \(f(x) = \cos(x)\) and \(g(x) = 12\cos(2(x - \pi/4))\) is not that much—both are cosine functions, one is just a simple enough transformation of the other. As such, we expect bounded, oscillatory behaviour with the details of how large and how fast the oscillations are to depend on the specifics of the function. Similarly, both these functions \(f(x) = 2^x\) and \(g(x)=e^x\) behave like exponential growth, the difference being only in the rate of growth. There are families of functions that are qualitatively similar, but quantitatively different, linked together by a few basic transformations.
There is a set of operations of functions, which does not really change the type of function. Rather, it basically moves and stretches how the functions are graphed. We discuss the four main transformations of \(f\) from Table 9.1.
| Transformation | Description |
|---|---|
| vertical shifts | The function \(h(x) = k + f(x)\) will have the same graph as \(f\) shifted up \(k\) units. |
| horizontal shifts | The function \(h(x) = f(x - k)\) will have the same graph as \(f\) shifted over right by \(k\) units. |
| stretching | The function \(h(x) = kf(x)\) will have the same graph as \(f\) stretched by a factor \(k\) of in the \(y\) direction. |
| scaling | The function \(h(x) = f(kx)\) will have the same graph as \(f\) scaled horizontally by a factor of \(1\) over \(k\) |
The functions \(h\) are derived from \(f\) in a predictable way. To implement these transformations within Julia, we define operators (functions which transform one function into another). As these return functions, the function bodies are anonymous functions. The basic definitions are similar, save for the x -> ... part that signals the creation of an anonymous function to return:
up(f, k) = x -> f(x) + k
over(f, k) = x -> f(x - k)
stretch(f, k) = x -> k * f(x)
scale(f, k) = x -> f(k * x)scale (generic function with 1 method)
To illustrate, let’s define a hat-shaped function as follows:
f(x) = max(0, 1 - abs(x))f (generic function with 1 method)
A plot over the interval \([-2,2]\) is shown in Figure 9.2:
plot(f, -2, 2; aspect_ratio=:equal)The same graph of \(f\) and its image shifted up by \(2\) units would be given by:
plot(f, -2, 2; aspect_ratio=:equal, label="f")
plot!(up(f, 2); label="up")A graph of \(f\) and its shift over by \(2\) units would be generated by:
plot(f, -2, 4; aspect_ratio=:equal, label="f")
plot!(over(f, 2); label="over")A graph of \(f\) and it being stretched by \(2\) units would be generated by:
plot(f, -2, 2; aspect_ratio=:equal, label="f")
plot!(stretch(f, 2); label="stretch")Finally, a graph of \(f\) and it being scaled by \(2\) would be generated by:
plot(f, -2, 2; aspect_ratio=:equal, label="f")
plot!(scale(f, 2); label="scale")Plot of \(f(x) = \max(0, 1 - \lvert x\rvert)\) and its transformation scaled in \(x\) by \(2\) units
Scaling by \(2\) shrinks the non-zero domain, scaling by \(1/2\) would stretch it. If this is not intuitive, the definition x-> f(x/c) could have been used, which would have opposite behaviour for scaling.
More exciting is what happens if we compose these operations. Before doing so, let’s note how the syntax of composition would be:
up(over(f, 2), 1)The 1 is the value for up and can get lost without careful parsing. It might be better to see something like this instead:
f |> over(2) |> up(1)Meaning, take f, shift it over, 2 and then up 1. To make this happen, we need to pass in a function and return a function from both over and up. We define another set of functions using multiple dispatch to sort out which is which:
up(k) = f -> up(f, k)
over(k) = f -> over(f, k)
stretch(k) = Base.Fix2(stretch, k)
scale(k) = Base.Fix2(scale, k)scale (generic function with 2 methods)
We did this two ways, the last two using Fix2 to fix the second argument, leaving a function of just the first argument.
Now, a shift right by \(2\) and up by \(1\) is achieved through
plot(f, -2, 4; aspect_ratio=:equal, label="f")
plot!(f |> over(2) |> up(1); label="over and up")Shifting and scaling can be confusing. Here we graph the action of over by \(2\) and then scale by \(1/3\):
plot(f, -1, 9; aspect_ratio=:equal, label="f")
plot!(f |> over(2) |> scale(1/3); label="over and scale")Figure 9.7 show \(f(x)\) after being moved over by \(2\) and *then$ scaled by \(1/3\). It’s center moves to \(6\) and instead of stretching from \(6-1\) to \(6+1\) it stretches from \(6-3\) to \(6+3\). Mathematically, we have \(h(x) = f((1/3)\cdot x - 2)\)
Compare this to the same operations in opposite order:
plot(f, -1, 5; aspect_ratio=:equal, label="f")
plot!(f |> scale(1/3) |> over(2); label="scale and over")Figure 9.8 shows the transform of \(f(x)\) first scaled, stretching from \(-3\) to \(3\), then shifted over right by \(2\). The resulting function is \(f((1/3)\cdot (x-2))\).
As a last example, following up on the last example, a common transformation mathematically is
\[ h(x) = \frac{1}{a}f(\frac{x - b}{a}). \]
We can view this as a composition of “scale” by \(1/a\), then “over” by \(b\), and finally “stretch” by \(1/a\), as seen in Figure 9.9:
a = 2; b = 5
plot(f, -1, 8; aspect_ratio=:equal, label="f", xticks=-1:8)
plot!(f |> scale(1/a) |> over(b) |> stretch(1/a); label="h")This transformation keeps the same amount of area in the triangles, can you tell from the graph?
Example: a growth model in fisheries
The von Bertalanffy growth equation is \(L(t) =L_\infty \cdot (1 - e^{-k\cdot(t-t_0)})\). This family of functions can be viewed as a transformation of the exponential function \(f(t)=e^t\). Part is a scaling and shifting (the \(e^{-k \cdot (t - t_0)}\)) along with some shifting and stretching. The various parameters have physical importance which can be measured: \(L_\infty\) is a carrying capacity for the species or organism, and \(k\) is a rate of growth. These parameters may be estimated from data by finding the “closest” curve to a given data set. For this set of parameters2
L, k = 53.1120, 0.0335 # from https://doi.org/10.1093/icesjms/34.2.295
t0 = 1968
u = exp |> scale(-k) |> over(t0) |> stretch(-1) |> up(1) |> stretch(L)
plot(u, t0, t0+200; legend=false)Example
A model for the length of a day in New York City must take into account periodic seasonal effects. A simple model might be a sine curve. However, there would need to be many modifications: Obvious ones would be that the period would need to be about \(365\) days, the oscillation around \(12\) and the amplitude of the oscillations no more than \(12\).
We can be more precise. According to dateandtime.info in \(2015\) the longest day will be June \(21\)st when there will be \(15\)h \(8\)m \(55\)s of sunlight, the shortest day will be December \(21\)st when there will be \(9\)h \(18\)m \(23\)s of sunlight. On March \(21\)st, there will be \(12\)h \(13\)m \(42\)s of sunlight.
A model for a transformed sine curve is
\[ a + b\sin(d(x - c)) \]
Where \(b\) is related to the amplitude, \(c\) the shift and the period is \(T=2\pi/d\). We can find some of these easily from the above:
a = 12 + 13/60 + 42/60/60
b = ((15 + 8/60 + 55/60/60) - (9 + 18/60 + 23/60/60)) / 2
d = 2pi/3650.01721420632103996
If we let January \(1\)st be \(x=0\) then the first day of spring, March \(21\)st, is day \(80\) (Date(2015, 3, 21) - Date(2015, 1, 1) + Day(1)). This day aligns with the shift of the sine curve. This shift is \(80\):
c = 8080
Putting this together, we have our graph is “scaled” by \(d\), “over” by \(c\), “stretched” by \(b\) and “up” by \(a\). Here we plot it over slightly more than one year so that we can see that the shortest day of light is in late December (\(x \approx -10\) or \(x \approx 355\)).
newyork = sin |> scale(d) |> over(c) |> stretch(b) |> up(a)
plot(newyork, -20, 385)To test, if we match up with the model powering dateandtime.info we note that it predicts “\(12\)h \(10\)m \(38\)s” on September \(23\)th, \(2015\). This is day \(266\) (Date(2015, 9, 23) - Date(2015, 1, 1) + Day(1)). Our model prediction has a difference of
datetime = 12 + 10/60 + 38/60/60
delta = (newyork(266) - datetime) * 60-7.486713746311189
This is off by a fair amount—almost \(8\) minutes. Clearly a trigonometric model, based on the assumption of circular motion of the earth around the sun, is not accurate enough for precise work, but it does help one understand how summer days are longer than winter days and how the length of a day changes fastest at the spring and fall equinoxes.
Example: Representing data visually
The Plots.jl package also uses transformations to display different shapes on a graphic. This next example shows how scale can be used to display a third piece of information to augment the two pieces shown through location.
Suppose we have a data set like Table 9.2:3
| flipper length | bill length | body mass | gender | species |
|---|---|---|---|---|
| 38.8 | 18.3 | 3701 | male | Adelie |
| 48.8 | 18.4 | 3733 | male | Chinstrap |
| 47.5 | 15.0 | 5076 | male | Gentoo |
We might want to plot on an \(x\)-\(y\) axis flipper length versus bill length but also indicate body size with a larger-sized marker for bigger sizes.
We could do so by transforming a marker: scaling by size, then shifting it to an x-y position; then plotting. Figure 9.12 illustrates.
flipper = [38.8, 48.8, 47.5]
bill = [18.3, 18.4, 15.0]
bodymass = [3701, 4733, 5076]
shape = Shape(:star5)
p = plot(; legend=false)
for (x, y, sz) in zip(flipper, bill, bodymass)
sz = (sz - 2000) ÷ 1000
new_shape = Plots.translate(Plots.scale(shape, sz, sz), x, y);
plot!(p, new_shape; fill=(:red, 0.25), stroke=(:black, 2))
end
pWhile some of the commands in this example are unfamiliar and won’t be explained further, the use of translate and scale for shapes is very similar to how transformations for functions are being described (Though this translate function combines up and over; and this scale function allows different values depending on direction.) In the above, the function names are qualified, as they are not exported by the Plots.jl package. More variables from the data set could be encoded through colors, different shapes etc. allowing very data-rich graphics.
9.1.3 Operators
In computer science a higher order function is one that either takes a function as input, returns a function as its output, or both. We prefer the less standard, but more mathematical sounding operator to describe higher-order functions like up and over. The use of operators fits in with the template action(f, args...). The action is what we are doing, such as plot, over, and others to come. The function f here is just an object that we are performing the action on. For example, a plot takes a function and renders a graph using the additional arguments to select the domain to view, etc.
Creating operators that return functions typically involves the use of anonymous functions, using these operators is relatively straightforward. In this next example, we create two new operators that will be prototypes for two foundational operator in calculus.
Example: two operators
(See Krill for background on this example.) Consider two operations on functions. The first takes the difference between adjacent points. We call this D:
D(f::Function) = k -> f(k) - f(k-1)D (generic function with 1 method)
As written, D takes a function, f, and then returns a function that finds the difference of f at k and k-1.
To see how D works, we take a typical function:
f(k) = 1 + k^2f (generic function with 1 method)
Then we have:
D(f)(3), f(3) - f(3-1)(5, 5)
That the two are the same value is by design.
The calling syntax D(f)(3) is a bit awkward and is read as two steps: the first finds D(f) the second calls this function at 3. The first could have been stored and then called, as with:
df = D(f)
df(3)5
Now we want a function to cumulatively sum the values \(S(f)(k) = f(1) + f(2) + \cdots + f(k-1) + f(k)\), as a function of \(k\). Adding up \(k\) terms is easy to do with a generator and the function sum:
S(f) = k -> sum(f(i) for i in 1:k)S (generic function with 1 method)
To check if this works as expected, compare these two values:
S(f)(4), f(1) + f(2) + f(3) + f(4)(34, 34)
So one function adds, the other subtracts. Addition and subtraction are somehow inverse to each other so should “cancel” out. This holds for these two operations as well, in the following sense: subtracting after adding leaves the function alone:
k = 10 # some arbitrary value k >= 1
λ = D(S(f))
λ(k), f(k)(101, 101)
Any positive integer value of k will give the same answer (up to overflow). This says the difference of the accumulation process is just the last value to accumulate.
Adding after subtracting also leaves the function alone, save for a vestige of \(f(0)\). For example, k=15:
γ = S(D(f))
γ(15), f(15) - f(0)(225, 225)
That is, the accumulation of differences is just the difference of the end values.
These two operations are discrete versions of the two main operations of calculus—the derivative and the integral. This relationship will be known as the “fundamental theorem of calculus.”
9.2 Questions
Question
If \(f(x) = 1/x\) and \(g(x) = x-2\), what is \(g(f(x))\)?
Question
If \(f(x) = e^{-x}\) and \(g(x) = x^2\) and \(h(x) = x-3\), what is \(f \circ g \circ h\)?
Question
If \(h(x) = (f \circ g)(x) = \sin^2(x)\) which is a possibility for \(f\) and \(g\):
Question
Which function would have the same graph as the sine curve shifted over by 4 and up by 6?
Question
Let \(h(x) = 4x^2\) and \(f(x) = x^2\). Which is not true:
Question
Consider the function
\[ g(x) = f(\frac{x - a}{b}) \]
This is
Consider the function
\[ g(x) = f(\frac{x}{b} - a) \]
This is
Question
The transformation \(h(x) = (1/a) \cdot f((x-b)/a)\) can be viewed in one sequence:
Question
This is the graph of a transformed sine curve.
What is the period of the graph in Figure 9.13?
What is the amplitude of the graph in Figure 9.13?
What is the form of the function graphed in Figure 9.13?
Question
Consider this expression
\[ \begin{align*} \left(f(1) - f(0)\right) &+ \left(f(2) - f(1)\right) + \cdots + \left(f(n) - f(n-1)\right) \\ &= -f(0) + f(1) - f(1) + f(2) - f(2) + \cdots + f(n-1) - f(n-1) + f(n) \\ &=f(n) - f(0). \end{align*} \]
Referring to the definitions of D and S in the example on operators, which relationship does this support:
Question
Consider this expression:
\[ \left(f(1) + f(2) + \cdots + f(n-1) + f(n)\right) - \left(f(1) + f(2) + \cdots + f(n-1)\right) = f(n). \]
Referring to the definitions of D and S in the example on operators, which relationship does this support:
There could be such operations defined. For example, if
+is imported fromBase, then this commandf::Function + g::Function = x -> f(x) + g(x)would define such a operation. However, this definition in general is kind of limiting, as functions in mathematics and Julia can be much more varied than just the univariate functions we have defined addition for. Further, users shouldn’t be modifying base methods on types they don’t control, as that can lead to really unexpected and undesirable behaviours. This is called type piracy.↩︎From Campbell and Phillips shows steady but decelerating growth of a whelk population.↩︎
Which comes from the “Palmer Penguins” data set↩︎