Julia语言程序设计——实例:体型分布

           使用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个人的身高及体重样本。实现的代码如下:

# 使用均匀分布随机数生成1000个 身高 样本,取值范围为 [0,1)

heights = rand(Float64, 1000)

# 使用均匀分布随机数生成1000个 体重 样本,取值范围为 [0,1)

weights = rand(Float64, 1000)

其中的heights与weights均是元素类型为Float64的数组,长度为1000,内容类似于:

1000-element Array{Float64,1}:

 0.327989

 0.371755

 0.640065

 0.891165

 0.735425

 0.428819

 ... # 已省略

需要注意的是,随机函数rand()的取值区间为[0,1),所以需要转换到正常人的身高与体重范围。我们采用区间映射的方式,函数为:

该函数可以将[amin, amax]中值a映射到区间[bmin, bmax]中的b值。

利用此函数,可以分别将身高heights数组元素值均映射到[1.5, 1.8)区间,将体重weights数组元素值映射到[30, 100)区间,即身高分布在1.5~1.8米范围同时体重分布在30~100千克,具体实现语句为:

# 将 身高 数据映射到 [1.5, 1.8) 米

heights = heights .* (1.8-1.5)+1.5



# 将 体重 数据映射到 [30, 100) 千克

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指数。如果一定想用一下循环结构,可以如下实现:

indexes = Array{Float64,1}(1000)              # 创建有1000个元素的一维数组对象

for i in 1:1000                              # 循环1000次

    indexes[i] = bmi(weights[i], heights[i])      # 成对取得体重与身高,计算第i个样本的BMI指数

end

可见,这样的方式中代码量多出了很多,远没有上述的一句话实现简洁。

更令人惊奇的是,对于BMI指数这种计算简单的过程,完全可以不用预先定义bmi()函数,而是采用Julia特有的点操作符直接实现:

indexes = weights ./ (heights.^2)

这种同样能够将运算过程自动施用于weights和heights的元素中,而且仍不需要循环结构,表达即为高效、简洁。在本书之后的内容中,我们便能够深入了解这种逐元计算机制,即所谓的矢量化计算方法。

在BMI指数计算完成后,我们定义一个名为bmi_category的函数,用于对得到的指数进行分类,代码如下:

# 对BMI指数进行分类

# 1-体重过低,2-正常范围,3-肥胖前期,4-I度肥胖,5-II度肥胖,6-III度肥胖

function bmi_category(index::Float64)

  class = 0

  if index < 18.5

     class = 1

  elseif index < 24

     class = 2

  elseif index < 28

     class = 3

  elseif index < 30

     class = 4

  elseif index < 40

     class = 5

  else

     class = 6

  end

 

  class            # 返回分类编号

end

由于该函数语句较多,所以该函数并没有采用bmi()函数定义时那种“直接赋值”的方式,而是采用了带有function关键字的常规函数定义方式,并利用if~elseif判断结构实现了对输入BMI指数值index进行逐层判断。

在得到最终的分类编号class之后,需要将其返回。在Julia中,只需在函数结束的最后语句中直接列出该变量即可,显式的return关键字不是必须的。

然后,便可通过该函数对indexes中的1000个BMI指数进行分类了,实现语句为:

classes = bmi_category.(indexes)  #注意函数名之后一个小点号

同样采用点操作实现了数组的逐元计算。可见,Julia的点操作不仅适用于上述的运算符,也同样适用于普通定义的函数。该语句执行后,会得到类似如下的结果:

1000-element Array{Int64,1}:

 2

 5

 3

 1

 2

 1

 5

 ... # 其他已省略

最后,对classes中的类别编号进行统计:

# 统计每个类别的数量

for c in [1 2 3 4 5 6]             # 遍历6个类别,c为类别ID

  n = count(x->(x==c), classes)   # x->(x==c)为匿名函数

  println("category ", c, " ", n)    # 打印结果

end

实现中使用了for循环结构对类别编号集合进行遍历,逐一对各类型进行统计。其中的count()函数是Julia内置的,能够对数组中满足条件的元素进行计数,而条件由该函数的第一个参数提供。条件参数需是一个函数对象,有一个输入参数,并需返回布尔型值。在上述代码中,这个条件函数为x->(x==c),是Julia形式的匿名函数,等效于:

condition(x) = (x==c)

不过,因为简短,又需作为另外一个函数的参数,所以匿名函数的定义方式是非常合适的。

至此,需求需要实现的功能全部完成了。我们打开的REPL,执行以下语句:

julia> include("/path/to/bmi_array.jl")     # 文件路径根据实际情况提供

便可获得最终的结果,显式的内容类似于:

category 1 291   # 体重过低共计291人

category 2 221

category 3 151

category 4 77

category 5 238

category 6 22    # Ⅲ度肥胖共计22人

这里,我们总结一下:在整个实现中,数据流主要以数组结构表达,并在对数组的逐元操作中,利用Julia的点操作及braodcast()函数两种方式进行矢量化计算,避免了大量的循环结构,代码的实现极为简洁、高效、直观。

  • 复合类型实现方式

下面,我们再尝试另外一种实现方式。同样地,在磁盘中新建一个名称为bmi_struct.jl的脚本文件,并使用文本编辑器进行编辑。

首先,定义个复合结构类型,包括四个成员字段,分别表示某个人的身高、体重、BMI指数及BMI分类编号。

mutable struct Person

  height       # 身高,单位米

  weight      # 体重,单位千克

  bmi         # 计算得到的BMI指数

  class        # 根据BMI指数计算得到的分类标识

              # 1-体重过低,2-正常范围,3-肥胖前期,4-I度肥胖,5-II度肥胖,6-III度肥胖

end

再定义一个集合类型,用于容纳样本数据,如下:

people = Set{Person}()

随之使用均匀分布生成1000个体型数据,并放入集合people中:

for i = 1:1000

  h = rand() * (1.8-1.5)+1.5        # 生成身高数据,并将其映射到[1.5, 1.8)区间

  w = rand() * (100-30)+30        # 生成体重数据,并将其映射到[30, 100)区间

  p = Person(h, w, 0, 0)           # 基于身高与体重数据创建Person对象p,

# BMI指数和分类编号均初始化为无效的0值



  push!(people, p)               # 将对象放入集合people中

end

然后定义bmi()函数,基于每个Person对象的身高及体重数据,计算其BMI指数并同时进行分类,代码如下:

function bmi(p::Person)

    p.bmi = p.weight/(p.height^2)      # 计算BMI指数

    p.class = bmi_category(p.bmi)      # 分类,得到类别ID,已在前文实现过

end

函数bmi()中原型中的::Person用于限定输入参数p变量只能是Person类型。

最后,遍历people中的1000个样本,对BMI类别分别进行统计:

# 对1000个样本执行BMI计算,并统计分布

categories = Dict()            # 字典结构,记录各类的人数

for p ∈ people              # 遍历1000个样本

   bmi(p)                   # 计算BMI指数并分类,会直接修改p中的属性字段

   categories[p.class] = get(categories, p.class, 0) + 1    # 对p.class类的计数器累加

end

实现中,对Dict类型的对象categories采用了两种访问方式,一种是与数组下标极为类似,用于获得类别对应的计数器,另一种是get()函数,也是用于获得类别对应的计数器内容,区别在于后者能够在categories还不存在键p.class时,也能够返回有效值(默认值0)。

其中通过字典结构了不同类型的计数。

完成后,打印BMI分类统计的结果。方法极为简单,只需直接列出变量名:

categories

至此,功能全部实现了。打开的REPL,执行以下语句:

julia> include("/path/to/bmi_struct.jl")     # 文件路径根据实际情况提供

便可获得最终结果,内容类似于:

Dict{Any,Any} with 6 entries:

  4 => 89

  2 => 219

  3 => 158

  5 => 234

  6 => 18        # Ⅲ度肥胖共计18人

  1 => 282       # 体重过低共计282人

不过注意Dict中的数据是无序的,所以打印的内容中没有按照类别ID排列。

有别于第一种实现方式,本方式采用复合类型对数据和操作进行了封装,在业务概念或逻辑上能够显得更为条理清晰,而且整个实现过程也并不复杂。

          ------- 节选自本人新书内容:魏坤著《Julia语言程序设计》,机械工业华章图文出版。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值