R语言编程:基于tidyverse
上QQ阅读APP看书,第一时间看更新

0.1 怎么学习编程语言

编程语言是人与计算机沟通的一种语言形式,根据设计好的编程元素和语法规则,严格规范地表达我们想要做的事情的每一步(程序代码),使计算机能够明白并正确执行,最终得到期望的结果。

编程语言和数学语言很像,数学语言是最适合表达科学理论的形式语言,用数学符号、数学定义和逻辑推理可以规范地表达科学理论。

很多人说:“学数学,就是靠大量刷题;学编程,就是照着别人的代码敲代码”。

我不认可这种观点,这样的学习方法事倍功半,关键是这样做你学不会真正的数学,也学不会真正的编程!

那么应该怎么学习编程语言呢?

打个比方,要成为一个好的厨师,首先得熟悉各种常见食材的特点,掌握各种基本的烹饪方法,然后就能根据客人需要随意组合食材和烹饪方法制作出各种可口的大餐。

数学的食材就是定义,烹饪方法就是逻辑推理,一旦你真正掌握了定义和逻辑推理,各种基本的数学题都不在话下,而且你还学会了数学知识。

同理,编程的食材和烹饪方法就是编程元素语法规则,例如数据结构(如容器)、分支/循环结构、自定义函数等。一旦你掌握了这些编程元素语法规则,根据问题的需要,你就能自由组合并优化它们,从而写出代码解决问题。

学习任何一门编程语言,根据我的经验,有这么几点建议(步骤)。

(1)理解该编程语言的核心思想,比如Python面向对象,R语言面向函数也面向对象。另外,高级编程语言还倡导向量化编程。读者应在核心思想的引领下学习编程语言并思考如何编写代码。

(2)学习该编程语言的基础知识,这些基础知识本质上是相通的,只是不同的编程语言有其对应的编程语法,相关的基础知识包括数据类型及数据结构(容器)、分支/循环结构、自定义函数、文件读写、可视化等。

(3)前两步完成之后,就算基本入门[1]了。读者可以根据需要,结合遇到的问题,借助网络搜索或他人的帮助,分析问题并解决问题,逐步提升编程技能,用得越多会得到越多,也越熟练。


[1] 至少要经历过一种编程语言的入门,再学习其他编程语言就会快很多。

以上是学习编程语言的正确、快速、有效的方法,切忌不学基础语法,用到什么就学什么,基于别人的代码乱改。这样的结果是,自以为节省时间,实际上是浪费了更多的时间,关键是始终无法入门,更谈不上将来提高。其中的道理也很简单,总是在别人的代码上乱改,是学不到真正的编程知识的,也很难真正地入门编程。当你完整地学习了编程语法,再去基于别人的代码进行修改,这实际上是在验证你所学的编程知识,那么你的编程水平自然也会逐步提高。

再来谈一个学编程过程中普遍存在的问题:如何跨越“能看懂别人的代码”到“自己写代码”之间的鸿沟。

绝大多数人在编程学习过程中,都要经历这样一个过程:

第1步:学习基本语法

第2步:能看懂并调试别人的代码

↓(“编程之门”)

第3步:自己编写代码

前两步没有任何难度,谁都可以做到。从第2步到第3步是一个“坎”,很多人困惑于此而无法真正进入“编程之门”。网上也有很多讲到如何跨越这一步的文章,但基本都是脱离实际操作的空谈(比如照着编写书上的代码之类),往往治标不治本(只能提升编程基本知识)。

我所倡导的理念也很简单,无非就是“分解问题 + 实例梳理 + ‘翻译’及调试”,具体如下:

将难以入手的大问题分解为可以逐步解决的小问题;

用计算机的思维去思考并解决每个小问题;

借助类比的简单实例和代码片段,梳理出详细的算法步骤;

将详细的算法步骤用编程语法逐片段地“翻译”成代码并调试通过。

高级编程语言的程序代码通常是逐片段调试的,借助简单的实例按照算法步骤从上一步的结果调试得到下一步的结果,依次向前推进直至到达最终的结果。另外,写代码时,随时跟踪并关注每一步的执行结果,观察变量、数据的值是否到达你所期望的值,非常有必要!

这是我用数学建模的思维得出的比较科学的操作步骤。为什么大家普遍感觉在自己写代码解决具体问题时有些无从下手呢?

这是因为你总想一步就从问题代码,没有中间的过程,即使编程高手也做不到。当然,编程高手也许能缩减这个过程,但不能省略这个过程。其实你平时看编程书是被表象“欺骗”了:编程书上只介绍问题(或者简单分析问题)紧接着就提供代码,给人的感觉就是应该从问题直接到代码,其实不然。

改变从问题直接到代码的固化思维,可以参考前面说的步骤(分解问题+实例梳理+“翻译”及调试)去操作,每一步是不是都不难解决?这样一来,自然就从无从下手转变到能锻炼自己独立写代码了。

开始你或许只能通过写代码解决比较简单的问题,但是慢慢就会有成就感,再加上慢慢锻炼,写代码的能力会越来越强,能解决的问题也会越来越复杂。当然这一切的前提是,你已经真正掌握了基本编程语法,可以随意取用。当然二者也是相辅相成和共同促进的。

好,说清了这个道理,接下来用一个具体的小案例来演示一下。

例0.1 计算并绘制ROC曲线

ROC曲线是二分类机器学习模型的性能评价指标,已知测试集或验证集中每个样本的真实类别及其模型预测概率值,就可以计算并绘制ROC曲线。

先来梳理一下问题,ROC曲线是在不同分类阈值上对比真正率(TPR)与假正率(FPR)的曲线。

分类阈值就是根据预测概率判定预测类别的阈值,要让该阈值从0到1以足够小的步长变化,对于每个阈值c(如0.85),则当预测概率≥0.85时,判定为“Pos”;当预测概率时,判定为“Neg”。这样就得到了预测类别。

根据真实类别和预测类别,就能计算混淆矩阵,各单元格含义如图0.1所示。

图0.1 混淆矩阵示意图

进一步就可以计算:

有一个阈值,就能计算一组TPRFPR,循环迭代并计算所有的TPRFPR,且将相关数值进行保存。再以FPRx轴,以TPRy轴进行绘制,就可以得到ROC曲线。

在此,我们梳理一下经过分解后的问题。

让分类阈值以某步长在[1,0]上变化取值。

对某一个阈值:

计算预测类别;

计算混淆矩阵;

计算TPRFPR

循环迭代,计算所有阈值的TPRFPR

根据TPRFPR数据绘制ROC曲线。

下面以一个小数据为例,借助代码片段来推演上述过程。现在读者不用纠结于代码,更重要的是体会自己写代码并解决实际问题的过程。

library(tidyverse)
df = tibble(
ID = 1:10,
真实类别 = c(“Pos”,”Pos”,”Pos”,”Neg”,”Pos”,”Neg”,”Neg”,”Neg”,”Pos”,”Neg”),
预测概率 = c(0.95,0.86,0.69,0.65,0.59,0.52,0.39,0.28,0.15,0.06))
knitr::kable(df)

以上代码的运行结果如表0.1所示。

表0.1 真实类别和预测概率

先对某一个阈值,计算对应的TPRFPR,这里以为例。

计算预测类别,实际上就是if-else语句根据条件赋值,当然是用整洁的tidyverse来做。顺便多做一件事情:把类别变量转化为因子型,以保证“Pos”和“Neg”的正确顺序,与混淆矩阵中一致。

c = 0.85
df1 = df %>%
mutate(
预测类别 = ifelse(预测概率 >= c, “Pos”, “Neg”),
预测类别 = factor(预测类别, levels = c(“Pos”, “Neg”)),
真实类别 = factor(真实类别, levels = c(“Pos”, “Neg”)))
knitr::kable(df1)

上述代码的运行结果如表0.2所示。

表0.2 真实类别、预测概率和预测类别

计算混淆矩阵,实际上就是统计交叉频数(例如真实值为“Pos”且预测值也为“Pos”的情况有多少,等等)。用R自带的table()函数就能搞定:

cm = table(df1$预测类别, df1$真实类别)
cm
##
##       Pos Neg
##   Pos   2   0
##   Neg   3   5

计算TPRFPR比较简单,根据计算公式,从混淆矩阵中取值进行计算即可。这里咱们再高级一点,用向量化编程来实现。向量化编程是对一列矩阵中的数同时做同样的操作,既提升程序效率又大大简化代码。

向量化编程关键是要用整体考量的思维来思考和表示运算。比如这里计算TPRFPR,通过观察可以发现:混淆矩阵的第1行的各个元素,都除以其所在列的和,正好是TPRFPR

cm[“Pos”,] / colSums(cm)
## Pos Neg
## 0.4 0.0

这就完成了本问题的核心部分。接下来,要进行循环迭代,对每个阈值都计算一遍TPRFPR。用for循环当然可以,但咱们仍然更高级一点,使用泛函式编程。

先把上述计算封装成一个自定义函数,该函数只要接收一个原始的数据框df和一个阈值c,就能返回来你想要的TPRFPR。然后,再把该函数应用到数据框df和一系列的阈值上,循环迭代自然就完成了。这就是泛函式编程

cal_ROC = function(df, c) {
df = df %>%
mutate(
预测类别 = ifelse(预测概率 >= c, “Pos”, “Neg”),
预测类别 = factor(预测类别, levels = c(“Pos”, “Neg”)),
真实类别 = factor(真实类别, levels = c(“Pos”, “Neg”)))
cm = table(df$预测类别, df$真实类别)
t = cm[“Pos”,] / colSums(cm)
list(TPR = t[[1]], FPR = t[[2]])
}

测试一下这个自定义函数,能不能算出来刚才的结果:

cal_ROC(df, 0.85)
## $TPR
## [1] 0.4
##
## $FPR
## [1] 0

没问题,下面将该函数应用到一系列阈值上(循环迭代) ,并一步到位将每次计算的两个结果按行合并到一起,这就彻底完成了数据计算:

c = seq(1, 0, -0.02)
rocs = map_dfr(c, cal_ROC, df = df)
head(rocs)      # 查看前6个结果
## # A tibble: 6 x 2
##     TPR   FPR
##   <dbl> <dbl>
## 1   0       0
## 2   0       0
## 3   0       0
## 4   0.2     0
## 5   0.2     0
## 6   0.2     0

最后,用著名的ggplot2包绘制ROC曲线图形,如图0.2所示:

rocs %>%
ggplot(aes(FPR, TPR)) +
geom_line(size = 2, color = “steelblue”) +
geom_point(shape = “diamond”, size = 4, color = “red”) +
theme_bw()

以上就是我所主张的学习编程的正确方法,我认为照着别人的编程书敲代码不是学习编程的好方法。

图0.2 绘制ROC曲线