Introduction to R: Objects, Data, and Environmental Analysis

Marc Los Huertos

EA030 · Compiled 2026-04-14

R as Calculator Objects Vectors Data Frames Summary Stats Graphics Linear Models Case Study

🎯 Learning Outcomes

  1. Use R as a calculator with arithmetic operators and mathematical functions.
  2. Create and interrogate vectors — the fundamental R object.
  3. Build data frames and access variables with the $ operator.
  4. Compute summary statistics on environmental data.
  5. Produce informative base-R graphics with labeled axes.
  6. Fit and interpret a simple linear model with lm().
  7. Apply these skills to a real lake temperature profile dataset.

What this module covers. R is an open-source statistical computing environment used across environmental science, ecology, hydrology, and climate research. This module builds the foundation you need to work with data in R: understanding how R stores information as objects, how to combine objects into data frames, and how to explore and model environmental data. All data used here are self-contained — no files to download.


1. R as a Calculator

Before writing complex analyses, get comfortable using R interactively. R understands standard arithmetic directly at the console prompt (>).

Arithmetic operators

Operation Operator Example Result
Addition + 6 + 5 11
Subtraction - 6 - 5 1
Multiplication * 6 * 5 30
Division / 3 / 5 0.6
Exponentiation ^ 3 ^ 2 9
Integer division %/% 7 %/% 2 3
Modulo (remainder) %% 7 %% 2 1
# Basic arithmetic — type these in your console and observe the results
6 + 5
## [1] 11
3 / 5
## [1] 0.6
3 ^ 2
## [1] 9

Mathematical functions

For more complex calculations, R provides built-in functions. All functions use parentheses; what goes inside is called the argument.

log10(100)         # base-10 logarithm: how many pH units?
## [1] 2
log(2.718282)      # natural log (base e)
## [1] 1
sqrt(144)          # square root
## [1] 12
abs(-7.3)          # absolute value — useful for anomalies
## [1] 7.3
round(3.14159, 2)  # round to 2 decimal places
## [1] 3.14

💡 No news is good news When R executes your command without printing an error, it worked. Silence means success. When something does go wrong, R prints a message starting with Error or Warning. Read those messages carefully — they usually tell you exactly what went wrong.

Environmental example: the Michaelis-Menten equation

Many ecological processes follow the Michaelis-Menten form — nutrient uptake by phytoplankton, enzyme kinetics, oxygen consumption. The equation is:

\[V = V_{max} \frac{S}{K_s + S}\]

where \(V\) is the uptake rate, \(V_{max}\) is the maximum rate, \(S\) is substrate concentration, and \(K_s\) is the half-saturation constant. With \(V_{max} = 0.18\), \(S = 0.5\), and \(K_s = 0.3\):

Vmax <- 0.18
S    <- 0.5
Ks   <- 0.3
V    <- Vmax * (S / (Ks + S))
V   # print the result
## [1] 0.1125

Notice how assigning values to named objects (<-) makes the formula readable and easy to modify. Change \(S\) from 0.5 to 1.5 and re-run — does the uptake rate saturate as expected?

📌 Try it: explore operator precedence Type 2 + 3 * 4 and then (2 + 3) * 4. R follows standard mathematical order of operations. When in doubt, use parentheses to make your intent explicit.


2. Objects in R

The single most important concept in R is the object. Everything in R — a number, a list of measurements, a statistical model, a plot — is an object with a name you assign. You create objects with the assignment operator <-.

# Create a scalar object (length-1 vector)
air_temp <- 18.5     # degrees Celsius, a single measurement

# Print it by typing its name
air_temp
## [1] 18.5

Object names are case-sensitive: air_temp, Air_Temp, and AIR_TEMP are three different objects. R convention uses snake_case (lowercase words separated by underscores). Start names with a letter, never a digit.

Common R object types

Vector
c(), seq(), rep()
An ordered sequence of values of a single type. The most fundamental R object.
Data Frame
data.frame(), read.csv()
A table: columns are equal-length vectors (like a spreadsheet). The workhorse of data analysis.
Matrix
matrix(), as.matrix()
A 2-D array of values, all the same type. Used in multivariate analyses.
List
list()
An ordered collection that can hold objects of different types and lengths — including other lists.
Factor
factor(), as.factor()
A categorical variable with fixed levels. Tells R to treat values as groups, not numbers.
Linear Model
lm(), aov()
The output of a model fit — an object containing coefficients, residuals, fit statistics, and more.

Don’t try to memorize all of these now. As you work through this module you will create each type naturally.


3. Vectors: R’s Core Data Structure

A vector stores an ordered sequence of values of a single type (numeric, character, or logical). Use c() — short for concatenate — to create one.

Creating numeric vectors

# A vector of water temperatures measured at a field site (°C)
temperature <- c(22.1, 21.8, 20.5, 18.2, 14.3, 11.0, 10.2, 9.8)

# A corresponding vector of depths (m)
depth <- c(0, 1, 2, 3, 4, 5, 6, 7)

# Print them
temperature
## [1] 22.1 21.8 20.5 18.2 14.3 11.0 10.2  9.8
depth
## [1] 0 1 2 3 4 5 6 7

Interrogating vectors

Once you have a vector, a set of built-in functions let you explore it:

length(temperature)  # how many observations?
## [1] 8
sum(temperature)     # total sum
## [1] 127.9
mean(temperature)    # arithmetic mean
## [1] 15.9875
min(temperature)     # minimum value
## [1] 9.8
max(temperature)     # maximum value
## [1] 22.1
range(temperature)   # min and max together
## [1]  9.8 22.1
sd(temperature)      # standard deviation
## [1] 5.290271

Sequences and repetition

Typing out every value by hand gets tedious. Use seq() and rep() for structured sequences:

# Depths from 0 to 14 m in 2-m increments
depth_2m <- seq(from = 0, to = 14, by = 2)
depth_2m
## [1]  0  2  4  6  8 10 12 14
# Replicate a treatment label 5 times each
treatment <- c(rep("Control", 5), rep("Heated", 5), rep("Ambient", 5))
treatment
##  [1] "Control" "Control" "Control" "Control" "Control" "Heated"  "Heated" 
##  [8] "Heated"  "Heated"  "Heated"  "Ambient" "Ambient" "Ambient" "Ambient"
## [15] "Ambient"

Character and logical vectors

Vectors don’t have to contain numbers. Environmental data often include site names, species identifiers, or quality-control flags:

# Character vector of sampling sites
sites <- c("Kern River", "Mojave Basin", "Santa Ana", "LA River")

# Logical vector: is the site impaired?
impaired <- c(FALSE, TRUE, TRUE, TRUE)

# Check type
class(sites)
## [1] "character"
class(impaired)
## [1] "logical"

⚠️ Vectors must be a single type If you mix numbers and characters in c(), R silently converts everything to character — a process called coercion. Try c(1, 2, “three”) and check its class. This is a common source of confusion when reading messy data files.

Indexing: selecting elements

Use square brackets [ ] to extract elements by position (index starts at 1, not 0):

temperature[1]        # first element (surface temperature)
## [1] 22.1
temperature[6]        # sixth element
## [1] 11
temperature[1:3]      # elements 1 through 3
## [1] 22.1 21.8 20.5
temperature[c(1, 4, 8)]  # specific positions
## [1] 22.1 18.2  9.8

You can also use a logical condition to filter:

# Which depths have temperatures below 12°C? (hypolimnion)
depth[temperature < 12]
## [1] 5 6 7
# The actual temperatures in the hypolimnion
temperature[temperature < 12]
## [1] 11.0 10.2  9.8

📝 Try it: vector arithmetic Vectors support element-wise arithmetic. Convert temperature from Celsius to Fahrenheit: temperature * 9/5 + 32. Create a new object called temp_f and print it. Notice R applies the conversion to every element at once — no loop needed.


4. Data Frames: Tabular Environmental Data

A data frame is a collection of equal-length vectors arranged as columns — exactly like a spreadsheet, but with R’s statistical power behind it. Each column is a variable; each row is an observation.

Building a data frame from vectors

# pHake Lake: a simulated alpine lake used in EA030 labs
# Data represent a summer thermal stratification profile

phake_depth <- c(0, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14)
phake_temp  <- c(22.4, 22.1, 21.8, 20.9, 19.1, 16.5,
                 13.2, 11.4, 10.8, 10.4, 10.1, 10.0, 9.9, 9.8)

# Confirm vectors are the same length before combining
length(phake_depth) == length(phake_temp)
## [1] TRUE
# Build the data frame
phake <- data.frame(
  depth_m     = phake_depth,
  temp_c      = phake_temp
)

Inspecting a data frame

# Preview the top six rows
head(phake)
##   depth_m temp_c
## 1     0.0   22.4
## 2     0.5   22.1
## 3     1.0   21.8
## 4     2.0   20.9
## 5     3.0   19.1
## 6     4.0   16.5
# Structure: types, dimensions, a few values
str(phake)
## 'data.frame':    14 obs. of  2 variables:
##  $ depth_m: num  0 0.5 1 2 3 4 5 6 7 8 ...
##  $ temp_c : num  22.4 22.1 21.8 20.9 19.1 16.5 13.2 11.4 10.8 10.4 ...
# Quick summary statistics for every column
summary(phake)
##     depth_m           temp_c     
##  Min.   : 0.000   Min.   : 9.80  
##  1st Qu.: 2.250   1st Qu.:10.18  
##  Median : 5.500   Median :12.30  
##  Mean   : 5.821   Mean   :14.89  
##  3rd Qu.: 8.750   3rd Qu.:20.45  
##  Max.   :14.000   Max.   :22.40

Accessing columns with $

The $ operator drills into a data frame column and returns it as a vector:

phake$depth_m        # depth column as a vector
##  [1]  0.0  0.5  1.0  2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0 10.0 12.0 14.0
mean(phake$temp_c)   # mean temperature across all depths
## [1] 14.88571

📚 The data frame as the unit of analysis Keeping all your variables together in a single data frame — rather than as separate loose vectors — is good practice. It prevents length mismatches, makes your code readable, and is required by most modelling and plotting functions that use the data = argument.

Adding a derived column

We can add new variables directly to the data frame:

# Convert temperature to Fahrenheit and add as a new column
phake$temp_f <- phake$temp_c * 9 / 5 + 32

# A depth class column: epilimnion vs. hypolimnion
phake$layer <- ifelse(phake$temp_c > 15, "Epilimnion", "Hypolimnion")

head(phake)
##   depth_m temp_c temp_f      layer
## 1     0.0   22.4  72.32 Epilimnion
## 2     0.5   22.1  71.78 Epilimnion
## 3     1.0   21.8  71.24 Epilimnion
## 4     2.0   20.9  69.62 Epilimnion
## 5     3.0   19.1  66.38 Epilimnion
## 6     4.0   16.5  61.70 Epilimnion

5. Exploring Data: Summary Statistics

Before fitting any model, always explore your data. Summary statistics reveal the central tendency, spread, and shape of your variables.

Descriptive statistics

# Central tendency
mean(phake$temp_c)
## [1] 14.88571
median(phake$temp_c)
## [1] 12.3
# Spread
sd(phake$temp_c)          # standard deviation
## [1] 5.278091
var(phake$temp_c)         # variance
## [1] 27.85824
range(phake$temp_c)       # min and max
## [1]  9.8 22.4
IQR(phake$temp_c)         # interquartile range
## [1] 10.275
# The all-in-one function
summary(phake$temp_c)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    9.80   10.18   12.30   14.89   20.45   22.40

Stratified summaries with tapply()

The tapply() function computes a statistic on a variable within groups — here the epilimnion versus hypolimnion layers we defined above:

tapply(phake$temp_c, phake$layer, mean)
##  Epilimnion Hypolimnion 
##    20.46667    10.70000
tapply(phake$temp_c, phake$layer, sd)
##  Epilimnion Hypolimnion 
##    2.279181    1.142679

📌 Check your data before you model it Always run summary() and look at your data frame with head() and str() before doing any analysis. Watch for: impossible values (temperature of −99 is usually a missing data code), unexpected NA values, and variables stored as the wrong type (numbers stored as characters).


6. Basic Graphics

R’s base graphics system produces publication-ready figures directly from data frames and vectors. The key function is plot().

A simple scatter plot

plot(phake$temp_c, phake$depth_m,
     xlab = "Temperature (°C)",
     ylab = "Depth (m)",
     main = "pHake Lake — Summer Thermal Profile",
     pch  = 16,        # solid circle symbols
     col  = "steelblue")

Inverting the y-axis for depth profiles

Limnologists and oceanographers always plot depth increasing downward on the y-axis, matching the physical orientation of the water column. Reversing the y-axis requires passing a reversed ylim:

# Classic depth profile: depth on y (inverted), temperature on x
plot(x    = phake$temp_c,
     y    = phake$depth_m,
     xlim = c(8, 25),
     ylim = rev(range(phake$depth_m)),  # reverse: deeper = lower
     xlab = "Water Temperature (°C)",
     ylab = "Depth (m)",
     main = "pHake Lake: Thermal Stratification, Summer",
     type = "b",      # "b" = both points and lines
     pch  = 16,
     col  = "steelblue3",
     lwd  = 2,
     las  = 1)         # horizontal axis labels

# Add a horizontal line marking the thermocline
abline(h = 4, lty = 2, col = "firebrick", lwd = 1.5)
text(x = 23, y = 4.5, labels = "Thermocline", col = "firebrick", cex = 0.85)

Key graphical parameters

Parameter Meaning Example values
xlab, ylab Axis labels "Temperature (°C)"
main Plot title use sparingly — use figure captions instead
pch Point symbol 16 = filled circle, 17 = triangle, 1 = open circle
col Color "steelblue", "#2d5a3d", rgb(0.2, 0.4, 0.3)
lwd Line width 1 (default), 2, 3
lty Line type 1 = solid, 2 = dashed, 3 = dotted
cex Character expansion (size) 1 (default), 1.5 = 50% larger
las Axis label orientation 1 = horizontal (recommended)
type Plot type "p" = points, "l" = lines, "b" = both

📈 Science figures vs. Excel defaults Excel defaults are designed for business presentations, not scientific communication. In scientific figures: omit the box frame (bty = “n”), always label axes with units, avoid decorative colors, and let a figure caption — not a title — describe what the reader should take away. R makes it easy to adopt these conventions.

Histograms and boxplots

par(mfrow = c(1, 2))  # two panels side by side

hist(phake$temp_c,
     breaks = 8,
     col    = "#d4e6da",
     border = "#1e3a2f",
     xlab   = "Temperature (°C)",
     main   = "Temperature Distribution")

boxplot(temp_c ~ layer, data = phake,
        col    = c("#8aad96", "#4a7c5e"),
        ylab   = "Temperature (°C)",
        xlab   = "Lake Layer",
        las    = 1,
        main   = "Epilimnion vs. Hypolimnion")

par(mfrow = c(1, 1))  # reset to single panel

7. Linear Models in R

The linear model is the cornerstone of quantitative analysis in environmental science. Whether you are testing for a treatment effect (ANOVA) or estimating a continuous relationship (regression), the underlying framework is the same:

\[y \sim \alpha + \beta \cdot x + \varepsilon\]

\(\alpha\) is the intercept, \(\beta\) is the slope, and \(\varepsilon\) is the residual error. In R, ~ means “is modelled as a function of.”

Fitting a model with lm()

Temperature decreases with depth in a stratified lake. We can estimate this relationship formally:

# Fit temperature as a function of depth
temp_model <- lm(temp_c ~ depth_m, data = phake)

# Inspect the coefficients
coef(temp_model)
## (Intercept)     depth_m 
##   21.271919   -1.097017

The intercept is the estimated surface temperature (depth = 0); the slope is the average change in temperature per additional metre of depth.

Reading the model summary

summary(temp_model)
## 
## Call:
## lm(formula = temp_c ~ depth_m, data = phake)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.2898 -1.8965  0.4087  1.5630  3.8863 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  21.2719     1.0190  20.875 8.46e-11 ***
## depth_m      -1.0970     0.1416  -7.748 5.21e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.242 on 12 degrees of freedom
## Multiple R-squared:  0.8334, Adjusted R-squared:  0.8195 
## F-statistic: 60.04 on 1 and 12 DF,  p-value: 5.207e-06

Key values to read:

⚠️ Does a straight line make sense here? Lake temperature vs. depth is not actually linear — there is an abrupt thermocline. A linear model here simplifies reality. Always plot your data and inspect residuals before trusting a model summary. The R² being high does not mean the model is appropriate.

Plotting the fitted line

plot(x    = phake$depth_m,
     y    = phake$temp_c,
     xlab = "Depth (m)",
     ylab = "Temperature (°C)",
     main = "Temperature vs. Depth — Linear Model Fit",
     pch  = 16, col = "steelblue3", las = 1)

abline(temp_model, col = "firebrick", lwd = 2)

# Add the R² value as a text annotation
r2 <- round(summary(temp_model)$r.squared, 3)
text(x = 1, y = 11,
     labels = paste0("R² = ", r2),
     col = "firebrick", cex = 0.9)

Extracting specific model outputs

# Coefficients
coef(temp_model)["(Intercept)"]
## (Intercept) 
##    21.27192
coef(temp_model)["depth_m"]
##   depth_m 
## -1.097017
# Fitted values (predicted temperatures at each observed depth)
head(fitted(temp_model))
##        1        2        3        4        5        6 
## 21.27192 20.72341 20.17490 19.07789 17.98087 16.88385
# Residuals (observed minus fitted)
head(residuals(temp_model))
##          1          2          3          4          5          6 
##  1.1280812  1.3765895  1.6250979  1.8221146  1.1191314 -0.3838519
# Confidence intervals for the coefficients
confint(temp_model)
##                 2.5 %     97.5 %
## (Intercept) 19.051657 23.4921804
## depth_m     -1.405495 -0.7885387

📌 Getting help To see what arguments a function accepts, type ?lm or help(lm) in the console. The Examples section at the bottom of any help page is the fastest way to learn — paste it in and run it, then adapt it to your data.


8. Case Study: pHake Lake Temperature Profile

pHake Lake is a simulated alpine lake used in EA030 field laboratories. In this case study we work through a complete analysis workflow — from raw data to interpreted figure — using the seasonal temperature profile collected by a data logger.

The dataset

The data below represent temperature readings taken at standard depth intervals during summer stratification. All data are embedded directly in this document — no external file needed.

# Full summer dataset — three replicate casts averaged
phake_full <- data.frame(
  depth_m  = c(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5,
               4.0, 4.5, 5.0, 5.5, 6.0, 7.0, 8.0, 9.0,
               10.0, 11.0, 12.0, 13.0, 14.0),
  temp_c   = c(22.4, 22.2, 22.0, 21.7, 21.2, 20.3, 19.0, 17.4,
               15.3, 13.1, 11.5, 10.9, 10.6, 10.3, 10.1, 10.05,
               10.0, 9.98, 9.95, 9.92, 9.90),
  do_mgl   = c(9.1, 9.0, 9.0, 8.9, 8.8, 8.5, 8.2, 7.8,
               7.1, 6.5, 5.9, 5.2, 4.8, 4.5, 4.3, 4.2,
               4.1, 4.1, 4.0, 4.0, 3.9)   # dissolved oxygen (mg/L)
)

str(phake_full)
## 'data.frame':    21 obs. of  3 variables:
##  $ depth_m: num  0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 ...
##  $ temp_c : num  22.4 22.2 22 21.7 21.2 20.3 19 17.4 15.3 13.1 ...
##  $ do_mgl : num  9.1 9 9 8.9 8.8 8.5 8.2 7.8 7.1 6.5 ...
summary(phake_full)
##     depth_m           temp_c          do_mgl     
##  Min.   : 0.000   Min.   : 9.90   Min.   :3.900  
##  1st Qu.: 2.500   1st Qu.:10.05   1st Qu.:4.200  
##  Median : 5.000   Median :11.50   Median :5.900  
##  Mean   : 5.857   Mean   :14.66   Mean   :6.281  
##  3rd Qu.: 9.000   3rd Qu.:20.30   3rd Qu.:8.500  
##  Max.   :14.000   Max.   :22.40   Max.   :9.100

Identifying the thermocline

The thermocline is the depth zone of maximum temperature change — where the water column transitions from warm, well-mixed epilimnion to cold, stable hypolimnion. We can find it by computing the temperature gradient (change per metre):

# Calculate temperature gradient between consecutive depths
delta_temp  <- diff(phake_full$temp_c)   # differences between adjacent rows
delta_depth <- diff(phake_full$depth_m)
gradient    <- delta_temp / delta_depth  # °C per metre

# Depth of maximum gradient (most negative = steepest cooling)
thermo_depth <- phake_full$depth_m[which.min(gradient)]
cat("Thermocline centre depth:", thermo_depth, "m\n")
## Thermocline centre depth: 4 m
cat("Maximum gradient:", round(min(gradient), 2), "°C/m\n")
## Maximum gradient: -4.4 °C/m

Publication-quality depth profile

par(mar = c(4.5, 4.5, 2, 1), las = 1, cex = 0.95)

plot(x    = phake_full$temp_c,
     y    = phake_full$depth_m,
     xlim = c(8, 25),
     ylim = rev(range(phake_full$depth_m)),
     xlab = "Water Temperature (°C)",
     ylab = "Depth (m)",
     main = "pHake Lake: Thermal Profile",
     type = "b",
     pch  = 16,
     col  = "steelblue3",
     lwd  = 2,
     bty  = "l")   # only left and bottom axes

# Shade epilimnion vs. hypolimnion
rect(xleft = 8, xright = 25,
     ybottom = thermo_depth + 1, ytop = 14,
     col = rgb(0.18, 0.36, 0.56, 0.08), border = NA)

# Mark thermocline
abline(h = thermo_depth, lty = 2, col = "firebrick", lwd = 1.5)
text(x = 23.5, y = thermo_depth - 0.6,
     labels = paste0("Thermocline\n(", thermo_depth, " m)"),
     col = "firebrick", cex = 0.75, adj = 1)

# Add layer labels
text(x = 22.5, y = 1,   labels = "Epilimnion",  cex = 0.8, col = "#1e3a2f")
text(x = 22.5, y = 12,  labels = "Hypolimnion", cex = 0.8, col = "#1e3a2f")

Dual-variable profile: temperature and dissolved oxygen

Dissolved oxygen (DO) often mirrors temperature in stratified lakes — warm surface waters lose oxygen at depth. Plotting both on the same figure requires a second axis:

par(mar = c(4.5, 4.5, 2, 4.5), las = 1, cex = 0.9)

# --- Temperature (primary x-axis, bottom) ---
plot(x    = phake_full$temp_c,
     y    = phake_full$depth_m,
     xlim = c(8, 25),
     ylim = rev(range(phake_full$depth_m)),
     xlab = "Temperature (°C)",
     ylab = "Depth (m)",
     main = "pHake Lake: Temperature & Dissolved Oxygen",
     type = "b", pch = 16, col = "steelblue3", lwd = 2, bty = "n")

# --- Dissolved oxygen (secondary x-axis, top) ---
par(new = TRUE)
plot(x    = phake_full$do_mgl,
     y    = phake_full$depth_m,
     xlim = c(3, 10),
     ylim = rev(range(phake_full$depth_m)),
     xlab = "", ylab = "", axes = FALSE,
     type = "b", pch = 17, col = "darkorange3", lwd = 2)

axis(side = 3, col = "darkorange3", col.axis = "darkorange3")
mtext("Dissolved Oxygen (mg/L)", side = 3, line = 2.5, col = "darkorange3", cex = 0.85)

legend("bottomright",
       legend = c("Temperature (°C)", "Dissolved O₂ (mg/L)"),
       col    = c("steelblue3", "darkorange3"),
       lty    = 1, pch = c(16, 17), lwd = 2,
       bty    = "n", cex = 0.8)

Fitting a segmented model concept

A simple linear model misses the non-linearity of thermal stratification. Below we compare R² for depth zones to illustrate how the relationship differs across layers:

epi  <- phake_full[phake_full$depth_m <= thermo_depth, ]
hypo <- phake_full[phake_full$depth_m >  thermo_depth, ]

# Epilimnion model
lm_epi  <- lm(temp_c ~ depth_m, data = epi)
# Hypolimnion model
lm_hypo <- lm(temp_c ~ depth_m, data = hypo)

cat("Epilimnion slope:  ", round(coef(lm_epi)["depth_m"],  3), "°C/m\n")
## Epilimnion slope:   -1.673 °C/m
cat("Hypolimnion slope: ", round(coef(lm_hypo)["depth_m"], 3), "°C/m\n")
## Hypolimnion slope:  -0.214 °C/m
cat("Epilimnion R²:  ", round(summary(lm_epi)$r.squared,  3), "\n")
## Epilimnion R²:   0.864
cat("Hypolimnion R²: ", round(summary(lm_hypo)$r.squared, 3), "\n")
## Hypolimnion R²:  0.545

The steeply negative slope in the epilimnion captures the thermocline gradient; the near-zero slope below confirms the isothermal hypolimnion.


🔑 Introduction to R — Key Takeaways

  • Everything in R is an object. Use <- to assign names. Objects persist in your Environment until you remove or overwrite them.
  • Vectors are the atom. All other structures are built from them. Create with c(), index with [ ], filter with logical conditions.
  • Data frames keep related vectors together. Always use the data = argument in plot() and lm() — it makes code readable and avoids length errors.
  • No news is good news. A silent return means success. Read error messages carefully — they usually identify the line and cause.
  • Explore before you model. Run summary(), str(), and head() on every new dataset before fitting anything.
  • The linear model formula is y ~ x. The tilde means “is modelled as a function of.” The lm() output object holds coefficients, residuals, R², and everything else.
  • Plot axis labels must include units. xlab = “Temperature (°C)” — never leave axes unlabeled in scientific figures.
  • Invert the y-axis for depth profiles with ylim = rev(range(depth)) so deeper = lower on the page.

What’s next?