天天看點

如何利用維基百科的資料可視化當代音樂史◆ ◆ ◆◆ ◆ ◆◆ ◆ ◆◆ ◆ ◆

薦文計劃:

你的工作是否和資料有關?

又或者是否大資料愛好者?

如果你經常發現好文章并期望與讀者分享,

歡迎點選文末“閱讀原文”,加入我們!

薦文一旦采納,我們會在文章開頭緻謝并宣傳。

本期薦文:

如何利用維基百科的資料可視化當代音樂史◆ ◆ ◆◆ ◆ ◆◆ ◆ ◆◆ ◆ ◆

翻譯校對:丁雪 吳怡雯    程式驗證修改:李小帥

“我相信馬塞勒斯·華萊士,我的丈夫,你的老闆吩咐你帶我出門做我想做的任何事。現在,我想跳舞,我要赢,我想得到那個獎杯,把舞跳好來!”

如何利用維基百科的資料可視化當代音樂史◆ ◆ ◆◆ ◆ ◆◆ ◆ ◆◆ ◆ ◆

《黑色追緝令》是我一直以來最喜歡的電影。令人驚奇的故事情節、演員、表演以及導演會讓我想要前去影院觀看,當别人問起“你看過這部電影嗎?”,我可以打破僵局。電影中最具标志性的場景可能是烏瑪•瑟曼和約翰•特拉沃爾塔在傑克兔子餐廳的舞池跳扭扭舞的那段。雖然這可能是烏瑪•瑟曼最經典的舞蹈場景,但約翰•特拉沃爾塔似乎根本停不下來,在電影《邁克》、《發膠》、《黑色追緝令》、《油脂》、《周末夜狂熱》和《都市牛郎》中約翰所飾演的角色總是梳着锃亮的大背頭、烏黑的頭發、極富本性地跳着舞。

雖然很多人可能會笑約翰在舞池中央跟着迪斯科音樂跳舞的場景,但扪心自問,所有酷酷的舞蹈電影是否都注定是相同的。随着時間流逝我們是否還會被《魅力四射》(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']

essay-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, essay-headers=essay-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 = []

          #捕獲表頭、表行和頁面異常

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

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

           try:

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

                if (essay-header == 'music sample'):

             # music sample表示資訊表的結束,如果滿足條件中斷循環以節省時間

                    break

                try:

                    # 如果表頭不是musicsample,收集”tr”對象中所有可能的資訊

                    trs = tr.findall('td')

                    infotable.append([essay-header,trs])

               except:

                    notrsfound = true

           except:

                noheaderfound = true

       nopagefound = true

   #如果subjects清單中存在記錄,儲存html字元串形式

    infocolumns = []

    for subject in subjects:

        instancebool = false

        for essay-header, info in infotable:

            if (subject in essay-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)

        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'))

       #對資料框的每列除以”sums”列,添加精度1e-12,排除分母為零的情況

       #傳回資料框并丢棄“sums”列

#對資料框重新排序并對求平均

原文釋出時間為:2016-06-07

本文來自雲栖社群合作夥伴“大資料文摘”,了解相關資訊可以關注“bigdatadigest”微信公衆号