TCGA 数据分析实战 —— WGCNA

前言

加权基因共表达网络分析(WGCNA, Weighted gene co-expression network analysis)是一种用来描述不同基因在样本中的表达关联模式的系统生物学方法。

通过将表达高度相关的基因聚集成不同的模块,并探究不同模块与样本表型之间的关联。还可以探究模块内的关键基因的功能,作为潜在的生物标志物或治疗靶点进行后续分析

WGCNA 模块识别算法大致包含以下几个步骤:

  1. 数据预处理
  2. 构建加权相关性邻接矩阵
  3. 计算拓扑重叠矩阵(TOM
  4. 对基因进行层次聚类,划分模块

1. 数据预处理

1.1 数据格式

输入数据的格式要符合行为样本,列为基因的矩阵格式,因为计算的是基因之间的相关性,所以数据可以是标准化的表达值或者是 read counts

1.2 过滤基因

探针集或基因可以通过平均表达量或方差(如中位数或绝对中位差)进行过滤,因为低表达或无变化的基因通常代表噪音。

注意:并不推荐使用差异基因作为输入矩阵,通过差异表达基因过滤将会导致一个(或几个高度相关的)基因聚成一个模块,同时,也破坏了无标度拓扑的假设,所以通过无标度拓扑拟合来选择软阈值的将会失败。

1.3 样本过滤

主要是过滤一些离群或异常的样本,可以对样本数据进行聚类,如果存在异常样本,则其在聚类图中会显示出离群现象,可考虑将其剔除。

2. 构建相似矩阵

2.1 计算相关性

首先,对基因的表达量进行 0-1 标准化,即

scale(x) = \frac{x - mean(x)}{\sqrt{var(x)}}

其中,var(x) 为样本方差

然后,使用 pearson 计算基因之间的相关性

cor(x, y) = \frac{\left\langle{scale(x), scale(y)}\right\rangle}{||scale(x)||\cdot||sacle(y)||}

两个基因的共表达相似性表示为
s_{ij} = |cor(i, j)|

2.2 硬阈值

然后将基因之间的相似度转换为邻接值,对于非加权网络,计算方式为

其中 \tau 为硬阈值,大于等于该阈值表示这两个基因之间存在连接,而低于阈值则认为两个基因没有连接。它们并不能反映共表达信息的连续性质,因此可能导致信息损失。例如,阈值为 0.8,那 0.79 是不是应该也有一定的相关性呢?

2.3 软阈值

在介绍软阈值之前,我们先引出两个图论的概念:

  • 随机网络:网络中,每个节点的连接度差不多,没有特别高或低连接度的节点,节点之间的连接相对随机
  • 无标度网络(scale-free):网络中大部分节点只和很少的节点连接,而有极少的节点与非常多的节点连接,连接度很高

度表示为节点所连接的边的数量

无标度网络具有很好的鲁棒性,网络中某些节点的错误并不会导致整个网络的瘫痪,具有很多的代偿连接。而这一特点,与生物体中的复杂生化网络非常类似,只有少数的基因执行着关键性的功能,而大多数的基因执行较为单一的功能。

无标度网络中,节点 d 的度为 k 的概率满足幂律分布
P(d = k) \propto k^{-\gamma}

通过对数变换,变为
logP(d = k) \propto -\gamma log(k)

从这个公式可以看出,节点的度数与其出现的概率是负相关的,通过计算各个节点的度数 k 与该度数 k 在所有节点度数中的占比的 pearson 相关性,我们可以得到关于无标度网络的适应系数。该系数越接近 1 则越像无标度网络,越接近 0 则越像随机网络。

所以,对于加权网络,其邻接值的计算方式为:
a_{ij} = s_{ij}^\beta

当软阈值 \beta \ge 1 时,会让相关系数小的更小,而大的更大。

可以根据适应系数来筛选软阈值

3. 拓扑重叠矩阵(TOM)

光有邻接矩阵是不够的,基因间的相似性应该要同时体现在其表达和网络拓扑水平,为了能能够尽可能地最小化噪音和假阳性的影响,因此引入了拓扑重叠矩阵

这个概念的主要表达的是,两个基因 ab 之间的相关性,不光考虑两个基因的表达相关性,还需要考虑一些 AB 共有的表达相关基因 u,如果 u 足够多,则说明 AB 的网络重叠性强,应该被聚成一类

换个说法,两个人之间的亲密度不仅与他们两人之间有关,还与他们的共同好友有关,共同好友越多,说明他们两人之间应该越亲密

计算公式为:
TOM(i,j) = \frac{\sum_ua_{iu}a_{uj} + a_{ij}}{min(k_i, k_j) + 1 - a_{ij}}
其中,k_i, k_j 分别为 ij 的度数

TOM(x,y) 表示的是两个基因的相似性,转换成距离度量就是 1 - TOM(x,y),并使用该值来进行聚类,并分割模块

应用实例

我们以 TCGA 的乳腺癌数据作为示例,来完整的做一遍 WGCNA 分析

先安装模块

install.packages("BiocManager")
BiocManager::install("WGCNA")

1. 数据预处理

获取 50 个样本的 FPKM 数据,WGCNA 最少需要 15 个样本,20 个以上的样本会更好,样本越多越好,这里为了方便,我们只挑了 50 个样本

library(tidyverse)
library(org.Hs.eg.db)
library(TCGAbiolinks)
library(SummarizedExperiment)

get_expression <- function(proj, n = 20) {
  query <- GDCquery(
    project = proj,
    data.category = "Transcriptome Profiling",
    data.type = "Gene Expression Quantification", 
    workflow.type = "HTSeq - FPKM"
  )
  query$results[[1]] <- query$results[[1]][1:n,]
  GDCdownload(query)
  data <- GDCprepare(query)
  exp <- assay(data) %>% as.data.frame() %>%
    rownames_to_column(var = "Ensembl_ID") %>% {
      unimap <- mapIds(
        org.Hs.eg.db, keys = .$Ensembl_ID, keytype = "ENSEMBL", 
        column = "SYMBOL", multiVals = "filter")
      filter(., Ensembl_ID %in% names(unimap))
    } %>%
    mutate(Symbol = AnnotationDbi::select(
      org.Hs.eg.db, keys = .$Ensembl_ID, keytype = "ENSEMBL",
      columns = "SYMBOL")$SYMBOL, .before = Ensembl_ID) %>%
    dplyr::select(!Ensembl_ID) %>%
    filter(!is.na(Symbol)) %>%
    group_by(Symbol) %>%
    summarise_all(mean) %>% 
    column_to_rownames(var = "Symbol")
  return(exp)
}

exp <- get_expression("TCGA-BRCA", n = 50)

过滤基因,取绝对中位差 top 5000 的基因

data.mat <- t(exp[order(apply(exp, 1, mad), decreasing = T)[1:5000],])

过滤异常样本

library(WGCNA)

gsg <- goodSamplesGenes(data.mat, verbose = 3)
# 如果存在异常样本或基因
if (!gsg$allOK) {
  # 异常的基因
  if (sum(!gsg$goodGenes)>0) 
    printFlush(paste("Removing genes:", 
                     paste(names(data.mat)[!gsg$goodGenes], collapse = ",")));
  # 异常的样本
  if (sum(!gsg$goodSamples)>0) 
    printFlush(paste("Removing samples:", 
                     paste(rownames(data.mat)[!gsg$goodSamples], collapse = ",")));
  # 删除异常的样本和基因
  data.mat = data.mat[gsg$goodSamples, gsg$goodGenes]
}
# 绘制样本聚类图
sampleTree <- hclust(dist(data.mat), method = "average")
plot(sampleTree, main = "Sample clustering to detect outliers", sub="", xlab="")

2. 确定软阈值

确定软阈值的时候,需要选择网络类型,不同的网络类型,其计算邻接值的方法是不一样的。

  • unsigned
    adjacency = |cor|^{power}
  • signed
    adjacency = (0.5 * (1+cor))^{power}
  • signed hybrid
    adjacency = \begin{cases} cor^{power}, &cor > 0 \\ 0, & otherwise \\ \end{cases}

默认为 unsigned

# 打开多线程
# enableWGCNAThreads()
allowWGCNAThreads()
type <- "unsigned"
powers <- c(1:10, seq(from = 12, to=30, by=2))
sft <- pickSoftThreshold(
  data.mat, powerVector=powers, 
  networkType=type, verbose=3
)

我在 RStudio 中使用 enableWGCNAThreads() 会引发下面的错误

Error in datk[c(startG:endG), ] = foreach(t = actualThreads, .combine = rbind) %dopar% 

所以,我改用了 allowWGCNAThreads(),就可以运行了

绘制软阈值曲线

par(mfrow = c(1, 2))
cex1 = 0.9
plot(
  sft$fitIndices[, 1], 
  -sign(sft$fitIndices[, 3]) * sft$fitIndices[, 2],
  xlab = "Soft Threshold (power)",
  ylab = "Scale Free Topology Model Fit,signed R^2",
  type = "n",
  main = paste("Scale independence")
)
text(
  sft$fitIndices[, 1],
  -sign(sft$fitIndices[, 3]) * sft$fitIndices[, 2],
  labels = powers,
  cex = cex1,
  col = "red"
)
abline(h = 0.9, col = "red")

plot(
  sft$fitIndices[, 1],
  sft$fitIndices[, 5],
  xlab = "Soft Threshold (power)",
  ylab = "Mean Connectivity",
  type = "n",
  main = paste("Mean connectivity")
)
text(
  sft$fitIndices[, 1],
  sft$fitIndices[, 5],
  labels = powers,
  cex = cex1,
  col = "red"
)

其中横坐标为软阈值的梯度,第一幅图的纵坐标为无标度网络适应系数,越大越好;第二幅图的纵坐标为节点的平均连通度,越小越好。

查看系统给我们推荐的软阈值

> sft$powerEstimate
[1] 5

与我们从图上看到的结果是一致的,如果出现了异常的值,也就是说在有效的 power 梯度范围内(无向网络在 power 小于 15,有向网络 power 小于 30),无法使适应系数的值超过 0.8,且平均连接度在 100 以上

可能是由于部分样品与其他样品差别较大。这可能是由于批次效应、样品异质性或实验条件对表达影响太大等因素造成的。

可以对样本绘制聚类图来查看有无异常样品,如果这确实是由于生物学差异引起的,也可以使用下面的经验 power 值。

if (is.na(power)) {
  power = ifelse(
    nSamples < 20,
    ifelse(type == "unsigned", 9, 18),
    ifelse(
      nSamples < 30,
      ifelse(type == "unsigned", 8, 16),
      ifelse(
        nSamples < 40,
        ifelse(type == "unsigned", 7, 14),
        ifelse(type == "unsigned", 6, 12)
      )
    )
  )
}

3. 构建共表达网络

3.1 构建网络

一步法构建网络,我们使用上面推荐的软阈值 5

net <- blockwiseModules(
  data.mat,
  power = 5,
  # 最大模块数量
  maxBlockSize = 2000,
  TOMType = type,
  minModuleSize = 30,
  reassignThreshold = 0,
  # 需要合并模块的阈值
  mergeCutHeight = 0.25,
  # 以数字作为模块的名字
  numericLabels = TRUE,
  pamRespectsDendro = FALSE,
  saveTOMs = TRUE,
  saveTOMFileBase = "~/Downloads/TCGA_FPKM_TOM",
  verbose = 3
)

查看各模块的基因数量

> table(net$colors)

   0    1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16   17   18   19 
 247 1189  457  266  254  218  168  154  153  133  128  118  111  103   99   87   78   69   69   65 
  20   21   22   23   24   25   26   27   28   29   30   31   32   33   34   35   36   37 
  64   61   55   55   54   51   48   48   48   47   45   44   40   39   36   33   33   33 

3.2 可视化

可以使用 labels2colors 函数将数值转换为颜色名称

labels2colors(net$colors)

使用 plotDendroAndColors 函数来展示各个模块的层次聚类结果

moduleColors <- labels2colors(net$colors)
plotDendroAndColors(
  net$dendrograms[[1]],
  moduleColors[net$blockGenes[[1]]],
  "Module colors",
  dendroLabels = FALSE,
  hang = 0.03,
  addGuide = TRUE,
  guideHang = 0.05
)

其中,无法聚类到模块中的基因会标示为灰色,如果灰色区域较多,可能由于样本中基因共表达趋势不明显,可能需要调整基因过滤的方法。

展示模块之间的相关性

MEs_col <- net$MEs
colnames(MEs_col) <- paste0("ME", labels2colors(
  as.numeric(str_replace_all(colnames(MEs_col),"ME",""))))
MEs_col <- orderMEs(MEs_col)

# 根据基因间表达量进行聚类所得到的各模块间的相关性图
# marDendro/marHeatmap 设置下、左、上、右的边距
plotEigengeneNetworks(
  MEs_col,
  "Eigengene adjacency heatmap",
  marDendro = c(3, 3, 2, 4),
  marHeatmap = c(3, 4, 2, 2),
  plotDendrograms = T,
  xLabelsAngle = 90
)

展示 TOM 矩阵,为了节省时间,我们只使用第一个聚类分支

# 导入之前保存的 TOM 矩阵,共保存成了 3 个文件,我们就取其中一个
load(net$TOMFiles[1], verbose = TRUE)
# 如果未保存,需要重新计算
# TOM <- TOMsimilarityFromExpr(data.mat, power=5, networkType=type)

TOM <- as.matrix(TOM)
dissTOM <- 1-TOM
plotTOM <- dissTOM^7
diag(plotTOM) <- NA
TOMplot(
  plotTOM, 
  # 层次聚类的第一个
  net$dendrograms[[1]], 
  # 取对应块基因的颜色
  moduleColors[net$blockGenes[[1]]],
  main = "Network heatmap plot, all genes"
)

或者更换一种配色

mycolor <- gplots::colorpanel(250, 'red', "orange", 'lemonchiffon')
TOMplot(
  plotTOM, net$dendrograms[[1]], 
  # 取对应块基因的颜色
  moduleColors[net$blockGenes[[1]]],
  main = "Network heatmap plot, all genes",
  col = mycolor
)

颜色越深表示基因表达的相关性更高,我们可以看到,模块内的基因之间具有较高的共表达,而模块之间的表达相关性较低

3.3 模块导出

将整个网络全部导出成 Cytoscape 输入文件

file <- "~/Downloads/TCGA/BRCA.net"
genes <- names(net$colors[net$blockGenes[[1]]])
dimnames(TOM) <- list(genes, genes)
cyt <- exportNetworkToCytoscape(
  TOM,
  edgeFile = paste0(file, ".edges.txt"),
  nodeFile = paste0(file, ".nodes.txt"),
  weighted = TRUE,
  threshold = 0,
  nodeNames = genes,
  nodeAttr = moduleColors[net$blockGenes[[1]]]
)

保存网络

save(net, file = "~/Downloads/TCGA/BRCA.net.rda")

也可以提取某一模块的基因

module <- "paleturquoise"
moduleGenes <- names(net$colors)[which(moduleColors == module)]

获取到基因之后,可以进行富集分析找到相关的生物学通路

4. 表型相关

4.1 识别表型相关模块

我们可以分析各网络模块与样本表型之间的关系,从而找到与我们感兴趣表型相关的模块。

样本表型可以是各种指标,比如肿瘤分期分级、已知的分类亚型、药物响应等,并计算模块与这些表型之间是否具有显著相关性

但是模块是一个矩阵,无法直接计算矩阵和向量之间的相关性,需要转换为向量之间的相关性。

WGCNA 选择使用 PCA 的方法对数据降维,并将第一主成分定义为 eigengenes,然后计算 eigengenes 与表型之间的相关性

先获取并处理临床数据

query <- GDCquery(
  project = "TCGA-BRCA",
  data.category = "Clinical",
  data.type = "Clinical Supplement",
  data.format = "BCR Biotab",
  file.type = "patient"
)
GDCdownload(query)
clinical <- GDCprepare(query)

data.clin <- clinical$clinical_patient_brca[-c(1,2),]
use.clin <- data.clin %>%
  subset(bcr_patient_barcode %in% substr(colnames(exp), 1, 12)) %>%
  dplyr::select(c("bcr_patient_barcode", "er_status_by_ihc", "pr_status_by_ihc",
                  "her2_status_by_ihc")) %>%
  `names<-`(c("sample", "ER", "PR", "HER2")) %>%
  mutate_at(vars("ER", "PR", "HER2"), ~ifelse(. %in% c("Positive", "Negative"), ., "Unknown"))
> head(use.clin)
# A tibble: 6 × 4
  sample       ER       PR       HER2    
  <chr>        <chr>    <chr>    <chr>   
1 TCGA-A2-A0CX Positive Negative Positive
2 TCGA-A2-A0D0 Negative Negative Negative
3 TCGA-A2-A0EV Positive Positive Negative
4 TCGA-A2-A3XT Negative Negative Negative
5 TCGA-A2-A4S2 Positive Positive Negative
6 TCGA-A7-A0CE Negative Negative Unknown 

计算模块与 ER 状态的相关性

rownames(MEs_col) <- substr(rownames(MEs_col), 1, 12)
# 构建 stage 分类矩阵
design <- model.matrix(~0 + use.clin$ER)
dimnames(design) <- list(use.clin$sample, sort(unique(use.clin$ER)))
design <- design[rownames(MEs_col),]

# 计算 pearson 相关性和显著性
modTraitCor <- cor(MEs_col, design, use = "p")
modTraitP <- corPvalueStudent(modTraitCor, dim(use.clin)[1])

如果使用的是其他相关性方法,则可以使用 bicorAndPvalue 函数来计算显著性

modTraitCorP = bicorAndPvalue(MEs_col, design)
modTraitCor = modTraitCorP$bicor
modTraitP   = modTraitCorP$p

绘制相关性图

textMatrix <- paste0(signif(modTraitCor, 2), "\n(", signif(modTraitP, 1), ")")
dim(textMatrix) <- dim(modTraitCor)
labeledHeatmap(
  Matrix = modTraitCor,
  xLabels = colnames(design),
  yLabels = colnames(MEs_col),
  cex.lab = 0.5,
  ySymbols = colnames(MEs_col),
  colorLabels = FALSE,
  colors = blueWhiteRed(50),
  textMatrix = textMatrix,
  setStdMargins = FALSE,
  cex.text = 0.5,
  zlim = c(-1, 1),
  main = paste("Module-trait relationships")
)

可以看到有些模块的相关性挺高的,而且也具有显著性。我们计算出模块与表型之间相关性之后,可以挑选最相关的那些模块来进行后续分析。但是,模块本身可能还包含很多的基因,还需要进一步识别关键基因基因。

4.2 提取关键基因

如何寻找关键基因呢?我们可以计算所有基因与模块之间的相关性,也可以计算基因与表型之间的相关性。如果存在一些基因,既与表型显著相关又跟某个模块显著相关,那么这些基因可能就是非常重要的关键基因了

从上图中,我们可以看到 paleturquoise 具有较高的相关性,且具有显著性,我们就来尝试找找这个模块的关键基因

计算基因与模块的相关性

nSamples <- dim(data.mat)[1]
geneModuleMembership <- cor(data.mat, MEs_col, use = "p")
MMPvalue <- corPvalueStudent(geneModuleMembership, nSamples)

再计算基因与表型的相关性

geneSignificanceCor <- cor(data.mat, design, use = "p")
geneSignificanceP <- corPvalueStudent(geneSignificanceCor, nSamples)

展示模块内基因与模块和表型之间的相关性

module <- "paleturquoise"
column <- paste0("ME", module)
moduleGenes <- names(net$colors)[which(moduleColors == module)]
MM <- abs(geneModuleMembership[moduleGenes, column])
GS <- abs(geneSignificanceCor[moduleGenes, 1])
verboseScatterplot(
  MM, GS,
  xlab = paste("Module Membership in", module, "module"),
  ylab = "Gene significance for proliferating",
  main = paste("Module membership vs. gene significance\n"),
  abline = TRUE,
  pch = 21,
  cex.main = 1.2,
  cex.lab = 1.2,
  cex.axis = 1.2,
  col = "black",
  bg = module
)

从图中我们可以看出,基因与表型的相关性和基因与模块的相关性还是有一定的线性趋势的,这说明与表型高度相关的基因,通常也是该表型对应模块内比较重要的基因。

因此,当我们要选择关键基因时,推荐选取散点图中右上角部分的基因,即两个相关性均较大的基因

> moduleGenes[(GS > 0.7 & MM > 0.7)]
[1] "TUBB6"

4.3 导出模块网络

我们可以导出这个模块的网络

TOM <- TOMsimilarityFromExpr(data.mat, power=5, networkType=type)
dimnames(TOM) <- list(colnames(data.mat), colnames(data.mat))
modTOM <- TOM[moduleGenes, moduleGenes]
cyt <- exportNetworkToCytoscape(
  modTOM,
  edgeFile = paste0(file, module, ".edges.txt"),
  nodeFile = paste0(file, module, ".nodes.txt"),
  weighted = TRUE,
  threshold = 0,
  nodeNames = moduleGenes,
  nodeAttr = module
)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容