Python sklearn实例:预测波士顿房价

下面使用前面学到的回归分析的知识,借助机器学习框架 sklearn,实现波士顿房价的预测。

这个案例使用的数据集(Boston House Price Dataset)源自 20 世纪 70 年代中期美国人口普查局收集的美国马萨诸塞州波士顿住房价格有关信息。该数据集统计了当地城镇人均犯罪率、城镇非零售业务比例等共计 13 个指标(特征),第 14 个特征(相当于标签信息)给出了住房的中位数报价。

现在我们的任务是,找到这些指标(特征)与房价(目标)之间的关系。由于房价是连续变化的实数,很明显,这个任务属于回归分析。该数据集在卡耐基梅隆大学统计与数据科学实验室或 Kaggle 等网站均可下载。下载后,需要删除部分额外的数据描述信息,并将文件另存为 CSV 格式,然后利用之前介绍的 Pandas 来读取数据。

在诸如 sklearn 这样的机器学习框架中,有一个便利之处:它内置了很多经典的数据集,一旦安装了 sklearn,无须另外下载,只要调用专门的 API 函数即可导入数据,甚是方便。下面将 Jupyter 作为代码运行的平台,逐步加载数据并进行必要的回归分析。

利用sklearn加载数据

sklearn 中内置了多种数据集,其中一种就是自带的小数据集(packaged dataset)。在成功安装 sklearn 后,只需调用对应的数据导入方法,即可完成数据的加载。这些数据导入方法的命名规则是 sklearn.datasets.load_<name>。这里的 <name> 就是对应的数据集名称。常见的数据集如表 1 所示。

<caption>表 1:sklearn 中常见的数据集</caption>
| 导入数据的函数名称 | 对应的数据集 |
| load_boston() | 波士顿房价数据集 |
| load_breast_cancer() | 乳腺癌数据集 |
| load_iris() | 鸢尾花数据集 |
| load_diabetes() | 糖尿病数据集 |
| load_digits() | 手写数字数据集 |
| load_linnerud() | 体能训练数据集 |
| load_wine() | 红酒品类数据集 |

通过表 1 可知,load_boston( ) 就是专门用于导入波士顿房价数据集的。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [1]:
from sklearn.datasets import load_boston
boston = load_boston()</pre>

需要注意的是,在上述代码中,当我们要导入某个方法时,不需要添加方法后面的那对圆括号。变量名 boston 实际上是一个字典类型的对象,我们可以用它的 keys( ) 方法输出它所包含的属性值。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [2]: boston.keys()
Out[2]:dict_keys(['data', 'target', 'feature_names', 'DESCR', 'filename'])</pre>

在 sklearn 框架中,所有内置的数据集(如表 1 所示)都有这 5 个属性值。它们所代表的含义分别如下:

  • 首先,data 并不泛指数据,而是特指除标签之外的特征数据,针对波士顿房价数据集,它指的是前面的13个特征;
  • 相对而言,target 的本意是“目标”,这里是指标签(label)数据。针对波士顿房价数据集,就是指房价;
  • 属性值 feature_names 给出的实际上就是 data 对应的各个特征的名称。对于波士顿房价数据集而言,它指的就是影响房价的 13 个特征的名称;
  • 属性值 DESCR 其实是英文单词“description”的简写。顾名思义,它是对当前数据集的详细描述,有点类似于数据集的说明文档。比如,这个数据从哪里来,它有什么特征,每个特征是什么数据类型,如果引用数据集该引用哪些论文,等等;
  • 最后一个属性值就是 filename,它说明的是这个数据集的名称,以及在当前计算机中的存储路径。

我们可分别尝试输出这 5 个属性值,感性理解它们的含义。首先,我们输出 data。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [3]: boston.data
Out[3]:
array([[6.3200e-03, 1.8000e+01, 2.3100e+00, ..., 1.5300e+01, 3.9690e+02, 4.9800e+00] , [2.7310e-02, 0.0000e+00, 7.0700e+00, .…, 1.7800e+01, 3.9690e+02, 9.1400e+00],
...,(省略大部分数据)
[4.7410e-02, 0.0000e+00, 1.1930e+01, .…, 2.1000e+01, 3.9690e+02, 7.8800e+00]])</pre>

从输出结果可以看出,boston.data 输出的是除 target 之外的所有特征数据,由于难以显示完全,所以 sklearn 会省略大部分数据。从输出样式可以看出,data 是用一个二维数组存储的。如果想输出第 0 条记录(从 0 开始计数,下同),那么就可以用 boston.data[0] 来获取。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [4]: boston.data[0]
Out[4]:array([6.320e-03, 1.800e+01, 2.310e+00, 0.000e+00, 5.380e-01, 6.575e+00, 6.520e+01, 4.090e+00, 1.000e+00, 2.960e+02, 1.530e+01, 3.969e+02, 4.980e+00])</pre>

很显然,上面一条完整的记录中包括 13 个特征,它是一维数组。所以,如果我们想接着输出第 0 条记录的第 2 个特征,就可以用 boston.data[0][2] 实现。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [5]: boston.data[0][2]
Out[5]: 2.31</pre>

当然,如果你对 Python 语法比较熟悉,上述指令还有简单写法。
https://docs.qq.com/pdf/DR1doYmNBYUZ3RVNX

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [6]: boston.data[0,2]
Out[6]: 2.31</pre>

如果我们想知道 data 中一共有多少条数据,每条数据中有多少个特征,可利用 shape 属性获得。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [7]: boston.data.shape
Out[7]:(506, 13)</pre>

该属性返回行和列的数量,从输出结果可以看出,共有 506 条数据,每条数据共有 13 个特征。由于 shape 属性输出的是一个包括两个元素的元组,所以如果仅仅想知道有多少条记录,可以如下操作:

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [8]: boston.data.shape[0]
Out[8]: 506</pre>

类似地,如果我们仅仅想获取数据集共有多少个已知特征,也可以如下操作:

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [9]: boston.data.shape[1]
Out[9]: 13</pre>

下面,我们再输出 target(目标),看看它是什么样的数据,类似于 boston.data 的形式,用 boston.target 即可实现。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [10] : boston.tArget
Out[10]:
array([24., 21.6, 34.7, 33.4, 36.2, 28.7, 22.9, 27.1, 16.5, 18.9, 15., 18.9, 21.7, 20.4, 18.2, 19.9, 23.1, 17.5, 20.2, 18.2, 13.6, 19.6,
,(手动省略大部分数据)
20.6, 21.2, 19.1, 20.6, 15.2, 7., 8.1, 13.6, 20.1, 21.8, 24.5, 23.1, 19.7, 18.3, 21.2, 17.5, 16.8, 22.4, 20.6, 23.9, 22., 11.9])</pre>

我们很容易猜到,上面输出的每条数据对应的 target(房价)共有 506 个数据,因为每一组特征都对应一个目标变量(房价)。当然,我们也可以用 shape 属性来验证我们的猜想。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [11]: boston.target.shape
Out[11]: (506,)</pre>

从输出来看,这是一个包含一个元素的元组。因为输出是一个元组对象,因此,即使内部只有一个元素,在通过 target 获取共有多少条数据时,也需要中规中矩地通过访问元组元素的语法实现,即通过元组的方括号和下标 0 来获取。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [12]: boston.target.shape[0]
Out[12]: 506</pre>

接下来,如果我们想知道这 13 个特征分别是什么意思,就可以借助 feature_names 来输出各个特征的名称。通常来说,sklearn 中都有良好的命名规则,能够“见名知意”,因此,这个关键字能在一定程度上帮助我们理解数据。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [13]: boston.feature_names
Out[13]: array(['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT'], dtype='<U7')</pre>

从输出结果看,我们可以得到波士顿房价数据集中 13 个特征的简写。如果对这些缩写并不了然,还是一头雾水,该如何是好呢?这时,属性值 DESCR 就起作用了,它会清楚“描述”这个数据集的详细信息。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [14]: print(boston.DESCR)
Out[14]:
.._boston_dataset:

Boston house prices dataset

Data Set Characteristics:
:Number of Instances: 506
:Number of Attributes: 13 numeric/categorical predictive. Median Value (attribute 14) is usually the target.
:Attribute Information (in order):
-CRIM per capita crime rate by town
-ZN proportion of residential land zoned for lots over 25,000 sq.ft.
-INDUS proportion of non-retail business acres per town
...,(手动省略部分输出)
-B 1000(Bk - 0.63)^2 where Bk is the proportion of blacks by town

  • LSTAT % lower status of the population
    -MEDV Median value of owner-occupied homes in $1000 ' s</pre>

这里需要注意的是,在 Jupyter 代码块中,我们使用的是 print(boston.DESCR)。在这里,print( ) 函数通常不可少,如果少了会发生什么呢?请读者自行尝试,并思考原因。

为了方便读者理解,这里给出缩写特征的中文描述,如表 2 所示。

<caption>表 2:波士顿房价数据集缩写特征的中文描述</caption>
| 名称 | 中文描述 |
| CRIM |

住房所在城镇的人均犯罪率

|
| ZN | 住房用地超过 25000 平方尺的比例 |
| INDUS | 住房所在城镇非零售商用土地的比例 |
| CHAS | 有关查理斯河的虚拟变量(如果住房位于河边则为1,否则为0 ) |
| NOX | 一氧化氮浓度 |
| RM | 每处住房的平均房间数 |
| AGE | 建于 1940 年之前的业主自住房比例 |
| DIS | 住房距离波士顿五大中心区域的加权距离 |
| RAD |

距离住房最近的公路入口编号

|
|

TAX

| 每 10000 美元的全额财产税金额 |
| PTRATIO |

住房所在城镇的师生比例

|
| B | 1000(Bk-0.63)^2,其中 Bk 指代城镇中黑人的比例 |
| LSTAT | 弱势群体人口所占比例 |
| MEDV | 业主自住房的中位数房价(以千美元计) |

注:1平方尺≈0.093平方米。

boston.DESCR 的输出中不仅包括 data 的 13 个特征描述,还包括第 14 个特征,即自住房屋的中位数房价,它作为目标变量(target)—我们使用前面的 13 个特征作为解释变量,建立一个回归模型,对其进行预测。

boston 对象的最后一个属性值是 filename,我们也可以用类似的方法将其输出。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [15]: boston.filename
Out[15]: 'C:\Users\Yuhong\Anaconda3\lib \site-packages \sklearn \datasets \data\boston_house_prices.csv'</pre>

上述输出结果基于 Windows 系统(Anaconda 安装的路径不同,会有不同的输出)。输出结果为数据集的名称及所处的路径。知道了这些信息,我们就可以“按图索骥”地找到这个数据集,再也不用担心从哪里下载了。

需要注意的是,在 Windows 系统中,子目录之间的分隔符是两个反斜杠\\,其中第一个反斜杠\是转义字符。如果在 macOS、Linux 系统中,则不存在这个转义字符的困扰,因为它的子目录分隔符是 /,输出结果如下所示:

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">'/anaconda3/lib/python3.6/site-packages/sklearn/datasets/date/boston_house_p rices.csv'</pre>

到现在为止,我们对如何利用 sklearn 加载内置数据已有所了解。接下来,结合 Pandas 来处理数据。

利用Pandas处理数据

如前介绍,boston.data 是仅包含特征信息的 NumPy 数组,它可直接作为 Pandas 的数据源。在使用 Pandas 之前,需要加载这个第三方工具包。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [16]:
01 import pandas as pd
02 bos = pd.DataFrame(boston.data)
03 bos.head () #验证语句,非必需</pre>

程序执行结果为:

pandas程序执行结果

简单解释一下上述代码。第 01 行的作用是加载 Pandas。第 02 行的作用是将由 sklearn 读取的特征数据作为 Pandas 的数据源,并将返回结果赋值给 bos(这是一个 DataFrame 对象)。第 03 行代码输出数据集的前 5 行,Pandas 中的 head( ) 函数默认返回前 5 行数据。

从上面的运行结果可以看出,Pandas 并没有输出每列的特征名称,13 个特征名称是用阿拉伯数字 0~12 来表征的,这样的数字编号让用户难以理解各个特征的含义。这时,我们可以给 Pandas 的 columns 属性赋值,手动添加特征名称,代码如下所示:

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [17]:
01 bos.columns = boston.feature_names #手动添加特征名称
02 bos.head()</pre>

程序执行结果为:

程序执行结果

从上面的运行结果可以看出,Pandas 的各个列已成功拥有了特征名称,可读性增强了。

在前面的讨论中,我们已经提到,sklearn 是把数据集的特征和标签分开存储的。如果我们希望将这两类“合二为一”该怎么办呢?这在 Pandas 中是很容易做到的,增加一列存储标签信息即可。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [18]:
01 bos['PRICE' ] = boston.target #为 DmtmFrAme 增加一列 PRICE
02 bos.head () #显示前 5 行</pre>

程序执行结果为:

程序执行结果

从上面的运行结果可以看到,DataFrame 中的确增加了一个新列 PRICE,它的值就代表波士顿的房价信息。

分割数据集

如前所述,通常我们至少要把整个数据集分割为两部分:训练集和测试集(在模型的初步阶段,验证集不是必需的)。训练集用于训练,测试集用于测试。为了保证数据分割的随机性和专业性,sklearn 提供了专门的分割函数 train_test_split()。

前面为了回顾 Pandas 的使用方法,我们利用 Pandas 把数据集的 data 和 target 合二为一了。这是因为,sklearn 之外的数据集,其特征数据和标签数据通常是共存在一个文件中的,这是一种更普遍的状态。

为了处理方便,train_test_split() 要求特征数据和标签数据必须是分开的。那怎么能把一个原本完整的数据分开呢?如果我们利用 Pandas,可通过 drop( ) 方法实现。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [19]:
01 X = bos.drop('PRICE', axis = 1) #把名为 PRICE 的列删除,将剩余特征赋值给 X
02 y = bos ['PRICE'] #把名为 PRICE 的列赋值给 y</pre>

在上述代码中,第 01 行的 drop( ) 函数“指名道姓”地删除了名为 PRICE 的数据,为了准确起见,还指定了删除数据所处的坐标轴(axis),axis 值为 1,表示删除 PRICE 所在的列。

另外,在 sklearn 中还常有不成文的约定:通常用大写的 X 表示特征(这里共有 13 个),而用小写的 y 表示预测的目标(标签,这里有 1 个)。如果仅仅操作 sklearn 自带的数据集,那么我们无须这么折腾,上述代码可以简写为如下形式:

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">01 X = boston.data
02 y = boston.target</pre>

下面,我们就利用函数 train_test_split( ) 分别把 X 和 y 分割为两个测试集和训练集。由于 X 和 y 都被分割为两个部分,因此需要四个变量分别来接收它们。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [20]:
01 from sklearn.model_selection import train_test_split
02 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size =0.3, random_state = 0)</pre>

上述代码的第 01 行导入了分割训练集与测试集的函数 train_test_split( )。第 02 行表示实施分割操作,将 X(特征)和 y(标签)分割为两个部分,其中测试集占 30%(第 3 个参数的值可以自定义,默认值为 0.25)。第 4 个参数表示配合随机抽取数据的种子。有意思的是,设置这个随机状态(random_state)是为了保证数据集的“不随机”。

为什么这么说呢?因为设置 random_state 的目的就是确保每次运行分割程序时,获得完全一样的训练集和测试集。否则,同样的算法模型在不同的训练集和测试集上的效果不一样。如果每次都随机抽样,那么在确定模型和初始参数后,你会发现,模型每运行一次,就会得到不同的预测准确率(因为模型性能通常都对训练集敏感),从而使得调参无法有效进行。

你可以这样理解,每个随机状态(random_state,即某个整数值)都代表一批不同的训练集和测试集。如果它的值不变,无论程序运行多少次,获取的都是固定的一批训练集和测试集,这种稳定性为我们进行模型调参提供了方便。

一旦模型调参完毕,这个值就不需要设置了。如果不设置这个值,就会启用它的默认值 None。一旦这个值被设置为 None,就启用 np.random 作为随机种子,即默认以系统时间为随机种子。我们知道,时光荏苒,每时每刻的系统时间都不同,反而让样本的抽取更趋近随机抽样状态。

如前所述,train_test_split( ) 函数同时返回四个值,分别赋给 X_train、X_test、y_train、y_test,这四个变量的名称自然可以不同,但它们的逻辑顺序一定要正确,它们依次为训练集的特征数据、测试集的特征数据、训练集的标签数据、测试集的标签数据。

https://docs.qq.com/pdf/DR1doYmNBYUZ3RVNX

在分割数据后,可以利用 shape 属性来验证训练集和测试集的尺寸。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [21]:
01 print(X_train.shape)
02 print(X_test.shape)
03 print(y_train.shape)
04 print(y_test.shape)
Out[21]:
(354, 13)
(152, 13)
(354,)
(152,)</pre>

导入线性回归模型

在数据分割完成之后,就可以依次导入线性回归模型,训练模型并进行模型预测了。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [22]:
01 from sklearn.linear model import LinearRegression #导入模型
02 LR = LinearRegression() #生成模型
03 LR.fit(X_train, y_train) #训练模型
04 y_pred = LR.predict(X_test) #模型预测</pre>

在上述代码中,第 01 行的作用是导入线性回归模型,它是由 sklearn 提供的,无须我们自己编写。第 02 行创建了一个线性回归模型实例 LR。第 03 行用于在训练集上拟合数据,在 sklearn 中,训练模型的方法统称为 fit( )。由于回归分析属于监督学习,所以 fit( ) 函数提供两个参数,前者是特征数据,后者是标签数据。第 04 行的作用就是在测试集上实施模型预测。

查看线性回归模型的系数

获得线性回归模型的核心,就是找到关键参数:各个特征的权值(包括截距)。它们是支撑模型的关键,我们可以很容易地利用如下两行代码输出这些关键参数。事实上,输出这些参数并不是必需的,这么做仅仅是为了加深读者对线性回归模型的理解。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [23]:
01 print("w0 = ", LR.intercept ) #输出截距
02 print ("W = ", LR. coef_) #输出每个特征的权值
Out[23]:
w0 = 37.93710774183255
W = [-1.21310401e-01 4.44664254e-02 1.13416945e-02 2.51124642e+00 -1.62312529e+01 3.85906801e+00 -9.98516565e-03 -1.50026956e+00 2.42143466e-01 -1.10716124e-02 -1.01775264e+00 6.81446545e-03 -4.86738066e-01]</pre>

我们知道,波士顿房价数据集中共计 13 个特征,所以至少有 13 个权值,外加一个截距,即应拟合出 14 个权值。从上面的代码输出结果可以看出,权值数量上符合预期。有了上述 14 个参数,利用之前讲过的公式,我们就很容易把线性回归模型建立起来。一旦模型建立好,用这个模型来预测新样本(如测试集数据)就水到渠成了。

NumPy 输出的数组中包含 13 个权值,默认的输出格式为科学计数法。对普通人而言,这样的数组可能不太适合观察。事实上,我们可以通过设置 NumPy 的输出参数来改变输出格式,代码如下所示。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [24]:
01 np.set_printoptions(precision = 3, suppress = True)
02 print('w0 = {0:.3f}'.format(LR.intercept_))
03 print('W = {}'.format(LR.coef_))
Out[24]:
w0 = 37.937
W = [-0.121 0.044 0.011 2.511 -16.231 3.859 -0.01 -1.5 0.242 -0.011 -1.018 0.007 -0.487]</pre>

在上述代码中,第 01 行表示设置参数,仅对第 03 行的代码有效。第02行用于为一个普通浮点数设置显示精度,保留3位小数。在第 01 行中,对于 set_printoptions( ) 函数的两个参数,precision 用来控制小数点后面最多显示的位数,suppress=True 用来取消使用科学计数法。

从输出结果可以看出,截距(代码中的 w0)的作用主要是平移线性回归模型的超平面,它对特征选择的指导意义并不大。但权值则不同,它们对于特征选择具有很强的指导意义。通常来说,权值的符号为-,表明它和目标(如房价)是负相关的,针对波士顿房价数据集,这样的权值会抑制房价。负值的绝对值越大,对房价的抑制程度就越大。反之,如果权值的符号为+,则表明它与房价呈正相关,其值越大,表明它对房价的提升效果越好。

我们尝试解释代码中若干特征的权值。权值 w1=-0.121,对比表 2 可知,CRIM 表示的是住房所在城镇的人均犯罪率。这个很容易理解,犯罪率越高的地区,周边的房价越低,于是权值和目标呈现负相关。

随后的三个权值都是正值(分别是 0.044、0.011 和 2.511),说明它们多少都能提升房价,以此类推便可以解读全部权值与目标的关系。其中,抑制房价最明显的是特征 NOX,它表示一氧化氮的浓度,权值(抑制因子)达到 -16.231。基于常识可知,一氧化氮浓度越高,说明住房所在地的环境污染越严重。不难理解,谁也不想在一个污染严重的地方“安居置业”。

对房价提升最明显的特征是 RM,权值为 3.859,查表 2 可知,RM 指的是每处住房的平均房间数量。这也是很容易理解的,房间越多,通常来说房屋总面积就越大,而面积越大,总房价就高,这也在情理之中。

通过前面的描述,可以看出,作为数据分析工程师,我们可以构建出模型,拟合出参数,但要想对数据进行解读,还需要领域背景知识,否则就容易贻笑大方。

绘制预测结果

由于可视化能给我们带来最直观的认知,所以下面将通过可视化的方法,来展示回归模型预测的效果。通过以下代码,可以得到针对波士顿房价数据集,预测房价和实际房价之间的对比图。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [25]:
01 import matplotlib.pyplot as plt
02 import numpy as np
03
04 plt.scatter(y_test, y_pred)
05 plt.xlabel("Price: Y_i”)
06 plt.ylabel("Predicted prices: \hat{Y}_i")
07 plt.title("Prices vs Predicted prices: Y_i vs \hat{Y}_i")
08 plt.grid()
09
10 x = np.arange(0, 50)
11 y = x
12 plt.plot(x, y, color = 'red', lw = 4)
13 plt.text(30,40, "predict line")
14 plt.savefig ("price.eps")</pre>

程序执行结果为:

预测房价和实际房价的对比图

预测房价和实际房价的对比图

在代码层面,有两点需要说明:Matplotlib 允许添加包含 LaTeX 公式的标签;除了可在屏幕上显示图片,我们还可以利用 savefig 把图片保存为 .eps 格式,这个格式是利用 LaTeX 撰写学术论文时常用的矢量图格式。

在代码功能实现层面,我们知道,如果预测房价和实际房价一致的话,那么所有的数据点都应该汇集在 y=x 这条线上,但这并不是现实,于是可以看到,除了少数点,大部分点散落在 y=x 附近,大趋势说明预测的结果还不错。

除了可以利用常规的 Matplotlib 绘制图形,还可以利用前面学习的 Seaborn 绘制更加“炫丽”的线性回归模型趋势图,这时就要用到 lmplot( ) 方法了。该方法用以绘制回归趋势图,描述线性关系,拟合数据集回归模型。hue、col、row 参数可用来控制绘图变量,代码如下:

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [26]:
01 import numpy as np
02 import seaborn as sns
03 import matplotlib.pyplot as plt
04 #构造 DataFrame 数据集
05 data = pd.concat([pd.Series(y_test.values), pd.Series(y_pred)], axis = 1)
06 data, columns =['实际房价','预测房价']
07
08 #解决中文显示问题
09 plt.rcParams['font.sans-serif'] = ['SimHei']
10 sns.Implot (x = '实际房价', y = '预测房价', data = data)
11 plt.show()</pre>

程序执行结果为:

利用Seaborn绘制的线性回归模型趋势图

利用 Seaborn 绘制的线性回归模型趋势图

简单解释一下上述代码,由于 lmplot( ) 方法中需要导入一个 DataFrame 对象作为 data 的参数,而 y_test 和 y_pred 数据类型不统一(一个为 Series 对象,一个为 Array 对象),所以我们要先提取它们的值(value),然后将它们拼接为一个 DataFrame 对象,接着将这两列数据分别赋值给 lmplot( ) 方法中的 x 和 y,分别表示 X 轴和 Y 轴数据。

https://docs.qq.com/pdf/DR1doYmNBYUZ3RVNX

值得一提的是,这里有一个隐含的参数 ci(取值范围为 0~100),表示拟合曲线的置信区间。在默认情况下,lmplot( ) 方法返回的是一个散点图、线性回归曲线、95% 置信区间的组合图。当然,如果我们调整置信区间的大小,如设置 ci=60,则表示置信区间为 60%。

预测的结果到底怎样,光靠感性的目测认知是不够的。下面就用 sklearn 提供的评估函数来实际测量一下。

预测效果的评估

由于回归分析的目标值是连续值,因此我们不能用准确率之类的评估标准来衡量模型的好坏,而应该比较预测值(Predict)和实际值(Actual)之间的差异程度。其中,均方根误差(root-mean-square error,简称 RMSE)是最常见的评估标准之一。


image

在使用 sklearn 的评估函数之前,需要先导入这些性能评估模型。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [27]:
01 from sklearn import metrics
02 mse = metrics.mean_squared_error(y_test, y_pred)
03 print(mse)
Out[26]:
27.19596576688338</pre>

在测试集上,如果我们想查看线性回归模型输出的预测房价和实际房价之间的对比情况,利用Pandas很容易实现。

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [28]: df = pd.DataFrame ({'实际房价': y_test, '预测房价': y_pred})
In [29]: df</pre>

程序执行结果为:

image

需要说明的是,本节中的案例是基于 Jupyter 平台演示的,前面每节的代码都是按 Ctrl+Enter 组合键执行过的。也就是说,执行过的变量或函数均已被加载到内存之中,代码从前到后,环环相扣。读者朋友们不可孤立看待某节的代码,否则将无法理解后面代码运行的结果。

为了帮助读者熟悉 sklearn 的使用方法,我们比较详尽地介绍了与线性回归没有太大关联的知识。在后面的实战中,这部分知识不会重复解释。抛开这些,我们发现使用 sklearn 使得代码非常简单,逻辑非常清晰。抽丝剥茧,我们给出简单版的例 1。

【例 1】利用 sklearn 预测波士顿房价(boston-housing-regression.py)

<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">01 #(1)导入数据
02 from sklearn.datasets import load_boston
03 boston = load_boston()
04 # (2)分割数据
05 from sklearn.model_selection import train_test_split
06 X_train, X_test, y_train, y_test = train_test_split( boston.data, boston.target, test_size = 0.3, random_state = 0)
07 # (3)导入线性回归模型并训练模型
08 from sklearn.linear_model import LinearRegression
09 LR = LinearRegression()
10 LR.fit(X_train, y_train)
11 # (4)在测试集上预测
12 y_pred = LR.predict(X_test)
13 # (5)评估模型
14 from sklearn import metrics
15 mse = metrics.mean_squared_error(y_test, y_pred)
16 print("MSE = ", mse) #性能评估:模型的均方差</pre>

程序执行结果为:

MSE = 27.195965766883234

从上面的代码可以看出,利用 sklearn 来做回归分析,除了注释部分,核心代码只有十几行,可谓“言简意赅”,这就是利用机器学习框架的好处!

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

推荐阅读更多精彩内容