什么是将分类数组转换为数字数组的完美方法?

7

如何将分类数组转换为简单数字数组?例如:

using CategoricalArrays
a = CategoricalArray(["X", "X", "Y", "Z", "Y", "Y", "Z"])
b = recode(a, "X"=>1, "Y"=>2, "Z"=>3)

由于转换,即使我们明确指定分配值的类型,仍然会得到一个分类数组。
b = recode(a, "X"=>1::Int64, "Y"=>2::Int64, "Z"=>3::Int64)

看起来这里需要一些其他的解决方案,但我想不到要去哪个方向

3个回答

9

您有两个自然选择:

julia> recode(unwrap.(a), "X"=>1, "Y"=>2, "Z"=>3)
7-element Vector{Int64}:
 1
 1
 2
 3
 2
 2
 3

或者
julia> mapping = Dict("X"=>1, "Y"=>2, "Z"=>3)
Dict{String, Int64} with 3 entries:
  "Y" => 2
  "Z" => 3
  "X" => 1

julia> [mapping[v] for v in a]
7-element Vector{Int64}:
 1
 1
 2
 3
 2
 2
 3

Dict方法较慢,但如果你需要映射多个级别,则更灵活。

关键函数是unwrap,它取消了CategoricalValue的“范畴”概念(在Dict风格中,unwrap会自动调用)。

还要注意的是,如果您只想获取存储在CategoricalArray中的值的levelcode(这是R默认的操作),则可以执行以下操作:

julia> levelcode.(a)
7-element Vector{Int64}:
 1
 1
 2
 3
 2
 2
 3

请注意,使用levelcode时,missing会被映射为missing
julia> x = CategoricalArray(["Y", "X", missing, "Z"])
4-element CategoricalArray{Union{Missing, String},1,UInt32}:
 "Y"
 "X"
 missing
 "Z"

julia> levelcode.(x)
4-element Vector{Union{Missing, Int64}}:
 2
 1
  missing
 3

3
除了Bogumił的答案之外,一个可能相当快速的方法是:
julia> b = recode!(similar(a, Int), a, "X"=>1, "Y"=>2, "Z"=>3)
7-element Vector{Int64}:
 1
 1
 2
 3
 2
 2
 3


3
Bogumił的回答涵盖了大部分问题,但我认为添加另一种解决方案可能会很有用:
unwrap.(recode(a, "X"=>1, "Y"=>2, "Z"=>3))

随着CategoricalArray的长度相对于类别数量的增长,这种解决方案比其他任何解决方案(截至目前)更具执行性,并且对我来说似乎是一种非常自然的解决方案(它几乎与OP的尝试相同)。 更重要的是,它在这些情况下更具执行性说明了关于CategoricalArrays以及调用这些函数时实际发生的事情的事情。
通过在a上调用dump,您可以看到此分类数组的结构。 这是一个简化版本:
CategoricalVector{String, UInt32, String, CategoricalValue{String, UInt32}, Union{}}
  refs: UInt32[0x00000001, 0x00000001, 0x00000002, 0x00000003, 0x00000002, 0x00000002, 0x00000003]
  pool: CategoricalPool{String, UInt32, CategoricalValue{String, UInt32}}
    levels: String["X","Y","Z"]
    invindex: Dict{String, UInt32}("Y" => 0x00000002, "Z" => 0x00000003, "X" => 0x00000001)

每个类别都被编码为一个UInt32。编码后的值存储在Vector refs中。 CategoricalPool pool包含:
  • levels:从级别代码到类别的映射(Vector{String},“key”是索引)
  • invindex:从类别到级别代码的映射(Dict{String, UInt32}
这种结构可以非常高效地重新编码。在许多情况下,我们可以创建一个具有新类别的分类数组,而无需触及任何refs,只需交换描述代码的pool部分即可。
mapping = Dict("X"=>1, "Y"=>2, "Z"=>3)
b = CategoricalArray{Int64,1,UInt32}(undef, 0)
b.refs = a.refs
levels!(b.pool, [mapping[l] for l in levels(a.pool)])

在实际的recode函数中,创建了一个与a长度相同的新空分类数组,并考虑了更多边缘情况(可能最重要的是当多个类别在新代码中合并为一个时的情况)。
广播的unwrap然后由Julia能够优化得非常好的简单查找池组成。
基准测试 Bogumił Kamiński的第一个解决方案@btime recode(unwrap.($a), "X"=>1, "Y"=>2, "Z"=>3) 中译英:
length btime result
100 1.268 μs (5 allocations: 1.84 KiB)
1000 14.872 μs (5 allocations: 15.97 KiB)
10000 151.881 μs (7 allocations: 156.50 KiB)

Bogumił Kamiński's second solution:

@btime [$mapping[v] for v in $a]

长度 btime 结果
100 2.439 微秒(101 次分配:4.00 KiB)
1000 23.715 微秒(1001 次分配:39.19 KiB)
10000 240.292 微秒(10002 次分配:390.70 KiB)

Milan Bouchet-Valat's solution:

@btime recode!(similar($a, Int), $a, "X"=>1, "Y"=>2, "Z"=>3)

中译英:
长度 btime 结果
100 2.158 μs(104 次分配:4.09 KiB)
1000 21.347 μs(1004 次分配:39.28 KiB)
10000 208.035 μs(10005 次分配:390.80 KiB)

此解决方案:

@btime unwrap.(recode($a, "X"=>1, "Y"=>2, "Z"=>3))

长度 btime 结果
100 2.360 微秒(45 次分配:4.56 KiB)
1000 4.420 微秒(45 次分配:15.20 KiB)
10000 20.212 微秒(47 次分配:120.55 KiB)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接