本文完整程式碼及資料已上傳至我的
Github
倉庫https://github.com/CNFeffery/FefferyViz
1 簡介
新冠疫情對很多實體經濟帶來衝擊的同時,也給很多公司帶來了新的增長點。前段時間我看到圖1所示的資料視覺化作品,針對2020年1月1日到6月16日之間,世界範圍內市值增大最多的25家公司進行視覺化:
這樣一張典型的商業圖表,看起來形式巧妙,且表現出很多資料資訊。而今天的文章,我就將帶大家學習如何利用matplotlib
來條理清楚地製作出這種型別的視覺化作品。
2 模仿過程
首先我們還是像過往的文章中一樣分析一下原作品的元素構成:
- 立體感的營造
其實原作品咋一看起來的立體感,只是玩了個花招,我們本質上只需要建立出最左列豎直方向上等分25份的填充區域,再向右偏移適合的距離後,縮小豎直方向上的總體範圍再25等分,最後將這兩部分等分的填充區域連線起來,最後再為中間的連線區域蒙上一層等大小的帶透明度的暗色蒙版即可~
- logo與國旗圖片的插入
原作品中眾多圖片,只要仔細觀察就可以發現是手動PS上去的,存在著一些微小的瑕疵,而我們既然要用matplotlib
來製作這張圖,當然直接寫迴圈控制圖片的插入即可。
在matplotlib
中向畫板插入其他圖片有很多方法,我們為了控制好眾多logo之間的協調,可以使用matplotlib
中的inset_axes()
來插入指定位置和尺寸的子圖。
- 數值標註的控制
原作品中不同公司市值增長的不同體現在不同長度柱體以及不同大小文字標註的對映之上的,我們可以配合簡單的歸一化變換,來約束字型和柱體長度的對映。
搞明白原作品中主要元素的實現方式之後,我們首先來讀入原始資料(你可以在文章開頭的Github
倉庫中找到原始資料及相關附件):
import matplotlib.pyplot as plt
import pandas as pd
# 設定預設字型
plt.rcParams['font.sans-serif'] = ['Times New Roman']
raw = pd.read_excel('data.xlsx')
raw.head()
接著為了方便處理公司型別向指定配色的對映,我們先來建立一個對映字典:
type2color = {
'Technology': '#e2a080',
'E-Commerce': '#ebb66a',
'Automotive': '#c198ba',
'Finance': '#aab5d8',
'Tele-communications': '#bdd7e4',
'Media': '#efcfde',
'Software': '#d5c1c4',
'Pharmaceutical': '#f9e4ad',
'Alcohol': '#c3d3ac',
'Retail': '#88bb70'
}
而為了建立出原作品中最重要的不同條帶,我們可以配合matplotlib
中的fill_between()
。
而為了處理好左側與右側的豎直方向25等分割槽域,我們可以在對原資料每一行迴圈的過程中,自定義下列函式來計算區域範圍:
def create_fill_area(row, top_y=0.8, bottom_y=0.01):
# 初始化包圍填充區域的上下線條y座標
line1, line2 = [1 - 0.04*row, 1 - 0.04*row], [1- 0.04*(row+1), 1- 0.04*(row+1)]
# 追加陰影段y座標
line1.append(0.01 + (25 - row) * (0.8 - 0.01) / 25)
line2.append(0.01 + (25 - row - 1) * (0.8 - 0.01) / 25)
# 追加最後一段平行段y座標
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
做好這些準備工作之後,剩餘的繪圖過程就很簡單了,最終得到的模仿作品如下:
完整程式碼如下,雖然看起來略多,其實大部分都是重複的邏輯傳入不同的引數而已,還是比較簡單的:
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]):
# 定義區域填充對應的x座標
x = [0, 0.15, 0.215, 0.6+raw.at[row, 'Grown'] / 1000]
# 生成區域填充對應的y座標
line1, line2 = create_fill_area(row)
# 對指定區域進行填充
ax.fill_between(x,
line1,
line2,
color=type2color[raw.at[row, 'Type']],
edgecolor='none')
# 從logo資料夾下讀取對應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']])
# 處理單個及多個國家情況下的國旗繪製
for idx, country in enumerate(raw.at[row, 'Country'].split('&')[::-1]):
# 讀取對應國旗圖片
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')
# 處理第一名文字在填充區域內部,其餘文字在填充區域外的情況
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:
# 配合歸一化對字型進行大小對映
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')
# 對指定區域進行帶透明度的黑色蒙版,以達到陰影效果
ax.fill_between([0.15, 0.215],
[0, 0.01],
[1, 0.8],
color='black',
alpha=0.2, # 設定透明度
edgecolor='none')
# 補充其餘文字標註
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')
# 補充上方數值刻度
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')
# 補充下排圖例
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')
# 補充上排圖例
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')
你可以自由嘗試不同的配色方案,或者換成你的資料,快速製作出同樣別緻的視覺化作品?~
以上就是本文的全部內容,歡迎在評論區與我進行討論~