Bonus: Here you’ll find leaf transcriptomic protocols for this experiment
Leaves from four biological replicates of plants (i.e. independent plants) were used. Each biological replicate corresponds to the three last leaves formed before S deficiency imposition (i.e. vegetative leaves), which were collected 25 Days after Flowering (DAF). The leaves were immediately frozen in liquid nitrogen, then stored at -80°C. RNA was extracted from 100 mg of frozen powder using the RNeasy Plant Mini Kit according to manufacturer’s protocol (Qiagen, Courtaboeuf, France). RNA quality was checked on agarose gel 1.5%, then using the Agilent 2100 Bioanalyzer. The subsequent steps were performed at IPS2 (Institute of Plant Sciences Paris-Saclay). The Pea NimbleGen-microarrays were developed to profile expression of 40795 sequences: 40454 mRNA originating from the PsCameor_Uni_Lowcopy set (Alves‐Carvalho et al. 2015), 323 putative precursors of miRNA predicted in the “Test assembly multiple k-mer” contig set (Alves‐Carvalho et al. 2015), and 18 controls. The Ambion MessageAmpTM II aRNA Amplification Kit was used to amplify sufficient amounts of copy RNA extracted from leaves of the four biological replicates. The Double stranded cDNA synthesis was realized using T7-oligo-dT and the antisense RNA (aRNA) was created by in vitro transcription according to manufacturer’s protocol (Life technologies SAS, Saint Aubin, France). The labeling with Cy3 or Cy5 was performed by reverse transcription of aRNA using labeled nucleotides (Cy3-dUTP or Cy5-dUTP, Perkin-Elmer-NEN Life Science Products). For each sample, the following co-hybridizations were performed: (1) W78* mutant vs. wildtype 1 under +S; (2) E568K mutant vs. wild-type 2 under +S; (3) W78* mutant vs. wild-type 1 under –S; (4) E568K mutant vs. wild-type 2 under –S; (5) wild-types under –S vs. wild-type under +S (for this comparison, the biological replicates under each condition were made of two wildtype 1 plants and two wild-type 2 plants). For each comparison, a dye swap was realized. These probes were spotted in triplicates on the GENOPEA array. The hybridization of labeled samples on the slides, scanning and data normalization were performed as previously described (Lurin et al. 2004).
Differential analysis was based on the log2 ratios averaged on the dye-swap: the technical replicates were averaged to get one log2 ratio per biological replicate and these values were used to perform a paired t-test. The raw P-values were adjusted by the Benjamini Hochberg method, which controls the family wise error rate, and probes were considered as differentially expressed when the Benjamini Hochberg Pvalue was <0.05. Transcriptome datasets were deposited in the NCBI Gene Expression Omnibus database with the accession numbers GSE121967. All pea sequences with "PsCam" accession numbers could be retrieved from the pea RNAseq gene atlas at http://bios.dijon. inra.fr/ (PsUniLowCopy data set).
Code
# install.packages(here::here("data/microarray/org.Psativum1c.eg.db"), repos = NULL, type = "source")#pkglibrary(readxl)library(affy) #BiocManager::install("affy") # Affymetrix pre-processinglibrary(limma) # two-color pre-processing; differentiallibrary(tidyverse)library(dplyr)library(patchwork)library(progress)library(ggrepel)library(plotly)library(htmlwidgets)library("FactoMineR")library("factoextra")library("corrplot")library(missMDA)library(ComplexHeatmap)library(knitr)library(kableExtra)library(ggtext)library(ggnewscale)library(org.Psativum1c.eg.db) # if not working install itlibrary(clusterProfiler)library(readxl)library(GO.db)library(AnnotationDbi) # BiocManager::install("AnnotationDbi")library(ggnewscale) # to have two scale_fill# srcsource(here::here("src/function/stat_function/stat_analysis_main.R")) # for make plot source(here::here("src/function/fig_export.R")) # This function saves a given plot (plot_x) as both a PDF and a high-resolution PNG file at specified dimensions.source(here::here("src/function/upsetplot_condition_merge_sign.R")) # function "create_presence_matrix" and "upsetplot_condition_merge_sign"source(here::here("src/function/microarray/GO_on_different_group.R")) # function that find GO terme# cosmeticssulfate_pallet=read_excel(here::here("data/color_palette.xlsm")) %>%filter(set =="sulfure_condition") %>% dplyr::select(color, treatment) %>%pull(color) %>%setNames(read_excel(here::here("data/color_palette.xlsm")) %>%filter(set =="sulfure_condition") %>%pull(treatment) )my_color_palette <-read_excel(here::here("data/color_palette.xlsm"))
13.2 Data importation
Code
# for the list of all comparisoncomparison=read_excel(here::here("data/microarray/Resultats_4plex-POIS-2014_01_230415_modKG.xlsx"), sheet ="Complete", range ="L10:EF11", col_names =FALSE) %>%t() %>%as.data.frame() %>%rownames_to_column("to_del") %>% dplyr::select(-"to_del") %>%mutate(comparison =paste0(V1, ".", V2)) %>%distinct(comparison) %>% tidyr::separate(comparison, into =c("Green", "Red"),sep ="\\.")write_csv(x = comparison, file = here::here("data/microarray/output/comparison_microarray.csv"))
Code
# show resultsread_csv(here::here("data/microarray/output/comparison_microarray.csv")) %>%knitr::kable(., caption ="List of all comparison")
List of all comparison
Green
Red
5_1Mut-
1_1WT-
6_2Mut-
2_2WT-
7_3Mut-
3_3WT-
8_4Mut-
4_4WT-
13_5Mut-
9_5WT-
14_6Mut-
10_6WT-
15_7Mut-
11_7WT-
16_8Mut-
12_8WT-
21_1Mut+
17_1WT+
22_2Mut+
18_2WT+
23_3Mut+
19_3WT+
24_4Mut+
20_4WT+
29_5Mut+
25_5WT+
30_6Mut+
26_6WT+
31_7Mut+
27_7WT+
32_8Mut+
28_8WT+
1_1WT-
17_1WT+
4_4WT-
19_3WT+
9_5WT-
25_5WT+
12_8WT-
28_8WT+
The data is very poorly organized in the data table, so it will take some time to import. There are two types of data. Columns that come directly from the chips and correspond to the log2 of the original values but normalized chip by chip between Red and Green. And average data and stats corresponding to one of the paired student tests, then corrected between the four biological repeats. This data has been previously analized here.)
Import raw data (green and red)
Code
# Function that import all data (take a lot of time)source (here::here("src/function/microarray/import_raw_data_microarray.R"))path_i = here::here("data/microarray/Resultats_4plex-POIS-2014_01_230415_modKG.xlsx")path_info_range_i = here::here("data/microarray/info_range_Resultats_4plex-POIS-2014_01_230415_modKG.xlsx")begin_end_line <-c("12", "41122")sheet_i ="Complete"execut =import_raw_data_microarray(path_i, path_info_range_i, sheet_i, begin_end_line)write_csv(execut[["global_df"]], here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.csv"))write_csv(execut[["df_info_sample_clean_compile"]], here::here("data/microarray/output/list_microarray_microarray_leaf_PeaSulf.csv"))
On microarray chips, it’s quite common to find probes that do not belong to the species under study. These probes are typically placed there for various reasons, such as serving as exogenous controls (often called alien probes, spike-ins, or external controls) or to check hybridization specificity. Here is from human or bacteria. In this step i will import it.
# import raw data red green# data1 = read_csv(here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.csv"), show_col_types = FALSE) %>% # as.data.frame() %>% # dplyr::select(c("id_probe", "Mut_SD_5.1_Rep1_Red_1", "WT_SD_1.1_Rep1_Green_1"))# # p_non_norm = data1 %>% # pivot_longer(-"id_probe") %>% # ggplot(aes(x = value, col = name)) +# geom_density() +# labs(# title = "Density of values",# x = "Values",# y = "Density"# ) +# theme_minimal()# # data_longer_test = data1 %>% column_to_rownames("id_probe")# # norm_data <- normalizeBetweenArrays(data_longer_test, method = "cyclicloess")# # p_norm = norm_data %>% # as.data.frame() %>% # rownames_to_column("id_probe") %>% # pivot_longer(-"id_probe") %>% # ggplot(aes(x = value, col = name)) +# geom_density() +# labs(# title = "Density of normalized values",# x = "Values",# y = "Density"# ) +# theme_minimal()# p_non_norm / p_norm
Creation of the RGlist file (with absolute value (befor log2))
Code
# Import raw data red green df_raw_RG_log2 =read_csv(here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.csv"), show_col_types =FALSE) %>%as.data.frame()# Creation of the RG list for background stemps ##### Converted into input data (exp of value with log2)df_raw_RG<- df_raw_RG_log2cols_to_transform <-grep("Red|Green", names(df_raw_RG))df_raw_RG[cols_to_transform] <-lapply(df_raw_RG[cols_to_transform], function(col) 2^col)# Verification before creation of the RGListred_cols <-grep("Red_\\d+$", colnames(df_raw_RG), value =TRUE)green_cols <-grep("Green_\\d+$", colnames(df_raw_RG), value =TRUE)extract_index <-function(x, color =c("Red","Green")) { pattern <-paste0(".*", color, "_(\\d+)$") sub(pattern, "\\1", x)}numbers_red <-extract_index(red_cols, "Red")numbers_green <-extract_index(green_cols, "Green")common_indices <-intersect(numbers_red, numbers_green)cat("Same number find :", common_indices, "\n")# Parameter of the arrayn_spots <-nrow(df_raw_RG)n_arrays <-length(common_indices) # 20 = 4 rep * 5 comparisonR_matrix <-matrix(NA, nrow = n_spots, ncol = n_arrays) %>%as.data.frame()G_matrix <-matrix(NA, nrow = n_spots, ncol = n_arrays) %>%as.data.frame()k <-1for (i in common_indices) {# Trouver la/les colonnes Red_... qui finissent par Red_i# (en théorie, une seule par index i, mais grep renvoie un vecteur - on prend le premier) col_red <-grep(paste0("Red_", i, "$"), red_cols, value =TRUE) col_green <-grep(paste0("Green_", i, "$"), green_cols, value =TRUE)# Remplir la matrice R_matrix[, k] <- df_raw_RG[[col_red]] G_matrix[, k] <- df_raw_RG[[col_green]]# Nommer la colonne pour s’y retrouver (ex : "i" ou le nom exact "Sample1")# Ici on va prendre "Red_xxxx" comme titre, ou "i" tout simplementcolnames(R_matrix)[k] <- col_redcolnames(G_matrix)[k] <- col_green k <- k +1}# Creation of the RG list end ####RG <-new("RGList")RG$R <- R_matrixRG$G <- G_matrixRG$genes <-data.frame(ID = df_raw_RG$id_probe)MA_raw <-normalizeWithinArrays(RG, method ="none") # without normalisation# export RGsave(RG,MA_raw, file =here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.RData"))
13.3 Standardization of the data cross-chip data
Bonus: If i apply double normalization ((loess and quartil) not necessary for this experiment))
In a two-channel microarray, we often apply within-array normalization (such as LOESS) to correct for individual slide biases (systematic variations linked to position on the chip, channel, etc.). Next, a so-called between-array normalization (such as quantile normalization) can be applied to make distributions between replicates comparable. Normalization functions are available in R/Bioconductor (limma package, etc.).
# Load dataload(here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.RData"))# Raw data visualization ####source (here::here("src/function/microarray/plot_rg_density.R")) # Load function for see density of the differentent normalizationp_raw <-plot_rg_density(RG)+labs(title ="plotDensities without normalization") # execut function# Remove or adjust background values (noise) to obtain intensities closer to the true spot intensity ##### Helps model and remove technical noise at lower intensities.# The normexp method is commonly recommended because it avoids negative or zero intensities by adding an offset. In input is absolute valueRG_background <-backgroundCorrect(RG, method ="normexp", offset =20) # 20 test multiple value. Perfect for delet noise rownames(RG_background$R) <- RG_background[["genes"]][["ID"]]rownames(RG_background$G) <- RG_background[["genes"]][["ID"]]# visualisation p_background <-plot_rg_density(RG_background)+labs(title ="plotDensities without normalization but with background correction")log2(min(RG_background$G)) # need to be positiveMA_raw <-normalizeWithinArrays(RG, method ="none") # without normalisationplotMA(MA_raw, array =1) # interesting to se this plotMA_background <-normalizeWithinArrays(RG_background, method ="none") # with normalisationplotMA(MA_background, array =1)# Normalization loess ####MA.p <-normalizeWithinArrays(RG_background, method ="loess")p_loess <-plot_rg_density(MA.p, normalized = T)+labs(title ="plotDensities with normalization between the two chanel", subtitle ="Loess normalization")# Normalisation Between Arrays (Aquantile)MA.pAq <-normalizeBetweenArrays(MA.p, method="Aquantile")p_quantile <-plot_rg_density(MA.pAq, normalized = T)+labs(title ="plotDensities with normalization between the two chanel and between sample", subtitle ="Quantile normalization")# Filter on low value ? ####RG.MA(MA.pAq)$R keep <-rowMeans(RG_background$R) >1119.909&rowMeans(RG_background$G) >1119.909keep <-rowMeans(RG_background$R) >1119.909&rowMeans(RG_background$G) >1119.909RG_filtered <- RG[keep, ]# Calculate how many genes were removedn_removed <- n_original - n_filtered# Calculate the percentage of genes removedpercent_removed <- (n_removed / n_original) *100# Display resultscat("Number of genes removed:", n_removed, "\n")cat("Percentage of genes removed:", round(percent_removed, 2), "%\n")p_filtered <-plot_rg_density(RG_filtered)+labs(title ="plotDensities without normalization but with filtration on low intensity")fig_export(here::here("report/microarray/plot/normalization/without_normalization_with_low_intensity"), p_filtered, height_i =5, width_i =12, res_i =300)# Export ####fig_export(here::here("report/microarray/plot/normalization/without_normalization"), p_raw, height_i =5, width_i =12, res_i =300)fig_export(here::here("report/microarray/plot/normalization/without_normalization_with_background_substraction"), p_background, height_i =5, width_i =12, res_i =300)fig_export(here::here("report/microarray/plot/normalization/with_loess_normalization"), p_loess, height_i =5, width_i =12, res_i =300)fig_export(here::here("report/microarray/plot/normalization/with_quantile_normalization"), p_quantile, height_i =5, width_i =12, res_i =300)# all_fig <- p_raw/p_background/p_filtered/p_loess/p_quantileall_fig <- p_raw/p_background/p_loess/p_quantilefig_export(here::here("report/microarray/plot/normalization/all_step_normalization"), all_fig, height_i =18, width_i =12, res_i =300)# RG_filteredsave(RG,MA_raw, MA.pAq, file =here::here("data/microarray/output/normalized_data_microarray_leaf_PeaSulf.RData"))# Delet of low intensity ####n_original <-nrow(RG) # Total genes before filteringn_filtered <-nrow(RG_filtered) # Total genes after filteringred_row_means <-rowMeans(RG_background$R)# 2. Combine with Gene IDs into a data framedf_red_means <-data.frame(GeneID = RG_background$genes, # Adjust column name if neededMeanRed = red_row_means)# 3. Subset for the specific gene of interestdf_red_means[df_red_means$ID =="PsCam042688", ]df_red_means[df_red_means$ID =="PsCam036750", ]row_means_red <-rowMeans(RG$R)# 2. Convert to a data framedf_row_means <-data.frame(mean_intensity = row_means_red)# 3. Plot with ggplot2ggplot(df_row_means, aes(x =log2(mean_intensity))) +geom_histogram(bins =50, color ="black", fill ="skyblue") +theme_minimal() +labs(title ="Distribution of Row Means (Red Channel)",x ="Mean Intensity (Red Channel)",y ="Count")ggplot(df_row_means, aes(x =log2(mean_intensity))) +geom_histogram(bins =50, color ="black", fill ="skyblue") +scale_x_log10() +theme_minimal() +labs(title ="Distribution of Row Means (Red Channel) [Log Scale]",x ="Log2 (Mean Intensity) (Red Channel) [log10]",y ="Count" )# RG is your RGList with RG$R and RG$G (non-log)# Suppose you want to keep probes with average raw intensity above 100 in *both* channels# Combination of all plots
Expression of PSULT4 with normalized data
Code
load(file =here::here("data/microarray/output/normalized_data_microarray_leaf_PeaSulf.RData"))list_microarray <-read_csv(here::here("data/microarray/output/list_microarray_microarray_leaf_PeaSulf.csv"), show_col_types =FALSE) ID <- MA.pAq$genes[[1]]A <- MA.pAq$A # Average log intensityM <- MA.pAq$M # Log ratioR <-2^(A + (M /2)) # Calculate Red intensitiesG <-2^(A - (M /2)) # Calculate Green intensitiesR <-as.data.frame(R)G <-as.data.frame(G)rownames(R) <- IDrownames(G) <- ID# Red had good name of column but not green data. i will change thatcolnames(G) <-colnames(RG$G)RG_norm_filtr <-cbind(R, G)RG_norm_filtr_log2<-log2(RG_norm_filtr)RG_norm_filtr_log2_h <- RG_norm_filtr_log2 %>%rownames_to_column("ID") %>%pivot_longer(-ID, values_to ="value", names_to ="sample_id") %>%left_join(.,list_microarray, by ="sample_id")#export datawrite_csv(RG_norm_filtr_log2_h, here::here("data/microarray/output/normalized_data_microarray_leaf_PeaSulf.csv"))
Comparison of the two chanel
Code
RG_norm_filtr_log2_h =read_csv(here::here("data/microarray/output/normalized_data_microarray_leaf_PeaSulf.csv"), show_col_types =FALSE)p_sult4 <- RG_norm_filtr_log2_h %>%mutate(sulfur_condition = forcats::fct_relevel(sulfur_condition, "SS", "SD"),genotype = forcats::fct_relevel(genotype, "WT1", "W78*", "WT2", "E568K")) %>%filter(ID =="PsCam042688") %>%ggplot(aes(x = genotype, y = value, col= color, fill = sulfur_condition, group =interaction(genotype, sulfur_condition, color))) +geom_boxplot(outlier.shape =NA) +geom_jitter(position =position_jitterdodge(jitter.width =0.6)) +scale_fill_manual(values =c("SS"="#FFD74C", "SD"="gray"), # Adjust based on your `color` levelsname ="Sulfure Condition"# Custom title for color legend )+scale_color_manual(values =c("Red"="red", "Green"="forestgreen"), # Adjust based on your `color` levelsname ="Channel"# Custom title for color legend )+theme_minimal()+theme(# Hide panel borders and remove grid linespanel.border =element_blank(),panel.grid.major =element_blank(),panel.grid.minor =element_blank(),# Change axis lineaxis.line =element_line(colour ="black") )+labs(x ="Genotype", y ="Microarray gene expression of SULTR4")fig_export(here::here("report/microarray/plot/exression_of_genes/SULTR4"), p_sult4, height_i =4, width_i =6, res_i =300)
13.4.1 Using limma contrasts (method recommended by Marie Laure Martin-Magniette)
In multi-factor or multi-group microarray analyses, we often want to compare specific conditions such as different genotypes, treatments, or their combinations.
Why use contrasts? A linear model created with lmFit describes the contribution of each factor in the experiment, but it doesn’t automatically test the specific comparisons we are interested in. A contrast is a linear combination of the model’s coefficients that defines a particular comparison, for example, comparing genotype A to genotype B, or comparing interaction effects. Using a contrast matrix allows us to explicitly define and test these comparisons, ensuring that limma computes the appropriate log-fold changes and p-values.
How it works: First, we create a design matrix using model.matrix(…), which captures the main effects and possibly the interactions between experimental factors. Then we define a contrast matrix using a custom function (like Contrasts) or limma’s built-in tools. Each row of the contrast matrix corresponds to a specific biological question or comparison.
After this, the lmFit(…) function estimates gene-wise model coefficients. We apply our contrasts using contrasts.fit(…), which re-expresses the model in terms of the contrasts of interest. The eBayes(…) function then moderates the variance estimates and calculates statistical significance (p-values and adjusted p-values).
In the rest of the code, we use p.adjust(…) to control for multiple testing (FDR), identify significantly up- or down-regulated genes based on a fold-change threshold, and generate summary outputs such as barplots and volcano plots. This provides a clear overview of differential expression across the comparisons that matter most in the study.
In summary, contrasts are essential in differential analysis using limma, as they allow us to tailor the comparisons to the specific hypotheses of the experiment, especially when multiple factors and interactions are involved.
Code
#parameterlfc_lim_i =0pval_i =0.05load(file = here::here("data/microarray/output/targets_design.RData"))source(here::here("src/function/microarray/Contrasts.R"))# creation of a complete dataframeRG_aquantil <- limma::RG.MA(MA_aquantil)colnames(RG_aquantil$G) <-colnames(RG$G)df_RG_aquantil_log2 <-cbind(RG_aquantil$R, RG_aquantil$G, RG_aquantil$genes) %>%column_to_rownames("ID") %>%mutate(across(everything(), log2))save(df_RG_aquantil_log2, file = here::here("data/microarray/output/df_RG_aquantil_log2.RData"))Target <- matrix_info %>% dplyr::select(sulfur_condition, genotype, num_combination)Target=as.data.frame(Target)Target$sulfur_condition =as.factor(Target$sulfur_condition)Target$genotype =as.factor(Target$genotype)model <-model.matrix(as.formula(paste("~", paste(colnames(Target)[1:2], collapse =" + "), paste(colnames(Target[,1:2]),collapse =":"), sep =" + ")), data=Target)contrast.matrix =Contrasts(model,Target,FALSE,TRUE)mycontrast1 <-c(0, 0, +1, -1, +1, +1, -1, +1)mycontrast2 <-c(0, 0, +1, -1, +1, 0, 0, 0)contrast.matrix # votre matrice après Contrasts(...)# On l'augmente en ajoutant nos 2 lignes :contrast.matrix <-rbind( contrast.matrix, mycontrast1, mycontrast2)# On renomme les deux nouvelles lignes :n <-nrow(contrast.matrix)rownames(contrast.matrix)[(n-1):n] <-c("[SS_W78*-SS_WT1]-[SS_E568K-SS_WT2]","[SD_W78*-SD_WT1]-[SD_E568K-SD_WT2]")contrast.matrixfit <-lmFit(df_RG_aquantil_log2, model)#contrast.matrix <- makeContrasts(SpecialDiet-Control, SpecialDietDrug1-SpecialDiet, SpecialDietDrug1-Control, levels=design) fit2 <-contrasts.fit(fit, t(contrast.matrix))fit2 <-eBayes(fit2)# topTable(fit2, coef=1, adjust="fdr", sort.by="B", number=10) # creation of a plot to count the number of difference between eatch contrastep_adj <-apply(fit2$p.value, 2, p.adjust, method ="BH")is_significant <- p_adj <0.05# Matrice TRUE/FALSE pour FDR < 0.05is_upregulated <- is_significant & (fit2$coefficients > lfc_lim_i) # logFC > 0is_downregulated <- is_significant & (fit2$coefficients <-lfc_lim_i) # logFC < 0vector_with_interest <-c("[SD-SS]","[WT1-WT2]","[E568K-WT2]", "[W78*-WT1]", "[WT1_SD-WT1_SS]","[W78*_SD-W78*_SS]", "[WT2_SD-WT2_SS]", "[E568K_SD-E568K_SS]", "[SS_W78*-SS_WT1]", "[SS_E568K-SS_WT2]", "[SD_W78*-SD_WT1]", "[SD_E568K-SD_WT2]", "[SD_W78*-SD_WT1]-[SS_W78*-SS_WT1]", "[SD_E568K-SD_WT2]-[SS_E568K-SS_WT2]", "[SS_W78*-SS_WT1]-[SS_E568K-SS_WT2]", "[SD_W78*-SD_WT1]-[SD_E568K-SD_WT2]" )summary_DEG <-tibble(Contrast =colnames(fit2$p.value),Upregulated =colSums(is_upregulated),Downregulated =colSums(is_downregulated)) %>%pivot_longer(cols =-c("Contrast"), names_to ="sign", values_to ="length") %>%mutate(sign =recode(sign,"Upregulated"="Up","Downregulated"="Down")) %>% dplyr::group_by(Contrast) %>%mutate(total =sum(length),percent =round(100* length / total) ) %>%ungroup() %>%filter(Contrast %in%c(vector_with_interest)) %>%mutate(sign =fct_relevel(sign, c("Up", "Down")), Contrast =fct_relevel(Contrast, rev(vector_with_interest)))# Show results px <-ggplot(summary_DEG, aes(x = Contrast, y = length, fill = sign)) +geom_col() +# 1) Texte pour les barres "assez grandes" (>= 5)geom_text(data =subset(summary_DEG, length >=ifelse(lfc_lim_i ==0, 600,30)),aes(label =paste0(length, "\n(", percent, "%)") ),position =position_stack(vjust =0.5),color ="white", # texte en blancsize =3,# hjust = 0.5 (par défaut) - pas indispensable ) +# 2) Texte pour les barres "trop petites" (< 5)geom_text(data =subset(summary_DEG, length <ifelse(lfc_lim_i ==0, 600,30)),aes(label =paste0(length, " (", percent, "%)"),color = sign, # couleur du texte = même code couleur que "sign"vjust =ifelse(sign =="Up", -0.5, 1.5),y=total ),#position = position_stack(vjust = 0.5),hjust=-0.2,# On décale à droite (hors de la barre). En coord_flip(), # hjust < 0 place le texte davantage à droite.#hjust = -0.2,size =3 ) +theme_minimal() +labs(x ="Comparison",y ="Number of genes deregulated",title =paste0("Number of genes deregulated for each contrast (FDR=0.05 ; LogFC=", lfc_lim_i, ")") ) +coord_flip() +# Couleurs de remplissage (barres)scale_fill_manual(values =c('Down'="#1d4877", 'Up'="#ee3e32"),name ="Sign" ) +# Couleurs du texte (identique aux barres), sans nouvelle légendescale_color_manual(values =c('Down'="#1d4877", 'Up'="#ee3e32"),guide =FALSE ) ; pxfig_export(path = here::here(paste0("report/microarray/plot/DEG/barplot_differential_expression_lfc_",lfc_lim_i)), px, height =6.5, width =12, res_i =600)# export eatch contrastres_list <-list()# Loop through each contrast in your vector of interestfor (contrast in vector_with_interest) {# Construct vector names, e.g., "[SD-SS](Up)" and "[SD-SS](Down)" up_name <-paste0("(Up)", contrast) down_name <-paste0("(Down)", contrast)# Extract the rownames of all Upregulated genes for this contrast genes_up <-rownames(is_upregulated)[is_upregulated[, contrast]]# Extract the rownames of all Downregulated genes for this contrast genes_down <-rownames(is_downregulated)[is_downregulated[, contrast]]# Store them into the result list with meaningful names res_list[[up_name]] <- genes_up res_list[[down_name]] <- genes_down}save(res_list, file = here::here(paste0("data/microarray/output/contraste_lfc_",lfc_lim_i,".RData")))# make vulcanoplot for each contrastefor (contrast in vector_with_interest) {cat(paste0(contrast, "\n")) volcano_data <-tibble(Contrast = contrast,Gene =rownames(fit2$coefficients),logFC = fit2$coefficients[, contrast],pvalue = fit2$p.value[, contrast],adj_pvalue = p_adj[, contrast] ) %>%mutate(Significance =case_when( adj_pvalue <0.05& logFC > lfc_lim_i ~"Upregulated", adj_pvalue <0.05& logFC <-lfc_lim_i ~"Downregulated",TRUE~"Not significant" ) ) %>% dplyr::rename(PsCam = Gene) %>%# add info of genesleft_join(.,read_excel(here::here("data/microarray/Resultats_4plex-POIS-2014_01_230415_modKG.xlsx"), sheet ="Pscam_to_Psat_v1c_mrna_besthit-", col_names = T), by ="PsCam") %>%left_join(.,read_csv(here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.csv"), show_col_types =FALSE) %>%as.data.frame() %>% dplyr::select(c("id_probe", "CAT")) %>% dplyr::rename(PsCam = id_probe),by ="PsCam" ) num_upregulated <-sum(volcano_data$Significance =="Upregulated") num_downregulated <-sum(volcano_data$Significance =="Downregulated")# Création du titre avec couleurs HTML plot_title <-paste0("Volcano Plot - ", contrast, "<br>"," | <span style='color:#B02428;'>Upregulated: ", num_upregulated, "</span> | ", "<span style='color:#6697EA;'>Downregulated: ", num_downregulated, "</span>" ) p1 <-ggplot(volcano_data, aes(x = logFC, y =-log10(adj_pvalue), color = Significance)) +geom_point(alpha =0.7) +scale_color_manual(values =c("Upregulated"="#B02428", "Downregulated"="#6697EA", "Not significant"="grey")) +geom_vline(xintercept =c(-lfc_lim_i, lfc_lim_i), linetype ="dashed") +geom_hline(yintercept =-log10(pval_i), linetype ="dashed") +geom_text_repel(data = volcano_data %>%filter(Significance %in%c("Upregulated", "Downregulated")) %>%slice_max(abs(logFC),n =20), aes(label =sub("^symbols:", "", CAT), color = Significance),size =3,max.overlaps =0 ) +theme_minimal() +labs(title = plot_title,x =expression(log[2]~Fold~Change),y =expression(-log[10](p-value)) ) +theme(plot.title =element_markdown() # Permet d'afficher du texte formaté en HTML )fig_export(path = here::here(paste0("report/microarray/plot/volcanoplot/volacano_contrast_", gsub("\\*", "", contrast), "_lfc_",lfc_lim_i)), p1, height =6, width =12, res_i =300)# for interactive p1_interactive <-ggplot(volcano_data, aes(x = logFC, y =-log10(adj_pvalue), color = Significance)) +geom_point(aes(text =paste("PsCam:", PsCam, "<br>Psat:", Psat, "<br>CAT:", CAT, "<br>logFC:", logFC, "<br>FDR:", adj_pvalue)), size =1, alpha = .6) +scale_color_manual(values =c("Not significant"="gray", "Downregulated"="#6697EA", "Upregulated"="#B02428" ) ) +theme_minimal() +labs(title = plot_title,x ="log2 Fold Change", # Replace expression with plain texty ="-log10(p-value)" ) +theme(plot.title =element_markdown() # Permet d'afficher du texte formaté en HTML )ggplotly(p1_interactive) %>%saveWidget(here::here(paste0("report/microarray/plot/volcanoplot/volcano_interactive_contraste_",gsub("\\*", "", contrast),"_lfc_",lfc_lim_i,".html")))}
load(file = here::here("data/microarray/output/upset_result_condition_sign_lfc_contrast_0.RData"))my_gene_of_interest <-"PsCam042688"in_up <-names(keep(lt_up, ~ my_gene_of_interest %in% .x))in_down <-names(keep(lt_down, ~ my_gene_of_interest %in% .x))df_up <-data.frame("In Up Regulated"= in_up, check.names =FALSE)df_down <-data.frame("In Down Regulated"= in_down, check.names =FALSE)# Generate HTML for each table with kabletable1_html <-kable(df_up, format ="html", table.attr ="class='table'") %>%kable_styling(full_width =FALSE)table2_html <-kable(df_down, format ="html", table.attr ="class='table'") %>%kable_styling(full_width =FALSE)# Combine the two panels side by side using a flexbox dividerhtml_output <-paste0("<div style='display: flex; justify-content: center; gap: 20px;'>","<div>", table2_html, "</div>","<div>", table1_html, "</div>","</div>")# Display HTML without escapementknitr::asis_output(html_output)
In Down Regulated
SS_WT1 vs SS_W78*
SS_WT2 vs SS_E568K
SD_WT1 vs SD_W78*
In Up Regulated
Which group does the psult4 gene belong to?
Code
load(file = here::here("data/microarray/output/upset_result_condition_sign_lfc_contrast_1.RData"))my_gene_of_interest <-"PsCam042688"in_up <-names(keep(lt_up, ~ my_gene_of_interest %in% .x))in_down <-names(keep(lt_down, ~ my_gene_of_interest %in% .x))df_up <-data.frame("In Up Regulated"= in_up, check.names =FALSE)df_down <-data.frame("In Down Regulated"= in_down, check.names =FALSE)# Generate HTML for each table with kabletable1_html <-kable(df_up, format ="html", table.attr ="class='table'") %>%kable_styling(full_width =FALSE)table2_html <-kable(df_down, format ="html", table.attr ="class='table'") %>%kable_styling(full_width =FALSE)# Combine the two panels side by side using a flexbox dividerhtml_output <-paste0("<div style='display: flex; justify-content: center; gap: 20px;'>","<div>", table2_html, "</div>","<div>", table1_html, "</div>","</div>")# Display HTML without escapementknitr::asis_output(html_output)
In Down Regulated
SS_WT1 vs SS_W78*
SD_WT1 vs SD_W78*
In Up Regulated
13.7 GO enrichment
Code
#parameterlfc_lim_i =1# collect data and add Psat to PsCAMload(here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.RData"))load(file = here::here(paste0("data/microarray/output/upset_result_condition_sign_lfc_contrast_",lfc_lim_i,".RData")))df_info_gene <- RG$genes %>% dplyr::rename(PsCam = ID) %>%left_join(.,read_excel(here::here("data/microarray/Resultats_4plex-POIS-2014_01_230415_modKG.xlsx"), sheet ="Pscam_to_Psat_v1c_mrna_besthit-", col_names = T), by ="PsCam")cluster_info <-rbind(purrr::imap_dfr(lt_down, ~tibble(PsCam = .x, comparison = .y)) %>%mutate(cluster =paste0("(Down) ", comparison)), purrr::imap_dfr(lt_up, ~tibble(PsCam = .x, comparison = .y)) %>%mutate(cluster =paste0("(Up) ", comparison))) %>%full_join(., df_info_gene, by ="PsCam") %>%mutate(ID = Psat) %>%drop_na(comparison, ID)df_GO =GO_on_different_group(functional_roles ="BP",group_info = cluster_info,group ="cluster",ID ="ID",top =10 )# Merge this with your original data to fill missing combinationsCluster_selected_GO_filled <-df_GO %>%# on s'arrete ici pour la fonction. dplyr::rename(cluster = group) %>%mutate(cluster =factor(cluster,levels =unique(cluster)), comparison =str_trim(str_remove(cluster, "\\s*\\(.*?\\)")),sign =str_extract(cluster, "(?<=\\().*?(?=\\))") ) %>%mutate(comparison = forcats::fct_relevel(comparison, "SS_WT1 vs SS_W78*", "SS_WT2 vs SS_E568K", "SD_WT1 vs SD_W78*", "SD_WT2 vs SD_E568K"))#vector_color <- c("#067B5B", "#6AD6AD", "#B87100", "#FFAC5C", "#9381FF", "#067B5B", "#6AD6AD", "#B87100", "#FFAC5C", "#9381FF")# Plot the enrichment by GOpx <-ggplot(Cluster_selected_GO_filled, aes(x = cluster, y = Description_GO, fill =`|-log10(Pval)|`)) +geom_tile(color ="black") +scale_fill_gradient2(low ="white", high ="red", na.value ="white", limits =c(0, NA)) +theme_minimal() +theme(axis.text =element_text(size =8, colour ="black"),axis.text.x =element_text(angle =90, vjust =0.5, hjust =1),panel.grid.major =element_blank(),plot.background =element_rect(fill ="white", colour ="white")) +labs(fill ="-log10(FDR)",x="Biological process",y ="Cluster", title ="GO terme for all gene deregulated") +#scale_size_manual(values = c(dot = 2, no_dot = NA), guide = "none")+new_scale_fill() +scale_fill_manual(values =c(lighten(as.character(sulfate_pallet[1]), amount =-.2),lighten(as.character(sulfate_pallet[1]), amount = .5),lighten(as.character(sulfate_pallet[2]), amount =-.2),lighten(as.character(sulfate_pallet[2]), amount = .3),"#9381FF"),name ="Comparison" ) +geom_tile(aes(x = cluster,y =-1, # If you truly want it at a negative y-value, # ensure your y-scale is continuous or can handle thisfill = comparison,width =0.90,height =0.80 ),data = Cluster_selected_GO_filled,color ="black",alpha =1,inherit.aes =FALSE )+new_scale_fill() +scale_fill_manual(values =c('Down'="#1d4877", 'Up'="#ee3e32"),name ="Sign" ) +geom_tile(aes(x = cluster,y =-0.05, # If you truly want it at a negative y-value,# ensure your y-scale is continuous or can handle thisfill = sign,width =0.90,height =0.80 ),data = Cluster_selected_GO_filled,color ="black",alpha =1,inherit.aes =FALSE ) ; px# export fig_export(path =paste0("report/microarray/plot/GO/GO_BP_all_absolute_lfc_contrast_",lfc_lim_i), plot_x = px, height_i =13, width_i =9, res =600)
Code
# euler (or upset)#parameterlfc_lim_i =1# collect data and add Psat to PsCAMload(here::here("data/microarray/output/raw_data_microarray_leaf_PeaSulf.RData"))load(file = here::here(paste0("data/microarray/output/upset_result_condition_sign_lfc_contrast_",lfc_lim_i,".RData")))df_info_gene <- RG$genes %>% dplyr::rename(PsCam = ID) %>%left_join(.,read_excel(here::here("data/microarray/Resultats_4plex-POIS-2014_01_230415_modKG.xlsx"), sheet ="Pscam_to_Psat_v1c_mrna_besthit-", col_names = T), by ="PsCam")df_DEG_condition <-rbind(create_presence_matrix(lt_up, "Up"),create_presence_matrix(lt_down, "Down")) %>%mutate(Sign =as.factor(Sign),Sign =relevel(Sign, ref ="Up") )gene_id_col <-colnames(df_DEG_condition)[1]v_conditions <-colnames(df_DEG_condition)[2:(ncol(df_DEG_condition) -1)]df_gene_intersections <- df_DEG_condition %>%rowwise() %>%filter(Sign =="Down") %>%mutate(intersection =paste0(c_across(all_of(v_conditions)), collapse ="")) %>%ungroup()intersection_list_lfc0 <-list(I1 ='SS_WT2 vs SS_E568K', I2 ='SD_WT2 vs SD_E568K',I3 =c('SS_WT2 vs SS_E568K', 'SD_WT2 vs SD_E568K'),I4 =c('SD_WT1 vs SD_W78*', 'SD_WT2 vs SD_E568K'),I5 ='SD_WT1 vs SD_W78*',I6 =c('SS_WT2 vs SS_E568K', 'SD_WT2 vs SD_E568K','SD_WT1 vs SD_W78*'),I7 =c('SS_WT2 vs SS_E568K','SD_WT1 vs SD_W78*'),I8 =c('SS_WT2 vs SS_E568K','SD_WT1 vs SD_W78*','SS_WT1 vs SS_W78*'))intersection_list_lfc1 <-list(I1 ='SD_WT2 vs SD_E568K',I2 ='SS_WT2 vs SS_E568K', I3 =c('SS_WT2 vs SS_E568K', 'SD_WT2 vs SD_E568K'),I4 =c('SD_WT1 vs SD_W78*', 'SD_WT2 vs SD_E568K'),I5 ='SD_WT1 vs SD_W78*',I6 =c('SD_WT1 vs SD_W78*','SS_WT1 vs SS_W78*'))if(lfc_lim_i ==1){ intersection_list = intersection_list_lfc1}elseif(lfc_lim_i ==0){ intersection_list = intersection_list_lfc0}intersection_patterns <-map(intersection_list, function(conds) {sapply(v_conditions, function(x) as.integer(x %in% conds)) %>%paste0(collapse ="")})signs <-c("Down", "Up")df_gene_intersections_by_sign <-map(signs, function(s) { df_DEG_condition %>%filter(Sign == s) %>%rowwise() %>%mutate(intersection =paste0(c_across(all_of(v_conditions)), collapse ="")) %>%ungroup()})names(df_gene_intersections_by_sign) <- signsintersection_genes_all <-list()for(i innames(intersection_list)) {for(s in signs) { pattern <- intersection_patterns[[i]] genes <- df_gene_intersections_by_sign[[s]] %>%filter(intersection == pattern) %>%pull(!!sym(gene_id_col))# Nommer l'élément comme "I1 (Up)" ou "I1 (Down)", etc. intersection_genes_all[[paste0("(", s, ") ", i)]] <- genes }}# and then same hase beforecluster_info <- purrr::imap_dfr(intersection_genes_all, ~tibble(PsCam = .x, intersection = .y)) %>%mutate(cluster = intersection) %>%full_join(., df_info_gene, by ="PsCam") %>%mutate(ID = Psat) %>%drop_na(intersection, ID)df_GO =GO_on_different_group(functional_roles ="BP",group_info = cluster_info,group ="cluster",ID ="ID",top =10 )# Merge this with your original data to fill missing combinationsCluster_selected_GO_filled <- df_GO %>% dplyr::rename(cluster = group) %>%mutate(cluster =factor(cluster,levels =unique(cluster)), comparison =str_trim(str_remove(cluster, "\\s*\\(.*?\\)")),sign =str_extract(cluster, "(?<=\\().*?(?=\\))") ) #vector_color <- c("#067B5B", "#6AD6AD", "#B87100", "#FFAC5C", "#9381FF", "#067B5B", "#6AD6AD", "#B87100", "#FFAC5C", "#9381FF")# Plot the enrichment by GOpx <-ggplot(Cluster_selected_GO_filled, aes(x = cluster, y = Description_GO, fill =`|-log10(Pval)|`)) +geom_tile(color ="black") +scale_fill_gradient2(low ="white", high ="red", na.value ="white", limits =c(0, NA)) +theme_minimal() +theme(axis.text =element_text(size =8, colour ="black"),axis.text.x =element_text(angle =90, vjust =0.5, hjust =1),panel.grid.major =element_blank(),plot.background =element_rect(fill ="white", colour ="white")) +labs(fill ="-log10(FDR)",x="Biological process",y ="Cluster", title ="GO terme for all gene deregulated") +#scale_size_manual(values = c(dot = 2, no_dot = NA), guide = "none")+new_scale_fill() +scale_fill_manual(values =c('Down'="#1d4877", 'Up'="#ee3e32"),name ="Sign" ) +geom_tile(aes(x = cluster,y =-0.05, # If you truly want it at a negative y-value,# ensure your y-scale is continuous or can handle thisfill = sign,width =0.90,height =0.80 ),data = Cluster_selected_GO_filled,color ="black",alpha =1,inherit.aes =FALSE ) ; px# export fig_export(path =paste0("report/microarray/plot/GO/GO_BP_euler_lfc_contrast_", lfc_lim_i), plot_x = px, height_i =20, width_i =12, res =600)
For this plot I only took the standardised data and the comparisons or similarities that I found interesting. For the comparisons I generated a contrast matrix for a linear model which I then used to carry out the differential analysis in limma. For the similarity, I looked at which genes were similar to the two groups. I’m only going to do this for a log fold change equal to 1.
lfc_lim_i =0type_calcul_i ="hypergeometric"#clusterProfiler # hypergeometric # fisher # in reality its exactly the sameload(file = here::here(paste0("data/microarray/output/contraste_publication_lfc_",lfc_lim_i,".RData")))vector_contrast0 <-c("[SD-SS]", common1_key, common2_key, common3_key,"[SS_W78*-SS_WT1]-[SS_E568K-SS_WT2]","[SD_W78*-SD_WT1]-[SD_E568K-SD_WT2]","[SD_W78*-SD_WT1]-[SS_W78*-SS_WT1]","[SD_E568K-SD_WT2]-[SS_E568K-SS_WT2]", common4_key, common5_key#,# "[E568K-WT2]",# "[W78*-WT1]",# "[WT1_SD-WT1_SS]",# "[W78*_SD-W78*_SS]",# "[WT2_SD-WT2_SS]",# "[E568K_SD-E568K_SS]",# "[SS_W78*-SS_WT1]",# "[SS_E568K-SS_WT2]",# "[SD_W78*-SD_WT1]",# "[SD_E568K-SD_WT2]")common_keys <-c(common1_key, common2_key, common3_key, common4_key, common5_key)# 2. Pour chaque contraste brut, on colle les bons préfixesvector_contrast0_sign <-unlist(lapply(vector_contrast0, function(ct) {# si c'est un common → on utilise les préfixes "_common"if (ct %in% common_keys) {paste0(c("(Up_common)", "(Down_common)"), ct) } else {paste0(c("(Up)", "(Down)"), ct) } }))gene_table <-tibble(contrast =rep(names(res_list), lengths(res_list)),PsCam =unlist(res_list)) %>%left_join(.,read_excel(here::here("data/microarray/Resultats_4plex-POIS-2014_01_230415_modKG.xlsx"), sheet ="Pscam_to_Psat_v1c_mrna_besthit-", col_names = T), by ="PsCam") %>%drop_na(Psat) %>%filter(contrast %in% vector_contrast0_sign)nrow(gene_table %>%filter(contrast =="(Down)[SD-SS]"))universe_vector <-unique(gene_table$Psat)df_GO =GO_on_different_group(functional_roles ="BP",group_info = gene_table,group ="contrast",ID ="Psat",top =10,universe_i = universe_vector, type_calcul = type_calcul_i )# combined_results = GO_on_different_group(functional_roles = "BP",# group_info = gene_table,# group = "contrast",# ID = "Psat",# top = FALSE,# universe_i = universe_vector, # type_calcul = type_calcul_i# )Cluster_selected_GO_filled <- df_GO %>% dplyr::rename(cluster = group) %>%mutate(cluster =factor(cluster,levels =unique(cluster)), comparison =str_trim(str_remove(cluster, "\\s*\\(.*?\\)")),sign =str_extract(cluster, "(?<=\\().*?(?=\\))") )# Plot the enrichment by GOpx <-ggplot(Cluster_selected_GO_filled, aes(x = cluster, y = Description_GO, fill =`|-log10(Pval)|`)) +geom_tile(color ="black") +scale_fill_gradient2(low ="white", high ="red", na.value ="white", limits =c(0, NA)) +theme_minimal() +theme(axis.text =element_text(size =8, colour ="black"),axis.text.x =element_text(angle =90, vjust =0.5, hjust =1),panel.grid.major =element_blank(),plot.background =element_rect(fill ="white", colour ="white")) +labs(fill ="-log10(FDR)",x="Biological process",y ="Cluster", title =paste0("GO terme for all gene deregulated with lfc:",lfc_lim_i)) +#scale_size_manual(values = c(dot = 2, no_dot = NA), guide = "none")+new_scale_fill() +scale_fill_manual(values =c('Down'="#1d4877", 'Up'="#ee3e32"),name ="Sign" ) +geom_tile(aes(x = cluster,y =-0.05, # If you truly want it at a negative y-value,# ensure your y-scale is continuous or can handle thisfill = sign,width =0.90,height =0.80 ),data = Cluster_selected_GO_filled,color ="black",alpha =1,inherit.aes =FALSE ) ; pxfig_export(path =paste0("report/microarray/plot/GO/GO_BP_contrast_",type_calcul_i,"_",lfc_lim_i), plot_x = px, height_i =20, width_i =12, res =600)
Alves‐Carvalho, Susete, Grégoire Aubert, Sébastien Carrère, Corinne Cruaud, Anne‐Lise Brochot, Françoise Jacquin, Anthony Klein, et al. 2015. “Full‐length de Novo Assembly of RNA‐seq Data in Pea ( <Span Style="font-Variant:small-Caps;">p</Span> Isum Sativum l.) Provides a Gene Expression Atlas and Gives Insights into Root Nodulation in This Species.”The Plant Journal 84 (1): 1–19. https://doi.org/10.1111/tpj.12967.
Bolstad, B. M., R. A Irizarry, M. Åstrand, and T. P. Speed. 2003. “A Comparison of Normalization Methods for High Densityoligonucleotide Array Data Based on Variance and Bias.”Bioinformatics 19 (2): 185–93. https://doi.org/10.1093/bioinformatics/19.2.185.
Lurin, Claire, Charles Andreés, Seébastien Aubourg, Mohammed Bellaoui, Freédeérique Bitton, Cleémence Bruyère, Michel Caboche, et al. 2004. “Genome-Wide Analysis of Arabidopsis Pentatricopeptide Repeat Proteins Reveals Their Essential Role in Organelle Biogenesis[w].”The Plant Cell 16 (8): 2089–2103. https://doi.org/10.1105/tpc.104.022236.