使用seaborn进行数据可视化¶
seaborn 简介¶
Seaborn是一种基于matplotlib的图形可视化python libraty。它提供了一种高度交互式界面,便于用户能够做出各种有吸引力的统计图表。Seaborn其实是在matplotlib的基础上进行了更高级的API封装,从而使得作图更加容易,在大多数情况下使用seaborn就能做出很具有吸引力的图,而使用matplotlib就能制作具有更多特色的图。应该把Seaborn视为matplotlib的补充,而不是替代物。同时它能高度兼容numpy与pandas数据结构以及scipy与statsmodels等统计模式。掌握seaborn能很大程度帮助我们更高效的观察数据与图表,并且更加深入了解它们。
其有如下特点:
- 基于matplotlib aesthetics绘图风格,增加了一些绘图模式
- 增加调色板功能,利用色彩丰富的图像揭示您数据中的模式
- 运用数据子集绘制与比较单变量和双变量分布的功能
- 运用聚类算法可视化矩阵数据
- 灵活运用处理时间序列数据
- 利用网格建立复杂图像集
在接下来的一段时间内,我们将带大家深入地了解各类seaborn绘图函数。
使用散点图发现数据之间的关联¶
散点图是数据可视化中最常用的图像。它直观地向我们展示了数据之间的分布关系,如果数据之间存在线性或者其他相关关系,很容易通过散点图观察出来。
除了之前提到的plt.scatter()
,使用seaborn也可以绘制散点图。对用的命令是scatterplot()
There are several ways to draw a scatter plot in seaborn. The most basic, which should be used when both variables are numeric, is the scatterplot()
function. In the categorical visualization tutorial, we will see specialized tools for using scatterplots to visualize categorical data. The scatterplot()
is the default kind in relplot()
(it can also be forced by setting kind="scatter"
):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")
</div>
tips = sns.load_dataset("tips")
</div>
tips.head()
</div>
plt.scatter(tips.total_bill,tips.tip)
</div>
sns.scatterplot(tips.total_bill,tips.tip)
</div>
除此之外,我们还可以通过在 relplot()
中指定kind="scatter"
获取同样的效果。
sns.relplot(x="total_bill", y="tip", data=tips);
</div>
相比于scatterplot
,relplot
的集成度更高,在使用方法上也更为方便。例如,如果我们想给散点图加上第三个维度,就直接可以通过一个参数hue
进行传递即可。该参数将会给不同的类别赋以不同的颜色。
sns.relplot(x="total_bill", y="tip", hue="smoker", data=tips);
</div>
To emphasize the difference between the classes, and to improve accessibility, you can use a different marker style for each class:
除此之外,我们还可以继续修改散点的形状,只需要引入style
参数
sns.relplot(x="total_bill", y="tip", hue="smoker", style="size",
data=tips);
</div>
结合hue
和style
两个参数,我们就可以实现四维数据的绘图。需要注意的是,人眼的形状的敏感性不如颜色,因此该方法应该慎用,因为第四个维度不容易被观察到。
sns.relplot(x="total_bill", y="tip", hue="smoker", style="time", data=tips);
</div>
在上面的案例中,hue
参数接受到的是离散的类别数据,而如果我们给它传入一个数值形式的数据,那么relplot
将会用连续变化的色谱来区分该数据。
sns.relplot(x="total_bill", y="tip", hue="size", data=tips);
</div>
当然了,除了散点的颜色的形状,我们还可以修改散点的大小,只需要指定size
参数即可。
sns.relplot(x="total_bill", y="tip", size="size", data=tips);
</div>
下面是一个综合使用以上参数,得到的一个五维的散点图。
sns.relplot(x="total_bill", y="tip", hue='sex',
style = 'time',size="size", data=tips);
</div>
数据的汇总和不确定性的展示¶
fmri = sns.load_dataset("fmri")
fmri
</div>
sns.relplot(x="timepoint", y="signal", kind="line", data=fmri);
</div>
此外,不确定度还可以用标准差来衡量,只需要设置ci='sd'
即可
sns.relplot(x="timepoint", y="signal", kind="line", ci="sd", data=fmri);
</div>
如果我们关掉数据汇总,那么绘制出来的图像会非常奇怪。这是因为在某一个时间,会有多个测量数据。
sns.relplot(x="timepoint", y="signal", estimator=None, kind="line", data=fmri);
</div>
绘制子数据集¶
同scatterplot()
一样,我们可以通过修改hue
,style
,size
,来增加更多的绘图维度,用法也是非常一致的,这意味着我们可以非常简单地在两种方法之间进行替换。
例如,如果我们引入hue
参数,对event进行分类。即可得到如下的数据汇总图。
sns.relplot(x="timepoint", y="signal", hue="event", kind="line", data=fmri);
</div>
我们继续加入另一个style
参数,可以把region也考虑进去。
sns.relplot(x="timepoint", y="signal", hue="region", style="event",
kind="line", data=fmri);
</div>
为了突出显示,我们还可以修改线型。
sns.relplot(x="timepoint", y="signal", hue="region", style="event",
dashes=False, markers=True, kind="line", data=fmri);
</div>
下面我们考虑另一个数据集
dots = sns.load_dataset("dots").query("align == 'dots'")
dots.head()
</div>
上面的例子我们用的是离散变量作为参数hue
和chioce
的值,实际上,他们也可以使用连续变量。比如下面的例子
sns.relplot(x="time", y="firing_rate",
hue="coherence", style="choice",
kind="line", data=dots);
</div>
请注意,当style
的可能性增多时,肉眼可能不太能够区分不同的线型,因此该方法需要在变量取值范围较小的情况下使用。
palette = sns.cubehelix_palette(light=.8, n_colors=6)
sns.relplot(x="time", y="firing_rate",
hue="coherence", size="choice",
palette=palette,
kind="line", data=dots);
</div>
使用多张图展示数据之间的相互关系¶
下面我们学习如何使用多张子图,分析与展示数据之间的相关性。使用的参数主要是col
sns.relplot(x="total_bill", y="tip", hue="smoker",
col="time", data=tips);
</div>
此外,我们还可以通过row
参数,进一步地扩大子图的规模
sns.relplot(x="timepoint", y="signal", hue="subject",
col="region", row="event", height=4,
kind="line", estimator=None, data=fmri);
</div>
col_wrap
参数可以帮我们把多张子图切分为规定列数的格式。
sns.relplot(x="timepoint", y="signal", hue="event", style="event",
col="subject", col_wrap=3,
height=3, aspect=.75, linewidth=2.5,
kind="line", data=fmri.query("region == 'frontal'"));
</div>
以上的一系列可视化方法,称为小倍数绘图( “lattice” plots or “small-multiples”),在研究大规模数据集的时候尤为重要,因为使用该方法,可以把复杂的数据根据一定的规律展示出来,并且借助可视化,使人的肉眼可以识别这种规律。需要注意的是,有的时候,简单的图比复杂的图更能帮助我们发现和解决问题。
绘制离散形式的变量¶
在上一节中,我们学习了如何使用relplot()
描述数据集中多变量之间的关系,其中我们主要关心的是两个数值型变量之间的关系。本节我们进一步地,讨论离散型( categorical)变量的绘制方法。
在seaborn中,我们有很多可视化离散型随机变量的方法。类似于relplot()
之于scatterplot()
和 lineplot()
的关系, 我们有一个catplot()
方法,该方法提高了我们一个从更高层次调用各类函数的渠道,例如swarmplot()
,boxplot()
,violinplot()
等。
在详细地学习这些方法之前,对他们做一个系统地分类是非常有必要的,他们按照绘制内容可以分成如下三类:
Categorical scatterplots:
stripplot()
(with kind="strip"; the default)swarmplot()
(with kind="swarm")
Categorical distribution plots:
boxplot()
(with kind="box")violinplot()
(with kind="violin")boxenplot()
(with kind="boxen")
- Categorical estimate plots:
pointplot()
(with kind="point")barplot()
(with kind="bar")countplot()
(with kind="count")
以上三个类别代表了绘图的不同角度,在实践中,我们要根据要解决的问题,合理地选择使用其中哪种方法。如果不知道哪种方法比较好,可以都尝试一遍,选择可视化效果更好的一个。
在本教程中,我们主要使用catplot()
函数进行绘图,如前所述,该函数是基于其他许多函数基础上一个更高层次的调用渠道。因此如果有针对其中任何一种方法的疑问,都可以在对应的方法的详细介绍中找到解释。
首先,我们需要导入需要的库seaborn
和matplotlib.pyplot
import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="ticks", color_codes=True)
</div>
分类散点图¶
The default representation of the data in catplot() uses a scatterplot. There are actually two different categorical scatter plots in seaborn. They take different approaches to resolving the main challenge in representing categorical data with a scatter plot, which is that all of the points belonging to one category would fall on the same position along the axis corresponding to the categorical variable. The approach used by stripplot(), which is the default “kind” in catplot() is to adjust the positions of points on the categorical axis with a small amount of random “jitter”:
catplot()
中的默认绘图方法是scatterplot
,在catplot()
中。如果我们只有一个类别,那么散点图绘制的时候,许多数据点会重叠(overlap)在一起,数据区分度和美观性都不强。为了解决这一问题,实际上有两类绘制散点图的方法。
方法一: 我们可以考虑采用stripplot()
,该方法通过给每一个数据点一个在x
轴上的小扰动,使得数据点不会过分重叠。stripplot()
是catplot()
的默认参数。
tips = sns.load_dataset("tips")
tips.head()
</div>
sns.catplot(x="day", y="total_bill", data=tips);
</div>
我们可以吧jitter
参数关掉,观察一下效果。
sns.catplot(x="day", y="total_bill", jitter=0.2, data=tips);
</div>
方法二: 使用swarmplot()
,该方法通过特定算法将数据点在横轴上分隔开,进一步提高区分度,防止重叠。该方法对于小数据集尤其适用,调用该方法只需要在catplot()
中指定参数kind="swarm"
即可。
sns.catplot(x="day", y="total_bill", kind="swarm", data=tips);
</div>
类似于relplot()
,catplot()
也可以通过添加颜色进一步增加绘图的维度,对应的参数为hue
。需要注意的是,catplot()
暂时不支持sytle
和size
参数。
sns.catplot(x="day", y="total_bill", hue="sex",kind="swarm", data=tips);
</div>
不像数值型的数据,有的时候,我们对于离散型的类别数据很难得到一个排序的标准。当然,seaborn会尽可能地按照合理的方法给他们排序,如果需要的话也可以人为指定排序。
sns.catplot(x="size", y="total_bill", kind="swarm",
data=tips.query("size != 3"));
</div>
当我们想要指定绘制顺序的时候,可以使用order
参数。
sns.catplot(x="smoker", y="tip", order=["Yes", "No"], data=tips);
</div>
有的时候,我们想要把散点图横着画,尤其是在类别比较多的时候,这时我们可以对调x
和y
参数,达到该效果。
sns.catplot(x="day", y="total_bill", hue="time", kind="swarm", data=tips);
</div>
# sns.catplot(x="sex", y="day", hue="time", kind="swarm", data=tips);
</div>
分类分布统计图¶
如前所述,类别形式的散点图受限于数据集的大小,当数据量很大时,即使是使用swarmplot
或者stripplot
也无法使数据点分开。此时,我们考虑使用基于数据分布的绘图方法,而不再采用散点图。
Boxplots¶
基于分布的绘图方法中最简单的就是箱线图了,关于箱线图的理论已经在之前的讲义中进行了介绍,这里不再展开。箱线图的调用方法也很简单,直接kind = "box"
就行。
sns.catplot(x="day", y="total_bill", kind="box", data=tips);
</div>
当然了,我们还可以加入hue
参数,增加数据的绘图维度。
sns.catplot(x="day", y="total_bill", hue="smoker", kind="box", data=tips);
</div>
有的时候,我们想认为指定绘图的染色方法,此时可以借用dodge
参数。
tips["weekend"] = tips["day"].isin(["Sat", "Sun"])
#tips
</div>
sns.catplot(x="day", y="total_bill", hue="weekend",
kind="box", dodge=False, data=tips);
</div>
有一个和箱线图很像的图,叫boxenplot()
,它会绘制一个跟箱线图很像的图片,但是展示了更多箱线图无法展示的信息,尤其适用于数据集比较大的情况。
diamonds = sns.load_dataset("diamonds")
diamonds
</div>
diamonds = sns.load_dataset("diamonds")
sns.catplot(x="color", y="price", kind="boxen",
data=diamonds.sort_values("color"));
</div>
Violinplots¶
接下来我们看小提琴图violinplot()
,它结合了箱线图核密度估计的思想(该思想会在后面介绍)
sns.catplot(x="total_bill", y="day", hue="time",
kind="violin", data=tips);
</div>
该方法用到了kernel density estimate (KDE),进而提供了更为丰富的数据分布信息。另一方面,由于KDE的引入,该方法也有更多的参数可以修改,例如bw.
和cut
sns.catplot(x="total_bill", y="day", hue="time",
kind="violin", bw=0.5, cut=0,
data=tips);
</div>
此外,如果数据的hue
参数只有两个类别,一种非常高效的方法是给定split=True
,仅绘制具有对称性的图像的一半。
f, ax = plt.subplots(figsize=(20, 5))
a = sns.catplot(x="day", y="total_bill", hue="sex",
kind="violin", split=True, data=tips,ax = ax);
</div>
当然了,小提琴图的内部绘制也可以修改,如果我们想要展示原始数据点而不是分位数等统计数据,我们可以指定inner="stick"
,那么所有的原始数据点会被绘制在图中。
sns.catplot(x="day", y="total_bill", hue="sex",
kind="violin", inner="stick", split=True,
palette="Set1", data=tips);
</div>
我们还可以把swarmplot()
或者striplot()
放置在boxplot
或者violinplot
中,从而实现总体与局部的整体展示。
g = sns.catplot(x="day", y="total_bill", kind="violin", inner=None, data=tips)
sns.swarmplot(x="day", y="total_bill", color="k", size=3, data=tips, ax=g.ax);
</div>
g = sns.catplot(x="day", y="total_bill", kind="box",data=tips)
sns.swarmplot(x="day", y="total_bill", color="k", size=3, data=tips, ax=g.ax);
</div>
在分类数据中实现参数估计¶
在有些情况下,我们不仅想要了解数据的分布,还想要了解数据的发展趋势。seaborn提供了两种方法,分别是barplot
和pointplots
Bar plots¶
我们最常用的分类数据的可视化方式是柱状图方式,在seaborn中,barplot()
在总的数据集中选用某种估计方法进行参数的估计(默认是平均值)。 当每一个类别中有多个数据时,该方法还会使用bootstrapping绘制出均值的置信区间(通过errorbar的形式)
titanic = sns.load_dataset("titanic")
titanic.head()
</div>
titanic = sns.load_dataset("titanic")
sns.catplot(x="sex", y="survived", hue="class", kind="bar", data=titanic);
</div>
有的时候我们只是想了解一下在不同类别中数据的个数,这时候我们只需要使用简单的countplot()
即可。
sns.catplot(x="deck", kind="count", palette="rocket", data=titanic);
</div>
当然了,barplot()
和 countplot()
都可以传入其他控制参数,这里给了一个详细的示例说明。
sns.catplot(y="deck", hue="class", kind="count",
palette="pastel", edgecolor=".6",
data=titanic);
</div>
Point plots¶
另一种展示数据分布趋势的方法是折线图。该折线图会包含的要素包括:均值、置信区间以及连接不同类别的连线,从而展示数据在不同类别间的变化趋势。
sns.catplot(x="sex", y="survived", hue="class", kind="point", data=titanic);
</div>
此外,如果我们需要增强图片对于黑白打印的支持,除了指定不同hue
参数之外,我们还可以改变线条和标记的形状。
sns.catplot(x="class", y="survived", hue="sex",
palette={"male": "g", "female": "m"},
markers=["^", "o"], linestyles=["-", "--"],
kind="point", data=titanic);
</div>
处理更多种类型的数据¶
除了支持以上的传入格式外,这些函数也支持传入其他形式的数据,比如DataFrame 和 two-dimensional numpy arrays。
iris = sns.load_dataset("iris")
iris.head()
</div>
iris = sns.load_dataset("iris")
sns.catplot(data=iris, orient="h", kind="box");
</div>
sns.violinplot(x=iris.species, y=iris.sepal_length);
</div>
如果需要控制输出图片的形状和大小,可以通过figsize
参数
f, ax = plt.subplots(figsize=(10, 5))
sns.countplot(y="deck", data=titanic, color="m");
</div>
在多张图片中展示数据¶
正如之前relplot()
中提到的,如果我们希望绘制多张分类的图像,只需要通过设定row
和col
参数即可。
sns.catplot(x="time", y="total_bill", hue="smoker",
col="day", aspect=.6,
kind="swarm", data=tips);
</div>
如果我们需要进一步修改图像的其他性质,我们可以返回一个对象。
g = sns.catplot(x="fare", y="survived", row="class",
kind="box", orient="h", height=1.5, aspect=4,
data=titanic.query("fare > 0"))
g.set(xscale="log");
</div>
当我们遇到一个新的数据集的时候,往往我们首先要搞清楚的就是其中每一个变量的分布。本节我们将会给大家介绍seaborn中一些用于可视化数据分布的函数。
首先我们导入numpy
,pandas
,seaborn
,pyplot
和stats
。
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
</div>
sns.set(color_codes=True)
</div>
绘制单变量分布¶
在seaborn中,绘制单变量分布的最简单的函数是displot()
,该函数默认返回一张频率分布直方图以及其对应的核密度估计曲线(KDE)。
x = np.random.normal(size =100)
sns.distplot(x);
</div>
频率分布直方图¶
seaborn中的频率分布直方图displot()
和matplotlib中的hist()
非常相似。不过,seaborn给出了更为高层次的调用方法,我们可以通过参数kde
和rug
控制直方图中kde估计和数据点标记的展示与否。
sns.distplot(x, kde=True, rug=True);
</div>
当绘制直方图的时候,我们经常会调整的一个参数是直方的个数,控制直方个数的参数是bins
,如果不认为指定bins
的取值,seaborn会根据自己的算法得到一个较为合理的直方个数,但是通过人为调整直方个数,我们往往能发现新的规律。
sns.distplot(x, bins=5, kde=False, rug=True);
</div>
sns.distplot(x, bins=25, kde=False, rug=True);
</div>
核密度估计Kernel density estimation¶
核密度估计是一种分布的平滑(smooth)方法,所谓核密度估计,就是采用平滑的峰值函数(“核”)来拟合观察到的数据点,从而对真实的概率分布曲线进行模拟。
sns.distplot(x, hist=False, rug=True,kde = True);
</div>
那么,我们是符合得到这样一条曲线的呢? 实际上,我们将每一个数据点用一个以其为中心的高斯分布曲线代替,然后将这些高斯分布曲线叠加得到的。
x = np.random.normal(0, 1, size=30) # 生成中心在0,scale为1,30维的正态分布数据
bandwidth = 1.06 * x.std() * x.size ** (-1 / 5.) # 确定带宽
support = np.linspace(-4, 4, 200)
kernels = []
for x_i in x:
kernel = stats.norm(x_i, bandwidth).pdf(support)
kernels.append(kernel)
plt.plot(support, kernel, color="r")
sns.rugplot(x, color=".2", linewidth=3);
</div>
将每一个数据转化为以其为中心的正态分布曲线以后,将其叠加,然后归一化,即可得到最终的KDE曲线。
from scipy.integrate import trapz
density = np.sum(kernels, axis=0)
density /= trapz(density, support) # 使用梯形积分计算曲线下面积,然后归一化
plt.plot(support, density);
</div>
我们可以通过观察,发现,使用seaborn中的kdeplot()
我们会得到同样的曲线,或者使用distplot(kde = True)
也有同样的效果。
sns.kdeplot(x, shade=True);
</div>
除了核函数,另一个影响KDE的参数是带宽(h)。带宽反映了KDE曲线整体的平坦程度,也即观察到的数据点在KDE曲线形成过程中所占的比重 — 带宽越大,观察到的数据点在最终形成的曲线形状中所占比重越小,KDE整体曲线就越平坦;带宽越小,观察到的数据点在最终形成的曲线形状中所占比重越大,KDE整体曲线就越陡峭。
sns.kdeplot(x)
sns.kdeplot(x, bw=.2, label="bw: 0.2")
sns.kdeplot(x, bw=2, label="bw: 2")
plt.legend();
</div>
通过观察以上的图像我们可以发现,由于高斯分布的引入,我们往往会扩大了变量的取值范围,我们可以通过cut
参数控制最终图像距离最小值和最大值的距离。需要注意的是,cut
参数仅仅是改变了图像的展示方法,对kde的计算过程没有影响。
sns.kdeplot(x, shade=True, cut=4)
sns.rugplot(x);
</div>
参数分布的拟合¶
我们也可以使用displot()
拟合参数分布,并且将拟合结果与实际数据的分布做对比。
x = np.random.gamma(6, size=200)
sns.distplot(x, kde=False, fit=stats.gamma); # 是用gamma分布拟合,并可视化
</div>
绘制两变量之间的联合分布¶
有的时候,我们在数据分析的时候,也会关系两个变量之间的联合概率分布关系。seaborn中给我们提供了一个非常方便的jointplot()
函数可以实现该功能。
mean = [0, 1]
cov = [(1, .5), (.5, 1)]
data = np.random.multivariate_normal(mean, cov, 200)
df = pd.DataFrame(data, columns=["x", "y"])
</div>
df.head()
</div>
散点图¶
我们最熟悉的绘制联合分布的方法莫过于散点图了。jointplot()
会返回一张散点图(联合分布),并在上方和右侧展示两个变量各自的单变量分布。
sns.jointplot(x="x", y="y", data=df);
</div>
Hexbin plots¶
与一维柱状图对应的二维图像称之为Hexbin plots,该图像帮助我们统计位于每一个六边形区域的数据的个数,然后用颜色加以表示,这种方法尤其对于大规模的数据更为适用。
x, y = np.random.multivariate_normal(mean, cov, 1000).T
sns.jointplot(x=x, y=y, kind="hex", color="k");
# with sns.axes_style("white"):
# sns.jointplot(x=x, y=y, kind="hex", color="k");
</div>
该方法尤其适用于白色风格
x, y = np.random.multivariate_normal(mean, cov, 1000).T
with sns.axes_style("white"):
sns.jointplot(x=x, y=y, kind="reg", color="k");
</div>
联合分布的核密度估计¶
类似于一维情况,我们在二维平面一样可以进行核密度估计。通过设置kind = 'kde'
,我们就可以得到一个核密度估计的云图,以及两个单变量的核密度估计曲线。
sns.jointplot(x="x", y="y", data=df, kind="kde");
</div>
with sns.axes_style("white"):
sns.jointplot(x="x", y="y", data=df, kind="kde");
</div>
我们也可以直接使用kdeplot()
绘制二维平面上的核密度估计。而且,结合面向对象的方法,我们还可以把新的绘图加入到已有的图片上。
f, ax = plt.subplots(figsize=(6, 6))
sns.kdeplot(df.x, df.y, ax=ax)
sns.rugplot(df.x, color="g", ax=ax)
sns.rugplot(df.y, vertical=True, ax=ax);
</div>
If you wish to show the bivariate density more continuously, you can simply increase the number of contour levels:
f, ax = plt.subplots(figsize=(6, 6))
cmap = sns.cubehelix_palette(as_cmap=True, dark=0, light=1, reverse=True)
sns.kdeplot(df.x, df.y, cmap=cmap, n_levels=509, shade=True);
</div>
我们还可以给图片添加新的图层,将数据的散点图绘制在原图上,包括给图片添加坐标轴标签等等。
g = sns.jointplot(x="x", y="y", data=df, kind="kde", color="m")
g.plot_joint(plt.scatter, c="w", s=30, linewidth=1, marker="+")
g.ax_joint.collections[0].set_alpha(0.5)
g.set_axis_labels("$X$", "$Y$");
</div>
分组可视化¶
借助于上述的双变量分布绘图方法,我们可以绘制多变量两两之间的联合分布,seaborn中实现这个功能的函数为pairplot()
,该函数会返回一个方形的绘图窗口,在该窗口中绘制两两变量之间的关系。在对角线上,pairplot()
会展示单变量分布。
iris = sns.load_dataset("iris")
sns.pairplot(iris);
</div>
import matplotlib.pyplot as plt
import numpy as np
x = [1, 2, 3, 4, 5]
y1 = [3, 1, 5, 9, 4]
y4 = [4, 2, 1, 3, 9]
barWidth = 0.2
plt.bar(x, y1, barWidth, align= 'center', color = 'c', tick_label = ['label1','label2','labe3','label4','label5']) # c means greenish blue
plt.bar(np.array(x)+barWidth, y4, barWidth, align= 'center', bottom = np.add(y1, y4), color = 'g', tick_label = ['label1','label2','labe3','label4','label5'])
</div>
可视化线性关系¶
许多数据集都包含了众多变量,有的时候我们希望能够将其中的一个或者几个联系起来。上一节我们讲到了seaborn中很多绘制联合分布的方法,比如jointplot()
,本节我们进一步地,讨论变量之间线性关系的可视化。
需要注意的是,seaborn并不是一个统计学的库,seaborn想要实现的是:通过运用简单的统计工具,尽可能简单而直观地给我们呈现出数据之间相互关系。有的时候,对数据有一个直观的认识,能帮助我们更好地建立模型。
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
</div>
sns.set(color_codes=True)
</div>
tips = sns.load_dataset("tips")
</div>
绘制线性回归的函数¶
在seaborn中,有两个函数经常被用于实现线性回归,他们是lmplot
和regplot
。接下来我们会介绍这两个函数的异同。
在最简单的情况下,两个函数均会返回一个散点图,并给出关于的线性回归方程以及一个95%的置信区间。
sns.regplot(x="total_bill", y="tip", data=tips);
</div>
sns.lmplot(x="total_bill", y="tip", data=tips);
</div>
我们发现,除了图的尺寸,这两个图的内容是完全一致的。
那么,这两个函数有什么不同呢?
regplot()
能接受更多种形式的数据,例如numpy arrays, pandas Series, references to variables in a pandas DataFrame,而lmplot()
只能接受references to variables in a pandas DataFrame,也就是只能接受“tidy” dataregplot()
仅仅指出lmplot()
的一部分参数
我们可以对一个离散变量和一个连续变量绘制线性回归线,不过,可视化结果往往是不尽如人意的。
sns.lmplot(x="size", y="tip", data=tips);
</div>
针对上面这个图像,一个选择是给每一个离散的变量增加一个随机的扰动jitter
,使得数据的分布更容易观察,请注意,jitter
参数的存在仅仅是改变了可视化的效果,不会影响线性回归方程。
sns.lmplot(x="size", y="tip", data=tips, x_jitter=.1);
</div>
另一个选择是,我们直接将每一个离散类别中的所有数据统一处理,得到一个综合的趋势以及每个数据点对应的置信区间。
sns.lmplot(x="size", y="tip", data=tips, x_estimator=np.mean);
</div>
拟合其他形式的模型¶
简单的线性拟合非常容易操作,也很容易理解。但是真实的数据往往不一定是线性相关的,因此我们需要考虑更多的拟合方法。
我们这里使用的是 The Anscombe’s quartet dataset,在这个数据集中,不同形式的数据会得到同样的一个回归方程,但是拟合效果却是不同的。
首先我们来看第一个
anscombe = sns.load_dataset("anscombe")
</div>
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'I'"),
ci=None, scatter_kws={"s": 80});
</div>
我们接着来看第二个线性拟合,其拟合方程和第一个模型是一样的,但是显然其拟合效果并不好。
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'II'"),
ci=None, scatter_kws={"s": 80});
</div>
我们可以给lmplot()
传入一个order
参数,修改数据拟合的阶次,进而可以拟合非线性趋势。
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'II'"),
order=129, ci=None, scatter_kws={"s": 80});
</div>
接着我们来看第三个例子,在这个案例中,我们引入了一个离群点,由于离群点的存在,其拟合方程显然偏离了主要趋势。
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'III'"),
ci=None, scatter_kws={"s": 80});
</div>
此时我们可以通过引入robust
参数增强拟合的稳定性,该参数设置为True的时候,程序会自动忽略异常大的残差。
sns.lmplot(x="x", y="y", data=anscombe.query("dataset == 'III'"),
robust=True, ci=None, scatter_kws={"s": 80});
</div>
当y参数传入了二分数据的时候,线性回归也会给出结果,但是该结果往往是不可信的。
tips["big_tip"] = (tips.tip / tips.total_bill) > .15
sns.lmplot(x="total_bill", y="big_tip", data=tips,
y_jitter=.03);
</div>
可以考虑采取的一个方法是引入逻辑回归,从而回归的结果可以用于估计在给定的数据下,的概率
sns.lmplot(x="total_bill", y="big_tip", data=tips,
logistic=True, y_jitter=.03);
</div>
请注意,相比如简单的线性回归,逻辑回归以及robust regression 计算量较大,同时,置信区间的计算也会涉及到bootstrap,因此如果我们想要加快计算速度的话,可以把bootstrap关掉。
其他拟合数据的方法包括非参数拟合中的局部加权回归散点平滑法(LOWESS)。LOWESS 主要思想是取一定比例的局部数据,在这部分子集中拟合多项式回归曲线,这样我们便可以观察到数据在局部展现出来的规律和趋势。
sns.lmplot(x="total_bill", y="tip", data=tips,
lowess=True);
</div>
使用residplot()
,我们可以检测简单的线性回顾是否能够比较好地拟合原数据集。 理想情况下,简单线性回归的残差应该随机地分布在附近。
sns.residplot(x="x", y="y", data=anscombe.query("dataset == 'I'"),
scatter_kws={"s": 80});
</div>
如果出现了如下图所示的残差图,则说明线性回归的效果并不好。
sns.residplot(x="x", y="y", data=anscombe.query("dataset == 'II'"),
scatter_kws={"s": 80});
</div>
引入第三个参数¶
我们知道,线性回归可以帮助我们描述两个变量之间的关系。不过,一个跟有趣的问题是:“这两个变量之间的关系是否跟第三个因素有关呢?”
这时regplot()
和lmplot()
就有区别了。regplot()
只能展示两个变量之间的关系,而lmplot()
则能进一步地引入第三个因素(categorical variables)。
我们可以通过不同的颜色来区分不同的类别,在同一张图中绘制多个线性回归曲线:
sns.lmplot(x="total_bill", y="tip", hue="smoker", data=tips);
</div>
除了颜色之外,为了观察和打印方便,我们还可以引入不同的图形标记,区分不同的类别。
sns.lmplot(x="total_bill", y="tip", hue="smoker", data=tips,
markers=["o", "x"], palette="Set1");
</div>
To add another variable, you can draw multiple “facets” which each level of the variable appearing in the rows or columns of the grid:
如果我们想进一步地增加维度(变成四维绘图甚至五位),我们可以增加一个col
参数。
sns.lmplot(x="total_bill", y="tip", hue="smoker", col="time", data=tips);
</div>
sns.lmplot(x="total_bill", y="tip", hue="smoker",
col="time", row="sex", data=tips);
</div>
调整绘图的尺寸和形状¶
前面我们注意到了,regplot
和lmplot
做出的图像基本类似,但是在图像的尺寸和形状上有所区别。
这是因为,regplot
的绘图,是图层层面的绘图,这意味着我们可以同时对多个图层进行操作,然后对每个图层进行精细化的格式设置。为了控制图片尺寸,我们必须先生成一个固定尺寸的对象。
f, ax = plt.subplots(figsize=(5, 6))
sns.regplot(x="total_bill", y="tip", data=tips, ax=ax);
</div>
与regplot
不同的是,lmplot
是一个集成化的命令,如果我们想要修改图片的尺寸和大小,只能通过传入参数的格式进行实现,size
和aspect
分别用来控制尺寸和长宽比。
sns.lmplot(x="total_bill", y="tip", col="day", data=tips,
col_wrap=2, height=3);
</div>
sns.lmplot(x="total_bill", y="tip", col="day", data=tips,
aspect=.5);
</div>
在其他绘图中加入线性回归¶
其他的一些seaborn函数也在更高的层面上支持了线性回归的加入。例如,在我们之前讲过的jointplot
里面,我们通过给出kind = 'reg'
参数,就可以绘制出数据的线性回归。
sns.jointplot(x="total_bill", y="tip", data=tips, kind="reg");
</div>
同样的,pairplot
也是支持线性回归的加入的。
sns.pairplot(tips, x_vars=["total_bill", "size"], y_vars=["tip"],
height=5, aspect=.8, kind="reg");
</div>
进一步地,我们可以通过在pairplot
中引入hue
和kind = 'reg'
,研究更高维度数据的线性线性关系。
sns.pairplot(tips, x_vars=["total_bill", "size"], y_vars=["tip"],
hue="smoker", height=5, aspect=.8, kind="reg");
</div>
当我我们探索中等维度的数据集的时候,将数据全部展示在一张图片中已经不太现实,但是通过网格化绘图的方法,我们仍可以方便地观察数据的分布规律。前面我们已经在matplotlib中提到了网格化绘图方法subplot
,seaborn在matplotlib的基础上,进一步集成了网格化绘图的方法,使得我们能够更加方便地可视化中等维度的数据。
不过需要指出的是,为了使用seaborn的网格化绘图方法,我们的原始数据必须是Pandas DataFrame
格式的。同时,该数据也必须是“tidy” data
,换句话说,该数据的构成必须是每列代表一个特征,每行代表一个样本。不符合标准的数据是无法使用seaborn 的网格化绘图方法的。
import seaborn as sns
import matplotlib.pyplot as plt
</div>
sns.set(style="ticks")
</div>
FacetGrid 网格化绘图方法¶
FacetGrid
方法至多可以帮助我们绘制四个维度的数据,其运用的三个参数主要是hue
,col
和row
.其中hue
代表颜色,col
代表行,row
代表列。
FacetGrid
方法首先根据我们给定的hue
,col
和row
参数,初始化一个绘图网格,需要注意的是,这三个参数需要取为离散型(或者类别型)的数据,否则,绘制的图片会非常过,不美观也不利于观察。
其实,之前的relplot()
, catplot()
, 以及 lmplot()
内部都使用了这里介绍的网格化绘图方法,因此借用本节课学到的知识,也可以对这三个函数的运行结果进行修改。
tips = sns.load_dataset("tips")
</div>
Initializing the grid like this sets up the matplotlib figure and axes, but doesn’t draw anything on them.
这里的sns.FacetGrid()
的作用是初始化绘图网格,和之前提到过的plt.figure()
一样,他们只初始化,而不进行绘图。
如果我们想要绘图,需要使用FacetGrid.map()
方法,该方法中,我们需要提供的参数包括:
- 绘图变量名
- 绘图函数
例如,我们想绘制午饭和晚饭的小费分布,就可以执行如下的命令:
g = sns.FacetGrid(tips, col="time")
g.map(plt.hist, "tip");
</div>
我们可以看到,FacetGrid方法会自动给我们的坐标轴加上注释,同时加上标题。这大大节省了我们调整格式的时间。
再比如,我们想研究性别、是否吸烟、总花费与小费数的关系。就可以使用如下的代码。从中我们可以看到,关键词参数alpha = 0.7
也可以直接作用FacetGrid.map()
的输入,该参数也会传递给绘图函数,在这个问题中,绘图函数就是plt.scatter
。
g = sns.FacetGrid(tips, col="sex", hue="smoker")
g.map(plt.scatter, "total_bill", "tip", alpha=.7)
g.add_legend();
</div>
当我们绘制FacetGrid的时候,还有一些其他的参数可以调节,比如margin_titles
。
g = sns.FacetGrid(tips, row="smoker", col="time", margin_titles=True)
g.map(sns.regplot, "size", "total_bill", color=".3", fit_reg=False, x_jitter=.1);
</div>
当然了,我们也是能够调整图片的尺寸的,主要用的两个参数是height
和aspect
。
g = sns.FacetGrid(tips, col="day", height=4, aspect=.5)
g.map(sns.barplot, "sex", "total_bill");
</div>
FacetGrid中的默认绘图顺序是根据Dataframe中的信息来的。通常地,绘图会根据类别出现的先后次序绘图。当然,在需要的时候,我们可以人为给定绘图的顺序。
ordered_days = ['Sat', 'Sun', 'Thur', 'Fri']
g = sns.FacetGrid(tips, row="day", row_order=ordered_days,
height=1.7, aspect=4,)
g.map(sns.distplot, "total_bill", hist=True, rug=True);
</div>
不仅row
和col
顺序可调,hue
参数对应的颜色我们也可以自行制定,例如下面的例子:
pal = dict(Lunch="seagreen", Dinner="gray")
g = sns.FacetGrid(tips, hue="time", palette=pal, height=5)
g.map(plt.scatter, "total_bill", "tip", s=50, alpha=.7, linewidth=.5, edgecolor="white")
g.add_legend();
</div>
之前我们提到过,如果仅仅依靠颜色区别类别,在黑白打印的情况下,读者就不能提取信息了,因此我们需要引入形状参数,如果说在FacetGrid
中想要引入形状参数,可以用如下的方法。
g = sns.FacetGrid(tips, hue="sex", palette="Set1", height=5, hue_kws={"marker": ["o", "v"]})
g.map(plt.scatter, "total_bill", "tip", s=100, linewidth=.5, edgecolor="white")
g.add_legend();
</div>
有些情况下,尽管我们给row
或者col
指定的是一个类别型的数据。由于类别数过大,图像还是比较拥挤。
attend = sns.load_dataset("attention").query("subject <= 12")
g = sns.FacetGrid(attend, col="subject", height=2, ylim=(0, 10))
g.map(sns.pointplot, "solutions", "score", color=".3", ci=None);
</div>
此时,我们只需要引入一个col_wrap
参数就可以解决这个问题。请注意,如果用了col_wrap
参数,就不能用row
参数了。
attend = sns.load_dataset("attention").query("subject <= 12")
g = sns.FacetGrid(attend, col="subject", col_wrap=4,height=2, ylim=(0, 10))
g.map(sns.pointplot, "solutions", "score", color=".3", ci=None);
</div>
当然我们还可以对绘图以后的格式进行调整,使用的方法是FacetGrid.set()
方法
with sns.axes_style("white"):
g = sns.FacetGrid(tips, row="sex", col="smoker", margin_titles=True, height=2.5)
g.map(plt.scatter, "total_bill", "tip", color="#334488", edgecolor="white", lw=.5);
g.set_axis_labels("Total bill (US Dollars)", "Tip");
g.set(xticks=[10, 30, 50], yticks=[2, 6, 10]);
g.fig.subplots_adjust(wspace=.02, hspace=.02);
</div>
For even more customization, you can work directly with the underling matplotlib Figure and Axes objects, which are stored as member attributes at fig and axes (a two-dimensional array), respectively. When making a figure without row or column faceting, you can also use the ax attribute to directly access the single axes.
g = sns.FacetGrid(tips, col="smoker", margin_titles=True, height=4)
g.map(plt.scatter, "total_bill", "tip", color="#338844", edgecolor="white", s=50, lw=1)
for ax in g.axes.flat:
ax.plot((0, 50), (0, .2 * 50), c=".2", ls="--")
g.set(xlim=(0, 60), ylim=(0, 14));
</div>
Using custom functions¶
You’re not limited to existing matplotlib and seaborn functions when using FacetGrid. However, to work properly, any function you use must follow a few rules:
It must plot onto the “currently active” matplotlib Axes. This will be true of functions in the matplotlib.pyplot namespace, and you can call plt.gca to get a reference to the current Axes if you want to work directly with its methods. It must accept the data that it plots in positional arguments. Internally, FacetGrid will pass a Series of data for each of the named positional arguments passed to FacetGrid.map(). It must be able to accept color and label keyword arguments, and, ideally, it will do something useful with them. In most cases, it’s easiest to catch a generic dictionary of **kwargs and pass it along to the underlying plotting function. Let’s look at minimal example of a function you can plot with. This function will just take a single vector of data for each facet:
from scipy import stats
def quantile_plot(x, **kwargs):
qntls, xr = stats.probplot(x, fit=False)
plt.scatter(xr, qntls, **kwargs)
g = sns.FacetGrid(tips, col="sex", height=4)
g.map(quantile_plot, "total_bill");
</div>
If we want to make a bivariate plot, you should write the function so that it accepts the x-axis variable first and the y-axis variable second:
def qqplot(x, y, **kwargs):
_, xr = stats.probplot(x, fit=False)
_, yr = stats.probplot(y, fit=False)
plt.scatter(xr, yr, **kwargs)
g = sns.FacetGrid(tips, col="smoker", height=4)
g.map(qqplot, "total_bill", "tip");
</div>
Because plt.scatter accepts color and label keyword arguments and does the right thing with them, we can add a hue facet without any difficulty:
g = sns.FacetGrid(tips, hue="time", col="sex", height=4)
g.map(qqplot, "total_bill", "tip")
g.add_legend();
</div>
This approach also lets us use additional aesthetics to distinguish the levels of the hue variable, along with keyword arguments that won’t be dependent on the faceting variables:
g = sns.FacetGrid(tips, hue="time", col="sex", height=4,
hue_kws={"marker": ["s", "D"]})
g.map(qqplot, "total_bill", "tip", s=40, edgecolor="w")
g.add_legend();
</div>
Sometimes, though, you’ll want to map a function that doesn’t work the way you expect with the color and label keyword arguments. In this case, you’ll want to explicitly catch them and handle them in the logic of your custom function. For example, this approach will allow use to map plt.hexbin, which otherwise does not play well with the FacetGrid API:
def hexbin(x, y, color, **kwargs):
cmap = sns.light_palette(color, as_cmap=True)
plt.hexbin(x, y, gridsize=15, cmap=cmap, **kwargs)
with sns.axes_style("dark"):
g = sns.FacetGrid(tips, hue="time", col="time", height=4)
g.map(hexbin, "total_bill", "tip", extent=[0, 50, 0, 10]);
</div>
Plotting pairwise data relationships¶
PairGrid also allows you to quickly draw a grid of small subplots using the same plot type to visualize data in each. In a PairGrid, each row and column is assigned to a different variable, so the resulting plot shows each pairwise relationship in the dataset. This style of plot is sometimes called a “scatterplot matrix”, as this is the most common way to show each relationship, but PairGrid is not limited to scatterplots.
It’s important to understand the differences between a FacetGrid and a PairGrid. In the former, each facet shows the same relationship conditioned on different levels of other variables. In the latter, each plot shows a different relationship (although the upper and lower triangles will have mirrored plots). Using PairGrid can give you a very quick, very high-level summary of interesting relationships in your dataset.
The basic usage of the class is very similar to FacetGrid. First you initialize the grid, then you pass plotting function to a map method and it will be called on each subplot. There is also a companion function, pairplot() that trades off some flexibility for faster plotting.
iris = sns.load_dataset("iris")
g = sns.PairGrid(iris)
g.map(plt.scatter);
</div>
It’s possible to plot a different function on the diagonal to show the univariate distribution of the variable in each column. Note that the axis ticks won’t correspond to the count or density axis of this plot, though.
g = sns.PairGrid(iris)
g.map_diag(plt.hist)
g.map_offdiag(plt.scatter);
</div>
A very common way to use this plot colors the observations by a separate categorical variable. For example, the iris dataset has four measurements for each of three different species of iris flowers so you can see how they differ.
g = sns.PairGrid(iris, hue="species")
g.map_diag(plt.hist)
g.map_offdiag(plt.scatter)
g.add_legend();
</div>
By default every numeric column in the dataset is used, but you can focus on particular relationships if you want.
g = sns.PairGrid(iris, vars=["sepal_length", "sepal_width"], hue="species")
g.map(plt.scatter);
</div>
It’s also possible to use a different function in the upper and lower triangles to emphasize different aspects of the relationship.
g = sns.PairGrid(iris)
g.map_upper(plt.scatter)
g.map_lower(sns.kdeplot)
g.map_diag(sns.kdeplot, lw=3, legend=False);
</div>
The square grid with identity relationships on the diagonal is actually just a special case, and you can plot with different variables in the rows and columns.
g = sns.PairGrid(tips, y_vars=["tip"], x_vars=["total_bill", "size"], height=4)
g.map(sns.regplot, color=".3")
g.set(ylim=(-1, 11), yticks=[0, 5, 10]);
</div>
Of course, the aesthetic attributes are configurable. For instance, you can use a different palette (say, to show an ordering of the hue variable) and pass keyword arguments into the plotting functions.
g = sns.PairGrid(tips, hue="size", palette="GnBu_d")
g.map(plt.scatter, s=50, edgecolor="white")
g.add_legend();
</div>
PairGrid is flexible, but to take a quick look at a dataset, it can be easier to use pairplot(). This function uses scatterplots and histograms by default, although a few other kinds will be added (currently, you can also plot regression plots on the off-diagonals and KDEs on the diagonal).
sns.pairplot(iris, hue="species", height=2.5);
</div>
You can also control the aesthetics of the plot with keyword arguments, and it returns the PairGrid instance for further tweaking.
g = sns.pairplot(iris, hue="species", palette="Set2", diag_kind="kde", height=2.5)
</div>
画出令人赏心悦目的图形,是数据可视化的目标之一。我们知道,数据可视化可以帮助我们向观众更加直观的展示定量化的insight, 帮助我们阐述数据中蕴含的道理。除此之外,我们还希望可视化的图表能够帮助引起读者的兴趣,使其对我们的工作更感兴趣。
Matplotlib给了我们巨大的自由空间,我们可以根据自己的需要,任意调整图像的风格。然而,为了绘制一张上述的“令人赏心悦目”的图片,往往需要长期的绘图经验。这对新手来说时间成本无疑是非常高的。为此,seaborn也给我们集成好了一些设置好的绘图风格,使用这些内置风格,我们就能“傻瓜式”地获得美观的绘图风格。
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
</div>
让我们来定义一簇简单的正弦曲线,然后观察一下不同的绘图风格的区别。
def sinplot(flip=1):
x = np.linspace(0, 14, 100)
for i in range(1, 7):
plt.plot(x, np.sin(x + i * .5) * (7 - i) * flip)
</div>
这是matplotlib默认风格:
sinplot()
</div>
现在我们切换成seaborn默认风格。
sns.set()
sinplot()
</div>
(Note that in versions of seaborn prior to 0.8, set() was called on import. On later versions, it must be explicitly invoked).
Seaborn 把matplotlib中的参数分为了两类。其中第一类用来调整图片的风格(背景、线型线宽、字体、坐标轴等),第二类用来根据不同的需求微调绘图格式(图片用在论文、ppt、海报时有不同的格式需求。)
其他格式修改方法¶
Seaborn 绘图风格¶
在seaborn中,有五种预置好的绘图风格,分别是:darkgrid
, whitegrid
, dark
, white
和ticks
。其中darkgrid
是默认风格。
用户可以根据个人喜好和使用场合选择合适的风格。例如,如果图像中数据非常密集,那么使用white
风格是比较合适的,因为这样就不会有多于的元素影响原始数据的展示。再比如,如果看图的读者有读数需求的话,显然带网格的风格是比较好的,这样他们就很容易将图像中的数据读出来。
先来看whitegrid
风格
sns.set_style("whitegrid")
data = np.random.normal(size=(20, 6)) + np.arange(6) / 2
sns.boxplot(data=data);
</div>
在很多场合下(比如ppt展示时,用户不会详细读数据,而主要看趋势),用户对网格的需求是不大的,此时我们可以去掉网格。
sns.set_style("dark")
sinplot()
</div>
sns.set_style("white")
sinplot()
</div>
ticks风格介于grid风格与完全没有grid的风格之间,坐标轴上提供了刻度线。
sns.set_style("ticks")
sinplot()
</div>
移除侧边边界线¶
sinplot()
sns.despine()
</div>
当然,左侧和下方的线也是可以移除的。
sns.set_style("white")
sns.boxplot(data=data, palette="deep")
sns.despine(left=True,bottom=True)
</div>
暂时性地设置风格¶
尽管你可以在风格与风格之间通过sns.set_style("ticks")
转换风格,你也可以临时使用一次某种风格,而不影响剩余图片的风格。这种操作可以通过with
实现。
f = plt.figure()
with sns.axes_style("darkgrid"):
ax = f.add_subplot(1, 2, 1)
sinplot()
ax = f.add_subplot(1, 2, 2)
sinplot(-1)
</div>
自定义seaborn styles¶
当然了,如果这五种seaborn自带风格也不能满足你的需求,你还可以自行设置自己的风格,可以设置的参数有:
sns.axes_style()
</div>
设置的方法如下:
sns.set_style("white", {"ytick.right": True,'axes.grid':False})
sinplot()
</div>
Env Navigator项目设计
传统Winform系统的转小程序化设想
.net 混淆和反混淆工具
ObjectListView 使用技巧
Dapr资料汇总
Keycloak保护Spring Boot Restful API接口
将B站英语教学视频转成mp3和课件
Java: 如何将XML格式化
Java: 非泛型类如何设计List<T>这样的属性