; PSY 1903
PSY 1903 Programming for Psychologists

Further Troubleshooting Loops and Functions

When creating new functions and loops, it is important to make sure they can work independently and give you the outcome you expect. An easy and effective way to to do this is to clear the global environment or non-crucial global variables that may be sneaking their way into your code, and then running your function or loop in isolation.

Why Clear the Global Environment?

Clearing the global environment gives you a 'fresh start,' removing all previously defined variables, data frames, and functions. This prevents old or unintended objects from interfering with new code, which is especially important for testing, troubleshooting, and ensuring your code runs independently.

When to Clear the Global Environment

Clear the environment if:

  • You’re troubleshooting functions or loops and want to make sure they don’t rely on any leftover variables.
  • You’re testing code for accuracy and want a clean slate.
  • You’re starting a new analysis and don’t want interference from previous work.

How to Clear the Global Environment

  • In RStudio: Click the broom icon in the Environment panel.
  • In Code: Run rm(list = ls()) to remove all objects in the global environment.

Running Functions and Loops in Isolation

Testing functions and loops separately helps identify issues within them and ensures they aren’t unintentionally using global variables. It also lets you see if they work with only the necessary inputs.

How to Run Functions in Isolation

  1. Define the Function Separately: Ensure all inputs are passed as arguments, so it doesn’t rely on global variables.
  2. Create Test Inputs: Make a small, controlled dataset to test the function. You can create your own new test data frame (ask ChatGPT!) or use a small subset of one of your existing data frames (benefit: keeping the same structure as your actual data).
  3. Run and Check Output: Call the function with test data and use print() statements to monitor key values and the ls() function to make sure variables are defined where you expect them, making sure the function works as expected.

How to Run Loops in Isolation

  1. Separate the Loop Code: Copy the loop to a new script or comment out unrelated code.
  2. Use Simple Data: Use a small data frame or vector so you can see each step’s output clearly. Again, create a new small data set or subset from one of your larger ones.
  3. Print Loop Variables: Add print() and ls() statements to observe changes in variables at each step, making it easy to spot logical errors.

By clearing the environment and isolating functions or loops, you’re setting up a clean and controlled testing space, which is essential for identifying errors and making sure each part of your code works properly.


More Details for Reference

Below are more details on saving improtant objects and clearing your environment to test functions and loops.

Clearing the Global Environment

Once you have a chunk of code working the way you want it to, particulary a function() or loop that sets a local environment that interfaces with the global environment in some way, make sure it continues to work after you have reset the global environment or removed all non-crucial objects from the global environment.

Let's start with defining some objects to save, clear, and reload:

#### Creating some local test data ---------------------------------------------
## Defining my_data as a data frame
my_data <- data.frame(
  participant_id = 1:5,
  age = c(25, 30, 22, 40, 28),
  gender = c("F", "M", "F", "M", "F"),
  reaction_time = c(350, 420, 310, 390, 410)
)

## Print the data frame
print(my_data)

## Defining my_model as a linear regression model
my_model <- lm(reaction_time ~ age, data = my_data)

## Print the model summary to inspect the coefficients
summary(my_model)

## Defining my_variable as a numeric value
my_variable <- 0.5

## Print the value of my_variable
print(my_variable)

First, Save Important Objects!!!

Make sure you have saved any important objects (particularly data frames, statistical models, or functions not defined in your RScript) before clearing the global environment. This is important for objects that you have modified from their original version and want to keep in their modified format.

  • Save only specific objects (e.g., my_data, my_model, and my_variable) to an .RData file
    • .RData files are good when you have multiple objects to save at once and need them stored together.
#### Saving important objects --------------------------------------------------
## Save objects "my_data", "my_model" and "my_variable" together
save(my_data, my_model, `my_variable`, file = "important_objects.RData")

## Clear global environment
rm(list = ls())

## Reload those three objects with the same object names "my_data", "my_model" and "my_variable":
load("important_objects.RData")
  • To back up the entire workspace, save all objects to an .RData file:
## Save workspace
save.image(file = "workspace_backup.RData")

## Clear global environment
rm(list = ls())

## Troubleshoot your code

## Reload workspace
load("workspace_backup.RData") # Note, this will reload all global objects, so don't troubleshoot after reloading workspace or you'll run into the same issues
  • Saving Important Objects as CSVs
    • CSVs are a good way to save tabular data (e.g., your data frames), especially when you want them human-readable format, for instance to open and view in applications like Excel or Google Sheets.
## Save important data frames or objects, e.g., "my_data", as CSV:
write.csv(my_data, file = "my_data.csv", row.names = FALSE)

## Clear global environment
rm(list = ls())

## Reload "my_data"
my_data <- read.csv("my_data.csv")
  • Saving Important Objects as .rds
    • An .rds file in R is a file format used to save a single R object, such as a data frame, model, or any other R object, to your computer in a way that preserves its structure and content. It is particularly useful when you want to save individual objects (rather than the entire workspace) and reload them later.
## Save other important objects, e.g., "my_data", as .rds:
saveRDS(my_data, file = "my_data.rds")

## Clear global environment
rm(list = ls())

## Reload "my_data"
my_data <- readRDS("my_data.rds")

Summary of Saving Objects

Feature .csv .rds .RData
Stores single object Yes (only data frames) Yes (any R object) No, stores multiple objects
Data type supported Data frames only Any R object Any R object
Human-readable Yes No No
Compressed No Yes Yes
Required load function read.csv() / write.csv() readRDS() / saveRDS() load() / save()
Naming on load Must specify name manually Must specify name manually Restores original object names
Cross-compatible Yes No No
Use case Sharing tabular data, reading in other software Single R object in analysis Multiple R objects saved together

Second, Clear the Global Environment or Clear Specific Objects

Once you have saved all important objects, you can safely clear your global environment or specific objects to troubleshoot your code.

  • Clearing the Global Environment will clear all objects (data frames, variables, functions) that are defined in the global environment. You can clear the global environment by clicking the broom "clear objects from the workspace" icon in the Environment Panel, or by using the code rm(list = ls())

    • When to use:
      • Starting Fresh: This is helpful when you want a completely clean slate, especially for troubleshooting or testing code.
      • Avoiding Interference: A cleared environment ensures that no residual objects from previous code executions interfere with new runs.
      • Setting Up Reproducible Code: By clearing the environment and only loading necessary files or running required code, you can verify that your script works independently of any leftover objects.
    • Pros:
      • Complete Reset: No leftover objects can interfere with code.
      • Better Testing: Helpful for testing functions, loops, and other code segments to ensure they don’t rely on unintended global variables.
  • Cons:

    • Loss of Important Data: If you have unsaved objects, you may lose them when the environment is cleared.
    • Reloading Time: After clearing, you may need to reload or redefine essential objects, which can slow down the workflow.
  • Clearing Specific Objects will only remove the objects you specify, by using the rm() function and the names of the objects you want to clear. For example, to clear my_data and my_variable, you can use rm(my_data, my_variable).

    • When to use:
      • Keeping Key Objects: If you have data, models, or other objects you plan to use frequently, you can keep them while removing only temporary or unwanted objects.
      • Selective Reset: Useful if you only want to clear temporary objects from a function or intermediate calculations that are no longer needed.
    • Pros:
      • Targeted Clearing: Only specific objects are removed, so you can retain important data or models in the environment.
      • Efficient Workflow: Avoids reloading or recomputing important data while still removing clutter from the workspace.
    • Cons:
      • Possibility of Errors: If you accidentally remove essential objects, it may affect your code’s behavior.
      • Potential for Overlooked Interference: Specific clearing may leave behind objects you didn’t intend to keep, potentially causing interference and not solving your original issue of having global-local object interference.

Deciding Between the Two Clear Entire Environment if:

  • You’re troubleshooting or testing functions and want no interference from other variables.
  • You’re starting a new analysis and want a clean slate.

Clear Specific Objects if:

  • You have key objects (e.g., loaded data, models) that you want to retain.
  • You’re in a long R session and just want to clear temporary or intermediate results without losing primary data.

Summary of Removing Objects

Action Code Example Use Case Pros Cons
Clear Entire Environment rm(list = ls()) Starting fresh or testing code Ensures no interference May lose unsaved data
Clear Specific Objects rm(object1, object2) or rm(list = setdiff(ls(), c("keep1", "keep2"))) Retain key objects while clearing temporary ones Keeps essential data, efficient workflow Risk of unintended leftovers or deletion

Reload Necessary Objects from Saved Files

You can reload only the necessary objects you saved, typically with load() for .RData files, readRDS() for .rds files, or read.csv() for .csv files. This ensures that you only have the specific data you need in your global environment, reducing the risk of unintended interference. But make sure you are only loading necessary objects that you know won't interfere with your function(), loop, or other code that isn't working.

Alternatively, Run Functions or Loops in Isolation

Running functions and loops in isolation is a debugging technique that involves executing a function or loop separately from the rest of your code to ensure it works as expected. This approach helps identify issues within a specific part of your code, especially if you suspect it may not be functioning correctly, relying on a global variable, or incorrectly assigning a local variable.

  • Run each function independently after clearing the environment to verify that it operates correctly with only the reloaded objects.
  • Test functions using small, controlled inputs to confirm they perform as expected without reliance on hidden global variables.
  • By isolating functions, you’ll notice if an error arises due to a missing global variable or incorrect assumptions about available data.

Steps to Run a Function in Isolation

  1. Define the Function Separately: Ensure the function is saved and defined correctly before calling it in your main script. Confirm that all required inputs are passed as arguments.
  2. Create Test Inputs: Define small, controlled inputs or sample data to use with your function, which can help identify issues with input handling.
#### Creating test input to test functions and loops ---------------------------
test_data <- data.frame(x = c(1, 2, 3), y = c(4, 5, 6))

test_data

x y
1 4
2 5
3 6
  1. Run the Function Alone: Call the function with test data outside the main code to check that it performs as expected.
#### Testing a function in isolation -------------------------------------------
## Function Example in Isolation:
my_function <- function(data) {
  data[,"product"] <- data[,"x"] * data[,"y"]
  return(data)
}
test_data <- my_function(test_data) # Adds column "product" and fills it with the product of x * y
  1. Use Print Statements: Add print() statements within the function to display the values of key variables, which can reveal if certain values are not being calculated or passed correctly.
my_function <- function(data) {
    print(data)  # Check input data
    result <- test_data$x * test_data$y
    print(result)  # Check calculated result
    return(result)
}
  1. Use traceback() Immediately After an Error: If you encounter an error, immediately run traceback() to see the call stack, which shows the sequence of function calls leading to the error. This helps identify the specific line or function where the error occurred, which is particularly useful in nested loops and function calls.

Steps to Run a Loop in Isolation

  1. Separate the Loop Code: Copy and paste the loop code into a new R script, or comment out unrelated code around it.
  2. Use Simple Input Data: Use smaller, sample data if possible to test the loop's functionality. This helps you quickly identify logical issues without processing a full dataset.
  3. Run the Loop Step-by-Step: You can manually run each line of the loop to see how it operates at each iteration, particularly if you’re debugging specific steps.
  4. Inspect Loop Variables with print(): Print key variables within the loop to observe changes at each iteration, which helps identify unintended values or logical issues.
#### Testing a loop in isolation -----------------------------------------------
## Loop Example in Isolation:
test_data$product <- NA # Adds blank column "product"
for (row in 1:3) {
  test_data[row,"product"] <- test_data[row,"x"] * test_data[row,"y"] # fills each row with product of x * y
  print(test_data[row,"product"])
}
  1. Confirm Expected Output: Compare the output of the loop to what you expect at each step. This comparison helps confirm whether the loop logic is correct.
  2. Use Temporary Variables: Store outputs from each iteration in a temporary variable or list to check that data is being updated correctly and as expected.
results <- vector("list", length(test_data$x))  # Define a list to store results

for (i in 1:length(test_data$x)) {
    results[[i]] <- test_data[i, "x"] * test_data[i, "y"] # Fill i-th item of list with the product of x * y from the i-th row of test_data
    print(results[[i]])  # Print result for the current iteration
}

Conclusion

Understanding and managing scope is essential for writing clean, efficient, and error-free R code. The global scope allows variables and functions to be accessed throughout the entire script, but it can lead to unintended consequences if not handled carefully. By defining variables and functions within the appropriate local scope, you can ensure that changes are contained within their intended environment. Passing variables explicitly to functions and using unique variable names helps prevent conflicts between local and global variables.

Additionally, clearing the global environment, saving important objects, and isolating functions() and loops for testing can significantly improve the debugging process. Overall, good scope management helps ensure your code runs as intended and minimizes unexpected behavior, leading to more maintainable and reproducible analyses.