一. 问题背景
参加工作以来我一直从事的是服务器后端的开发工作,做过娱乐直播也做过手游开发。但在我内心深处一直有一个声音告诉我:机器学习才是我真正的兴趣所在。俗话说“少壮不努力,老大徒伤悲”。我希望自己能利用8小时以外的时间为自己的人生创造某种可能。所以我把晚上和周末的时间利用起来,报了Udacity的在线教育课程。作为“数据分析”纳米学位的学员,最近一段时间一直在接触有关数据提取,清洗和质量评估有关的知识。一次偶然的机会,还真就在实际工作中用上了!如果不是因为听过课,我还真不知道处理这样的工作任务应该注意什么问题。所学的课程内容和自己在实际工作中接触到的问题契合度相当高,于是我决定把这次解决问题的过程记录下来,作为自己加入Udacity优等生班要分享的第一篇博客。
最近一段时间以来我一直在负责一个云平台的开发和维护工作,这个平台以API的形式提供与小区物业和房屋租赁有关的云服务。比如房源的录入,租房,支付定金和租金等等。用户可以通过手机App登录然后访问这些服务。现在有一些用户是云平台开发之前就存在了的,大概有几百个,他们的信息需要导入到这个系统中。具体包含如下几类信息:
- 姓名
- 性别
- 公寓名称
- 房间号
- 租期
- 起始时间
- 到期时间
- 付租方式
- 已付租金
- 已付租数
- 定金
- 押金
- 身份证号
- 手机号
这些信息以excel表格的形式提供,是人工编制的。要做的工作就是将每个用户的信息拆分成四类数据放入MongoDB数据中。首先是用户数据,用户数据代表的是登录账户,包含用户名(手机号),密码等信息;其次是实名认证数据,比如住址,真实姓名,身份证号,性别等等;然后是租约数据,表示用户当前租赁信息,比如公寓名称,房间号,租期起止时间等等;最后是订单数据,表示用户已经支付和还未支付的定金,租金等信息。
只要是人编辑录入的信息,就有可能出错。所以在清洗,加工和导入数据之前,必须要对数据的质量进行评估,发现有问题的数据。特别是租约数据和订单数据,它们分别代表一个业务系统中必不可少的两类数据,前者代表状态,后者代表流水。流水数据必须要和状态数据对应起来,不能有差错。特别是如果后期要进行用户行为分析的话,录入的数据一定要是正确的,否则会得出错误的结论,进而导致错误的商业决策。所以数据整理和清洗工作绝对是数据分析中极其重要的一个环节。
二. 数据整理
第一步要做的工作是将excel表格中的数据用Python语言读取出来。如果数据一直存放在电子表格中,那就只能靠人工检查数据的正确性,如果数据量很大的话这是不现实的。只要能将数据读出来,我们就能写代码通过程序去做验证,省事又省力,还能保证较高的正确率。
1. 数据的整理和加工
Python语言第三方库xlrd就是专门用来读取excel电子表格的,它提供了强大的功能实现对excel表格的各种操作,具体请参考官方文档。首先来看看如何通过Python语言读取excel表格的数据:
def load_data_excel(excel):
# load_data_excel loads data from excel into a list of dicts
workbook = xlrd.open_workbook(excel)
sheet = workbook.sheet_by_index(0)
headers = sheet.row_values(0)
data = []
for i in range(sheet.nrows):
if i == 0:
continue
row = sheet.row_values(i)
entry = {}
for j in range(len(row)):
if headers[j] in [u'起始时间', u'到期时间']:
# entry[headers[j]] = xlrd.xldate.xldate_as_datetime(float(row[j]), 0).replace(tzinfo=tz.tzlocal())
entry[headers[j]] = datetime.datetime.strptime(row[j], '%Y-%M-%d').replace(tzinfo=tz.tzlocal())
elif headers[j] == u'手机号':
entry[headers[j]] = str(int(row[j]))
elif headers[j] == u'租期':
if row[j][-1] == u'年':
entry[headers[j]] = int(row[j][0]) * 12
elif row[j][-1] == u'月':
entry[headers[j]] = int(row[j][0])
elif headers[j] in [u'公寓名称', u'房间号']:
entry[headers[j]] = row[j].strip()
elif headers[j] == u'性别':
entry[headers[j]] = 'male' if row[j].strip() == u'男' else 'female'
elif headers[j] == u'付租方式':
d = {
u'月付': 1,
u'季付': 3,
u'半年付': 6,
u'年付': 12,
}
entry[headers[j]] = d[row[j]]
elif headers[j] == u'定金':
entry[headers[j]] = 0 if row[j] == '' else int(row[j] * 100) # convert to fen
elif headers[j] in [u'押金', u'已付租金']:
entry[headers[j]] = int(row[j] * 100) # convert to fen
else:
entry[headers[j]] = row[j]
data.append(entry)
return data
load_excel_data()这个函数读取excel中的数据,然后将这些数据转换成一个列表,列表中的每个元素是一个字典,代表一个用户的各项数据,例如:
[
{
u'姓名': '张三',
u'性别': '男',
u'公寓名称': 'XX公寓',
u'房间号': 'YY房间',
u'租期': 12,
u'起始时间': datetime('2017-05-30'),
u'到期时间': datetime('2018-05-29'),
u'付租方式': 1,
u'已付租金': 80000,
u'已付租数': 1,
u'定金': 20000,
u'押金': 112000,
u'身份证号': 'XXX',
u'手机号': 'ZZZ',
},
...
]
其中部分数据需要加工处理,比如涉及到日期的数据,要转换成Python的datetime.datetime;性别数据要使用英文,转换成male和female;租期要转换为以月为单位的整数;为了后面审核数据质量的需要,公寓名称和房间号要注意去掉多余的空格;所有涉及到金额的数据要转换成以分为单位的数字。
2. 数据的审核
有关数据质量的评估,Udacity课程介绍了五个评估指标:
- 有效性(validity):数据模式符合定义或满足其他约束
- 精确性(accuracy):与权威数据相符合
- 完整性(completeness):是全部记录吗?
- 一致性(consistency):多个数据之间保持一致
- 统一性(uniformity):是否使用相同单位
关于用户数据,至少可以对其有效性,精确性和一致性进行审核。比如性别数据取值只能是male或者female,否则的话原始数据一定存在错误;“起始时间”,“到期时间”和“租期”之间一定要满足一致性,也就是说“起始时间+租期=到期时间”;特别重要的是,一定要检查公寓名称和房间名称在数据库中是否已存在(精确性)。如果任何一条数据不满足以上要求,都不能进行导入,必须修正。
三. 数据导入
只要数据经过了审核,导入工作就相对来说比较简单了,直接按照各不同collection的schema进行导入即可。
1. 用户数据
def store_users(data, local_db):
users = []
for d in data:
user = {
'createdAt': datetime.datetime.utcnow(),
'avartarID': '',
'nickname': d[u'手机号'],
'phone': d[u'手机号'],
}
users.append(user)
local_db.users.insert_many(users)
2. 实名认证数据
def store_real_names(data, local_db):
# find all users' _id and phone, make them a dict whose keys are phones
users = {}
for u in local_db.users.find({}, {"_id": 1, "phone": 1}):
users[u['phone']] = u
real_names = []
for d in data:
real_name = {
'createdAt': datetime.datetime.utcnow(),
'userID': users[d[u'手机号']]['_id'],
'name': d[u'姓名'],
'gender': d[u'性别'],
'certNumber': d[u'身份证号'],
}
real_names.append(real_name)
local_db.real_name_record.insert_many(real_names)
3. 租约数据
def store_rents(data, local_db):
utc_zone = tz.gettz('UTC')
...
online_db = client.service_cloud
# find all users' _id and phone, make them a dict whose keys are phones
users = {}
for u in local_db.users.find({}, {"_id": 1, "phone": 1}):
users[u['phone']] = u
rents = []
for d in data:
apartment = online_db.apartments.find_one({'name': d[u'公寓名称']})
rooms = online_db.apartments.find_one({"name": d[u'公寓名称'], "rooms.name": d[u'房间号']}, {"rooms.name": 1, "rooms._id": 1})
for r in rooms['rooms']:
if r['name'] == d[u'房间号']:
roomID = r['_id']
break
rent = {
'name': '{}-{}'.format(d[u'公寓名称'], d[u'房间号']),
'createAt': datetime.datetime.utcnow(),
'apartmentID': apartment['_id'],
'roomID': roomID,
'renterID': users[d[u'手机号']]['_id'],
'rentBegin': d[u'起始时间'].astimezone(utc_zone),
'rentEnd': d[u'到期时间'].astimezone(utc_zone),
}
rents.append(rent)
local_db.rent.insert_many(rents)
4. 订单数据
def store_orders(data, local_db):
utc_zone = tz.gettz('UTC')
# find all users' _id and phone, make them a dict whose keys are phones
users = {}
for u in local_db.users.find({}, {'_id': 1, 'phone': 1}):
users[u['phone']] = u
# find all rents' _id and renterID, make them a dict whose keys are renterIDs
rents = {}
for r in local_db.rent.find({}, {'_id': 1, 'renterID': 1, 'apartmentID': 1, 'roomID': 1, 'ownerID': 1}):
rents[r['renterID']] = r
orders = []
for d in data:
renter_id = users[d[u'手机号']]['_id']
rent_id = rents[renter_id]['_id']
if d[u'定金'] > 0:
order = {
'name': '{}-{} reserved'.format(d[u'公寓名称'], d[u'房间号']),
'createdAt': datetime.datetime.utcnow(),
'rentID': rent_id,
'apartmentID': rents[renter_id]['apartmentID'],
'roomID': rents[renter_id]['roomID'],
'ownerID': rents[renter_id]['ownerID'],
'renterID': renter_id,
'rentBegin': d[u'起始时间'].astimezone(utc_zone),
'rentEnd': d[u'起始时间'].astimezone(utc_zone),
'rentStyle': d[u'付租方式'],
'fee': d[u'定金'],
}
orders.append(order)
time_pairs = calculate_time_pairs(d[u'起始时间'], d[u'到期时间'], d[u'付租方式'], d[u'租期'])
for i in range(len(time_pairs)):
order = {
'name': '{}-{} signed'.format(d[u'公寓名称'], d[u'房间号']),
'createdAt': datetime.datetime.utcnow(),
'rentID': rent_id,
'apartmentID': rents[renter_id]['apartmentID'],
'roomID': rents[renter_id]['roomID'],
'ownerID': rents[renter_id]['ownerID'],
'renterID': renter_id,
'rentBegin': time_pairs[i][0].astimezone(utc_zone),
'rentEnd': time_pairs[i][1].astimezone(utc_zone),
'rentStyle': d[u'付租方式'],
'fee': d[u'定金'],
}
if i == 0:
order['depositFee'] = d[u'押金']
orders.append(order)
local_db.orders.insert_many(orders)
四. 后记
数据分析工作涉及到的面比较广,据说经常跟数据打交道的工程师,一大半时间都花在了数据整理上,这个过程虽然繁琐,不如构建模型那样酷炫吸引人,但它是很重要的一个环节,一定要掌握好。