Notes of EK2002: http://www.humoon.xyz/notes/classic-papers/trade/EK2002/_book/index.html
本项目用 R 语言重写了 EK20021 的祖传 GAUSS 代码,并以文学化编程的方式展示该论文数据处理的全流程。
文学化编程(Literate Programming):自动生成输出文件(html、pdf、docx 等格式),将文段、图表、代码和代码运行结果混排。或许可以意译为“文档化编程”。
GAUSS 是一门非常古老的数学和统计编程语言,目前已基本无人使用,成为一门僵尸语言。僵尸语言最大的问题还不是技术上的落后,而是帮助文档和资料的极度稀缺,以及生态上的不足。比如,宇宙第一编辑器 VSCode 就不支持 GUASS 语言的代码高亮,每次要看源代码都不得不断网、修改系统时间,才能打开破解版 GAUSS 软件。
EK2002 源代码里的很多命令和函数,我都不知道是什么意思,因为不可能有时间系统地学习一门僵尸语言,或一个一个细查官方文档。更多的是在 GAUSS 软件中调试,逆推那些命令和函数的作用。有时候程序对特定数据的依赖性比较强,无法即时输出,就只能根据运行结果,猜测中间的步骤。以上种种,本项目堪称一个逆向工程。
在宏观层次,逆向工程需要思想的指导,如果我不懂 EK2002 这篇论文建模-估计-模拟的整体架构,和数学上的一系列难点,是绝不可能成功复现其结果的。在微观层次,逆向工程不需要严格一致的还原,而仅需要逻辑等价的还原。EK2002 的源码有很多繁冗、难解之处,如果更简洁的代码能够实现同样的结果,就没有必要 100% 地“翻译”源码。
EK2002 的源码触犯了多条软件工程的禁忌,如:
EK2002 Source Code | 现代代码规范 |
---|---|
代码表意不明 英文单词多为缩写形式,且两个缩写之间没有任何分隔符,使读者很难看懂一个变量的含义。 为此,不得不依靠大量注释,解释每一行做了什么。 | 代码自解释(自我描述型代码) 更多使用单词的完整拼写,优先保证代码本身的表意功能。 少量注释,更多地表达操作目的和背后的原理,而非做了什么本身。 |
变量名长度过短 修改时不易通过搜索准确定位。 | 独特变量名 不怕变量名长,关键是要保证变量名的独特性,搜索时不至于搜索到太多其他变量。 |
多次修改一个变量的值 比如 wage ,最开始代表以各国货币衡量的工资,后面变成美元工资,再后面是各国工资相对于美国工资的比率,最后是这个比率的对数……调试时不得不频繁打印该变量,才能确切地知道它在每一处到底是什么。 | 确保一个变量只有一个值 如果变量经过变换生成了一个新的值,并且需要保存,就另存为一个新变量。 尽量用管道串联一系列操作,减少中间变量的个数,以免变量名不够用。 如左边的例子,对初始变量 wage 的每一步操作都用管道串联起来,最后结果保存为新变量 normalized_wage . |
冗余变量和函数 有些函数变量和函数定义后居然从未被使用过,有些函数的作用居然是经过一系列变换后返回参数本身。 | |
低内聚 EK2002 的源代码有 6 个脚本文件,脚本之间有很多重复的功能,不仅没有用统一的代码段覆盖,而且连表达相同含义的变量名都变了。 | 高内聚 重复程度较高的脚本,一般逻辑关系紧密,可以合并。则重复的代码段只需要写一次。 |
高耦合 一个文件计算出的结果,有时被硬编码到其他文件中。如果需要修改数据源或计算方法,就要在许多相关文件中修改。 | 低耦合 被依赖的文件运算结果保存为数据,由依赖文件导入。 |
层次不清晰 数据、函数和命令夹杂在一起,让人不易理清其逻辑。 | 模块化 复用程度高的函数,应单独保存在脚本文件中,称为模块,由主程序文件导入。 |
违反数据的一致性 同一个变量可以从不同的数据源读取或变形获得,而且无法严格对应。 例如,作者用来做参数估计的对数标准化的贸易数据,有两种获得途径:第一种是读取原始双边贸易数据,经过处理和变换得到;第二种是直接读取被保存为文件的对数标准化贸易数据。而这两种途径获得的数据居然是不相等的,有时能相差 10% 以上。 | 为了复现论文的结果,不得不在估计部分使用被保存为文件的对数标准化贸易数据,在模拟部分使用原始双边贸易数据。 |
R 是研究场景中最适合处理数据的编程语言,因为
它内置了向量、矩阵和数据框三种数据结构,绝大多数相关运算都可以使用定义得非常简洁、完善的内置函数
它默认支持向量化操作(不像 Python 必须通过第三方库才能引入“广播”功能),而向量化操作可以大大简化向量和矩阵的运算。
它对泛函编程范式和管道传输的支持非常好
无论在计量经济学、还是数值分析(Numerical Analysis,国内也称为数值计算)领域,R 都具有相当完善的生态系统,第三方包提供了许多功能强大、且封装得非常简洁的函数。在执行 EK2002 中的回归和求解非线性方程组等任务时,用 GAUSS 需要写很多行的代码,R 往往三五行即可完成。代码的简洁,能使论文逻辑得到更好的凸显。
综上,在数据科学的研发场景2,R 是目前最高效、最优雅的编程语言。
大量使用向量化操作、泛函编程范式和管道传输,尽量避免出现for
、while
等显式循环。
x1#######################################################
2## TABLE VI: States of Technology
3# 由(30)式估计的 S_i 和各种方法估计的 theta,利用(27)式计算 T_i
4#######################################################
5
6# %>% 为管道操作符,表示把上一个函数的变换结果作为参数,传递给下一个函数
7
8table6 <- theta_list %>% # 多个 theta 值,依次传递给高阶函数 map_dfc()
9 map_dfc(function(theta) {
10 # 作为 map_dfc() 参数的匿名函数可以对每个 theta 分别计算一个 19 国技术水平列向量
11 # _dfc 表示将这些向量纵向 (column, c) 堆叠,组成一个 data.frame(df) 类的数据表
12 # 每行对应一个国家,每列代表一个 theta 情境,这就是 TABLE VI 的雏形
13
14 # 根据 (27) 式 S_i = ln(T_i) / beta - theta * ln(w_i) 求 T_i
15 tech <- log(adjusted_relative_wage) %>% # ln(w_i)
16 `*`(theta) %>% # * theta
17 `+`(source_parameters) %>% # + S_i
18 `*`(beta) %>% # × beta
19 exp() # e^
20 # 上面几行的四则、指数、对数运算全部是向量化操作
21 # 这样写不仅清楚(每一行包含且仅包含一个运算),而且符合人类面对 (27) 式时的思维:
22 # 先计算最右边的一坨,移项到等号左边化减为加,两边乘以 beta,最后再做指数变换
23
24 # 如果不用管道,就不得不写成这样
25 tech <- exp(beta * (source_parameters + theta * log(adjusted_relative_wage)))
26 # 不仅可读性差,而且只有先在纸上完成对 (27) 的变形,导出 T_i,才能写出这行代码
27
28
29 (tech / tech[N]) %>% # 向量化操作,各国技术水平以美国为基准进行标准化
30 sprintf("%.2f", .) %>%
31 as.data.frame() %>%
32 set_colnames(str_c("$\\theta=", sprintf("%.2f", theta), "$"))
33 }) %>%
34 mutate( # 再加入两列,一列是国家名称,一列是 S_i (Source-country estimates)
35 Country = country_table$name,
36 `Estimated Source-country Competitiveness` = source_parameters %>% sprintf("%.2f", .)
37 ) %>%
38 select(Country, `Estimated Source-country Competitiveness`, everything())
39 # 把新加的两列重新排列在最左边
./docs/
文件夹下推了一部分,不知正确否。从 GAUSS source code 中应该能完整地逆推出来,较繁琐,待完成。