# air.R -- days above the PM2.5 standard at one monitor, 2025
# Your Name, NetID
# 0) Setup ----
STANDARD <- 35 # US EPA 24-hour standard, ug/m3
pm <- read.csv("data/epa_pm25_compton_2025.csv",
colClasses = c("Site.ID" = "character"))
# 1) One instrument, one month ----
# 2) The numbers ----
# 3) Write results.csv ----6 · How to code
Thursday, September 10, 2026
Slides · Live script · Source
How a project and a script are organized, the habits that make a script readable and checkable, writing functions instead of copy-pasting, and what to do when a number is wrong.
Objectives
By the end of this session you can:
- Lay out a project: folders for code, data and output, one script per task, and a master script that runs them in order.
- Structure a script: sections, names, comments that say why, named constants, no duplicated code.
- Write a function and call it with its own arguments.
- Run one function over every file in a folder with
lapply(). - Stop a script with
stopifnot()when a number is not what you wrote down. - Localize a mistake by halving the script, and write the diagnosis down: line, mechanism, fix, proof.
Plan for today
- Motivation and philosophy
- Organizing projects, scripts, and code
- Guiding principles
- Writing functions
- Checking a script as you write it
- Finding a mistake when the number is wrong
Disclaimers
- I’m not a computer scientist.
- The ideas here reflect my experience and that of others.
- Although our work is in R, many of these ideas apply to other languages.
Motivation
Good code…
- Increases confidence in your work
- Increases the chances other researchers will reproduce, replicate and cite your work
- Saves you (and others) time
- Reduces the chances of making mistakes
- Is easy to understand
- May be portable
Coding philosophy
You are writing code for others. That includes yourself at \(t+1\).
A script is written once and read many times. You read it again when a referee asks where a number came from, when a coauthor wants the same table on a different sample, when you reopen a project after a semester on something else. Each of those readers works from the file. None of them works from what you remember about the file.
Most of the time that reader is you. Not the you who wrote it, who knew which rows were dropped and why. The you six months from now, who does not. What you knew at the keyboard is not in the file; only what you wrote down is. Everything in this session follows from that.
Two kinds of efficiency
Efficiency has two meanings and they pull against each other.
One is the machine’s: how many operations, how much memory, how long the run takes. The other is yours: how long it takes you to read the script, change it, and work out why a number moved.
Compact, fast code can be hard to follow. Code you can trace line by line often does more work than it strictly needs to. Neither is always right, but at the scale of a research script the machine’s time is cheap and yours is not. A script that takes four seconds instead of one has cost you nothing. A script you cannot follow has cost you an afternoon.
This session is about the second kind.
Organizing a project
A project comes with general instructions
- It could be a
readme.txtfile - It should specify the authors, what the project is about, and describe the files
- It could be a
Folders for different types of files
- Don’t mix raw data with “cleaned” data
No single best way of organizing, but this is a good start:
- Folder for code (no subfolders)
- Folder(s) for input(s): raw data you never modify, and cleaned data your scripts create
- Folder(s) for output(s): figures, tables
- Folder for packages (optional)
Example of folder structure
code— all script files, including the RStudio projectdataraw— data as downloaded from the web, can be largeclean— data needed for the analysis
output— what shows up in the paperfigurestables
paperpresentationsOthers: meeting notes, documentation
Organizing your script files
A script file should accomplish a specific and well-identified task
- Cleaning data, regression analysis, and so on
Never have all your code in a single script file, unless it is a tiny project — your homework, for instance
Script files should have intuitive names and follow a workflow:
1.1_clean_economic_data.R1.2_clean_pollution_data.R1.3_prepare_regression_data.R2.1_regression_analysis.R2.2_post_estimation_analysis.R
Good practice to create special script files
A master script file,
master.R, that runs all the project’s scripts in the correct order- Installs and loads all necessary packages
- Simplifies life for whoever reproduces your work
- Typically written at the very end of the project
A file containing user-defined functions,
functions.R- Improves automation when the same function is used in several scripts
- Invites you to write more portable functions for future projects
Code within a script file
A title block says what the script does and who wrote it. A setup section holds packages, constants, and the data coming in. Then one section per task, numbered in the order they run, and nothing from setup reappears lower down. A comment ending in four dashes is a section: RStudio’s outline (Ctrl+Shift+O, Cmd+Shift+O on a Mac) lists them, and you can jump between them. A script does one job. Two jobs, two scripts.
Two versions of the same script
Both count the days above the standard at one monitor. Both are correct.
STANDARD <- 35 # US EPA 24-hour standard, ug/m3
pm <- read.csv("data/epa_pm25_compton_2025.csv")
names(pm)[names(pm) == "Daily.Mean.PM2.5.Concentration"] <- "pm25"
pm <- pm[pm$POC == 1, ] # one instrument
sum(pm$pm25 > STANDARD)compose <- function(...) { fs <- list(...); function(x) Reduce(\(a, f) f(a), fs, x) }
run <- compose(read.csv, \(d) subset(d, POC == 1),
\(d) d$Daily.Mean.PM2.5.Concentration, \(v) sum(v > 35))
run("data/epa_pm25_compton_2025.csv")The second version is shorter, and by the standards of software engineering it is better: nothing is repeated, each step is a separate piece, and the whole thing could be pointed at another file tomorrow. It is also unreadable at a glance. The 35 has disappeared into the last argument as a bare number with no name, there is no object between the file and the answer, and you have to read it inside out to work out what happens first.
Research code is read far more often than it is reused. These are the dimensions that decide whether a reader can follow it:
| The first version | The second | |
|---|---|---|
| Names | STANDARD says what 35 is |
35 is a bare number in the last argument |
| Visible values | every number is on its own line | values are buried inside calls |
| Intermediate objects | pm exists; you can print it |
nothing between the file and the answer |
| Somewhere to put a check | after any step, on pm |
nowhere |
| Layers to hold at once | one line at a time | compose, Reduce, and four unnamed functions |
| Reading order | top to bottom, in the order it happens | inside out |
The same list applies to a single function: a function is easy to read when its name says what it returns, its arguments are visible at the call, and its body is short enough to hold in your head at once.
Guiding principles 1/2
Think constantly about reproducibility
- Your script files should run entirely without errors — start with your own computer
- Rely on relative paths, no hard coding
- RStudio projects help. Use them.
Do not repeat yourself
- Do not copy-paste code within a script file. Write a function and use loops.
- Do not copy-paste code across script files. Put the function in its own file and load it when needed.
- Copy-pasting code is a red flag.
Give objects and folders simple and intuitive names
- Good names are short and informative
- Limit upper case and odd characters:
MyModified.GDP.varorlog_gdp?
Guiding principles 2/2
Annotate your code concisely but generously
- Better to err on the side of too many annotations
- They should help a reader understand the purpose of any significant piece of your code
- Annotate single lines when the code is relatively complex
Make your code look good, which is to say easy to read
- Keep consistent indentation and spacing
- Exploit RStudio’s ability to fold your code
- Avoid deep nesting when possible
- Let your code breathe — leave some blank space
- Favor several lines with short expressions over one long line with several expressions
General tips on the process of writing code
Before starting, visualize the workflow
- “If you don’t know where you’re going, any road’ll take you there” — after Alice in Wonderland, Lewis Carroll
- Have a clear idea of the repetitive tasks
- Think about how the project and the code should be structured when it is done
Then start writing your code interactively
- Working in the console and the editor together
- But do not lose sight of the forest for the trees
Do not run a big task without testing it on a small dataset
Constantly re-check that your code runs entirely, not just in pieces
Leave breadcrumbs so you can find a mistake later
Getting the data
Two files: one monitor in Compton, and every PM2.5 monitor in Los Angeles County, both for 2025. This chunk fetches them if you do not already have them, and does nothing if you do.
dir.create("data", showWarnings = FALSE)
f <- "data/epa_pm25_compton_2025.csv"
if (!file.exists(f)) {
download.file(paste0("https://arielortizbobea.github.io/aem6850/",
"fall-2026/sessions/", f), f)
}
f <- "data/epa_pm25_la_county_2025.csv"
if (!file.exists(f)) {
download.file(paste0("https://arielortizbobea.github.io/aem6850/",
"fall-2026/sessions/", f), f)
}
list.files("data", pattern = "^epa_.*\\.csv$")#> [1] "epa_pm25_compton_2025.csv" "epa_pm25_la_county_2025.csv"
dir.create() makes the folder and stays quiet when it is already there. download.file() fetches a URL to a path. The if (!file.exists(f)) wrapper means running this a second time costs nothing.
A script that fetches its own inputs runs on a machine that has never seen the data. That includes a coauthor’s, a referee’s, and your own six months from now.
Building the Compton dataframe
Everything below runs off the Compton file, read here so this page and its script stand on their own.
pm <- read.csv("data/epa_pm25_compton_2025.csv",
colClasses = c("Site.ID" = "character"))
names(pm)[names(pm) == "Daily.Mean.PM2.5.Concentration"] <- "pm25"
names(pm)[names(pm) == "Local.Site.Name"] <- "site"
pm$date <- as.Date(pm$Date, format = "%m/%d/%Y")
pm <- pm[pm$POC == 1 & pm$AQS.Parameter.Code == 88101, ]
nrow(pm) # one regulatory instrument, the days it ran#> [1] 301
Five lines: read the file with the identifier protected, rename the concentration and site columns, convert the date, keep one instrument. Every EPA (the US Environmental Protection Agency) file needs the same five lines, and every script that reads one starts by typing them again. A function lets you type them once.
Naming things and named constants
STANDARD <- 35 # US EPA 24-hour PM2.5 standard, ug/m3
sum(pm$pm25 > STANDARD) # days above the standard, 2025#> [1] 10
Names are short and informative, lower case, with underscores: pm25, site, days_above. Verbs for functions. Not MyModified.PM.var, not T for TRUE.
A number that means something gets a name at the top of the script, in capitals, and is used by that name everywhere. 35 typed in six places is six chances to change five of them.
Comments say why, not what. # EPA 24-hour standard tells a reader something the code cannot; # set x to 35 repeats the line below it.
Write code that is easy to check
Whether code can be checked is a property of how it is built, not of how hard you look at it afterwards. Small pieces with one job each. Intermediate values that land in named objects, so nrow(pm) after the filter and range(pm$date) after the date conversion are one line away. No step that changes three things at once. Results that come out where a check can reach them. Code you cannot check is code you cannot defend, whatever it computed.
function(): naming a block of code
days_above <- function(x, standard = 35) {
sum(x > standard)
}
dec <- pm[format(pm$date, "%m") == "12", ]
days_above(dec$pm25) # December, at the 35 standard#> [1] 5
days_above(dec$pm25, standard = 50) # the same days, a higher bar#> [1] 3
days_above(c(1, 50)) # a case you can check by eye#> [1] 1
function() builds a function. The names in its parentheses are the arguments; standard = 35 gives one of them a default, so a call can leave it out. The body runs when you call the function, with x standing for whatever you passed in. The value of a function is its last expression. return(x) says the same thing explicitly, and is only needed to leave a function early.
December had 5 days above the standard and 3 above 50. The last call is the one to keep in the habit: two numbers you can check by eye, and the answer must be 1.
Two versions of the same function
Both return the number of days above the standard.
days_above <- function(x, standard = 35) {
sum(x > standard)
}da <- function(v, s = 35, na = TRUE, f = NULL) {
if (!is.null(f)) v <- f(v)
length(which(if (na) v[!is.na(v)] > s else v > s))
}The second returns the same answer, and it does more: it drops missing values, and it will apply a transformation first if you hand it one. Every one of those additions was free to write and is paid for on every reading.
da does not say what it returns. v, s, na and f do not say what they take. Three of the four arguments have defaults nobody at the call site will see, so da(x) hides three decisions. length(which(...)) counts what sum() counts, in more steps.
The same dimensions from the two script versions apply here: does the name say what comes back, are the values visible at the call, and is the body short enough to hold at once.
Local names and global names
count_pos <- function(x) {
n <- sum(x > 0) # n exists inside the call
n
}
count_pos(c(-1, 2, 3))#> [1] 2
n # and not outside it#> Error: object 'n' not found
A name made inside a function dies at the closing brace. Outside the call, n never existed. That is what lets a function use short names like n and d without trampling anything in the workspace.
A function can read a global name
standard <- 35
share_above <- function(x) sum(x > standard) / length(x)
share_above(dec$pm25)#> [1] 0.1612903
rm(standard)
share_above(dec$pm25) # worked until the global was gone#> Error in share_above(dec$pm25): object 'standard' not found
The other direction is the trap. A function can read a name from outside itself, and share_above() does: it uses standard without ever being given it. It works while standard happens to sit in the workspace and fails in a fresh session with object 'standard' not found. Everything a function needs should come in through its arguments. Then it works anywhere, and a two-number test tells you it reads its own input.
Writing the five lines once
read_epa <- function(path) {
d <- read.csv(path, colClasses = c("Site.ID" = "character"))
names(d)[names(d) == "Daily.Mean.PM2.5.Concentration"] <- "pm25"
names(d)[names(d) == "Local.Site.Name"] <- "site"
d$date <- as.Date(d$Date, format = "%m/%d/%Y")
d[d$POC == 1 & d$AQS.Parameter.Code == 88101, ]
}
files <- list.files("data", pattern = "^epa_.*\\.csv$", full.names = TRUE)
frames <- lapply(files, read_epa)
names(frames) <- basename(files)
la <- frames[["epa_pm25_la_county_2025.csv"]]
la_jan <- la[format(la$date, "%m") == "01", ]
by_site <- split(la_jan, la_jan$site)
sapply(by_site, nrow)#> Compton Lancaster - Fairgrounds
#> 31 31
#> Long Beach-Route 710 Near Road Los Angeles-North Main Street
#> 26 30
#> Pasadena Pico Rivera #2
#> 11 11
#> Reseda Signal Hill (LBSH)
#> 11 6
The five lines from the build, with path in place of the file name and the frame as the value. Copy-pasting code is a red flag, and the function is the fix: a change to the rename or to the instrument filter now happens in one place, and every file gets it.
lapply() hands the function every EPA file in the folder. split() cuts January into one frame per site. Eight sites, not the county’s eleven: three run no regulatory instrument, so the filter inside read_epa() removed them. Count rows before you trust a mean — four sites report between 6 and 11 January days.
Exercise
# 1. Write mean_above(x, standard = 35): the mean of the readings in x
# that are above the standard.
# 2. Run it on every site in by_site with sapply().
# 3. One number to compare with the room: Pasadena.
# 4. One site returns NaN. Say why in one sentence, and whether that is
# wrong.mean_above <- function(x, standard = 35) mean(x[x > standard])
round(sapply(by_site, function(s) mean_above(s$pm25)), 1)#> Compton Lancaster - Fairgrounds
#> 46.9 NaN
#> Long Beach-Route 710 Near Road Los Angeles-North Main Street
#> 39.2 82.1
#> Pasadena Pico Rivera #2
#> 58.1 40.1
#> Reseda Signal Hill (LBSH)
#> 45.1 49.3
Pasadena 58.1. Lancaster - Fairgrounds returns NaN: no January reading there was above 35, so x[x > standard] is empty and the mean of nothing is not a number. That is the honest answer. A function that quietly returned 0 would be lying.
stopifnot(): a check that stops the script
jan <- pm[format(pm$date, "%m") == "01", ]
stopifnot(nrow(pm) == 301)
stopifnot(nrow(jan) == 31)stopifnot() takes conditions. If every one is TRUE it does nothing; if any is FALSE it stops the script and names the first that failed, so a reader knows what was expected without opening the script. Several conditions fit in one call, one per line when they are long. A check that sits commented out is turned on by selecting its lines and pressing Ctrl+Shift+C (Cmd+Shift+C on a Mac).
Put the check right after the step it guards. A step that changes a row count is where a mistake enters without announcing itself, and a check there turns a wrong number into a stopped script.
Test small, then run big
days_above(c(1, 50)) # a case you can check by eye#> [1] 1
nrow(read_epa(files[1])) # one file before the folder#> [1] 301
system.time(lapply(files, read_epa)) # the whole folder, timed#> user system elapsed
#> 0.043 0.002 0.044
Never run the big job untested. A function gets a case you can check by eye. A loop runs on one file before the folder, on one site before all of them. system.time() around the big call tells you what the next run will cost: nothing here, and minutes when a folder holds a thousand files.
Restart R and source the script
# Session > Restart R (Ctrl+Shift+F10 on Windows, Cmd+Shift+0 on a Mac)
ls() # character(0), or it was not a clean restart
getwd() # where "data/..." is looked for
list.files("data") # what is there
source("air.R") # your script, top to bottom, in a clean sessionA script that runs only because of what is already in your environment is not a script yet. Restart R: packages unload, and the workspace empties once Tools > Global Options > General has “Save workspace to .RData on exit” set to Never and “Restore .RData into workspace at startup” unchecked. On the default settings RStudio puts the old objects back after a restart, so set both once, and expect ls() to print character(0). Then source() the file. It runs top to bottom and stops at the first error. rm(list = ls()) clears objects but not packages or options; it is not a restart.
source() reads "data/..." relative to the working directory. Opening a project’s .Rproj file sets the working directory to the project folder, which is why every path in a script is relative: "data/file.csv", never "/Users/you/Desktop/file.csv", which runs on one laptop and nowhere else. When R says a file does not exist, check getwd() and list.files("data") before blaming the file.
Debugging as a strategy
Read the error before changing anything. It names the call that failed and usually the cause. Reproduce the failure on the smallest input that still fails: one file, one site, two numbers. Form one hypothesis at a time and test it. Change one thing per run, so that when the output changes you know why.
Then the tool, matched to the failure. traceback() for where it died. print() for how a value evolved. Halving for a wrong number that never errored.
traceback(): where it died
frames <- lapply(c("data/epa_pm25_compton_2025.csv", "data/nope.csv"), read_epa)
traceback()Error in file(file, "rt") : cannot open the connection
In addition: Warning message:
In file(file, "rt") :
cannot open file 'data/nope.csv': No such file or directory
5: file(file, "rt")
4: read.table(file = file, header = header, sep = sep, quote = quote,
dec = dec, fill = fill, comment.char = comment.char, ...)
3: read.csv(path, colClasses = c(Site.ID = "character"))
2: FUN(X[[i]], ...)
1: lapply(c("data/epa_pm25_compton_2025.csv", "data/nope.csv"),
read_epa)
traceback() prints the calls that were running when the error happened, innermost first. Read it from the bottom: your lapply() (1) called your function (2, FUN is read_epa), which called read.csv() (3), which could not open the file (5). The warning names the file. The error is in the line you wrote at 1, not in read.csv().
print() and cat(): how a value evolved
for (site in names(by_site)[1:3]) {
s <- by_site[[site]]
cat(site, ": ", nrow(s), " days, ", days_above(s$pm25), " above\n", sep = "")
}#> Compton: 31 days, 4 above
#> Lancaster - Fairgrounds: 31 days, 0 above
#> Long Beach-Route 710 Near Road: 26 days, 3 above
print(c(days = nrow(jan), above = days_above(jan$pm25)))#> days above
#> 31 4
Inside a loop or a function, a print() or cat() line shows what a value was on each pass. print() shows an object the way the console would; cat() pastes text and numbers into one line, and "\n" ends it. A named vector, c(days = , above = ), prints label and value together. Take the line out once the question is answered.
browser() is the same idea with a pause. Put browser() inside a function and the run stops there with a Browse[1]> prompt; you can print any local name, n steps to the next line, c continues, Q quits.
Halving: a wrong number that never errored
No error, no warning, and the number is wrong. traceback() has nothing to point at. Where could the wrong number have entered? Every step that changes a row count or a value is a suspect.
Write down what each step should produce: rows after the read, rows after the filter, the date range, the count. Comment out the second half of the script and run the first half. Compare what printed with what you wrote down. Halve the half that disagrees. Repeat until the mistake has nowhere to hide. It is usually one line, and the line is usually not the one that printed the number.
Documenting a diagnosis
# Line: share_above <- function(x) sum(x > standard) / length(x)
# Mechanism: standard is not an argument; the function reads it from the
# workspace and fails in a fresh session
# Fix: the standard comes in as an argument, with a default
share_above <- function(x, standard = 35) sum(x > standard) / length(x)
# Proof:
stopifnot(share_above(c(1, 50)) == 0.5)Four lines, as comments, next to the fix. The line that was wrong. The mechanism, meaning why it produced what it produced. The fix. And the proof: a small case that now gives the right answer, written as a check that runs. That is what a fixed script carries. When a check fails and you cannot fix it, the same four lines with the fix left blank are the honest note, and they are worth more than an edited check.
Practice
# Everything below uses pm, dec, jan, files, frames, la, by_site,
# read_epa() and days_above(), all built earlier in this script.
# Functions
# 1. Write peak_day(d): the date of the highest pm25 in a data frame d.
# Run it on pm. Check the answer against the row that which.max()
# points at.
# 2. Run peak_day() on every site in by_site. Which site peaked last in
# January? (sapply() turns dates into numbers; wrap the call in
# format() to keep them readable.)
# 3. From memory, without scrolling up: write share_above(x, standard = 35)
# so that it works in a fresh session, with nothing read from the
# workspace. Run it on dec$pm25 and on c(1, 50).
# Lists and loops
# 4. How many sites does each frame in frames hold? One sapply() call.
# 5. One plot per site, written to disk: for each site in by_site, a
# line plot of pm25 against date saved as a PNG named after the site.
# Do not run it until it works for one site.
# Checks
# 6. After read_epa() on the county file, write one stopifnot() with two
# conditions: exactly 8 sites, and no date outside 2025.
# 7. This check fails on la. Say why, and whether the data or the check
# is wrong. Then write the check that is right for this frame.
# stopifnot(nrow(la) == length(unique(la$date)))
# 8. Time read_epa() on the county file alone, then lapply() over both
# files. Does the second take more than the first? By roughly how much?# 1.
peak_day <- function(d) d$date[which.max(d$pm25)]
peak_day(pm)#> [1] "2025-12-13"
pm[which.max(pm$pm25), c("date", "pm25")]#> date pm25
#> 283 2025-12-13 56
# 2. Pasadena, on 2025-01-10. Four sites peaked on January 1.
sapply(by_site, function(s) format(peak_day(s)))#> Compton Lancaster - Fairgrounds
#> "2025-01-01" "2025-01-07"
#> Long Beach-Route 710 Near Road Los Angeles-North Main Street
#> "2025-01-09" "2025-01-08"
#> Pasadena Pico Rivera #2
#> "2025-01-10" "2025-01-01"
#> Reseda Signal Hill (LBSH)
#> "2025-01-01" "2025-01-01"
# 3.
share_above <- function(x, standard = 35) sum(x > standard) / length(x)
round(share_above(dec$pm25), 3)#> [1] 0.161
share_above(c(1, 50))#> [1] 0.5
# 4.
sapply(frames, function(d) length(unique(d$site)))#> epa_pm25_compton_2025.csv epa_pm25_la_county_2025.csv
#> 1 8
# 6. The second condition is a whole vector; every element must be TRUE.
stopifnot(length(unique(la$site)) == 8,
format(la$date, "%Y") == "2025")
# 7. The check is wrong for this frame, not the data. Several sites
# report on the same day, so dates repeat by design. The right check
# is one row per site and day.
nrow(la) == length(unique(la$date))#> [1] FALSE
stopifnot(nrow(la) == nrow(unique(la[, c("site", "date")])))
# 8. Barely. The Compton file is about a seventh the size of the county
# file, so the second call costs about a seventh more.
system.time(read_epa("data/epa_pm25_la_county_2025.csv"))#> user system elapsed
#> 0.037 0.001 0.038
system.time(lapply(files, read_epa))#> user system elapsed
#> 0.042 0.003 0.046
# 5. Test on one site first: for (site in names(by_site)[1]). Then all of them.
for (site in names(by_site)) {
s <- by_site[[site]]
png(paste0(site, ".png"), width = 1600, height = 800, res = 150)
plot(s$date, s$pm25, type = "l", main = site,
xlab = "", ylab = "PM2.5 (ug/m3)")
dev.off()
}Quick reference
Functions and lists
| Code | What it does |
|---|---|
f <- function(x, k = 35) { ... } |
A function; k = 35 is a default; the value is the last expression |
return(x) |
Leave the function now with x; only needed to leave early |
function(s) ... inside lapply() |
A function with no name, written where it is used |
list.files("data", pattern =, full.names = TRUE) |
Paths you can hand to a function |
lapply(files, f) |
A list in, a list out: f on each element |
sapply(x, f) |
The same, simplified when every element has the same shape |
split(d, d$g) |
A list, one data frame per value of g |
Checks and habits
| Code | What it does |
|---|---|
stopifnot(a, b) |
Stop the script and name the first condition that is not TRUE |
stopifnot("why" = a) |
The same, printing your message instead |
STANDARD <- 35 |
A named constant at the top; used by name everywhere |
# Name ---- |
A section RStudio’s outline can jump to |
system.time(expr) |
Seconds the line took; wrap the big call once |
Session > Restart R, then source("air.R") |
The only test that a script runs on its own |
getwd(), list.files("data") |
Where relative paths are looked for, and what is there |
When something is wrong
| Symptom | Tool |
|---|---|
| An error | Read it. Then traceback(): your call is at the bottom |
| A loop or function misbehaves on some passes | print() or cat() inside it; browser() to pause there |
| A wrong number and no error | Halve the script: write down what each step should give, compare |
object 'x' not found inside a function |
The function reads a global; make x an argument |
NaN from a mean |
The subset was empty; that is the honest answer |
| A file “does not exist” | getwd() and list.files() before blaming the file |