# Let's build a data frame for the 2025-26 Golden State Warriors roster
warriors <- data.frame(
player = c("Stephen Curry", "Klay Thompson", "Draymond Green",
"Andrew Wiggins", "Kevon Looney", "Jonathan Kuminga",
"Moses Moody", "Brandin Podziemski", "Gary Payton II",
"Chris Paul"),
jersey_num = c(30, 11, 23, 22, 5, 00, 4, 2, 8, 3),
position = c("PG", "SG", "PF", "SF", "C", "SF", "SG", "PG", "SG", "PG"),
age = c(37, 34, 34, 29, 28, 22, 22, 21, 32, 40),
ppg = c(26.4, 17.9, 8.5, 14.2, 6.3, 16.1, 9.4, 12.8, 7.1, 9.0),
is_allstar = c(TRUE, TRUE, TRUE, FALSE, FALSE, FALSE,
FALSE, FALSE, FALSE, TRUE)
)Lab 5: Data Frames and Matrices
BI 255 · Bethune-Cookman University · Fall 2026
From Vectors to Tables: NBA Roster Data
So far we’ve worked with individual vectors — one column of data at a time. Real biological and sports data comes in tables: multiple columns with different types of information about the same subjects.
Today we learn data frames (R’s version of a spreadsheet) and matrices (tables of numbers only).
By the end of this lab you will be able to:
- Create a data frame using
data.frame() - Explore a data frame with
str(),head(),tail(),dim(),nrow(),ncol() - Access columns with
$and[ ] - Create a matrix using
matrix() - Understand the key difference between data frames and matrices
Part 1: What Is a Data Frame?
A data frame is a table where: - Each row is one observation (e.g., one player) - Each column is one variable (e.g., points, rebounds, position) - Each column can have a different data type (numbers AND text can live together)
This is exactly like a spreadsheet, and it’s the most important data structure in R for biological data.
Part 2: Exploring a Data Frame
Once you have a data frame, these functions help you understand its contents immediately:
# How big is it? (rows, columns)
dim(warriors)[1] 10 6
nrow(warriors) # Number of rows (players)[1] 10
ncol(warriors) # Number of columns (variables)[1] 6
# Show the first 6 rows
head(warriors) player jersey_num position age ppg is_allstar
1 Stephen Curry 30 PG 37 26.4 TRUE
2 Klay Thompson 11 SG 34 17.9 TRUE
3 Draymond Green 23 PF 34 8.5 TRUE
4 Andrew Wiggins 22 SF 29 14.2 FALSE
5 Kevon Looney 5 C 28 6.3 FALSE
6 Jonathan Kuminga 0 SF 22 16.1 FALSE
# Show the last 6 rows
tail(warriors) player jersey_num position age ppg is_allstar
5 Kevon Looney 5 C 28 6.3 FALSE
6 Jonathan Kuminga 0 SF 22 16.1 FALSE
7 Moses Moody 4 SG 22 9.4 FALSE
8 Brandin Podziemski 2 PG 21 12.8 FALSE
9 Gary Payton II 8 SG 32 7.1 FALSE
10 Chris Paul 3 PG 40 9.0 TRUE
# The most important function: str() shows the STRucture
str(warriors)'data.frame': 10 obs. of 6 variables:
$ player : chr "Stephen Curry" "Klay Thompson" "Draymond Green" "Andrew Wiggins" ...
$ jersey_num: num 30 11 23 22 5 0 4 2 8 3
$ position : chr "PG" "SG" "PF" "SF" ...
$ age : num 37 34 34 29 28 22 22 21 32 40
$ ppg : num 26.4 17.9 8.5 14.2 6.3 16.1 9.4 12.8 7.1 9
$ is_allstar: logi TRUE TRUE TRUE FALSE FALSE FALSE ...
str() Is Your Best Friend
Always run str() on a new dataset. It tells you: - How many rows and columns there are - The name and type of every column - A preview of the first few values
In biology, the first thing you do with any new dataset is str() then head().
# summary() gives quick statistics for every column
summary(warriors) player jersey_num position age ppg
Length :10 Min. : 0.00 Length :10 Min. :21.0 Min. : 6.300
N.unique :10 1st Qu.: 3.25 N.unique : 5 1st Qu.:23.5 1st Qu.: 8.625
N.blank : 0 Median : 6.50 N.blank : 0 Median :30.5 Median :11.100
Min.nchar:10 Mean :10.80 Min.nchar: 1 Mean :29.9 Mean :12.770
Max.nchar:18 3rd Qu.:19.25 Max.nchar: 2 3rd Qu.:34.0 3rd Qu.:15.625
Max. :30.00 Max. :40.0 Max. :26.400
is_allstar
Mode :logical
FALSE:6
TRUE :4
Part 3: Accessing Columns with $
The $ operator lets you pull out a single column as a vector:
# Get the player names
warriors$player [1] "Stephen Curry" "Klay Thompson" "Draymond Green"
[4] "Andrew Wiggins" "Kevon Looney" "Jonathan Kuminga"
[7] "Moses Moody" "Brandin Podziemski" "Gary Payton II"
[10] "Chris Paul"
# Get their points per game
warriors$ppg [1] 26.4 17.9 8.5 14.2 6.3 16.1 9.4 12.8 7.1 9.0
# Calculate team statistics
cat("Team average PPG:", round(mean(warriors$ppg), 1), "\n")Team average PPG: 12.8
cat("Highest scorer: ", warriors$player[which.max(warriors$ppg)], "\n")Highest scorer: Stephen Curry
cat("Average age: ", round(mean(warriors$age), 1), "years\n")Average age: 29.9 years
which.max() and which.min()
These functions return the position of the largest or smallest value in a vector — very useful for finding who scored the most, which game had the fewest points, etc.
which.max(warriors$ppg) # Position 1 = Curry (highest PPG)[1] 1
which.min(warriors$age) # Position 8 = Podziemski (youngest)[1] 8
Part 4: Accessing Rows and Cells with [ , ]
For a data frame, you can select specific rows and/or columns using [row, column]:
# First row (first player)
warriors[1, ] player jersey_num position age ppg is_allstar
1 Stephen Curry 30 PG 37 26.4 TRUE
# First column (all players' names)
warriors[, 1] [1] "Stephen Curry" "Klay Thompson" "Draymond Green"
[4] "Andrew Wiggins" "Kevon Looney" "Jonathan Kuminga"
[7] "Moses Moody" "Brandin Podziemski" "Gary Payton II"
[10] "Chris Paul"
# Row 1, Column 5 (Curry's PPG)
warriors[1, 5][1] 26.4
# Multiple rows
warriors[1:3, ] player jersey_num position age ppg is_allstar
1 Stephen Curry 30 PG 37 26.4 TRUE
2 Klay Thompson 11 SG 34 17.9 TRUE
3 Draymond Green 23 PF 34 8.5 TRUE
# Specific columns by name
warriors[, c("player", "ppg")] player ppg
1 Stephen Curry 26.4
2 Klay Thompson 17.9
3 Draymond Green 8.5
4 Andrew Wiggins 14.2
5 Kevon Looney 6.3
6 Jonathan Kuminga 16.1
7 Moses Moody 9.4
8 Brandin Podziemski 12.8
9 Gary Payton II 7.1
10 Chris Paul 9.0
[row, column]
In R, brackets for 2D objects always go [row, column].
warriors[2, ]→ second row, all columnswarriors[, 3]→ all rows, third columnwarriors[2, 3]→ specific cell: row 2, column 3
Leave a position blank to mean “all of them.”
Part 5: Adding Columns
You can add new columns to an existing data frame using $:
# Add a fantasy basketball value column
# (PPG * 1.5 + bonus 5 if All-Star)
warriors$fantasy_value <- warriors$ppg * 1.5 + ifelse(warriors$is_allstar, 5, 0)
warriors[, c("player", "ppg", "is_allstar", "fantasy_value")] player ppg is_allstar fantasy_value
1 Stephen Curry 26.4 TRUE 44.60
2 Klay Thompson 17.9 TRUE 31.85
3 Draymond Green 8.5 TRUE 17.75
4 Andrew Wiggins 14.2 FALSE 21.30
5 Kevon Looney 6.3 FALSE 9.45
6 Jonathan Kuminga 16.1 FALSE 24.15
7 Moses Moody 9.4 FALSE 14.10
8 Brandin Podziemski 12.8 FALSE 19.20
9 Gary Payton II 7.1 FALSE 10.65
10 Chris Paul 9.0 TRUE 18.50
# Add a "veteran" column (TRUE if age >= 30)
warriors$veteran <- warriors$age >= 30
warriors[, c("player", "age", "veteran")] player age veteran
1 Stephen Curry 37 TRUE
2 Klay Thompson 34 TRUE
3 Draymond Green 34 TRUE
4 Andrew Wiggins 29 FALSE
5 Kevon Looney 28 FALSE
6 Jonathan Kuminga 22 FALSE
7 Moses Moody 22 FALSE
8 Brandin Podziemski 21 FALSE
9 Gary Payton II 32 TRUE
10 Chris Paul 40 TRUE
Part 6: Matrices
A matrix is like a data frame but with one important restriction: all values must be the same type (usually all numbers).
Matrices are useful for mathematical operations in biology — like comparing gene expression across samples, or a season-by-season stats table.
# Create a matrix of playoff statistics for 4 Warriors players
# Rows = Players, Columns = Stats (Points, Assists, Rebounds per game)
playoff_stats <- matrix(
c(29.5, 6.2, 5.1, # Curry
18.3, 3.1, 4.8, # Klay
9.1, 7.4, 8.9, # Draymond
15.7, 4.0, 5.2), # Wiggins
nrow = 4,
ncol = 3,
byrow = TRUE # Fill row by row
)
# Add row and column names
rownames(playoff_stats) <- c("Curry", "Klay", "Draymond", "Wiggins")
colnames(playoff_stats) <- c("PPG", "APG", "RPG")
playoff_stats PPG APG RPG
Curry 29.5 6.2 5.1
Klay 18.3 3.1 4.8
Draymond 9.1 7.4 8.9
Wiggins 15.7 4.0 5.2
# Matrix math: calculate team totals for each stat
colSums(playoff_stats) PPG APG RPG
72.6 20.7 24.0
# Row means (average stat per player across all categories)
rowMeans(playoff_stats) Curry Klay Draymond Wiggins
13.600000 8.733333 8.466667 8.300000
# Access like a data frame: [row, column]
playoff_stats["Curry", "PPG"] # Curry's playoff PPG[1] 29.5
playoff_stats[, "RPG"] # All players' rebounds Curry Klay Draymond Wiggins
5.1 4.8 8.9 5.2
| Feature | Data Frame | Matrix |
|---|---|---|
| Column types | Can be mixed (text + numbers) | All must be the same type |
| Best for | Real datasets with names, categories, numbers | Pure numerical calculations |
| Access columns | $column_name |
[row, col] |
| Math functions | sum(), mean() per column |
colSums(), rowMeans() etc. |
In biology: Most of your data will be data frames. You’ll use matrices mainly for statistics and gene expression analysis.
Part 7: A Quick Visualization
# Bar chart of Warriors players' PPG
barplot(
warriors$ppg,
names.arg = warriors$player,
col = "#1D428A",
las = 2, # Rotate x-axis labels vertically
cex.names = 0.7, # Smaller font for labels
ylab = "Points Per Game",
main = "Golden State Warriors\n2025–26 Points Per Game",
ylim = c(0, 35)
)
abline(h = mean(warriors$ppg), col = "#FFC72C", lty = 2, lwd = 2)Part 8: Importing and Exporting CSV Files — Amino Acid Properties
Almost all real-world biological data arrives as a CSV file (Comma-Separated Values) — a plain text file where each row is one observation and columns are separated by commas. This is the universal format for sharing data between spreadsheets, databases, and R.
In this section we will work with a real biochemistry dataset compiled from standard reference values for the 20 canonical amino acids. The physicochemical properties we will use — molecular weight, isoelectric point, and hydrophobicity — are foundational to understanding protein structure and function and appear in virtually every biochemistry and structural biology paper.
The file amino_acid_properties.csv contains properties for all 20 standard amino acids. Key columns include:
| Column | Description |
|---|---|
amino_acid |
Full name |
abbrev_3 |
Three-letter code (e.g., Ala) |
abbrev_1 |
Single-letter code (e.g., A) |
classification |
Chemical class (nonpolar, polar, charged, aromatic) |
molecular_weight_gmol |
Molecular weight in g/mol |
isoelectric_point |
pH at which net charge = 0 (pI) |
hydrophobicity_kd |
Kyte-Doolittle hydrophobicity index |
num_codons |
Number of codons encoding this amino acid |
essential |
Whether it is an essential amino acid (TRUE/FALSE) |
Hydrophobicity values are from: Kyte J & Doolittle RF (1982) A simple method for displaying the hydropathic character of a protein. J Mol Biol 157:105–132. Isoelectric points and molecular weights are from Lehninger’s Principles of Biochemistry (8th ed.).
Step 1: Upload the CSV to Posit Cloud
Your instructor will provide the file amino_acid_properties.csv via Canvas. Download it to your computer, then upload it to your Posit Cloud project:
- In your Posit Cloud project, look at the Files panel (bottom right)
- Click the Upload button (upward arrow icon)
- Click Choose File, select
amino_acid_properties.csvfrom your computer - Click OK
The file will appear in your project’s file list and R can now read it.
Step 2: Read the CSV into R
# Read the amino acid properties CSV into a data frame
aa <- read.csv("amino_acid_properties.csv")read.csv() Arguments
| Argument | What it does | Default |
|---|---|---|
file |
File name or path | (required) |
header |
Is the first row column names? | TRUE |
na.strings |
Text to treat as missing | "NA" |
stringsAsFactors |
Convert text columns to factors? | FALSE |
For most clean CSV files, read.csv("filename.csv") with no extra arguments works perfectly.
If your instructor shares a data file via a web link, you can skip the upload step entirely:
url <- "https://example.com/data/amino_acid_properties.csv"
aa <- read.csv(url)This downloads and reads the file in one step. We will use this approach regularly in later labs.
Step 3: Inspect the Imported Data
After importing any dataset, always run these three checks before doing anything else:
# How many rows and columns?
dim(aa)[1] 20 9
# What are the column names and data types?
str(aa)'data.frame': 20 obs. of 9 variables:
$ amino_acid : chr "Alanine" "Arginine" "Asparagine" "Aspartate" ...
$ abbrev_3 : chr "Ala" "Arg" "Asn" "Asp" ...
$ abbrev_1 : chr "A" "R" "N" "D" ...
$ classification : chr "nonpolar_aliphatic" "positively_charged" "polar_uncharged" "negatively_charged" ...
$ molecular_weight_gmol: num 89.1 174.2 132.1 133.1 121.2 ...
$ isoelectric_point : num 6.01 10.76 5.41 2.77 5.07 ...
$ hydrophobicity_kd : num 1.8 -4.5 -3.5 -3.5 2.5 -3.5 -3.5 -0.4 -3.2 4.5 ...
$ num_codons : int 4 6 2 2 2 2 2 4 2 3 ...
$ essential : logi FALSE TRUE FALSE FALSE FALSE FALSE ...
# What do the first few rows look like?
head(aa) amino_acid abbrev_3 abbrev_1 classification molecular_weight_gmol
1 Alanine Ala A nonpolar_aliphatic 89.09
2 Arginine Arg R positively_charged 174.20
3 Asparagine Asn N polar_uncharged 132.12
4 Aspartate Asp D negatively_charged 133.10
5 Cysteine Cys C polar_uncharged 121.16
6 Glutamate Glu E negatively_charged 147.13
isoelectric_point hydrophobicity_kd num_codons essential
1 6.01 1.8 4 FALSE
2 10.76 -4.5 6 TRUE
3 5.41 -3.5 2 FALSE
4 2.77 -3.5 2 FALSE
5 5.07 2.5 2 FALSE
6 3.22 -3.5 2 FALSE
dim()— Do you have the right number of rows and columns? (Should be 20 rows, 9 columns)str()— Are the column types correct? Molecular weight should benum, notchrhead()— Does the data look right, or did the header row accidentally become row 1?
These checks take 10 seconds and will save you from a lot of downstream confusion.
Step 4: Explore the Biochemistry
Now that the data is loaded, let’s use what we learned earlier in this lab to explore it:
# Quick statistical summary of all numeric columns
summary(aa) amino_acid abbrev_3 abbrev_1 classification
Length :20 Length :20 Length :20 Length :20
N.unique :20 N.unique :20 N.unique :20 N.unique : 5
N.blank : 0 N.blank : 0 N.blank : 0 N.blank : 0
Min.nchar: 6 Min.nchar: 3 Min.nchar: 1 Min.nchar: 8
Max.nchar:13 Max.nchar: 3 Max.nchar: 1 Max.nchar:18
molecular_weight_gmol isoelectric_point hydrophobicity_kd num_codons
Min. : 75.03 Min. : 2.770 Min. :-4.50 Min. :1.00
1st Qu.:118.63 1st Qu.: 5.570 1st Qu.:-3.50 1st Qu.:2.00
Median :132.61 Median : 5.815 Median :-0.85 Median :2.00
Mean :136.90 Mean : 6.024 Mean :-0.49 Mean :3.05
3rd Qu.:150.70 3rd Qu.: 5.987 3rd Qu.: 2.05 3rd Qu.:4.00
Max. :204.23 Max. :10.760 Max. : 4.50 Max. :6.00
essential
Mode :logical
FALSE:10
TRUE :10
# Which amino acid has the highest molecular weight?
aa$amino_acid[which.max(aa$molecular_weight_gmol)][1] "Tryptophan"
# Which is most hydrophobic (highest Kyte-Doolittle score)?
aa$amino_acid[which.max(aa$hydrophobicity_kd)][1] "Isoleucine"
# Which is most hydrophilic (lowest score)?
aa$amino_acid[which.min(aa$hydrophobicity_kd)][1] "Arginine"
# How many essential vs non-essential amino acids?
table(aa$essential)
FALSE TRUE
10 10
# Average molecular weight by chemical classification
tapply(aa$molecular_weight_gmol, aa$classification, mean) aromatic negatively_charged nonpolar_aliphatic polar_uncharged
183.5367 140.1150 115.4214 124.7280
positively_charged
158.5167
# Visualize: molecular weight vs isoelectric point
plot(aa$molecular_weight_gmol, aa$isoelectric_point,
xlab = "Molecular Weight (g/mol)",
ylab = "Isoelectric Point (pI)",
main = "Amino Acid Properties\nMolecular Weight vs. Isoelectric Point",
pch = 16,
col = ifelse(aa$essential, "#C8102E", "#1D428A"))
text(aa$molecular_weight_gmol, aa$isoelectric_point,
labels = aa$abbrev_1,
pos = 3,
cex = 0.7)
legend("topright",
legend = c("Essential", "Non-essential"),
col = c("#C8102E", "#1D428A"),
pch = 16)The isoelectric point (pI) is the pH at which an amino acid carries no net electrical charge. At pH values below the pI, the molecule is positively charged; above the pI, it is negatively charged.
Notice in the plot that the positively charged amino acids (Arg, Lys, His) cluster at high pI values (above 7), while negatively charged ones (Asp, Glu) have very low pI values. This property is exploited in techniques like gel electrophoresis and ion-exchange chromatography.
Step 5: Write a Filtered Dataset to CSV
Suppose you want to save just the essential amino acids to a separate file to share with a colleague:
# Subset: essential amino acids only
essential_aa <- aa[aa$essential == TRUE, ]
cat("Number of essential amino acids:", nrow(essential_aa), "\n")Number of essential amino acids: 10
# Write to a new CSV
write.csv(essential_aa,
file = "essential_amino_acids.csv",
row.names = FALSE) # ALWAYS use row.names = FALSE
cat("File saved. Check your Files panel.\n")File saved. Check your Files panel.
row.names = FALSE
By default, write.csv() prepends a column of row numbers (1, 2, 3…) to your file. This is almost never useful and creates problems when you re-import the file. Always include row.names = FALSE.
# Read it back to confirm it saved correctly
check <- read.csv("essential_amino_acids.csv")
dim(check)[1] 10 9
head(check) amino_acid abbrev_3 abbrev_1 classification molecular_weight_gmol
1 Arginine Arg R positively_charged 174.20
2 Histidine His H positively_charged 155.16
3 Isoleucine Ile I nonpolar_aliphatic 131.17
4 Leucine Leu L nonpolar_aliphatic 131.17
5 Lysine Lys K positively_charged 146.19
6 Methionine Met M nonpolar_aliphatic 149.21
isoelectric_point hydrophobicity_kd num_codons essential
1 10.76 -4.5 6 TRUE
2 7.59 -3.2 2 TRUE
3 5.98 4.5 3 TRUE
4 5.98 3.8 6 TRUE
5 9.74 -3.9 2 TRUE
6 5.74 1.9 1 TRUE
Step 6: Download the CSV from Posit Cloud
After saving a file in R, retrieve it from Posit Cloud to your own computer:
- In the Files panel (bottom right), check the box next to
essential_amino_acids.csv - Click More (the gear icon at the top of the Files panel)
- Select Export
- Click Download
The file will download to your Downloads folder and can be opened in Excel or shared with collaborators.
In nearly every lab going forward, the workflow will be:
- Import —
read.csv("file.csv")to load your data - Explore —
str(),head(),summary()to understand it - Analyze — filter, calculate, summarize
- Export —
write.csv(df, "results.csv", row.names = FALSE)to save your results - Download — use the Files panel to get the CSV onto your computer
Mastering this cycle means you can take data from any published source, process it in R, and share your results in a format any software can open.
3-Minute Knowledge Check
Close your notes. Answer these on your own — you have 3 minutes. We’ll go through the answers together.
1. You have a data frame called roster. Which function gives you a quick overview of column names, types, and sample values?
a) `summary(roster)` b) `str(roster)` c) `head(roster)` d) `dim(roster)`
2. How do you access the ppg column of a data frame called warriors?
3. In df[3, ], what does leaving the column position blank mean?
a) Select no columns b) Select all columns c) Select the third column d) It causes an error
4. What is the key difference between a data frame and a matrix in R?
5. True or False: A data frame can have a column of text AND a column of numbers.
1. b) str(roster) — str() shows the structure: number of rows/columns, column names, data types, and sample values. This is the single most useful first-look function in R.
2. warriors$ppg — The $ operator accesses a named column as a vector. You can also use warriors[, "ppg"] or warriors[, 5] if PPG is the 5th column.
3. b) Select all columns — In [row, column] notation, leaving a position blank means “give me everything.” So df[3, ] means row 3, all columns.
4. A data frame can hold mixed types (numbers, text, logical values in different columns). A matrix can only hold one type — usually all numbers. Most real data belongs in a data frame.
5. True — This is one of the most important features of data frames. You can have a player column (character) and a ppg column (numeric) in the same data frame.
Lab 5 Checklist
Before you leave, make sure you can:
Build a data frame for any sports team of your choice with at least 5 players and 4 variables (at least one should be text, one numeric, one logical). Use str() and summary() to explore it. Calculate the team average for at least one numeric variable. Add it to your Quarto document!
Before Next Class (Monday, Week 3)
- Read Intro2r Ch. 3 §3.2–3.3
- Monday (Lab 6) we learn subsetting — how to filter and select specific parts of your data
- Practice: try adding two more columns to the
warriorsdata frame above