Julia語言程式設計——實例:體型分佈
https://blog.csdn.net/conkty/article/details/81490456
使用Julia實現一個比Hello World更複雜點的例子:有1000個人的體型樣本,包括體重與身高兩項指標,不考慮性別和年齡因素,計算每個人的BMI(Body Mass Index)指數,並根據關於肥胖的參考標準(見下表),統計各種體型分類的人數。為了程式設計的方便,在表中先對BMI分類進行了編號,對應1~6類。另外,在實現時,初始的樣本資料可採用亂數的方式生成。
表 1-1 體型BMI指數標準(資料來自網路)
BMI分類編號
|
BMI 分類
|
參考標準
|
相關疾病發病的危險性
|
1
|
體重過低
|
BMI < 18.5
|
低(但其它疾病危險性增加)
|
2
|
正常範圍
|
18.5 ≤ BMI < 24
|
平均水準
|
3
|
肥胖前期
|
24 ≤ BMI < 28
|
輕度增加
|
4
|
I度肥胖
|
28 ≤ BMI < 30
|
中度增加
|
5
|
II度肥胖
|
30 ≤ BMI < 40
|
嚴重增加
|
6
|
Ⅲ度肥胖
|
BMI ≥ 40.0
|
非常嚴重增加
|
下面,我們採用兩種不同的方式實現上述的需求:一種是基於陣列的方式,另外一種基於複合結構的方式,具體見下文。由於涉及的代碼會比較多,所以本例不再在REPL中演示,而在腳本中實現,詳細源碼可在https://gitee.com/juliaprog/bookexamples.git中下載。
首先在磁片中新建一個名稱為bmi_array.jl的檔,並使用常用的文字編輯器(例如VS Code、Notepad++、Sublime、UltraEditor、Atom等)打開進行編輯。
第一步,便是使用隨機函數rand()生成原始資料,包括1000個人的身高及體重樣本。實現的代碼如下:
1. # 使用均勻分佈亂數產生1000個 身高 樣本,取值範圍為 [0,1)
2.
3. heights = rand(Float64, 1000)
及
1. # 使用均勻分佈亂數產生1000個 體重 樣本,取值範圍為 [0,1)
2.
3. weights = rand(Float64, 1000)
其中的heights與weights均是元素類型為Float64的陣列,長度為1000,內容類別似於:
1. 1000-element Array{Float64,1}:
2.
3. 0.327989
4.
5. 0.371755
6.
7. 0.640065
8.
9. 0.891165
10.
11. 0.735425
12.
13. 0.428819
14.
15. ... # 已省略
需要注意的是,隨機函數rand()的取值區間為[0,1),所以需要轉換到正常人的身高與體重範圍。我們採用區間映射的方式,函數為:
該函數可以將[amin, amax]中值a映射到區間[bmin,
bmax]中的b值。
利用此函數,可以分別將身高heights陣列元素值均映射到[1.5, 1.8)區間,將體重weights陣列元素值映射到[30, 100)區間,即身高分佈在1.5~1.8米範圍同時體重分佈在30~100千克,具體實現語句為:
1. # 將 身高 資料映射到 [1.5, 1.8) 米
2.
3. heights = heights .* (1.8-1.5)+1.5
4.
5.
6.
7. # 將 體重 資料映射到 [30, 100) 千克
8.
9. weights = weights .* (100-30)+30
由於涉及到標量與向量(陣列)的混合計算,所以採用了Julia特有的點操作(後文會進行深入地學習)。上述語句雖然涉及到陣列的逐元計算,但並沒有使用迴圈結構,語法極為簡潔直觀。
需要說明一點:這兩項資料呈現正態分佈是較為複合實現實情況的,不過為了轉換的方便,我們採用了均勻分佈的rand()函數,有興趣的讀者可以更換為randn()函數,來生成初始的樣本資料。
接下來,定義一個用於計算BMI指數的函數,如下:
bmi(w, h) =
w / (h^2)
這是一種Julia式的函式定義方式,適用於實現簡短的函數。其中的bmi是函數名,(w, h)中的w和h是輸入參數,分別是體重與身高值。
此後,便可基於已有的身高與體重資料計算上述1000個樣本的BMI指數了,實現如下:
indexes =
broadcast(bmi, weights, heights)
按常規,此處應該有迴圈,但是並沒有。我們利用Julia特有的broadcast()函數,實現了將函數bmi()逐一施用於陣列weights和heights元素,並能夠自動取得兩個陣列的對應元素,自動將兩對元素作為bmi()函數的輸入參數,計算對應體型樣本的BMI指數。如果一定想用一下迴圈結構,可以如下實現:
1. indexes =
Array{Float64,1}(1000)
# 創建有1000個元素的一維陣列物件
2.
3. for i in
1:1000
# 迴圈1000次
4.
5. indexes[i] = bmi(weights[i],
heights[i]) # 成對取得體重與身高,計算第i個樣本的BMI指數
6.
7. end
可見,這樣的方式中代碼量多出了很多,遠沒有上述的一句話實現簡潔。
更令人驚奇的是,對於BMI指數這種計算簡單的過程,完全可以不用預先定義bmi()函數,而是採用Julia特有的點操作符直接實現:
indexes =
weights ./ (heights.^2)
這種同樣能夠將運算過程自動施用於weights和heights的元素中,而且仍不需要迴圈結構,表達即為高效、簡潔。在本書之後的內容中,我們便能夠深入瞭解這種逐元電腦制,即所謂的向量化計算方法。
在BMI指數計算完成後,我們定義一個名為bmi_category的函數,用於對得到的指數進行分類,代碼如下:
1. # 對BMI指數進行分類
2.
3. # 1-體重過低,2-正常範圍,3-肥胖前期,4-I度肥胖,5-II度肥胖,6-III度肥胖
4.
5. function bmi_category(index::Float64)
6.
7. class = 0
8.
9. if index < 18.5
10.
11. class = 1
12.
13. elseif index < 24
14.
15. class = 2
16.
17. elseif index < 28
18.
19. class = 3
20.
21. elseif index < 30
22.
23. class = 4
24.
25. elseif index < 40
26.
27. class = 5
28.
29. else
30.
31. class = 6
32.
33. end
34.
35.
36.
37.
class # 返回分類編號
38.
39. end
由於該函數語句較多,所以該函數並沒有採用bmi()函式定義時那種“直接賦值”的方式,而是採用了帶有function關鍵字的常規函式定義方式,並利用if~elseif判斷結構實現了對輸入BMI指數值index進行逐層判斷。
在得到最終的分類編號class之後,需要將其返回。在Julia中,只需在函數結束的最後語句中直接列出該變數即可,顯式的return關鍵字不是必須的。
然後,便可通過該函數對indexes中的1000個BMI指數進行分類了,實現語句為:
classes =
bmi_category.(indexes) #注意函數名之後一個小點號
同樣採用點操作實現了陣列的逐元計算。可見,Julia的點操作不僅適用於上述的運算子,也同樣適用于普通定義的函數。該語句執行後,會得到類似如下的結果:
1. 1000-element Array{Int64,1}:
2.
3. 2
4.
5. 5
6.
7. 3
8.
9. 1
10.
11. 2
12.
13. 1
14.
15. 5
16.
17. ... # 其他已省略
最後,對classes中的類別編號進行統計:
1. # 統計每個類別的數量
2.
3. for c in [1 2 3
4 5 6] #
遍歷6個類別,c為類別ID
4.
5. n = count(x->(x==c), classes) #
x->(x==c)為匿名函數
6.
7. println("category ",
c, " ", n) #
列印結果
8.
9. end
實現中使用了for迴圈結構對類別編號集合進行遍歷,逐一對各類型進行統計。其中的count()函數是Julia內置的,能夠對陣列中滿足條件的元素進行計數,而條件由該函數的第一個參數提供。條件參數需是一個函數物件,有一個輸入參數,並需返回布林型值。在上述代碼中,這個條件函數為x->(x==c),是Julia形式的匿名函數,等效於:
condition(x)
= (x==c)
不過,因為簡短,又需作為另外一個函數的參數,所以匿名函數的定義方式是非常合適的。
至此,需求需要實現的功能全部完成了。我們打開的REPL,執行以下語句:
1. julia> include("/path/to/bmi_array.jl") # 檔路徑根據實際情況提供
2.
3. 便可獲得最終的結果,顯式的內容類別似於:
4.
5. category 1 291 # 體重過低共計291人
6.
7. category 2 221
8.
9. category 3 151
10.
11. category 4 77
12.
13. category 5 238
14.
15. category 6 22 # Ⅲ度肥胖共計22人
這裡,我們總結一下:在整個實現中,資料流程主要以陣列結構表達,並在對陣列的逐元操作中,利用Julia的點操作及braodcast()函數兩種方式進行向量化計算,避免了大量的迴圈結構,代碼的實現極為簡潔、高效、直觀。
下面,我們再嘗試另外一種實現方式。同樣地,在磁片中新建一個名稱為bmi_struct.jl的指令檔,並使用文字編輯器進行編輯。
首先,定義個複合結構類型,包括四個成員欄位,分別表示某個人的身高、體重、BMI指數及BMI分類編號。
1. mutable struct Person
2.
3. height # 身高,單位米
4.
5. weight # 體重,單位千克
6.
7. bmi # 計算得到的BMI指數
8.
9. class # 根據BMI指數計算得到的分類標識
10.
11.
# 1-體重過低,2-正常範圍,3-肥胖前期,4-I度肥胖,5-II度肥胖,6-III度肥胖
12.
13. end
再定義一個集合類型,用於容納樣本資料,如下:
people =
Set{Person}()
隨之使用均勻分佈生成1000個體型資料,並放入集合people中:
1. for i = 1:1000
2.
3. h = rand() * (1.8-1.5)+1.5 # 生成身高資料,並將其映射到[1.5, 1.8)區間
4.
5. w = rand() * (100-30)+30 # 生成體重資料,並將其映射到[30, 100)區間
6.
7. p = Person(h, w, 0, 0)
# 基於身高與體重資料創建Person物件p,
8.
9. # BMI指數和分類編號均初始化為無效的0值
10.
11.
12.
13. push!(people,
p)
# 將物件放入集合people中
14.
15. end
然後定義bmi()函數,基於每個Person物件的身高及體重資料,計算其BMI指數並同時進行分類,代碼如下:
1. function bmi(p::Person)
2.
3. p.bmi = p.weight/(p.height^2) # 計算BMI指數
4.
5. p.class =
bmi_category(p.bmi) # 分類,得到類別ID,已在前文實現過
6.
7. end
函數bmi()中原型中的::Person用於限定輸入參數p變數只能是Person類型。
最後,遍歷people中的1000個樣本,對BMI類別分別進行統計:
1. # 對1000個樣本執行BMI計算,並統計分佈
2.
3. categories =
Dict() # 字典結構,記錄各類的人數
4.
5. for p ∈
people
# 遍歷1000個樣本
6.
7.
bmi(p)
# 計算BMI指數並分類,會直接修改p中的屬性欄位
8.
9. categories[p.class] = get(categories,
p.class, 0) + 1
# 對p.class類的計數器累加
10.
11. end
實現中,對Dict類型的物件categories採用了兩種訪問方式,一種是與陣列下標極為類似,用於獲得類別對應的計數器,另一種是get()函數,也是用於獲得類別對應的計數器內容,區別在於後者能夠在categories還不存在鍵p.class時,也能夠返回有效值(預設值0)。
其中通過字典結構了不同類型的計數。
完成後,列印BMI分類統計的結果。方法極為簡單,只需直接列出變數名:
categories
至此,功能全部實現了。打開的REPL,執行以下語句:
julia>
include("/path/to/bmi_struct.jl") # 檔路徑根據實際情況提供
便可獲得最終結果,內容類別似於:
1. Dict{Any,Any} with 6 entries:
2.
3. 4 => 89
4.
5. 2 => 219
6.
7. 3 => 158
8.
9. 5 => 234
10.
11. 6 => 18 # Ⅲ度肥胖共計18人
12.
13. 1 => 282 # 體重過低共計282人
不過注意Dict中的資料是無序的,所以列印的內容中沒有按照類別ID排列。
有別於第一種實現方式,本方式採用複合類型對資料和操作進行了封裝,在業務概念或邏輯上能夠顯得更為條理清晰,而且整個實現過程也並不複雜。
沒有留言:
張貼留言