輕鬆畫出有學術質感的圖表

前言

本篇示範一個用 matplotlib 畫一個基本的折線圖,在 ubuntu 跟 windows 10 都有試過,不過本篇以 windows 10 的指令為主。

雖然有想過寫成一個工具,使用者只要提供資料,就可以自動畫出圖。不過後來發現這個想法是不實際,因為每張圖幾乎都需要客製化,所以就改成提供一個模板,要做任何調整會比較方便。

安裝套件

Python3.9

Windows 直接到官網下載即可,Ubuntu 需要使用 PPA1

示意圖
windows python3.9

windows python3.9

OuO

安裝 virtualenv 並進入虛擬環境

> pip install virtualenv

要進入虛擬環境時需要執行 active 的腳本,在 Ubuntu 就很方便執行 $ source venv/bin/activate,但是 powershell 有權限問題,所以要先設定2

> mkdir matplotlib_test
> cd matplotlib_test
> virtualenv --python python3.9 venv
> Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
> .\venv\Scripts\activate.ps1
(venv)> pip list

pip list of a clear venv

pip list of a clear venv

OuO

Matplotlib and SciencePlots

圖表樣式主要靠 SciencePlots3 來完成,所以我們只要專注使用 Matplotlib 來製作圖表即可。

(venv)> pip install matplotlib==3.4.2
(venv)> pip install SciencePlots==1.0.8
(venv)> pip list # 詳細版本資訊參考用
Package         Version
--------------- -------
cycler          0.10.0
kiwisolver      1.3.1
matplotlib      3.4.2
numpy           1.21.1
Pillow          8.3.1
pip             21.2.3
pyparsing       2.4.7
python-dateutil 2.8.2
SciencePlots    1.0.8
setuptools      57.4.0
six             1.16.0
wheel           0.37.0

畫圖技巧

強烈建議先去看一下這篇 matplotlib:先搞明白plt. /ax./ fig再画。我原先用 matplotlib 也是亂用,看過之後了解大概的區塊是哪個模組在控制。

下圖的各個部位的名稱可以大概記一下,這樣在搜尋時會比較方便。主要中心思想就是關於實際畫圖的都使用 ax,只有在建立跟輸出時才用 plt。下方會有範例。

Parts of a Figure4

Parts of a Figure4

OuO

components of a Matplotlib figure5

components of a Matplotlib figure5

OuO

實際範例

n,A,B,C,D
10,1,2,1,6
100,3,3,6,6
1000,2,9,1,6
10000,3,7,7,6
100000,3,10,8,6
1000000,16,19,2,6
10000000,125,86,41,6
  • plot.py:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import matplotlib.pyplot as plt
import numpy as np

# Config
fin = "data.csv"
fout = "result.pdf"
x_str = "x label"
y_str = "y label"
title_str = "Title"
col_names =         ['n', 'A', 'B', 'C', 'D'] # cannot contain any special char
col_label_names =   ['A (a)', 'B (b)', 'C']
skip_cols = ['D']
label_cols = ['A', 'C']
add_dot_points = [(10000, 'B'), (1000000, 'A')]
colors = ['red', 'black', 'blue', 'green']

# Read data
table = np.genfromtxt(fin, delimiter=',', skip_header=1, names=col_names)
print(table)

# Plot the figure
with plt.style.context(['science', 'ieee']):
    fig, ax = plt.subplots()

    color_idx = 0
    for i, col_name in enumerate(col_names[1:]):
        if col_name in skip_cols:
            continue
        
        # Plot line
        line = ax.plot(table[col_names[0]], table[col_name], label=col_label_names[i], color=colors[color_idx])
        color_idx += 1

        # Plot data label
        if col_name in label_cols:
            for x, y in zip(table[col_names[0]], table[col_name]):
                ax.annotate(f"{y:.1f}", xy=(x-0.0*x, y-0.2*y), textcoords='data', fontsize=4, color=line[0].get_color())
        
        # Plot special points
        for x, y in zip(table[col_names[0]], table[col_name]):
            if (x, col_name) in add_dot_points:
                ax.plot(x, y, marker=".", markersize=4, color=line[0].get_color(), linestyle=line[0].get_linestyle())


    ax.legend(fontsize=6) # according to col_label_names
    # ax.set_title(title_str) # not used by default
    # x, y label
    ax.set_xlabel(x_str, fontsize=6)
    ax.set_ylabel(y_str, fontsize=7)

    # x, y tick
    ax.set_xscale('log', base=2)
    ax.set_yscale('log')
    ax.xaxis.set_tick_params(labelsize=5)
    ax.yaxis.set_tick_params(labelsize=5)
    ax.yaxis.tick_left()

    ax.autoscale()
    plt.tight_layout()
    # plt.show()
    plt.savefig(fout, bbox_inches='tight')
  • 程式碼解釋
    • [10], [11] 會有兩個 name 主要是因為讀取時的欄位名稱不能有特殊符號,所以在繪製時才綁定顯示名稱 [31]
    • [12] 某些資料要跳過但是檔案裡還是會有
    • [13] 哪些需要標出資料標籤的數值
    • [14] 哪些資料點需要特別用圓點標記
    • [37] f"{y:.1f}" 可設定資料標籤輸出格式,例如: 取到小數點第一位。x-0.0*x, y-0.2*y 後方有減去一個位移是用來避免線段與資料標籤重疊
    • [46] 一般論文中的圖表標題是用 Latex 語法 (caption) 來定義,所以生成時不需要,不過若是要拿來做簡報的話有標題會比較好。
    • [48] ~ [56] 設定 x、y 軸資訊
    • [61] 存成 pdf
  • 成果
    折線圖

    折線圖

    OuO

[2021.08.13] 修正 linestyle

右軸 (twinx) 範例

[2024.02.26] 新增 (Windows 11, Python 3.12.2, matplotlib 3.8.3, numpy 1.26.4, SciencePlots 2.1.1, pillow 10.2.0)

A, B, C, D, E
7, 13, 5, 17, 10
50, 40, 30, 20, 10
import matplotlib.pyplot as plt
import numpy as np
import scienceplots

# File input/output
fin = "data.csv"
fout = "result.pdf"

# Read data from file
table = np.genfromtxt(fin, delimiter=",", names=True)
print(f"{table = }")
categories = list(table.dtype.names)
primary_values = list(table[0])
secondary_values = list(table[1])

# Data hardcode (optional)
# categories = ['A', 'B', 'C', 'D', 'E']
# primary_values = [7, 13, 5, 17, 10]
# secondary_values = [50, 40, 30, 20, 10]

# Plot the figure
plt.style.use(["science", "ieee"])
plt.rcParams.update(
    {"text.usetex": True, "font.family": "serif", "font.serif": ["Lin Libertine"]}
)

# Create figure and axes
fig, ax_l = plt.subplots()
# Create a second y-axis sharing the same x-axis
ax_r = ax_l.twinx()

# Create bar graph for the primary data
bars1 = ax_l.bar(categories, primary_values, label="Primary", color="C2")

# Create a line plot for the secondary data
ax_r.plot(categories, secondary_values, "C1.-", label="Secondary")

# Show legend
ax_l.legend(loc="upper left")
ax_r.legend(loc="upper right")

# Rotate the text on the X axis
ax_l.tick_params(axis='x', labelrotation=60)

# Add labels and title
plt.xlabel("Categories")
ax_l.set_ylabel("Primary values (\\%)")
ax_r.set_ylabel("Secondary values (s)")
plt.title("Graph Title")

# Adjust ylim to add a gap between the highest bar and the top rule of the figure
ax_l.set_ylim(0, max(primary_values) * 1.2)
ax_r.set_ylim(0, max(secondary_values) * 1.2)

plt.tight_layout()
# plt.show()
plt.savefig(fout, bbox_inches="tight")

print("plot finished")

twinx

twinx

OuO

其他參考

離題

用 Matplotlib 在一個 Figure 中塞入多個 Axes 是可以的,不過我目前遇到的都是用 Latex 的 minipage 來完成,這樣就可以有各自的 label 可以分開 ref。如下,不過這就有點離題了。

\begin{table}[tbh!]
  \centering
  \begin{minipage}[t]{.47\linewidth}
    \includegraphics[width=\linewidth]{figures/A.pdf}
    \captionof{figure}{AAA}
    \label{fig:A}
  \end{minipage}
  \qquad
  \begin{minipage}[t]{.47\linewidth}
    \includegraphics[width=\linewidth]{figures/B.pdf}
    \captionof{figure}{BBB}
    \label{fig:B}
  \end{minipage}
\end{table}

錯誤排除

FileNotFoundError: missing font metrics file: rsfs10

Solution 6

> miktex-maketfm.exe rsfs10

PermissionError: [Errno 13] Permission denied: 'result.pdf'

輸出覆寫 pdf 時有視窗開著該檔案,把 result.pdf 關掉即可。

  • ⊛ Back to top
  • ⊛ Go to bottom