Anchor article: Baumgaertner Nunn, E. (2026, Sept. 8). Can geneticists fight disease by targeting “nurture” instead of “nature”? The New York Times.https://www.nytimes.com/2026/09/08/science/epigenetic-editing.html?smid=url-share.
This article describes epigenome editing which is where chemical tags (i.e. methylation, acetylation) are added or removed in certain regions of the genome to up or down regulate gene expression. One example in the article is a single injection that would down regulate a liver gene involved in clearing low density lipoprotein (LDL) or “bad” cholesterol, as a possible replacement for a daily medication. This description fits the gene PCSK9, which is the target in the scientific articles that I used for this analysis.
This analysis test three research questions:
I used the Source Data from Cappelluti et al. (2024), which is open access. It comes as Excel files with one sheet per figure panel. I saved each sheet I needed as a CSV file. For the mouse experiments, each mouse got one injection of nanoparticles carrying either placebo or an epigenome editor.
| CSV file | Paper figure | What it holds | |
|---|---|---|---|
| pcsk9_330days.csv | Fig. 3B | PCSK9 in blood, 17 time points up to 330 days | |
| pcsk9_43days.csv | Fig. 5B | PCSK9 in blood, first 43 days | |
| total_cholesterol.csv | Fig. 5C | Total cholesterol before treatment and 30 days after | |
| ldl_cholesterol.csv | Fig. 5D | LDL-cholesterol before treatment and 30 days after | |
| methylation_cpg.csv | Fig. 5E | Percent methylation at 45 CpG sites in the Pcsk9 promoter | |
| ldl_earlier_cohort.csv | Extended Data Fig. 3E | LDL-cholesterol at day 30 in an earlier cohort of mice |
| Group | What the mice received |
|---|---|
| Vehicle | Salt solution only (baseline control) |
| Mock | Nanoparticles carrying a harmless message (controls for the injection) |
| Cas9 | Classic CRISPR gene editor of Pcsk9 |
| ZFP-ETRs | First epigenetic editor (three parts) |
| EvoETR-8 | Improved all-in-one epigenetic editor |
PCSK9 values are normalised to the vehicle group, so 1.0 means “same as baseline” and 0.4 means “60% lower then baseline.”
The CSV files are laid out for people, not for R: one mouse per
column, group names only in the top cell of each block, title rows above
the numbers, and time points down the rows. For each file I (1) skip the
title rows with skip =, (2) give the columns clear names
with col_names =, (3) treat the blank cells and the text
na as missing with na =, and (4)
pivot_longer() into tidy format (one row per mouse per time
point). Every file has an empty first column (column A in Excel), which
I name blank and drop.
First, the column names for each layout (group, underscore, mouse number):
# load packages
library(tidyverse)
library(lme4)
# create column names for pcsk9_330days.csv, this file contains 7 ZFP-ETR mice, 5 Mock mice, 4 Vehicle mice, and 3 Cas9 mice.
# "blank" represents the empty first column in the CSV.
# "day" represents the time point for each measurement.
names_long <- c(
"blank", "day",
# Name the 7 ZFP-ETR mouse columns
paste0("ZFP-ETRs_", 1:7),
# Name the 5 Mock mouse columns
paste0("Mock_", 1:5),
# Name the 4 Vehicle mouse columns
paste0("Vehicle_", 1:4),
# Name the 3 Cas9 mouse columns
paste0("Cas9_", 1:3)
)
# create column names for pcsk9_43days.csv,, total_cholesterol.csv, and ldl_cholesterol.csv.
# each file contains 6 mice in each of four groups: ZFP-ETR, EvoETR-8, Vehicle, and Cas9.
names_short <- c(
"blank", "day",
# Name the 6 ZFP-ETR mouse columns
paste0("ZFP-ETRs_", 1:6),
# Name the 6 EvoETR-8 mouse columns
paste0("EvoETR-8_", 1:6),
# Name the 6 Vehicle mouse columns
paste0("Vehicle_", 1:6),
# Name the 6 Cas9 mouse columns
paste0("Cas9_", 1:6)
)
# create column names for methylation_cpg.csv., this file contains 3 mice in each of three groups: ZFP-ETR, EvoETR-8, and Vehicle.
# "position" identifies the CpG site or genomic position associated with each methylation measurement.
names_meth <- c(
"blank", "position",
# Name the 3 ZFP-ETR mouse columns
paste0("ZFP-ETRs_", 1:3),
# Name the 3 EvoETR-8 mouse columns
paste0("EvoETR-8_", 1:3),
# Name the 3 Vehicle mouse columns
paste0("Vehicle_", 1:3)
)
# load the pcsk over 330 days csv, skip the four title lines, add column names
pcsk9_wide <- read_csv("pcsk9_330days.csv", skip = 4, col_names = names_long,
na = c("", "na")) %>%
select(-blank)
head(pcsk9_wide)
## # A tibble: 6 × 20
## day `ZFP-ETRs_1` `ZFP-ETRs_2` `ZFP-ETRs_3` `ZFP-ETRs_4` `ZFP-ETRs_5`
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 -1 1.70 1.54 1.33 1.19 1.15
## 2 1 0.160 0.0888 0.182 0.172 0.161
## 3 3 0.305 0.153 0.275 0.381 0.284
## 4 6 0.660 0.170 0.227 0.434 0.199
## 5 10 0.0260 -0.0505 0.243 0.236 0.0394
## 6 15 0.346 0.209 0.423 0.362 0.190
## # ℹ 14 more variables: `ZFP-ETRs_6` <dbl>, `ZFP-ETRs_7` <dbl>, Mock_1 <dbl>,
## # Mock_2 <dbl>, Mock_3 <dbl>, Mock_4 <dbl>, Mock_5 <dbl>, Vehicle_1 <dbl>,
## # Vehicle_2 <dbl>, Vehicle_3 <dbl>, Vehicle_4 <dbl>, Cas9_1 <dbl>,
## # Cas9_2 <dbl>, Cas9_3 <dbl>
# above is in a wide format with one column per mouse I want to reshape it so that it is in a long format where the columns are Id, group, and mouse number
pcsk9_long <- pcsk9_wide %>%
pivot_longer(-day, names_to = "id", values_to = "pcsk9") %>%
separate(id, into = c("group", "mouse"), sep = "_", remove = FALSE) %>%
mutate(group = factor(group, levels = c("Vehicle", "Mock", "Cas9", "ZFP-ETRs")))
# I'm only showing the first few rows here but I looked at the table in the R environment to make sure it worked properly
# check the group names
table(pcsk9_long$group, useNA = "ifany")
##
## Vehicle Mock Cas9 ZFP-ETRs
## 68 85 51 119
head(pcsk9_long)
## # A tibble: 6 × 5
## day id group mouse pcsk9
## <dbl> <chr> <fct> <chr> <dbl>
## 1 -1 ZFP-ETRs_1 ZFP-ETRs 1 1.70
## 2 -1 ZFP-ETRs_2 ZFP-ETRs 2 1.54
## 3 -1 ZFP-ETRs_3 ZFP-ETRs 3 1.33
## 4 -1 ZFP-ETRs_4 ZFP-ETRs 4 1.19
## 5 -1 ZFP-ETRs_5 ZFP-ETRs 5 1.15
## 6 -1 ZFP-ETRs_6 ZFP-ETRs 6 1.33
# load data for PCSK9 over 43 days (same as above)
pcsk9_short <- read_csv("pcsk9_43days.csv", skip = 3, col_names = names_short,
na = c("", "na")) %>%
select(-blank) %>%
pivot_longer(-day, names_to = "id", values_to = "pcsk9")
chol <- read_csv("total_cholesterol.csv", skip = 4, col_names = names_short,
na = c("", "na")) %>%
select(-blank) %>%
pivot_longer(-day, names_to = "id", values_to = "chol")
ldl <- read_csv("ldl_cholesterol.csv", skip = 5, col_names = names_short,
na = c("", "na")) %>%
select(-blank) %>%
pivot_longer(-day, names_to = "id", values_to = "ldl")
head(pcsk9_short)
## # A tibble: 6 × 3
## day id pcsk9
## <dbl> <chr> <dbl>
## 1 1 ZFP-ETRs_1 0.173
## 2 1 ZFP-ETRs_2 0.295
## 3 1 ZFP-ETRs_3 0.257
## 4 1 ZFP-ETRs_4 0.373
## 5 1 ZFP-ETRs_5 0.373
## 6 1 ZFP-ETRs_6 0.221
head(chol)
## # A tibble: 6 × 3
## day id chol
## <dbl> <chr> <dbl>
## 1 -1 ZFP-ETRs_1 83
## 2 -1 ZFP-ETRs_2 76
## 3 -1 ZFP-ETRs_3 75
## 4 -1 ZFP-ETRs_4 79
## 5 -1 ZFP-ETRs_5 72
## 6 -1 ZFP-ETRs_6 89
head(ldl)
## # A tibble: 6 × 3
## day id ldl
## <dbl> <chr> <dbl>
## 1 -1 ZFP-ETRs_1 19
## 2 -1 ZFP-ETRs_2 17
## 3 -1 ZFP-ETRs_3 17
## 4 -1 ZFP-ETRs_4 20
## 5 -1 ZFP-ETRs_5 18
## 6 -1 ZFP-ETRs_6 22
# for the cholesterol models I need one row per mouse with its "before" (day -1) and "after" (day 30) values side by side. I get this by filtering each table and joining them
# start with LDL baseline
base <- ldl %>% filter(day == -1) %>% select(id, ldl_base = ldl)
# combine each mouse's baseline and day 30 cholesterol, LDL, and PCSK9 measurements into one dataset, and organize mice by treatment group.
mice <- base %>%
left_join(ldl %>% filter(day == 30) %>% select(id, ldl_30 = ldl), by = "id") %>%
left_join(chol %>% filter(day == -1) %>% select(id, chol_base = chol), by = "id") %>%
left_join(chol %>% filter(day == 30) %>% select(id, chol_30 = chol), by = "id") %>%
left_join(pcsk9_short %>% filter(day == 30) %>% select(id, pcsk9_30 = pcsk9), by = "id") %>%
separate(id, into = c("group", "mouse"), sep = "_", remove = FALSE) %>%
mutate(group = factor(group, levels = c("Vehicle", "Cas9", "ZFP-ETRs", "EvoETR-8")))
head(mice)
## # A tibble: 6 × 8
## id group mouse ldl_base ldl_30 chol_base chol_30 pcsk9_30
## <chr> <fct> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 ZFP-ETRs_1 ZFP-ETRs 1 19 14 83 78 0.383
## 2 ZFP-ETRs_2 ZFP-ETRs 2 17 21 76 90 0.699
## 3 ZFP-ETRs_3 ZFP-ETRs 3 17 15 75 88 0.749
## 4 ZFP-ETRs_4 ZFP-ETRs 4 20 17 79 85 0.717
## 5 ZFP-ETRs_5 ZFP-ETRs 5 18 13 72 66 0.436
## 6 ZFP-ETRs_6 ZFP-ETRs 6 22 15 89 78 0.710
#load methylation csv (same as above)
meth <- read_csv("methylation_cpg.csv", skip = 4, col_names = names_meth,
na = c("", "na")) %>%
select(-blank) %>%
pivot_longer(-position, names_to = "id", values_to = "meth") %>%
separate(id, into = c("group", "mouse"), sep = "_", remove = FALSE) %>%
mutate(group = factor(group, levels = c("Vehicle", "ZFP-ETRs", "EvoETR-8")))
# load earlier cohort csv and put groups side by side, there are gaps because of the differing sample size for each group so "NA" fills those gaps
cohort1 <- read_csv("ldl_earlier_cohort.csv", skip = 5,
col_names = c("blank", "ZFP-ETRs", "Cas9", "Vehicle"), na = c("", "na")) %>%
select(-blank) %>%
pivot_longer(everything(), names_to = "group", values_to = "ldl") %>%
drop_na(ldl) %>%
mutate(group = factor(group, levels = c("Vehicle", "Cas9", "ZFP-ETRs")))
head(meth)
## # A tibble: 6 × 5
## position id group mouse meth
## <dbl> <chr> <fct> <chr> <dbl>
## 1 106463706 ZFP-ETRs_1 ZFP-ETRs 1 12.3
## 2 106463706 ZFP-ETRs_2 ZFP-ETRs 2 11.0
## 3 106463706 ZFP-ETRs_3 ZFP-ETRs 3 13.5
## 4 106463706 EvoETR-8_1 EvoETR-8 1 17.6
## 5 106463706 EvoETR-8_2 EvoETR-8 2 14.4
## 6 106463706 EvoETR-8_3 EvoETR-8 3 13.0
head(cohort1)
## # A tibble: 6 × 2
## group ldl
## <fct> <dbl>
## 1 ZFP-ETRs 11
## 2 Cas9 12
## 3 Vehicle 20
## 4 ZFP-ETRs 13
## 5 Cas9 14
## 6 Vehicle 8
# calculate the mean PCSK9 level and standard error for each group at each time point
time_summary <- pcsk9_long %>%
group_by(group, day) %>%
summarise(mean = mean(pcsk9), se = sd(pcsk9) / sqrt(n()))
# create a line graph to compare PCSK9 levels over time between treatment groups
ggplot(time_summary, aes(x = day, y = mean, colour = group)) +
geom_line() +
geom_pointrange(aes(ymin = mean - se, ymax = mean + se), size = 0.3) +
labs(x = "Days after injection", y = "PCSK9 in blood (1.0 = baseline)",
colour = "Group",
title = "Does a single dose lower PCSK9 in the blood and for how long",
caption = "Points = group mean, bars = +/- 1 standard error")
Ananlysis: PCSK9 falls in a few days in the ZFP-ETR and Cas9 mice. Mock mice spike to 2 early and then wander back toward 1. The ZFP-ETR curve rises over the first weeks but stays below the control line. Because the data are normalised to the vehicle mean at each time point, the vehicle line is at 1, and the treated groups were already higher than vehicle on day -1, before treatment.
# separate mouse IDs into treatment group and mouse number, and label the time points
ldl_plot <- ldl %>%
separate(id, into = c("group", "mouse"), sep = "_", remove = FALSE) %>%
mutate(group = factor(group, levels = c("Vehicle", "Cas9", "ZFP-ETRs", "EvoETR-8")),
time = factor(day, levels = c(-1, 30), labels = c("Before", "30 days after")))
# create a line graph showing changes in LDL cholesterol for each mouse, separated by treatment group
ggplot(ldl_plot, aes(x = time, y = ldl, group = id, colour = group)) +
geom_line() + geom_point() +
facet_wrap(~ group, nrow = 1) +
labs(x = NULL, y = "LDL-cholesterol (mg/dL)",
title = "LDL Cholesterol Levels Before and After Teatments",
caption = "Each line is one mouse") +
theme(legend.position = "none") #delete legend as its repetitive
# calculate the mean DNA methylation for each treatment group at each CpG position, then convert the positions to relative distances in base pairs
meth_summary <- meth %>%
group_by(group, position) %>%
summarise(mean_meth = mean(meth)) %>%
mutate(bp = position - min(position))
# create a line graph comparing average DNA methylation across the Pcsk9 promoter between treatment groups
ggplot(meth_summary, aes(x = bp, y = mean_meth, colour = group)) +
geom_line() + geom_point() +
labs(x = "Position along the Pcsk9 promoter (base pairs)", y = "% of DNA methylated",
colour = "Group",
title = "Percentage of DNA Methylation on the Pcsk9 promoter")
Analysis: the treatment groups both have more methylation accross the promotor, with the EvoETR-8 being the highest
# select PCSK9 measurements at day 330 and join each mouse's baseline PCSK9 level
d330 <- pcsk9_long %>%
filter(day == 330) %>%
left_join(pcsk9_long %>% filter(day == -1) %>% select(id, baseline = pcsk9), by = "id")
# linear model to compare PCSK9 levels at day 330 between treatments
m1 <- lm(pcsk9 ~ group, data = d330)
summary(m1)
##
## Call:
## lm(formula = pcsk9 ~ group, data = d330)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.11019 -0.07220 -0.01005 0.05695 0.17710
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.00000 0.05042 19.833 3.56e-12 ***
## groupMock 0.02226 0.06765 0.329 0.747
## groupCas9 -0.52255 0.07702 -6.785 6.15e-06 ***
## groupZFP-ETRs -0.37861 0.06321 -5.990 2.48e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1008 on 15 degrees of freedom
## Multiple R-squared: 0.8601, Adjusted R-squared: 0.8321
## F-statistic: 30.73 on 3 and 15 DF, p-value: 1.193e-06
# calculate confidence interval
confint(m1)
## 2.5 % 97.5 %
## (Intercept) 0.8925311 1.1074689
## groupMock -0.1219258 0.1664434
## groupCas9 -0.6867112 -0.3583884
## groupZFP-ETRs -0.5133264 -0.2438878
# generate diagnostic plots to check whether the linear model meets its assumptions
par(mfrow = c(2, 2))
plot(m1)
# extract the estimated coefficients from the linear model and store them in b1
b1 <- coef(m1)
Analysis: The intercept (1) is the Vehicle mean. ZFP-ETR mice mean is 0.38 lower, which is about 38% lower than baseline mice almost a year after one injection. Cas9 mice mean is about 52% lower. At day 330, Mock mice are not different from Vehicle, so the injection alone does not explain the drop. This model explains 86% of the variation (R-squared).
# for ZFP-ETR mice only, I model PCSK9 against time from day 30 onward (after the first drop).
# I express time in units of 100 days so the slope is for change per 100 days.
pcsk9_long <- pcsk9_wide %>%
pivot_longer(-day, names_to = "id", values_to = "pcsk9") %>%
separate(id, into = c("group", "mouse"), sep = "_", remove = FALSE) %>%
mutate(group = factor(group, levels = c("Vehicle", "Mock", "Cas9", "ZFP-ETRs")))
zfp <- pcsk9_long %>%
filter(group == "ZFP-ETRs", day >= 30) %>%
mutate(day100 = day / 100)
# linear model for PCSK9 levels over time in ZFP-ETRs mice
m2 <- lm(pcsk9 ~ day100, data = zfp)
summary(m2)
##
## Call:
## lm(formula = pcsk9 ~ day100, data = zfp)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.30397 -0.13085 -0.02923 0.10715 0.60778
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.49866 0.04324 11.533 <2e-16 ***
## day100 0.03285 0.02227 1.475 0.145
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1876 on 68 degrees of freedom
## Multiple R-squared: 0.03099, Adjusted R-squared: 0.01674
## F-statistic: 2.175 on 1 and 68 DF, p-value: 0.1449
confint(m2)
## 2.5 % 97.5 %
## (Intercept) 0.41237710 0.58494287
## day100 -0.01159727 0.07729631
# plot the second linear model with ZFP-ETRs mice's PCSK9 levels over time
ggplot(zfp, aes(x = day, y = pcsk9)) +
geom_point() +
geom_smooth(method = "lm") +
geom_hline(yintercept = 1, linetype = "dashed") +
labs(x = "Days after injection", y = "PCSK9 (1.0 = untreated)",
title = "PCSK9 levels in ZFP-ETR mice after day 30")
# generate diagnostic plots to check whether the linear model meets its assumptions
par(mfrow = c(2, 2))
plot(m2)
Analysis: The slope from day 30 is 0.03 per 100 days
(95% CI). The interval includes zero, so there is no clear evidence of a
steady upward drift after day 30. The group mean moved from at day 30 to
at day 330, but the plot shows that most of this happens in the first
~50 days and the values then stay around 0.5.
A problem with this model. The same 7 mice are measured at many time points, so the observations are not independent: a mouse that is high in PCSK9 at one time point tends to be high at the others. A linear model treats the 70 values as if they came from 70 different mice. A linear mixed model fixes this by giving each mouse its own baseline level, which is done bellow.
# linear mixed model of the above data
m2_mixed <- lmer(pcsk9 ~ day100 + (1 | id), data = zfp)
summary(m2_mixed)
## Linear mixed model fit by REML ['lmerMod']
## Formula: pcsk9 ~ day100 + (1 | id)
## Data: zfp
##
## REML criterion at convergence: -37.7
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -1.74945 -0.75872 -0.07708 0.45187 2.89238
##
## Random effects:
## Groups Name Variance Std.Dev.
## id (Intercept) 0.01080 0.1039
## Residual 0.02564 0.1601
## Number of obs: 70, groups: id, 7
##
## Fixed effects:
## Estimate Std. Error t value
## (Intercept) 0.49866 0.05391 9.250
## day100 0.03285 0.01902 1.727
##
## Correlation of Fixed Effects:
## (Intr)
## day100 -0.586
confint(m2_mixed, parm = "day100", method = "Wald")
## 2.5 % 97.5 %
## day100 -0.004425286 0.07012432
# create two diagnostic plots to check the assumptions of the linear mixed model
par(mfrow = c(1, 2))
# plot residuals against fitted values to check for patterns and equal variance
plot(fitted(m2_mixed), resid(m2_mixed),
xlab = "Fitted", ylab = "Residuals")
# add a horizontal reference line at zero
abline(h = 0, lty = 2)
# create a Q-Q plot to check whether the residuals are normally distributed
qqnorm(resid(m2_mixed))
qqline(resid(m2_mixed))
Both models have almost the same slope (0.033 from both). The mixed model’s 95% interval is -0.004 to 0.07, slightly narrower than the linear model’s interval (-0.012 to 0.077), but this can be accounted for by mouse to mouse differences.The interval still includes zero, so there is still no clear evidence of a steady upward drift after day 30.
Each mouse has a before and an after value. Instead of only comparing “after” values, I compare groups at the same starting level.
# linear model of the LDL data to compare LDL cholesterol levels at day 30 between treatment groups
m3_ldl <- lm(ldl_30 ~ group + ldl_base, data = mice)
summary(m3_ldl)
##
## Call:
## lm(formula = ldl_30 ~ group + ldl_base, data = mice)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.553 -1.365 0.087 1.307 4.750
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 20.02936 3.52635 5.680 1.78e-05 ***
## groupCas9 -5.20454 1.35064 -3.853 0.00107 **
## groupZFP-ETRs 0.08323 1.39898 0.059 0.95318
## groupEvoETR-8 -4.61361 1.35392 -3.408 0.00295 **
## ldl_base -0.22722 0.19968 -1.138 0.26932
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.339 on 19 degrees of freedom
## Multiple R-squared: 0.5729, Adjusted R-squared: 0.483
## F-statistic: 6.373 on 4 and 19 DF, p-value: 0.00199
# calculate 95% confidence interval
confint(m3_ldl)
## 2.5 % 97.5 %
## (Intercept) 12.6486236 27.4101047
## groupCas9 -8.0314651 -2.3776074
## groupZFP-ETRs -2.8448709 3.0113351
## groupEvoETR-8 -7.4473949 -1.7798226
## ldl_base -0.6451627 0.1907277
# generate diagnostic plots to check whether the linear model meets its assumptions
par(mfrow = c(2, 2))
plot(m3_ldl)
# the same model as above for total cholesterol
m3_chol <- lm(chol_30 ~ group + chol_base, data = mice)
summary(m3_chol)
##
## Call:
## lm(formula = chol_30 ~ group + chol_base, data = mice)
##
## Residuals:
## Min 1Q Median 3Q Max
## -16.775 -4.180 0.147 5.153 13.287
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 101.7793 18.1821 5.598 2.13e-05 ***
## groupCas9 -22.2675 4.8519 -4.589 0.0002 ***
## groupZFP-ETRs 0.9622 4.8414 0.199 0.8446
## groupEvoETR-8 -25.1471 4.8560 -5.179 5.34e-05 ***
## chol_base -0.2773 0.2309 -1.201 0.2446
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8.359 on 19 degrees of freedom
## Multiple R-squared: 0.7187, Adjusted R-squared: 0.6595
## F-statistic: 12.14 on 4 and 19 DF, p-value: 4.575e-05
veh_ldl <- predict(m3_ldl, newdata = data.frame(group = "Vehicle",
ldl_base = mean(mice$ldl_base)))
coef(m3_ldl)["groupEvoETR-8"] / veh_ldl * 100 # % difference for EvoETR-8
## groupEvoETR-8
## -28.65525
coef(m3_ldl)["groupCas9"] / veh_ldl * 100 # % difference for Cas9
## groupCas9
## -32.32552
Analysis: After adjusting for each mouse’s starting LDL-C, mice that received the improved epigenetic editor (EvoETR-8) had LDL-C about 4.6 mg/dL lower than untreated mice 30 days after treatment (95% CI −7.5 to −1.8; p = 0.003). Mice that received Cas9 gene editing had LDL-C about 5.2 mg/dL lower (95% CI −8.0 to −2.4; p = 0.001). These values are roughly 29% and 32% lower than the untreated mice LDL-C levels (about 16 mg/dL), which match the values in the paper. In contrast, the ZFP-ETR editor produced no detectable change in LDL-C (95% CI −2.8 to 3.0; p = 0.95), even though it lowers PCSK9. Thus, the improved editor (EvoETR-8) matches the results of the Cas9 treatment, which matches what the news article mentions about the use of epigenetic editing being a comperable way to treat disease. This model compares each group only to untreated mice, so I cannot say whether EvoETR-8 and Cas9 differ from each other. However, with only six mice per group and cholesterol measured only up to day 30, these data cannot show how long the cholesterol drop lasts.
# linear model of LDL at day 30 and pcsk9 at day 30
m4a <- lm(ldl_30 ~ pcsk9_30, data = mice)
# add treatment group to see if lower pcsk9 still means lower LDL in each treatment
m4b <- lm(ldl_30 ~ pcsk9_30 + group, data = mice)
# coefficient table for first model
summary(m4a)$coefficients
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 10.063604 0.9241692 10.889352 2.506169e-10
## pcsk9_30 6.774719 1.4784730 4.582241 1.455179e-04
# 95% confidence interval for first model
confint(m4a)
## 2.5 % 97.5 %
## (Intercept) 8.146994 11.980214
## pcsk9_30 3.708554 9.840884
# coefficient table for second model
summary(m4b)$coefficients
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 10.5757592 4.220072 2.50606109 0.02146429
## pcsk9_30 5.5909065 4.113587 1.35913172 0.19002020
## groupCas9 -1.0913673 3.281092 -0.33262319 0.74306064
## groupZFP-ETRs 1.8150545 2.067232 0.87801209 0.39090522
## groupEvoETR-8 -0.2539152 3.396309 -0.07476211 0.94118553
# 95% confidence interval for second model
confint(m4b)
## 2.5 % 97.5 %
## (Intercept) 1.743046 19.408472
## pcsk9_30 -3.018930 14.200743
## groupCas9 -7.958772 5.776037
## groupZFP-ETRs -2.511711 6.141820
## groupEvoETR-8 -7.362472 6.854641
# plot comparing PCSK9 levels to LDL levels in mice
ggplot(mice, aes(x = pcsk9_30, y = ldl_30, colour = group)) +
geom_point(size = 3) +
geom_smooth(aes(group = 1), method = "lm", se = FALSE, colour = "black") +
labs(x = "PCSK9 at day 30 (1.0 = untreated)", y = "LDL-C at day 30 (mg/dL)",
colour = "Group", title = "LDL-C and PCSK9 levels in each mouse")
Ananlysis: Across all 24 mice, higher PCSK9 means higher LDL-C (slope 6.8 mg/dL per 1.0 unit; 95% CI 3.7 to 9.8). However, this relationship could be explained by treatment groups. After accounting for treatment groups, the slope was similar (slop 5.6 mg/dL per 1.0 unit; 95% CI −3.0 to 14.2). So the data are consistent with PCSK9 lowering leading to lower LDL-C. Treatment lowers both PCSK9 and LDL-C, so the two levels are confounded with which treatment a mouse got.
# average methylation across the 45 CpG sites for each mouse (n = 3 per group) and then compare groups.
meth_mouse <- meth %>%
group_by(group, id) %>%
summarise(mean_meth = mean(meth))
# linear model for methylation
m5 <- lm(mean_meth ~ group, data = meth_mouse)
summary(m5)
##
## Call:
## lm(formula = mean_meth ~ group, data = meth_mouse)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.5394 -0.2749 0.0494 1.1338 3.2806
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.434 1.523 1.599 0.16102
## groupZFP-ETRs 11.553 2.153 5.365 0.00172 **
## groupEvoETR-8 23.553 2.153 10.937 3.47e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.637 on 6 degrees of freedom
## Multiple R-squared: 0.9522, Adjusted R-squared: 0.9363
## F-statistic: 59.82 on 2 and 6 DF, p-value: 0.0001089
# 95% confidence interval
confint(m5)
## 2.5 % 97.5 %
## (Intercept) -1.291734 6.160235
## groupZFP-ETRs 6.284083 16.822759
## groupEvoETR-8 18.283688 28.822364
# mean methylation (%) of each group and average methylation of the mice in that group
mm <- meth_mouse %>% group_by(group) %>% summarise(m = mean(mean_meth))
get_m <- function(g) mm$m[mm$group == g]
mm
## # A tibble: 3 × 2
## group m
## <fct> <dbl>
## 1 Vehicle 2.43
## 2 ZFP-ETRs 14.0
## 3 EvoETR-8 26.0
Vehicle mice average about 2.43% methylation across this region, ZFP-ETR mice 14% and EvoETR-8 mice 26%. the EvoETR-8 group has about 1.9 times as much methylation as the ZFP-ETR group, close to the number reported in the paper. Since three are 3 mice per group the confidence intervals are wide.
| Question | Model | Answer |
|---|---|---|
| 1. Does one dose lower PCSK9? | pcsk9 to group (day 330) | Yes, ZFP-ETRs about 38% lower than vehicle |
| 2. Does the effect last? | pcsk9 to day (ZFP-ETR mice, day 30 onward) | Partly, PCSK9 stays well below normal but no clear steady drift (slope CI includes 0) |
| 3. Does cholesterol fall? | LDL at day 30 to group and LDL baseline | EvoETR-8 -29%, Cas9 -32%; ZFP-ETRs no clear change |
| Mechanism | mean methylation per group | Yes, more methylation in treated mice, most in EvoETR-8 |
Connection to NY Times article: In these mice, one injection of an epigenetic editor lowered PCSK9 for almost a year and changed methylation levels at the Pcsk9 gene. The improved editor (EvoETR-8) also lowered cholesterol about as much as the Cas9 approach. There were two caveats: the silencing was partial rather than complete, and cholesterol was only followed in this study for 30 days.
Cappelluti, M. A., Mollica Poeta, V., Valsoni, S., Quarato, P., Merlin, S., Merelli, I., & Lombardo, A. (2024). Durable and efficient gene silencing in vivo by hit-and-run epigenome editing. Nature, 627(8003), 416–423. https://doi.org/10.1038/s41586-024-07087-8 * data from this article was used for analysis Ivashchenko, V., de Groot, M., Derks, R., den Ouden, A., Khazeeva, G., van den Heuvel, S., Timmermans, R., Corominas Galbany, J., Pfundt, R., Hofste, T., Yntema, H., Vissers, L., Hoischen, A., Hampstead, J., & Gilissen, C. (2026). Genome-wide methylation detection and episignature analysis using PacBio long-read sequencing. Genome medicine, 18(1), 11. https://doi.org/10.1186/s13073-025-01506-9 Nuñez, J. K., Chen, J., Pommier, G. C., Cogan, J. Z., Replogle, J. M., Adriaens, C., Ramadoss, G. N., Shi, Q., Hung, K. L., Samelson, A. J., Pogson, A. N., Kim, J. Y. S., Chung, A., Leonetti, M. D., Chang, H. Y., Kampmann, M., Bernstein, B. E., Hovestadt, V., Gilbert, L. A., & Weissman, J. S. (2021). Genome-wide programmable transcriptional memory by CRISPR-based epigenome editing. Cell, 184(9), 2503–2519.e17. https://doi.org/10.1016/j.cell.2021.03.025 Tay, Y. L., Thomson, S. B., Hnatova, S., Ng, S., Teo, S. R., McCallum, R., Sim, B., Tarantini, L., Tai, F. L., Bollati, V., Loh, M., Hayden, M. R., Leavitt, B. R., & Pouladi, M. A. (2026). Silencing of human HTT by targeted CRISPR/dCas9-mediated epigenetic editing. Journal of Huntington’s disease, 15(3), 399–407. https://doi.org/10.1177/18796397251415368 Tremblay, F., Xiong, Q., Shah, S. S., Ko, C. W., Kelly, K., Morrison, M. S., Giancarlo, C., Ramirez, R. N., Hildebrand, E. M., Voytek, S. B., El Sebae, G. K., Wright, S. H., Lofgren, L., Clarkson, S., Waters, C., Linder, S. J., Liu, S., Eom, T., Parikh, S., Weber, Y., … Jaffe, A. B. (2025). A potent epigenetic editor targeting human PCSK9 for durable reduction of low-density lipoprotein cholesterol levels. Nature medicine, 31(4), 1329–1338. https://doi.org/10.1038/s41591-025-03508-x