library(tidyverse)
library(memoise)
library(rlang)
library(knitr)
library(purrr)
In my first post, we will be implementing some interesting mathematical concepts in R.
The Binomial Theorem
\[\begin{align*} (x+y)^n=\sum_{k=0}^{n}\binom{n}{k}x^{k}y^{n-k} \end{align*}\]
Expanding:
\[\begin{align*} (x+y)^n=\binom{n}{0}x^{0}y^{n-0}+\binom{n}{1}x^{1}y^{n-1}+\binom{n}{2}x^{2}y^{n-2}+... \\ +\binom{n}{n-1}x^{n-1}y^{n-(n-1)}+\binom{n}{n}x^{n}y^{n-n} \end{align*}\]
In R, we can tackle the implementation of the Binomial Theorem in three parts:
- The binomial coefficient
- raising the real number \(x\) to the vector of powers \(k\)
- Raising the real number \(y\) to the vector of powers \(n-k\)
Show code
<- function(x, y, n) {
binomial_theorem # Create a sequence from k = 0 to k = n
<- seq.int(from = 0, to = n, by = 1)
seq_k_n
# Pre-allocate container for storing coefficients
<- vector(mode = "double", length = n + 1)
binom_coeffs # Binomial coefficients
<- map_dbl(.x = seq_k_n, .f = choose, n = n)
binom_coeffs
# Pre-allocate container for storing the x's
<- vector(mode = "double", length = n + 1)
vector_of_x # Raise x to the power of y
<- x^(seq_k_n)
vector_of_x
# Pre-allocate container for storing the y's
<- vector(mode = "double", length = n + 1)
vector_of_y # Raise y to the power of n-k
<- y^(n - (seq_k_n))
vector_of_y
# Product of the two vectors and their coefficients
<- binom_coeffs * vector_of_x * vector_of_y
prod
# Summation operator
<- sum(prod)
result
result }
Let’s see it in action:
Show code
# Test
<- 924
x <- 23
y <- 39
n # Compute by hand
+ y)^n (x
[1] 1.195774e+116
Show code
# Compute using custom function
binomial_theorem(x = x, y = y, n = n)
[1] 1.195774e+116
As can be seen, the results are exactly the same.
Pascal’s Triangle
Directly related to the Binomial coefficient is Pascal’s triangle, whose entries in each row are usually staggered relative to the numbers in the adjacent rows.
To implement Pascal’s triangle, we will use a for loop. Our goal is to write a program that finds the next row of Pascal’s triangle, given the previous rows.
Show code
# Finding the (n + 1)th row of a Pascal's triangle given n rows that precede it
<- function(x) {
pascal_triangle_n_plus_1 if (!is.list(x)) {
::abort(message = "The input object must a be a list containing the rows of Pascal's Triangle.")
rlang
}
# Set n equal to depth of the input list "x", that is, the number of elements in x, where each represents a row
<- length(x)
n # Extract the last element (the nth row) from the input list "x" and store it as a new variable x_n
# Use [[ to extract the value rather than a sub-list
<- x[[n]]
x_n # Repeat the integer "1" (n + 1) times
# Note that the (n + 1)th row has (n + 1) elements beginning and ending with 1
<- rep(x = 1, times = n + 1)
x_n_plus_1
# Loop to add all adjacent pairs in the nth row to obtain the (n + 1)th row
# Start with the second element and end with second to last element of each row
# This is because the first and last numbers in any given row are always 1
if (n > 1) {
# This is the prefix form of for loop
`for`(
var = i,
seq = 2:n,
action = x_n_plus_1[[i]] <- x_n[[i - 1]] + x_n[[i]]
)
}
# Append the (n + 1)th row to the list object
::append(x, values = list(x_n_plus_1))
base }
Let’s see it in action:
Show code
# Create a Pascal's Triangle with 4 rows
<- list(c(1), c(1, 1), c(1, 2, 1), c(1, 3, 3, 1))
x # Row 5
<- pascal_triangle_n_plus_1(x = x)
x 5]] x[[
[1] 1 4 6 4 1
Show code
# Row 6
<- pascal_triangle_n_plus_1(x = x)
x 6]] x[[
[1] 1 5 10 10 5 1
Show code
# We know that these row entries can be computed are the binomial coefficients 5 choose 0 thru 5 choose 5
map2_dbl(
.x = rep(x = 5, times = 6),
.y = seq.int(from = 0, to = 5, by = 1),
.f = choose
)
[1] 1 5 10 10 5 1
Fibonacci with Memoization
The Fibonacci sequence is defined recursively, where the first two values are given by convention, \(f(0) = 0\), \(f(1) = 1\), and the rest are calculated as follows:
\[\begin{align*} f(n) = f(n-1) + f(n-2) \end{align*}\]
However, a naive recursive implementation is inefficient because it repeatedly recalculates the same values during each step. Using memoise, we can cache previous computations, significantly speeding up the process.
Show code
<- function(n) {
fib if (n < 2) {
return(1)
}fib(n - 2) + fib(n - 1)
}
# Testing the naive implementation
system.time(fib(31))
user system elapsed
0.842 0.009 0.854
Memoizing the function allows us to compute Fibonacci numbers more efficiently.
Show code
<- memoise::memoise(function(n) {
fib_memo if (n < 2) {
return(1)
}fib_memo(n - 2) + fib_memo(n - 1)
})
# Testing the memoized implementation
system.time(fib_memo(31))
user system elapsed
0.015 0.000 0.014
By caching the results, we avoid redundant calculations, improving performance for large inputs. Furthermore, calling the memoized function for the same input or the next input will be much faster than the naive implementation.
Show code
system.time(fib_memo(32))
user system elapsed
0 0 0
Show code
system.time(fib(32))
user system elapsed
1.332 0.010 1.342
Newton-Raphson Method
The Newton-Raphson method is an iterative numerical method for finding the roots (or zeros) of a real-valued function. The method uses the idea of linear approximation: at each iteration, the method refines the guess for the root by using the slope (derivative) of the function.
Given a function \(f(x)\) and its derivative \(f^{\prime}(x)\), the Newton-Raphson iteration formula is:
\[\begin{align*} x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)} \end{align*}\]
This formula is applied repeatedly, starting from an initial guess \(x_0\), until the difference between successive approximations is less than a predefined tolerance \(\varepsilon\), i.e.,
\[\begin{align*} |x_{n+1} - x_n| < \varepsilon \end{align*}\]
The implementation below has some slight overhead due to our storing the intermediate steps in a vector, but this is a small price to pay for the added functionality.
Show code
<- function(f, f_prime, x0, tol = 1e-8, max_iter = 100) {
newton_raphson # Define a function that applies a single Newton-Raphson step
<- x0
x <- numeric(max_iter + 1) # Preallocate space to store the steps
steps 1]] <- x0
steps[[
for (i in seq_len(max_iter)) {
# Perform a Newton-Raphson step
<- x - f(x) / f_prime(x)
x_new + 1]] <- x_new
steps[[i # If the tolerance condition is met, stop the iteration
if (abs(x_new - x) < tol) {
<- steps[1:(i + 1)] # Truncate the steps to only valid iterations
steps break
}<- x_new
x
}return(steps)
}
As an example, let’s find the root of the function:
\[\begin{align*} f(x) = x^3 - 2x - 5 \end{align*}\]
and
\[\begin{align*} f^{\prime}(x) = 3x^2 - 2 \end{align*}\]
Show code
<- function(x) x^3 - 2 * x - 5
f <- function(x) 3 * x^2 - 2
f_prime <- seq(-10, 10, by = 0.1)
x_vals <- map_dbl(x_vals, ~ f(.x))
y_vals
ggplot(data.frame(x = x_vals, y = y_vals), aes(x = x, y = y)) +
geom_line() +
geom_hline(yintercept = 0, linetype = "dashed") +
labs(
title = "Graph of f(x) = x^3 - 2x - 5",
x = "x",
y = "f(x)"
+
) theme_minimal()
We will use an initial guess of \(x_0 = 20\) and a tolerance of \(\varepsilon = 1e-8\):
Show code
<- newton_raphson(f = f, f_prime = f_prime, tol = 1e-8, x0 = 20, max_iter = 100) intermediate_steps
The chart below shows the convergence of the Newton-Raphson method to the root of the function \(f(x)\):
Show code
<- data.frame(
data iteration = 1:length(intermediate_steps),
approximations = intermediate_steps
)
ggplot(data, aes(x = iteration, y = approximations)) +
geom_line() +
geom_point() +
labs(
title = "Convergence of Newton-Raphson Method",
x = "Iteration",
y = "Approximation of Root"
+
) theme_minimal()
That’s it for my first post!