# Mastering Atomic Vectors in R

Sunday, Mar 11, 2018
R

Everything that exists in R is an object and anything that happens is a function call.

### Creating Our First Atomic Vector

We are going to create and manipulate vectors using existing functions and the resulting objects will be atomic vectors. To create the objects (atomic vectors in our case), we will use an assignment operator (<-). So we create our first object (num_one), and assign it a value of 3:

num_one <- 3


Atomic vectors can only have one ‘type’ of data, and we start with numeric data type since this will be the most common one for our data analysis work. Our simple vector contains only one element (commonly called a scalar), but in all respects, it is a vector in R. Expressions on the right side are evaluated first and then assigned to the object name on the left. Let's look at an expression evaluation:

num_one_log <- log(3)


log(3) is evaluated first and then assigned to an object named num_one_log. Let's create a slightly longer vector:

num_five <- c(2, 15, -8, 0, -65)


Now we have a vector containing five elements instead of one. Here we make use of a function, c, that ‘concatenates’ the objects that are fed to it as its arguments. A function is ‘called’ by typing its name (in this case its just c) followed by one or more arguments typed inside parenthesis.

### Peeking into our Vector

Let's look into what kind of vector it is:

str(num_five)

##  num [1:5] 2 15 -8 0 -65


str function gives a concise information of what kind of object we are dealing with. In this case, we have a numeric (num) object of length 5 ([1:5]). The above output is a good visual information but sometimes we want to use this output later in our work so it is good to know functions that extract type and length of the vector separately:

typeof(num_five)

## [1] "double"

length(num_five)

## [1] 5


Note that ‘numeric’ type is further divided into double and integer. Sometimes we want to do a logical test to see whether a vector is of particular type before carrying out any further computations:

library(purrr)
is_numeric(num_five)

## Warning: Deprecated

## [1] TRUE


Above we used a function is_numeric from purrr package. There are equivalent functions in the base package too but slightly less consistent.

### Objects as Arguments to Functions

Objects can be arguments to a function:

long_vec <- c(num_five, num_one, num_one_log)
long_vec

## [1]   2.000000  15.000000  -8.000000   0.000000 -65.000000   3.000000   1.098612


Here we created a longer vector using existing vector objects as arguments to c function. This provides us a framework for doing more interesting work.

### Other Ways of Creating a Vector

In addition to creating a vector from numbers or concatenating existing vectors, we can create sequences, repeat vectors, or pick numbers from common probability distributions:

seq_simple <- -5:10
seq_interval <- seq(from = 3, to = 50, by = 0.5)
seq_length <- seq(from = 3, to = 50, length.out = 20)
rep_each <- rep(num_five, each = 2)
rep_times <- rep(num_five, times = 2)
norm_std <- rnorm(25)
norm_unique <- rnorm(25, mean = 10, sd = 5)


First we created a simple sequence of whole numbers. Then we used seq function to create a sequence with arbitrary start and end points and fixed interval between the elements. After that we created another one (seq_length) with desired number of elements between arbitrary start and end points. Then using rep function, we created a vector by repeating each element twice and then by repeating the vector twice. In the end we created a vector (norm_std) with 25 elements from a standard normal distribution using rnorm and lastly created a vector with non-standard normal distribution.

### Vector Operations

Now that we know how to create vector objects, let's play with these objects a bit and see what kind of operations we can perform on our vectors. We will use one of the vectors we created earlier:

rep_times

##  [1]   2  15  -8   0 -65   2  15  -8   0 -65


Arithmatic Operations

We can do all kinds of math operations with our vectors. We are not going to store these in new objects but watch these function calls in action:

rep_times ^ 2

##  [1]    4  225   64    0 4225    4  225   64    0 4225

rep_times * 1 / 5

##  [1]   0.4   3.0  -1.6   0.0 -13.0   0.4   3.0  -1.6   0.0 -13.0


Note that the way these operations are handled is slightly different than you might think. *, ^ and other operators are vectorized - so first the number 2, which is a vector of length one, is recycled to make it the same length as the larger vector (ten in this case). Then each element of first vector is operated upon with the corresponding element of the second vector.

There is an interesting math operator called modulo operator (%%) - which is used to get different parts of answer of division operation. If you divide two numbers, top number is called dividend and bottom number is called divisor. The result will be a whole number (called quotient), and whatever fraction is left out is remainder.

rep_times %% 2      # get remainder of rep_times divided by 2.

##  [1] 0 1 0 0 1 0 1 0 0 1

rep_times %/% 2     # get quotient of rep_times divided by 2.

##  [1]   1   7  -4   0 -33   1   7  -4   0 -33


Statistical Operations

R will make you dizzy for the amount of statistical wizardry it packs up its sleeve. We are not going to take a deep dive, not even scratching the surface by any standards:

mean(rep_times)

## [1] -11.2

sum(rep_times)

## [1] -112

sd(rep_times)

## [1] 29.40446

max(rep_times)

## [1] 15

summary(rep_times)

##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
##   -65.0    -8.0     0.0   -11.2     2.0    15.0


Other Interesting Operations

sort(rep_times)

##  [1] -65 -65  -8  -8   0   0   2   2  15  15

sort(rep_times, decreasing = TRUE)

##  [1]  15  15   2   2   0   0  -8  -8 -65 -65

unique(rep_times)

## [1]   2  15  -8   0 -65

rev(rep_times)

##  [1] -65   0  -8  15   2 -65   0  -8  15   2


### Chaining of Functions

The output of one function can be used as an input to another function. Say we want to find standard deviation of 50 standard normals:

sd(rnorm(50))

## [1] 0.9552438


You probably want to avoid more convoluted chains as it will be confusing to read.

### Selecting Few Elements of a Vector (Subsetting)

So far we have created vectors using a variety of functions. Now, if a vector is given to us and we are tasked with selecting certain elements from it, how do we do that. There are two useful ways to do that:

Subsetting using position

In R, elements of a vector are positioned (indexed) from 1 (many languages start at 0) to the length of the vector. Our rep_times vector has 10 elements, and say we want to extract first and last element, we can feed these (index values) as integer vector inside square brackets:

rep_times[c(1, length(rep_times))]

## [1]   2 -65


If we want to select everything but the first and last element, you can put a ‘-’ sign before the vector:

rep_times[-c(1, length(rep_times))]

## [1]  15  -8   0 -65   2  15  -8   0


A more powerful way of subsetting is through logical expressions.

Subsetting using logical expressions

Say if we had a light bulb that we could turn on if we want an element in that position and off if we don't, it would be great. It turns out that R (and generally any programming language) has a special data type for carrying out logical operations. Let's say we want to extract all odd numbers from our rep_times vector:

odd_filter <- rep(c(TRUE, FALSE), 5)
str(odd_filter)

##  logi [1:10] TRUE FALSE TRUE FALSE TRUE FALSE ...

is_logical(odd_filter)

## [1] TRUE

rep_times[odd_filter]

## [1]   2  -8 -65  15   0


Cool, but most of the times we will not create this switch vector manually. Instead, logical expressions will be used to do this job, and this will serve as a handy tool in our R toolbox. Let's look at some logical expressions and their output:

rep_times > 0

##  [1]  TRUE  TRUE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE

rep_times < -10

##  [1] FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE

rep_times == 0

##  [1] FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE FALSE

rep_times %% 2 != 0     # selecting odd numbers

##  [1] FALSE  TRUE FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE  TRUE


The output of these logical expressions is either TRUE or FALSE depending on whether the logical condition is satisfied or not. Now we can feed the output of these expressions inside square brackets for subsetting our vector:

rep_times[rep_times < -10]

## [1] -65 -65


Logical expressions can be combined using and (&) and or (|) operators

(rep_times > 0) | (rep_times < -10)

##  [1]  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE FALSE FALSE  TRUE

(rep_times > 0) & (rep_times %% 2 != 0)

##  [1] FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE


### More Uses of Logical Subsetting

TRUE = 1 and FALSE = 0

Behind the scenes, TRUE is encoded as 1 and FALSE is encoded as 0. This means we can do some neat math operations on logical vectors. Let's say we want to know how many values are positive in my vector:

sum(rep_times > 0)

## [1] 4


Or say what proportion of values are non-zeros:

mean(rep_times != 0)

## [1] 0.8


Reassignment of some elements of a vector

Sometimes we might need to reassign specific elements of a vector to new values. For example, let's say, we want to replace all 0's in our vector to -99:

rep_times[rep_times == 0]  <- -99
rep_times

##  [1]   2  15  -8 -99 -65   2  15  -8 -99 -65


Any or all values matching a condition

Let's say we just want to know if any of the values in our vector are negative:

any(rep_times < 0)

## [1] TRUE


Or, if all of the values are non-zeros:

all(rep_times != 0)

## [1] TRUE


### Two Types of Vectors

Ok, now we know two types of atomic vectors: numeric, one that contains numeric numbers and logical, that contain logical values of either TRUE or FALSE.

### Pick Elements Randomly

So far we have looked at subsetting of vectors where we know which element we need either by their index or by logical subsettin. Sometimes we want to randomly select some elements, and sample function will come to our rescue here:

sample(rep_times, 5)    # randomly select five elements from rep_times

## [1]   2 -99  -8  15  15


Select certain fraction of elements

sample(rep_times, 0.4 * length(rep_times))    # 40% of elements

## [1]  -8 -65 -65  15


A quick shortcut to shuffle integers

sample(10)

##  [1]  9  5  4 10  1  2  6  7  8  3


### Some Special Values

There are some special values that our two types of vectors can take, so we will discuss those here.

NA - Not Available

In real data, we do not always get nice clean numbers - some data points will be missing. A sensor could go bad, there could be a power failure during recording of data, a subject selected to interview is not at home etc. Before we decide what to do with these missing values, we need to understand some basic information on these values, for example how many or what proportion of the values are missing. R represents this missing data with NA. In reality the data exists, but we just don't know what those values are.

Let's first randomly assign some values as NA to our vector:

rep_times[sample(length(rep_times), 4)] <- NA
rep_times

##  [1]   2  NA  -8  NA -65   2  NA  -8 -99  NA


Now to find how many or what proportion of NAs we have, we could use is.na function:

sum(is.na(rep_times))

## [1] 4

mean(is.na(rep_times))

## [1] 0.4


NAs are contagious:

sum(rep_times)

## [1] NA

mean(rep_times)

## [1] NA


But R provides arguments in these functions, and others, to ignore NAs while computing these statistics:

sum(rep_times, na.rm = TRUE)

## [1] -176

mean(rep_times, na.rm = TRUE)

## [1] -29.33333


What to do with these NAs?

There are two basic treatments we can do with vectors containing NAs.

1. We can remove those observations:
rep_times_na_removed <- rep_times[!is.na(rep_times)]
rep_times_na_removed

## [1]   2  -8 -65   2  -8 -99

1. We can replace NAs with the mean of rest of observations:
rep_times_clean <- rep_times
rep_times_clean[is.na(rep_times_clean)] <- mean(rep_times_clean, na.rm = TRUE)
rep_times_clean

##  [1]   2.00000 -29.33333  -8.00000 -29.33333 -65.00000   2.00000 -29.33333
##  [8]  -8.00000 -99.00000 -29.33333


Inf and NaN

Although not created intentionally, some chain of math operations in our code may lead to situations where we are dividing by 0. We want to avoid these situations since this will lead to errors and exceptions. So it is a good idea to check for these conditions on inputs before using the inputs in our functions. is.infinite and is.nan functions can be used to check these conditions:

undesirable <- c(0, 1) / 0
undesirable

## [1] NaN Inf

is.infinite(undesirable)

## [1] FALSE  TRUE

is.nan(undesirable)

## [1]  TRUE FALSE


### Sequences for Loops

Sometimes we need a vector of indices of a vector so we can do iterations, loops etc. seq_along will be a useful function for that. This will generate an index vector 1, 2,…, length(vector).

seq_along(rep_times)

##  [1]  1  2  3  4  5  6  7  8  9 10


### Creating an empty vector

We may want to create an empty vector sometimes. This will be useful when we want to populate the vector using loops.

new_num <- vector("numeric", 10)
new_log <- vector("logical", 10)
new_num

##  [1] 0 0 0 0 0 0 0 0 0 0

new_log

##  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE


### Some Useful Vector Transformations

Recenter around mean 0

data <- sample(1:20, 10) * sample(10)
data - mean(data)

##  [1]  -7.1  -5.1  66.9 -61.1 -10.1 116.9 -63.1  -9.1  -3.1 -25.1


Recenter around mean 0 and standard deviation 1

(data - mean(data)) / sd(data)

##  [1] -0.13021521 -0.09353487  1.22695737 -1.12058439 -0.18523572  2.14396587
##  [7] -1.15726473 -0.16689555 -0.05685453 -0.46033827


Rescale so all values are between 0 and 1

(data- min(data)) / (max(data) - min(data))

##  [1] 0.31111111 0.32222222 0.72222222 0.01111111 0.29444444 1.00000000
##  [7] 0.00000000 0.30000000 0.33333333 0.21111111


### Comparing Two Vectors

Sometimes we just want to check if two vectors are equal. all.equal can be a useful function for that:

x1 <- c(1, 2, 3)
y1 <- c(1, 2, 3)
y2 <- c(2, 3, 4)
all.equal(x1, y1)

## [1] TRUE

all.equal(x1, y2)

## [1] "Mean relative difference: 0.5"


### Third Type of Atomic Vector

The third and last type of atomic vector is character which is very powerful in data analysis as well as general programming. Although we have not created this type of vector so far explicitly, note that the output of some functions that we have used was a string of characters:

x <- typeof(rep_times)
x

## [1] "double"

typeof(x)

## [1] "character"

is_character(x)

## [1] TRUE


Mastering Regular Expressions is key to working with character data so we will learn the basics here. We are going to use stringr package to play with our character vectors.

library(stringr)


You can create a character vector by entering anything inside quotes. We are going to use double quotes for consistency:

greet <- "hello!"
greet

## [1] "hello!"

typeof(greet)

## [1] "character"

is_character(greet)

## [1] TRUE


Alright, let's create another vector and combine the two with c just like we did earler for numeric vectors:

subject <- "world"
greetings <- c(greet, subject)
greetings

## [1] "hello!" "world"

length(greetings)

## [1] 2


So our greetings vector contains two string elements. It is interesting to note that both strings can have arbitrary number of characters. Let's say we want to know how many characters each string element has, we will use str_length function:

str_length(greetings)

## [1] 6 5


Combining elements of a vector

If we want to combine the individual elements of a single character vector, we will use str_c function with collapse argument:

str_c(greetings, collapse = "")

## [1] "hello!world"


Combining elements of two vectors

If we want to combine two character vectors (element wise), we will use the same str_c function but with sep argument:

str_c(greetings, "!", sep = "")

## [1] "hello!!" "world!"


### Sorting Character Vectors and Changing Case

We will use data set, words available in stringr for this exercise:

word_sample <- words[sample(length(words), 5)]
word_sample

## [1] "do"      "similar" "tax"     "general" "king"

str_sort(word_sample, locale = "en")

## [1] "do"      "general" "king"    "similar" "tax"

str_to_lower(word_sample)

## [1] "do"      "similar" "tax"     "general" "king"

str_to_upper(word_sample)

## [1] "DO"      "SIMILAR" "TAX"     "GENERAL" "KING"

str_to_title(word_sample)

## [1] "Do"      "Similar" "Tax"     "General" "King"


### Pattern Matching in Character Vectors using Regular Expressions

Now we are going to scratch the surface of regular expressions. Regular expressions is a subject in itself in computing world but we will cover minimum stuff just to get us started with cleaning data where needed and not worry (yet) about text analysis.

Everything in ASCII (or Unicode) is a character (letters, digits, spaces, punctuations, etc.) and sequence of characters is called a string.

stringr has different functions to do different things using regular expressions but they have the same format. We will use one, str_extract, which extracts the matched pattern. At the end we will apply our pattern matching skills to other tools. Let's begin by picking five random strings from sentences data available in stringr. Note that we are using set.seed function, this lets us repeatedly get the same set of sentences:

set.seed(1234)
sent_five <- sentences[sample(length(sentences), 5)]
sent_five

## [1] "ii cloud of dust stung his tender eyes."
## [2] "Oak is strong and also gives shade."
## [3] "A whiff of it will cure the most stubborn cold."
## [4] "The pods of peas ferment in bare fields."
## [5] "The cone costs five cents on Mondays."


Literal match - just type what you want to match

str_extract(sent_five, "northern")

## [1] NA NA NA NA NA


Use dot (.) to match any character

Here we introduce regex‘s first metacharacter (something that is not matched literally but indicates some pattern). “.” will match any single character. Let's say we are interested in grabbing ten characters following the:

str_extract(sent_five, "the..........")

## [1] NA              NA              "the most stub" NA
## [5] NA


It picked the and the following ten characters including spaces. Note that it did not pick The from third sentence because it starts with a capital T. If we want to disregard the case, there are multiple ways but let's create a character class in this case to solve the problem.

Use character class [ ] to match one of the characters defined in the class:

str_extract(sent_five, "[Tt]he..........")

## [1] NA              NA              "the most stub" "The pods of p"
## [5] "The cone cost"


Now it matched either t or T followed by he followed by any ten characters. Great, now let's say we want to pick sentences that begin with The or the. In this case, it will only be the third sentence.

• appending ^ to pattern inside [] will match all characters excluding those defined in the class.

^ to match the beginning of the string

If you want to match a pattern only at the beginning of the string, you would prepend your pattern with ^:

str_extract(sent_five, "^[Tt]he..........")

## [1] NA              NA              NA              "The pods of p"
## [5] "The cone cost"


$to match the end of the string Append your pattern with$ to match it only at the end of the string:

str_extract(sent_five, "mouse$")  ## [1] NA NA NA NA NA  Hmm..it did not match our first string that we were hoping for. Aahhh, we missed the period (.). All strings are ending with a period so we have to include that too in our pattern, but remember that period already is a metacharacter. So how do we let regex engine know that we want the literal period. Well, we append our metacharacter with an escape character, \. One problem here is that \ is also used in our strings and regular expressions are string arguments to our functions. We we have to escape the escape character! To match regex's metacharacter literally, append it with two escape charaters: \\ str_extract(sent_five, "mouse\\.$")

## [1] NA NA NA NA NA


Use writeLines to check if your regex pattern is what you expected

Since these escape characters can cause confusion on what the regex engine is finally seeing, it is a good idea to feed our argument to writeLines first:

writeLines("mouse\\.$")  ## mouse\.$


Repeating Patterns

So far we have looked at single character matches. So if we want to match two capital letter in the following vector, we will define a character class and repeat it twice:

states <- c("AL", "MI", "Hawaii", "ME", "Illinois" )
str_extract(states, "[A-Z][A-Z]")

## [1] "AL" "MI" NA   "ME" NA


This can get messy quickly, say if we want to match five capital letters, or ten digits. It turns out regex engine has a very powerful feature of matching repetetive patterns.

• {n} match preceding character or group exactly n number of times:
str_extract(states, "[A-Z]{2}")

## [1] "AL" "MI" NA   "ME" NA

• {m,} match the preceding character or a group m or more number of times:
str_extract(sent_five, "l{2,}")

## [1] NA   NA   "ll" NA   NA


Similar concept applies for {,n} to match up to n times and {m,n} to match at least m times and at the most n times. There are shortcuts for some common repetitions:

• + to match 1 or more times:
money <- c("$23", "$ab", "$4000") str_extract(money, "\\$[0-9]+")

## [1] "$23" NA "$4000"

• * to match 0 to more times:
str_extract(money, "\\$[0-9]*")  ## [1] "$23"   "$" "$4000"

• ? to match 0 or 1 times:
days <- c("sunday", "mon", "tue", "wednesday")
str_extract(days, "((sun)|(mon)|(tue)|(wednes))(day)?")

## [1] "sunday"    "mon"       "tue"       "wednesday"


Here we also quickly demonstrated the conecept of or, |, and groups using ()

More character classes

Earlier we learned a couple of examples of how to define a character class. Another example is [A-Za-z0-9] will match any letter or digit. There are shortcuts available however for commonly used classes. These when used with repetetion matches results in can result in flexible and powerful patterns:

• \d will match any single digit:
str_extract(money, "\\$\\d+")  ## [1] "$23"   NA      "$4000"  • \D will match any single non-digit address <- c("abc street", "NY", "US", "54875") str_extract(address, "\\D+")  ## [1] "abc street" "NY" "US" NA  • \w will match any alphanumeric character including . This is equivalent to [A-Za-z0-9] some_txt <- c("$$Name..", " Class--", "$$$Address$$") str_extract(some_txt, "\\w+")  ## [1] "Name" "Class" "Address"  • \W will match any non-alphanumeric character str_extract(some_txt , "\\W+")  ## [1] "$$"   "   $" "$$"  • \s will match any whitespace (space, tab, newline, carriage return) str_extract(some_txt , "\\s+")  ## [1] NA " " " "  • \S will match any non-whitespace str_extract(some_txt , "\\S+")  ## [1] "$$Name.." "$Class--"     "$$Address$$"

• \b will match a word boundary (end of word)
str_extract(sent_five, "\\w+\\b")

## [1] "ii"  "Oak" "A"   "The" "The"


### Stringr Tools

Ok, now that we are comfortable using regular expressions, we need to put these into use with the available tools in stringr. We have already used one - str_extract which extracts the matched pattern. There are few others that can be very useful.

Logical output if a match is found

str_detect(address, "\\D+")

## [1]  TRUE  TRUE  TRUE FALSE


Note that this approach can also be used to simplify regular expressions.

How many matches are there in the string

str_count(sent_five, "the")

## [1] 0 0 1 0 0


Replace a match with new string

str_replace(sent_five, "the", "***")

## [1] "ii cloud of dust stung his tender eyes."
## [2] "Oak is strong and also gives shade."
## [3] "A whiff of it will cure *** most stubborn cold."
## [4] "The pods of peas ferment in bare fields."
## [5] "The cone costs five cents on Mondays."


Split a string into pieces

str_split(sent_five[1], " ")

## [[1]]
## [1] "ii"     "cloud"  "of"     "dust"   "stung"  "his"    "tender" "eyes."


Note that this gave us a list object, which we have not studied yet, but you can see the function of splitting a string here.

Not one but all matches

All the tools that we have looked at so far will match and give you the first occurence in a string. All others are ignored. Above functions have equivalent _all commands that will apply to all matches. For example, str_extract_all will extract all matches. These will result in a data structure called list, which we have not studied yet but will look into in the future.

### Factors - an Augmented Vector Type

Alright, we are going to look at one more vector type, which is not an atomic vector by definition but it occurs so frequently in our data analysis that our vector discussion will be incomplete without it. It is an augmented vector because it is built on top of one of the atomic vectors - integer. Often times in our data, we will see variables that are categorical in nature, meaning they have discrete categories. For example, sex, month name, color, class etc. These type of data are modeled with factor. Let's create a simple factor vector:

sex <- c("M", "M", "F", "M", "F")
sex_fac <- factor(sex)
sex_fac

## [1] M M F M F
## Levels: F M

is.factor(sex_fac)

## [1] TRUE


R automatically assigns levels for the factor vector. This assignment will be alphabetical. If we don't want that, we have to define the levels when creating the vector:

sort(sex_fac)

## [1] F F M M M
## Levels: F M

sex_fac2 <- factor(sex, levels = c("M", "F"))
sort(sex_fac2)

## [1] M M M F F
## Levels: M F


Recoding factor levels

In the above case, the levels are apparently clear due to clear object name. Sometimes, we may want to recode to make the levels more clear. We will use fct_recode function from the package forcats:

library(forcats)
levels(sex_fac2)

## [1] "M" "F"

sex_fac2 <- fct_recode(sex_fac2,
"Male" = "M",
"Female" = "F")
levels(sex_fac2)

## [1] "Male"   "Female"


Defining order where it matters

Some variables will have an implied order. For example, size of a clothing line. We can define the order while creating the vector with ordered = TRUE argument:

size <- factor(c("small", "large", "medium", "small", "medium"),
levels = c("small", "medium", "large"),
ordered = TRUE)
size[2] > size[1]

## [1] TRUE


Sumamrize factors

We can use summary function used earlier with numeric vectors to get concise infor about the factor vector as well:

summary(size)

##  small medium  large
##      2      2      1


### Summary

Now we have with us the fundamental building blocks using which we can understand the existing higher level data structures as well as create some of our own! So this is really exciting. Next we will take a dive into learning some programming concepts using which we can do some more powerful things beyond the one liners that we have been using here.