数据清洗及特征处理
通常原数据都是不干净的,可能存在异常值,缺失值以及其他问题。所以一般进行数据分析之前都需要先对数据进行清洗。
读个文件先
#加载所需的库
import numpy as np
import pandas as pd
#加载数据train.csv
df = pd.read_csv('train.csv')
缺失值观察与处理
缺失值,可能是人为失误或者机器失误造成部分数据值的空缺,如果不对这些空缺的地方进行操作,极大可能会影响我们后续分析或建模得到的结果。
首先是观察缺失值,观察缺失值的方法可以直接通过info()
来查看,info()
可以观察到行数、各列列名、每列非缺失值数、数据类型以及数据总量
df.info()
#运行结果
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
可以看到总共891行,但是Age、Cabin和Embarked的非缺失值数都没有达到891,说明其中都含有缺失值,但是这样子看不够直观,而且不能快速的看出各列一共有多少给缺失值,通过isnull()
和sum()
组合可以查看每列的缺失值个数。
df.isnull()
df.isnull().sum()
#运行结果
PassengerId 0
Survived 0
Pclass 0
Name 0
Sex 0
Age 177
SibSp 0
Parch 0
Ticket 0
Fare 0
Cabin 687
Embarked 2
dtype: int64
如果再组合一个sum()
就可以直接得出缺失值的总和
df.isnull().sum().sum()
#运行结果
866
isnull()
是元素级别的判断,把对应的所有元素的位置都列出来,元素为空或者为Nan就显示True,否则就是False。pandas中会将NaN和None都处理为np.nan
。
None
None
为python自带的,为None Type
类型,代表空类型,并不能参与运算
df[df['Age']==None]
#结果显示为空,未检测到有空值
np.nan
numpy中的isnan对应的是NaN值,代表“不是数字”,数值类型为float,数组中显示为nan,能参与运算,但结果显示为NaN。
df[df['Age']==np.nan]
#结果显示依旧为空,这里并不是真的不存在空值!
注意:这里卡了我好久,后来在论坛上找了下,找到了原因。计算机中本没有绝对相等的数据,所谓的相等只是精度允许的条件下相等!np.nan原意为not a number,所以,对于某值是否为nan进行判断,只能使用np.isnan(i),绝不能用i == np.nan来判断,因为nan具有不等于自身的属性!
参考资料:https://blog.csdn.net/craftsman2020/article/details/107739405
isnull()
这里顺带一提,numpy用isnan()
检查是否存在NaN,pandas用isna()
或者isnull()
检查是否存在NaN。pandas它是构建在numpy之上的,在numpy中,即没有na
也没有null
,而只有NaN
(“Not A Number”),因此,pandas也沿用NaN值,所以造成了isna和isnull两个名称不同但功能相同的情况。
在不同的应用场景中,对缺失值的处理都不一样,大致可分为:
- 删除元组
- 数据补齐
- 不处理
删除元组就算将存在遗漏信息属性值的对象(元组,记录)删除,从而得到一个完备的信息表,以减少历史数据来换取信息的完备,但可能会对数据资源造成一定的浪费,若在数据集不大且缺失值较多的情况下,该方法可能并不适用。
数据补齐是最常用的缺失值处理方式,补齐的方法也比较多,例如:人工填写、特殊值填充、平均值填充、K最近距离邻法等等,使用这些方法需要对数据有一定的了解,寻找最合适的填充方式。
不处理缺失值我认为在某些人工神经网络中,尚不确定缺失值对模型训练结果有何影响,可以考虑不处理缺失值来进行观测。
参考资料:https://blog.csdn.net/zrjdds/article/details/50223091
删除元组
删除元组的操作通常都采用dropna()
,此操作会将缺失值所在行一并删除
#原数据
df.head(5)
#删除缺失值
df.dropna().head(5)
通过观察乘客ID可以发现,部分乘客数据已经消失了。
数据补齐
在上述中,我们已经可以正确判断缺失值元组,如果再进行简单的数据填充就很方便了
直接对空缺的位置进行赋值,我这里用0去填充
df[df['Age'].isnull()] = 0
df.head(6)
这里可以观察到如果是像上述代码进行操作的话,不仅缺失值会变为0,缺失值所在行的所有值也一并变为了0。为了避免这种情况,我自己试着写了一个很丑的代码:
首先是观察
print(df['Age'][0])
# 运行结果
22.0
好的,这相当于一个二维数组。前面试过了判断缺失值,那我尝试将Age
列的缺失值判断出来
df['Age'].isnull()
# 运行结果
0 False
1 False
2 False
3 False
4 False
...
886 False
887 False
888 True
889 False
890 False
Name: Age, Length: 891, dtype: bool
挑选出缺失值
df['Age'][df['Age'].isnull()]
# 运行结果
5 NaN
17 NaN
19 NaN
26 NaN
28 NaN
..
859 NaN
863 NaN
868 NaN
878 NaN
888 NaN
Name: Age, Length: 177, dtype: float64
然后赋值
df['Age'][df['Age'].isnull()] = 0
df.head(6)
可以看到第六行的数据原本为NaN的’Age’,此时已经变成了0,其他数值都没有改变。
当然,我这种写法好像是有问题的,因为它弹出来了一个SettingWithCopyWarning:A value is trying to be set on a copy of a slice from a DataFrame
看它的意思可能是df['Age'][df['Age'].isnull()]
是df
的一个切片,而切片在pandas里面是视图,只能读不能写,如果只是查看df['Age'][df['Age'].isnull()]
是没问题的,但是如果要对它进行修改的话,就会发生问题(但是我不知道为什么我操作好像成功了,理论上应该是出不来结果的)。
解决方法
虽然我这边不小心运行成功了,但是还是得记一下解决方法,从网上找了好多相关的分享,发现最多的就是前面加多了copy()
和采用loc
提取列或者是把某一列新建成一个list然后再整个赋值回去。
fillna( )
除了我的错误代码,当然还有正规的方法
fillna(self, value=None, method=None, axis=None, inplace=False, limit=None, downcast=None, **kwargs)
df.fillna(0).head(6) # 用0填充缺失数据
可以观察到所有的缺失值都被赋值了,而不仅仅只有特定某列的值
均值填充
df.fillna(df.mean()).head(6) # 用每列特征的均值填充缺失数据
这个均值填充方式并不是所有情况下都适用,例如这里的年龄Age和客舱Cabin,在这个表里的年龄都是整数,但是经过操作之后就带有小数了,改变了原数据的格式。而客舱Cabin的值都是字符串,字符串自然没有办法进行均值运算,所以缺失值并没有得到正确的处理,依旧为缺失值。
中位数填充
df.fillna(df.median()).head(6) # 用每列特征的中位数填充缺失数据
中位数填充方式可以很好的保证数值类型的数据的格式,但是对字符串类型依旧无效。
相邻特征填充
df.fillna(method='bfill').head(7) # 用相邻后面(back)特征填充前面空值
这里是用后面的特征值来填充前面的缺失值,可以解决前几个方法无法填充字符串类型缺失值的问题,但这种做法对后面的模型训练造成什么样的影响目前还是不清楚的,需要学习者不断尝试不同的填充方法。
重复值观察与处理
df[df.duplicated()] #查看重复值
df = df.drop_duplicates() # 删除重复值
特征观察与处理
分箱(离散化)处理
经过上面的填充操作,可以发现很多时候因为不同类型的特征导致个别操作存在问题。经过我的(参考答案)缜密观察,可以把特征大概分为两大类:
- 数值型特征:Survived ,Pclass, Age ,SibSp, Parch, Fare,其中Survived, Pclass为离散型数值特征,Age,SibSp, Parch, Fare为连续型数值特征
- 文本型特征:Name, Sex, Cabin,Embarked, Ticket,其中Sex, Cabin, Embarked, Ticket为类别型文本特征
数值型特征一般可以直接用于模型的训练,但有时候为了模型的稳定性及鲁棒性会对连续变量进行离散化。文本型特征往往需要转换成数值型特征才能用于建模分析。就像对鸢尾花数据集进行模型训练的时候,会将各种花类型名称更改为指定的数值,比如0,1,2等等。
在建模中,需要对连续变量离散化,特征离散化后,模型会更稳定,降低了模型过拟合的风险,分箱操作有等距分箱和等频分箱
等距分箱
cut将根据值本身来选择箱子均匀间隔,即每个箱子的间距都是相同的
#将连续变量Age划分为(0,5] (5,15] (15,30] (30,50] (50,80]五个年龄段,并分别用类别变量12345表示
df['AgeBand'] = pd.cut(df['Age'],[0,5,15,30,50,80],labels = [1,2,3,4,5])
df.head(3)
这里只考虑边界,每个等份里面的实例数量可能不等。
#将连续变量Age平均分箱成5个年龄段,并分别用类别变量12345表示
df['AgeBand'] = pd.cut(df['Age'], 5,labels = [1,2,3,4,5])
df.head()
等频分箱
qcut是根据这些值的频率来选择箱子的均匀间隔,即每个箱子中含有的数的数量是相同的
#将连续变量Age按10% 30% 50 70% 90%五个年龄段,并用分类变量12345表示
df['AgeBand'] = pd.qcut(df['Age'],[0,0.1,0.3,0.5,0.7,0.9],labels = [1,2,3,4,5])
df.head()
详细参考链接:https://blog.csdn.net/starter_____/article/details/79327997
文本变量转换
查看类别文本变量名及种类
- value_counts( )
value_counts(normalize=False, sort=True, ascending=False, bins=None, dropna=True)
参数:
-
normalize : boolean, default False。默认false,如为true,则以百分比的形式显示
-
sort : boolean, default True。默认为true,会对结果进行排序
-
ascending : boolean, default False。默认降序排序
-
bins : integer, 格式(bins=1),意义不是执行计算,而是把它们分成半开放的数据集合,只适用于数字数据
-
dropna : boolean, default True。默认删除na值
示例:
df['Sex'].value_counts()
# 输出结果
male 453
female 261
0 1
Name: Sex, dtype: int64
- unique( )
对于一维数组或者列表,unique函数去除其中重复的元素,并按元素小到大返回一个新的无元素重复的元组或者列表。
df['Sex'].unique()
# 输出结果
array(['male', 'female', 0], dtype=object)
类别文本转换
- replace( )
df['Sex_num'] = df['Sex'].replace(['male','female'],[1,2])
df.head()
- map( )
这个方法具体是什么原理我还不是很清楚,和我在网上查阅的资料不太一样。网上描述的是map() 会根据提供的函数对指定序列做映射。课程的案例里边的用法和网上展示的不太一样,不知道能不能把下面map( )里的键映射为对应的值。
df['Sex_num'] = df['Sex'].map({'male': 1, 'female': 2})
df.head()
- sklearn.preprocessing的LabelEncoder
LabelEncoder的作用是将n个类别编码为0~n-1之间的整数
from sklearn.preprocessing import LabelEncoder
for feat in ['Cabin', 'Ticket']:
lbl = LabelEncoder()
label_dict = dict(zip(df[feat].unique(), range(df[feat].nunique())))
df[feat + "_labelEncode"] = df[feat].map(label_dict)
df[feat + "_labelEncode"] = lbl.fit_transform(df[feat].astype(str))
df.head()
参考链接:sklearn.preprocessing.LabelEncoder的使用 - ColdCode - 博客园 (cnblogs.com)
- one hot编码
one hot编码是将类别变量转换为机器学习算法易于利用的一种形式的过程,又称为一位有效编码,主要是采用N位状态寄存器来对N个状态进行编码,每个状态都由他独立的寄存器位,并且在任意时候只有一位有效。我们的分类结果,得到的往往是隶属于某个类别的概率,这样在进行损失函数(例如交叉熵损失)或准确率计算时,变得非常方便。
# OneHotEncoder
for feat in ["Age", "Embarked"]:
x = pd.get_dummies(df[feat], prefix=feat)
df = pd.concat([df, x], axis=1)
df.head()
参考链接:https://www.cnblogs.com/shuaishuaidefeizhu/p/11269257.html
提取特征
在Name特征里,除了乘客的姓名外还有称呼,例如Miss、Mrs等,这些可以判断出乘客的婚姻情况,也可以作为用于模型训练的新特征。
df['Title'] = df.Name.str.extract('([A-Za-z]+)\.', expand=False)
df.head()
数据重构
先读取个文件
# 载入data文件中的:result.csv
text = pd.read_csv('result.csv')
text.head()
数据聚合与运算
这一部分首先得先来看看什么是GroupBy机制
GroupBy
-
作用:进行数据的分组以及分组后地组内运算
-
语法:
df[](指输出数据的结果属性名称).groupby([df[属性],df[属性]).mean()
说实话还不太会表述GroupBy,我感觉我是大概懂它是做什么的,下面看示例代码:
df = text['Fare'].groupby(text['Sex'])
df
# 运行结果
<pandas.core.groupby.generic.SeriesGroupBy object at 0x000001EA18771340>
可以看到,此时数据不再是DataFrame数据,而是一种DataFrameGroupBy对象。顾名思义由DataFrame转换而来的分组对象为DataFrameGroupBy,由Series转换的分组对象就是SeriesGroupBy。
接下来我们来查看泰坦尼克号男性与女性的平均票价
means = df.mean()
means
# 运行结果
Sex
female 44.479818
male 25.523893
Name: Fare, dtype: float64
输出的结果是男性和女性的平均票价,也就是说通过GroupBy( )
操作将Fare这一列数据按照Sex进行分类了,就好像是把男性的票价归为一类,女性票价归为一类,我只是加了个平均运算,最终的结果就输出出来了,如果要计算男性与女性的票价和,只需要把mean( )
改为sum( )
就好了。
sums = df.sum()
sums
# 运行结果
Sex
female 13966.6628
male 14727.2865
Name: Fare, dtype: float64
看吧看吧,就是这个样子,那接下来统计就方便很多了
例如说统计泰坦尼克号中男女的存活人数
survived_sex = text['Survived'].groupby(text['Sex']).sum()
survived_sex
# 运行结果
Sex
female 233
male 109
Name: Survived, dtype: int64
通过这个统计结果,我们可以更进一步算出男性和女性的存活率,对于初步的数据观测是有一定的必要的,这些能反映出一些问题。
再比如不同等级的客舱存活的人数
survived_pclass = text['Survived'].groupby(text['Pclass']).sum()
survived_pclass
# 运行结果
Pclass
1 136
2 87
3 119
Name: Survived, dtype: int64
通过不同等级的客舱存活的人数,可以猜测可能事发最严重的可能是二等客舱,因为2等客舱存活人数相对来说较少,当然也需要观察不同等级的客舱入住人数,才可以进一步做判断,总之这些初步的观测不是没有意义的,虽然后面还有建模,但是如果能从这些数据表面获得一些重要事实的话,或许我们可以在模型训练的时候,往这些我们认为比较重要的特征上增加权重,以此提高模型预测的准确性。
数据可视化
导入包
import matplotlib.pyplot as plt
导入文件
text = pd.read_csv(r'result.csv')
text.head()
我认为可视化是数据分析里必不可缺的一环,因为很多时候良好的可视化可以让我们更直观的读取到数据信息,如果要展示给外人看那就更应该运用可视化了,可视化的重要性应该不用再说太多了。比如给出泰坦尼克号数据集中男女中生存人数,虽然数字的大小一眼就能看出来,但是数值大概相差多少在人脑里并不能第一时间得知,没什么概念,还需要再心算一下(没错就是我,我最懒了,不想心算)
sex = text.groupby('Sex')['Survived'].sum()
sex.plot.bar()
plt.title('survived_count')
plt.show()
看吧看吧,直观感觉就是女性存活人数要比男性存活人数多一半左右
再来看看生存与死亡在男性女性中的比例
text.groupby(['Sex','Survived'])['Survived'].count().unstack().plot(kind='bar',stacked='True')
plt.title('survived_count')
plt.ylabel('count')
可以非常直观的看到,男性总人数远多与女性,死亡率也是远高于女性,我的想法可能是当时大部分男性展现出了一定的风度,感动.
unstack( )
顺带一提的是代码里新加了unstack( )
函数,与之相关的是stack( )
函数
- unstack( ):将数据的行索引转换为列索引
- stack( ):将数据的列索引转换为行索引
本废物六级还未过,首先就吃了没文化的亏,stack的意思是堆叠,unstack就是“不要堆叠”,从图表上理解的话,就是Survived这个特征并没有在x轴上另起一条,而是以堆叠的方式呆在了Sex特征上(我也不知道我在胡说八道什么)。这里面的列索引我们可以简单理解为列名,初次见面还是有点难理解,让我们先看看不加untack( )
的结果是什么:
text.groupby(['Sex','Survived'])['Survived'].count().plot(kind='bar',stacked='True')
plt.title('survived_count')
plt.ylabel('count')
我们发现原来的柱状图被拆分了开来,且x的值从[female、male]变为了[(female, 0), (female, 1), (male, 0), (male, 1)]。接下来继续上图,毕竟这一节可是可视化,用可视化来学习可视化理所应当吧哈哈哈哈,接下来从数据层面上来观察stack和unstack的差别。
原数据
text.groupby(['Sex','Survived'])['Survived'].count()
# 运行结果
Sex Survived
female 0 81
1 233
male 0 468
1 109
Name: Survived, dtype: int64
原数据就是stack( )的状态,可以看到Sex和Survived同为行索引,并且初步感觉Sex的“等级”要高于Survived,接下来观察unstack( )状态下的数据。
原数据 + unstack( )
text.groupby(['Sex','Survived'])['Survived'].count().unstack()
很容易发现原本作为行索引的Survived特征,现在已经变成了列索引,整体变为了一个二维的表格。我觉得现在应该可以大致理解unstack( )
的原理了。
参考链接:(4条消息) pandas中DataFrame的stack()、unstack()和pivot()方法的对比_S_o_l_o_n的博客-CSDN博客
随机演示
不同仓位等级的人员幸存情况
import seaborn as sns
sns.countplot(x="Pclass", hue="Survived", data=text)
不同年龄的人员幸村情况
facet = sns.FacetGrid(text, hue="Survived",aspect=3)
facet.map(sns.kdeplot,'Age',shade= True)
facet.set(xlim=(0, text['Age'].max()))
facet.add_legend()
不同舱位等级的人员分部情况
text.Age[text.Pclass == 1].plot(kind='kde')
text.Age[text.Pclass == 2].plot(kind='kde')
text.Age[text.Pclass == 3].plot(kind='kde')
plt.xlabel("age")
plt.legend((1,2,3),loc="best")
可视化的库里边有太多的函数可以用,这些都需要我们花时间进一步去发掘,良好的可视化不仅看起来炫酷,而且看起来还很炫酷。就像是有些潜在的关系就可以通过可视化非常直观的体现出来,如果没有用可视化进行操作的话,那么它就没有被可视化进行操作(哈哈哈哈哈哈哈哈哈废话文学带师了我)
好了不开玩笑了,这篇学习记录之后还是会添加内容的,添加的内容取决于我学的东西,所以有时候可能这里插入一点内容,那里又插入一点内容,不过应该也没人看,这个纯粹就是我用于证明我学习过程东西,虽然写的很烂,但是我学的也不怎么样,主要是知识面还不够广,遇到新问题不能用以前的学过类似的东西来类比,然后是语文水平较低,很多东西不知道应该如何正确的表达,好多次都只想写个“自己意会”敷衍过去了(有一说一,除了平时多研究编程的东西外,我觉得我也要多地接触文学作品,现在真的像个莽夫)。
接下来应该会继续补充可视化的内容的,虽然说别的数据清洗、特征提取这些也只是鸡毛蒜皮,但是可视化这边连鸡毛蒜皮还没达到,所以都先拉到同一水平,而且我个人认为可视化在学习的过程中也能起到一定的帮助。YoungCat加油!