在Python中實作自訂函數擬合數據
發表於: 2025-06-07

什麼是擬合 (fitting)?

當我們執行完一組實驗後,會將兩個物理量分別設成 x 座標和 y 座標,然後將數據點畫成散佈圖 (scatter diagram)。然而,只有數據的圖還不夠完整,還需要確認數據和理論解釋是否吻合,而比較的方式之一,就是以理論函數擬合 (fitting) 數據。

四步驟撰寫擬合程式

如果過去沒有寫程式的經驗,或是打算接觸新語言時,建議先用一組已知趨勢的函數當作數據(如: $\sin(x)$ 或 $\cos(x)$ 函數)來練習擬合,等到熟悉整組程式的運作和調整方式,再來擬合實驗數據。

撰寫擬合程式碼,通常會直接呼叫已經建好的函式或套件包 (package),而不同程式語言只差在語法不同,但邏輯是共通的。

以下列出我所知的程式語言中,各個擬合步驟常使用的函式名稱,有需要的話可以進一步閱讀各函式的官方文件來深入理解。

Python Mathematica
載入實驗數據檔 (e.g. txt, csv, xlsx) pd.read Import
定義擬合函數 def func() Function
程式執行擬合 curve_fit Findfit
畫擬合結果 plt.plot Plot

程式中的行、列和陣列

在 Excel 介面中,可以很清楚的看到一橫列 (row) 和一直行 (column) 的樣子,但在寫程式碼的時候,尤其是 Mathematica,會需要自行想像變數內儲存的資料是列還是行,我曾經因為這樣收穫不少次執行失敗。

動手前的工具選擇

我會用免費和需不需要碰程式碼來大致分類,因為大多數學校提供微軟 Office 的授權,所以我直接分到免費類,有些單位會提供付費軟體的授權,以下表格除了Origin和Excel提供直覺的表格操作以外,其他會需要跟文字為主的程式碼介面互動,所以如果需求不複雜,建議可以從Excel開始嘗試,Origin則需要看所屬單位有無自行購買而且我沒用過,所以這裡補個問號

免費 付費
要碰程式碼 Python Matlab, Mathematica
不需程式碼 Excel Origin (?)

所以這篇文章以 Python 展示擬合數據的原因,除了因為常用好上手,重點是完全免費 (大概)!

Python 的語法相對易學易懂,背後有龐大社群在提供各式套件,現在常應用於機器學習或資料科學領域。不負責任觀察到這個語言有導入到各校課程規劃的趨勢。

撰寫擬合程式時,通常會運用 Python 的幾個知名套件,包含處理數據計算的 numpy、科學計算見長的 scipy、畫圖用的 matplotlib 和讀數據檔的 pandas。我是下載 Anaconda 這個組合包來一次取得這些套件,當然這些套件能做到的事情不僅擬合而已,若想繼續探索,可以多看其他人撰寫的程式,或是官方提供的文件。

Python的註解用#來標示,而接下來的程式碼是在 Spyder 這個 IDE 中撰寫,#%%寫法是 Spyder 提供的 cell 功能,讓程式碼可以像在 Jupyter notebook 一樣可以分區執行。另外,寫 Python 時務必注意縮排問題。

練習:$\sin (x)$ 擬合

在這個部分,我們可以完成第一個擬合程式,用 $y=\rm{sin}(0.8x)$ 的函數數值當成待擬合數據,$y=\rm{sin}(kx)$則當成擬合函數,我們會從擬合結果得知未知數 k 的數值,最後將數據和擬合函數值畫在同一張圖上比較。

1. 引入套件與函式

1
2
3
4
5
6
7
#%% 引入套件
# 為了使用數學函數
import numpy as np
# 用plt將matplotlib引入
import matplotlib.pyplot as plt
# 提供fitting函式
from scipy.optimize import curve_fit

2. 設定練習用數據

1
2
3
4
5
6
7
8
9
10
11
12
#%% 設定數據&畫圖觀察趨勢
'''python
np.linspace()函式用途
- 用來從範圍從-5和5之間取100個點,間隔由程式決定
- 這個函式會輸出一連串的數字,
之後將這串數字儲存到x這個變數(看成資料的容器)裡
'''

# 把x的值代入sin函數,所以x代表一連串的數字
x = np.linspace(-5, 5, num=100)
# y這個變數則儲存的sin(x)函數值,和x中的數字一一對應
y = np.sin(0.8*x)
1
2
3
4
5
6
# ============畫散布圖==============
plt.plot(x, y) #把y對x畫圖
plt.xlabel('x') #設定x座標軸名稱為'x'
plt.ylabel('y') #設定y座標軸名稱為'y'
# plt.savefig('test.png')
# 如果要存圖檔的話,把第20行的#拿掉

3. 定義用來擬合的數學函數

1
2
3
4
5
6
7
8
9
#%% curve fitting
'''define fitting function
- def 函式名稱(自變數1, 自變數2,...):是python定義函式的方式
- 將np.sin(a*x)這個擬合函數,儲存成func(x,a)這個函式
- x和a是"殭屍變數",代表等待輸入函式的值,換個名字其實沒關係
- a是需要用擬合決定的參數(目標)
'''
def func(x, a):
return np.sin(a*x)

4. 執行擬合

1
2
3
4
5
6
7
8
9
10
# ============conduct fitting=============
'''
curve_fit 這個函式

* 要求程式依照擬合函數,決定出最符合數據趨勢的理論線
* 3 個輸入: 依序為擬合函數 func、數據 x 座標、數據 y 座標
* 2 個輸出結果: ppot 和 pcov,ppot 會用到,代表擬合函數中的參數
* 若參數不只一個,ppot 裝的值依擬合函數中的參數順序對應
'''
ppot, pcov = curve_fit(func, x, y)

5. 繪製數據和擬合結果圖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#%% 讓座標軸刻度全部朝圖表中心,個人偏好
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['ytick.direction'] = 'in'

#%% 比較數據和擬合結果,疊圖
# 先畫數據
# 樣式為圓點,顏色由程式指定,label為圖例顯示出的名稱
plt.plot(x, y, 'o', label = 'data')
'''畫擬合結果有兩個步驟
1. 把擬合出的參數ppot依序代進原函數func,並求出函數值
對應程式碼 func(x, *ppot)
2. 畫圖, 'r-'代表畫出紅色實線
'''
# tuple 是 python 的一種資料格式

# label 中的 %5.1f 是 python 的 f-string,讓數值取到小數點第一位
plt.plot(x, func(x, *ppot), 'r-', label = 'fit: a = %5.1f' % tuple(ppot))
plt.xlabel('x') # x座標軸名稱
plt.ylabel('y') # y座標軸名稱
plt.legend() #顯示圖例
plt.xlim((-6, 7)) #指定 x 座標軸範圍為 [-6, 7)
plt.tick_params(right = True, top = True) #圖表加外框

執行程式碼後可以得到下面這張圖,藍色圓點為原始數據,紅色實線為擬合結果。圖例中印出參數 a 經擬合後的值為 0.8,和設定數據時一致;從圖表上看來也相當符合,擬合得不錯。

進階:與 Exp 函數擬合

在此會練習到載入數據檔的語法,以及多個參數的擬合,會用到自然指數函數 $y=a \times e^{-bx}+c$ 來擬合,a、b 和 c 由擬合決定。

這裡的原始數據檔是我在 Excel 中製作,並儲存成 csv 格式,x 介於 1 到 20,y 為 x 代入 $y=e^{-2x}$ 的值,用 python 的 pandas 套件讀入檔案後,會將檔案內的數據存成 dataframe 格式,每個 column 的名稱就會直接沿用 (1,1) 和 (1,2) 的設定。

  • 原始數據檔一角

  • python 讀檔結果,可以發現 x 和 y 直接當成 column 名稱


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
#%% 引入套件
import numpy as np #為了使用數學函數
import matplotlib.pyplot as plt #用plt將matplotlib引入
from scipy.optimize import curve_fit #提供fitting函式
import pandas as pd #為了讀.csv檔

#%% 讓座標軸刻度全部朝圖表中心,個人偏好
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['ytick.direction'] = 'in'

#%%read csv files & plot scatter diagram
# ======讀檔=======
df = pd.read_csv(r'example.csv') #讀取.csv檔,將結果存在df變數中
x = df['x'] #告訴程式,把df裡面叫做x的column儲存到變數x中
y = df['y'] #告訴程式,把df裡面叫做y的column儲存到變數y中
# ======畫散布圖,觀察數據趨勢=======
plt.plot(x, y, 'o',label = 'data')
plt.xlabel('x') #設定x座標軸名稱為x
plt.ylabel('y') #設定y座標軸名稱為y
plt.tick_params(right = True, top = True #加外框

#%%fitting
# ========設定擬合函數=======
# 共有 3 個參數等待擬合來決定,依序為a,b,c
def func(x, a, b, c):
return a*(np.exp(-x*b)) + c
#======curve fitting========
# bounds 用來規定 a,b,c 的下限和上限

# 對擬合來說,初始條件和範圍會對結果有影響
ppot, pcov = curve_fit(func, x, y, bounds=([-5, -5, -5], [5, 5, 5]))
xf = np.linspace(1, 20, num=100) #用來代入擬合函數的x
# 可以觀察 ppot 儲存的值,會發現總共有 3 個值,index 由小到大的數值,個別對應到 a,b,c

#%%plot the fitting
# ======畫原始數據======
plt.plot(x, y, 'o', label = 'data')
# ======畫擬合結果======
# ppot有三個需要代入擬合函數func的參數,用*ppot來完成代入
# 這裡f-string比較複雜,是為了個別指定a,b,c的小數點位置
plt.plot(xf, func(xf, *ppot), 'r-', label = 'fit: %.0f*exp(-%.1f*x)+ %.2f' %tuple (ppot))
plt.xlabel('x', fontweight = 'bold') #x座標軸名稱設定為x,格式為粗體
plt.ylabel('y',fontweight = 'bold') #x座標軸名稱設定為x,格式為粗體
plt.legend() #圖例
plt.title('Fitting result', fontweight = 'bold', fontsize = 16) #圖表標題,格式為粗體(bold),字體大小為16
plt.tick_params(right = True, top = True) #加外框

:::


  • ppot中的參數擬合結果
  • 程式畫出的數據和理論比較圖,得到a=1,b=2和c趨近於0的結果,符合原先數據設定。