(在模仿中精進資料視覺化04)舊金山街道樹木分佈視覺化

費弗裡發表於2020-11-22

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

1 簡介

  開門見山,今天我們要模仿的資料視覺化作品來自#TidyTuesday活動於2020年1月28日釋出的舊金山街道樹木資料集下的眾多參賽作品中,由Philippe Massicotte創作的(如圖1所示)非常受歡迎的Street trees of San Francisco

(在模仿中精進資料視覺化04)舊金山街道樹木分佈視覺化
圖1

  原作者使用的工具是R語言,而今天的文章內容,我就將帶大家學習如何在Python中模仿圖1的風格進行類似資料資訊的視覺化展示(其實原作品有一些令人困惑的瑕疵,因此我在下文中在一些地方採用了與原作者不同的分析方式,因此最終的成品與原作品有一些不同之處)。

2 模仿過程

  今天我們要模仿的這張圖,咋一看上去似乎略複雜,但如果你曾經閱讀過我的基於geopandas的空間資料分析系列文章,就一下子可以在腦中將此圖構成進行分解:

2.1 過程分解

  我們仔細觀察原作品,可以get到其主要視覺元素是將統計出的數值對映到每個社群面色彩之上,且外圍的輪廓描邊,很明顯是整個地區對應整體的向外緩衝區,再輔以道路網,使得整張圖看起來顯得很“精密”。

  結合我們手頭的資料:舊金山社群資料、有登記的街道樹木資料,至於道路網資料我們則可以利用第三方庫osmnx進行獲取(建議利用conda install -c conda-forge osmnx進行安裝)。

  將過程拆分為下列步驟:

  • 資料準備

  首先我們需要讀入已有的資料並進行相應的向量化:

(在模仿中精進資料視覺化04)舊金山街道樹木分佈視覺化
圖2

  而路網資料我們則可以利用osmnx進行線上獲取,只需傳入我們的舊金山面資料bbox範圍,配合

osmnx進行獲取即可:

(在模仿中精進資料視覺化04)舊金山街道樹木分佈視覺化
圖3

  接著我們在上述資料基礎上對每個社群面內部的街道樹木數量進行統計並對資料進行分箱,配上預設區間的色彩值:

# 統計每個社群內部的樹木數量
sf_trees = \
(
    gpd
    # 空間連線
    .sjoin(left_df=sf,
           right_df=trees,
           op='contains',
           how='left')
    # 按照name分組計數(這裡未連線到任何數的社群被
    # 記為1本質上是錯誤的,但我們繪圖分段後這一點不影響)
    .groupby('name')
    .agg({
        'name': 'count',
        'geometry': 'first'
    })
    .rename(columns={'name': '數量'})
    .reset_index(drop=False)
    # 直接轉為GeoDataFrame
    .pipe(gpd.GeoDataFrame, crs='EPSG:4326')
)

sf_trees['顏色'] = (
    pd
    .cut(sf_trees['數量'], 
         bins=[0, 2500, 5000, 7500, 10000, max(sf_trees['數量'])], 
         labels=['#e4f1e1', '#c0dfd1', '#67a9a2', '#3b8383', '#145e64'])
)

  最後別忘記了我們作為輪廓的緩衝區生成:

# 生成輪廓緩衝區
sf_bounds = gpd.GeoSeries([sf.buffer(0.001).unary_union], crs='EPSG:4326')
  • 主要視覺元素繪製  

  做好這些準備後我們直接就可以先將影像的主體元素繪製出來:

import matplotlib.pyplot as plt
from matplotlib import font_manager as fm

# 設定全域性預設字型
plt.rcParams['font.sans-serif'] = ['Times New Roman']

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

# 設定背景色
ax.set_facecolor('#333333')
fig.set_facecolor('#333333')

# 圖層1:緩衝區輪廓
ax = (
    sf_bounds
    .plot(ax=ax, facecolor='none', edgecolor='#cccccc', linewidth=1)
)

# 圖層2:帶有樹木統計資訊的社群面
ax = (
    sf_trees
    .plot(color=sf_trees['顏色'], edgecolor='#333333',
          linewidth=0.5, ax=ax)
)

# 圖層3:osm路網
ax = (
    roads
    .plot(linewidth=0.05, edgecolor='#3c3d3d',
          ax=ax)
)

# 設定x軸
ax.set_xticks([-122.5, -122.45, -122.4, -122.35])
ax.set_xticklabels(['122.5°W', '122.45°W', '122.4°W', '122.35°W'])

# 設定y軸
ax.set_yticks([37.72, 37.74, 37.76, 37.78, 37.8, 37.82])
ax.set_yticklabels(['37.72°N', '37.74°N', '37.76°N', '37.78°N', '37.8°N', '37.82°N'])

# 設定座標軸樣式
ax.tick_params(axis='both', labelcolor='#737373', color='none', labelsize=8)

# 隱藏周圍的spines線條
ax.spines['left'].set_color('none')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')

# 匯出影像
fig.savefig('圖4.png', dpi=600, bbox_inches='tight')
(在模仿中精進資料視覺化04)舊金山街道樹木分佈視覺化
圖4
  • 輔助視覺元素的新增

  接下來我們只需要補充上各種點睛之筆的小元素即可,其中值得一提的是下方的圖例我們用inset_axes()插入子圖的方式靈活實現。

  並且外部字型檔案的使用也是很添彩的,我們這裡就分別在標題刻度標籤處使用到了兩種特殊的字型(你可以在開頭的Github倉庫找到我用到的所有字型檔案):

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

# 設定背景色
ax.set_facecolor('#333333')
fig.set_facecolor('#333333')

# 圖層1:緩衝區輪廓
ax = (
    sf_bounds
    .plot(ax=ax, facecolor='none', edgecolor='#cccccc', linewidth=1)
)

# 圖層2:帶有樹木統計資訊的社群面
ax = (
    sf_trees
    .plot(color=sf_trees['顏色'], edgecolor='#333333',
          linewidth=0.5, ax=ax)
)

# 圖層3:osm路網
ax = (
    roads
    .plot(linewidth=0.05, edgecolor='#3c3d3d',
          ax=ax)
)

# 設定x軸
ax.set_xticks([-122.5, -122.45, -122.4, -122.35])
ax.set_xticklabels(['122.5°W', '122.45°W', '122.4°W', '122.35°W'])

# 設定y軸
ax.set_yticks([37.72, 37.74, 37.76, 37.78, 37.8, 37.82])
ax.set_yticklabels(['37.72°N', '37.74°N', '37.76°N', '37.78°N', '37.8°N', '37.82°N'])

# 設定座標軸樣式
ax.tick_params(axis='both', labelcolor='#737373', color='none', labelsize=8)

# 隱藏周圍的spines線條
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_bar = ax.inset_axes((0.25, -0.12, 0.5, 0.015))
ax_bar.set_facecolor('#333333')
ax_bar.spines['left'].set_color('none')
ax_bar.spines['right'].set_color('none')
ax_bar.spines['top'].set_color('none')
ax_bar.spines['bottom'].set_color('none')

ax_bar.bar(range(5), [1]*5, width=0.975, color=['#e4f1e1', '#c0dfd1', '#67a9a2', '#3b8383', '#145e64'])
ax_bar.set_yticks([])
ax_bar.set_xticks([i+0.5 for i in range(4)])
ax_bar.set_xticklabels(['2500', '5000', '7500', '10000'], 
                       fontdict={'fontproperties': fm.FontProperties(fname="RobotoCondensed-Regular.ttf")})
ax_bar.tick_params(color='none', labelcolor='#ffffff', labelsize=8, pad=0)

ax.set_title('Street trees of San Francisco', 
             fontsize=24,
             color='#ffffff',
             pad=40,
             fontproperties=fm.FontProperties(fname="Amaranth-Bold.ttf"))

ax.text(0.5, 1.08, '''There are a total of 192987 trees in San Francisco regrouped into 571 species.
The district with the most number of trees is Mission whereas the one with
the least number of trees is LincoLn Park / Ft. Miley.''', transform=ax.transAxes, ma='center',
        ha='center', va='top', color='#ffffff')

ax.text(0.5, -0.22, 'Visualization by CNFeffery', fontsize=8,
        color='#737373', ha='center', transform=ax.transAxes)

# 匯出影像
fig.savefig('圖5.png', dpi=600, bbox_inches='tight')
(在模仿中精進資料視覺化04)舊金山街道樹木分佈視覺化
圖5

  以上就是本文的全部內容,歡迎在評論區與我進行討論~

相關文章