R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
206: Anonymization Techniques in R
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
digestpackage to hash theparticipant_id. Use the salt"trial_secret_2024". - Generalize: Convert the
agecolumn into 10-year bins (e.g., 20-29, 80-89). - Suppress: Find any
zip_codethat appears fewer than 3 times in the dataset and replace the value with"MASKED". - Cleanup: Remove the
full_namecolumn entirely.
Produce the final trial_data_final dataframe that meets all these security criteria.
There are no comments for now.