Chapter 1 Introduction to R and Data Structures

By the end of this chapter, you will be able to:

  • Navigate the RStudio interface and use all four panes effectively
  • Write well-structured, commented R scripts with proper headers
  • Create and manipulate vectors, matrices, arrays, data frames, factors, and lists
  • Import and export data from CSV, Excel (.xlsx), and other file formats
  • Install and load R packages, and understand the namespace system

Why this chapter? R is the world’s most widely used statistical programming language in academia and research. Before any analysis can happen, you need to understand the tools and building blocks. This chapter sets up your analytical workspace — by the end, you will have everything you need to load a real dataset and begin exploring it.

  • Computer literacy: Basic familiarity with files, folders, and installing software
  • Mathematics: Basic arithmetic (no calculus or advanced math needed here)
  • R/RStudio: Must be installed — download from CRAN and RStudio

Welcome to your first chapter! By the end of this chapter, you’ll be comfortable navigating RStudio, managing your workspace, importing data from several file formats, and working confidently with every core R data structure: vectors, matrices, arrays, data frames, factors, and lists.

1.1 The R and RStudio Environment

R is the language. RStudio is the program you use to write and run R code comfortably — think of R as the engine and RStudio as the car built around it.

When you open RStudio, you’ll typically see four panes:

Pane Location What it’s for
Source Top-left Where you write and save scripts (.R files) — your permanent, reusable code
Console Bottom-left Where code actually runs, one command at a time
Environment/History Top-right Shows every object currently in memory, and what commands you’ve run
Files/Plots/Packages/Help Bottom-right Browse files, view graphs, manage packages, read documentation

The Source pane is for code you want to keep. The Console is for code you want to try out once. A common beginner habit is typing everything directly into the Console — resist this. Write it in a script so you (and your future self) can re-run it later.

1.1.1 R Script Structure: Headers, Comments, and Timestamps

When writing code in R scripts (.R files) or R Markdown (.Rmd), maintaining a consistent, well-documented structure is essential for reproducibility and clarity.

1.1.1.1

  1. Script Header Every R script should start with a header comment block describing the script’s metadata (course, name, author, date, and description).
# ==============================================================================
# Course: Statistical Computing and Non-Parametric Inference Using R
# Script: Unit 1 - R Basics & Data Structures
# Author: Shrikrishna Bhat K
# Date: 2026-07-13
# Description: Examples and exercises covering basic R commands and data structures.
# ==============================================================================

1.1.1.2

  1. Code Comments Use the # symbol in R to add comments. Comments are ignored by R during execution and are used to explain why the code does what it does.
x <- 5  # Assign the value 5 to object x

1.1.1.3

  1. Execution Timestamps Adding a timestamp print statement at the beginning or end of your script execution is a best practice. It documents exactly when the calculations were run.
# Print current date and time
Sys.time()
#> [1] "2026-07-23 17:09:30 UTC"
# Print formatted date and time
cat("Execution Timestamp:", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), "\n")
#> Execution Timestamp: 2026-07-23 17:09:30

1.1.2 Getting help

Every function and package in R has documentation. Here are the core ways to find help:

# 1. Open documentation for a specific function
?mean          
help(median)   

# 2. Search all installed help pages for a keyword or phrase
??"confidence interval"   
help.search("weighted mean")

# 3. Open the documentation index for a specific package
help(package = "readxl")

1.1.3 Using Packages & Namespaces

R functions are organized into packages. To use a package’s functions, you must first install it once from CRAN, and then load it into your current R session:

# Install once from CRAN
install.packages("dplyr")

# Load it in every new script
library(dplyr)

Sometimes you only want to use a single function from a package without loading the entire package into your workspace, or you want to prevent name conflicts (when two packages have functions with the same name). In R, you can access a function directly from its namespace using the double colon (::) operator:

# Use the select() function from the dplyr package explicitly
dplyr::select(mtcars, mpg, hp)

This namespace notation is highly recommended for clean, reproducible code!

1.2 Workspace Management

Your workspace is the collection of every object (data, variables, functions) currently loaded in R’s memory.

alpha <- 5
b <- "hello"
scores <- c(88, 76, 92, 65)

ls()  # list everything currently in your workspace
#> [1] "alpha"  "b"      "scores" "x"

Removing objects:

rm(b)
ls()
#> [1] "alpha"  "scores" "x"

Clearing everything:

rm(list = ls())   # wipes the entire workspace - use with care!

Saving and restoring your work:

save.image("my_session.RData")   # saves everything
load("my_session.RData")          # restores it later

Working directory — this is the folder R looks in by default when you read or write files:

getwd()                     # where am I?
setwd("~/Documents/RCourse") # change to alpha specific folder

setwd() with a hardcoded path (like "C:/Users/Yourname/Desktop") will break the moment someone else — or you, on a different computer — tries to run your script. If you’re working in an RStudio Project (.Rproj file), you never need setwd() at all; the working directory is set automatically. This habit will save you real pain later.

  1. Create three objects: your age (numeric), your name (character), and a logical value for whether you’ve used R before.
  2. List your workspace with ls().
  3. Remove just the logical value.
  4. Confirm it’s gone.

1.3 Importing Data

Real analysis starts with real data, and real data rarely arrives as R code — it arrives as files. R can read almost any format; here are the three you’ll use constantly.

This section uses student_scores, a small dataset of 40 students with their group, exam score, and hours studied — provided in three formats (.csv, .xlsx, .txt) so you can practice importing all three. See the Appendix for its full column description.

1.3.1 CSV files (the workhorse format)

scores_csv <- read.csv("data/student_scores.csv")
head(scores_csv, 3)

1.3.2 Excel files

library(readxl)
scores_xlsx <- read_excel("data/student_scores.xlsx")
head(scores_xlsx, 3)

1.3.3 Tab-delimited text files

scores_txt <- read.table("data/student_scores.txt", header = TRUE, sep = "\t")
head(scores_txt, 3)

1.3.4 Inspecting what you just imported

Three functions you should run on every dataset the moment you load it:

dim(scores_csv)     # rows x columns
#> [1] 40  5
str(scores_csv)     # structure: column names, types, first few values
#> 'data.frame':    40 obs. of  5 variables:
#>  $ id           : int  1 2 3 4 5 6 7 8 9 10 ...
#>  $ name         : chr  "Student_01" "Student_02" "Student_03" "Student_04" ...
#>  $ group        : chr  "A" "A" "A" "B" ...
#>  $ score        : int  63 66 49 89 69 95 92 69 77 92 ...
#>  $ hours_studied: num  4.7 5.3 4.6 4.4 1.5 4.5 1.4 7.3 3 5.3 ...
summary(scores_csv) # quick numeric summary of every column
#>        id               name          group        score       hours_studied  
#>  Min.   : 1.00   Length   :40   Length   :40   Min.   :49.00   Min.   :1.100  
#>  1st Qu.:10.75   N.unique :40   N.unique : 2   1st Qu.:56.00   1st Qu.:3.075  
#>  Median :20.50   N.blank  : 0   N.blank  : 0   Median :69.00   Median :4.650  
#>  Mean   :20.50   Min.nchar:10   Min.nchar: 1   Mean   :68.47   Mean   :4.840  
#>  3rd Qu.:30.25   Max.nchar:10   Max.nchar: 1   3rd Qu.:76.25   3rd Qu.:6.450  
#>  Max.   :40.00                                 Max.   :95.00   Max.   :9.800

str() is arguably the single most useful function in this entire chapter. It instantly tells you: how many rows and columns you have, what type each column is (numeric, character, factor…), and a preview of the actual values. Make it a habit to run str() immediately after importing anything.

Notice that read.csv() and read_excel() can treat text columns differently — read.csv() gives you plain character vectors by default, while read_excel() also returns characters, but numeric columns from Excel can sometimes import with unexpected decimal points or date formats if the original spreadsheet was formatted oddly. Always run str() and check.

Import data/student_scores.txt yourself, then check: does str() show group as a character column or something else? What about in the .csv version? Are they the same?

1.3.5 Interactive Editing inside RStudio

While we typically import prepared files, R also provides a built-in spreadsheet-like GUI editor for making quick changes to a data frame or creating new data from scratch.

  • edit(): Opens the editor. Crucially, edit() returns the modified data frame, so you must assign the result back to an object to save your changes (e.g., my_data <- edit(my_data)).
  • fix(): A shortcut that modifies the data frame in-place without needing an assignment (e.g., fix(my_data)).

Never run edit() or fix() inside an R Markdown (.Rmd) file that you plan to compile/render. Because these functions open an interactive window, the compiler will hang indefinitely waiting for user input, causing the build to fail. Only use them interactively in the R Console (we demonstrate them using eval=FALSE in code chunks so they don’t run during compile).

Here is how you can use them:

# INTERACTIVE ONLY — Never run inside an Rmd code block during render!
# Edit an existing dataset
scores_edited <- edit(scores_csv)

# INTERACTIVE ONLY
# Edit in-place
fix(scores_csv)

Historical Note: The stringsAsFactors Shift In older R versions (prior to version 4.0.0), any character string column imported via read.csv() or read.table() was automatically converted into a factor by default. To prevent this, developers had to write stringsAsFactors = FALSE on almost every import. Since R 4.0.0, the default behavior has changed to stringsAsFactors = FALSE. You will still see stringsAsFactors = FALSE in older textbooks, online code snippets, and legacy projects. In modern R, it is rarely needed unless you explicitly want to override a factor conversion!

Exercise: Create a new dataset using edit()

Since we cannot run interactive GUI functions during Bookdown compiling, try this in your RStudio Console:

  1. Create a blank template with columns of different classes (character, integer, logical): my_log <- data.frame(Name = character(), Age = integer(), Present = logical(), stringsAsFactors = FALSE)

  2. Run the editor: my_log <- edit(my_log)

  3. A grid will pop up. Enter 3 rows of values. R will automatically enforce the column types you defined (e.g., entering letters in the Age column or Present column will warn you or coerce them).

  4. Close the grid. Run str(my_log) to verify that your entries were saved and that the column classes remain correct.

1.4 Vectors

A vector is R’s most basic data structure: an ordered collection of values, all of the same type.

Visual Mental Map of R Data Structures

Before dive-bombing into vectors, use this simple mental map to visualize R’s primary data structures based on their dimensionality and type constraints:

              Type Constraints
          Homogeneous      Heterogeneous
      +-----------------+-----------------+
1D    |     Vector      |  List (Flexible)|
      +-----------------+-----------------+
2D    |     Matrix      |   Data Frame    |
      +-----------------+-----------------+
N-D   |     Array       |        -        |
      +-----------------+-----------------+

Note: A List is technically 1-dimensional (it is a vector of elements), but it is recursive and recursive lists can contain other lists, making it the most flexible container in R.

1.4.1 Creating vectors

ages <- c(23, 45, 31, 29, 52)
names_vec <- c("Asha", "Ravi", "Meera", "Kiran", "Divya")
passed <- c(TRUE, FALSE, TRUE, TRUE, FALSE)

ages
#> [1] 23 45 31 29 52
class(ages)
#> [1] "numeric"
class(names_vec)
#> [1] "character"
class(passed)
#> [1] "logical"

Other common ways to build vectors:

seq(1, 20, by = 4)        # alpha sequence
#> [1]  1  5  9 13 17
seq_len(6)                 # 1 through 6
#> [1] 1 2 3 4 5 6
rep(c("Low", "High"), times = 3)   # repeat alpha pattern
#> [1] "Low"  "High" "Low"  "High" "Low"  "High"
rep(1:3, each = 2)                  # repeat each element
#> [1] 1 1 2 2 3 3

1.4.2 Indexing and Subsetting Vectors

Subsetting means pulling out specific elements — and this is where a lot of real data wrangling happens. You can subset by position (using indices), by value (using logical criteria), or by name:

# 1. Subsetting by position
ages[1]            # first element
#> [1] 23
ages[c(1, 3)]      # first and third
#> [1] 23 31
ages[-1]           # everything EXCEPT the first
#> [1] 45 31 29 52
ages[2:4]          # elements two to four
#> [1] 45 31 29
ages[-(2:4)]       # all elements except two to four
#> [1] 23 52
# 2. Subsetting by value (logical indexing)
ages[ages > 30]    # only ages above 30
#> [1] 45 31 52
ages[ages %in% c(23, 52)] # only elements that are in the set {23, 52}
#> [1] 23 52

Logical indexing — ages[ages > 30] — is one of the most powerful ideas in R. ages > 30 produces a vector of TRUE/FALSE values (one per element), and putting that inside [ ] keeps only the elements where it’s TRUE. You will use this constantly, for the rest of the course and beyond.

Named Vectors

You can also assign names to the elements of a vector, allowing you to subset them by name (like lookup tables):

# Assign names to the ages vector
names(ages) <- c("Asha", "Ravi", "Meera", "Kiran", "Divya")
ages
#>  Asha  Ravi Meera Kiran Divya 
#>    23    45    31    29    52
# Subset by name
ages["Meera"]
#> Meera 
#>    31
ages[c("Asha", "Divya")]
#>  Asha Divya 
#>    23    52

1.4.3 Essential Vector Functions

R provides several built-in functions to sort, summarize, and clean up vectors:

# 1. sort() - return vector sorted (default ascending)
sort(ages)
#>  Asha Kiran Meera  Ravi Divya 
#>    23    29    31    45    52
sort(ages, decreasing = TRUE)
#> Divya  Ravi Meera Kiran  Asha 
#>    52    45    31    29    23
# 2. rev() - return vector reversed
rev(ages)
#> Divya Kiran Meera  Ravi  Asha 
#>    52    29    31    45    23
# 3. unique() - see unique values
unique(c(1, 1, 2, 3, 3, 3))
#> [1] 1 2 3
# 4. table() - count frequencies of values
table(c("Yes", "No", "Yes", "Yes", "No"))
#> 
#>  No Yes 
#>   2   3

1.4.4 Vectorized operations

R applies math to whole vectors at once — no loop required.

heights_cm <- c(150, 162, 158, 170, 145)
heights_m <- heights_cm / 100
heights_m
#> [1] 1.50 1.62 1.58 1.70 1.45
bonus <- c(2, 2, 2, 2, 2)
ages + bonus
#>  Asha  Ravi Meera Kiran Divya 
#>    25    47    33    31    54

Example with a second dataset — mtcars (built into R):

data(mtcars)
head(mtcars[, c("mpg", "hp")], 4)
# Convert miles-per-gallon to km-per-litre for every car at once
mtcars$kpl <- mtcars$mpg * 0.425
head(mtcars$kpl)
#> [1] 8.9250 8.9250 9.6900 9.0950 7.9475 7.6925

Using the ages vector above: (1) extract everyone under 30; (2) compute everyone’s age in months; (3) count how many people are 30 or older using sum(ages >= 30) (this works because TRUE counts as 1 and FALSE as 0).

1.4.5 Converting Between Data Types (Coercion)

In R, you will frequently need to convert a variable from one data type to another. This process is called coercion. R provides a set of as....() functions for this purpose:

Function Output Data Type R Example Output
as.logical() Logical (TRUE/FALSE) as.logical(c(1, 0, 5)) TRUE FALSE TRUE
as.numeric() Numeric (Floating point/double) as.numeric(c(TRUE, FALSE)) 1 0
as.character() Character (Text string) as.character(c(1.5, TRUE)) "1.5" "TRUE"
as.factor() Factor (Categorical with levels) as.factor(c("A", "B", "A")) A B A (Levels: A B)

Let’s see coercion in action:

# 1. Convert logicals to numbers (useful for summing TRUEs)
logical_vec <- c(TRUE, FALSE, TRUE)
as.numeric(logical_vec)
#> [1] 1 0 1
# 2. Convert character strings to numbers (must contain valid numbers!)
char_num <- c("24", "45", "30")
as.numeric(char_num)
#> [1] 24 45 30
# 3. Warning: invalid conversions return NA (Not Available)
as.numeric(c("24", "Asha", "30"))
#> [1] 24 NA 30

R Coercion Hierarchy If you try to combine different data types inside a single vector using c(), R will automatically coerce them to the most flexible type to maintain homogeneity. The hierarchy from least flexible (strictest) to most flexible is: \[\text{Logical} \longrightarrow \text{Integer} \longrightarrow \text{Numeric} \longrightarrow \text{Character}\] For example, combining a number and a text string will result in R converting everything to characters: c(10, "hello") becomes c("10", "hello").

1.5 Matrices

A matrix extends a vector to two dimensions — rows and columns — but like a vector, every element must be the same type.

scores_mat <- matrix(c(85, 90, 78, 92, 88, 76, 95, 81, 89),
                      nrow = 3, ncol = 3, byrow = TRUE)
rownames(scores_mat) <- c("Test1", "Test2", "Test3")
colnames(scores_mat) <- c("StudentA", "StudentB", "StudentC")
scores_mat
#>       StudentA StudentB StudentC
#> Test1       85       90       78
#> Test2       92       88       76
#> Test3       95       81       89

Indexing a matrix uses [row, column]:

scores_mat[1, ]        # entire first row
#> StudentA StudentB StudentC 
#>       85       90       78
scores_mat["Test2", ]  # same idea, using the row name
#> StudentA StudentB StudentC 
#>       92       88       76
scores_mat[, "StudentC"]  # entire column
#> Test1 Test2 Test3 
#>    78    76    89
scores_mat[2, 3]        # single element
#> [1] 76

Row/column summaries:

rowMeans(scores_mat)
#>    Test1    Test2    Test3 
#> 84.33333 85.33333 88.33333
colMeans(scores_mat)
#> StudentA StudentB StudentC 
#> 90.66667 86.33333 81.00000
apply(scores_mat, 1, max)   # max of each row (margin = 1)
#> Test1 Test2 Test3 
#>    90    92    95
apply(scores_mat, 2, min)   # min of each column (margin = 2)
#> StudentA StudentB StudentC 
#>       85       81       76

Second example — a distance matrix from mtcars:

subset_cars <- mtcars[1:5, c("mpg", "hp", "wt")]
dist_mat <- as.matrix(dist(subset_cars))
round(dist_mat, 1)
#>                   Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive
#> Mazda RX4               0.0           0.3       17.1            0.7
#> Mazda RX4 Wag           0.3           0.0       17.1            0.5
#> Datsun 710             17.1          17.1        0.0           17.1
#> Hornet 4 Drive          0.7           0.5       17.1            0.0
#> Hornet Sportabout      65.0          65.0       82.1           65.1
#>                   Hornet Sportabout
#> Mazda RX4                      65.0
#> Mazda RX4 Wag                  65.0
#> Datsun 710                     82.1
#> Hornet 4 Drive                 65.1
#> Hornet Sportabout               0.0

In apply(X, MARGIN, FUN), MARGIN = 1 means “do this for every row” and MARGIN = 2 means “do this for every column.” Students often mix these up — a helpful trick: 1 looks like a tall vertical stroke (rows go down), 2 has a flat bottom (columns go across).

1.6 Arrays

An array generalizes a matrix to three or more dimensions. Think of it as stacking several matrices of the same shape.

quarterly_sales <- array(
  data = 1:24,
  dim = c(2, 3, 4),   # 2 rows, 3 columns, 4 "layers" (quarters)
  dimnames = list(
    c("ProductA", "ProductB"),
    c("RegionN", "RegionS", "RegionE"),
    c("Q1", "Q2", "Q3", "Q4")
  )
)
quarterly_sales[, , "Q1"]     # the entire Q1 matrix
#>          RegionN RegionS RegionE
#> ProductA       1       3       5
#> ProductB       2       4       6
quarterly_sales["ProductA", "RegionS", ]  # ProductA in RegionS, across all quarters
#> Q1 Q2 Q3 Q4 
#>  3  9 15 21
dim(quarterly_sales)
#> [1] 2 3 4

Using quarterly_sales: extract every value for ProductB in RegionE, across all four quarters. Then compute the total across all quarters for ProductA in RegionN.

1.7 Data Frames

A data frame is the structure you will use the most in this entire course. It looks like a spreadsheet: rows are observations, columns are variables, and — unlike a matrix — different columns can hold different types.

survey <- data.frame(
  id = 1:5,
  age = c(23, 45, 31, 29, 52),
  city = c("Chennai", "Pune", "Chennai", "Delhi", "Pune"),
  passed_exam = c(TRUE, FALSE, TRUE, TRUE, FALSE)
)
survey
str(survey)
#> 'data.frame':    5 obs. of  4 variables:
#>  $ id         : int  1 2 3 4 5
#>  $ age        : num  23 45 31 29 52
#>  $ city       : chr  "Chennai" "Pune" "Chennai" "Delhi" ...
#>  $ passed_exam: logi  TRUE FALSE TRUE TRUE FALSE

Selecting columns and rows:

survey$age             # alpha single column, by name
#> [1] 23 45 31 29 52
survey[, "city"]        # same thing, different syntax
#> [1] "Chennai" "Pune"    "Chennai" "Delhi"   "Pune"
survey[survey$age > 30, ]   # rows where age > 30 (logical indexing on alpha data frame!)
survey[1:2, c("id", "city")]  # specific rows AND columns

Adding a new column:

survey$age_group <- ifelse(survey$age < 35, "Younger", "Older")
survey

Second example — built-in iris dataset:

data(iris)
str(iris)
#> 'data.frame':    150 obs. of  5 variables:
#>  $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
#>  $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
#>  $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
#>  $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
#>  $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
# Average petal length per species
tapply(iris$Petal.Length, iris$Species, mean)
#>     setosa versicolor  virginica 
#>      1.462      4.260      5.552

tapply(X, INDEX, FUN) applies a function to X, split by groups defined in INDEX. It’s a compact way to answer “what’s the average of this, broken down by that group?” — you’ll use this pattern constantly in Chapter 2.

1.8 Factors

A factor represents categorical data with a fixed, known set of possible values (called levels). Factors look like character vectors but behave very differently under the hood.

severity <- factor(c("Low", "High", "Medium", "Low", "High"),
                    levels = c("Low", "Medium", "High"))
severity
#> [1] Low    High   Medium Low    High  
#> Levels: Low Medium High
levels(severity)
#> [1] "Low"    "Medium" "High"
as.integer(severity)   # internally, factors are stored as integers!
#> [1] 1 3 2 1 3

Ordered factors carry ranking information:

severity_ordered <- factor(c("Low", "High", "Medium"),
                            levels = c("Low", "Medium", "High"),
                            ordered = TRUE)
severity_ordered
#> [1] Low    High   Medium
#> Levels: Low < Medium < High
severity_ordered[1] < severity_ordered[2]   # "Low" < "Medium" — TRUE, because it's ordered
#> [1] TRUE

A plain (unordered) factor doesn’t support < or > comparisons in any meaningful way, even if it looks like it should. If your categories have a natural order (Low/Medium/High, Mild/Moderate/Severe), always set ordered = TRUE — Chapter 4 depends on this for trend tests.

Second example — using PlantGrowth (built into R):

data(PlantGrowth)
str(PlantGrowth)   # 'group' is already alpha factor with 3 levels
#> 'data.frame':    30 obs. of  2 variables:
#>  $ weight: num  4.17 5.58 5.18 6.11 4.5 4.61 5.17 4.53 5.33 5.14 ...
#>  $ group : Factor w/ 3 levels "ctrl","trt1",..: 1 1 1 1 1 1 1 1 1 1 ...
levels(PlantGrowth$group)
#> [1] "ctrl" "trt1" "trt2"
table(PlantGrowth$group)   # how many observations per level
#> 
#> ctrl trt1 trt2 
#>   10   10   10

1.9 Lists

A list is the most flexible data structure in R. Unlike vectors, matrices, or data frames, a list does not enforce any structure or data type constraints. It is an ordered collection where each element can be a completely different type or size — a list can contain vectors, matrices, data frames, or even other lists.

Let’s create a list to record a student’s profile:

student_record <- list(
  name = "Anjali Rao",
  scores = c(78, 85, 91, 88),
  passed = TRUE,
  metadata = data.frame(term = "Fall 2026", credits = 4)
)
print(student_record)
#> $name
#> [1] "Anjali Rao"
#> 
#> $scores
#> [1] 78 85 91 88
#> 
#> $passed
#> [1] TRUE
#> 
#> $metadata
#>        term credits
#> 1 Fall 2026       4

1.9.1 The Big List Indexing “Gotcha” (Single vs. Double Brackets)

One of the most common syntax errors students face in R is the difference between single brackets [ ], double brackets [[ ]], and the dollar sign $. Let’s break it down using a simple analogy:

The Cargo Train Analogy: Think of a list as a train made of cargo cars. Each car contains some cargo inside it.

  • [ Single Brackets ] gets you the train car(s). The result is always another list, even if you only select one element.
  • [[ Double Brackets ]] opens the car and gets you the cargo inside. The result is the actual object (vector, data frame, etc.) stored there.
  • $ Dollar Sign is a shortcut for named cargo. It behaves exactly like [[ ]], extracting the raw cargo by its name instead of its index.

Let’s test this in code:

# 1. Using Single Brackets [ ]
car_list <- student_record[1]
class(car_list)     # Still alpha list! 
#> [1] "list"
print(car_list)
#> $name
#> [1] "Anjali Rao"
# 2. Using Double Brackets [[ ]]
cargo_val <- student_record[[1]]
class(cargo_val)    # A character vector!
#> [1] "character"
print(cargo_val)
#> [1] "Anjali Rao"
# 3. Using the Dollar Sign $
cargo_by_name <- student_record$name
class(cargo_by_name)
#> [1] "character"
print(cargo_by_name)
#> [1] "Anjali Rao"

1.9.2 Accessing Nested Elements

If you want to extract a sub-element inside a list (like the third score from the scores vector inside student_record), you must extract the vector first using [[ ]] or $, and then index it:

# Step 1: Get the vector
my_scores <- student_record$scores 
# Step 2: Get the 3rd element
my_scores[3]
#> [1] 91
# Combine both steps in a single line:
student_record$scores[3]
#> [1] 91

1.9.3 Lists in Statistical Functions

Many statistical functions in R (such as t.test(), chisq.test(), wilcox.test()) calculate their results and bundle them into a list. Knowing how to extract elements from a list is crucial for automating statistical workflows.

# Run a simple t-test comparing weight in two plant groups
test_result <- t.test(PlantGrowth$weight[PlantGrowth$group == "ctrl"],
                      PlantGrowth$weight[PlantGrowth$group == "trt1"])

class(test_result)     # It's alpha list!
#> [1] "htest"
names(test_result)     # See what variables are in the list
#>  [1] "statistic"   "parameter"   "p.value"     "conf.int"    "estimate"   
#>  [6] "null.value"  "stderr"      "alternative" "method"      "data.name"
# Extract specific results:
p_val <- test_result$p.value
conf_interval <- test_result$conf.int

cat("P-value:", p_val, "\n")
#> P-value: 0.2503825
print(conf_interval)
#> [1] -0.2875162  1.0295162
#> attr(,"conf.level")
#> [1] 0.95

Exercise: List Indexing Practice

  1. Create a list of your favorite things: my_favorites <- list(movies = c("Inception", "Interstellar"), rating = 9.5)

  2. Extract the second movie ("Interstellar") in one line of code.

  3. Run class(my_favorites[1]) and class(my_favorites[[1]]). Explain in your own words why they are different.

Remember: almost every statistical test in R returns its results as a list. Knowing how to pull out $p.value or $conf.int from that list is a fundamental skill you will use throughout this course.

1.10 Comparing All Six Structures

Structure Dimensions Data Types Typical Use
Vector 1D Single type A single variable’s values
Matrix 2D Single type Numeric computation, algebra
Array N-D Single type Multi-way numeric data
Data Frame 2D Mixed (per column) Real datasets, rows = observations
Factor 1D Categorical labels Grouping / categorical variables
List Flexible Mixed (any) Bundling heterogeneous objects

For each of the following real-world situations, decide which structure fits best, and why:

  1. The monthly temperature readings for one city over a year
  2. A gradebook with student names, three test scores, and a pass/fail column
  3. Sales figures for 5 products, across 4 regions, across 4 quarters
  4. The blood type of 200 patients (A, B, AB, O)
  5. The complete output of a clustering analysis (cluster assignments, centers, and a summary data frame)
  • RStudio’s four panes each have a distinct job — write reusable code in Source, not the Console.
  • Always run str() immediately after importing any dataset.
  • Vectors, matrices, and arrays require a single data type; data frames and lists allow mixed types.
  • Logical indexing (x[x > 30]) is the single most useful indexing pattern you’ll use all course.
  • Factors store categorical data efficiently and — when ordered = TRUE — carry ranking information that later chapters depend on.
  • Statistical test functions return lists; knowing how to extract $p.value and similar elements is essential going forward.

Next, in Chapter 2, we’ll build on data frames and vectors to compute descriptive statistics and write our own R functions and loops.


Chapter 1 covered:

  • RStudio: Source, Console, Environment, and Files/Plots panes; scripts vs. interactive commands
  • R data types: numeric (double and integer), character, logical, complex — and automatic coercion rules
  • Core data structures:
    • vector: 1D, homogeneous
    • matrix / array: 2D or nD, homogeneous
    • data.frame: 2D, heterogeneous — the workhorse of data analysis
    • factor: categorical variable with defined levels
    • list: heterogeneous container for any R objects
  • Data import: read.csv(), readxl::read_excel(), read.table()
  • Package management: install.packages() once; library() each session

Coming up: Chapter 2 applies these data structures to real data analysis — computing descriptive statistics, visualising distributions, and writing reusable functions and loops.

1.11 Lab 1: Building Your Analytical Workspace

Objective: Apply everything from Chapter 1 in one connected workflow.

Dataset: We will create a small dataset from scratch to simulate a course registration scenario.

1.11.1 Step 1: Write a Proper Script Header

Open a new R script (File → New File → R Script). Add this header:

# ==============================================================================
# Course: Statistical Computing and Non-Parametric Inference Using R
# Lab:    Chapter 1 — Building Your Analytical Workspace
# Author: [Your Name]
# Date:   [Today's Date]
# ==============================================================================

1.11.2 Step 2: Create and Manipulate Vectors

# Student names (character vector)
students <- c("Alice", "Bob", "Cynthia", "David", "Eva",
              "Frank", "Grace", "Hani", "Ivan", "Jana")

# Mid-semester scores (numeric vector, some missing)
scores <- c(78, 85, NA, 92, 67, 88, NA, 74, 95, 81)

# Check: how many students have missing scores?
sum(is.na(scores))
#> [1] 2
# Compute mean score, ignoring NA
mean(scores, na.rm = TRUE)
#> [1] 82.5

1.11.3 Step 3: Build a Data Frame

course_df <- data.frame(
  student   = students,
  score     = scores,
  programme = factor(c("MPH", "MSc", "MPH", "MSc", "MSc",
                       "MPH", "MSc", "MPH", "MSc", "MPH")),
  stringsAsFactors = FALSE
)

# Inspect the structure
str(course_df)
#> 'data.frame':    10 obs. of  3 variables:
#>  $ student  : chr  "Alice" "Bob" "Cynthia" "David" ...
#>  $ score    : num  78 85 NA 92 67 88 NA 74 95 81
#>  $ programme: Factor w/ 2 levels "MPH","MSc": 1 2 1 2 2 1 2 1 2 1
summary(course_df)
#>       student       score      programme
#>  Length   :10   Min.   :67.0   MPH:5    
#>  N.unique :10   1st Qu.:77.0   MSc:5    
#>  N.blank  : 0   Median :83.0            
#>  Min.nchar: 3   Mean   :82.5            
#>  Max.nchar: 7   3rd Qu.:89.0            
#>                 Max.   :95.0            
#>                 NAs    :2

1.11.4 Step 4: Subset and Transform

# Students with scores above 80
course_df[!is.na(course_df$score) & course_df$score > 80, ]
# Add a Pass/Fail column
course_df$result <- ifelse(course_df$score >= 75, "Pass", "Fail")
course_df$result[is.na(course_df$score)] <- "Absent"
print(course_df)
#>    student score programme result
#> 1    Alice    78       MPH   Pass
#> 2      Bob    85       MSc   Pass
#> 3  Cynthia    NA       MPH Absent
#> 4    David    92       MSc   Pass
#> 5      Eva    67       MSc   Fail
#> 6    Frank    88       MPH   Pass
#> 7    Grace    NA       MSc Absent
#> 8     Hani    74       MPH   Fail
#> 9     Ivan    95       MSc   Pass
#> 10    Jana    81       MPH   Pass

1.11.5 Step 5: Save and Reload

write.csv(course_df, "lab1_output.csv", row.names = FALSE)
reloaded <- read.csv("lab1_output.csv")
identical(course_df$student, reloaded$student)  # Should be TRUE
#> [1] TRUE

1.11.6 Lab Exercises

  1. Add a column adjusted_score that adds 5 bonus points to every non-missing score, capped at 100.
  2. How many students in each programme have a score above 80? Use table() or manual subsetting.
  3. Save the MPH students only to a separate CSV called lab1_mph.csv.