作者 | Haoyue Zhang
題圖 | 站酷海洛
DT數據俠與紐約數據科學院(New York City Data Science Acadamy)合作的第一期數據俠Python訓練營10月結營,在完成對數據爬取、數據分析與數據可視化的訓練之後,DT君選取一些訓練營成員的作品供大家欣賞。
本篇文章來自於數據俠Haoyue Zhang,她通過抓取維密大秀的數據,發現了很多“維多利亞”的“秘密”喲!大家先睹為快吧!
一年一度的維密大秀又要來了!每年的維密大秀從試鏡到選定模特,再到彩排、錄製、播出,可都會獲得無數時尚圈人士和美好肉體愛好者的目光和熱議,究竟維密大秀的寵兒們背後有怎樣的秘密?我分析了1995年以來的走秀記錄,用數據來為你揭秘!
維多利亞的秘密時尚秀從1995年開始舉辦,夢幻炫目的造型和獨到的商業化策略使其迅速在時尚產業立足。能參加走秀是許多模特自童年開始就擁有的夢想,而佩戴Fantasy Bra(注:Victoria's Secret每年大秀的壓軸胸罩,接近天價,會由年度最紅的天使超模佩戴),進行開秀和閉秀,成為一名維密天使更是令人榮耀!
通過這次數據分析,我想要探究的問題主要有:
- 誰是參加過最多次走秀的模特?
- 哪位模特開秀/閉秀/佩戴Fantasy Bra的次數最多?
- 維密天使都來自於哪些國家?
- 開秀/閉秀/佩戴Fantasy Bra最多次的模特來自於哪些國家?能否成為維密天使與參加大秀的次數是否有關?
- 開秀/閉秀/佩戴Fantasy Bra與參加大秀的次數是否有關?
- 開秀/閉秀/佩戴Fantasy Bra的模特一定都是維密天使嗎?
- 中國模特們在維密大秀是否也有了一席之地?
- 維密選人是不是越來越“花心”了?
話不多說,我們直接進入正題吧!
▍數據爬取
我在Wikipedia(維基百科)上找到了兩個頁面,第一個頁面“Victoria's Secret Fashion Show”中有3個數據表格提供了我們想要的信息,分別是:
(圖片說明:記錄了每年大秀的時間、地點、參加模特、演出者等信息的summary table)
(圖片說明:記錄了每年Fantasy Bra的佩戴模特、模特國籍、bra價值等信息的表格)
(圖片說明:記錄了每年開秀、閉秀模特的表格)
在第二個Wikipedia頁面“List of Victoria's Secret models”中,有兩個表格分別提供了維密天使、參加維密大秀的非天使模特的信息,表格部分截圖如下:
(圖片說明:維密天使的信息表格)
(圖片說明:參加維密大秀的模特(不包括維密天使)的表格)
對於這五個表格,我使用了scrapy包,自行搭建了5個爬蟲,對想要的信息進行抓取,然後以csv格式進行存儲。
以第一個表格summary table為例,我想要獲取的列分別是:Event, Locations, Models.
Summary table的Spider代碼如下:
from scrapy import Spider
from summary.items import SummaryItem
class SummarySpider(Spider):
name = 'summary_spider'
allowed_urls = ['https://en.wikipedia.org']
start_urls = ['https://en.wikipedia.org/wiki/Victoria\'s_Secret_Fashion_Show']
def parse(self, response):
rows = response.xpath('//*[@id="mw-content-text"]/div/table[2]//tr')
event_patterns = ['./td[1]/a/text()','./td[1]/text()']
location_patterns = ['./td[3]//a/text()','./td[3]/text()']
for row in rows:
for event_pattern in event_patterns:
Event = row.xpath(event_pattern).extract_first()
if Event:
break
for location_pattern in location_patterns:
Locations = row.xpath(location_pattern).extract()
if Locations:
break
Models = row.xpath('./td[6]/div/ul//a/text()').extract()
Performers = row.xpath('./td[8]//a/text()').extract()
item = SummaryItem()
item['Event'] = Event
item['Locations'] = Locations
item['Models'] = Models
item['Performers'] = Performers
yield item
在這裡需要注意的是,原網頁中Event和Locations列的數據格式比較複雜,有文本和超鏈接的多種混合形式,為了儘可能不丟失數據,我們需要多試幾次,把所有pattern的xpath記錄下來,以便在爬取的時候遍歷它們。
從Fantasy Bra表格抓取年份、bra名稱、模特名字、bra價值等信息的Spider代碼如下:
from scrapy importSpider
from fantasy.items importFantasyItem
classFantasySpider(Spider):
name= 'fantasy_spider'
allowed_urls= ['https://en.wikipedia.org']
start_urls= ['https://en.wikipedia.org/wiki/Victoria\'s_Secret_Fashion_Show']
defparse(self, response):
rows= response.xpath('//*[@id="mw-content-text"]/div/table[3]//tr')
for row in rows:
year= row.xpath('./td[1]/text()').extract_first()
name= row.xpath('./td[2]/text()').extract_first()
model_name= row.xpath('./td[3]/a/text()').extract()
model_country= row.xpath('./td[3]/span/a/@title').extract()
value= row.xpath('./td[4]/text()').extract()
in_show= row.xpath('./td[5]/text()').extract_first()
item= FantasyItem()
item['year'] = year
item['name'] = name
item['model_name'] = model_name
item['model_country'] = model_country
item['value'] = value
item['in_show'] = in_show
yield item
抓取開秀、閉秀記錄的Spider代碼如下:
from scrapy importSpider
from openclose.items importOpencloseItem
classOpencloseSpider(Spider):
name= 'openclose_spider'
allowed_urls= ['https://en.wikipedia.org']
start_urls= ['https://en.wikipedia.org/wiki/Victoria\'s_Secret_Fashion_Show']
defparse(self, response):
rows= response.xpath('//*[@id="mw-content-text"]/div/table[5]//tr')
name_patterns= ['./td[2]/a/text()', './td[1]/a/text()']
country_patterns= ['./td[2]/span/a/@title','./td[1]/span/a/@title']
for row in rows:
for name_pattern in name_patterns:
model_name= row.xpath(name_pattern).extract_first()
if model_name:
break
for country_pattern in country_patterns:
model_country= row.xpath(country_pattern).extract_first()
if model_country:
break
item= OpencloseItem()
item['model_name'] = model_name
item['model_country'] = model_country
yield item
抓取維密天使名字、國籍的Spider代碼如下:
from scrapy importSpider
from ModelList.items importModellistItem
classModellistSpider(Spider):
name= 'modellist_spider'
allowed_urls= ['https://en.wikipedia.org']
start_urls= ['https://en.wikipedia.org/wiki/List_of_Victoria%27s_Secret_models']
defparse(self, response):
rows= response.xpath('//*[@id="mw-content-text"]/div/table[1]/tbody//tr')
for row in rows:
model_name= row.xpath('./td[1]//a/@title').extract_first()
model_country= row.xpath('./td[3]//a/@title').extract_first()
item= ModellistItem()
item['model_name'] = model_name
item['model_country'] = model_country
yield item
抓取參加過維密大秀的非天使模特信息的代碼如下:
from scrapy importSpider
from modellist2.items importModellist2Item
classmodellit2Spider(Spider):
name= 'modellist2_spider'
allowed_urls= ['https://en.wikipedia.org']
start_urls= ['https://en.wikipedia.org/wiki/List_of_Victoria%27s_Secret_models']
defparse(self, response):
rows= response.xpath('//*[@id="mw-content-text"]/div/table[2]/tbody//tr')
for row in rows:
model_name= row.xpath('./td[1]/span/a/text()').extract_first()
model_country= row.xpath('./td[2]/a/@title').extract_first()
item= Modellist2Item()
item['model_name'] = model_name
item['model_country'] = model_country
yield item
▍數據清洗
獲得了我們想要的數據後,直接在jupyter notebook中將csv文件導入進行清洗,在這個過程中我運用到了numpy、pandas以及正則表達式這幾個工具包。
在這裡我將對每個表格的數據清洗過程分別進行簡要的概括,具體細節和操作代碼可以移步jupyter notebook中查看。
1.Summary Table
原始數據:
原始表格中每一年的大秀為一條記錄。Event列的有效信息只有年份,因此我們新建一列Event_year加在表格中。
Locations列的字符串既有“地點+城市“,又有”城市+國家“的組合,比較混亂,我們根據實際情況新建一列Event_country,手動賦值,用於記錄每年大秀的舉辦國家。
Models這一列的情況更為複雜,每一年參加走秀的所有模特名字都被記錄在1個單元格內,然而我們想要進行的是建立在模特個體層級上的數據分析,因此需要對模特名字的列表進行拆分,使得每一年每一個參與走秀的model都佔據一行記錄。
清洗後的數據:
2.開秀/閉秀記錄
原始數據:
網頁中的開秀和閉秀按照時間順序被記錄在了一起,原始數據表格中index為奇數的行是開秀模特,偶數行為閉秀模特,大秀時間從1995年到2017年(其中2004年沒有走秀,而是以巡迴展出的形式呈現)。我將開秀和閉秀分離成兩個dataframe,並添加上年份一列。
清洗後的開秀記錄:
清洗後的閉秀記錄:
最後我們將開秀和閉秀的記錄合併到第一步的summary table中。創建新的列open,若該模特在該年的走秀中擔任開秀,則open取值為1,否則為0:
#編寫for循環,查找歷年走秀記錄中的每一個(模特,年份)組合是否出現在開秀記錄中
for i inrange (0,777):
if(df_model_summary['Event_year'][i], df_model_summary['model_name'][i]) in[(df_open['year'][i],df_open['model_name'][i]) for i inrange(0,22)]:
df_model_summary['open'][i] = 1
同樣的方法再創建close列,最終得到summary table如下:
3.Fantasy Bra
原始數據:
我們將爬取下來的原始數據中的換行字符去掉,year列的數據類型改為整數;
2014年維密推出了一對Fantasy Bra,由兩位模特佩戴,但是數據中只用一行記錄了下來,在這裡我們用之前拆分模特名字的方法處理一下,將2014年的記錄分成兩行,每位模特佔一行,然後手動修正國籍、bra價值,使2014年的記錄以如下形式呈現:
清洗後的Fantasy Bra表格:
在summary table新建一列,記錄Fantasy Bra的佩戴情況,使用的方法與之前創建open列和close列的相同,得到新的summary table:
4.維密天使表格
導入原始數據(部分截圖):
這個表格相對乾淨一些,進行的數據處理操作有:去掉全部為空的第一行;去掉了第11行模特名字中多餘的字符串‘(model)‘;補齊了在數據爬取過程中丟失的一個模特國籍;創建一列angel,用於記錄模特是否為維密天使,對這個表格中所有的模特賦值為1。
清洗後數據如下:
5.參與走秀的非天使模特表格
導入原始數據(部分截圖):
這裡除了丟失的模特國籍數據的處理過程,其他都與上一個天使表格的操作過程類似。
至於國籍丟失的問題,我查看了原網頁,是由於原網頁將相鄰的兩個同國籍模特的兩個國籍單元格合併成一個導致的,例如下圖:
在這個例子中,我們抓取下來的數據,只有第一行Ana Beatriz的國籍沒有丟失,後面兩個國籍被合併的模特,在原數據表格中的國籍都是NaN
寫一個循環解決這個問題就好了:
for i inrange (0,236):
ifmodel_list_2.model_country[i] is np.nan:
model_list_2.model_country[i] =model_list_2.model_country[i-1]
得到修復好的非天使模特列表:
最後將天使和非天使列表連接在一起,得到所有走秀的模特列表。
至此,我們得到了下一步數據可視化需要的兩個表格:
1.歷年走秀記錄(每一行是每年參與走秀的一位模特,記錄模特在當年的大秀中是否開秀/閉秀/佩戴fb)
2.全部模特列表(每一行是一位模特,記錄該模特的國籍、是否為維密天使)
數據清洗過程中,我對DataFrame應用了很多次for loop,後來經過紐約數據科學院的老師點撥,如果改用pd.dataframe的apply方法來處理會有更高的效率。
▍數據可視化
首先還是從導入數據開始,導入歷年走秀記錄(df_show)和模特列表(df_model)兩個表格。
這裡發現了一個小問題。對於df_show,我想要進行模特個體水平的aggregation,計算每個模特參加走秀的總次數,最終得到了278個模特;但是df_model有273個模特,為了發現問題,這裡我將兩個表格中得到的模特名單merge了一下:
temp =pd.merge(df_show_count, df_model, how = 'left', on ='model_name')
temp.loc[temp.model_country.isnull(),:]
下表這些名字,都是在df_show中有記錄,但在df_model中查找不到的。倒數三行是從網頁中抓取出的特殊註解字符,刪除它們即可;其他的名字查找不到的原因,經過我的排查,大部分是含有特殊字母的模特名字在兩邊表格中的拼寫方式沒有統一(比如名字中含有字母é的名字,在另外的表中用的是e來進行拼寫);好在這裡沒有出現大小寫不統一的情況,如果有的話,先把兩表中所有的名字都改成lower case,再去匹配兩個表,就可以避免mismatch的情況啦。
對於特殊字符(就是像é這樣含有上標的字母)拼寫不統一的情況,我用的方法就是在兩邊的表格逐個查詢、逐個修改……真的山窮水盡了……累哭我了!
解決了名字拼寫的問題,我們以df_show為基礎,對每個模特的走秀次數、開秀次數、閉秀次數、佩戴fb次數進行計算,然後將計算結果merge到df_model中,得到model_summary表格(部分截圖如下):
終於可以進入正經的可視化階段了!這個部分我用到的主要是matplotlib和seaborn兩個工具包。
Q1. 誰是參加過最多次走秀的模特?
先來看一下每個模特走秀總次數的頻數分佈直方圖,絕大部分模特都只走過1次秀,極少數模特走過最多19次維密大秀,可以說是元老級別的模特了!
這位參加過19次維密秀的模特就是Adriana Lima, 緊跟其後的是在2017年完成人生中第18次也是最後一次維密秀的Alessandra Ambrosio,何穗是進入上圖排行榜的唯一中國模特,她將在今年迎來自己的第八次維密秀。
Q2. 哪位模特開秀/閉秀/佩戴Fantasy Bra的次數最多?
維密的元老級天使Heidi Klum為維密秀開秀3次,是獨一無二的榜首。
相比於開秀,閉秀的分配集中在了更少的幾個模特當中。走了19年維密秀的Adriana Lima閉秀次數最多,高達五次。
至於Fantasy Bra的分配,佩戴次數最多的三位分別是Tyra Banks, Heidi Klum, Adriana Lima,都是開秀/閉秀經驗豐富的模特,維密真的很寵她們!
Q3. 維密天使都來自於哪些國家?
維密天使中,來自美國的佔比最多,超過了四分之一;巴西和荷蘭緊隨其後,與美國組成前三,這三個國家的維密天使佔據了超過半壁江山。
Q4. 開秀/閉秀/佩戴Fantasy Bra最多次的模特來自於哪些國家?
巴西的模特被選為開秀模特的次數最多,其次是來自南非的模特;令我驚訝的是,維密天使人數最多的美國,還沒有模特開過秀。
看到閉秀分配這張圖,前面的疑慮好像得到了解答哈哈哈……從未開過秀的美國模特,獲得了最多次的閉秀任務。閉秀次數排在第二的是德國,同樣地,來自這個國家的模特也沒有開過秀。
到了Fantasy Bra這裡,前兩位分別是巴西和美國。巴西是開秀次數top1,美國是閉秀次數top1。很好,有點雨露均霑的意思……
Q5. 成為維密天使與否與參加大秀的次數是否有關?
我對天使和非天使的走秀總次數分別繪製了箱線圖,肉眼可見的分別,維密天使的走秀從總次數比非天使要多出很多。
再跑一個2 sample t-test 驗證一下:
#跑一個2sample t-test 驗證我們的猜想
number_of_shows_walked= model_summary['number_of_shows_walked']
shows_non_angel =number_of_shows_walked[model_summary['angel']==0]
shows_angel =number_of_shows_walked[model_summary['angel']==1]
shows_non_angel =shows_non_angel.dropna()
shows_angel = shows_angel.dropna()
from scipy importstats
stats.ttest_ind(shows_non_angel,shows_angel)
Ttest_indResult(statistic=-13.017789081684617, pvalue=2.3704995718195955e-30)
P值接近0,說明非天使模特和天使的走秀次數差異真的是非常大了!不過這也不能說明走秀次數多就是成為天使的原因……有一些計量經濟學基礎的朋友都懂的
Q6. 開秀/閉秀/佩戴Fantasy Bra與參加大秀的次數是否有關?這些模特一定都是維密天使嗎?
a.開秀
左半邊綠色部分說明,非天使開秀的例子是有的,但是這些模特最多隻開過一次秀,她們分別是Carmen Kass,Claudia Schiffer,Ingrid Seynhaeve,Naomi Campbell,值得注意的是Claudia Schiffer在她僅有的一次走秀歷史中,就擔任了開秀的任務。
右半邊橘色部分說明在天使中,走秀次數與開秀次數有一定的正相關關係,但是也有模特曾經走秀18次卻只開秀過一次的情況。
b.閉秀
閉秀機會的獲得對天使身份的要求似乎更寬鬆一些,我們發現了非天使模特閉秀2次的例子,但是這些參與過閉秀的非天使模特,無一例外地都有4-6次的走秀經歷。
她們分別是Ana Beatriz Barros,Carmen Kass,Magdalena Frackowiak,Naomi Campbell,Toni Garrn,其中Naomi也有過開秀經歷,是非天使模特中很受維密重視的角色!
右半邊橘色部分呈現出閉秀次數與走秀次數之間存在一定的正相關關係。
c. Fantasy Bra
到了更為珍貴的Fantasy Bra這裡,對天使身份的要求就非常嚴格了,所有的Fantasy Bra佩戴者都是維密天使,佩戴FB的次數與天使們走秀的年資也呈正相關關係。
Q7. 中國模特們在維密大秀是否也有了一席之地?
最後一個話題是我們都很關心的中國模特在維密T臺上的發展態勢,自從2009年有中國模特出現在維密T臺上,歷年參加維密大秀的國模數量總體是呈現上升趨勢的。
在2017年維密來到中國上海舉辦大秀時,國模數量達到了頂峰,體現出了維密對主辦地市場足夠的重視度;不過在今年即將舉辦的大秀上,國模的數量又回到了2名(分別是何穗、奚夢瑤,法籍華裔模特陳瑜未計入內),雎曉雯、謝欣、賀聰等模特的落選是讓我覺得特別遺憾的。
Q8. 維密選人是不是越來越“花心”了?
放眼望去,每年維密大秀啟用的模特總數量都在逐年上升(04年的斷崖是因為當年維密沒有舉辦大秀)。那麼維密每年啟用的新人比例是否也同樣在上升呢?帶著這個問題我們構造了一個新的變量new_model來記錄某年的某位模特是否是首次上秀,代碼如下:
year = list(range(1995, 2019))
#將每年走秀的所有模特名字放在一個set中,從1995年到2018年一共24個set
model_list_by_year = list(range(0,24))
for i inrange(0,24):
model_list_by_year[i] = set(df_show['model_name'][df_show['Event_year']==i+1995])
#令每一年的模特名字和前一年的取並集
model_list_by_year_accumulative= list(range(0,24))
model_list_by_year_accumulative[0] = model_list_by_year[0]
for i inrange (1,24):
model_list_by_year_accumulative[i] =list(model_list_by_year[i].union(model_list_by_year[i-1]))
#將年份和每年的accumulative模特名單放在一起,組成一個dataframe
model_set =pd.DataFrame({'year': year,'model_set':model_list_by_year_accumulative})
model_set.head()
#以1996年為例,先提取出某一年的走秀記錄,到model_set中對年份進行匹配,再查找模特名字
model_1996 =df_show.loc[df_show['Event_year']==model_set['year'][1]].apply(lambdax: x['model_name'] notin model_set['model_set'][0], axis = 1)
#寫一個for循環,對每一年分別進行查詢操作,list中每一年都對應一個boolean series,一共有23年,得到一個由23個series組成的list
new_model=list()
for i inrange(0,23):
new_model.append(df_show.loc[df_show['Event_year']==model_set['year'][1+i]].apply(lambdax: x['model_name']notin model_set['model_set'][i], axis = 1))
#將23個series合併成一整個series,再轉成datarame
new_model_list =new_model[0]
for i inrange(1,23):
new_model_list = pd.concat([new_model_list,new_model[i]],axis=0)
new_model_list =new_model_list.to_frame()
new_model_list.columns=['new_model']
#取df_show從1996年開始的數據,研究啟用新模特比例的問題
df_show_new =df_show.loc[df_show['Event_year']!=1995,:]
#和new_model變量合併
df_show_new =pd.concat([df_show_new, new_model_list],axis=1)
#計算每年新上秀的模特比例,畫圖
plt.figure(figsize=[12,6])
plt.xlabel('Year of the show')
plt.ylabel('Ratio of New Models')
(df_show_new.loc[df_show_new['new_model']==True,:].groupby('Event_year')['model_name'].count()/df_show_new.groupby('Event_year')['model_name'].count()).plot.bar(color= '#f08080')
plt.title('How many new models were chosen every year?', fontsize=15)
雖然每年上秀的模特數量都在增加,每年上秀的新人比例卻是在一直波動。2004年維密舉辦了Angels Across America Tour 來替代大秀,由五名老牌天使擔任模特,所以我們在04年看到了一個觸底的值。
2006年之後除2009年的每一年,新人比例看起來都要比2002年以前的比例要低一些,選人的策略相比歷史更為保守和固定了。
不過今年的新人比例與2017年相比,也是有一點提升,而且由於今年的模特基數是歷年最高,嶄新的面孔也就特別多了。
在今年將要上秀的50餘名模特中,首次上秀的模特有接近20名。一想到有這麼多的新晉美好肉體將要出現在今年的大秀上,我就特別激動!嚇得我趕緊放下手裡的肥宅快樂水去減肥啦……
注:本文寫於2018年10月9日,文中相關數據截至2018年10月9日。內容僅為作者觀點,不代表DT數據俠立場。文中圖片部分來自作者。
期待更多數據俠乾貨分享、話題討論、福利發放?在公眾號DT數據俠(ID:DTdatahero)後臺回覆“數據社群”,可申請加入DT數據社群。
▍數據俠門派
本文數據俠Haoyue Zhang,研究生畢業於杜克大學量化管理專業,想要進一步鞏固數據分析技能而加入數據俠Python訓練營。一枚熱愛娛樂、文化、時尚行業的沙雕少女兼歸國務工數據民工~
▍加入數據俠
數據俠計劃是由第一財經旗下DT財經發起的數據社群,包含數據俠專欄、數據俠實驗室系列活動和數據俠聯盟,旨在聚集大數據領域精英,共同挖掘數據價值。申請入群請添加DT君微信(dtcaijing003)並備註“數據社群”,合作請聯繫[email protected]。
閱讀更多 DT財經 的文章