特征工程(一)数据清理

现实的数据是多种多样的,即使它们已经是结构化的,仍可能存在各种问题,如数据不完整、丢失、类型错误、前后不一致等。因此需要进行数据清理(Data cleaning),也译为数据清洗。

数据清理

1.1 基本概念

通常以二维表的方式表示数据,Pandas的DataFrame类型的数据是最常见的。

import pandas as pd
df = pd.read_csv("datasets/pm2.csv")
df.sample(10)
RANK CITY_ID CITY_NAME Exposed days
126 138 622 陇南 96
54 62 44 朔州 58
68 76 562 贵阳 65
89 99 284 景德镇 80
150 171 501 桂林 112
211 234 390 武汉 158
225 248 359 洛阳 178
144 161 245 铜陵 108
177 200 538 德阳 130
186 209 428 株洲 137

上面显示的二维表格中,每行是一个对象,它被冠以的名称有“记录Record”、“样本Sample”、“实例Instance”,甚至干脆就是“行Row”。

二维表中,列就是描述对象的“属性Attribute”,这是一个常用的名称,还有一个常用名称叫“特征Feature”,也是列的常用名称,此外还有“维度Dimension”和“变量Variable”等称呼。

df.shape

(264, 4)

该数据集有264个样本、4个特征,每个特征的数据也各有不同的类型

df.dtypes

RANK int64
CITY_ID int64
CITY_NAME object
Exposed days int64
dtype: object

1.2 转化数据类型

基础知识

数据集中的数据、有的是整数或浮点数类型,有的是字符串类型、布尔类型等。数据分析和机器学习算法喜欢的是整数或浮点数(数值类型),如果数据集中出现非数值类型的数据,就需要对其进行适当转化。

import pandas as pd
df = pd.DataFrame([{'col1':'a', 'col2':'1'}, 
                           {'col1':'b', 'col2':'2'}])
df.dtypes

col1 object
col2 object
dtype: object

“col2”特征的值貌似是整数,但通过df.dtypes查看却是字符串类型,可以通过astype方法进行强转。

# 转int 类型
df['col2-int'] = df['col2'].astype(int)    

但用astype方法,只能转化全部由数字组成的数据,如果遇到下面的问题,就无能为力了。

# could not convert string to float: 'pandas'
s = pd.Series(['1', '2', '4.7', 'pandas', '10'])    
s.astype(float)

Pandas提供了另外一个实现类型转化的函数to_numeric

# Unable to parse string "pandas" at position 3
pd.to_numeric(s,errors='coerce')

0 1.0
1 2.0
2 4.7
3 NaN
4 10.0
dtype: float64

可以看到,这次将字符串类型的数据转化为浮点数类型,并且将原来数据中由字母组成的字符型强制转化为了NaN——表示缺失值,但它是一个浮点数。

项目案例

去取数据集sales_types.csv中的数据,根据要求对特征数据类型进行转化。

  • 将"Customer Number"的数据转化为字符类型
  • 将“2016”和“2017”的数据转化为浮点数类型
  • 将“Percent Growth”的数据转化为浮点数类型
  • 将“Jan Units ”中的数据转化为浮点数类型
  • 将“Month”、“Day”、“Year“三个特征的数据合并为一个日期类型的特征
  • 将“Active”的数据用1和0表示
import pandas as pd
import numpy as np

def convert_money(value):
    new_value = value.replace(",","").replace("$","")
    return float(new_value)

df2 = pd.read_csv("datasets/sales_types.csv",
                  dtype = {'Customer Number': 'int'},
                  converters = {'2016': convert_money,
                                '2017': convert_money,
                                'Percent Growth': lambda x: float(x.replace("%", "")) / 100,
                                'Jan Units': lambda x: pd.to_numeric(x, errors='coerce'),
                                'Active': lambda x: np.where(x =='Y', 1, 0),
                               })
df2['Date'] = pd.to_datetime(df[['Month', 'Day', 'Year']])
df2
Customer Number Customer Name 2016 2017 Percent Growth Jan Units Month Day Year Active Date
0 10002 Quest Industries 125000.0 162500.0 0.30 500.0 1 10 2015 1 2015-01-10
1 552278 Smith Plumbing 920000.0 1012000.0 0.10 700.0 6 15 2014 1 2014-06-15
2 23477 ACME Industrial 50000.0 62500.0 0.25 125.0 3 29 2016 1 2016-03-29
3 24900 Brekke LTD 350000.0 490000.0 0.04 75.0 10 27 2015 1 2015-10-27
4 651029 Harbor Co 15000.0 12750.0 -0.15 NaN 2 2 2014

其中用到的特殊函数有

  • replace是字符串的方法
  • Python高级特性lambda函数
  • np.where能够实现条件判断功能

1.3 处理重复数据

基础知识

如果数据集中的某个特征下的重复数据比例较高,则会造成该特征标准差降低,如下面的数据:

import pandas as pd
d = {'Name':['Newton', 'Galilei', 'Einstein', 'Feynman', 'Newton', 'Maxwell', 'Galilei'],
             'Age':[26, 30, 28, 28, 26, 39, 40],
             'Score':[90, 80, 90, 100, 90, 70, 90]}
df = pd.DataFrame(d,columns=['Name','Age','Score'])
df
Name Age Score
0 Newton 26 90
1 Galilei 30 80
2 Einstein 28 90
3 Feynman 28 100
4 Newton 26 90
5 Maxwell 39 70
6 Galilei 40 90

duplicated()方法可以用来检查是否有重复数据

Considering certain columns is optional.

  • subset:用它可以指明数据子集,即某个特征或某几个特征
  • keep='first':当遇到重复数据时保留哪一个,被保留的数据标记为False,其他标记为True
  • keep='last':表示要保留重复数据中的最后一个。

检查df的特征“Age”中是否有重复数据,并且用keep='last'的方式声明保留最后一个数据。

df.drop_duplicates('Age', keep='last')
Name Age Score
1 Galilei 30 80
3 Feynman 28 100
4 Newton 26 90
5 Maxwell 39 70
6 Galilei 40 90

项目案例

计算CPI有关数据的重复率

cpi = pd.read_excel("datasets/cpi.xls")
cpi.columns = cpi.iloc[1]    
cpi = cpi[2:]    
cpi.drop([11, 12], axis=0, inplace=True)    
cpi['cpi_index'] = ['总体消费', '食品烟酒', '衣着', '居住', '生活服务', '交通通信', '教育娱乐', '医保', '其他']
cpi.drop(['指标'], axis=1, inplace=True)    
cpi.reset_index(drop=True, inplace=True)    
cpi.columns.rename('', inplace=True)    
cpi
2019年3月 2019年2月 2019年1月 2018年12月 2018年11月 2018年10月 2018年9月 2018年8月 2018年7月 2018年6月 2018年5月 2018年4月 cpi_index
0 102.3 101.5 101.7 101.9 102.2 102.5 102.5 102.3 102.1 101.9 101.8 101.8 总体消费
1 103.5 101.2 102 102.4 102.5 102.9 103 101.9 101 100.8 100.7 101.1 食品烟酒
2 102 102 101.6 101.5 101.4 101.4 101.2 101.3 101.2 101.1 101.1 101.1 衣着
3 102.1 102.2 102.1 102.2 102.4 102.5 102.6 102.5 102.4 102.3 102.2 102.2 居住
4 101.2 101.3 101.5 101.4 101.5 101.5 101.6 101.6 101.6 101.5 101.5 101.5 生活服务
5 100.1 98.8 98.7 99.3 101.6 103.2 102.8 102.7 103 102.4 101.8 101.1 交通通信
6 102.4 102.4 102.9 102.3 102.5 102.5 102.2 102.6 102.3 101.8 101.9 102 教育娱乐
7 102.7 102.8 102.7 102.5 102.6 102.6 102.7 104.3 104.6 105 105.1 105.2 医保
8 101.9 102 102.3 101.6 101.5 101.3 100.7 101.2 101.2 100.9 101 100.9 其他
dup_ratio = []
for column in cpi.columns:
    col = cpi[column]
    ratio = col[col.duplicated()].count() / col.count()
    dup_ratio.append(round(ratio, 2))
dr = pd.Series(dup_ratio, index=cpi.columns)
dr

2019年3月 0.00
2019年2月 0.11
2019年1月 0.00
2018年12月 0.00
2018年11月 0.22
2018年10月 0.22
2018年9月 0.00
2018年8月 0.00
2018年7月 0.11
2018年6月 0.00
2018年5月 0.11
2018年4月 0.22
cpi_index 0.00
dtype: float64

1.4 处理缺失数据

出于各种原因,现实的数据总会出现缺失现象,这似乎是很难避免的,其中有主观原因,也有客观原因。

检查缺失数据

Python中有一个特殊对象None,可以用来表示“缺失” “没有”

Numpy提供一种表示“缺失”数据的对象np.nan

它可以与数字进行运算,因为它本身就是浮点数类型

import pandas as pd
s = pd.Series([1, 2, None, np.nan])    
s

0 1.0
1 2.0
2 NaN
3 NaN
dtype: float64

s中虽然有缺失值,但不影响计算过程

s.sum()

3.0

检测是否缺失

0 False
1 False
2 True
3 True
dtype: bool

也可以用DataFrame类型的对象调用

df = pd.DataFrame({"one":[1, 2, np.nan], "two":[np.nan, 3, 4]})
df.isna()
one two
0 False True
1 False False
2 True False

使用DataFrame实例的dropna方法可以实现对缺失数据的删除

df.dropna(axis=0, how='any', thresh=None, subset=None, inplace=False)

主要参数说明:

Parameters
----------
axis : {0 or 'index', 1 or 'columns'}, default 0
    Determine if rows or columns which contain missing values are
    removed.

    * 0, or 'index' : Drop rows which contain missing values.
    * 1, or 'columns' : Drop columns which contain missing value.

    .. versionchanged:: 1.0.0

       Pass tuple or list to drop on multiple axes.
       Only a single axis is allowed.

how : {'any', 'all'}, default 'any'
    Determine if row or column is removed from DataFrame, when we have
    at least one NA or all NA.

    * 'any' : If any NA values are present, drop that row or column.
    * 'all' : If all values are NA, drop that row or column.

thresh : int, optional
    Require that many non-NA values.
subset : array-like, optional
    Labels along other axis to consider, e.g. if you are dropping rows
    these would be a list of columns to include.
inplace : bool, default False
    If True, do operation inplace and return None.

how声明删除条件

  • 默认any 表示行或列中只要有缺失值,就删除该行或列
  • 如果为all,则要求都是缺失值才删除

非缺失值小于2的删除

df.dropna(thresh=2) 

用指定值填补

某些数据集中,可以用指定数值填补

df = pd.DataFrame({'ColA':[1, np.nan, np.nan, 4, 5, 6, 7], 'ColB':[1, 1, 1, 1, 2, 2, 2]})
df
ColA ColB
0 1.0 1
1 NaN 1
2 NaN 1
3 4.0 1
4 5.0 2
5 6.0 2
6 7.0 2

fillna可以指定数值填补缺失值

  • method='ffill' 表示用当前缺失值前面的值填补
  • method='bfill' 表示用当前缺失值后面的值填补
df['ColA'].fillna(method='ffill')

0 1.0
1 1.0
2 1.0
3 4.0
4 5.0
5 6.0
6 7.0
Name: ColA, dtype: float64

df['ColA'].fillna(method='bfill')

0 1.0
1 4.0
2 4.0
3 4.0
4 5.0
5 6.0
6 7.0
Name: ColA, dtype: float64

scikit-learn专门提供了名为SimpleImputer的模块,用来做填充

from sklearn.impute import SimpleImputer
df = pd.DataFrame({"name": ["Google", "Huawei", "Facebook", "Alibaba"], 
                   "price": [100, -1, -1, 90]
                  })
imp = SimpleImputer(missing_values=-1, strategy='constant', fill_value=110)
imp.fit_transform(df['price'].values.reshape((-1, 1)))

array([[100],[110],[110],[ 90]], dtype=int64)

  • missing_values = -1 声明将整数-1作为缺失值
  • fill_value 声明填补数值
  • strategy 填补策略可选,mean平均值,median中位数,most_frequent众数,constant用参数fill_value指定的数值填补缺失数据

根据规律填补缺失值

用指定数值填补缺失数据,因为所有填补的数值都是一样的,这样做会导致数据标准差降低,对模型的泛化能力有一定影响。如果能发现特征中所有数据的规律,比如符合某个线性关系,就不会因为用特定数值填补而使标准差降低了。

df = pd.DataFrame({"one":np.random.randint(1, 100, 10), 
                   "two": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
                  "three":[5, 9, 13, np.nan, 21, np.nan, 29, 33, 37, 41]})
df
one two three
0 52 2 5.0
1 48 4 9.0
2 60 6 13.0
3 60 8 NaN
4 8 10 21.0
5 71 12 NaN
6 61 14 29.0
7 60 16 33.0
8 69 18 37.0
9 52 20 41.0

对简单的数据可以凭观察找到规律,对于复杂的数据可用机器学习模型来解决

下面是使用线性回归模型填充缺失值的过程,将此处的缺失值填补问题转化为了预测问题,将特征“three”视为每个样本的标签,缺失值就是要预测的值。

from sklearn.linear_model import LinearRegression    

df_train = df.dropna()    #训练集
df_test = df[df['three'].isnull()]    #测试集

regr = LinearRegression() #线性回归模型
regr.fit(df_train['two'].values.reshape(-1, 1), df_train['three'].values.reshape(-1, 1))    # ⑦
df_three_pred = regr.predict(df_test['two'].values.reshape(-1, 1))   

# 将所得数值填补到原数据集中
df.loc[(df.three.isnull()), 'three'] = df_three_pred
df
one two three
0 52 2 5.0
1 48 4 9.0
2 60 6 13.0
3 60 8 17.0
4 8 10 21.0
5 71 12 25.0
6 61 14 29.0
7 60 16 33.0
8 69 18 37.0
9 52 20 41.0

项目案例

读取train.csv数据,找到有缺失值数据的特征,并对数据进行填充

import pandas as pd

train_data = pd.read_csv("datasets/train.csv")
train_data.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

不难看出来,有一些特征是存在缺失值的,比如Age计数只有714

train_data.isna().any()

PassengerId False
Survived False
Pclass False
Name False
Sex False
Age True
SibSp False
Parch False
Ticket False
Fare False
Cabin True
Embarked True
dtype: bool

df = train_data[['Age','Fare', 'Parch', 'SibSp', 'Pclass']]    #可能跟年龄有关的特征
known_age = df[df['Age'].notnull()].values
unknown_age = df[df['Age'].isnull()].values

y = known_age[:, 0]
X = known_age[:, 1:]

引入随机森林回归模型,准备用它来预测缺失值

from sklearn.ensemble import RandomForestRegressor    
rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1)    
rfr.fit(X, y)    

pred_age = rfr.predict(unknown_age[:, 1:])    
pred_age.mean()

29.43784237405462

用预测结果填充缺失数据

train_data.loc[(train_data.Age.isnull()), 'Age'] = pred_age
train_data.isna().any()

PassengerId False
Survived False
Pclass False
Name False
Sex False
Age False
SibSp False
Parch False
Ticket False
Fare False
Cabin True
Embarked True
dtype: bool

为直观地查看填补之后对原有数据分布是否造成太大影响,分别对填补前后的数据绘制直方图

#填补前数据分布
%matplotlib inline
import seaborn as sns
sns.distplot(y)

1

填补后数据分布

sns.distplot(train_data['Age'])

填补前后的Age数据没有太大的变化,说明填补的数据几乎是符合原数据分布特点的,似乎也可以使用平均填充,分布图如下:

df_mean = df['Age'].fillna(df['Age'].mean())
sns.distplot(df_mean)

显然,使用平均填充之后,数据分布有了较大到的变化,那么使用平均填充并不是一个很好地选择。

1.5 处理离群数据

所谓离群数据,是指少量数据显著不同于其他数据。 下图中,可以看到右上角部分数据明显不符合某种直线关系,这些数据就是离群数据。

%matplotlib inline
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("datasets/experiment.csv", index_col=0)

fig, ax = plt.subplots()
ax.scatter(df['alpha'], df['belta'])

一般离群数据划分为以下三种:

  • 全局离群数据
  • 情景离群数据
  • 集体离群数据

在实际业务中,一个数据集可能包含多种类型的离群数据。此外,一个离群数据可能数据多种类型。检验不同类型的离群值,可能有不同的目的和应用。

基础知识

箱线图示一种检验离群数据的常用方法

箱形图(Box-plot)又称为盒须图、盒式图或箱线图,是一种用作显示一组数据分散情况资料的统计图。因形状如箱子而得名。在各种领域也经常被使用,常见于品质管理。它主要用于反映原始数据分布的特征,还可以进行多组数据分布特征的比 较。箱线图的绘制方法是:先找出一组数据的上边缘、下边缘、中位数和两个四分位数;然后, 连接两个四分位数画出箱体;再将上边缘和下边缘与箱体相连接,中位数在箱体中间。

import seaborn as sns
sns.set(style="whitegrid")

tips = sns.load_dataset("tips")    #加载数据集
tips.sample(5)
total_bill tip sex smoker day time size
36 16.31 2.00 Male No Sat Dinner 3
134 18.26 3.25 Female No Thur Lunch 2
198 13.00 2.00 Female Yes Thur Lunch 2
184 40.55 3.00 Male Yes Sun Dinner 2
165 24.52 3.48 Male No Sun Dinner 3

tips变量引用的是一个关于顾客给消费的数据集,下面用箱线图检查顾客给小费的金额中有没有离群值

sns.boxplot(x="day", y="tip", data=tips, palette="Set3")

显然,Sat有比较明显的离群特征。餐厅服务员在周六工作比较合算。

sns.swarmplot以不重叠的点表示数据分布情况

ax = sns.boxplot(x="day", y="tip", data=tips)
ax = sns.swarmplot(x="day", y="tip", data=tips, color=".25")   

项目案例

Boston房价数据集中是否有离群值,如果有,则将离群值从数据集中剔除

加载数据集

from sklearn.datasets import load_boston
import pandas as pd
boston = load_boston()
x = boston.data
y = boston.target
columns = boston.feature_names

#为了操作方便,将数据集转化为DataFrame类型
boston_df = pd.DataFrame(boston.data)    
boston_df.columns = columns
boston_df.head()
CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT
0 0.00632 18.0 2.31 0.0 0.538 6.575 65.2 4.0900 1.0 296.0 15.3 396.90 4.98
1 0.02731 0.0 7.07 0.0 0.469 6.421 78.9 4.9671 2.0 242.0 17.8 396.90 9.14
2 0.02729 0.0 7.07 0.0 0.469 7.185 61.1 4.9671 2.0 242.0 17.8 392.83 4.03
3 0.03237 0.0 2.18 0.0 0.458 6.998 45.8 6.0622 3.0 222.0 18.7 394.63 2.94
4 0.06905 0.0 2.18 0.0 0.458 7.147 54.2 6.0622 3.0 222.0 18.7 396.90 5.33
boston_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 506 entries, 0 to 505
Data columns (total 13 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   CRIM     506 non-null    float64
 1   ZN       506 non-null    float64
 2   INDUS    506 non-null    float64
 3   CHAS     506 non-null    float64
 4   NOX      506 non-null    float64
 5   RM       506 non-null    float64
 6   AGE      506 non-null    float64
 7   DIS      506 non-null    float64
 8   RAD      506 non-null    float64
 9   TAX      506 non-null    float64
 10  PTRATIO  506 non-null    float64
 11  B        506 non-null    float64
 12  LSTAT    506 non-null    float64
dtypes: float64(13)
memory usage: 51.5 KB

计算箱线图的IQR(四分位距),即数据集的75%的值和25%的值的差

# axis=0 表示是计算各个特征(列)数据的四分位值
percentlier = boston_df.quantile([0, 0.25, 0.5, 0.75, 1], axis=0) 
IQR = percentlier.iloc[3] - percentlier.iloc[1]
IQR

CRIM 3.595038
ZN 12.500000
INDUS 12.910000
CHAS 0.000000
NOX 0.175000
RM 0.738000
AGE 49.050000
DIS 3.088250
RAD 20.000000
TAX 387.000000
PTRATIO 2.800000
B 20.847500
LSTAT 10.005000
dtype: float64

在箱型图中,判断某数据是否为离群值,常以(Q1-1.5IQR)和(Q3+1.5IQR)两个数为界限。

Q1 = percentlier.iloc[1]    #第1四分位
Q3 = percentlier.iloc[3]    #第3四分位
(boston_df < (Q1 - 1.5 * IQR)).any()  

CRIM False
ZN False
INDUS False
CHAS False
NOX False
RM True
AGE False
DIS False
RAD False
TAX False
PTRATIO True
B True
LSTAT False
dtype: bool

(boston_df > (Q3 + 1.5 * IQR)).any()

CRIM True
ZN True
INDUS False
CHAS True
NOX False
RM True
AGE False
DIS True
RAD False
TAX False
PTRATIO False
B False
LSTAT True
dtype: bool

利用两个边界计算,将数据集的离群值剔除

boston_df_out = boston_df[~((boston_df < (Q1 - 1.5 * IQR)) |(boston_df > (Q3 + 1.5 * IQR))).any(axis=1)]
boston_df_out.shape

(274, 13)

经剔除后,还剩下274条记录,安装箱线图的原则,现有的数据集boston_df_out中就没有离群值了

如果数据是正态分布的,还可以依据正态分布的有关统计理论检查、处理离群数据。

# 计算z值
from scipy import stats    #统计专用模块
import numpy as np
rm = boston_df['RM']
z = np.abs(stats.zscore(rm))    # 计算RM特征下所有数据的Z分数
st = boston_df['RM'].std()    # 计算RM特征的标准差
st

0.7026171434153237

threshold = 3 * st   #阈值
print(np.where(z > threshold))  # 如果Z分数大于三个标准差,则认为所对应的原数据为离群值

(array([ 97, 98, 162, 163, 166, 180, 186, 195, 203, 204, 224, 225, 226,232, 233, 253, 257, 262, 267, 280, 283, 364, 365, 367, 374, 384,386, 406, 412, 414], dtype=int64),)

保留所有Z分数绝对值大于三个标准差的数据

rm_in = rm[(z < threshold)]
rm_in.shape

(476,)

posted @ 2022-06-09 14:02  王陸  阅读(289)  评论(0编辑  收藏  举报