前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何利用维基百科的数据可视化当代音乐史

如何利用维基百科的数据可视化当代音乐史

作者头像
大数据文摘
发布2018-05-24 15:12:39
1.6K0
发布2018-05-24 15:12:39
举报
文章被收录于专栏:大数据文摘大数据文摘

翻译校对:丁雪 吴怡雯 程序验证修改:李小帅

“我相信马塞勒斯·华莱士,我的丈夫,你的老板吩咐你带我出门做我想做的任何事。现在,我想跳舞,我要赢,我想得到那个奖杯,把舞跳好来!”

《黑色追缉令》是我一直以来最喜欢的电影。令人惊奇的故事情节、演员、表演以及导演会让我想要前去影院观看,当别人问起“你看过这部电影吗?”,我可以打破僵局。电影中最具标志性的场景可能是乌玛•瑟曼和约翰•特拉沃尔塔在杰克兔子餐厅的舞池跳扭扭舞的那段。虽然这可能是乌玛•瑟曼最经典的舞蹈场景,但约翰•特拉沃尔塔似乎根本停不下来,在电影《迈克》、《发胶》、《黑色追缉令》、《油脂》、《周末夜狂热》和《都市牛郎》中约翰所饰演的角色总是梳着锃亮的大背头、乌黑的头发、极富本性地跳着舞。

虽然很多人可能会笑约翰在舞池中央跟着迪斯科音乐跳舞的场景,但扪心自问,所有酷酷的舞蹈电影是否都注定是相同的。随着时间流逝我们是否还会被《魅力四射》(Bring it On,美国系列青春校园电影——译者注)和《街舞少年》(Stompthe Yard)中的音乐所感动?如果看一看这些年最流行音乐风格的变化趋势(如下图),大众对流行乐偏好的变化似乎没有迪斯科的节奏那么快。

◆ ◆ ◆

可视化

通过分析Billboard年终榜单中前100首歌曲,我们可以根据每年Billboard上最流行歌曲所代表的音乐风格的份额来量化现代音乐的走向。图中我们可以看出,迪斯科(Disco)只有短短十几年的光辉,从90年代以来饶舌(Rap)和嘻哈(Hip-Hop)音乐风格才持续出现。有趣的是,本世纪初随着历史的重复,饶舌和嘻哈音乐处于巅峰,迪斯科的变动与流行音乐中一些最低份额的流派保持一致。慢摇滚(Soft Rock)和硬摇滚(HardRock)的光景甚至比迪斯科更糟糕,在2005年完全灭绝。相反的是,麦当娜在2005年的复兴单曲继续延续了迪斯科的影响力,在2010年后,我们被火星哥(Bruno Mars)和魔力红(Maroon 5)的歌洗脑。

这一可视化视图是如何绘制而成的?

维基百科是一座金矿,里面有列表,列表里面套着列表,甚至被套着的列表里面还套着列表。其中一个列表恰巧是Billboard最热门的100首单曲,它使我们能够很容易地浏览维基百科的数据。在快速查看网址后,我们能够简单地生成页面,从中爬取数据,这样更简单。我们从为程序加载必要的模块和参数开始。

#iPython 内联查看画图并导入必要的包

import numpy as np

import pandas as pd

import seaborn as sns

import pylab as pylab

import matplotlib.pyplot as plt

import requests, cPickle, sys, re, os

from bs4 import BeautifulSoup as bs

import logging

logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',level=logging.INFO)

requests = requests.Session()

# 设置画板大小

pylab.rcParams['figure.figsize'] = 32, 16

接着程序脚本利用我们在网址中找到的模式,尝试从页面中提取所有可能存在的链接。

# 定义一个从维基百科表格中抓取相关信息的函数

如果没有返回NaN

def tryInstance(td, choice):

try:

# 歌曲只有一个维基百科链接,但是歌手可能有许多链接。

我们创建一个选择标志, #用来决定抓取文本信息还是链接信息

if (choice == 0):

return td.text

elif (choice == 1):

links = [x['href'] for x in td.findAll('a')]

if (len(links) != 0):

return links

else:

return float('NaN')

except:

return float('NaN')

#找到页面的第一个table,尽量抓取所有表格行的信息

pandaTableHeaders = ['year', 'pos', 'song','artists', 'song_links', 'artist_links']

headers = {'User-Agent': 'Mozilla/5.0 (X11;Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116Safari/537.36'}

cookies = {'cookie': 'CP=H2;WMF-Last-Access=16-Apr-2016; GeoIP=CN:01:Xuancheng:30.95:

118.76:v4;enwikimwuser-sessionId=588324132f65192a'}

def scrapeTable(year):

#创建url路径,用BeautifulSoup解析页面内容,创建列表用来存储表数据

url ='https://en.wikipedia.org/wiki/Billboard_Year-End_Hot_100_singles_of_'+str(year)

soup =bs(requests.get(url, headers=headers, cookies=cookies).content)

table = []

#由于文本格式的不同,我们针对4种特例使用不同的code来创建临时souptable变量

souptable= soup.find('table')

if (year in [2006, 2012, 2013]):

souptable = soup.findAll('table')[1]

elif (year in [2011]):

souptable = soup.findAll('table')[4]

#从上面迭遍历程序得到的table中收集每个表格行的信息

for pos, tr in enumerate(souptable.findAll('tr')):

tds = tr.findAll('td')

if (len(tds) > 0):

toAppend = [

year, pos,

tryInstance(tds[-2], 0),tryInstance(tds[-1], 0),

tryInstance(tds[-2], 1),tryInstance(tds[-1], 1)

]

table.append(toAppend)

#创建并返回表数据的数据框形式

df = pd.DataFrame(table)

df.columns = pandaTableHeaders

return df

#遍历所有可能的年份,序列化存储,方便以后使用

dfs =pd.DataFrame(pandaTableHeaders).set_index(0).T

for year in xrange(1956, 2016):

print year,

dfs = dfs.append(scrapeTable(year))

cPickle.dump(dfs.reset_index().drop('index',axis=1), open('wikipediaScrape.p', 'wb'))

借助存储在数据帧中的所有链接,我们可以加载每个维基百科页面,并从每一页右上角信息表中提取信息。不幸的是,当所有这些信息表的长度不同,有不同的 HTML 嵌套和不完整数据时,这些数据会变得特别混杂(竟然没有人将Gorillaz 音乐进行归类?!)。

为了解决这一问题,我们在代码中查找表对象,并将其作为字符串保存并在之后的分析进行加载。这样做的优点是加倍的,它可以让我们从一次运行中收集所有必要的信息;同时,也帮助我们从用户的定义中对音乐流派关键词进行分类。

#从wikipediaScrape.p文件中加载数据框,创建新的列,边抓取信息边填充

dfs =cPickle.load(open('wikipediaScrape.p', 'rb'))

subjects =['Genre', 'Length', 'Producer', 'Label', 'Format', 'Released', 'B-side']

for subject insubjects:

dfs[subject] = float('NaN')

# 与上面的tryInstance函数类似,尽可能抓取更多信息

# 捕获缺失异常,使用NaNs替代缺失值

# 另外,还有一个问题是tables难于管理。其内容可能存在或不存在,可能有错别字

# 或不同的名字。

def extractInfoTable(url):

infoTable = []

#捕获表头、表行和页面异常

try:

soup = bs(requests.get(url, headers=headers, cookies=cookies).content)

for tr in soup.find('table').findAll('tr'):

try:

header = tr.find('th').text

if (header == 'Music sample'):

# Music sample表示信息表的结束,如果满足条件中断循环以节省时间

break

try:

# 如果表头不是Musicsample,收集”tr”对象中所有可能的信息

trs = tr.findAll('td')

infoTable.append([header,trs])

except:

noTrsFound = True

except:

noHeaderFound = True

except:

noPageFound = True

#如果subjects列表中存在记录,保存HTML字符串形式

infoColumns = []

for subject in subjects:

instanceBool = False

for header, info in infoTable:

if (subject in header):

infoColumns.append([subject,str(info)])

instanceBool = True

break

if (not instanceBool):

infoColumns.append([subject,float('NaN')])

#返回所有抓取的信息

return infoColumns

#对数据帧中所有的歌曲使用scraping函数

forsongIndex in xrange(0,dfs.shape[0]):

printsongIndex, dfs.ix[songIndex].year, dfs.ix[songIndex].song

try:

# 获取链接

song_links =['https://en.wikipedia.org' + x for x in dfs.ix[songIndex].song_links]

# 抽取信息

logging.info('extract info')

infoTable =extractInfoTable(song_links[0])

# 存储index和subjectstore信息

for idx, subject in enumerate(subjects):

dfs.loc[:,(subject)].ix[songIndex]= str(infoTable[idx][1])

#每100首歌曲序列化保存

if (songIndex % 100 == 0):

cPickle.dump(dfs.reset_index().drop('index', axis=1), open('full_df.p','wb'))

except(TypeError):

print 'NaN link found'

# 保存所有的数据帧信息

cPickle.dump(dfs.reset_index().drop('index',axis=1), open('full_df.p', 'wb'))

现在,我们开始对所有HTML字符串进行分析。当音乐流派可以被识别时,我们就可以抽取关键词列表,之后将它们分入“脏列表”(脏,表示数据还未被清洗——译者注)。这一列表充满了错别字、名称不统一的名词、引用等等。

#创建流派字典,比如,对于“folk”和“country”范围的分析则认为是相同的音乐流#派

genreList= {

'electronic': ['electronic'],

'latin' : ['latin'],

'reggae' : ['reggae'],

'pop' : ['pop'],

'dance' : ['dance'],

'disco' : ['disco', 'funk'],

'folk' : ['folk', 'country'],

'r&b' : ['r&b'],

'blues' : ['blues'],

'jazz' : ['jazz'],

'soul' : ['soul'],

'rap' : ['rap', 'hip hop'],

'metal' : ['metal'],

'grunge' : ['grunge'],

'punk' : ['punk'],

'alt' : ['alternative rock'],

'soft rock' : ['soft rock'],

'hard rock' : ['hard rock'],

}

#加载数据帧并抽取相关的流派

# 添加“dirty”列,名单包括HTML元素

# “ dirty”列包含的错别字、引用等记录都会导致异常发生,但是我们感兴趣的是从

# 混乱的字符串中抽取相关的关键字,通过简单匹配所有的小写实例,计数最后的

#“pop”流派个数

df =cPickle.load(open('full_df.p', 'rb'))

defextractGenre(x):

sx = str(x)

try:

dirtyList = [td.text.replace('\n', '')for td in BeautifulSoup(sx).findAll('td')]

return dirtyList

except:

return float('NaN')

df['Genre']= df['Genre'].apply(extractGenre)

# 打印df['Genre']

最后我们为每首歌所代表的音乐流派创建标志列,使绘制图片更加容易。

#添加”key”列,如果key是流派字典的键值则为1,否则为0。拷贝数据帧,使

#用.loc[(tuple)]函数以避免切片链警告。

for keyin genreList.keys():

df[key] = 0

dfs =df.copy()

# 对于genreList字典中每个流派匹配字符串,如果能匹配,则标志指定列,以便能够在后面输出布尔结果

forgenre in genreList:

ans=0

for idx in xrange(0, df.shape[0]):

if (len(df.loc[(idx,'Genre')]) > 0):

if (any([x indf.loc[(idx,'Genre')][0].lower() for x in genreList[genre]])):

dfs.loc[(idx, genre)] = 1

ans+=1

print genre, ans

sys.stdout.flush()

cPickle.dump(dfs,open('genre_df.p', 'wb'))

◆ ◆ ◆

微调变量后导出数据

df =cPickle.load(open('genre_df.p', 'rb'))

defaverageAllRows(gdf):

# 添加”sums”列

gdf['sums'] = gdf.sum(axis=1)

#对数据帧的每列除以”sums”列,添加精度1e-12,排除分母为零的情况

logging.info('averageAllRows')

for col in gdf.columns:

gdf[col] =gdf[col].divide(gdf['sums']+1e-12)

#返回数据帧并丢弃”sums”列

return gdf.drop('sums', axis=1)

pylab.rcParams['figure.figsize']= 32, 16

gdf =pd.DataFrame()

for g ingenreList.keys():

gdf[g] = df.groupby('year')[g].sum()

# 自定义打印顺序

gl2 = [

'jazz', 'blues', 'folk', 'soul', 'pop','disco', 'rap', 'soft rock',

'hard rock', 'dance', 'r&b', 'alt','latin', 'reggae', 'electronic', 'punk',

'grunge', 'metal',

]

#对数据帧重新排序并对所有行求平均

gdf =gdf[gl2]

gdf =averageAllRows(gdf)

# 创建百分比条形图

ax =gdf.plot(kind='bar', width=1,stacked=True, legend=False, cmap='Paired',linewidth=0.1)

ax.set_ylim(0,1)

ax.legend(loc='centerleft', bbox_to_anchor=(1, 0.5))

locs,labels = plt.xticks()

plt.setp(labels,rotation=90)

plt.show()

◆ ◆ ◆

最后的输出

◆ ◆ ◆

编后语

由于程序是对1956年-2016年期间的Wiki年度热门歌手页面的爬取,处理过程很耗时,因此,我们将1956-2016时间段分成了6部分,每部分包含了跨度为10年的年度热门歌手页面的处理。具体方法是将”for year in xrange(1956, 2016)”程序修改为” foryear in xrange(1956, 1966)”等。您也可以使用我们训练好的模型进行验证,模型文件genre_df.p已按照年份保存到对应目录了,在加载模型文件的目录地址一定不要写错了。

df =cPickle.load(open('./06_16/genre_df.p', 'rb'))

defaverageAllRows(gdf):

# 添加”sums”列

gdf['sums'] = gdf.sum(axis=1)

#对数据框的每列除以”sums”列,添加精度1e-12,排除分母为零的情况

logging.info('averageAllRows')

for col in gdf.columns:

gdf[col] =gdf[col].divide(gdf['sums']+1e-12)

#返回数据框并丢弃“sums”列

return gdf.drop('sums', axis=1)

pylab.rcParams['figure.figsize']= 32, 16

gdf =pd.DataFrame()

for g ingenreList.keys():

gdf[g] = df.groupby('year')[g].sum()

# 自定义打印顺序

gl2 = [

'jazz', 'blues', 'folk', 'soul', 'pop','disco', 'rap', 'soft rock',

'hard rock', 'dance', 'r&b', 'alt','latin', 'reggae', 'electronic', 'punk',

'grunge', 'metal',

]

#对数据框重新排序并对求平均

gdf =gdf[gl2]

gdf =averageAllRows(gdf)

# 创建百分比条形图

ax =gdf.plot(kind='bar', width=1,stacked=True, legend=False, cmap='Paired',linewidth=0.1)

ax.set_ylim(0,1)

ax.legend(loc='centerleft', bbox_to_anchor=(1, 0.5))

locs,labels = plt.xticks()

plt.setp(labels,rotation=90)

plt.show()

维基百科真是个宝,里面有很多可以挖掘的内容,欢迎读者朋友们也试试,给我们投投稿,谢谢!

原文链接:http://www.microbrewdata.com/defining-a-modern-history-of-music-using-wikipedia-data/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-06-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 大数据文摘 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ◆ ◆ ◆
  • ◆ ◆ ◆
  • ◆ ◆ ◆
  • ◆ ◆ ◆
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档