在我的第一篇关于 Groovy 编程语言入门的文章中,我以一个在 Groovy 中读取 CSV 文件的示例结束。在本文中,我将转向更地道的 Groovy 风格(正如某些人所说,使其更 Groovy),介绍 Groovy 映射作为查找表的使用,并最终使用映射来计算一些结果。
首先,这是上一篇文章的最终示例,采用更地道的 Groovy 风格
import com.opencsv.CSVReader
def mdCountryCSV = "Metadata_Country_API_SP.POP.TOTL_DS2_en_csv_v2.csv"
new File(mdCountryCSV).withReader { reader ->
def csvReader = new CSVReader(reader)
def fNam = csvReader.readNext()
csvReader.each { fVal ->
def valByNam = [fNam,fVal].transpose().collectEntries()
println valByNam
}
}
将此代码片段另存为 ex04.groovy,与前三个示例以及您从世界银行下载的数据放在同一目录下,并运行它。您看到了什么?
需要做一些解释。
首先,为了使示例更具可读性,我缩短了我的长变量名。
其次,与 Java 相比,Groovy 对类型检查持有不同的观点。正如 Burt Beckwith 在他的关于 Grails(和 Groovy)的优秀著作《Programming Grails》中描述的那样,Groovy 支持可选类型,允许用户控制类型或将其留给语言在运行时通过使用 def 关键字声明变量来确定。我将补充 Burt 的精彩阐述,即古老的格言“能力越大,责任越大”。将类型决策留给语言可能会引入难以发现的错误,并且在任何情况下,都会将类型错误的检测推迟到运行时。但是,这种语言设计决策也意味着 Groovy 不仅可以提供更简洁的代码(更少阅读意味着更少调试),还可以提供一些与运行时具体化行为相关的有趣的动态功能。
第三,用于复制由 csvReader 为文件的每一行提供的列名数组和相应的列值数组的已简洁代码已被 Groovy 一些用于处理集合和映射的优秀方法所取代
[fNam, fVal]
创建一个由两个元素组成的新列表:当前行的字段名数组和字段值数组.transpose()
将其转换为一个新列表,其中每个元素都是每个字段的 [name,value] 对.collectEntries()
将 [name,value] 对的列表转换为一个映射,其中键是字段名,值是字段值
最后,行 println valByNam
只是转储上面生成的映射。
好了,解释够多了。上面的代码只是略微有趣,因为,真的,谁需要将国家元数据的内容重新格式化为映射/键值对?让我们用这些数据做点什么!
创建查找表
我们在国家元数据中看到的一件事是,世界银行有一个根据国家收入对国家进行分类的系统。 此列在文件中称为IncomeGroup(无空格)。作为将两个单独的文件链接在一起的示例,让我们
- 从国家元数据创建一个国家代码与收入组的查找表——我们将其称为
iGLU;
- 使用该查找表,根据人口数据文件计算按收入组划分的人口增长率。
为此,我们首先需要声明一个查找表并填充它。我们将重新利用上面的代码
import com.opencsv.CSVReader
def iGLU = [:] // income group lookup table
def mdCountryCSV = "Metadata_Country_API_SP.POP.TOTL_DS2_en_csv_v2.csv"
new File(mdCountryCSV).withReader { reader ->
def csvReader = new CSVReader(reader)
def fNam = csvReader.readNext()
csvReader.each { fVal ->
def valByNam = [fNam,fVal].transpose().collectEntries()
iGLU[valByNam."Country Code"] = valByNam.IncomeGroup
}
}
println iGLU
将此代码另存为 ex05.groovy 并运行它。
符号 [:]
创建一个空映射。我们可以更具体一些,例如,通过定义一个基于哈希表的映射,该映射接受 String 参数并产生字符串结果。如果您不太熟悉映射和哈希表,Groovy 文档的 2.2 节提供了更多详细信息。
另请注意使用点表示法访问映射——特别是,IncomeGroup
周围的引号不是必需的,只有 Country Code
周围的引号是必需的,因为它嵌入了空格。但是,如果点右侧的是变量而不是常量,我们必须用括号将其括起来,或者回到方括号并省略点。
创建和初始化累加器
为了计算按收入组划分的人口增长率,我们将不得不创建累加器。由于我们使用国家元数据中提供的收入组分类,因此如果我们的累加器定义为映射,则是有意义的。此外,我们将需要两个:一个用于起始年份的人口,一个用于结束年份的人口。最后,我们需要决定是在开始时初始化这些指标,还是在读取人口数据时每次遇到新的收入组时初始化。为了本练习的目的,我们将首先初始化
def iGSet = iGLU.values() as Set
def pop1 = [:] // population by income group in start year
def pop2 = [:] // population by income group in end year
iGSet.each { ig ->
pop1[ig] = 0l
pop2[ig] = 0l
}
将其附加到 ex05.groovy
的末尾(您可以删除 println
)。
第一行代码从 iGLU
收入组查找映射中获取所有值,并将它们转换为 Set
,Set
是一种集合,其中每个元素最多出现一次。我们将使用此 iGSet
来迭代国家元数据中定义的收入组的唯一值。
然后我们定义了我们的两个累加器,pop1
,我们用它来累加起始年份按收入组划分的总数,以及 pop2
,我们用它来累加结束年份的总数。
接下来的四行将 pop1
和 pop2
初始化为零(long——这就是 0l
的意思)。现在是指出 Groovy 的设计者决定 Groovy 源代码中不限定的整数是 BigInteger
类型,而不限定的十进制数是 BigDecimal
类型的好时机。我倾向于避免使用这些(软件实现的)类型。
使用查找和累加处理另一个文件
此时,我们可以读取人口数据并对其进行累加
def populationCSV = "API_SP.POP.TOTL_DS2_en_csv_v2.csv"
new File(populationCSV).withReader { reader ->
def csvReader = new CSVReader(reader)
def fNam
(1..5).each { fNam = csvReader.readNext() }
csvReader.each { fVal ->
def valByNam = [fNam,fVal].transpose().collectEntries()
def country = valByNam."Country Code"
if (country && iGLU.containsKey(country)) {
pop1[iGLU[country]] += Long.parseLong(valByNam."2014" ?: "0")
pop2[iGLU[country]] += Long.parseLong(valByNam."2015" ?: "0")
}
}
}
将其附加到 ex05.groovy 的末尾。
类似于元数据文件的处理(毕竟它都只是 CSV),值得注意的是,人口 CSV 的格式不正确——在列标题行之前有四行标题行。因此,定义列名列表更加复杂。代码 (1..5)
使用 Groovy 范围来生成包含 5 个元素 1、2、3、4、5 的列表,并且 each
为每个元素执行一次闭包。这在效果上等同于 C 或 Java(或 Groovy!)for (int i = 0; i < 5; i++) {...}
,但不需要我们定义一个虚假变量。通过这段代码,fNam
最终被设置为第五行——即列标题。
if 语句首先检查国家/地区是否为非空和非空白,然后确保在收入组查找表的键中找到它。现在是休息一下并研究一些 Groovy 语义的好时机,特别是 Groovy 中真值的含义(该页面的第 5 节)。基本上,非空、非空和非零值都为真。
总结结果
最后,我们此时使用 Long
值,因为有太多人无法在 32 位整数中枚举。
现在我们已经累积了数据,剩下的一个步骤是将其写出
iGSet.each { ig ->
printf "group %s growth rate %.2f %%\n",
(ig ?: "Unspecified"),100d * (pop2[ig] - pop1[ig]) / pop1[ig]
}
将其附加到 ex05.groovy 的末尾。此时,您拥有一个完整的程序,可以运行它,生成以下输出
group High income growth rate 0.56 %
group Low income growth rate 2.73 %
group Upper middle income growth rate 0.78 %
group Unspecified growth rate 1.27 %
group Lower middle income growth rate 1.46 %
请注意,上面的 (ig ?: "Unspecified")
使用了 Groovy Elvis 运算符,它是 (ig ? ig : "Unspecified")
的简写形式,并且是 DRY 原则(不要重复自己)的一个很好的例子。我们使用这种构造是为了使每个收入组都打印一个文本字符串,即使原始数据中没有指定一个。
作为一名数据分析师,我的目的不是解释这些结果;然而,我获取一些数据(在本例中是公开可用的数据)并挖掘其关系的整个过程正是我的许多工作内容。通过这个例子,您可以看到为什么 Groovy 简洁而强大的集合和映射抽象,再加上所有现有的 Java 库,使其成为我不可或缺的工具。
下一步去哪里?
在下一期中,我们将看看 CSV 文件以外的其他结构化数据源。
评论已关闭。