原来 Matplotlib 绘图也可以这么漂亮,这次真的是学习到了!
來源:Python大數(shù)據(jù)分析
作者:朱衛(wèi)軍
平時(shí)總是眼饞網(wǎng)易數(shù)讀、DT財(cái)經(jīng)的各種精美可視化圖,又限于自己手殘學(xué)不會(huì)AdoeIllutrator。
不過相信你們看完今天的文章之后,就知道掌握了Matplotlib,好看的數(shù)據(jù)可視化咱也可以做了!!!
01
簡(jiǎn)介
前不久「貝殼研究院」基于其豐富的房地產(chǎn)相關(guān)數(shù)據(jù)資源,發(fā)布了「2020 新一線城市居住報(bào)告」:
圖1
而在這個(gè)報(bào)告中有幾張數(shù)據(jù)可視化作品還是比較可圈可點(diǎn)的,作為(在模仿中精進(jìn)數(shù)據(jù)可視化)系列文章的開篇之作,我將基于我觀察原始數(shù)據(jù)可視化作品進(jìn)而構(gòu)思出的方式,以純Python的方式模仿復(fù)刻圖2所示作品:
圖2
02
復(fù)刻過程
2.1 觀察原作品
其實(shí)原作品咋一看上去有點(diǎn)復(fù)雜,但經(jīng)過觀察,將原始圖片主要元素拆分成幾個(gè)部分來構(gòu)思復(fù)現(xiàn)方式,還是不算復(fù)雜的,我總結(jié)為以下幾部分:
(1)坐標(biāo)系部分
稍微懂點(diǎn)數(shù)據(jù)可視化的人應(yīng)該都可以看出原作品的坐標(biāo)不是常規(guī)的笛卡爾坐標(biāo)系,而是極坐標(biāo)系,這里復(fù)現(xiàn)原作品極坐標(biāo)系的難點(diǎn)在于,其并不是完整的極坐標(biāo)系,即左邊略小于半圓的區(qū)域是隱藏了參考線的。因此與其在matplotlib中極坐標(biāo)系的基礎(chǔ)上想方法隱藏部分參考線,不如逆向思維,從構(gòu)造參考線的角度出發(fā),自己組織構(gòu)造參考線,會(huì)更加的自由和靈活。
(2)顏色填充
這里的「顏色填充」指的是以居住自由指數(shù)折線為中線,在購(gòu)房自由指數(shù)折線與租房自由指數(shù)折線之間的顏色填充區(qū)域,但困難的是這里當(dāng)購(gòu)房自由指數(shù)高于租房自由指數(shù)時(shí)對(duì)應(yīng)的顏色為淺藍(lán)綠色,而反過來則變?yōu)榛疑?#xff0c;與購(gòu)房自由指數(shù)、租房自由指數(shù)的顏色相呼應(yīng)。
圖3
2.2 開始動(dòng)手!
綜合考慮前面這些難點(diǎn),我決定借助matplotlib+geopandas+shapely操縱幾何對(duì)象和繪制調(diào)整圖像的方便快捷性,來完成這次的挑戰(zhàn)。
2.2.1 構(gòu)建坐標(biāo)系統(tǒng)
因?yàn)闃O坐標(biāo)系中的參考線非常類似俯視南北極點(diǎn)所看到的經(jīng)緯線,因此我們可以利用地圖學(xué)中坐標(biāo)參考系里的「正射投影」(Orthographic),可以理解為純粹的半球:
圖4
我們只需要設(shè)定中心點(diǎn)參數(shù)在南極點(diǎn)或北極點(diǎn),再配合簡(jiǎn)單的經(jīng)緯度相關(guān)知識(shí)就可以偽造出任意的經(jīng)緯線,再利用geopandas中的投影變換向設(shè)定好的「正射投影」進(jìn)行轉(zhuǎn)換,再作為平面坐標(biāo)進(jìn)行繪圖即可。譬如按照這個(gè)思路來創(chuàng)建東經(jīng)10度到東經(jīng)220度之間,以及南緯-90度到-80度之間,對(duì)應(yīng)的5條緯度線和對(duì)應(yīng)38個(gè)城市的經(jīng)線:
import geopandas as gpd from shapely.geometry import LineString, Point, Polygon import matplotlib.pyplot as plt import numpy as np import warnings plt.rcParams['font.sans-serif'] = ['SimHei'] # 解決matplotlib中文亂碼問題 plt.rcParams['axes.unicode_minus'] = False # 解決matplotlib負(fù)號(hào)顯示問題 warnings.filterwarnings('ignore') # 設(shè)置中心點(diǎn)在南極點(diǎn)的正射投影 crs = '+proj=ortho +lon_0=0 +lat_0=-90' # 構(gòu)建經(jīng)度線并設(shè)置對(duì)應(yīng)經(jīng)緯度的地理坐標(biāo)系 lng_lines = gpd.GeoDataFrame({ 'geometry': [LineString([[lng, -90], [lng, -78]]) for lng in np.arange(10, 220, 210 / 38)]}, crs='EPSG:4326') # 構(gòu)建緯度線并設(shè)置為對(duì)應(yīng)經(jīng)緯度的地理坐標(biāo)系 lat_lines = gpd.GeoDataFrame({ 'geometry': [LineString([[lng, lat] for lng in range(10, 220)]) for lat in range(-90, -79, 2)]}, crs='EPSG:4326')構(gòu)造好數(shù)據(jù)之后,將經(jīng)線與緯線對(duì)應(yīng)的GeoDataFrame轉(zhuǎn)換到設(shè)置好的「正射投影」crs上,再作為不同圖層進(jìn)行疊加繪制:
圖5
嘿嘿,是不是底層的參考線已經(jīng)有內(nèi)味了~
2.2.2 繪制指標(biāo)折線
坐標(biāo)系以及參考線的邏輯定了下來之后,接下來我們需要將原作品中所展現(xiàn)的3種指標(biāo)數(shù)據(jù)轉(zhuǎn)換為3條樣式不同的折線。首先我們來準(zhǔn)備數(shù)據(jù),因?yàn)樵瓐?bào)告中只能找到居住自由指數(shù)的具體數(shù)值,其他兩個(gè)指標(biāo)未提供,因此我們可以結(jié)合這3個(gè)數(shù)值的相互關(guān)系,推斷出每個(gè)城市的購(gòu)房自由指數(shù)與租房自由指數(shù)1個(gè)比自身的居住自由指數(shù)高,1個(gè)比居住自由指數(shù)低的規(guī)律來「?jìng)卧臁箶?shù)據(jù):
圖6
按照前面推斷出的規(guī)則來偽造示例數(shù)據(jù),并對(duì)偽造過程中的不合理數(shù)據(jù)進(jìn)行修正:
def fake_index(value): fake = [] fake.append(value+np.random.uniform(5, 10)) fake.append(value-np.random.uniform(5, 10)) return np.random.choice(fake, size=2, replace=False).tolist() data['購(gòu)房自由指數(shù)'], data['租房自由指數(shù)'] = list(zip(*data['居住自由指數(shù)'].apply(fake_index))) # 修正偽造數(shù)據(jù)中大于100和小于0的情況 data.loc[:, '居住自由指數(shù)':] = data.loc[:, '居住自由指數(shù)':].applymap(lambda v: 100 if v > 100 else v) data.loc[:, '居住自由指數(shù)':] = data.loc[:, '居住自由指數(shù)':].applymap(lambda v: 0 if v < 0 else v) data.head()圖7
至此我們的數(shù)據(jù)已經(jīng)偽造完成,接下來我們需要做的事情是對(duì)我們的指標(biāo)值進(jìn)行變換,使其能夠適應(yīng)前面所確立的坐標(biāo)系統(tǒng)。雖然嚴(yán)格意義上說俯視南極點(diǎn)所看到的每一段等間距的緯度帶隨著其越發(fā)靠近赤道,在平面上會(huì)看起來越來越窄,但因?yàn)槲覀冞x取的是南緯-90度到南緯-80度之間的區(qū)域,非常靠近極點(diǎn),因此可以近似視為每變化相同緯度寬度是相等的。利用下面的函數(shù)實(shí)現(xiàn)0-100向-90到-80的線性映射:
圖8
接下來我們就來為每個(gè)指標(biāo)構(gòu)造線與散點(diǎn)部分的矢量數(shù)據(jù),并在統(tǒng)一轉(zhuǎn)換坐標(biāo)參考系到「正射投影」之后疊加到之前的圖像上:
# 為每個(gè)城市生成1條經(jīng)線 lng_lines = gpd.GeoDataFrame({ 'geometry': [LineString([[lng, -90], [lng, -78]]) for lng in np.arange(10, 220, 210 / data.shape[0])]}, crs='EPSG:4326') # 居住自由指數(shù)對(duì)應(yīng)的折線 line1 = gpd.GeoDataFrame({ 'geometry': [LineString([(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]), data['居住自由指數(shù)_映射值'])])]}, crs='EPSG:4326') # 居住自由指數(shù)對(duì)應(yīng)的折線上的散點(diǎn) scatter1 = gpd.GeoDataFrame({ 'geometry': [Point(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]), data['居住自由指數(shù)_映射值'])]}, crs='EPSG:4326') # 購(gòu)房自由指數(shù)對(duì)應(yīng)的折線 line2 = gpd.GeoDataFrame({ 'geometry': [LineString([(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]), data['購(gòu)房自由指數(shù)_映射值'])])]}, crs='EPSG:4326') # 購(gòu)房自由指數(shù)對(duì)應(yīng)的折線上的散點(diǎn) scatter2 = gpd.GeoDataFrame({ 'geometry': [Point(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]), data['購(gòu)房自由指數(shù)_映射值'])]}, crs='EPSG:4326') # 租房自由指數(shù)對(duì)應(yīng)的折線 line3 = gpd.GeoDataFrame({ 'geometry': [LineString([(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]), data['租房自由指數(shù)_映射值'])])]}, crs='EPSG:4326') # 租房自由指數(shù)對(duì)應(yīng)的折線上的散點(diǎn) scatter3 = gpd.GeoDataFrame({ 'geometry': [Point(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]), data['租房自由指數(shù)_映射值'])]}, crs='EPSG:4326') fig, ax = plt.subplots(figsize=(8, 8)) # 繪制經(jīng)度線與緯度線 ax = lng_lines.to_crs(crs).plot(ax=ax, linewidth=0.4, edgecolor='lightgrey') ax = lat_lines.to_crs(crs).plot(ax=ax, linewidth=0.75, edgecolor='grey', alpha=0.8) ax = line1.to_crs(crs).plot(ax=ax, color='black', linewidth=1) ax = scatter1.to_crs(crs).plot(ax=ax, color='black', markersize=12) ax = line2.to_crs(crs).plot(ax=ax, color='#00CED1', linewidth=0.6) ax = scatter2.to_crs(crs).plot(ax=ax, color='#00CED1', markersize=4) ax = line3.to_crs(crs).plot(ax=ax, color='lightgrey', linewidth=0.6) ax = scatter3.to_crs(crs).plot(ax=ax, color='lightgrey', markersize=4) ax.axis('off'); # 關(guān)閉坐標(biāo)軸 fig.savefig('圖11.png',?dpi=500,?inches_bbox='tight',?inches_pad=0)?? 圖9哈哈,是不是更加有內(nèi)味了~,至此,我們的繪制指標(biāo)折線部分已完成。
2.2.3 繪制填充區(qū)域
在相繼解決完「坐標(biāo)系統(tǒng)」、「指標(biāo)折線繪制」之后,就到了最好玩的部分了,接下來我們來繪制圖中購(gòu)房自由指數(shù)與租房自由指數(shù)之間的折線,并且要按照「填充較大值對(duì)應(yīng)色彩」的原則來處理,接下來我們需要用到一點(diǎn)簡(jiǎn)單的拓?fù)鋵W(xué)知識(shí),首先我們分別構(gòu)造購(gòu)房自由指數(shù)_映射值和租房自由指數(shù)_映射值引入南極點(diǎn)后所圍成的多邊形:
圖10
圖11
接下來我們先暫停下來思考思考,購(gòu)房自由指數(shù)_映射值與租房自由指數(shù)_映射值之間彼此高低起伏交錯(cuò)而形成的填充區(qū)域?qū)?yīng)著上面兩個(gè)多邊形之間的什么關(guān)系?沒錯(cuò)!就是就是兩者去除掉彼此重疊區(qū)域后各自剩余的部分!
圖12
那么接下來我們要做的事就so easy了,只需要分別得到兩者去除重疊面后,剩余的部分,以對(duì)應(yīng)的填充色彩疊加繪制在圖11的圖像上就可以啦~,利用geopandas中的difference即可輕松實(shí)現(xiàn):
fig, ax = plt.subplots(figsize=(8, 8)) # 繪制經(jīng)度線與緯度線 ax = lng_lines.to_crs(crs).plot(ax=ax, linewidth=0.4, edgecolor='lightgrey') ax = lat_lines.to_crs(crs).plot(ax=ax, linewidth=0.75, edgecolor='grey', alpha=0.8) ax = line1.to_crs(crs).plot(ax=ax, color='black', linewidth=1) ax = scatter1.to_crs(crs).plot(ax=ax, color='black', markersize=12) ax = line2.to_crs(crs).plot(ax=ax, color='#00CED1', linewidth=0.6) ax = scatter2.to_crs(crs).plot(ax=ax, color='#00CED1', markersize=4) ax = line3.to_crs(crs).plot(ax=ax, color='lightgrey', linewidth=0.6) ax = scatter3.to_crs(crs).plot(ax=ax, color='lightgrey', markersize=4) ax = polygon1.difference(polygon2).plot(ax=ax, color='#00CED1', alpha=0.2) polygon2.difference(polygon1).plot(ax=ax, color='lightgrey', alpha=0.6) ax.axis('off'); # 關(guān)閉坐標(biāo)軸 fig.savefig('圖13.png',?dpi=500,?inches_bbox='tight',?inches_pad=0)??圖13
2.2.4 補(bǔ)充文字、標(biāo)注等元素
其實(shí)到這里,我們就已經(jīng)完成了對(duì)原作品復(fù)刻的精髓部分了,剩下的無(wú)非是添加些文字、刻度之類的,其實(shí)這部分很多都可以在出圖之后利用其他軟件PS完成,比寫代碼輕松,所以這部分只對(duì)添加「城市+指標(biāo)」的文字標(biāo)簽以及刻度值進(jìn)行補(bǔ)充:
圖14
再模仿原作品裁切一下圖片,主要元素是不是非常一致了~,大家也可以根據(jù)自己的喜好來修改不同的顏色:
圖15
推薦閱讀
誤執(zhí)行了rm -fr /*之后,除了跑路還能怎么辦?!
程序員必備58個(gè)網(wǎng)站匯總
大幅提高生產(chǎn)力:你需要了解的十大Jupyter Lab插件
總結(jié)
以上是生活随笔為你收集整理的原来 Matplotlib 绘图也可以这么漂亮,这次真的是学习到了!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 肝!教你用Python抓取某天下楼盘数据
- 下一篇: 年薪 50w 难吗?分享我的 2 个捷径