---
# System prepended metadata

title: Matplotlib 繪圖技巧：繪製選擇題用的函數圖形選項
tags: [Python]

---

<style>
.markdown-body table{
    display: unset;
}
</style>

# Matplotlib 繪圖技巧：繪製選擇題用的函數圖形選項

> 作者：王一哲
> 日期：2019/11/9

## 前言

有時候選擇題會以五張不同的函數圖形作為選項，之前我是用 [LibreOffice Draw](https://zh-tw.libreoffice.org/discover/draw/) 的繪圖工具畫好圖形之後再加上選項標籤，最後將五張函數圖形以及選項標籤匯出成一張圖片。下圖是我用 LibreOffice Draw 繪製的選項，題目是要選出自由落下的小球觸地時發生彈性碰撞的*v-t*圖，但是選項中的曲線畢竟是手動繪製的，只能算是示意圖。如果對於圖形的精準度要求不高，這樣的作法應該可以符合需求，但如果需要畫出精準的函數圖形，LibreOffice Draw 就不太適合了。

<img height="100%" width="100%" src="https://imgur.com/qOAEM3D.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<div style="text-align:center">使用 LibreOffice Draw 繪製的函數圖形選項</div>
<br />

於是我試著改用 NumPy 計算函數值，再用 Matplotlib 中的 subplots 將圖形及標籤一口氣畫好，以下是實際出題的例子。
<br />

## 使用 NumPy + Matplotlib 繪製都卜勒效應題目的選項

### 試題

假設聲源發出的聲波頻率為 1000 Hz，空氣中的聲速為 340 m/s，下表中是兩種不同狀況下觀察者觀測到的聲波頻率，單位為 Hz。狀況 I：聲源移動速度為 *v* 接近靜止的觀察者；狀況 II：觀察者移動速度為 *v* 接近靜止的聲源。下列的 5 張圖形中，請問何者最符合表格中數據？

<div style="text-align:center">

| *v* (m/s) | 0 | 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 |
| ------- | - | - | -- | -- | -- | -- | -- | -- | -- |
| 狀況 I 測到的聲波頻率 (Hz) | 1000.0 | 1014.9 | 1030.3 | 1046.2 | 1062.5 | 1079.4 | 1096.8 | 1114.8 | 1133.3 |
| 狀況 II 測到的聲波頻率 (Hz) | 1000.0 | 1014.7 | 1029.4 | 1044.1 | 1058.8 | 1073.5 | 1088.2 | 1102.9 | 1117.6 |

</div>
<br />

<img height="100%" width="100%" src="https://imgur.com/h74d6QI.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<br />

### 圖形對應的函數

假設波源移動時觀測到的聲波頻率為$f_s$，觀察者移動時觀測到的聲波頻率為$f_o$，空氣中的聲速為$v_0$，波源發出的聲波頻率為$f_0$，五張圖形對應的函數如下：
$$f_{s,1} = \frac{v_0}{v_0 - v} f_0 ~~~~~~~~~~ f_{o,1} = \frac{v_0 + v}{v_0} f_0$$
$$f_{s,2} = \frac{v_0 + v}{v_0} f_0 ~~~~~~~~~~ f_{o,2} = \frac{v_0}{v_0 - v} f_0$$
$$f_{s,3} = \frac{v_0 + v}{v_0 - v} f_0 ~~~~~~~~~~ f_{o,3} = \frac{v_0 + v}{v_0} f_0$$
$$f_{s,4} = \frac{v_0}{v_0 + v} f_0 ~~~~~~~~~~ f_{o,4} = \frac{v_0 - v}{v_0} f_0$$
$$f_{s,5} = \left( \frac{v_0}{v_0 - v} \right)^2 f_0 ~~~~~~~~~~ f_{o,5} = \left( \frac{v_0 + v}{v_0} \right)^2 f_0$$

<br />

### 程式碼

```python=
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

plt.rc('font', **{'family' : 'sans-serif'})
plt.rc('legend', fontsize=16)

f0, v0 = 1000, 340   # 原來的聲波頻率, 空氣中的聲速
# 產生陣列 v, fs, fo 共 5 組
vmin, vmax, num = 0, 40, 100
v = np.linspace(vmin, vmax, num)
fs1 = v0 / (v0 - v) * f0   # 正確選項
fo1 = (v0 + v) / v0 * f0
fs2 = (v0 + v) / v0 * f0   # 錯誤選項
fo2 = v0 / (v0 - v) * f0
fs3 = (v0 + v) / (v0 - v)* f0   # 錯誤選項
fo3 = (v0 + v) / v0 * f0
fs4 = v0 / (v0 + v) * f0   # 錯誤選項
fo4 = (v0 - v) / v0 * f0
fs5 = (v0 / (v0 - v))**2 * f0   # 錯誤選項
fo5 = ((v0 + v) / v0)**2 * f0

# 用 plt.subplots
fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(2, 3, figsize=(12, 8), dpi=72)
fig.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, wspace=0.4, hspace=0.3)

# 繪圖並設定線條顏色、寬度、圖例
line1_1, = ax1.plot(v, fs1, color='black', linestyle='-', linewidth=3, label='I')
line1_2, = ax1.plot(v, fo1, color='black', linestyle='--', linewidth=3, label='II')
ax1.legend(handles = [line1_1, line1_2], loc='upper left')
ax1.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax1.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax1.tick_params(axis='both', labelsize=14)
ax1.set_title('(A)', loc='left', fontsize=20)

line2_1, = ax2.plot(v, fs2, color='black', linestyle='-', linewidth=3, label='I')
line2_2, = ax2.plot(v, fo2, color='black', linestyle='--', linewidth=3, label='II')
ax2.legend(handles = [line2_1, line2_2], loc='upper left')
ax2.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax2.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax2.tick_params(axis='both', labelsize=14)
ax2.set_title('(B)', loc='left', fontsize=20)

line3_1, = ax3.plot(v, fs3, color='black', linestyle='-', linewidth=3, label='I')
line3_2, = ax3.plot(v, fo3, color='black', linestyle='--', linewidth=3, label='II')
ax3.legend(handles = [line3_1, line3_2], loc='upper left')
ax3.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax3.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax3.tick_params(axis='both', labelsize=14)
ax3.set_title('(C)', loc='left', fontsize=20)

line4_1, = ax4.plot(v, fs4, color='black', linestyle='-', linewidth=3, label='I')
line4_2, = ax4.plot(v, fo4, color='black', linestyle='--', linewidth=3, label='II')
ax4.legend(handles = [line4_1, line4_2], loc='upper right')
ax4.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax4.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax4.tick_params(axis='both', labelsize=14)
ax4.set_title('(D)', loc='left', fontsize=20)

line5_1, = ax5.plot(v, fs5, color='black', linestyle='-', linewidth=3, label='I')
line5_2, = ax5.plot(v, fo5, color='black', linestyle='--', linewidth=3, label='II')
ax5.legend(handles = [line5_1, line5_2], loc='upper left')
ax5.set_xlabel(r'$v~\mathrm{(m/s)}$', fontsize=16)
ax5.set_ylabel(r'$f~\mathrm{(Hz)}$', fontsize=16)
ax5.tick_params(axis='both', labelsize=14)
ax5.set_title('(E)', loc='left', fontsize=20)

ax6.axis('off')   # 隱藏右下角小圖

fig.savefig('DopplerEffectPlot.svg')
fig.savefig('DopplerEffectPlot.png')
fig.show()
```
<br />

整個程式可以分為兩大部分：

1. 第 8 - 21 行：定義圖形對應的函數並產生資料。
2. 第 24 - 72 行：繪製圖形並儲存成圖檔，雖然看起來行數很多，但其實只是同樣的事做了5次，由於我是用 numpy.subplots 指令產生 2 列、3 欄共 6 張小圖，但是只需要 5 張圖作為選項，因此最後用 ax6.axis('off') 隱藏右下角的小圖。

<br />

## 結語

雖然這樣的作法看起來很麻煩，但是以後如果要出其它的題目，只要修改函數以及 x 軸範圍再執行一次即可，畫出來的函數圖形絕對正確，而且圖形的風格能夠保持一致，以後出題會更方便。

<br />

## 參考資料

StackOverFlow https://stackoverflow.com/questions/10035446/how-can-i-make-a-blank-subplot-in-matplotlib

---

###### tags:`Python`