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) |