Monocle2 學習筆記

參考:

  1. 第六章 scRNA-seq數據分析

  2. 使用monocle包進行pseudotime分析

  3. monocle2 官方教程

  4. 單細胞轉錄組學習筆記-18-scRNA包學習Monocle2

  5. 實驗記錄10: 用Monocle進行偽時間分析 ***

  6. scRNA-seq擬時分析 || Monocle2 踩坑教程

測試所用的數據鏈接: https://pan.baidu.com/s/1TUysqHHbXrWGi7QGt-6L1g 提取碼: uqgh

image.png

Monocle[7]是一個基于pseudotime分析的軟件包。它通過從一系列差異表達的基因來描述細胞間的距離遠近,由此得到類似樹狀的分叉圖象來表示細胞間的差異性。 提供了聚類,pseudotime, 差異分析等多種功能,該項目的網址如下

https://cole-trapnell-lab.github.io/monocle-release/

image.png

主要內容

1. 讀取數據
2.預處理過濾細胞
3. 細胞分群/聚類
? ? step1:dispersionTable()
? ? step2:plot_pc_variance_explained() 耗時
? ? step3: 進行降維和聚類
4.篩選基因(用于擬分析時序)
? ? 4.1 一般過濾標準
? ? 4.2 使用差異表達基因BEAM分析
5.推斷發育軌跡
? ? 5.1. 選合適基因
? ? 5.2 降維
? ? 5.3 細胞排序
小結:

主要介紹使用該R包進行pseudotime分析的步驟.

Tips : pseudotime分析本質上就是輸入基因及其表達量矩陣,計算擬時序的值。并且選取一些基因,用于降維可視化。聚類只是降維后顏色填充而已.

1. 讀取數據

monocle包有很多種讀取數據的方式 ,這里只展示其中三種:(I)直接讀取;(II)來源于seurat2 包; (III)來源于seurat3包。

Tips : 由于版本更替較快,seurat2 可以直接導入monocle2.但是seurat3卻要手動寫代碼導入.

library(monocle)
library(DDRTree)
library(pheatmap)
library(data.table)
library(plyr)

下面以10x 數據為例,最好通過seurat對象直接讀入monocle2 方便一些.

(1)直接讀取: 當有三個文件,expr_matrix ; sample_info ;gene_annotation.

expr_matrix <- read.delim(nUMI.summary.file)
sample_info <- data.frame(sampleID=colnames(expr_matrix))
gene_annotation <- data.frame(symbol=rownames(expr_matrix))
pd <- new("AnnotatedDataFrame", data = sample_info)
fd <- new("AnnotatedDataFrame", data = gene_annotation)
d <- newCellDataSet(as.matrix(expr_matrix), phenoData = pd, featureData = fd)

(2)來源于seurat2 包,數據如下:

image.png
## 切換到工作目錄,讀入三個文本文件.
pbmc.data <- Read10X(data.dir = ".")
pbmc <- CreateSeuratObject(counts = pbmc.data, project = "10XPBMC")
## seurat2 可以直接讀入
sce <- importCDS(pbmc)

(3) 來源于seurat3包:

差別之處,不能直接用importCDS 導入.

## 由seurat3 導入函數newimport 替代上面的importCDS功能.

newimport <- function(otherCDS, import_all = FALSE) {
  if(class(otherCDS)[1] == 'Seurat') {
    requireNamespace("Seurat")
    data <- otherCDS@assays$RNA@counts
    
    if(class(data) == "data.frame") {
      data <- as(as.matrix(data), "sparseMatrix")
    }
    
    pd <- tryCatch( {
      pd <- new("AnnotatedDataFrame", data = otherCDS@meta.data)
      pd
    },
    #warning = function(w) { },
    error = function(e) {
      pData <- data.frame(cell_id = colnames(data), row.names = colnames(data))
      pd <- new("AnnotatedDataFrame", data = pData)
      
      message("This Seurat object doesn't provide any meta data");
      pd
    })
    
    # remove filtered cells from Seurat
    if(length(setdiff(colnames(data), rownames(pd))) > 0) {
      data <- data[, rownames(pd)]
    }
    
    fData <- data.frame(gene_short_name = row.names(data), row.names = row.names(data))
    fd <- new("AnnotatedDataFrame", data = fData)
    lowerDetectionLimit <- 0
    
    if(all(data == floor(data))) {
      expressionFamily <- negbinomial.size()
    } else if(any(data < 0)){
      expressionFamily <- uninormal()
    } else {
      expressionFamily <- tobit()
    }
    
    valid_data <- data[, row.names(pd)]
    
    monocle_cds <- newCellDataSet(data,
                                  phenoData = pd,
                                  featureData = fd,
                                  lowerDetectionLimit=lowerDetectionLimit,
                                  expressionFamily=expressionFamily)
    
    if(import_all) {
      if("Monocle" %in% names(otherCDS@misc)) {
        otherCDS@misc$Monocle@auxClusteringData$seurat <- NULL
        otherCDS@misc$Monocle@auxClusteringData$scran <- NULL
        
        monocle_cds <- otherCDS@misc$Monocle
        mist_list <- otherCDS
        
      } else {
        # mist_list <- list(ident = ident)
        mist_list <- otherCDS
      }
    } else {
      mist_list <- list()
    }
    
    if(1==1) {
      var.genes <- setOrderingFilter(monocle_cds, otherCDS@assays$RNA@var.features)
      
    }
    monocle_cds@auxClusteringData$seurat <- mist_list
    
  } else if (class(otherCDS)[1] == 'SCESet') {
    requireNamespace("scater")
    
    message('Converting the exprs data in log scale back to original scale ...')
    data <- 2^otherCDS@assayData$exprs - otherCDS@logExprsOffset
    
    fd <- otherCDS@featureData
    pd <- otherCDS@phenoData
    experimentData = otherCDS@experimentData
    if("is.expr" %in% slotNames(otherCDS))
      lowerDetectionLimit <- otherCDS@is.expr
    else
      lowerDetectionLimit <- 1
    
    if(all(data == floor(data))) {
      expressionFamily <- negbinomial.size()
    } else if(any(data < 0)){
      expressionFamily <- uninormal()
    } else {
      expressionFamily <- tobit()
    }
    
    if(import_all) {
      # mist_list <- list(iotherCDS@sc3,
      #                   otherCDS@reducedDimension)
      mist_list <- otherCDS
      
    } else {
      mist_list <- list()
    }
    
    monocle_cds <- newCellDataSet(data,
                                  phenoData = pd,
                                  featureData = fd,
                                  lowerDetectionLimit=lowerDetectionLimit,
                                  expressionFamily=expressionFamily)
    # monocle_cds@auxClusteringData$sc3 <- otherCDS@sc3
    # monocle_cds@auxOrderingData$scran <- mist_list
    
    monocle_cds@auxOrderingData$scran <- mist_list
    
  } else {
    stop('the object type you want to export to is not supported yet')
  }
  
  return(monocle_cds)
}

導入monocle2

## 讀入seurat3
pbmc.data <- Read10X(data.dir = ".")
pbmc <- CreateSeuratObject(counts = pbmc.data, project = "10XPBMC")

## 讀入monocle2
sce <- newimport(pbmc)

本文采用第1種方式,讀入文件

library(Seurat)
library(monocle)

expression_matrix <- readRDS("packer_embryo_expression.rds")
cell_metadata <- readRDS("packer_embryo_colData.rds")
gene_annotation <- readRDS("packer_embryo_rowData.rds")

## 數據框需要先保存為AnnotatedDataFrame 格式,才可以創建newCellDataSet 對象
# Error: CellDataSet 'phenoData' is class 'data.frame' but should be or extend 'AnnotatedDataFrame'
pd <- new("AnnotatedDataFrame", data = cell_metadata)
fd <- new("AnnotatedDataFrame", data = gene_annotation)

sce <- newCellDataSet(expression_matrix,
                      phenoData = pd, 
                      featureData = fd,
                      lowerDetectionLimit = 0.1, 
                      expressionFamily = VGAM::negbinomial.size()) # 默認參數

查看sce 類型:

> sce
CellDataSet (storageMode: environment)
assayData: 20222 features, 6188 samples 
  element names: exprs 
protocolData: none
phenoData
  sampleNames: AAACCTGCAAGACGTG-300.1.1 AAACCTGGTGTGAATA-300.1.1 ...
    TTTGTCAAGTACACCT-b02 (6188 total)
  varLabels: cell n.umi ... State (21 total)
  varMetadata: labelDescription
featureData
  featureNames: WBGene00010957 WBGene00010958 ... WBGene00007064 (20222 total)
  fvarLabels: id gene_short_name num_cells_expressed use_for_ordering
  fvarMetadata: labelDescription
experimentData: use 'experimentData(object)'
Annotation:

查看一下數據讀入到monocle2里面,它的表達分布類型:

sce@expressionFamily

## Family:  negbinomial.size 
## 
## Negative-binomial distribution with size known
## 
## Links:    loge(mu)
## Mean:     mu
## Variance: mu * (1 + mu / size) for NB-2

? 注意到構建CDS對象過程中有一個參數是:expressionFamily ,它是選擇了一個數據分布,例如FPKM/TPM 值是log-正態分布的;UMIs和原始count值用負二項分布模擬的效果更好。負二項分布有兩種方法,這里選用了negbinomial.size,另外一種negbinomial稍微更準確一點,但速度大打折扣,它主要針對非常小的數據集,如下表:

由于輸入10x genomic 數count 類似,所以表達分布為 negbinomial.size()

Family function 數據類型 備注
negbinomial.size() 原始計數值, 比如UMIs counts Negative binomial distribution with fixed variance
negbinomial() 原始計數值, 比如UMIs counts 比negbinomial.size()精確一點, 但是非常慢.
tobit() FPKM, TPM Tobits are truncated normal distributions. 注意這點不是log-transformed FPKM/TPM
gaussianff() log-transformed FPKM/TPMs, Ct values from single-cell qPCR 符合高期分布的數值

2.預處理

和處理RNA-seq數據一樣,monocle會有一些預處理的步驟,這包括估計size factors和dispersions,以及去除一些質量比較差的細胞。

  • 第一行代碼用于評估每個細胞的sizefactor, 用于后續歸一化;

  • 第二行代碼計算基因的方差,這一步可能會提醒我們有多少的outlier的細胞。所以下一步就是要去除掉這些細胞。

cds <- estimateSizeFactors(cds)
cds <- estimateDispersions(cds)
# Removing 143 outliers

通過pDatafData兩個函數,可以查看基因和細胞的相關信息,示意如下

  • 查看細胞注釋信息:pData.
image.png
  • 查看基因注釋信息:fData
image.png

過濾細胞

在準備樣品細胞的過程中,不可避免地會引入一下死的細胞,或者說是doublets(兩個細胞被標記成一個細胞)。 這些細胞對下游分析的影響會比較大,它們很有可能會占去很大分析權重。所以我們需要把它們去除掉。 去除它們的辦法就是設置一個比較合理的表達總值的上下限,然后把處于上下限外圍的細胞都去除掉。

  • 首先我們來查看一下低表達的基因。
### detectGenes計算每一個細胞表達的基因個數.
sce <- detectGenes(sce, min_expr = 0.1)
### 保留在10個細胞中表達的基因,當做表達基因.
expressed_genes <- row.names(subset(fData(sce),
                                    num_cells_expressed >= 10))

我們將表達值在10以上的基因保存為expressed_genes。下面的步驟會用到保存好的表達的基因。

  • 接下來我們要去除掉死細胞或者doublets了。

    head(pData(sce))
    

一般的,我們是沒有數據告訴我們哪些細胞是死的或者空的,哪些細胞是doublets的。但是通常而方言,我們都會拿到RPC(reads per cell)的計數,我們可以人為的設定一下表達總值的上下限。

pData(sce)$Total_mRNAs <- Matrix::colSums(exprs(sce))
sce <- sce[, pData(sce)$Total_mRNAs < 1e6 ]

##設置上下限:
upper_bound <- 10^(mean(log10(pData(sce)$Total_mRNAs)) +
                     2*sd(log10(pData(sce)$Total_mRNAs)))
lower_bound <- 10^(mean(log10(pData(sce)$Total_mRNAs)) -
                     2*sd(log10(pData(sce)$Total_mRNAs)))

## 可視化
qplot(Total_mRNAs, data = pData(sce), geom = "density") +
  geom_vline(xintercept = lower_bound) +
  geom_vline(xintercept = upper_bound) + xlim(0, 6000)+
  theme_classic()

可視化RPC(reads per cell) 分布

image.png

過濾不合格細胞

## 保留下通過標準的細胞
sce <- sce[,pData(sce)$Total_mRNAs > lower_bound &
             pData(sce)$Total_mRNAs < upper_bound]

## 新的sce 數據,評估細胞表達基因數.
sce <- detectGenes(sce, min_expr = 0.1)

> sce
CellDataSet (storageMode: environment)
assayData: 20222 features, 5943 samples 
  element names: exprs 
protocolData: none
phenoData
  sampleNames: AAACCTGCAAGACGTG-300.1.1 AAACCTGGTGTGAATA-300.1.1
    ... TTTGTCAAGTACACCT-b02 (5943 total)
  varLabels: cell n.umi ... Total_mRNAs (20 total)
  varMetadata: labelDescription
featureData
  featureNames: WBGene00010957 WBGene00010958 ... WBGene00007064
    (20222 total)
  fvarLabels: id gene_short_name num_cells_expressed
  fvarMetadata: labelDescription
experimentData: use 'experimentData(object)'
Annotation: 

細胞數目有6188 變成現在的5943個.

檢查過濾細胞的效果,過濾后的值基本上是符合lognormal分布的。

# Log-transform each value in the expression matrix.
L <- log(exprs(sce[expressed_genes,]))

# Standardize each gene, so that they are all on the same scale,
# Then melt the data with plyr so we can plot it easily
melted_dens_df <- melt(Matrix::t(scale(Matrix::t(L))))

# Plot the distribution of the standardized gene expression values.
qplot(value, geom = "density", data = melted_dens_df) +
  stat_function(fun = dnorm, size = 0.5, color = 'red') +
  xlab("Standardized log(counts)") +
  ylab("Density")

理想的圖:

image.png

得到過濾后的矩陣,可以分別進行聚類和擬時序分析.

3. 細胞分群/聚類

不使用marker 基因的細胞分類方法

使用函數clusterCells(),根據整體的表達量對細胞進行分組。例如,細胞表達了大量的與成肌細胞相關的基因,但就是沒有成肌細胞的marker--MYF5 ,我們依然可以判斷這個細胞屬于成肌細胞。

step1:dispersionTable()

首先就是判斷使用哪些基因進行細胞分群。當然,可以使用全部基因,但這會摻雜很多表達量不高而檢測不出來的基因,反而會增加噪音。挑有差異的,挑表達量不太低的

disp_table <- dispersionTable(sce)
unsup_clustering_genes <- subset(disp_table, 
                                 mean_expression >= 0.1) ## 數值因實驗而不同
sce <- setOrderingFilter(sce, unsup_clustering_genes$gene_id) 
plot_ordering_genes(sce)
image.png

其中, setOrderingFilter函數設置了將來會用于分群的細胞。這一步對于分群非常關鍵,所以可以試著使用不同的方法來過濾基因,以期達到滿意的效果。

plot_ordering_genes根據這些基因的平均表達水平展示了基因的表達差異程度,紅線表示Monocle基于這種關系對分散的期望。 上圖中實黑的為會使用的基因,灰色的是過濾掉的基因。

step2:plot_pc_variance_explained() 耗時

然后選一下主成分

plot_pc_variance_explained(cds, return_all = F,max_components = 100) # norm_method='log'
image.png

上圖與Seurat中使用到的PCElbowPlot類似,我們可以看到大約多少的PC將會被我們使用。 可以看到大約在15的時候就已經到的平原區了。下面試著分群。

step3: 進行降維和聚類

根據上面??的圖,選擇合適的主成分數量(這個很主觀,可以多試幾次),這里選前15個成分(需要注意的 使用主成分會影響后面結果)

關于處理批次效應:例如在芯片數據中經常會利用SVA的combat函數。
磨平批次效應實際上就是去掉各個組的前幾個主成分

# 進行降維 ,設置降維維度15.
cds <- reduceDimension(sce, max_components = 2, norm_method = 'log',
                       num_dim = 15, reduction_method = "tSNE", verbose = TRUE)
# Remove noise by PCA ...
# Reduce dimension by tSNE ...



### 如果需要去除批次效應(batch 和 幾群細胞基因表達量數目)
# cds <- reduceDimension(cds, max_components = 2, num_dim = 6,
#                          reduction_method = 'tSNE',
#                          residualModelFormulaStr = "~batch + #num_genes_expressed",
#                          verbose = T)



# 進行聚類 (指定cluster為4的時候,只能畫出來3個,為什么?)
cds <- clusterCells(cds, num_clusters = 4) 
# Distance cutoff calculated to 4.826512 

## 查看聚類效果
head(cds@phenoData@data)


# 查看tSNE圖
plot_cell_clusters(cds,x=1,y=2, color_by = "Cluster")

# 畫具體基因的表達分布圖
#plot_cell_clusters(HSMM, 1, 2, color = "Cluster",
# +                    markers = c("CDC20", "GABPB2"))
image.png

4.篩選基因(用于擬分析時序)

4.1 一般過濾標準

篩選基因沒有一個固定標準,可以根據自己的需要靈活選擇,這里只展示其中1種 .

disp_table <- dispersionTable(sce)
unsup_clustering_genes <- subset(disp_table, mean_expression >= 0.1)

## 得到用于擬時序分析基因
ordering_genes <- row.names(unsup_clustering_genes)
image.png

4.2 使用差異表達基因

對于差異表達分析,可以像RNA-seq一樣,直接到不同屬性的樣品進行分析

fullModelFormulaStr : 分組進行差異分析的變量.

我們可以看到,只要有不同的pData屬性,我們都可以做差異表達分析。比如State:

## 差異分析非常耗時,運行時間在幾分鐘至十幾分鐘不等
diff_test_res <- differentialGeneTest(sce[expressed_genes, ],
                                      fullModelFormulaStr = "Cluster")
sig_genes <- subset(diff_test_res, qval < 0.1)
sig_genes[1:6,c("gene_short_name", "pval", "qval")]

## 得到用于擬時序分析基因
ordering_genes <- row.names(sig_genes)

下面是可選部分??

再比如 Pseudotime

diff_test_res <- differentialGeneTest(sce[expressed_genes, ],
                                      fullModelFormulaStr = "~sm.ns(Pseudotime)")
sig_genes <- subset(diff_test_res, qval < 0.1)
sig_genes[1:6,c("gene_short_name", "pval", "qval")]

作圖(注意要將基因名變成character)

cg=as.character(head(sig_genes$gene_short_name))
# 普通圖
plot_genes_jitter(sce[cg,], 
                  grouping = "State", ncol= 2)
# 還能上色
plot_genes_jitter(sce[cg,],
                  grouping = "State",
                  color_by = "State",
                  nrow= 3,
                  ncol = NULL )

還可以繪制熱圖

sig_genes <- sig_genes[order(sig_genes$qval), ]
plot_pseudotime_heatmap(sce[row.names(sig_genes)[1:20],],
                num_clusters = 3,
                cores = 1,
                show_rownames = T)

得到類似圖

image.png

BEAM分析

BEAM分析的目的是比較分枝點與分枝末端的差異。

BEAM_res <- BEAM(sce[expressed_genes, ], branch_point = 1, cores = 4)
BEAM_res <- BEAM_res[order(BEAM_res$qval),]
BEAM_res <- BEAM_res[,c("gene_short_name", "pval", "qval")]
plot_genes_branched_heatmap(sce[row.names(subset(BEAM_res,
                                          qval < 1e-4)),],
                                          branch_point = 1,
                                          num_clusters = 4,
                                          cores = 1,
                                          use_gene_short_name = T,
                                          show_rownames = T)
image.png

可以針對基因做圖

plot_genes_branched_pseudotime(sce[rownames(subset(BEAM_res, qval < 1e-8)), ], 
                                 branch_point = 1, color_by = "orig.ident", 
                                 ncol = 3)
image.png

上面是可選部分 ??.

5.推斷發育軌跡

用上面過濾的基因進行降維, 以方便下一步將細胞分布到不同的狀態之間,可視化擬時序得分.

5.1: choosing genes that define progress

5.2: reducing the dimensionality of the data

5.3: ordering the cells in pseudotime

5.1. 選合適基因

得到上面兩種過濾的基因集:unsup_clustering_genes 或者 diff_test_res,選取合適基因進行排序.

## 以差異基因為例:
## ordering_genes <- row.names (subset(diff_test_res, qval < 0.01))
cds <- setOrderingFilter(cds, ordering_genes)
plot_ordering_genes(cds)
image.png

5.2 降維

默認使用DDRTree的方法進行降維分析,代碼如下

cds <- reduceDimension(
  cds,
  max_components = 2,
  method = 'DDRTree')

5.3 細胞排序

然后就是細胞排序 ,代碼如下:

cds <- orderCells(cds)

運行之后,對于每個細胞,會給出一個pseudotime值,示意如下

image.png

可視化代碼:

  • 默認用"state" 填充。應該是cluster 含義.
plot_cell_trajectory(sce)
image.png
  • 降維之后,在二維空間展示細胞pseudotime的分布,可以看到是一個樹狀結構,除了上述方法外,還可以根據pseudotime的值給細胞賦顏色,代碼如下
plot_cell_trajectory(sce, color_by = "Pseudotime")
image.png

其實就是將fData中對應的列設置為顏色,如果想要觀察不同細胞亞型的分布,可以在fData中新增一列細胞對應的cluster ID, 然后用這一類來設置顏色。

對于pseudotime分析,我們需要明白它的基本輸入就是一張基因在細胞中表達量的表格,與細胞的聚類結果無關,只不過在可視化的時候根據聚類的結果填充了顏色而已。

另外,plot_genes_in_pseudotime 可以對基因在不同細胞中的表達量變化進行繪圖

plot_genes_in_pseudotime(cds[cg,],
                         color_by = "State")

小結:

【Workflow以及與Seurat的異同】

  • ①創建CellDataSet對象(下簡稱CDS對象)
    若要創建新的CDS對象,則需要整理出3個輸入文件(基因-細胞表達矩陣、細胞-細胞特征注釋矩陣、基因-基因特征注釋矩陣),但方便的是,Monocle支持從Seurat中直接導入對象,通過importCDS命令實現。
    在創建之后,也會進行數據過濾和標準化,不同的是Seurat是基于作圖的方法去篩選掉異常的細胞,而Monocle的過濾方法則是根據表達數據的數學關系來實現。
    上限:
    image.png

下限:
image.png

其中X為基因表達總數, n 為細胞數,sd為表達水平方差
大概就是以一個大致的細胞表達水平為基準,表達量太高的跟太低的去除掉。

  • ②通過已知的Marker基因分類細胞
    方法一:通過一些現有的生物/醫學知識手動賦予它們細胞名,將這些細胞分為幾大類,然后再聚類細胞。
    方法二:與Seurat包的分類細胞方法類似,篩選出差異表達基因用于聚類,然后再根據每一個cluster的marker賦予細胞名。

  • ③聚類細胞
    采用的也是t-SNE算法。

  • ④將細胞按照偽時間的順序排列在軌跡上

    • Step1:選擇輸入基因用于機器學習
      這個過程稱為feature selection(特征選擇),這些基因對軌跡的形狀有著最重要的影響。我們應該要選擇的是最能反映細胞狀態的基因。
      如果直接通過Seurat輸出的一些重要的基因(比如每個cluster中的高FC值基因)作為輸入對象的話就能夠實現一個“無監督”分析。或者也可以利用生物學知識手動選擇一些重要的基因進行“半監督”分析。

    • Step2:數據降維
      利用Reversed graph embedding算法將數據降維。
      相對于PCA來說這個算法更能夠反應數據的內部結構(據monocle網站說是這樣)

    • Step3:將細胞按照偽時間排序
      這個過程稱為manifold learning(流形學習)。Monocle利用軌跡來描述細胞如何從一個狀態轉換到另一個狀態。得到的是一個樹狀圖,樹的根部或樹干表示的是細胞最初的狀態(如果有的話),隨著細胞的變化就沿著分叉樹枝發展。一個細胞的偽時間值(pseudotime value)為它的位置沿著樹枝到根部的距離。

分析scRNA-seq的數據的關鍵在于如何對細胞進行cluster。這其中有很多的算法,而之后的降維分析比如tSNE其實主要還是為了數據圖形化顯示方便。在細胞分群之后,差異表達分析其實與第三章的RNA-seq并無二致,我們只需要對需要比較的因素做到心中有數即可。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容