本文完整代碼及數據已上傳至我的Github倉庫https://github.com/CNFeffery/FefferyViz
1 簡介
新冠疫情對很多實體經濟帶來衝擊的同時,也給很多公司帶來了新的增長點。前段時間我看到圖1所示的數據可視化作品,針對2020年1月1日到6月16日之間,世界範圍內市值增大最多的25家公司進行可視化:
圖1
這樣一張典型的商業圖表,看起來形式巧妙,且表現出很多數據信息。而今天的文章,我就將帶大家學習如何利用
matplotlib
來條理清楚地製作出這種類型的可視化作品。
2 模仿過程
首先我們還是像過往的文章中一樣分析一下原作品的元素構成:
「立體感的營造」其實原作品咋一看起來的立體感,只是玩了個花招,我們本質上只需要創建出最左列豎直方向上等分25份的填充區域,再向右偏移適合的距離後,縮小豎直方向上的總體範圍再25等分,最後將這兩部分等分的填充區域連接起來,最後再為中間的連接區域蒙上一層等大小的帶透明度的暗色蒙版即可~
「logo與國旗圖片的插入」原作品中眾多圖片,只要仔細觀察就可以發現是手動PS上去的,存在著一些微小的瑕疵,而我們既然要用
matplotlib
來製作這張圖,當然直接寫循環控制圖片的插入即可。
在
matplotlib
中向畫板插入其他圖片有很多方法,我們為了控制好眾多logo之間的協調,可以使用
matplotlib
中的
inset_axes()
來插入指定位置和尺寸的子圖。
「數值標註的控制」原作品中不同公司市值增長的不同體現在不同長度柱體以及不同大小文字標註的映射之上的,我們可以配合簡單的歸一化變換,來約束字體和柱體長度的映射。
搞明白原作品中主要元素的實現方式之後,我們首先來讀入原始數據(「公眾號後臺回復top25獲取本文全部附件」):
import matplotlib.pyplot as pltimport pandas as pd# 設置默認字體plt.rcParams['font.sans-serif'] = ['Times New Roman']raw = pd.read_excel('data.xlsx')raw.head()
圖2
接著為了方便處理公司類型向指定配色的映射,我們先來創建一個映射字典:
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
做好這些準備工作之後,剩餘的繪圖過程就很簡單了,最終得到的模仿作品如下:
圖3
完整代碼如下,雖然看起來略多,其實大部分都是重複的邏輯傳入不同的參數而已,還是比較簡單的:
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')
你可以自由嘗試不同的配色方案,或者換成你的數據,快速製作出同樣別致的可視化作品~
以上就是本文的全部內容,歡迎在評論區與我進行討論~