本书正文的最后一章,我们来看一些真实世界的数据集。对于每个数据集,我们会用之前介绍的方法,从原始数据中提取有意义的内容。展示的方法适用于其它数据集,也包括你的。本章包含了一些各种各样的案例数据集,可以用来练习。
案例数据集可以在Github仓库找到,见第一章。
2011年,URL缩短服务Bitly跟美国政府网站USA.gov合作,提供了一份从生成.gov或.mil短链接的用户那里收集来的匿名数据。在2011年,除实时数据之外,还可以下载文本文件形式的每小时快照。写作此书时(2017年),这项服务已经关闭,但我们保存一份数据用于本书的案例。
以每小时快照为例,文件中各行的格式为JSON(即JavaScriptObjectNotation,这是一种常用的Web数据格式)。例如,如果我们只读取某个文件中的第一行,那么所看到的结果应该是下面这样:
importjsonpath='datasets/bitly_usagov/example.txt'records=[json.loads(line)forlineinopen(path)]现在,records对象就成为一组Python字典了:
In[12]:time_zones=[rec['tz']forrecinrecords]---------------------------------------------------------------------------KeyErrorTraceback(mostrecentcalllast)
In[13]:time_zones=[rec['tz']forrecinrecordsif'tz'inrec]In[14]:time_zones[:10]Out[14]:['America/New_York','America/Denver','America/New_York','America/Sao_Paulo','America/New_York','America/New_York','Europe/Warsaw','','','']只看前10个时区,我们发现有些是未知的(即空的)。虽然可以将它们过滤掉,但现在暂时先留着。接下来,为了对时区进行计数,这里介绍两个办法:一个较难(只使用标准Python库),另一个较简单(使用pandas)。计数的办法之一是在遍历时区的过程中将计数值保存在字典中:
defget_counts(sequence):counts={}forxinsequence:ifxincounts:counts[x]+=1else:counts[x]=1returncounts如果使用Python标准库的更高级工具,那么你可能会将代码写得更简洁一些:
fromcollectionsimportdefaultdictdefget_counts2(sequence):counts=defaultdict(int)#valueswillinitializeto0forxinsequence:counts[x]+=1returncounts我将逻辑写到函数中是为了获得更高的复用性。要用它对时区进行处理,只需将time_zones传入即可:
In[17]:counts=get_counts(time_zones)In[18]:counts['America/New_York']Out[18]:1251In[19]:len(time_zones)Out[19]:3440如果想要得到前10位的时区及其计数值,我们需要用到一些有关字典的处理技巧:
deftop_counts(count_dict,n=10):value_key_pairs=[(count,tz)fortz,countincount_dict.items()]value_key_pairs.sort()returnvalue_key_pairs[-n:]然后有:
In[21]:top_counts(counts)Out[21]:[(33,'America/Sao_Paulo'),(35,'Europe/Madrid'),(36,'Pacific/Honolulu'),(37,'Asia/Tokyo'),(74,'Europe/London'),(191,'America/Denver'),(382,'America/Los_Angeles'),(400,'America/Chicago'),(521,''),(1251,'America/New_York')]如果你搜索Python的标准库,你能找到collections.Counter类,它可以使这项工作更简单:
In[22]:fromcollectionsimportCounterIn[23]:counts=Counter(time_zones)In[24]:counts.most_common(10)Out[24]:[('America/New_York',1251),('',521),('America/Chicago',400),('America/Los_Angeles',382),('America/Denver',191),('Europe/London',74),('Asia/Tokyo',37),('Pacific/Honolulu',36),('Europe/Madrid',35),('America/Sao_Paulo',33)]用pandas对时区进行计数从原始记录的集合创建DateFrame,与将记录列表传递到pandas.DataFrame一样简单:
In[25]:importpandasaspdIn[26]:frame=pd.DataFrame(records)In[27]:frame.info()
In[29]:tz_counts=frame['tz'].value_counts()In[30]:tz_counts[:10]Out[30]:America/New_York1251521America/Chicago400America/Los_Angeles382America/Denver191Europe/London74Asia/Tokyo37Pacific/Honolulu36Europe/Madrid35America/Sao_Paulo33Name:tz,dtype:int64我们可以用matplotlib可视化这个数据。为此,我们先给记录中未知或缺失的时区填上一个替代值。fillna函数可以替换缺失值(NA),而未知值(空字符串)则可以通过布尔型数组索引加以替换:
In[31]:clean_tz=frame['tz'].fillna('Missing')In[32]:clean_tz[clean_tz=='']='Unknown'In[33]:tz_counts=clean_tz.value_counts()In[34]:tz_counts[:10]Out[34]:America/New_York1251Unknown521America/Chicago400America/Los_Angeles382America/Denver191Missing120Europe/London74Asia/Tokyo37Pacific/Honolulu36Europe/Madrid35Name:tz,dtype:int64此时,我们可以用seaborn包创建水平柱状图(结果见图14-1):
In[39]:frame['a'][1]Out[39]:'GoogleMaps/RochesterNY'In[40]:frame['a'][50]Out[40]:'Mozilla/5.0(WindowsNT5.1;rv:10.0.2)Gecko/20100101Firefox/10.0.2'In[41]:frame['a'][51][:50]#longlineOut[41]:'Mozilla/5.0(Linux;U;Android2.2.2;en-us;LG-P9'将这些"agent"字符串中的所有信息都解析出来是一件挺郁闷的工作。一种策略是将这种字符串的第一节(与浏览器大致对应)分离出来并得到另外一份用户行为摘要:
In[42]:results=pd.Series([x.split()[0]forxinframe.a.dropna()])In[43]:results[:5]Out[43]:0Mozilla/5.01GoogleMaps/RochesterNY2Mozilla/4.03Mozilla/5.04Mozilla/5.0dtype:objectIn[44]:results.value_counts()[:8]Out[44]:Mozilla/5.02594Mozilla/4.0601GoogleMaps/RochesterNY121Opera/9.8034TEST_INTERNET_AGENT24GoogleProducer21Mozilla/6.05BlackBerry8520/5.0.0.6814dtype:int64现在,假设你想按Windows和非Windows用户对时区统计信息进行分解。为了简单起见,我们假定只要agent字符串中含有"Windows"就认为该用户为Windows用户。由于有的agent缺失,所以首先将它们从数据中移除:
In[45]:cframe=frame[frame.a.notnull()]然后计算出各行是否含有Windows的值:
In[47]:cframe['os']=np.where(cframe['a'].str.contains('Windows'),....:'Windows','NotWindows')In[48]:cframe['os'][:5]Out[48]:0Windows1NotWindows2Windows3NotWindows4WindowsName:os,dtype:object接下来就可以根据时区和新得到的操作系统列表对数据进行分组了:
In[49]:by_tz_os=cframe.groupby(['tz','os'])分组计数,类似于value_counts函数,可以用size来计算。并利用unstack对计数结果进行重塑:
In[50]:agg_counts=by_tz_os.size().unstack().fillna(0)In[51]:agg_counts[:10]Out[51]:osNotWindowsWindowstz245.0276.0Africa/Cairo0.03.0Africa/Casablanca0.01.0Africa/Ceuta0.02.0Africa/Johannesburg0.01.0Africa/Lusaka0.01.0America/Anchorage4.01.0America/Argentina/Buenos_Aires1.00.0America/Argentina/Cordoba0.01.0America/Argentina/Mendoza0.01.0最后,我们来选取最常出现的时区。为了达到这个目的,我根据agg_counts中的行数构造了一个间接索引数组:
#UsetosortinascendingorderIn[52]:indexer=agg_counts.sum(1).argsort()In[53]:indexer[:10]Out[53]:tz24Africa/Cairo20Africa/Casablanca21Africa/Ceuta92Africa/Johannesburg87Africa/Lusaka53America/Anchorage54America/Argentina/Buenos_Aires57America/Argentina/Cordoba26America/Argentina/Mendoza55dtype:int64然后我通过take按照这个顺序截取了最后10行最大值:
In[54]:count_subset=agg_counts.take(indexer[-10:])In[55]:count_subsetOut[55]:osNotWindowsWindowstzAmerica/Sao_Paulo13.020.0Europe/Madrid16.019.0Pacific/Honolulu0.036.0Asia/Tokyo2.035.0Europe/London43.031.0America/Denver132.059.0America/Los_Angeles130.0252.0America/Chicago115.0285.0245.0276.0America/New_York339.0912.0pandas有一个简便方法nlargest,可以做同样的工作:
In[56]:agg_counts.sum(1).nlargest(10)Out[56]:tzAmerica/New_York1251.0521.0America/Chicago400.0America/Los_Angeles382.0America/Denver191.0Europe/London74.0Asia/Tokyo37.0Pacific/Honolulu36.0Europe/Madrid35.0America/Sao_Paulo33.0dtype:float64然后,如这段代码所示,可以用柱状图表示。我传递一个额外参数到seaborn的barpolt函数,来画一个堆积条形图(见图14-2):
#RearrangethedataforplottingIn[58]:count_subset=count_subset.stack()In[59]:count_subset.name='total'In[60]:count_subset=count_subset.reset_index()In[61]:count_subset[:10]Out[61]:tzostotal0America/Sao_PauloNotWindows13.01America/Sao_PauloWindows20.02Europe/MadridNotWindows16.03Europe/MadridWindows19.04Pacific/HonoluluNotWindows0.05Pacific/HonoluluWindows36.06Asia/TokyoNotWindows2.07Asia/TokyoWindows35.08Europe/LondonNotWindows43.09Europe/LondonWindows31.0In[62]:sns.barplot(x='total',y='tz',hue='os',data=count_subset)图14-2最常出现时区的Windows和非Windows用户这张图不容易看出Windows用户在小分组中的相对比例,因此标准化分组百分比之和为1:
defnorm_total(group):group['normed_total']=group.total/group.total.sum()returngroupresults=count_subset.groupby('tz').apply(norm_total)再次画图,见图14-3:
In[65]:sns.barplot(x='normed_total',y='tz',hue='os',data=results)图14-3最常出现时区的Windows和非Windows用户的百分比我们还可以用groupby的transform方法,更高效的计算标准化的和:
MovieLens1M数据集含有来自6000名用户对4000部电影的100万条评分数据。它分为三个表:评分、用户信息和电影信息。将该数据从zip文件中解压出来之后,可以通过pandas.read_table将各个表分别读到一个pandasDataFrame对象中:
importpandasaspd#Makedisplaysmallerpd.options.display.max_rows=10unames=['user_id','gender','age','occupation','zip']users=pd.read_table('datasets/movielens/users.dat',sep='::',header=None,names=unames)rnames=['user_id','movie_id','rating','timestamp']ratings=pd.read_table('datasets/movielens/ratings.dat',sep='::',header=None,names=rnames)mnames=['movie_id','title','genres']movies=pd.read_table('datasets/movielens/movies.dat',sep='::',header=None,names=mnames)利用Python的切片语法,通过查看每个DataFrame的前几行即可验证数据加载工作是否一切顺利:
In[76]:mean_ratings=data.pivot_table('rating',index='title',....:columns='gender',aggfunc='mean')In[77]:mean_ratings[:5]Out[77]:genderFMtitle$1,000,000Duck(1971)3.3750002.761905'NightMother(1986)3.3888893.352941'TilThereWasYou(1997)2.6756762.733333'burbs,The(1989)2.7934782.962085...AndJusticeforAll(1979)3.8285713.689024该操作产生了另一个DataFrame,其内容为电影平均得分,行标为电影名称(索引),列标为性别。现在,我打算过滤掉评分数据不够250条的电影(随便选的一个数字)。为了达到这个目的,我先对title进行分组,然后利用size()得到一个含有各电影分组大小的Series对象:
#SelectrowsontheindexIn[82]:mean_ratings=mean_ratings.loc[active_titles]In[83]:mean_ratingsOut[83]:genderFMtitle'burbs,The(1989)2.7934782.96208510ThingsIHateAboutYou(1999)3.6465523.311966101Dalmatians(1961)3.7914443.500000101Dalmatians(1996)3.2400002.91121512AngryMen(1957)4.1843974.328421.........YoungGuns(1988)3.3717953.425620YoungGunsII(1990)2.9347832.904025YoungSherlockHolmes(1985)3.5147063.363344ZeroEffect(1998)3.8644073.723140eXistenZ(1999)3.0985923.289086[1216rowsx2columns]为了了解女性观众最喜欢的电影,我们可以对F列降序排列:
In[85]:top_female_ratings=mean_ratings.sort_values(by='F',ascending=False)In[86]:top_female_ratings[:10]Out[86]:genderFMtitleCloseShave,A(1995)4.6444444.473795WrongTrousers,The(1993)4.5882354.478261SunsetBlvd.(a.k.a.SunsetBoulevard)(1950)4.5726504.464589Wallace&Gromit:TheBestofAardmanAnimation...4.5631074.385075Schindler'sList(1993)4.5626024.491415ShawshankRedemption,The(1994)4.5390754.560625GrandDayOut,A(1992)4.5378794.293255ToKillaMockingbird(1962)4.5366674.372611CreatureComforts(1990)4.5138894.272277UsualSuspects,The(1995)4.5133174.518248计算评分分歧假设我们想要找出男性和女性观众分歧最大的电影。一个办法是给mean_ratings加上一个用于存放平均得分之差的列,并对其进行排序:
In[87]:mean_ratings['diff']=mean_ratings['M']-mean_ratings['F']按"diff"排序即可得到分歧最大且女性观众更喜欢的电影:
In[88]:sorted_by_diff=mean_ratings.sort_values(by='diff')In[89]:sorted_by_diff[:10]Out[89]:genderFMdifftitleDirtyDancing(1987)3.7903782.959596-0.830782Jumpin'JackFlash(1986)3.2547172.578358-0.676359Grease(1978)3.9752653.367041-0.608224LittleWomen(1994)3.8705883.321739-0.548849SteelMagnolias(1989)3.9017343.365957-0.535777Anastasia(1997)3.8000003.281609-0.518391RockyHorrorPictureShow,The(1975)3.6730163.160131-0.512885ColorPurple,The(1985)4.1581923.659341-0.498851AgeofInnocence,The(1993)3.8270683.339506-0.487561FreeWilly(1993)2.9213482.438776-0.482573对排序结果反序并取出前10行,得到的则是男性观众更喜欢的电影:
#Reverseorderofrows,takefirst10rowsIn[90]:sorted_by_diff[::-1][:10]Out[90]:genderFMdifftitleGood,TheBadandTheUgly,The(1966)3.4949494.2213000.726351KentuckyFriedMovie,The(1977)2.8787883.5551470.676359Dumb&Dumber(1994)2.6979873.3365950.638608LongestDay,The(1962)3.4117654.0314470.619682CableGuy,The(1996)2.2500002.8637870.613787EvilDeadII(DeadByDawn)(1987)3.2972973.9092830.611985Hidden,The(1987)3.1379313.7450980.607167RockyIII(1982)2.3617022.9435030.581801Caddyshack(1980)3.3961353.9697370.573602ForaFewDollarsMore(1965)3.4090913.9537950.544704如果只是想要找出分歧最大的电影(不考虑性别因素),则可以计算得分数据的方差或标准差:
#StandarddeviationofratinggroupedbytitleIn[91]:rating_std_by_title=data.groupby('title')['rating'].std()#Filterdowntoactive_titlesIn[92]:rating_std_by_title=rating_std_by_title.loc[active_titles]#OrderSeriesbyvalueindescendingorderIn[93]:rating_std_by_title.sort_values(ascending=False)[:10]Out[93]:titleDumb&Dumber(1994)1.321333BlairWitchProject,The(1999)1.316368NaturalBornKillers(1994)1.307198TankGirl(1995)1.277695RockyHorrorPictureShow,The(1975)1.260177EyesWideShut(1999)1.259624Evita(1996)1.253631BillyMadison(1995)1.249970FearandLoathinginLasVegas(1998)1.246408BicentennialMan(1999)1.245533Name:rating,dtype:float64可能你已经注意到了,电影分类是以竖线(|)分隔的字符串形式给出的。如果想对电影分类进行分析的话,就需要先将其转换成更有用的形式才行。
美国社会保障总署(SSA)提供了一份从1880年到现在的婴儿名字频率数据。HadleyWickham(许多流行R包的作者)经常用这份数据来演示R的数据处理功能。
我们要做一些数据规整才能加载这个数据集,这么做就会产生一个如下的DataFrame:
In[4]:names.head(10)Out[4]:namesexbirthsyear0MaryF706518801AnnaF260418802EmmaF200318803ElizabethF193918804MinnieF174618805MargaretF157818806IdaF147218807AliceF141418808BerthaF132018809SarahF12881880你可以用这个数据集做很多事,例如:
利用前面介绍过的那些工具,这些分析工作都能很轻松地完成,我会讲解其中的一些。
如果你在阅读本书的时候这个页面已经不见了,也可以用搜索引擎找找。
下载"Nationaldata"文件names.zip,解压后的目录中含有一组文件(如yob1880.txt)。我用UNIX的head命令查看了其中一个文件的前10行(在Windows上,你可以用more命令,或直接在文本编辑器中打开):
In[94]:!head-n10datasets/babynames/yob1880.txtMary,F,7065Anna,F,2604Emma,F,2003Elizabeth,F,1939Minnie,F,1746Margaret,F,1578Ida,F,1472Alice,F,1414Bertha,F,1320Sarah,F,1288由于这是一个非常标准的以逗号隔开的格式,所以可以用pandas.read_csv将其加载到DataFrame中:
In[95]:importpandasaspdIn[96]:names1880=pd.read_csv('datasets/babynames/yob1880.txt',....:names=['name','sex','births'])In[97]:names1880Out[97]:namesexbirths0MaryF70651AnnaF26042EmmaF20033ElizabethF19394MinnieF1746...........1995WoodieM51996WorthyM51997WrightM51998YorkM51999ZachariahM5[2000rowsx3columns]这些文件中仅含有当年出现超过5次的名字。为了简单起见,我们可以用births列的sex分组小计表示该年度的births总计:
In[98]:names1880.groupby('sex').births.sum()Out[98]:sexF90993M110493Name:births,dtype:int64由于该数据集按年度被分隔成了多个文件,所以第一件事情就是要将所有数据都组装到一个DataFrame里面,并加上一个year字段。使用pandas.concat即可达到这个目的:
years=range(1880,2011)pieces=[]columns=['name','sex','births']foryearinyears:path='datasets/babynames/yob%d.txt'%yearframe=pd.read_csv(path,names=columns)frame['year']=yearpieces.append(frame)#ConcatenateeverythingintoasingleDataFramenames=pd.concat(pieces,ignore_index=True)这里需要注意几件事情。第一,concat默认是按行将多个DataFrame组合到一起的;第二,必须指定ignore_index=True,因为我们不希望保留read_csv所返回的原始行号。现在我们得到了一个非常大的DataFrame,它含有全部的名字数据:
In[100]:namesOut[100]:namesexbirthsyear0MaryF706518801AnnaF260418802EmmaF200318803ElizabethF193918804MinnieF17461880..............1690779ZymaireM520101690780ZyonneM520101690781ZyquariusM520101690782ZyranM520101690783ZzyzxM52010[1690784rowsx4columns]有了这些数据之后,我们就可以利用groupby或pivot_table在year和sex级别上对其进行聚合了,如图14-4所示:
In[101]:total_births=names.pivot_table('births',index='year',.....:columns='sex',aggfunc=sum)In[102]:total_births.tail()Out[102]:sexFMyear200618964682050234200719168882069242200818836452032310200918276431973359201017590101898382In[103]:total_births.plot(title='Totalbirthsbysexandyear')图14-4按性别和年度统计的总出生数下面我们来插入一个prop列,用于存放指定名字的婴儿数相对于总出生数的比例。prop值为0.02表示每100名婴儿中有2名取了当前这个名字。因此,我们先按year和sex分组,然后再将新列加到各个分组上:
defadd_prop(group):group['prop']=group.births/group.births.sum()returngroupnames=names.groupby(['year','sex']).apply(add_prop)现在,完整的数据集就有了下面这些列:
In[105]:namesOut[105]:namesexbirthsyearprop0MaryF706518800.0776431AnnaF260418800.0286182EmmaF200318800.0220133ElizabethF193918800.0213094MinnieF174618800.019188.................1690779ZymaireM520100.0000031690780ZyonneM520100.0000031690781ZyquariusM520100.0000031690782ZyranM520100.0000031690783ZzyzxM520100.000003[1690784rowsx5columns]在执行这样的分组处理时,一般都应该做一些有效性检查,比如验证所有分组的prop的总和是否为1:
In[106]:names.groupby(['year','sex']).prop.sum()Out[106]:yearsex1880F1.0M1.01881F1.0M1.01882F1.0...2008M1.02009F1.0M1.02010F1.0M1.0Name:prop,Length:262,dtype:float64工作完成。为了便于实现更进一步的分析,我需要取出该数据的一个子集:每对sex/year组合的前1000个名字。这又是一个分组操作:
defget_top1000(group):returngroup.sort_values(by='births',ascending=False)[:1000]grouped=names.groupby(['year','sex'])top1000=grouped.apply(get_top1000)#Dropthegroupindex,notneededtop1000.reset_index(inplace=True,drop=True)如果你喜欢DIY的话,也可以这样:
pieces=[]foryear,groupinnames.groupby(['year','sex']):pieces.append(group.sort_values(by='births',ascending=False)[:1000])top1000=pd.concat(pieces,ignore_index=True)现在的结果数据集就小多了:
In[108]:top1000Out[108]:namesexbirthsyearprop0MaryF706518800.0776431AnnaF260418800.0286182EmmaF200318800.0220133ElizabethF193918800.0213094MinnieF174618800.019188.................261872CamiloM19420100.000102261873DestinM19420100.000102261874JaquanM19420100.000102261875JaydanM19420100.000102261876MaxtonM19320100.000102[261877rowsx5columns]接下来的数据分析工作就针对这个top1000数据集了。
有了完整的数据集和刚才生成的top1000数据集,我们就可以开始分析各种命名趋势了。首先将前1000个名字分为男女两个部分:
In[111]:total_births=top1000.pivot_table('births',index='year',.....:columns='name',.....:aggfunc=sum)现在,我们用DataFrame的plot方法绘制几个名字的曲线图(见图14-5):
一种解释是父母愿意给小孩起常见的名字越来越少。这个假设可以从数据中得到验证。一个办法是计算最流行的1000个名字所占的比例,我按year和sex进行聚合并绘图(见图14-6):
In[116]:table=top1000.pivot_table('prop',index='year',.....:columns='sex',aggfunc=sum)In[117]:table.plot(title='Sumoftable1000.propbyyearandsex',.....:yticks=np.linspace(0,1.2,13),xticks=range(1880,2020,10))图14-6分性别统计的前1000个名字在总出生人数中的比例从图中可以看出,名字的多样性确实出现了增长(前1000项的比例降低)。另一个办法是计算占总出生人数前50%的不同名字的数量,这个数字不太好计算。我们只考虑2010年男孩的名字:
In[118]:df=boys[boys.year==2010]In[119]:dfOut[119]:namesexbirthsyearprop260877JacobM2187520100.011523260878EthanM1786620100.009411260879MichaelM1713320100.009025260880JaydenM1703020100.008971260881WilliamM1687020100.008887.................261872CamiloM19420100.000102261873DestinM19420100.000102261874JaquanM19420100.000102261875JaydanM19420100.000102261876MaxtonM19320100.000102[1000rowsx5columns]在对prop降序排列之后,我们想知道前面多少个名字的人数加起来才够50%。虽然编写一个for循环确实也能达到目的,但NumPy有一种更聪明的矢量方式。先计算prop的累计和cumsum,然后再通过searchsorted方法找出0.5应该被插入在哪个位置才能保证不破坏顺序:
In[120]:prop_cumsum=df.sort_values(by='prop',ascending=False).prop.cumsum()In[121]:prop_cumsum[:10]Out[121]:2608770.0115232608780.0209342608790.0299592608800.0389302608810.0478172608820.0565792608830.0651552608840.0734142608850.0815282608860.089621Name:prop,dtype:float64In[122]:prop_cumsum.values.searchsorted(0.5)Out[122]:116由于数组索引是从0开始的,因此我们要给这个结果加1,即最终结果为117。拿1900年的数据来做个比较,这个数字要小得多:
In[123]:df=boys[boys.year==1900]In[124]:in1900=df.sort_values(by='prop',ascending=False).prop.cumsum()In[125]:in1900.values.searchsorted(0.5)+1Out[125]:25现在就可以对所有year/sex组合执行这个计算了。按这两个字段进行groupby处理,然后用一个函数计算各分组的这个值:
In[128]:diversity.head()Out[128]:sexFMyear1880381418813814188238151883391518843916In[129]:diversity.plot(title="Numberofpopularnamesintop50%")图14-7按年度统计的密度表从图中可以看出,女孩名字的多样性总是比男孩的高,而且还在变得越来越高。读者们可以自己分析一下具体是什么在驱动这个多样性(比如拼写形式的变化)。
#extractlastletterfromnamecolumnget_last_letter=lambdax:x[-1]last_letters=names.name.map(get_last_letter)last_letters.name='last_letter'table=names.pivot_table('births',index=last_letters,columns=['sex','year'],aggfunc=sum)然后,我选出具有一定代表性的三年,并输出前面几行:
In[131]:subtable=table.reindex(columns=[1910,1960,2010],level='year')In[132]:subtable.head()Out[132]:sexFMyear191019602010191019602010last_lettera108376.0691247.0670605.0977.05204.028438.0bNaN694.0450.0411.03912.038859.0c5.049.0946.0482.015476.023125.0d6750.03729.02607.022111.0262112.044398.0e133569.0435013.0313833.028655.0178823.0129012.0接下来我们需要按总出生数对该表进行规范化处理,以便计算出各性别各末字母占总出生人数的比例:
In[133]:subtable.sum()Out[133]:sexyearF1910396416.019602022062.020101759010.0M1910194198.019602132588.020101898382.0dtype:float64In[134]:letter_prop=subtable/subtable.sum()In[135]:letter_propOut[135]:sexFMyear191019602010191019602010last_lettera0.2733900.3418530.3812400.0050310.0024400.014980bNaN0.0003430.0002560.0021160.0018340.020470c0.0000130.0000240.0005380.0024820.0072570.012181d0.0170280.0018440.0014820.1138580.1229080.023387e0.3369410.2151330.1784150.1475560.0838530.067959.....................vNaN0.0000600.0001170.0001130.0000370.001434w0.0000200.0000310.0011820.0063290.0077110.016148x0.0000150.0000370.0007270.0039650.0018510.008614y0.1109720.1525690.1168280.0773490.1609870.058168z0.0024390.0006590.0007040.0001700.0001840.001831[26rowsx6columns]有了这个字母比例数据之后,就可以生成一张各年度各性别的条形图了,如图14-8所示:
In[143]:dny_ts.plot()图14-9各年出生的男孩中名字以d/n/y结尾的人数比例变成女孩名字的男孩名字(以及相反的情况)另一个有趣的趋势是,早年流行于男孩的名字近年来“变性了”,例如Lesley或Leslie。回到top1000数据集,找出其中以"lesl"开头的一组名字:
In[144]:all_names=pd.Series(top1000.name.unique())In[145]:lesley_like=all_names[all_names.str.lower().str.contains('lesl')]In[146]:lesley_likeOut[146]:632Leslie2294Lesley4262Leslee4728Lesli6103Leslydtype:object然后利用这个结果过滤其他的名字,并按名字分组计算出生数以查看相对频率:
In[147]:filtered=top1000[top1000.name.isin(lesley_like)]In[148]:filtered.groupby('name').births.sum()Out[148]:nameLeslee1082Lesley35022Lesli929Leslie370429Lesly10067Name:births,dtype:int64接下来,我们按性别和年度进行聚合,并按年度进行规范化处理:
In[149]:table=filtered.pivot_table('births',index='year',.....:columns='sex',aggfunc='sum')In[150]:table=table.div(table.sum(1),axis=0)In[151]:table.tail()Out[151]:sexFMyear20061.0NaN20071.0NaN20081.0NaN20091.0NaN20101.0NaN最后,就可以轻松绘制一张分性别的年度曲线图了(如图2-10所示):
{"id":21441,"description":"KENTUCKYFRIEDCHICKEN,FriedChicken,EXTRACRISPY,Wing,meatandskinwithbreading","tags":["KFC"],"manufacturer":"KentuckyFriedChicken","group":"FastFoods","portions":[{"amount":1,"unit":"wing,withskin","grams":68.0},...],"nutrients":[{"value":20.8,"units":"g","description":"Protein","group":"Composition"},...]}每种食物都带有若干标识性属性以及两个有关营养成分和分量的列表。这种形式的数据不是很适合分析工作,因此我们需要做一些规整化以使其具有更好用的形式。
从上面列举的那个网址下载并解压数据之后,你可以用任何喜欢的JSON库将其加载到Python中。我用的是Python内置的json模块:
In[154]:importjsonIn[155]:db=json.load(open('datasets/usda_food/database.json'))In[156]:len(db)Out[156]:6636db中的每个条目都是一个含有某种食物全部数据的字典。nutrients字段是一个字典列表,其中的每个字典对应一种营养成分:
In[157]:db[0].keys()Out[157]:dict_keys(['id','description','tags','manufacturer','group','portions','nutrients'])In[158]:db[0]['nutrients'][0]Out[158]:{'description':'Protein','group':'Composition','units':'g','value':25.18}In[159]:nutrients=pd.DataFrame(db[0]['nutrients'])In[160]:nutrients[:7]Out[160]:descriptiongroupunitsvalue0ProteinCompositiong25.181Totallipid(fat)Compositiong29.202Carbohydrate,bydifferenceCompositiong3.063AshOtherg3.284EnergyEnergykcal376.005WaterCompositiong39.286EnergyEnergykJ1573.00在将字典列表转换为DataFrame时,可以只抽取其中的一部分字段。这里,我们将取出食物的名称、分类、编号以及制造商等信息:
In[161]:info_keys=['description','group','id','manufacturer']In[162]:info=pd.DataFrame(db,columns=info_keys)In[163]:info[:5]Out[163]:descriptiongroupid\0Cheese,carawayDairyandEggProducts10081Cheese,cheddarDairyandEggProducts10092Cheese,edamDairyandEggProducts10183Cheese,fetaDairyandEggProducts10194Cheese,mozzarella,partskimmilkDairyandEggProducts1028manufacturer01234In[164]:info.info()
In[165]:pd.value_counts(info.group)[:10]Out[165]:VegetablesandVegetableProducts812BeefProducts618BakedProducts496BreakfastCereals403FastFoods365LegumesandLegumeProducts365Lamb,Veal,andGameProducts345Sweets341PorkProducts328FruitsandFruitJuices328Name:group,dtype:int64现在,为了对全部营养数据做一些分析,最简单的办法是将所有食物的营养成分整合到一个大表中。我们分几个步骤来实现该目的。首先,将各食物的营养成分列表转换为一个DataFrame,并添加一个表示编号的列,然后将该DataFrame添加到一个列表中。最后通过concat将这些东西连接起来就可以了:
顺利的话,nutrients的结果是:
In[167]:nutrientsOut[167]:descriptiongroupunitsvalueid0ProteinCompositiong25.18010081Totallipid(fat)Compositiong29.20010082Carbohydrate,bydifferenceCompositiong3.06010083AshOtherg3.28010084EnergyEnergykcal376.0001008..................389350VitaminB-12,addedVitaminsmcg0.00043546389351CholesterolOthermg0.00043546389352Fattyacids,totalsaturatedOtherg0.07243546389353Fattyacids,totalmonounsaturatedOtherg0.02843546389354Fattyacids,totalpolyunsaturatedOtherg0.04143546[389355rowsx5columns]我发现这个DataFrame中无论如何都会有一些重复项,所以直接丢弃就可以了:
In[168]:nutrients.duplicated().sum()#numberofduplicatesOut[168]:14179In[169]:nutrients=nutrients.drop_duplicates()由于两个DataFrame对象中都有"group"和"description",所以为了明确到底谁是谁,我们需要对它们进行重命名:
In[176]:ndata=pd.merge(nutrients,info,on='id',how='outer')In[177]:ndata.info()
In[180]:result=ndata.groupby(['nutrient','fgroup'])['value'].quantile(0.5)In[181]:result['Zinc,Zn'].sort_values().plot(kind='barh')图片14-11根据营养分类得出的锌中位值只要稍微动一动脑子,就可以发现各营养成分最为丰富的食物是什么了:
by_nutrient=ndata.groupby(['nutgroup','nutrient'])get_maximum=lambdax:x.loc[x.value.idxmax()]get_minimum=lambdax:x.loc[x.value.idxmin()]max_foods=by_nutrient.apply(get_maximum)[['value','food']]#makethefoodalittlesmallermax_foods.food=max_foods.food.str[:50]由于得到的DataFrame很大,所以不方便在书里面全部打印出来。这里只给出"AminoAcids"营养分组:
In[184]:fec=pd.read_csv('datasets/fec/P00000001-ALL.csv')In[185]:fec.info()
In[186]:fec.iloc[123456]Out[186]:cmte_idC00431445cand_idP80003338cand_nmObama,Barackcontbr_nmELLMAN,IRAcontbr_cityTEMPE...receipt_descNaNmemo_cdNaNmemo_textNaNform_tpSA17Afile_num772372Name:123456,Length:16,dtype:object你可能已经想出了许多办法从这些竞选赞助数据中抽取有关赞助人和赞助模式的统计信息。我将在接下来的内容中介绍几种不同的分析工作(运用到目前为止已经学到的方法)。
不难看出,该数据中没有党派信息,因此最好把它加进去。通过unique,你可以获取全部的候选人名单:
In[187]:unique_cands=fec.cand_nm.unique()In[188]:unique_candsOut[188]:array(['Bachmann,Michelle','Romney,Mitt','Obama,Barack',"Roemer,CharlesE.'Buddy'III",'Pawlenty,Timothy','Johnson,GaryEarl','Paul,Ron','Santorum,Rick','Cain,Herman','Gingrich,Newt','McCotter,ThaddeusG','Huntsman,Jon','Perry,Rick'],dtype=object)In[189]:unique_cands[2]Out[189]:'Obama,Barack'指明党派信息的方法之一是使用字典:
parties={'Bachmann,Michelle':'Republican','Cain,Herman':'Republican','Gingrich,Newt':'Republican','Huntsman,Jon':'Republican','Johnson,GaryEarl':'Republican','McCotter,ThaddeusG':'Republican','Obama,Barack':'Democrat','Paul,Ron':'Republican','Pawlenty,Timothy':'Republican','Perry,Rick':'Republican',"Roemer,CharlesE.'Buddy'III":'Republican','Romney,Mitt':'Republican','Santorum,Rick':'Republican'}现在,通过这个映射以及Series对象的map方法,你可以根据候选人姓名得到一组党派信息:
In[191]:fec.cand_nm[123456:123461]Out[191]:123456Obama,Barack123457Obama,Barack123458Obama,Barack123459Obama,Barack123460Obama,BarackName:cand_nm,dtype:objectIn[192]:fec.cand_nm[123456:123461].map(parties)Out[192]:123456Democrat123457Democrat123458Democrat123459Democrat123460DemocratName:cand_nm,dtype:object#AdditasacolumnIn[193]:fec['party']=fec.cand_nm.map(parties)In[194]:fec['party'].value_counts()Out[194]:Democrat593746Republican407985Name:party,dtype:int64这里有两个需要注意的地方。第一,该数据既包括赞助也包括退款(负的出资额):
In[195]:(fec.contb_receipt_amt>0).value_counts()Out[195]:True991475False10256Name:contb_receipt_amt,dtype:int64为了简化分析过程,我限定该数据集只能有正的出资额:
In[196]:fec=fec[fec.contb_receipt_amt>0]由于BarackObama和MittRomney是最主要的两名候选人,所以我还专门准备了一个子集,只包含针对他们两人的竞选活动的赞助信息:
In[197]:fec_mrbo=fec[fec.cand_nm.isin(['Obama,Barack','Romney,Mitt'])]根据职业和雇主统计赞助信息基于职业的赞助信息统计是另一种经常被研究的统计任务。例如,律师们更倾向于资助民主党,而企业主则更倾向于资助共和党。你可以不相信我,自己看那些数据就知道了。首先,根据职业计算出资总额,这很简单:
In[198]:fec.contbr_occupation.value_counts()[:10]Out[198]:RETIRED233990INFORMATIONREQUESTED35107ATTORNEY34286HOMEMAKER29931PHYSICIAN23432INFORMATIONREQUESTEDPERBESTEFFORTS21138ENGINEER14334TEACHER13990CONSULTANT13273PROFESSOR12555Name:contbr_occupation,dtype:int64不难看出,许多职业都涉及相同的基本工作类型,或者同一样东西有多种变体。下面的代码片段可以清理一些这样的数据(将一个职业信息映射到另一个)。注意,这里巧妙地利用了dict.get,它允许没有映射关系的职业也能“通过”:
occ_mapping={'INFORMATIONREQUESTEDPERBESTEFFORTS':'NOTPROVIDED','INFORMATIONREQUESTED':'NOTPROVIDED','INFORMATIONREQUESTED(BESTEFFORTS)':'NOTPROVIDED','C.E.O.':'CEO'}#Ifnomappingprovided,returnxf=lambdax:occ_mapping.get(x,x)fec.contbr_occupation=fec.contbr_occupation.map(f)我对雇主信息也进行了同样的处理:
emp_mapping={'INFORMATIONREQUESTEDPERBESTEFFORTS':'NOTPROVIDED','INFORMATIONREQUESTED':'NOTPROVIDED','SELF':'SELF-EMPLOYED','SELFEMPLOYED':'SELF-EMPLOYED',}#Ifnomappingprovided,returnxf=lambdax:emp_mapping.get(x,x)fec.contbr_employer=fec.contbr_employer.map(f)现在,你可以通过pivot_table根据党派和职业对数据进行聚合,然后过滤掉总出资额不足200万美元的数据:
In[201]:by_occupation=fec.pivot_table('contb_receipt_amt',.....:index='contbr_occupation',.....:columns='party',aggfunc='sum')In[202]:over_2mm=by_occupation[by_occupation.sum(1)>2000000]In[203]:over_2mmOut[203]:partyDemocratRepublicancontbr_occupationATTORNEY11141982.977.477194e+06CEO2074974.794.211041e+06CONSULTANT2459912.712.544725e+06ENGINEER951525.551.818374e+06EXECUTIVE1355161.054.138850e+06.........PRESIDENT1878509.954.720924e+06PROFESSOR2165071.082.967027e+05REALESTATE528902.091.625902e+06RETIRED25305116.382.356124e+07SELF-EMPLOYED672393.401.640253e+06[17rowsx2columns]把这些数据做成柱状图看起来会更加清楚('barh'表示水平柱状图,如图14-12所示):
In[205]:over_2mm.plot(kind='barh')图14-12对各党派总出资额最高的职业你可能还想了解一下对Obama和Romney总出资额最高的职业和企业。为此,我们先对候选人进行分组,然后使用本章前面介绍的类似top的方法:
defget_top_amounts(group,key,n=5):totals=group.groupby(key)['contb_receipt_amt'].sum()returntotals.nlargest(n)然后根据职业和雇主进行聚合:
In[207]:grouped=fec_mrbo.groupby('cand_nm')In[208]:grouped.apply(get_top_amounts,'contbr_occupation',n=7)Out[208]:cand_nmcontbr_occupationObama,BarackRETIRED25305116.38ATTORNEY11141982.97INFORMATIONREQUESTED4866973.96HOMEMAKER4248875.80PHYSICIAN3735124.94...Romney,MittHOMEMAKER8147446.22ATTORNEY5364718.82PRESIDENT2491244.89EXECUTIVE2300947.03C.E.O.1968386.11Name:contb_receipt_amt,Length:14,dtype:float64In[209]:grouped.apply(get_top_amounts,'contbr_employer',n=10)Out[209]:cand_nmcontbr_employerObama,BarackRETIRED22694358.85SELF-EMPLOYED17080985.96NOTEMPLOYED8586308.70INFORMATIONREQUESTED5053480.37HOMEMAKER2605408.54...Romney,MittCREDITSUISSE281150.00MORGANSTANLEY267266.00GOLDMANSACH&CO.238250.00BARCLAYSCAPITAL162750.00H.I.G.CAPITAL139500.00Name:contb_receipt_amt,Length:20,dtype:float64对出资额分组还可以对该数据做另一种非常实用的分析:利用cut函数根据出资额的大小将数据离散化到多个面元中:
In[210]:bins=np.array([0,1,10,100,1000,10000,.....:100000,1000000,10000000])In[211]:labels=pd.cut(fec_mrbo.contb_receipt_amt,bins)In[212]:labelsOut[212]:411(10,100]412(100,1000]413(100,1000]414(10,100]415(10,100]...701381(10,100]701382(100,1000]701383(1,10]701384(10,100]701385(100,1000]Name:contb_receipt_amt,Length:694282,dtype:categoryCategories(8,interval[int64]):[(0,1]<(1,10]<(10,100]<(100,1000]<(1000,10000]<(10000,100000]<(100000,1000000]<(1000000,10000000]]现在可以根据候选人姓名以及面元标签对奥巴马和罗姆尼数据进行分组,以得到一个柱状图:
In[213]:grouped=fec_mrbo.groupby(['cand_nm',labels])In[214]:grouped.size().unstack(0)Out[214]:cand_nmObama,BarackRomney,Mittcontb_receipt_amt(0,1]493.077.0(1,10]40070.03681.0(10,100]372280.031853.0(100,1000]153991.043357.0(1000,10000]22284.026186.0(10000,100000]2.01.0(100000,1000000]3.0NaN(1000000,10000000]4.0NaN从这个数据中可以看出,在小额赞助方面,Obama获得的数量比Romney多得多。你还可以对出资额求和并在面元内规格化,以便图形化显示两位候选人各种赞助额度的比例(见图14-13):
In[216]:bucket_sums=grouped.contb_receipt_amt.sum().unstack(0)In[217]:normed_sums=bucket_sums.div(bucket_sums.sum(axis=1),axis=0)In[218]:normed_sumsOut[218]:cand_nmObama,BarackRomney,Mittcontb_receipt_amt(0,1]0.8051820.194818(1,10]0.9187670.081233(10,100]0.9107690.089231(100,1000]0.7101760.289824(1000,10000]0.4473260.552674(10000,100000]0.8231200.176880(100000,1000000]1.000000NaN(1000000,10000000]1.000000NaNIn[219]:normed_sums[:-2].plot(kind='barh')图14-13两位候选人收到的各种捐赠额度的总额比例我排除了两个最大的面元,因为这些不是由个人捐赠的。
还可以对该分析过程做许多的提炼和改进。比如说,可以根据赞助人的姓名和邮编对数据进行聚合,以便找出哪些人进行了多次小额捐款,哪些人又进行了一次或多次大额捐款。我强烈建议你下载这些数据并自己摸索一下。
根据候选人和州对数据进行聚合是常规操作:
In[220]:grouped=fec_mrbo.groupby(['cand_nm','contbr_st'])In[221]:totals=grouped.contb_receipt_amt.sum().unstack(0).fillna(0)In[222]:totals=totals[totals.sum(1)>100000]In[223]:totals[:10]Out[223]:cand_nmObama,BarackRomney,Mittcontbr_stAK281840.1586204.24AL543123.48527303.51AR359247.28105556.00AZ1506476.981888436.23CA23824984.2411237636.60CO2132429.491506714.12CT2068291.263499475.45DC4373538.801025137.50DE336669.1482712.00FL7318178.588338458.81如果对各行除以总赞助额,就会得到各候选人在各州的总赞助额比例:
In[224]:percent=totals.div(totals.sum(1),axis=0)In[225]:percent[:10]Out[225]:cand_nmObama,BarackRomney,Mittcontbr_stAK0.7657780.234222AL0.5073900.492610AR0.7729020.227098AZ0.4437450.556255CA0.6794980.320502CO0.5859700.414030CT0.3714760.628524DC0.8101130.189887DE0.8027760.197224FL0.4674170.53258314.6总结我们已经完成了正文的最后一章。附录中有一些额外的内容,可能对你有用。