96-well Plate Assays
Multi-Well Plate Assays
The multi-well plate is utilized across many scientific fields. Not only does it allow for waste reduction (less reagents are used in a smaller well), but it is also compatible with a variety of screening assays. If you are running many such assays, you might even utilize a pipetting robot of some kind or other method to increase automation. In the spirit of increasing automation and reproducibility, we will use R to perform our analysis. In this example we are trying to determine the concentration of an unknown solute. To do this we generate a standard curve from which we can calculate the concentration of our unknown.
This data comes from a BCA assay which an assay designed to measure protein concentrations. If you are unfamiliar with the assay the general principle is that reagents are added to the sample such that a colorimetric reaction takes place. The absorbance of this reaction used to determine the protein concentration. To calculate the concentration of your unknowns (samples), you generate a standard curve of known protein concentrations at the same time. The relationship between absorbance and protein concentration should be linear, thus a simple line of best fit can be drawn to calculate the concentration your unknowns. If you are interested you can get more information about it here: http://tools.thermofisher.com/content/sfs/manuals/MAN0011430_Pierce_BCA_Protein_Asy_UG.pdf
Libraries Used for this Project
library(tidyverse)
library(reshape2)
library(Cairo)
library(viridis)
library(platetools)
library(readxl)
Data Structure
The data will come as an excel book with 3 individual sheets. The first sheet contains the raw data, the second contains the well identifiers, and the last sheet has the values of the standard curve.
Define Assay Specific Variables First
For the purposes of good record keeping, there are several variables that need to be addressed before we proceed. First we need to name the file that we are looking at. This is critical and cannot be skipped. Then we add the units as the variable std_units. This is optional, but good practice to document. In this case we are looking at ug/mL. The assay_id and abs_nm are also somewhat optional, but similary should be included as a matter of good documentation.
Finally we create an output directory where we will save our analysis and graphs.
file = "96_well_example.xls" # What file in your directory are you analyzing?
std_units = "ug/mL" # For Example: cell Number, ug, mg
assay_id = "BCA_Assay" # Any descriptive name is fine
abs_nm = "510" # What wavelength did you read at?
dir.create("96-well_output") # save output files
Now Perform the Analysis and Generate Outputs
First we need to import our data and do some minor manipulations. First we need to make sure that the columns are named appropriately and then we take our data from a standard wide plate layout to a long format that we can use in R.
The first sheet contains the measurements
readings = read_excel(file, sheet = 1)
head(readings)
## # A tibble: 6 x 13
## ..1 `1` `2` `3` `4` `5` `6` `7` `8` `9` `10` `11`
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 A 2.45 1.51 0.716 0.395 0.275 0.214 0.223 0.216 0.237 0.037 0.037
## 2 B 2.57 1.45 0.665 0.366 0.248 0.185 0.163 0.182 0.164 0.039 0.039
## 3 C 2.45 1.42 0.666 0.33 0.227 0.159 0.153 0.163 0.203 0.032 0.032
## 4 D 0.034 0.032 0.034 0.029 0.03 0.03 0.03 0.035 0.029 0.032 0.032
## 5 E 0.036 1.22 0.741 0.033 1.39 0.87 0.032 0.038 0.035 0.044 0.044
## 6 F 0.033 1.20 0.747 0.03 1.38 0.885 0.032 0.031 0.028 0.036 0.036
## # ... with 1 more variable: `12` <dbl>
plate_cols = c("alpha", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12")
colnames(readings) = plate_cols
# melt the data to take it from wide to long
readings = melt(data = readings, id.vars = "alpha", variable.name = "col_num" )
readings = unite(readings, "well", c("alpha", "col_num"), sep = "")
head(readings)
## well value
## 1 A01 2.451
## 2 B01 2.573
## 3 C01 2.449
## 4 D01 0.034
## 5 E01 0.036
## 6 F01 0.033
The second sheet contains the plate layout
template = read_excel(file, sheet = 2)
colnames(template) = plate_cols
# melt the data to take it from wide to long
template = melt(data = template, id.vars = "alpha", variable.name = "col_num", na.rm = TRUE) # na.rm is very important for downstream steps
template = unite(template, "well", c("alpha", "col_num"), sep = "")
# put the key and readings together
# inner join is a filtering join -only matches in well present in both sets are kept
plate = inner_join(template, readings, by = "well")
names(plate) <- c("well", "sample", "abs")
The last sheet contains the standard curve values
Whatever your curve is you should define it here. Just make sure that whatever you name your standards on here matches what you named them on the plate layout.
standards = read_excel(file, sheet = 3)
head(standards)
## # A tibble: 6 x 2
## sample std_con
## <chr> <dbl>
## 1 Standard_1 2000
## 2 Standard_2 1000
## 3 Standard_3 500
## 4 Standard_4 250
## 5 Standard_5 125
## 6 Standard_6 62.5
Calculate the mean absorbance of the samples
Note if you have named your standards with an identifier other than “Standard_X” you will need to adjust the code to make sure the filter is catching them. For simplicity, you should just try to keep all of your naming the same that way you don’t have to change things.
mean_stats = plate %>%
group_by(sample) %>%
summarise(mean_abs = mean(abs), sd_abs = sd(abs))
head(mean_stats)
## # A tibble: 6 x 3
## sample mean_abs sd_abs
## <chr> <dbl> <dbl>
## 1 Sample A 1.22 0.0226
## 2 Sample B 0.748 0.00808
## 3 Sample C 0.0307 0.00208
## 4 Sample D 1.40 0.0193
## 5 Standard_1 2.49 0.0710
## 6 Standard_2 1.46 0.0501
curve = mean_stats %>%
dplyr::filter(str_detect(sample, "^Standard")) # include all the standard data
curve = left_join(curve, standards, by = "sample")
print(curve)
## # A tibble: 9 x 4
## sample mean_abs sd_abs std_con
## <chr> <dbl> <dbl> <dbl>
## 1 Standard_1 2.49 0.0710 2000
## 2 Standard_2 1.46 0.0501 1000
## 3 Standard_3 0.682 0.0292 500
## 4 Standard_4 0.364 0.0326 250
## 5 Standard_5 0.25 0.0241 125
## 6 Standard_6 0.186 0.0275 62.5
## 7 Standard_7 0.180 0.0379 31.2
## 8 Standard_8 0.187 0.0269 15.6
## 9 Standard_9 0.201 0.0365 0
Perform Linear Regression Analysis
Since we know that the relationship between the absorbance and protein concentration is linear we are able to use a linear regression model. This is done with lm() where the arguments are as follows: lm(formula = y ~ x, data = df).
Once the linear model has been fit, we extract some variables that will be used for calculating the concentration of our unknowns. If you remember your basic math, the equation for a line is y=mx+b. We extract our slope (m) as well as our x-intercept (b) and store them as variables.
We also extract our R squared value which gives us an idea of how well our data fits a linear model. The equation variable will be used to print the equation of our line on a graph we will generate later.
fit = lm(formula = std_con ~ mean_abs, data = curve)
summary = summary(fit)
m = coefficients(fit)[["mean_abs"]]
b = coefficients(fit)[["(Intercept)"]]
r_sqr = summary[["r.squared"]]
equation = paste0("y=", round(m, 2), "x", "+", round(b, 2))
Find the Unknown Concentrations
This is pretty standard and we are really just solving for x. To do this, we filter out all the standards this time and then use the mutate function to create a new variable called sample_con that is the calculated protein concentration.
samples = mean_stats %>%
filter(!str_detect(sample, "^Standard")) %>% # remove all the standard data
dplyr::mutate(sample_con = (mean_abs * m + b ))
# Also save your data for later
# output the data table
write.xlsx(samples, file = paste0(Sys.Date(),"_", assay_id, "_", "calculated_96_well_assay_output", ".xlsx"), sheetName = "Sheet1",
col.names = TRUE, row.names = TRUE)
Graph your Curve Along with the Data Points
p = ggplot(curve, aes(x = mean_abs, y= std_con)) +
geom_point() +
geom_smooth(method = lm)+
geom_point(data = samples, aes(x = mean_abs , y = sample_con, color = sample)) +
geom_text(data = samples, mapping = aes(x = mean_abs , y = sample_con, label = round(sample_con, 2), color = sample, vjust=-2, hjust=0.5 )) +
theme_bw() +
labs(title = "96-well plate data",
subtitle = paste0(assay_id, " ", Sys.Date()),
tag = "Generated in R",
y = paste("Concentration", std_units ,sep = " "),
x = paste0("Mean Absorbance ", "(", abs_nm, ")" ),
caption = paste0("y=", round(m, 2), "x", "+", round(b, 2), " ", "R Squared: ", round(r_sqr, 4)) # print your equation and round the values to 2 digits
)
p
Use the Plate Tools Package to Visualize the Plate Data
# Use plate map to generate extra columns
readings_map = plate_map(data = readings$value,
well = readings$well)
readings_map = dplyr::right_join(plate, readings_map, by = "well")
# Genreate a ggplot variable for plotting
plate_plot = raw_map(data = readings_map$abs,
well = readings_map$well,
plate = 96)
plate_plot +
scale_fill_viridis() +
theme_bw() +
geom_text(mapping = aes(label = readings_map$sample), vjust=-4, size=1.5) +
labs(title = "96-well plate data (Mapped Samples)",
subtitle = paste0(assay_id, " ", Sys.Date()),
caption = paste0("Reading Wavelength: ", abs_nm, "nm")
)
Finally we will Save out High Resolution PDFs of our Graphs
#Save the plate map
setwd("96-well_output")
cairo_pdf(file= paste0(Sys.Date(),"_", assay_id, "_", "Heatplate_mapped", ".pdf"),
width=8.46,
height=4.7,
family = "Arial",
fallback_resolution = 300)
plate_plot +
scale_fill_viridis() +
theme_bw() +
geom_text(mapping = aes(label = readings_map$sample), vjust=-4, size=1.5) +
labs(title = "96-well plate data (Mapped Samples)",
subtitle = paste0(assay_id, " ", Sys.Date()),
caption = paste0("Reading Wavelength: ", abs_nm, "nm")
)
dev.off()
## pdf
## 2
#Save the linear regression
cairo_pdf(file= paste0(Sys.Date(),"_", assay_id, ".pdf"),
width=6,
height=5,
family = "Arial",
fallback_resolution = 300)
p
dev.off()
## pdf
## 2