首页 > 技术文章 > Kaggle案例分析1--Bestbuy

zhao441354231 2016-10-25 22:57 原文

1. 引言

Kaggle是一个进行数据挖掘和数据分析在线竞赛网站, 成立于2010年. 与Kaggle合作的公司可以提供一个数据+一个问题, 再加上适当的奖励, Kaggle上的计算机科学家和数据科学家们(也可能是像我这样的菜鸟)将会领取任务, 提供自己的解决方案. 你在提交自己的解决方案后, 在截止日期之前都可以做出修改. 全世界的人都可以在Kaggle上提供自己的解决方案, 充分发挥了集体智慧.

但是作为一个新手, 要先明白其中的套路(一切都是套路). 万事入门难, 最稳妥的方法是先对以往的项目中别人的开源解决方案进行分析, 学习, 明白套路之后, 再投入到这汪洋大海当中. 

本文今天分析的是一个时间区间为2012.08.18-2012.09.30的项目, 项目内容为: 基于用户的搜索请求历史记录, 预测他们所感兴趣的Xbox游戏. 奖励金额为$600, 详情见这里.

图1 项目在Kaggle上的展示 

2. 数据集描述

为了对数据有个直观的印象, 图2和图3分别截取了部分训练/测试集数据进行展示. 从图中我们可以看出, 训练集包含6列(42365行数据, 表示有42365条用户历史查询数据), 属性值分别为:

  1. user(用户的ID)
  2. sku(用户点击商品的库存量单元(可以理解为商品的ID, 如2032076)))
  3. category(此商品所属范畴, 如abcat0701002)
  4. query(用户搜索所用的关键词, 如"gears of war")
  5. click_time(商品被点击时的时间, 如"2011-10-09 17:22:56.101")
  6. query_time(商品搜索时间, 如"2011-10-09 17:21:42.917").

测试集(test.csv, 共有28241条数据)与训练集相比, 缺少了sku这一项, 也就是用户点击的商品的编号, 我们的目的就是通过其他的5个属性预测哪个sku会被用户点击!  需要注意的是:  这里并不能确保用户的点击来自于他的查询结果, 这里只能记录每个用户查询时的时间, 点击的时间, 而且点击的时间一定在查询时间之后的五分钟内.

图2 部分训练集

图3 部分测试集

3. FindBoat的实现

3.1 基本思路      

FindBoat给出了他对这个问题的解决方案, 他的解决方案可能不是最好的, 但是我们这里对他的解决方案进行分析, 可以学习到处理这类问题的一般思路. 他的思路很简单: 就是试着将查询和sku相匹配,  忽略了其他的特征, 例如时间, 游戏的名称等. 初始的方法是生成一个map, key代表用户的query, 值同样是一个map, 存储了用户点击的游戏以及此游戏被点击的频率, 如果少于5个游戏, 使用最popular的游戏来fill the gap. 我使用了一个优化来修正用户的查询, 因为这些查询包含了很多的错误或者短语, 我使用Levenshtein来计算两个查询之间的相似度. threshold被选择用来在cross validation set上进行测试. 最后, 我得到了74.3%的准确率, 而最高的准确率是78.9%.

训练集中有42365条数据, 每条数据里面包含了6个属性, 我们的目的是根据用户查询所产生的query, click_time, query_time, category, 以及用户编号user这5个属性来预测用户将要购买的物品(用sku属性表示,形如2375195, 可以理解为商品的编号). 首先对这5个属性进行分析, category表示物品的范畴, 我们是想对用户要购买的物品进行预测, 这里在测试集中怎么会出现物品范畴这个属性呢? 经过实验发现, 在训练集和测试集中,这个属性都是只取了一个值, 即: 'abcat0701002', 因此这个属性没有一点作用, 把它直接忽略(拿到数据之后, 首先要做的就是分析数据), 再看user这个属性, 在训练集当中, 同一用户可能会有多条数据(他买了多个东西), 实验发现: 训练集中, 虽然有42365条购买记录, 但是只有38024个客户, 有3509个客户至少买了2个商品, 最多时, 有2个客户('039fd968434e9f9e28bcdab06c54d955d1e4e322'和'5efc4486fb16f4796d97d5dfa061d3297b8ffaa9')分别对应了8条交易数据. 下图分别为这两个客户对应的8条交易数据, 我这里把它拿出来主要是想看看: 1)对同一用户重复购买同一件商品是一条交易记录还是多条; 2) 分析是否可以从单用户的购买数据中挖掘有用的信息, 总结出什么规律.

从图4的Index_4-5这两条交易数据可以看出: 搜索时间是相同的, 点击时间不同, 说明这个用户搜索了一次, 在搜索结果里面买了两件东西, 搜索关键词是'Batman', 买的两个东西是'2173065'和'2703101'; 比如我们在淘宝上搜"男鞋", 搜索结果出来的肯定是各式各样的男鞋, 然后我们买了一个休闲鞋, 一个运动鞋, 但是无论是什么, 它们都属于鞋, 相似度比较大; 同样道理, 此客户搜索'Batman', 买了'2173065'和'2703101', 说明这两个商品之间的相似度比较高, 互相之间有可能具有可替代性, 这是我们挖掘出来的一条有用的信息. 同样, 在图5中, 同样是Index_4-5这两条交易数据, 客户搜索了'Halo 4', 买了'2856544'和'2856517'这两个商品, 这两个商品之间可能也具有比较大的相似性.进一步, 如果把对应'2856544'和'2856517'这两个商品的交易记录都调出来, 对他们的query进行分析, 说不定还可以分析出来一些query之间的相关性. 这些信息怎么用? 能不能用? 我还没有想好!

图4 第一个用户的交易数据

图5 第二个用户的交易数据

再看查询时间和点击时间这两个属性, 一般在查询5分钟之内会有点击, 这两个属性看似对预测没有多大作用. 剩下的就是query这个属性了, 这个属性是最重要的属性, 它与待预测物品直接相关. 

 

3.2 代码实现

bestbuy.py

#!/usr/bin/env python
import csv  # 用于读取csv格式的数据
import re
import operator
import Levenshtein  # 用于计算字符串之间的Levenshtein距离(又称编辑距离)

# 将下载下来的训练/测试数据集放在data文件夹中(data文件夹与本程序同路径)
__train__ = './data/train.csv' __test__ = './data/test.csv'
# 此函数对csv数据进行读取, path: csv文件路径; cols: 要读取的列索引;
# ignore_header=True : 头部首行不读取 def read_data(path, cols, ignore_header=True): csv_file_object = csv.reader(open(path, 'rb')) if ignore_header: # 数据的首行为属性值,跳过首行 header = csv_file_object.next() x = [] for row in csv_file_object: r = [] for col in cols: r.append(row[col]) x.append(r) return x
# 对字符串进行规则化,大写都变成小写,去除空格,去除除数字/字母之外的字符
def string_normalize(s): res = s.lower() # 大写都弄成小写 res = res.replace(' ', '') # 去除空格 res = ''.join(c for c in res if c.isalnum()) return res
# 对数据进行预处理(主要是对搜索关键词进行预处理)
def preprocess_data(raw_data, col): for row in raw_data: row[col] = string_normalize(row[col]) # 计算keys里面与query之间最相似的一个key def best_match_key(keys, query): similarity = 0 best_key = None for key in keys: sim = Levenshtein.ratio(key, query) # 计算两个查询字符串之间的Levenshtein相似度 if sim > similarity: similarity = sim best_key = key return (best_key, similarity) def create_match(x, thresh=.85): match = {} for row in x: matched_key = None sku, query = row # Fuzzy matching. best_key, similarity = best_match_key(match.keys(), query) if similarity > thresh: matched_key = best_key else: match[query] = {sku : 1} if matched_key is None: continue if not match[matched_key].has_key(sku): match[matched_key][sku] = 1 else: match[matched_key][sku] += 1 # Sorts the dictionary. for key in match.keys(): tmp_dict = match[key] tmp_dict = sorted(tmp_dict.iteritems(), key=operator.itemgetter(1)) tmp_dict.reverse() match[key] = tmp_dict return match
# 统计训练集当中每个sku出现的次数,
def get_top(x): sku_count_dict = {} for row in x: if not sku_count_dict.has_key(row[0]): sku_count_dict[row[0]] = 1 else: sku_count_dict[row[0]] += 1 sorted_dict = sorted(sku_count_dict.iteritems(), key=operator.itemgetter(1)) sorted_dict.reverse() res = [] for i in range(len(sorted_dict)): res.append(sorted_dict[i][0]) return res; def predict(match, top, query, k, thresh=.7): res = [] matched_key, similarity = best_match_key(match.keys(), query) # if similarity < 0.8: # print 'matched_key = %s, query = %s, sim = %s' \ # % (matched_key, query, similarity) if similarity > thresh: for i in range(min(k, len(match[matched_key]))): res.append(match[matched_key][i][0]) if len(res) < k: for i in range(len(top)): if top[i] not in res: res.append(top[i]) if len(res) == k: break return res
# 程序从这里开始执行
if __name__ == '__main__': # 读取训练数据 print 'Reading and preprocessing data...' x = read_data(__train__, [1, 3]) # [1,3]的含义是只读取sku和query这两列属性值 preprocess_data(x, 1) # 将数据划分成训练集+验证集 l = int(len(x) * 1) x_cv = x[l - 10 : :] x = x[0 : l] top = get_top(x) # 出现的sku次数从大到小排序 # Predicts on cv. print 'Predicting...' k = 5 # thresh_match = [0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1] # thresh_predict = [0, 0.2, 0.4, 0.6, 0.7, 0.8] thresh_match = [0.75] thresh_predict = [0] best_match = None thresh1 = 0 thresh2 = 0 accuracy = -1 for t1 in thresh_match: for t2 in thresh_predict: match = create_match(x, t1) p_cv = [] for row in x_cv: q = row[1] p_cv.append(predict(match, top, q, k, t2)) correct = 0. for i in range(len(x_cv)): if x_cv[i][0] in p_cv[i]: correct += 1. ac = correct / len(x_cv) print 't1 = %f, t2 = %f, accuracy = %f' % (t1, t2, ac) if ac > accuracy: accuracy = ac thresh1 = t1 thresh2 = t2 best_match = match print 'thresh1 = %f, thresh2 = %f, accuracy = %f' \ % (thresh1, thresh2, accuracy) # 读取测试数据 x_test = read_data(__test__, [2]) preprocess_data(x_test, 0) # 数据的预处理 # 对测试数据进行预测 res = [] k = 5 for row in x_test: q = row[0] res.append(predict(best_match, top, q, k, thresh2))
# 将结果写入result.csv当中 open_file_object
= csv.writer(open("./data/result.csv", "wb")) open_file_object.writerow(['sku']) for p in res: open_file_object.writerow([' '.join(p)])

1. Levenshtein距离:又称编辑距离, 指的是两个字符串之间由一个转换成为另外一个所需要的最小编辑操作的次数(许可的编辑操作有: 替换字符, 插入字符, 删除字符).

Levenshtein是个第三方模块, 当我们import一个模块的时候,Python会在指定的路径下面去搜索对应的.py文件,如果找不到,就会报出: no module named *** 的错误.默认情况下,Python解释器会搜索当前目录,所有已安装的内置模块和第三方模块,搜索路径放在了sys模块里面的path变量里面:

 

import sys

sys.path
Out[4]: 
['',
 'D:\\Anaconda2\\python27.zip',
 'D:\\Anaconda2\\DLLs',
 'D:\\Anaconda2\\lib',
 'D:\\Anaconda2\\lib\\plat-win',
 'D:\\Anaconda2\\lib\\lib-tk',
 'D:\\Anaconda2',
 'd:\\anaconda2\\lib\\site-packages\\sphinx-1.4.1-py2.7.egg',
 'd:\\anaconda2\\lib\\site-packages\\setuptools-23.0.0-py2.7.egg',
 'D:\\Anaconda2\\lib\\site-packages',
 'D:\\Anaconda2\\lib\\site-packages\\win32',
 'D:\\Anaconda2\\lib\\site-packages\\win32\\lib',
 'D:\\Anaconda2\\lib\\site-packages\\Pythonwin',
 'D:\\Anaconda2\\lib\\site-packages\\IPython\\extensions',
 'C:\\Users\\WXC\\.ipython']

 

 

 

如果需要添加自己的搜索目录,有两种方法: 1)直接修改sys.path, 将搜索目录添加进去: 

sys.path.append('your path')

 

 

上面的这种方法可以放在程序的最前面, 在运行时候修改,运行结束后失效; 2) 设置环境变量PYTHONPATH.

 

 

2. python参数的传递[1](以preprocess_data()函数为例):

 1 def add(num):
 2     num[0] = 10
 3 
 4 def add2(num):
 5     num = 10
 6 
 7 d = [2]
 8 add(d)
 9 print d     # [12]
10 
11 d2 = 2
12 add2(d2)
13 print d2    # 2

针对这个问题的解释, 主要参考了文章[1], 文章从可更改对象(mutable object)和不可更改对象(immutable object)的角度来说明问题. 比如在Python中, list为可更改对象, tuple为不可更改对象, 上面代码的Line_5, num和d2同样指向了内存中原始的2这个对象, 由于2是个不可覆盖的对象, 因此num被赋值为10后, 具体的实行是创建了一个新的对象10, 将10这个对象束定在num上. 而d2依旧是指向2, 保存的值没有变化; 而Line_2中, 更改list的第一个元素的值, 因为list的值是可以改变的, 所以, 第一个元素变更为10, num和d指向的list对象不变, 但是list的内容发生了变化.

图4 值传递情况

再看一个例子: b是一个list, 经过changeValue函数运算后, b的值并没有发生任何改变, 原因和上面的解释相同. a和b都指向了list: [1,8,9,2,0], 但是row指向了一个int数据, 从上面的分析可知, 此int数据是不可覆盖的对象, 因此, row被赋值为新创建的对象: 10, a里面并没有发生任何变化.

1 def changeValue(a):
2     for row in a:
3         row = 10    # row的类型为:<type 'int'>
4 
5 b = [1,8,9,2,0]
6 changeValue(b)    # b的值并没有发生改变
7 print b
8 [1, 8, 9, 2, 0]

 下面这个例子, row是一个list, row指向的内容同a[*]指向相同的内容, list的可覆盖性保证了对row进行修改会影响到a的值以及c的值. preprocess_data()函数里面的情况和下面这个示例程序的情形相同.

1 def changeValue(a):
2     for row in a:
3         row[0] = 10
4 
5 c = [[1,2],[5,8]]
6 changeValue(c)
7 print c
8 [[10, 2], [10, 8]]

3. python排序(sort()和sorted())

如果要对list进行排序, 可以使用list的sort()方法: L.sort(cmp=None, key=None, reverse=False); 注意, sort是在原位进行重新排序: a的值变化了, 没有返回值(b的值为空)

1 a = [5,1,0,2,5]
2 b = a.sort()
3 print a
4 [0,1,2,5,5]
5 print b
6 None

sorted()函数是python里面的内建函数,语法:sorted(iterable, cmp=None, key=None, reverse=False) --> new sorted list

iterable: 可迭代的对象(如list); cmp: 用于比较的函数, 比较什么由key决定; key: 用列表元素的某个属性和函数作为关键词进行排序; reverse: 排序规则, 倒序或者正序.

a = [5,1,0,2,5]

print sorted(a)
[0, 1, 2, 5, 5]

print a
[5, 1, 0, 2, 5]

程序 tmp_dict = sorted(tmp_dict.iteritems(), key=operator.itemgetter(1)) 中用到了operator模块,下面介绍这个模块: 

operator模块函数[4]: 参考key的使用非常广泛, 因此python提供了一些方便的函数使得访问方法更加容易和快速. operator模块是python中内置的操作符函数接口,定义了很多算术和比较内置操作的函数; 同时, 它还定义了一些用作map(),sorted()等函数的可以进行快速字段抽取的函数,如:itemgetter, attrgetter, methodcaller等.

operator.itemgetter(item): 从字面上来说,itemgetter的意思是"获取元素",即从list或者string等中把指定的元素提取出来.

  • 执行f = itemgetter(2) ,调用f(r), 返回r[2];
  • 执行g = itemgetter(2,5,3) ,调用g(r), 返回(r[2],r[5],r[3])
from operator import itemgetter
# 后面的操作对象也要用()圈起来噻
print itemgetter(1)([1,2,3])   # 2
print itemgetter(1)('ABCDEFR')  # B
print itemgetter(1,3,4)('ABCDEFR')  # ('B', 'D', 'E')

itemgetter如何配合map使用呢?  getcount = itemgetter(1) 是个函数啊?!对map而言,将getcount应用到inventory的每个item上.对sorted函数的key参数来说,接收的值是函数,无论是匿名函数还是其他函数.

#! /usr/bin/env python
#coding=utf-8

from operator import itemgetter

inventory = [('apple',3),('banana',2),('pear',5),('orange',1)]
getcount = itemgetter(1)

print map(getcount, inventory)
输出: [3, 2, 5, 1]
print sorted(inventory, key = getcount)  # 用getcount指定key
输出: [('orange', 1), ('banana', 2), ('apple', 3), ('pear', 5)]

看下面的例子, students是个list, 每个元素是个tuple, 三列, 可以使用key来指定用那一列的数据进行排序:

In[64]: students = [('Jace','A',10),('Mara','A',52),('Mich','B',5)]
students
Out[66]: [('Jace', 'A', 10), ('Mara', 'A', 52), ('Mich', 'B', 5)]
sorted(students, key=lambda stud:stud[2])
Out[67]: [('Mich', 'B', 5), ('Jace', 'A', 10), ('Mara', 'A', 52)]

tmp_dict = sorted(tmp_dict.iteritems(), key=operator.itemgetter(1)) 理解: tmp_dict.iteritems: 将tmp_dict从dict数据类型转成list类型,然后使用第二个元素进行从小到大排序.

 

4. lambda表达式和map/reduce/filter函数

lambda表达式:考虑一种情况: 你需要定义一个函数, 这个函数写好之后, 可能只会调用一次, 这时你可能不想专门来定义一个单独的函数, 这个时候就可以使用lambda表达式来定义一个匿名函数

1 f = lambda x, y : x + y    # lambda表达式, : 左边为输入参数, : 右边为运算式
2 
3 f(1,5)
4 Out[60]: 6

上面说了, lambda表达式的一个重要用处是我们不用专门定义函数, 上面的例子中, 我们又定义了一个函数f, 只是比def那种定义方式更加简便些; 再看下面的例子:

1 p = map(lambda x: x*x, [y for y in range(10)])
2 
3 p
4 Out[62]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

map(function_to_apply, list_of_inputs) 是将函数function_of_apply应用到list_of_inputs的每个元素上,并且将每个元素对应的输出作为list返回.

filter(function, iterable, ...) 是将函数function应用到iterable的每个元素上.

reduce(function, list_of_inputs, [,starting_value]) : 对list_of_inputs里面的item顺序迭代调用function,如果有初始值,可以作为初始值调用.

map函数示例: 实现list中每个元素的平方

# 方式1: 利用循环
items = [1,2,3,4,5]
squared = []
for i in items:
    squared.append(i**2)
# 方式2: 利用匿名函数和map
items = [1,2,3,4,5]
squared = map(lambda x:x**2, items)  # [1,4,9,16,25]

map高级的用法: map的第二个参数为list_of_input, 里面的每个元素都是一个数字, 但是里面的元素就不能为函数吗?

 1 def multiply(x):
 2     return x * x
 3 
 4 def add(x):
 5     return x + x
 6 
 7 funcs = [multiply, add]
 8 for i in range(5):
 9     value = map(lambda x:x(i), funcs) # funcs里面的每个函数带入x,求x(i)就是求multiply(i)和add(i)
10     print value
11 
12 [0, 0]
13 [1, 2]
14 [4, 4]
15 [9, 6]
16 [16, 8]

filter函数示例: filter不是返回函数的结果, 而是返回函数结果为正时对应list元素的值, 从下面的程序可以看到, map返回的是匿名函数的结果, 一个包含了False/True的list, 而filter返回的是对应True位置处的原ls里面的值.filter的含义是"过滤",就是把输入ls里面的值过滤一下.

#! /usr/bin/env python
#coding=utf-8

ls = range(-3, 3)
print ls

less_than_zero1 = map(lambda x:x<0, ls)
print less_than_zero1

less_than_zero2 = filter(lambda x:x<0, ls)
print less_than_zero2

[-3, -2, -1, 0, 1, 2]
[True, True, True, False, False, False]
[-3, -2, -1]

reduce函数示例:

product = reduce(lambda x, y: x*y, [1,2,5,1])

product
Out[15]: 10

 

参考文献: 

[1] Python的函数参数传递: 传值? 引用? : http://winterttr.me/2015/10/24/python-passing-arguments-as-value-or-reference/

[2] [引自知乎] Python的函数是怎么传递参数的? https://www.zhihu.com/question/20591688

[3] python sort、sorted高级排序技巧: http://www.jb51.net/article/57678.htm

[4] 9.9. operator - Standard operators as functions: https://docs.python.org/2/library/operator.html

[5] G:\SomeProjects\Kaggle-master2

 

推荐阅读