作者:方跃文
Email: fyuewen@gmail.com
时间:始于2017年9月12日, 结束写作于
第二章笔记始于2017年9月12日,第一阶段结束语2017年9月28日晚(剩余两个分析案例)
时间: 2017年9月12日
尽管数据处理的目的和领域都大不相同,但是利用python数据处理时候基本都需要完成如下几个大类的任务:
1) 与外界进行数据交互
2) 准备:对数据进行清理、修整、规范化、重塑、切片切块
3) 转换:对数据集做一些数学和统计运算以产生新的数据集,e.g. 根据分组变量对一个大表进行聚合
4) 建模和计算:将数据跟统计模型、机器学习算法联系起来
5) 展示:创建交换式的或者静态的图片或者文字摘要。
ch02中的usagov_bitly_data2012-03-16-1331923249.txt是bit.ly网站收集到的每小时快照数据。文件中的格式为JavaScript Object Notation (JSON)——一种常用的web数据格式。例如如果我们只读取某个文件中的第一行,那么所看到的结果是下面这样:
In [1]:
path = "./pydata-book/ch02/usagov_bitly_data2012-03-16-1331923249.txt"
open(path).readline()
Out[1]:
In [9]:
print(path)
print(type(path))
In [6]:
import json
datach02= [json.loads(line) for line in open(path)]
python有许多内置或第三方模块可以将JSON字符转换成python字典对象。这里,我将使用json模块及其loads函数逐行加载已经下载好的数据文件:
In [3]:
import json
path = "./pydata-book/ch02/usagov_bitly_data2012-03-16-1331923249.txt"
records = [json.loads(line) for line in open(path)]
上面最后一行表达式,叫做“列表推导式 list comprehension”。这是一种在一组字符串(或一组别的对象)上执行一条相同操作(如json.loads)的简洁方式。在一个打开的文件句柄上进行迭代即可获得一个由行组成的序列。现在,records对象就成为一组python字典了。
In [4]:
records[0]
Out[4]:
In [4]:
records[0]['tz']
Out[4]:
In [10]:
time_zones = [rec['tz'] for rec in records]
然而我们发现上面这个出现了‘tz'的keyerror,这是因为并不是所有记录里面都有tz这个字段,为了让程序判断出来,我们需要加上if语句,即
In [11]:
time_zones = [i['tz'] for i in records if 'tz' in i]
time_zones[:2]
Out[11]:
In [12]:
time_zones = [rec['tz'] for rec in records if 'tz' in rec]
time_zones[:10]
Out[12]:
我们从上面可以看到,的确有些时区字段是空的。此处,为了对时区进行计算,介绍两种办法。
第一种,只使用python标准库
In [7]:
#这种方法是在遍历时区的过程中将计数值保留在字典中:
def get_counts(sequence):
counts = {}
for x in sequence:
if x in counts:
counts[x] += 1
else:
counts[x] = 1
return counts
#今天回头看这段代码发现看的不是很明白,特别是我在下面这个cell中,
#利用了上述的代码,发现这个结果让人看了有点费解。
#11th Jan. 2018
In [21]:
def get_counts(sequence):
counts = {}
for x in sequence:
if x in counts:
counts[x] += 1
else:
counts[x] = 1
return counts
sequence1={1,23,434,53,23,24}
a=get_counts(sequence1)
a[23]
#11th Jan. 2018
Out[21]:
非常了解Python标准库的话,可以将上述代码写得更加精简:
In [8]:
from collections import defaultdict
def get_counts2(sequence):
counts = defaultdict(int) #所有的值都会被初始化为0
for x in sequence:
counts[x] += 1
return counts
上述两种写法中,都将代码写到了函数中。这样的做法,是为了代码段有更高的可重要性,方便对时区进行处理。此处我们只需要将时区 time_zones 传入即可:
In [9]:
def get_counts(sequence):
counts = {}
for x in sequence:
if x in counts:
counts[x] += 1
else:
counts[x] = 1
return counts
counts = get_counts(time_zones)
counts['America/New_York']
Out[9]:
In [10]:
len(time_zones)
Out[10]:
如果要想得到前10位的时区及其计数值,我们需要用到一些有关字典的处理技巧:
In [11]:
def top_counts(count_dict, n =10):
value_key_pairs = [(count, tz) for tz, count in count_dict.items()]
value_key_pairs.sort()
return value_key_pairs[-n:]
top_counts(counts)
Out[11]:
我们还可在python标准库中找到collections.Counter类,它能使这个任务更加简单:
In [12]:
from collections import Counter
counts = Counter(time_zones)
In [13]:
counts.most_common(10)
Out[13]:
第二种,用pendas对时区进行计数
DataFrame 是pendas中最重要的数据结构,它用于将数据表示为一个表格。从一组原始记录中创建DataFrames是很简单的:
In [14]:
from pandas import DataFrame, Series
import pandas as pd; import numpy as np
frame = DataFrame(records)
frame
Out[14]:
In [15]:
frame['tz'][:10]
Out[15]:
这里frame的输出形式是摘要试图(summary view),主要是用于较大的DataFrame对象。frame['tz']所返回的Series对象有一个value_counts方法,该方法可以让我们得到所需的信息:
In [16]:
tz_counts = frame['tz'].value_counts()
tz_counts[:10]
Out[16]:
现在,我们想用matplotlib为这段数据生成一张图片。为此,我们先给记录中未知或缺失的时区天上一个替代值。fillna 函数可以替换缺失值(NA),而未知值(空字符串)可以通过布尔型数据索引加以替换:
In [17]:
clean_tz = frame['tz'].fillna('Missing')
In [18]:
clean_tz[clean_tz == ''] = 'Unknown'
In [19]:
tz_counts = clean_tz.value_counts()
In [20]:
tz_counts[:10]
Out[20]:
利用tz_counts对象的plot方法,我们开得到一张水平条形图:
In [21]:
%matplotlib inline
tz_counts[:10].plot(kind='barh', rot=0)
Out[21]:
我们还可以对这种数据进行很多的处理。比如说,a字段含有执行URL短缩操作的浏览器、设备、应用程序的相关信息:
In [22]:
frame['a'][1]
Out[22]:
In [23]:
frame['a'][50]
Out[23]:
In [24]:
frame['a'][51]
Out[24]:
将这些“agent"字符串(即浏览器的USER——AGENT)中的所有信息都解析出来是一件挺枯燥的工作。不过我们只要掌握了python内置的字符串函数和正则表达式,事情就好办许多了。
比如,我们可以将这种字符串的第一节(与浏览器大致呼应)分离出来并得到另外一份用户行为摘要:
In [25]:
results = Series([x.split()[0] for x in frame.a.dropna()])
In [26]:
results[:5]
Out[26]:
In [27]:
results.value_counts()[:8]
Out[27]:
现在假设我们想按Windows和非Windows用户对时区统计信息进行分解。为了简单,我们假定只要agent字符串中包含有"Windows"就认为该用户为Windows用户。由于有的agent确实,我们首先将它们从数据中移除:
In [28]:
cframe = frame[frame.a.notnull()]
接下来,根据a值计算出各行是否是Windows:
In [29]:
operating_system = np.where(cframe['a'].str.contains('Windows'), 'Windows','Not Windows')
In [30]:
operating_system[:5] #注意这句代码执行后的输出跟原书不同
Out[30]:
接下来可以根据时区和新的到的操作系统列表对数据进行分组了:
In [31]:
by_tz_os = cframe.groupby(['tz', operating_system])
然后通过size对分组结果进行计数(类似于上面的value_counts函数),并利用unstack对计数结果进行重塑:
In [32]:
agg_counts = by_tz_os.size().unstack().fillna(0)
In [33]:
agg_counts[:10]
Out[33]:
最后我们来选取最常出现的时区。为了达到这个目的,我们根据agg_counts中的行数构造了一个间接索引数组:
In [34]:
#用于按升序排列
indexer = agg_counts.sum(1).argsort()
In [35]:
indexer[:10]
Out[35]:
然后我们通过过take按照这个舒徐截取了最后的10行:
In [36]:
count_subset = agg_counts.take(indexer)[-10:]
In [37]:
count_subset
Out[37]:
这里可以生成一张条形图。我们将使用stacked = True来生成一张堆积条形图:
In [38]:
%matplotlib inline
normed_subset = count_subset.div(count_subset.sum(1), axis=0)
In [39]:
normed_subset.plot(kind='barh', stacked = True)
Out[39]:
这里所用到的所有方法都会在本书后续的章节中详细讲解。(我觉得这句话作者应该早点讲,害的我一直不敢继续读下去,原来这只是一个长长的说明案例啊)
GroupLens Research 采集了从上世纪九十年代到本世纪初MovieLens用户提供的电影评分数据。这些数据中包括电影评分、电影元数据(风格和年代)以及用户的人口学统计数据(性别年龄等)。基于机器学习算法的推荐系统一般都会对此类数据感兴趣。虽然这本书不会详细介绍机器学习技术,不会可以让我们学习如何对数据进行切片切块以满足需求。
MovieLens 1M数据集包含了来自6000名用户对4000部电影的100万条评分数据。它分为三个表:评分、用户信息和电源信息。可以通过pandas.read_table将各个表读到一个pandas DataFrame对象中:
In [40]:
import pandas as pd
unames = ['user_id', 'gender', 'age', 'occupation', 'zip']
users = pd.read_table('pydata-book/ch02/movielens/users.dat', sep='::',
header=None, names = unames)
rnames = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_table('pydata-book/ch02/movielens/ratings.dat', sep='::',
header=None, names = rnames)
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('pydata-book/ch02/movielens/movies.dat', sep='::',
header=None, names = mnames)
In [41]:
users[:5]
Out[41]:
In [42]:
ratings[:5]
Out[42]:
In [43]:
movies[:5]
Out[43]:
In [88]:
ratings[:10]
Out[88]:
注意,数据和职业是以编码形式给出的。他们的具体含义请参考该数据集的README文件。
分析散步在三个表中的数据不是件轻松的事情。假设我们想根据性别和年龄计算某电影的平均得分,如果将所有数据都合并为一个表中的话问题就简单多了。我们先用pandas的merge函数将ratings跟users合并到一起,然后再将movies合并进去。pandas会根据列明的重叠情况推断出哪些列是合并(或连接)键
In [45]:
data = pd.merge(pd.merge(ratings, users), movies)
In [89]:
data[:10]
Out[89]:
现在我们就可以根据任意个域用户或电源属性对评分数据进行聚合操作了。为了按性别计算每部电源的平均分,我们可以使用pivot_table方法:
In [64]:
#书中原文的代码是
mean_ratings = data.pivot_table('rating',
rows='title', cols='gender',aggfunc='mean')
显然上面执行通不过,从错误信息看根本没有‘rows’这个参数的功能。我本来想放弃这个代码了,但是还是留了个心眼,去Google了一下,发现去年有人讨论了这个问题:stackoverflow地址。
解决方案是
将
mean_ratings = data.pivot_table('rating', rows='title', cols='gender', aggfunc='mean')
改成
mean_ratings = data.pivot_table('rating', index='title', columns='gender', aggfunc='mean')
原因是:
书中的代码是旧的并且已经被移除了的语法。
In [90]:
mean_ratings = data.pivot_table('rating', index='title',
columns='gender', aggfunc='mean')
In [91]:
mean_ratings[:5]
Out[91]:
上述操作产生了另一个DataFrame,其内容为电源平均得分,行作为电影名。列标为性别。现在,我们打算过滤掉评分数据不够250条的电影。为了达到这个目的,可以先对title进行分组,然后利用size()得到的一个含有各个电影分组大小的Series对象:
In [68]:
ratings_by_title = data.groupby('title').size()
In [69]:
ratings_by_title[0:10]
Out[69]:
In [71]:
active_titles = ratings_by_title.index[ratings_by_title >= 250]
In [72]:
active_titles
Out[72]:
上述所得到的索引中含有评分数据大于250条的电影名称,然后我们就可以据此从前面的mean_ratings中选取所需的行了:
In [105]:
mean_ratings = mean_ratings.ix[active_titles]
#书中原文用了mean_ratings.ix 但是ix其实已经被弃用了
In [107]:
mean_ratings = mean_ratings.loc[active_titles]
In [108]:
mean_ratings
Out[108]:
为了了解女性观众最喜欢的电源,我们可以对F列降序:
In [109]:
top_female_ratings = mean_ratings.sort_index(by='F', ascending=False)
In [110]:
top_female_ratings = mean_ratings.sort_values(by='F', ascending=False)
In [111]:
top_female_ratings[:10]
Out[111]:
In [112]:
mean_ratings['diff'] = mean_ratings['M'] - mean_ratings['F']
按‘diff'排序即可得到分歧最大且女性观众更喜欢的电影:
In [93]:
sorted_by_diff = mean_ratings.sort_index(by = 'diff')
In [113]:
sorted_by_diff = mean_ratings.sort_values(by='diff')
In [114]:
sorted_by_diff[:15]
Out[114]:
对排序结果反序并取出10行,得到的就是男性更喜欢的电影啦:
In [115]:
sorted_by_diff[::-1][:15]
Out[115]:
如果只想找出分歧最大的电影并且不考虑性别因素,则可以计算得分数据的方差或者标准差:
In [127]:
#根据电影名称分组的得分数据的标准差
rating_std_by_title = data.groupby('title')['rating'].std()
In [128]:
#根据active_title 进行过滤
rating_std_by_title = rating_std_by_title.loc[active_titles]
In [129]:
#根据值对Series进行降序排列
rating_std_by_title.order(ascending=False)[:10]
In [130]:
#上一个书中源代码中的order已经被弃用。最新版的可以使用sort_values
rating_std_by_title.sort_values(ascending=False)[:10]
Out[130]:
作者按:
可能你已经注意到了,电影分类是以“|”分隔符给出的。如果想对电源的分类进行分析的话,就需要先将其转换成更有用的形式才行。本书后续章节将给出处理方法,到时还需用到这个数据。
In [ ]: