Skip to Content
Course content

206: Anonymization Techniques in R

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you're dealing with sensitive data—think healthcare records or financial transactions—simply deleting the "Name" column is rarely enough. I've seen plenty of analysts make the mistake of thinking they've anonymized a dataset, only to realize that a combination of birth date, zip code, and gender can uniquely identify a huge chunk of the population. We need to move beyond simple deletion and look at actual obfuscation.

How do I replace IDs with something random but still keep them linkable?

If you need to perform joins across multiple tables, you can't just delete the Patient ID. You need a "pseudonym." The best way to do this in R is through hashing. I usually reach for the digest package because it's reliable and straightforward. A hash takes an input and turns it into a fixed-length string that's mathematically impossible to reverse without the original key.

library(digest)
library(dplyr)

# Let's imagine a patient dataset
patients <- data.frame(
  patient_id = c("P101", "P102", "P103"),
  name = c("Alice Smith", "Bob Jones", "Charlie Brown"),
  diagnosis = c("Flu", "Cold", "Covid")
)

# We'll create a salt—a random string added to the ID to prevent 
# "rainbow table" attacks where someone guesses the ID.
salt <- "s3cr3t_k3y_2024"

patients_anon <- patients %>%
  mutate(patient_id = sapply(patient_id, function(x) {
    digest(paste0(x, salt), algo = "sha256")
  })) %>%
  select(-name) # Now we can safely drop the name

print(patients_anon)

What's the best way to handle dates and ages without giving away the person's identity?

Exact birth dates are a goldmine for re-identification. Unless your analysis specifically requires the exact day, you should "generalize" the data. I prefer binning. Instead of saying someone is 43, say they are in the "40-49" bracket. It preserves the statistical utility of the data while adding a layer of plausible deniability.

# Using a dataset with ages
clinical_data <- data.frame(
  patient_id = c("H1", "H2", "H3", "H4"),
  age = c(22, 25, 44, 48)
)

# I use cut() to create bins. 
# This transforms a precise number into a categorical range.
clinical_data_anon <- clinical_data %>%
  mutate(age_group = cut(age, 
                         breaks = seq(0, 100, by = 10), 
                         right = FALSE, 
                         labels = c("0-9", "10-19", "20-29", "30-39", "40-49", "50-59", "60-69", "70-79", "80-89", "90-99"))) %>%
  select(-age)

print(clinical_data_anon)

How do I stop "outliers" from leaking who a person is?

This is where things get tricky. Imagine a dataset of employees in a small company. Even if you remove names, if there is only one person in the "Executive" role who is over 60, everyone knows who that record belongs to. This is a failure of k-anonymity.

The simplest fix is suppression. If a specific combination of attributes (like Role + Age Group) appears fewer than, say, 3 times in your dataset, you mask those values. It's a bit aggressive, but it's the only way to be sure.

library(dplyr)

staff_data <- data.frame(
  role = c("Dev", "Dev", "Dev", "Dev", "CEO", "HR", "HR"),
  city = c("NY", "NY", "NY", "NY", "NY", "LA", "LA")
)

# Identify combinations that are too rare (less than 2 occurrences)
staff_anon <- staff_data %>%
  group_by(role, city) %>%
  mutate(count = n()) %>%
  ungroup() %>%
  mutate(role = ifelse(count < 2, "Other", role)) %>%
  select(-count)

# Notice how the 'CEO' became 'Other' because they were the only one in that group.
print(staff_anon)



📋 Practical Task

Exercise: Scrubbing the Clinical Trial Participant Registry

You have been handed a raw dataframe containing participants from a clinical trial. Your goal is to prepare this dataset for a third-party research team by applying the three techniques discussed in this lesson.

The Dataset:

trial_data <- data.frame(
  participant_id = c("T001", "T002", "T003", "T004", "T005"),
  full_name = c("John Doe", "Jane Roe", "Baby Yoda", "Sarah Connor", "Ellen Ripley"),
  age = c(24, 28, 21, 55, 82),
  zip_code = c("10001", "10001", "10001", "90210", "90210"),
  condition = c("Mild", "Mild", "Severe", "Mild", "Severe")
)

Your Requirements:

  • Pseudonymize: Use the digest package to hash the participant_id. Use the salt "trial_secret_2024".
  • Generalize: Convert the age column into 10-year bins (e.g., 20-29, 80-89).
  • Suppress: Find any zip_code that appears fewer than 3 times in the dataset and replace the value with "MASKED".
  • Cleanup: Remove the full_name column entirely.

Produce the final trial_data_final dataframe that meets all these security criteria.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.