大家好,歡迎來到Crossin的編程教室,!
今年肆虐全球的新冠疫情給世界各國經(jīng)濟(jì)都造成了相當(dāng)大的影響,,尤其對(duì)很多實(shí)體經(jīng)濟(jì)帶來巨大沖擊。不過,,這期間也有不少公司保持了較高的增長,。前段時(shí)間我看到一張數(shù)據(jù)圖表(圖1),針對(duì)2020年1月1日到6月16日之間,,世界范圍內(nèi)市值增大最多的25家公司進(jìn)行可視化:
圖1這樣一張典型的商業(yè)圖表,,看起來形式巧妙,且表現(xiàn)出很多數(shù)據(jù)信息,。今天的文章中,,我就將帶大家學(xué)習(xí)如何利用matplotlib
來?xiàng)l理清楚地制作出這種類型的可視化作品。
2 模仿過程
首先我們來分析一下原作品的元素構(gòu)成:
其實(shí)原作品咋一看起來的立體感,,只是玩了個(gè)花招,,我們本質(zhì)上只需要?jiǎng)?chuàng)建出最左列豎直方向上等分25份的填充區(qū)域,再向右偏移適合的距離后,,縮小豎直方向上的總體范圍再25等分,,最后將這兩部分等分的填充區(qū)域連接起來,最后再為中間的連接區(qū)域蒙上一層等大小的帶透明度的暗色蒙版即可~
原作品中眾多圖片,,只要仔細(xì)觀察就可以發(fā)現(xiàn)是手動(dòng)PS上去的,,存在著一些微小的瑕疵,,而我們既然要用matplotlib
來制作這張圖,,當(dāng)然直接寫循環(huán)控制圖片的插入即可。
在matplotlib
中向畫板插入其他圖片有很多方法,,我們?yōu)榱丝刂坪帽姸鄉(xiāng)ogo之間的協(xié)調(diào),,可以使用matplotlib
中的inset_axes()
來插入指定位置和尺寸的子圖。
原作品中不同公司市值增長的不同體現(xiàn)在不同長度柱體以及不同大小文字標(biāo)注的映射之上的,,我們可以配合簡單的歸一化變換,,來約束字體和柱體長度的映射。
搞明白原作品中主要元素的實(shí)現(xiàn)方式之后,,我們就來動(dòng)手了,。
首先來讀入原始數(shù)據(jù),這次我們使用的是現(xiàn)成的數(shù)據(jù),,保存在excel表格中(下載地址見文末):
import matplotlib.pyplot as plt
import pandas as pd
# 設(shè)置默認(rèn)字體
plt.rcParams['font.sans-serif'] = ['Times New Roman']
raw = pd.read_excel('data.xlsx')
raw.head()
圖2接著為了方便處理公司類型向指定配色的映射,,我們先來創(chuàng)建一個(gè)映射字典:
type2color = {
'Technology': '#e2a080',
'E-Commerce': '#ebb66a',
'Automotive': '#c198ba',
'Finance': '#aab5d8',
'Tele-communications': '#bdd7e4',
'Media': '#efcfde',
'Software': '#d5c1c4',
'Pharmaceutical': '#f9e4ad',
'Alcohol': '#c3d3ac',
'Retail': '#88bb70'
}
而為了創(chuàng)建出原作品中最重要的不同條帶,我們可以配合matplotlib
中的fill_between()
,。
而為了處理好左側(cè)與右側(cè)的豎直方向25等分區(qū)域,,我們可以在對(duì)原數(shù)據(jù)每一行循環(huán)的過程中,自定義下列函數(shù)來計(jì)算區(qū)域范圍:
def create_fill_area(row, top_y=0.8, bottom_y=0.01):
# 初始化包圍填充區(qū)域的上下線條y坐標(biāo)
line1, line2 = [1 - 0.04*row, 1 - 0.04*row], [1- 0.04*(row+1), 1- 0.04*(row+1)]
# 追加陰影段y坐標(biāo)
line1.append(0.01 + (25 - row) * (0.8 - 0.01) / 25)
line2.append(0.01 + (25 - row - 1) * (0.8 - 0.01) / 25)
# 追加最后一段平行段y坐標(biāo)
line1.append(0.01 + (25 - row) * (0.8 - 0.01) / 25)
line2.append(0.01 + (25 - row - 1) * (0.8 - 0.01) / 25)
return line1, line2
做好這些準(zhǔn)備工作之后,剩余的繪圖過程就很簡單了,,最終得到的模仿作品如下:
圖3完整代碼如下,,雖然看起來略多,其實(shí)大部分都是重復(fù)的邏輯傳入不同的參數(shù)而已,,還是比較簡單的:
fig, ax = plt.subplots(figsize=(4.8, 6))
ax.set_xlim(0, 1.01)
ax.set_ylim(0, 1)
for row in range(raw.shape[0]):
# 定義區(qū)域填充對(duì)應(yīng)的x坐標(biāo)
x = [0, 0.15, 0.215, 0.6+raw.at[row, 'Grown'] / 1000]
# 生成區(qū)域填充對(duì)應(yīng)的y坐標(biāo)
line1, line2 = create_fill_area(row)
# 對(duì)指定區(qū)域進(jìn)行填充
ax.fill_between(x,
line1,
line2,
color=type2color[raw.at[row, 'Type']],
edgecolor='none')
# 從logo文件夾下讀取對(duì)應(yīng)logo圖片
try:
logo = plt.imread(f'logo/{raw.at[row, 'Company']}.png')
except FileNotFoundError:
logo = plt.imread(f'logo/{raw.at[row, 'Company']}.jpg')
# 插入公司logo
ax_logo = ax.inset_axes((0.05, 1 - 0.04*(row+1)+0.005, 0.08, 0.025))
ax_logo.imshow(logo)
ax_logo.axis('off')
ax_logo.set_facecolor(type2color[raw.at[row, 'Type']])
# 處理單個(gè)及多個(gè)國家情況下的國旗繪制
for idx, country in enumerate(raw.at[row, 'Country'].split('&')[::-1]):
# 讀取對(duì)應(yīng)國旗圖片
flag = plt.imread(f'flag/{country}.png')
# 插入國旗子圖
ax_flag = ax.inset_axes((0.545-idx*0.06, 0.013+(25 - row - 1)*((0.8 - 0.01) / 25), 0.1, 0.025))
ax_flag.imshow(flag)
ax_flag.axis('off')
ax_flag.set_facecolor(type2color[raw.at[row, 'Type']])
# 繪制排名
ax.text(0.025, (1 - 0.04*row + 1 - 0.04*(row+1)) / 2, str(row+1),
ha='center', va='center',
fontsize=5, color='black')
# 繪制公司名稱
ax.text(0.215+0.01, 0.5 * (0.01 + (25 - row - 1) * (0.8 - 0.01) / 25 + 0.01 + (25 - row) * (0.8 - 0.01) / 25),
raw.at[row, 'Company'],
ha='left', va='center',
fontsize=6, color='#494948',
weight='bold')
# 處理第一名文字在填充區(qū)域內(nèi)部,,其余文字在填充區(qū)域外的情況
if raw.at[row, 'Company'] == 'Amazon':
ax.text(1, 0.5 * (0.01 + (25 - row) * (0.8 - 0.01) / 25
+ 0.01 + (25 - row - 1) * (0.8 - 0.01) / 25)-0.0025,
'$'+str(raw.at[row, 'Grown'])+'B',
color='white',
fontsize=10,
ha='right',
va='center',
weight='bold')
else:
# 配合歸一化對(duì)字體進(jìn)行大小映射
ax.text(0.6+raw.at[row, 'Grown'] / 1000 + 0.01,
0.5 * (0.01 + (25 - row) * (0.8 - 0.01) / 25 + 0.01 + (25 - row - 1) * (0.8 - 0.01) / 25)-0.0025,
'$'+str(raw.at[row, 'Grown'])+'B',
color=type2color[raw.at[row, 'Type']],
fontsize=5+((raw.at[row, 'Grown'] - raw['Grown'].min())
/ (raw['Grown'].max() - raw['Grown'].min())) * 5,
ha='left',
va='center',
weight='bold')
# 對(duì)指定區(qū)域進(jìn)行帶透明度的黑色蒙版,以達(dá)到陰影效果
ax.fill_between([0.15, 0.215],
[0, 0.01],
[1, 0.8],
color='black',
alpha=0.2, # 設(shè)置透明度
edgecolor='none')
# 補(bǔ)充其余文字標(biāo)注
ax.text(0.215+0.01, 0.805, 'Company',
color='#565555', fontsize=5,
ha='left')
ax.text(0.6, 0.805, 'Country',
color='#565555', fontsize=5,
ha='center')
# 補(bǔ)充上方數(shù)值刻度
ax.text(0.6, 0.825, '0',
color='#a9a8a8', fontsize=4,
ha='center')
for i in range(1, 5):
ax.text(0.6+0.1*i, 0.825, f'${i}00B',
color='#a9a8a8', fontsize=4,
ha='center')
ax.vlines(0.6+0.1*i, 0.01, 0.82,
color='#dcdcdb', linewidth=0.2)
ax.set_xticks([])
ax.set_yticks([])
ax.spines['left'].set_color('none')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
# 補(bǔ)充下排圖例
ax_bar1 = ax.inset_axes((0.215, 0.88, 0.57, 0.02), transform=ax.transAxes)
ax_bar1.set_xlim(-0.45, 4.45)
ax_bar1.bar(range(5), height=1, width=0.9,
color=['#efcfde', '#d5c1c4', '#f9e4ad', '#c3d3ac', '#88bb70'])
ax_bar1.set_xticks(range(5))
ax_bar1.set_xticklabels(['Media', 'Software', 'Pharmaceutical', 'Alcohol', 'Retail'],
fontsize=5, color='#4f4e4e', weight='bold')
ax_bar1.set_yticks([])
ax_bar1.spines['left'].set_color('none')
ax_bar1.spines['right'].set_color('none')
ax_bar1.spines['top'].set_color('none')
ax_bar1.spines['bottom'].set_color('none')
ax_bar1.tick_params(color='none', pad=-2)
ax_bar1.set_facecolor('#f8f8f8')
# 補(bǔ)充上排圖例
ax_bar2 = ax.inset_axes((0.215, 0.98, 0.57, 0.02), transform=ax.transAxes)
ax_bar2.set_xlim(-0.45, 4.45)
ax_bar2.bar(range(5), height=1, width=0.9,
color=['#e2a080', '#ebb66a', '#c198ba', '#aab5d8', '#bdd7e4'])
ax_bar2.set_xticks(range(5))
ax_bar2.set_xticklabels(['Technology', 'E-Commerce', 'Automotive', 'Finance', 'Tele-\ncommunications'],
fontsize=5, color='#4f4e4e', weight='bold')
ax_bar2.set_yticks([])
ax_bar2.spines['left'].set_color('none')
ax_bar2.spines['right'].set_color('none')
ax_bar2.spines['top'].set_color('none')
ax_bar2.spines['bottom'].set_color('none')
ax_bar2.tick_params(color='none', pad=-2)
ax_bar2.set_facecolor('#f8f8f8')
ax.set_facecolor('#f8f8f8')
fig.set_facecolor('#f8f8f8')
fig.savefig('圖3.png', dpi=800, bbox_inches='tight')
你可以自由嘗試不同的配色方案,,或者換成你的數(shù)據(jù),,快速制作出同樣別致的可視化作品??~
本文完整代碼及數(shù)據(jù):https://github.com/CNFeffery/FefferyViz/tree/master/assets/05%EF%BC%9A%E7%96%AB%E6%83%85%E6%9C%9F%E9%97%B4%E5%B8%82%E5%80%BC%E5%A2%9E%E9%95%BFtop25%E5%85%AC%E5%8F%B8