Fork me on GitHub

Titanic缺失数据处理

Kaggle的比赛入门,通过机器学习模型对泰坦尼克号上幸存情况进行预测,这里主要针对预测前的特征工程处理,对缺失数据进行分析和补全。

加载数据

加载csv文件数据

1
2
3
4
5
6
7
8
def load_data(path):
import csv as csv
reader = csv.reader(open(path, 'rb'))
header = reader.next()
data = []
for line in reader:
data.append(line)
return header, np.array(data)

测试代码

1
2
train_header, train_data = load_data(TRAIN_PATH)
print train_header

Output:

1
['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked']

检查缺失数据

遍历每一行,记下空字符串元素所在的位置

1
2
3
4
5
6
7
8
9
10
def check_empty(header, data):
empty = {}
for row, line in enumerate(data):
for column, value in enumerate(line):
if value == '':
feature = header[column]
if not empty.__contains__(feature):
empty[feature] = []
empty[feature].append(line[0])
return empty

测试代码

1
2
3
4
train_header, train_data = load_data(TRAIN_PATH)
empty = check_empty(train_header, train_data)
for key, value in empty.items():
print '%s: %d' % (key, len(value))

Output:

1
2
3
Age: 177
Cabin: 687
Embarked: 2

可以看到训练数据中Age特征有177条缺失数据,另外两个以此类推。

上面是训练集中缺失数据的特征,而我们要得到还有测试集的缺失信息,所以写一个merge函数,将训练集和测试集合并起来(由于现在是缺失值分析阶段,所以不区分训练集和测试集,接下来会一直沿用合并后的数据),然后再检查缺失信息。

1
2
3
4
5
6
7
8
9
10
def merge_data(train_data, test_data):
# 删除训练集中第二列的幸存信息,以保持数据格式统一
train_without_survive = np.delete(train_data, 1, axis=1)
return np.vstack([train_without_survive, test_data])

train_header, train_data = load_data(TRAIN_PATH)
test_header, test_data = load_data(TEST_PATH)
merged = merge_data(train_data, test_data)
for key, value in check_empty(test_header, merged).items():
print '%s: %d' % (key, len(value))

Output:

1
2
3
4
Fare: 1
Age: 263
Cabin: 1014
Embarked: 2

于是能够得到所有缺失的特征,分别是FareAgeCabinEmbarked,接下来开始对缺失项一一进行分析、补全。

缺失数据分析

Fare

表明乘客的票价,有1个缺失值,我们首先看一下该数据的范围,均值,均方差这些信息

1
2
3
4
5
fare = merged[:, test_header.index('Fare')]
fare_with_data = [float(f) for f in fare if f != '']
print 'Range: %.2f - %.2f' % (min(fare_with_data), max(fare_with_data))
print 'Mean: %.2f' % np.mean(fare_with_data)
print 'Mean variance: %.2f' % np.sqrt(np.var(fare_with_data))

Output:

1
2
3
Range: 0.00 - 512.33
Mean: 33.30
Mean variance: 51.74

通过这三项数据(以下简称RMM)我们能对票价信息有个大致的了解,范围在0 - 512.33波动,均值是33.3而均方差只有51.74,这表明大多数票价都是偏低的(也能从一定程度上分析出乘客的经济状况,这里就不继续延伸了)。

RMM信息获取很方便,但是不便于直观、确切的观察数据的分布情况,这时考虑通过matplot画出票价的密度分布图。

1
2
3
import matplotlib.pyplot as plt
plt.hist(fare_with_data, alpha=.7)
plt.show()

分布图如下

data_fix_fare

从图中可以很直观的看出绝大部分数据都是部分在50以下的,鉴于这种情况,我们很直观的选择是把均值作为补全值。

1
print np.mean(fare_with_data)

Output:

1
33.2954792813

这时33.2954792813便做为Fare补全值了。

为了确保万一,我们要查出该条数据索引

1
2
empty = check_empty(test_header, merged)
print empty['Fare']

Output:

1
['1044']

接下来我们根据PassengerId找到该条数据

1
1044,3,"Storey, Mr. Thomas",male,60.5,0,0,3701,,,S

对比着特征名来分析数据

1
PassengerId,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked

可以逐一分析,我们发现Pclass对应N等票,可能会影响到票价Fare特征,因此我们要取出所有跟该名乘客相同Pclass(从数据可以看出是3)的票价分布。

1
2
3
4
fare = merged[:, [test_header.index('Fare'), test_header.index('Pclass')]]
fare_with_pclass_3 = [float(f) for f, pclass in fare if f != '' and pclass == '3']
plt.hist(fare_with_pclass_3, alpha=.7)
plt.show()

分布图如下

data_fix_fare2

对比和上一张图的区别,我们能更加确切的了解到,三等票的票价没有高于70的,因此如果按照上面的结论直接把所有数据的票价均值作为补全值,在一定程度上会使得补全值的误差变大,取而代之的是用Pclass为3的乘客的票价均值作为补全值(这里还是有一定的优化空间,我们可以分析Pclass为3的乘客中的年龄、家属情况、船舱等对票价的影响,这里就不继续展开了)。

有了上面的分析,我们就可以重新计算补全值

1
print np.mean(fare_with_pclass_3)

Output:

1
13.3028887006

和上面输出的33.3还是有一定差距的

经过上面的一系列的分析,可以写出Fare特征的补全逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def compute_fare_data(all_data, pclass, fare_index, pclass_index):
fare_pclass = all_data[:, [fare_index, pclass_index]]
fare_with_pclass = [float(f) for f, c in fare_pclass if f != '' and c == pclass]
return np.mean(fare_with_pclass)


def fill_data_fare(all_data, need_fill_data, header):
fare_index = header.index('Fare')
pclass_index = header.index('Pclass')
fill_cache = {}
for f in need_fill_data:
if f[fare_index] == '':
pclass = f[pclass_index]
if not fill_cache.__contains__(pclass):
fill_cache[pclass] = compute_fare_data(all_data, pclass, fare_index, pclass_index)
f[fare_index] = fill_cache[pclass]

此时我们的主函数应该是这样子

1
2
3
4
5
train_header, train_data = load_data(TRAIN_PATH)
test_header, test_data = load_data(TEST_PATH)
merged = merge_data(train_data, test_data)
fill_data_fare(merged, train_data, train_header)
fill_data_fare(merged, test_data, test_header)

Age

表明乘客年龄,有263个缺失值,同上,我们先看一下该特征的RMM信息

1
2
3
4
5
age = merged[:, test_header.index('Age')]
age_with_data = [float(f) for f in age if f != '']
print 'Range: %.2f - %.2f' % (min(age_with_data), max(age_with_data))
print 'Mean: %.2f' % np.mean(age_with_data)
print 'Mean variance: %.2f' % np.sqrt(np.var(age_with_data))

Output:

1
2
3
Range: 0.17 - 80.00
Mean: 29.88
Mean variance: 14.41

均值29.9,均方差14.4,可见整体年龄层都分布在青壮年阶段,接下来直接看看密度分布图

从该图中我们只能观察到老人(50岁以上)和小孩(16岁以下)占比较小,年龄大多集中在17-40左右,我们必须试图在数据中寻找更好的估值标准。

我们尝试寻找AgePclass的关系,先来画出分布图

1
2
3
4
5
6
7
8
9
10
11
age_pclass = merged[:, [test_header.index('Age'), test_header.index('Pclass')]]
# 过滤掉Age为空的数据
age_pclass_with_data = [[int(float(a)), int(p)] for a, p in age_pclass if a != '']
age_pclass_with_data = np.array(age_pclass_with_data)
x = age_pclass_with_data[:, 0]
y = age_pclass_with_data[:, 1]
plt.scatter(x, y, alpha=.7)
plt.xlabel('Age')
plt.ylabel('Pclass')
plt.title('Pclass & Age')
plt.show()

分布图如下

从上图上中可以知道很难直接根据Pclass估值出Age的值。


------------- The end -------------