Chapter 1 Introduction to R and Data Structures
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 Getting help
Every function and package in R has documentation. Here are the core ways to find help:
1.1.2 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:
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:
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.
#> [1] "a" "b" "scores"
Removing objects:
#> [1] "a" "scores"
Clearing everything:
Saving and restoring your work:
Working directory — this is the folder R looks in by default when you read or write files:
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.
- Create three objects: your age (numeric), your name (character), and a logical value for whether you’ve used R before.
- List your workspace with
ls(). - Remove just the logical value.
- 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.4 Inspecting what you just imported
Three functions you should run on every dataset the moment you load it:
#> [1] 40 5
#> '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 ...
#> 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:
- Create a blank template with columns of different classes (character, integer, logical):
my_log <- data.frame(Name = character(), Age = integer(), Present = logical(), stringsAsFactors = FALSE) - Run the editor:
my_log <- edit(my_log) - 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
Agecolumn orPresentcolumn will warn you or coerce them). - 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
#> [1] "numeric"
#> [1] "character"
#> [1] "logical"
Other common ways to build vectors:
#> [1] 1 5 9 13 17
#> [1] 1 2 3 4 5 6
#> [1] "Low" "High" "Low" "High" "Low" "High"
#> [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] 23
#> [1] 23 31
#> [1] 45 31 29 52
#> [1] 45 31 29
#> [1] 23 52
#> [1] 45 31 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.
1.4.3 Essential Vector Functions
R provides several built-in functions to sort, summarize, and clean up vectors:
#> Asha Kiran Meera Ravi Divya
#> 23 29 31 45 52
#> Divya Ravi Meera Kiran Asha
#> 52 45 31 29 23
#> Divya Kiran Meera Ravi Asha
#> 52 29 31 45 23
#> [1] 1 2 3
#>
#> No Yes
#> 2 3
1.4.4 Vectorized operations
R applies math to whole vectors at once — no loop required.
#> [1] 1.50 1.62 1.58 1.70 1.45
#> Asha Ravi Meera Kiran Divya
#> 25 47 33 31 54
Example with a second dataset — mtcars (built into R):
# 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
#> [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]:
#> StudentA StudentB StudentC
#> 85 90 78
#> StudentA StudentB StudentC
#> 92 88 76
#> Test1 Test2 Test3
#> 78 76 89
#> [1] 76
Row/column summaries:
#> Test1 Test2 Test3
#> 84.33333 85.33333 88.33333
#> StudentA StudentB StudentC
#> 90.66667 86.33333 81.00000
#> Test1 Test2 Test3
#> 90 92 95
#> 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
#> Q1 Q2 Q3 Q4
#> 3 9 15 21
#> [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#> '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:
#> [1] 23 45 31 29 52
#> [1] "Chennai" "Pune" "Chennai" "Delhi" "Pune"
Adding a new column:
Second example — built-in iris dataset:
#> '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 ...
#> 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
#> [1] "Low" "Medium" "High"
#> [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
#> [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.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 ...
#> [1] "ctrl" "trt1" "trt2"
#>
#> 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] "list"
#> $name
#> [1] "Anjali Rao"
# 2. Using Double Brackets [[ ]]
cargo_val <- student_record[[1]]
class(cargo_val) # A character vector!#> [1] "character"
#> [1] "Anjali Rao"
#> [1] "character"
#> [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
#> [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 a list!#> [1] "htest"
#> [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
#> [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:
- The monthly temperature readings for one city over a year
- A gradebook with student names, three test scores, and a pass/fail column
- Sales figures for 5 products, across 4 regions, across 4 quarters
- The blood type of 200 patients (A, B, AB, O)
- 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.valueand 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.