Intro to R: Data types, flow control, and functions

Author

Jeremy Van Cleve

Published

January 15, 2024

Outline for today

  • Objects and functions
  • Statements and style
  • Types of objects
  • Flow control
  • Loops
  • More on functions

Objects

Everything in R is an object. For example numbers are objects,

10
[1] 10

and strings of characters, AKA “strings”, are objects,

"hello world\n"
[1] "hello world\n"

One way you can combine objects in R is by using an “operator” like +

10 + 100
[1] 110

or by using a function

exp(10)
[1] 22026.47

Objects are functions and functions are objects

Even functions, like print, which prints a string, is an object,

print("hello world")
[1] "hello world"

What is an object then?

An object is data stored in memory with a name (or identifier) that can have attributes and functions (often called methods) that do specific things to objects with specific attributes.

This means that even something as simple as the number 10 can have attributes that we can query and functions that are specific for use on it. To see an example, consider, the names attribute that comes up a lot and can provide a convenient way to access an element of a vector. Say we create a vector where we give each element a label:

c(a=1, b=2)
a b 
1 2 

We can get the labels by asking for the attributes of the vector or running

attributes(c(a=1, b=2))
$names
[1] "a" "b"
names(c(a=1, b=2))
[1] "a" "b"

What is a function then?

A function (or subroutine or method or procedure) is a set of instructions packaged as a unit. It may or may not take input and may or may not produce output.

For example, this function takes a number as input, calculates it square, gives us the answer as output:

sqrd = function(n) {
  n^2
}

sqrd(2)
[1] 4

Statements and style

Assignment statements

To create and save an object, you perform an “assignment statement” where the name of the object is on the left, the value of the object is on the right, and in the middle is the assignment operator, =,

an_object = "a string object"

or <-

an_object <- "a string object"

Either <- or = works as the assignment operator though most R aficionados will use <-. However, I prefer (strongly) the = operator since that’s what many other languages use. The ones that don’t use = often use :=. It also takes fewer keystrokes (1 vs 3) to type = compared to <-. I think <- looks funny too. I could keep going…

Style of statements

  • Every command in R must end in a “newline” (what you get when you hit “return”) or semicolon. Newlines are the norm in most R code and I will use them too.

  • If an R statement is incomplete and you enter a newline, R will let you continue the statement:

    new_statement = print("this continues on the
    next line")
    [1] "this continues on the\nnext line"
  • Use plenty of space in your statements to help readability.

    • This is more readable

      a = (100 + 3) - 2
      mean(c(a / 100, 642564624.34))
      [1] 321282313
    • than this

      a=(100+3)-2
      mean(c(a/100,642564624.34))
      [1] 321282313
  • Use “tabs” to set off blocks of code like loops and function definition. Good coding style will make these blocks look obvious since they are indented and spaced nicely. This makes understanding which parts of the code are run when much easier.

  • The “tidyverse” (R packages for data science) has a style guide, https://style.tidyverse.org/, that produces consistent and easy to read code.

Naming conventions

Now that you can create objects using statements, you have to know how to name them. R has some simple rules for this:

A syntactically valid name consists of letters, numbers and the dot or underline characters and starts with a letter or the dot not followed by a number. Names such as “.2way” are not valid, and neither are the reserved words.

This information comes from the help for the make.names command, which you can get type typing ?makes.names where “?” before a function gives you the help page:

?make.names

Then, you convert any string into a valid R variable name using the make.names command:

not_valid_name = "1 way to not be a valid name"
valid_name = make.names(not_valid_name)
print(valid_name)
[1] "X1.way.to.not.be.a.valid.name"

Even when your variable names are valid, knowing the rules for naming is important for the names attribute of objects like vectors and data.frames. When you read your data from a file into R, the column names must be “valid” or enclosed within backticks (``). Some of the nicer functions will use the backticks (e.g., read_csv from tidyverse) so that the name is preserved while others will convert the names into valid ones (e.g., read.csv in the utils package).

A couple tips for naming objects:

  • Err on the side of using longer object names that describe what that object stores: e.g., populationSize is often better than N. Note that R itself doesn’t always obey this rule; e.g., the function c combines values. Never name a function a single letter.

  • Maintain a convention for using multiple words in object names. E.g., underscores or periods for spaces or capitalize every word. The tidyverse style guide suggests:

    Variable and function names should use only lowercase letters, numbers, and _. Use underscores (_) (so called snake case) to separate words within a name.

  • Remember that R is case sensitive, so populationSize is a different object from populationsize.

  • Never use object names that differ only by upper or lower case; this will almost certainly lead to user created bugs.

Types of objects

Scalars

Scalars are the simplest object in R. They hold a single value like a number (called “numeric” by R) or a string (called “character” by R)

number = 3.1459
string = "hello again world"

How do you know these objects are the types you think they are? You can check with the function class, which gives you the value of the object’s class attribute:

class(number)
[1] "numeric"
class(string)
[1] "character"

You can ask for a little more by getting the “structure” of the object using the function str (by the way, this function is another example of a name that is too short; strike two R!).

str(number)
 num 3.15
str(string)
 chr "hello again world"

These commands may not be super useful now, but R has enough different types of object that you’ll want to be able to ask an object about itself at some point.

Vectors

Vectors are everywhere in R. They are simply lists of objects of a common type, like numeric or character. Actually, our scalars were just vectors with a single element. To create a vector, use the very poorly named c or combine function.

number_vector = c(1,10,100,1000)
number_vector
[1]    1   10  100 1000
string_vector = c("hello", "world", "for", "yet", "another", "time")
string_vector
[1] "hello"   "world"   "for"     "yet"     "another" "time"   

To find out the length of the vectors you just created, use the function length

length(number_vector)
[1] 4
length(string_vector)
[1] 6

Accessing elements of a vector is accomplishing using “indexing”. Vectors are index from 1 to the number of elements in the vector. To access an element, use brackets after the object name; printing the second element of string_vector looks like this:

print(string_vector[2])
[1] "world"

Lists

Suppose that you want a vector that mixes numbers and strings. You could try

mixed = c(1,2,3,"one","two","three")

but looking at the result

mixed
[1] "1"     "2"     "3"     "one"   "two"   "three"

you will find that R converted the numbers to strings. That’s because vectors only contain a single object type. This is important for a number of reasons though the one most relevant to using data in R is that data frames, which R uses to store tabular data, use vectors for columns, which means that each column must contain data of the same type. This means that you have to be careful reading in your data if for some reason some columns contains both 1 and "one" and both are supposed to mean the number one. Some care will have to be taken in such cases.

Lists however can contain multiple types. Thus, using the list function works:

mixed = list(1,2,3,"one","two","three")
str(mixed)
List of 6
 $ : num 1
 $ : num 2
 $ : num 3
 $ : chr "one"
 $ : chr "two"
 $ : chr "three"

You can index them just like a vector too, though they require double brackets (more on this later):

mixed[[1]]
[1] 1
mixed[[4]]
[1] "one"

The fact that lists can contain any mix of objects is super useful and comes up in data frames as well. Suppose you want to have one column of your tabular data that has student names, each of which is a string, and one column which is a numerical score for an assignment. The list will allow you to store both a vector of strings and a vector of numbers. Data frames in R are actually just special lists where each element is a vector that stores the same number of elements, which is the number of rows of the table.

Flow control and logical tests

Flow control is a technical term for telling a program which statements or blocks of code to execute. In R, you use “if-else” statements to accomplish this. For example, you can print a statement only if a variable takes a certain value:

test = -10
if (test > 0) {
  print("test is greater than zero")
} else {
  print ("test is not greater than zero")
}
[1] "test is not greater than zero"

The curly brackets enclose the block of statements that get executed if the condition is true or not. Note that the else is on the same line as the final curly bracket of the “if”. The “>” (greater than) symbol is an example of a relational operator; others include equals, “==”, not equals, “!=”, and “greater than or equal”, “>=” (likewise, less than operators exist too).

The outcome of a comparison with a relational operator is a “TRUE” or “FALSE” value, which are “Booleans”. You can combine TRUE and FALSE values with Boolean operators like “&&” (“and”), “||” (“or”), and “!” (“not”). For example, you can check our whether our test variable is greater than zero or less than -1:

test = -10
if (test > 0 || test < -1) {
  print("test is greater than zero or less than -1")
} else {
  print ("test is between zero and -1")
}
[1] "test is greater than zero or less than -1"

Loops

Loops are ways to tell R to perform a block of commands repeatedly. The most useful kind of loop is the “for” loop. First, you create an empty vector. Then, you use the for loop to fill its elements.

vec = c()
for (i in 1:100) {
  vec[i] = i*i
}
str(vec) 
 int [1:100] 1 4 9 16 25 36 49 64 81 100 ...

The structure of the for loop is for (index.object in index.values) { code block }. The first run of the loop sets index.object to the first element of index.value and executes the code block. The next run sets the index.object to the second value of index.value, executes the code block, and so on until the loop has run as many times as there are elements in index.value.

Note that the above loops shows you something else about vectors. You can build them dynamically by just assigning values to their elements. This is a very convenient thing that R lets you do, but many other languages will not (and sometimes for good reasons).

More on functions

Functions are important so we will discuss them in more detail. Functions take “arguments” between the parentheses and these arguments are the data you want the function to use. For example, the runif function generates uniformly distributed random numbers. There are three arguments to the function:

str(runif)
function (n, min = 0, max = 1)  

The first argument is how many numbers we want, the second is the minimum value for them, and the third is the maximum value. Thus, to create 10 random numbers between -1 and 1, you do this:

print(runif(10, -1, 1))
 [1] -0.06522172 -0.73823069  0.39638051 -0.52277184 -0.09437609 -0.21625430
 [7] -0.98774126 -0.54058156  0.10147410  0.01195585

In RStudio, a handy way to see what functions are available or to jog your memory about a function’s name is to begin typing the name and then hit “tab” when you have a few characters. RStudio will show you a list of possible functions and if you hit “tab” again it will complete your typing with the highlighted name. If you’re typing inside the parenthesis, hitting “tab” will help remind you what the arguments to the function are.

In R, the arguments to functions often have “names”. If they do, you can give the argument with it’s name:

print(runif(min = -1, max = 1, n = 10))
 [1] -0.96088721 -0.20486199  0.58995754 -0.63864172 -0.76934296 -0.04030757
 [7] -0.45793170  0.99578967 -0.94528361 -0.95566355

Notice that giving the name of the argument allows us to give the arguments in any order. If an arguments doesn’t have a name, then you must give the arguments in exactly the order they are listed in the definition of the function (see the help for a function with “?name_of_function” to get info on the arguments the function takes).

One of the most powerful parts of programming in any language is writing your own functions. To do this, you start with a name for the function and assign the object to function(arg1, arg2, ...) { function body }. This is called a function definition. For example, the function

runit = function(n = 1) {
  return(runif(n, max = 1, min = 0))
}

generates a random number in the unit interval (0,1) where the number of values you want is the only argument. We can exclude that argument since it has a “default” value of 1 or give the argument explicitly, with or without a name:

print(runit())
[1] 0.1948162
print(runit(n = 2))
[1] 0.1771931 0.5131920
print(runit(5))
[1] 0.05255979 0.33458188 0.32901506 0.36993703 0.03554562

Finally, the function returns whatever is in the “return” function or the last value in the function body. Thus, we can easily capture our random numbers or the output from any function:

rvals = runit(5)
print(rvals)
[1] 0.03440867 0.27611761 0.63422879 0.61022329 0.21320470

One situation that comes up often in data wrangling, processing, and analysis is that you’ll want to perform a small set of calculations on some data, like a column in a data frame, and writing a separate function definition seems like a lot of typing for that. R provides anonymous functions for just these situations. An anonymous function is one you can write quickly in a single line and doesn’t require a name for the function. You start with \(arg_x, arg_y) (for two arguments named arg_x and arg_y) and then write your line of code. For example, this anonymous function

\(x) x^2
\(x) x^2

squares its argument

(\(x) x^2)(2)
[1] 4

Lab

  1. Write a function that takes a single vector argument and that prints out the indices (i.e., location in vector or matrix) that are greater than zero. For example, if you create a vector or normally distributed random variables,

    rvec = rnorm(100)

    then your function should output the numbers between 1 and 100 where the vector is positive. For example,

    your_fuction(rvec)

    would then output something like [1] 3 56 19 40 58 99.

    Tips. Use a for loop.

  2. We’ll plot some COVID-19 data for this question. Use the code below to read in COVID-19 case and death data into the variable world.

    library(tidyverse)
    
    world = read_csv("https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/cases_deaths/full_data.csv")

    Write a function that plots new_cases or new_death from the world data between two dates for a specific country You function should take the following five arguments:

    1. the data you want to plot
    2. “new_cases” or “new_deaths” (so you can select which you want to plot)
    3. the country you want to plot (e.g, Ukraine)
    4. a beginning date
    5. an ending date

    This function should plot the data only between the beginning and ending date. Thanks to the lubridate and dplyr packages in tidyverse, this isn’t too hard. You can use the following code to get a “slice” of the date between two dates (e.g., “2022-01-01” and “2022-08-25”) and for a specific country (e.g., “Ukraine”):

    my_slice = filter(world, date > "2022-01-01", date < "2022-08-25", location == "Ukraine")

    Plotting this slice shows that it works.

    plot(my_slice[["date"]], my_slice[["new_cases"]], type='l')

    Thus, you could be able to call your function, say my_plot_function like this,

    my_plot_function(world, "new_cases", "2022-01-01", "2022-08-25", "Ukraine")

    and get the plot above.