初识Prophet模型(二)-- 应用篇

相关学习: 初识Prophet模型(一)--理论篇

7、Prophet 模型应用

7.0 背景描述

  • 该案例使用的是wiki网站日访问量(数值经过log处理)的csv数据文件
  • 描述的是美国著名橄榄球四分卫的维基页面浏览量,他是美国球员,一年里的周期规律会起很大作用,而一周里的周期规律也很明显。

7.1 导入数据

import pandas as pd
import numpy as np
from fbprophet import Prophet
import matplotlib.pyplot as plt
%matplotlib inline
df = pd.read_csv('data.csv')
df.head()
df.dtypes #检查下df的数据类型
df['ds'] = df['ds'].apply(pd.to_datetime)# ds列必须是pandas的datetime数据类型,使用pandas自带的pd.to_datetime将日期转为datetime类型
plt.rcParams['figure.figsize']=(20,10)
plt.style.use('ggplot')
df.set_index('ds').y.plot()

7.2 拟合模型

model = Prophet(daily_seasonality=True)
model.fit(df)
<fbprophet.forecaster.Prophet at 0x10715a0b8>

7.3 预测(使用默认参数)

  • 生成一个未来的日期的dataframe,然后用训练好的模型prophet来predict。
future = model.make_future_dataframe(periods=730)
future.tail()
  • 有了未来的日期,就可以使用学习到的趋势来预测未来日期的走势。

  • 预测的结果包括如下变量

'ds', 'trend', 'yhat_lower', 'yhat_upper', 'trend_lower', 'trend_upper',
       'additive_terms', 'additive_terms_lower', 'additive_terms_upper',
       'weekly', 'weekly_lower', 'weekly_upper', 'yearly', 'yearly_lower',
       'yearly_upper', 'multiplicative_terms', 'multiplicative_terms_lower',
       'multiplicative_terms_upper', 'yhat'
  • 我们只用 'ds', 'yhat', 'yhat_lower', 'yhat_upper'
forecast=model.predict(future)
forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].head()
print(fig1)

成分分析**

趋势是由不同的成分组成,比如总趋势、年、季节、月、周等等,我们要将这些成分从趋势中抽取出来看看不同成分的趋势情况

  • 预测的结果包括如下变量
'ds', 'trend', 'yhat_lower', 'yhat_upper', 'trend_lower', 'trend_upper',
       'additive_terms', 'additive_terms_lower', 'additive_terms_upper',
       'weekly', 'weekly_lower', 'weekly_upper', 'yearly', 'yearly_lower',
       'yearly_upper', 'multiplicative_terms', 'multiplicative_terms_lower',
       'multiplicative_terms_upper', 'yhat'
  • 下面图1是根据trend画出来的,图2是根据weekly画出来的,图3是根据yearly画出来的,图4是根据daily画出来的

  • 因为是加法模型,所以:

    • forecast['additive_terms'] = forecast['daily']+forecast['weekly'] + forecast['yearly'];
    • forecast['yhat'] = forecast['trend'] + forecast['additive_terms']
    • forecast['yhat'] = forecast['trend'] +forecast['daily']+ forecast['weekly'] + forecast['yearly']。
  • 如果有节假日因素,那么就会有:

    • forecast['yhat'] = forecast['trend'] +forecast['daily']+forecast['weekly'] + forecast['yearly'] + forecast['holidays']。
  • 对于那些是节假日的天数, forecast['holidays']才会有值

  • 不是节假日的天数,forecast['holidays']为0

  • 因为是加法模型,'multiplicative_terms', 'multiplicative_terms_lower', 'multiplicative_terms_upper'这3列为空。

因此,在下面的拆解图中,weekly中的Monday为0.3的意思就是,在trend的基础上,加0.3;Saturday为-0.3的意思就是,在trend的基础上,减0.3。因此,这条线的高低也在一定程度上反应了“销量的趋势“。

fig2 = model.plot_components(forecast)
print(fig2)
  • 第一幅趋势图里,可以看到按页面浏览量的总趋势。是因为作者最近退休,所以浏览量逐渐下降。
  • 第二幅按周的周期规律图里能看出,在比赛当天和赛后(周日和周一)访问量明显较高。
  • 美国NFL橄榄球比赛主要集中在九月到次年1月初,和二月初的超级碗。这也反映在第三幅图中,按年的周期规律图。
forecast_df = forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']]
df = pd.merge(df, forecast_df, on='ds', how='right')
df.set_index('ds').plot(figsize=(16,8), color=['royalblue', "green", "pink", "yellow"], grid=True);
x1 = forecast['ds']
y1 = forecast['yhat']
y2 = forecast['yhat_lower']
y3 = forecast['yhat_upper']
plt.plot(x1,y1)
plt.plot(x1,y2)
plt.plot(x1,y3)
plt.show()
print(forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())

7.4 趋势突变点

自动检测变化点

  • 步骤:
    • 1、通过大量的速率改变的点检测变化点
    • 2、对这些点做稀疏先验
  • 默认情况下会检测出25个变化点,这些点均匀的分布在前80%的时间序列中
fig = model.plot(forecast)
for cp in model.changepoints:
    plt.axvline(cp, c='pink', ls='--', lw=2)
  • 因为稀疏先验,大部分的变化点并没有用到,看看每个变化点的速率变化图:
deltas = model.params['delta'].mean(0)
fig = plt.figure(facecolor='w', figsize=(10, 6))
ax = fig.add_subplot(111)
ax.bar(range(len(deltas)), deltas, facecolor='#0072B2', edgecolor='#0072B2')
ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2)
ax.set_ylabel('Rate change')
ax.set_xlabel('Potential changepoint')
fig.tight_layout()
  • 变化点的数量可以通过参数n_changepoints指定,但最好还是通过调整正则化来修改
  • 看看比较明显的变化点
from fbprophet.plot import add_changepoints_to_plot
fig = model.plot(forecast)
a = add_changepoints_to_plot(fig.gca(), model, forecast) #虚线处为给定时间序列中的变点

调整趋势灵活性

  • 当趋势出现过拟合或者欠拟合的情况下,可以通过参数changepoint_prior_scale调整稀疏先验的程度,默认为0.05
  • 该参数值越大,则趋势越灵活
增大灵活性
m = Prophet(changepoint_prior_scale=0.5)
forecast = m.fit(df).predict(future)
fig = m.plot(forecast)
减少灵活性
forecast = m.fit(df).predict(future)
fig = m.plot(forecast)

指定变化点的位置

  • 通过参数changepoints手动指定位置,只有指定的这些点可以有速率变化
m = Prophet(changepoints=['2014-01-01'])
forecast = m.fit(df).predict(future)
fig = m.plot(forecast)

7.5季节性、假期效应和回归因子

假期和特殊事件建模

  • 要给假期或者其他重复事件建模,就需要创建一个包含holiday和ds列的DataFrame
  • 需要包含过去和将来所有的特殊日子,如果这些特殊日子没有出现在将来(要预测的日期),那预测就不会用到
  • 可以在这个数据框基础上再新建两列 lower_window 和 upper_window ,从而将节假日的时间扩展成一个区间 [ lower_window , upper_window ] ,例如:
    • 如果想将平安夜也加入到 “圣诞节” 里,就设置 lower_window = -1 , upper_window = 0 ;
    • 如果想将黑色星期五加入到 “感恩节” 里,就设置 lower_window = 0 , upper_window = 1
playoffs = pd.DataFrame({
  'holiday': 'playoff',
  'ds': pd.to_datetime(['2008-01-13', '2009-01-03', '2010-01-16',
                        '2010-01-24', '2010-02-07', '2011-01-08',
                        '2013-01-12', '2014-01-12', '2014-01-19',
                        '2014-02-02', '2015-01-11', '2016-01-17',
                        '2016-01-24', '2016-02-07']),
  'lower_window': 0,
  'upper_window': 1,
})
superbowls = pd.DataFrame({
  'holiday': 'superbowl',
  'ds': pd.to_datetime(['2010-02-07', '2014-02-02', '2016-02-07']),
  'lower_window': 0,
  'upper_window': 1,
})
holidays = pd.concat((playoffs, superbowls))

上面superbowl的日期也包含在playoff的日期中,也就是superbowl日期的影响会有个叠加效应

m = Prophet(holidays=holidays)
forecast = m.fit(df).predict(future)
  • 可以通过forecast看看假期效应
forecast[(forecast['playoff'] + forecast['superbowl']).abs() > 0][['ds', 'playoff', 'superbowl']][-15:]
  • 看看假期效应在图上的显示,playoff日期有高峰,superbowl日期有更明显的高峰
fig = m.plot_components(forecast)
  • 对假期单独画图
from fbprophet.plot import plot_forecast_component
plot_forecast_component(m, forecast, 'superbowl')

内置假期

  • 可以通过add_country_holidays使用内置假期
  • 通过模型的train_holiday_names方法查看哪些假期
m = Prophet(holidays=holidays)
m.add_country_holidays(country_name='CN')
m.fit(df)

m.train_holiday_names
INFO:fbprophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.

0                 playoff
1               superbowl
2          New Year's Day
3        Chinese New Year
4       Tomb-Sweeping Day
5               Labor Day
6    Dragon Boat Festival
7     Mid-Autumn Festival
8            National Day
dtype: object
m = Prophet(holidays=holidays)
m.add_country_holidays(country_name='US')
m.fit(df)

forecast = m.predict(future)
fig = m.plot_components(forecast)

季节性的傅里叶级数

  • 年度季节性的傅里叶级数默认是10
from fbprophet.plot import plot_yearly
m = Prophet().fit(df)
a = plot_yearly(m)
  • 默认值大多数是没问题的,但是季节性可能有更高频率的变化,并且通常没有这么平滑,这时可以增加这个值
  • 增加这个值也可能导致过拟合,这里增加到20
from fbprophet.plot import plot_yearly
m = Prophet(yearly_seasonality=20).fit(df)
a = plot_yearly(m)

自定义季节性

  • 时间序列超过两个周期时,Prophet默认训练星期和年的季节性

  • 在sub-daily的时间序列时,会训练每天的季节性

  • 可以使用函数add_seasonality添加小时/月/季度等其他季节性

  • 函数add_seasonality的参数:

    • name 哪种周期
    • period 季节性的周期
    • fourier_order 季节性的傅里叶级数
    • prior_scale 可选参数
    • 默认情况下,周的季节性傅里叶级数为3,年的季节性傅里叶级数为10
  • 这里将每周的季节性替换为每月的季节性(period=30.5)

m = Prophet(weekly_seasonality=False)
m.add_seasonality(name='monthly', period=30.5, fourier_order=5)
forecast = m.fit(df).predict(future)
fig = m.plot_components(forecast)

依赖于其他因素的季节性

  • 有时候季节性依赖于其他一些因素,比如每周的季节性在夏天和其他季节表现不一致,每天的季节性在周末和周内表现不一致,这种季节性可以使用带条件的季节性训练模型

  • 在一年中默认每周的季节性表现是一致的,但是可能希望每周的季节性在淡季和旺季(每周末有比赛)表现不一致

  • 可以使用带条件的季节性为淡季和旺季单独构建每周的季节性

这里先增加一列布尔类型的数据,来表示日期在淡季还是旺季:

def is_nfl_season(ds):
    date = pd.to_datetime(ds)
    return (date.month > 8 or date.month < 2)

df['on_season'] = df['ds'].apply(is_nfl_season)
df['off_season'] = ~df['ds'].apply(is_nfl_season)
  • 接着禁用内置的每周季节性,使用淡季的周季节性和旺季的周季节性代替

  • 因此,只有condition_name列为True的时候季节性才有日期

  • 在预测的DataFrame上,也要做同样的操作

m = Prophet(weekly_seasonality=False)
m.add_seasonality(name='weekly_on_season', period=7, fourier_order=3, condition_name='on_season')
m.add_seasonality(name='weekly_off_season', period=7, fourier_order=3, condition_name='off_season')

future['on_season'] = future['ds'].apply(is_nfl_season)
future['off_season'] = ~future['ds'].apply(is_nfl_season)
forecast = m.fit(df).predict(future)
fig = m.plot_components(forecast)

从图中可以看到,在旺季的时候每周末都会打球,周日和周一都有大幅度增长,但在淡季则完全没有。

假期和季节性的prior scale

  • 如果发现假期过拟合,可以设置参数holidays_prior_scale调整假期的prior scale使之平滑
  • 这个参数默认是10,减少可以限制假期效果
m = Prophet(holidays=holidays, holidays_prior_scale=0.05).fit(df)
forecast = m.predict(future)
forecast[(forecast['playoff'] + forecast['superbowl']).abs() > 0][['ds', 'playoff', 'superbowl']][-10:]

可以看到,比起之前假期效应被减弱了,特别是在观看最少的superbowls上

可以用下面的方式设置每周季节性的prior_scale

额外的回归特征

  • 可以使用函数add_regressor将其他回归特征添加到模型的线性部分
  • 训练和预测的数据集上都需要包含这些回归特征的值

下面,为NFL赛季的每周日添加这样一个回归特征,再画图看看这个特征的效果

def nfl_sunday(ds):
    date = pd.to_datetime(ds)
    if date.weekday() == 6 and (date.month > 8 or date.month < 2):
        return 1
    else:
        return 0
df['nfl_sunday'] = df['ds'].apply(nfl_sunday)

m = Prophet()
m.add_regressor('nfl_sunday')
m.fit(df)

future['nfl_sunday'] = future['ds'].apply(nfl_sunday)

forecast = m.predict(future)
fig = m.plot_components(forecast)
  • 也可以使用前面说过的holidays的接口,通过创建一个过去和未来的这些周日的list来处理NFL赛季周日的这种情况
  • 函数add_regressor为定义额外的线性回归提供了一个更加通用的接口

7.6 模型诊断(内置方法)

  • Prophet有交叉验证功能,具体做法是通过在历史数据中选择一些截断点,对于这些截断点,只使用这些点之前的数据来拟合模型,然后比较真实值和预测值

下面模型使用前五年的数据训练,预测后一年的数据

m = Prophet()
m.fit(df)
future = m.make_future_dataframe(periods=366)

from fbprophet.diagnostics import cross_validation

df_cv = cross_validation(m, '365 days', initial='1825 days', period='365 days')
cutoff = df_cv['cutoff'].unique()[0]
df_cv = df_cv[df_cv['cutoff'].values == cutoff]

fig = plt.figure(facecolor='w', figsize=(10, 6))
ax = fig.add_subplot(111)
ax.plot(m.history['ds'].values, m.history['y'], 'k.')
ax.plot(df_cv['ds'].values, df_cv['yhat'], ls='-', c='#0072B2')
ax.fill_between(df_cv['ds'].values, df_cv['yhat_lower'],
                df_cv['yhat_upper'], color='#0072B2',
                alpha=0.2)
ax.axvline(x=pd.to_datetime(cutoff), c='gray', lw=4, alpha=0.5)
ax.set_ylabel('y')
ax.set_xlabel('ds')
ax.text(x=pd.to_datetime('2010-01-01'),y=12, s='Initial', color='black',
       fontsize=16, fontweight='bold', alpha=0.8)
ax.text(x=pd.to_datetime('2012-08-01'),y=12, s='Cutoff', color='black',
       fontsize=16, fontweight='bold', alpha=0.8)
ax.axvline(x=pd.to_datetime(cutoff) + pd.Timedelta('365 days'), c='gray', lw=4,
           alpha=0.5, ls='--')
ax.text(x=pd.to_datetime('2013-01-01'),y=6, s='Horizon', color='black',
       fontsize=16, fontweight='bold', alpha=0.8);
  • 可以使用函数cross_validation给这些历史截断点自动完成交叉验证,参数如下:
    • horizon:代表每次从cutoff往后预测多少天
    • initial :一开始的时间是多少
    • period :代表每隔多长时间设置一个cutoff
    • 默认情况下,period是horizon的三倍,并且每隔半个horizon设置一个截断点
  • 交叉验证的输出是一个DataFrame,包含真实的y和预测的yhat,可以用来评判效果

下面的交叉验证,horizon=365天,initial=730天,period=180天,在八年的时间序列中,等于有11((365*8-730-365)/180)个总的预测

from fbprophet.diagnostics import cross_validation
df_cv = cross_validation(m, initial='730 days', period='180 days', horizon = '365 days')
df_cv.head()
  • 函数performance_metrics可以用来评判模型效果,计算得到:
    • 统计信息包括均方误差(mean squared error, MSE)
    • 均方根误差(root mean squared error, RMSE)
    • 平均绝对误差(mean absolute error, MAE)
    • 平均绝对误差(mean absolute percent error, MAPE)
    • yhat_lower和yhat_upper估计的覆盖率
from fbprophet.diagnostics import performance_metrics
df_p = performance_metrics(df_cv)
df_p.head()
  • 可以使用plot_cross_validation_metric来可视化这些指标,查看mape指标下的可视化,可以看到对未来一个月的预测有5%的误差,一年后的预测误差增加到了11%
from fbprophet.plot import plot_cross_validation_metric
fig = plot_cross_validation_metric(df_cv, metric='mape')

7.7 模型评估

  • 通过历史数据对已知的最后一年数据进行预测并评估模型
prediction_size = 365
train_df = df[:-prediction_size]
train_df.tail()
model2 = Prophet(daily_seasonality=True)
model2.fit(train_df)
future2 = model2.make_future_dataframe(periods=365)
forecast2 = model2.predict(future2)

model2 .plot(forecast2);
  • 定义一个辅助函数,用于从原数据集df中获取实际值y,然后和forecast对象中的预测值比较
def make_comparison_dataframe(historical, forecast):
    return forecast.set_index('ds')[['yhat', 'yhat_lower', 'yhat_upper']].join(historical.set_index('ds'))
cmp_df = make_comparison_dataframe(df, forecast2)
cmp_df.tail()
  • 预测评价指标:

    • MAE:平均绝对误差,
      M A E=\frac{1}{n} \sum_{i=1}^{n}\left|\hat{y}_{i}-y_{i}\right|

    范围[0,+∞),当预测值与真实值完全吻合时等于0,即完美模型;误差越大,该值越大。

    • MAPE:平均绝对百分比误差,

      M A P E=\frac{100 \%}{n} \sum_{i=1}^{n}\left|\frac{\hat{y}_{i}-y_{i}}{y_{i}}\right|

      范围[0,+∞),MAPE 为0%表示完美模型,MAPE 大于 100 %则表示劣质模型。

  • 定义计算MAPE和MAE的辅助函数

def calculate_forecast_errors(df, prediction_size):
    df = df.copy()
    df['e'] = df['y'] - df['yhat']
    df['p'] = 1* df['e'] / df['y']
    predicted_part = df[-prediction_size:]
    error_mean = lambda error_name: np.mean(np.abs(predicted_part[error_name]))
    return {'MAPE': error_mean('p'), 'MAE': error_mean('e')}
  • 计算MAPE和MAE
for err_name, err_value in calculate_forecast_errors(cmp_df, prediction_size).items():
    print(err_name, err_value)
MAPE 0.053184142465032766
MAE 0.4132303661978998

Box-Cox变换

  • 定义计算函数
def inverse_boxcox(y, lambda_):
    return np.exp(y) if lambda_ == 0 else np.exp(np.log(lambda_ * y + 1) / lambda_)
  • 准备数据,设置索引
train_df2 = train_df.copy().set_index('ds')
  • 应用Box-Cox变换。这里它将返回两个值,第一个值是转换后的序列,第二个值是找到的最优λ值(最大似然)
from scipy import stats
import statsmodels.api as sm
train_df2['y'], lambda_prophet = stats.boxcox(train_df2['y'])
train_df2.reset_index(inplace=True)
  • 创建一个新Prophet模型,并重复之前的拟合-预测流程
model3  = Prophet(daily_seasonality=True)
model3 .fit(train_df2)
future3 = model3.make_future_dataframe(periods=prediction_size)
forecast3 = model3.predict(future3)
  • 通过逆函数和已知的λ值反转Box-Cox变换
for column in ['yhat']:
    forecast3[column] = inverse_boxcox(forecast3[column],lambda_prophet)
  • 计算MAPE和MAE
cmp_df2 = make_comparison_dataframe(df, forecast3)
for err_name, err_value in calculate_forecast_errors(cmp_df2, prediction_size).items():
     print(err_name, err_value)
MAPE 0.04373071028220759
MAE 0.34260353853143777

对最后一年的真实值与预测值进行可视化对比

test_df = df[-prediction_size:]
test_df = test_df.set_index('ds')
forecast2 = forecast2[['ds','yhat']].set_index('ds')
df_all = forecast2.join(test_df).dropna()
df_all.head()
df_all.plot()
plt.rcParams['figure.figsize']=(30,20)
plt.style.use('ggplot')
plt.legend(['true', 'yhat'])
plt.show()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342