3.5 Julia 标准库

Julia 拥有 丰富的标准库,每个 Julia 发行版都可以使用这些库。 与截至目前提到的一切相反,例如类型,数据结构和文件系统;在使用特定的模块或函数前, 需要将标准库模块导入到环境中

这可以通过 usingimport实现。 本书将使用 using 导入代码:

using ModuleName

在执行上述操作后,就可以使用 ModuleName 中所有的函数和类型。

3.5.1 日期

了解如何处理日期和时间戳在数据科学中很重要。 正如在 为什么选择 Julia? (Section 2) 节讨论的那样,Python 中的 pandas 使用它自己的 datetime 类型处理日期。 R 语言中 TidyVerse 的 lubridate 包中也是如此,它也定义了自己的 datetime 类型来处理日期。 在 Julia 软件包中,不需要编写自己的日期逻辑,因为 Julia 标准库中有一个名为 Dates 的日期处理模块。

首先加载 Dates 模块到工作空间中:

using Dates

3.5.1.1 Date and DateTime Types

Dates 标准库模块有 两种处理日期的类型:

  1. Date: 表示以天为单位的时间和
  2. DateTime: 表示以毫秒为单位的时间。

构造 DateDateTime 的方法是,向默认构造器传递表示年,月,日,小时等等的整数:

Date(1987) # year
1987-01-01
Date(1987, 9) # year, month
1987-09-01
Date(1987, 9, 13) # year, month, day
1987-09-13
DateTime(1987, 9, 13, 21) # year, month, day, hour
1987-09-13T21:00:00
DateTime(1987, 9, 13, 21, 21) # year, month, day, hour, minute
1987-09-13T21:21:00

好奇的人会发现,1987 年 9 月 13 日 21 点 21 分正是第一作者 Jose 的官方出生时间。

也可以向默认构造器传递 Period 类型。 对于计算机来说,Period 类型是时间的等价表示。 Julia 的 Dates 具有如下的 Period 抽象类型:

subtypes(Period)
DatePeriod
TimePeriod

它被划分为如下的具体类型,并且它们的用法都是不言自明的:

subtypes(DatePeriod)
Day
Month
Quarter
Week
Year
subtypes(TimePeriod)
Hour
Microsecond
Millisecond
Minute
Nanosecond
Second

因此,也能以如下方式构造 Jose 的官方出生时间:

DateTime(Year(1987), Month(9), Day(13), Hour(21), Minute(21))
1987-09-13T21:21:00

3.5.1.2 序列化 Dates

多数情况下,我们不会从零开始构造 DateDateTime 示例。 实际上更可能是 将字符串序列化为 DateDateTime 类型

DateDateTime 构造器可以接收一个数字字符串和格式字符串。 例如,表示 1987 年 9 月 13 日的字符串 "19870913" 可被序列化为:

Date("19870913", "yyyymmdd")
1987-09-13

注意第二个参数是日期格式的字符串表示。 前四位表示年 y,后接着的两位表示月 m, 而最后两位数字表示日 d.

这也适用于 DateTime 的时间戳:

DateTime("1987-09-13T21:21:00", "yyyy-mm-ddTHH:MM:SS")
1987-09-13T21:21:00

可以在 Julia Dates’ documentation 了解到更多的日期格式。 不同担心需要时常浏览文档,我们在处理日期和时间戳时也是这样。

根据 Julia Dates’ documentation,当只需调用几次时,使用 Date(date_string, format_string) 方法也是可以的。 然而,如果需要处理大量相同格式的日期字符串,那么更高效的方法是先创建 DateFormat 类型,然后传递该类型而不是原始的格式字符串。 然后,先前的例子改为:

format = DateFormat("yyyymmdd")
Date("19870913", format)
1987-09-13

或者,在不损失性能的情况下,使用字符串字面量前缀 dateformat"..."

Date("19870913", dateformat"yyyymmdd")
1987-09-13

3.5.1.3 提取日期信息

很容易 DateDateTime 对象中提取想要的信息。 首先,创建一个具体日期的实例:

my_birthday = Date("1987-09-13")
1987-09-13

然后可以从 my_birthday 中提取任何想要的信息:

year(my_birthday)
1987
month(my_birthday)
9
day(my_birthday)
13

Julia 的 Dates 模块也提供了 返回值元组的复合函数

yearmonth(my_birthday)
(1987, 9)
monthday(my_birthday)
(9, 13)
yearmonthday(my_birthday)
(1987, 9, 13)

也能了解该日期是一周的第几天和其他方便的应用:

dayofweek(my_birthday)
7
dayname(my_birthday)
Sunday
dayofweekofmonth(my_birthday)
2

是的,Jose 出生在 9 月的第 2 个星期日。

NOTE: 如下是一个从 Dates 实例中提取工作日的便捷提示。 对 dayofweek(your_date) <= 5 使用 filter。 也可以使用 BusinessDays.jl 包进行工作日相关的操作。

3.5.1.4 日期操作

可以对 Dates 实例进行多种 操作 。 例如,可以对一个 DateDateTime 实例增加天数。 请注意,Julia 的 Dates 将自动地对闰年以及 30 天或 31 天的月份执行必要的调整(这称为 日历 算术)。

my_birthday + Day(90)
1987-12-12

我们想加多少天就加多少天:

my_birthday + Day(90) + Month(2) + Year(1)
1989-02-11

可能你想知道:“还能用 Dates 做些什么?还有哪些可用的方法?”,则可以使用 methodswith 检索这些方法。 这里只展示前 20 条结果:

first(methodswith(Date), 20)
[1] show(io::IO, dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/io.jl:736
[2] show(io::IO, ::MIME{Symbol("text/plain")}, dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/io.jl:734
[3] DateTime(dt::Date, t::Time) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/types.jl:403
[4] Day(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/periods.jl:36
[5] Month(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/periods.jl:36
[6] Quarter(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/periods.jl:36
[7] Week(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/periods.jl:36
[8] Year(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/periods.jl:36
[9] firstdayofmonth(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:84
[10] firstdayofquarter(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:157
[11] firstdayofweek(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:52
[12] firstdayofyear(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:119
[13] lastdayofmonth(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:100
[14] lastdayofquarter(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:180
[15] lastdayofweek(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:68
[16] lastdayofyear(dt::Date) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/adjusters.jl:135
[17] +(dt::Date, t::Time) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/arithmetic.jl:19
[18] +(dt::Date, y::Year) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/arithmetic.jl:27
[19] +(dt::Date, z::Month) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/arithmetic.jl:54
[20] +(x::Date, y::Quarter) in Dates at /opt/hostedtoolcache/julia/1.7.3/x64/share/julia/stdlib/v1.7/Dates/src/arithmetic.jl:73

由上可知,也能使用加 + 和减 - 运算符。 让我们看看 Jose 的年龄, 以天为单位:

today() - my_birthday
13414 days

Date 类型的 默认持续时间Day 实例。 而对于 DateTime 类型,默认持续时间是 Millisecond 实例。

DateTime(today()) - DateTime(my_birthday)
1158969600000 milliseconds

3.5.1.5 日期区间

关于 Dates 模块的一个好处是,可以轻松地构造 日期和时间区间。 Julia 足够聪明,因此不用去定义在 Section 3.3.6 中讨论的整个区间类型和操作。 它只需将为 range 定义的函数和操作扩展到 Date 类型。 这就是在 为什么选择 Julia? (Section 2) 中讨论过的多重派发。

例如,假设想要创建一个 Day 区间。 这可以轻松地通过冒号 : 运算符实现:

Date("2021-01-01"):Day(1):Date("2021-01-07")
2021-01-01
2021-01-02
2021-01-03
2021-01-04
2021-01-05
2021-01-06
2021-01-07

使用 Day(1) 作为间隔没有什么特别的, 可以使用 任意的 Period 类型 作为间隔。 比如,使用 3 天作为间隔:

Date("2021-01-01"):Day(3):Date("2021-01-07")
2021-01-01
2021-01-04
2021-01-07

又或者是月份:

Date("2021-01-01"):Month(1):Date("2021-03-01")
2021-01-01
2021-02-01
2021-03-01

注意, 这个区间的类型是内含 Date 和具体 Period 类型的StepRange,其中 Period 用于作为间隔:

date_interval = Date("2021-01-01"):Month(1):Date("2021-03-01")
typeof(date_interval)
StepRange{Date, Month}

可以使用 collect 函数将它转换为 向量

collected_date_interval = collect(date_interval)
2021-01-01
2021-02-01
2021-03-01

并具有全部的可用数组功能,例如索引:

collected_date_interval[end]
2021-03-01

也可以在 Date 向量中实现 日期操作的广播

collected_date_interval .+ Day(10)
2021-01-11
2021-02-11
2021-03-11

同理,这些例子也适用于 DateTime 类型。

3.5.2 随机数

Random 模块是另一重要的 Julia 标准库模块。 这个模块的用途是 生成随机数Random 是功能丰富的库,如果感兴趣,可以阅读查看 Julia’s Random documentation 了解更多信息。 接下来 讨论三个函数: randrandnseed!

在开始前,首先导入 Random 模块。 先精确地导入想使用的方法:

using Random: seed!

主要有 两个生成随机数的函数

3.5.2.1 rand

默认情况下,可以不带参数地调用 rand ,那么它就会返回一个位于区间 \([0, 1)\)Float64 随机数,该区间表示随机数的范围是 0 (包含)到 1 (排除)之间:

rand()
0.6070024145662858

可以使用多种方式更新 rand 的参数。 假设,想要获得多个随机数:

rand(3)
[0.5487241005162437, 0.2083075356176668, 0.3643483123672302]

或者,想在不同的区间抽样:

rand(1.0:10.0)
8.0

也可以给区间指定步长,甚至可以在不同的类型间抽样。 这里使用不带点 . 的数字,所以 Julia 会将它们解释为 Int64 而不是 Float64

rand(2:2:20)
14

还可以组合和匹配参数:

rand(2:2:20, 3)
[6, 12, 10]

它还支持元素集构成的元组:

rand((42, "Julia", 3.14))
Julia

也支持数组:

rand([1, 2, 3])
2

还可以是 Dict

rand(Dict(:one => 1, :two => 2))
:one => 1

最后要讨论的 rand 参数选项是,使用数字元组指定随机数的维度。 如果执行此操作,那么返回类型将会变为数组。 例如,如下是由 1.0-3.0 间的 Float64 随机数构成的 2x2 矩阵:

rand(1.0:3.0, (2, 2))
2×2 Matrix{Float64}:
 3.0  2.0
 3.0  3.0

3.5.2.2 randn

randn 遵循与 rand 相同的生成原理,但现在它只返回从 标准正态分布 中生成的随机数。 标准正态分布是平均值为 0 和标准差为 1 的正态分布。 其默认类型为 Float64,并且只接受 AbstractFloatComplex 的子类型:

randn()
0.21451483896752419

可以仅指定大小:

randn((2, 2))
2×2 Matrix{Float64}:
 -0.471473  -0.59437
  0.684233   0.286052

3.5.2.3 seed!

Random 概述的结尾部分, 我们接下来讨论 重现性。 我们经常需要让某些事能够 可复现。 这意味着,随机数生成器要每次 生成相同的随机数序列。 这可以通过 seed! 函数实现:

seed!(123)
rand(3)
[0.521213795535383, 0.5868067574533484, 0.8908786980927811]
seed!(123)
rand(3)
[0.521213795535383, 0.5868067574533484, 0.8908786980927811]

在一些例子中,在脚本开头调用 seed! 是不够好的。 为了避免 randrandn 依赖全局变量,那么可以转而定义一个 seed! 的实例,然后将它传递给 randrandn 的第一个参数。

my_seed = seed!(123)
Random.TaskLocalRNG()
rand(my_seed, 3)
[0.521213795535383, 0.5868067574533484, 0.8908786980927811]
randn(my_seed, 3)
[-0.21766510678354617, 0.4922456865251828, 0.9809798121241488]

NOTE: 请注意,对于不同的版本,这些数字可能会有所不同。 若要在不同的版本获得稳定的随机数流,请使用 StableRNGs.jl 库。

3.5.3 Downloads

最后一个要讨论的 Julia 标准库是 Downloads 模块。 这部分相当简短,因为只关注单个函数 download

假设想要 从互联网上下载文件到本地。 这可以用通过 download 函数实现。 最简单情况下,仅仅需要一个参数,那就是文件的 url。 还可以指定第二个参数作为下载文件的输出路径 (不要忘记文件系统的最佳实践!)。 如果不指定第二个参数,默认情况下,Julia 将使用 tempfile 函数创建一个临时文件。

首先导入 Downloads 模块:

using Downloads

例如,下载 JuliaDataScience GitHub 仓库Project.toml 文件。 注意,Downloads 模块并没有导出 download 函数,因此需要使用语法 Module.function 。 默认情况下, 它返回一个包含下载文件本地路径的字符串:

url = "https://raw.githubusercontent.com/JuliaDataScience/JuliaDataScience/main/Project.toml"

my_file = Downloads.download(url) # tempfile() being created
/tmp/jl_sDlBE4

可以使用 readlines 查看下载文件的前四行:

readlines(my_file)[1:4]
4-element Vector{String}:
 "name = \"JDS\""
 "uuid = \"6c596d62-2771-44f8-8373-3ec4b616ee9d\""
 "authors = [\"Jose Storopoli\", \"Rik Huijzer\", \"Lazaro Alonso\"]"
 ""

NOTE: 对于更复杂的 HTTP 交互过程,例如与 web API 交互,请使用 HTTP.jl 包。



CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso, 刘贵欣 (中文翻译), 田俊 (中文审校)