1. 程式人生 > 實用技巧 >(在模仿中精進資料視覺化01) 極座標折線圖

(在模仿中精進資料視覺化01) 極座標折線圖

本文完整程式碼已上傳至我的Github倉庫https://github.com/CNFeffery/FefferyViz

1 簡介

  前不久貝殼研究院基於其豐富的房地產相關資料資源,釋出了2020 新一線城市居住報告

圖1

  而在這個報告中有幾張資料視覺化作品還是比較可圈可點的,作為(在模仿中精進資料視覺化)系列文章的開篇之作,我將基於我觀察原始資料視覺化作品進而構思出的方式,以純Python的方式模仿復刻圖2所示作品:

圖2

2 復刻過程

2.1 觀察原作品

  其實原作品咋一看上去有點複雜,但經過觀察,將原始圖片主要元素拆分成幾個部分來構思復現方式,還是不算複雜的,我總結為以下幾部分:

  • 1 座標系部分

  稍微懂點資料視覺化的人應該都可以看出原作品的座標不是常規的笛卡爾座標系,而是極座標系,這裡復現原作品極座標系的難點在於,其並不是完整的極座標系,即左邊略小於半圓的區域是隱藏了參考線的。

  因此與其在matplotlib中極座標系的基礎上想方法隱藏部分參考線,不如逆向思維,從構造參考線的角度出發,自己組織構造參考線,會更加的自由和靈活。

  • 2 顏色填充

  這裡的顏色填充指的是以居住自由指數折線為中線,在購房自由指數折線與租房自由指數折線之間的顏色填充區域,但困難的是這裡當購房自由指數高於租房自由指數時對應的顏色為淺藍綠色,而反過來則變為灰色,與購房自由指數租房自由指數的顏色相呼應。

圖3

2.2 開始動手!

  綜合考慮前面這些難點,我決定藉助matplotlib+geopandas+shapely操縱幾何物件和繪製調整影象的方便快捷性,來完成這次的挑戰。

2.2.1 構建座標系統

  因為極座標系中的參考線非常類似俯視南北極點所看到的經緯線,因此我們可以利用地圖學中座標參考系裡的正射投影Orthographic),可以理解為純粹的半球:

圖4

  我們只需要設定中心點引數在南極點或北極點,再配合簡單的經緯度相關知識就可以偽造出任意的經緯線,再利用geopandas中的投影變換向設定好的正射投影進行轉換,再作為平面座標進行繪圖即可。

  譬如按照這個思路來建立東經10度到東經220度之間,以及南緯-90度到-80度之間,對應的5條緯度線和對應38個城市的經線:

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負號顯示問題
warnings.filterwarnings('ignore')

# 設定中心點在南極點的正射投影
crs = '+proj=ortho +lon_0=0 +lat_0=-90'

# 構建經度線並設定對應經緯度的地理座標系
lng_lines = gpd.GeoDataFrame({
    'geometry': [LineString([[lng, -90], [lng, -78]]) for lng in np.arange(10, 220, 210 / 38)]}, 
    crs='EPSG:4326')

# 構建緯度線並設定為對應經緯度的地理座標系
lat_lines = gpd.GeoDataFrame({
    'geometry': [LineString([[lng, lat] for lng in range(10, 220)]) for lat in range(-90, -79, 2)]}, 
    crs='EPSG:4326')

  構造好資料之後,將經線與緯線對應的GeoDataFrame轉換到設定好的正射投影crs上,再作為不同圖層進行疊加繪製:

圖5

  嘿嘿,是不是底層的參考線已經有內味了~

2.2.2 繪製指標折線

  座標系以及參考線的邏輯定了下來之後,接下來我們需要將原作品中所展現的3種指標資料轉換為3條樣式不同的折線。

  首先我們來準備資料,因為原報告中只能找到居住自由指數的具體數值,其他兩個指標未提供,因此我們可以結合這3個數值的相互關係,推斷出每個城市的購房自由指數租房自由指數1個比自身的居住自由指數高,1個比居住自由指數低的規律來偽造資料:

圖6

  按照前面推斷出的規則來偽造示例資料,並對偽造過程中的不合理資料進行修正:

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['購房自由指數'], data['租房自由指數'] = list(zip(*data['居住自由指數'].apply(fake_index)))

# 修正偽造資料中大於100和小於0的情況
data.loc[:, '居住自由指數':] = data.loc[:, '居住自由指數':].applymap(lambda v: 100 if v > 100 else v)
data.loc[:, '居住自由指數':] = data.loc[:, '居住自由指數':].applymap(lambda v: 0 if v < 0 else v)
data.head()
圖7

  至此我們的資料已經偽造完成,接下來我們需要做的事情是對我們的指標值進行變換,使其能夠適應前面所確立的座標系統。

  雖然嚴格意義上說俯視南極點所看到的每一段等間距的緯度帶隨著其越發靠近赤道,在平面上會看起來越來越窄,但因為我們選取的是南緯-90度到南緯-80度之間的區域,非常靠近極點,因此可以近似視為每變化相同緯度寬度是相等的。

  利用下面的函式實現0-100向-90到-80的線性對映:

圖8

  接下來我們就來為每個指標構造線與散點部分的向量資料,並在統一轉換座標參考系到正射投影之後疊加到之前的影象上:

# 為每個城市生成1條經線
lng_lines = gpd.GeoDataFrame({
    'geometry': [LineString([[lng, -90], [lng, -78]]) for lng in np.arange(10, 220, 210 / data.shape[0])]}, 
    crs='EPSG:4326')

# 居住自由指數對應的折線
line1 = gpd.GeoDataFrame({
    'geometry': [LineString([(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]),
                                                            data['居住自由指數_對映值'])])]}, 
    crs='EPSG:4326')

# 居住自由指數對應的折線上的散點
scatter1 = gpd.GeoDataFrame({
    'geometry': [Point(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]),
                                                     data['居住自由指數_對映值'])]}, crs='EPSG:4326')

# 購房自由指數對應的折線
line2 = gpd.GeoDataFrame({
    'geometry': [LineString([(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]),
                                                            data['購房自由指數_對映值'])])]}, 
    crs='EPSG:4326')

# 購房自由指數對應的折線上的散點
scatter2 = gpd.GeoDataFrame({
    'geometry': [Point(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]),
                                                     data['購房自由指數_對映值'])]}, crs='EPSG:4326')


# 租房自由指數對應的折線
line3 = gpd.GeoDataFrame({
    'geometry': [LineString([(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]),
                                                            data['租房自由指數_對映值'])])]}, 
    crs='EPSG:4326')

# 租房自由指數對應的折線上的散點
scatter3 = gpd.GeoDataFrame({
    'geometry': [Point(lng, lat) for lng, lat in zip(np.arange(10, 220, 210 / data.shape[0]),
                                                     data['租房自由指數_對映值'])]}, crs='EPSG:4326')

                             
fig, ax = plt.subplots(figsize=(8, 8))

# 繪製經度線與緯度線
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'); # 關閉座標軸

fig.savefig('圖11.png', dpi=500, inches_bbox='tight', inches_pad=0)
圖9

  哈哈,是不是更加有內味了~,至此,我們的繪製指標折線部分已完成。

2.2.3 繪製填充區域

  在相繼解決完座標系統指標折線繪製之後,就到了最好玩的部分了,接下來我們來繪製圖中購房自由指數租房自由指數之間的折線,並且要按照填充較大值對應色彩的原則來處理,接下來我們需要用到一點簡單的拓撲學知識,首先我們分別構造購房自由指數_對映值租房自由指數_對映值引入南極點後所圍成的多邊形:

圖10
圖11

  接下來我們先暫停下來思考思考,購房自由指數_對映值租房自由指數_對映值之間彼此高低起伏交錯而形成的填充區域對應著上面兩個多邊形之間的什麼關係?沒錯!就是就是兩者去除掉彼此重疊區域後各自剩餘的部分!

圖12

  那麼接下來我們要做的事就so easy了,只需要分別得到兩者去除重疊面後,剩餘的部分,以對應的填充色彩疊加繪製在圖11的影象上就可以啦~,利用geopandas中的difference即可輕鬆實現:

fig, ax = plt.subplots(figsize=(8, 8))

# 繪製經度線與緯度線
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'); # 關閉座標軸

fig.savefig('圖13.png', dpi=500, inches_bbox='tight', inches_pad=0)
圖13

2.2.4 補充文字、標註等元素

  其實到這裡,我們就已經完成了對原作品復刻的精髓部分了,剩下的無非是新增些文字、刻度之類的,其實這部分很多都可以在出圖之後利用其他軟體PS完成,比寫程式碼輕鬆,所以這部分只對新增城市+指標的文字標籤以及刻度值進行補充:

圖14

  再模仿原作品裁切一下圖片,主要元素是不是非常一致了~,大家也可以根據自己的喜好來修改不同的顏色:

圖15

  以上就是本文的全部內容,今後會不定期更新這個系列,為大家展示更多資料視覺化的奇妙知識,也歡迎小夥伴們私信投稿想要模仿出的視覺化作品~