Zero's Blog - 2022年4月 https://l2dy.sourceforge.io/2022/04/ zh-CN Sat, 30 Apr 2022 16:59:00 +0000 Sat, 30 Apr 2022 16:59:00 +0000 编译 R for Data Science https://l2dy.sourceforge.io/2022/04/30/r4ds-build-book.html https://l2dy.sourceforge.io/2022/04/30/r4ds-build-book.html Sat, 30 Apr 2022 16:59:00 +0000 Zero 首先用 conda 创建一个新环境并安装 R。

conda create -n r4ds_r4.1 r=4.1 r-devtools r-terra
conda activate r4ds_r4.1
rstudio

然后在 RStudio 的 Console 中执行以下命令,用 devtools 安装编译 r4ds 所需的包。

devtools::install_github("hadley/r4ds", dependencies = TRUE)

最后在 RStudio 中打开 r4ds.Rproj 项目,在菜单里选择 Build → Build All。

Update(2022-05-10): 用 conda 预先安装 DESCRIPTION 文件中提到的包可以减少 devtools 安装命令的耗时,因为可以跳过一部分包的编译。

]]>
0 https://l2dy.sourceforge.io/2022/04/30/r4ds-build-book.html#comments https://l2dy.sourceforge.io/feed/2022/04/
MacPorts distfiles 机制替代 Git 子模块 https://l2dy.sourceforge.io/2022/04/30/macports-distfiles-without-git-submodule.html https://l2dy.sourceforge.io/2022/04/30/macports-distfiles-without-git-submodule.html Sat, 30 Apr 2022 15:58:00 +0000 Zero 虽然最理想的情况是所有依赖的项目都分别打包到不同的 port,但不同包对同一个依赖项目可能有不兼容的要求。OpenSSL、Boost 这种基础包可以通过拆包来解决兼容问题,例如 Boost 在 MacPorts 就打包了 boost169boost171boost173 等等很多个版本,但这也要求所有这些子包的使用方都要对编译 flags 进行修改,才能让编译器找到正确版本的依赖,导致整个过程非常复杂,维护也非常耗费时间。

一般使用方不多的库不会有分版本的 subport,为了提供一个小众软件要求的版本实现多版本并存有点得不偿失。此时如果项目的构建系统本来就把依赖 vendor 到 Git 子模块里,我们就可以通过 distfiles 的手段把依赖库的源代码拉下来放到子模块的文件夹里。通过 master_sitesdistfiles-appendgithub 1.0 等关键字进行组合搜索可以找到 macports-ports 中可以参考的方案,最终效果如下。

PortGroup           github  1.0
PortGroup           meson   1.0

github.setup        joshkunz ashuffle 3.13.3 v
github.tarball_from archive
master_sites        ${github.master_sites}:ashuffle
distfiles           ${distname}${extract.suffix}:ashuffle

...

# BEGIN abseil (requires C++17 build)
set abseil_project  abseil-cpp
set abseil_version  20211102.0

master_sites-append https://github.com/abseil/${abseil_project}/archive/${abseil_version}:abseil
distfiles-append    ${abseil_project}-${abseil_version}${extract.suffix}:abseil
checksums-append    ${abseil_project}-${abseil_version}${extract.suffix} \
                    rmd160  bca4a16eaab1602cdc7ace8dd1ff82467b71b59e \
                    sha256  dcf71b9cba8dc0ca9940c4b316a0c796be8fab42b070bb6b7cab62b48f0e66c4 \
                    size    1884080

post-extract {
    foreach submodule [list subprojects/absl:${abseil_project}] {
        set submodule_target [lindex [split ${submodule} :] 0]
        set submodule_package [lindex [split ${submodule} :] 1]
        delete ${worksrcpath}/${submodule_target}
        move {*}[glob ${workpath}/${submodule_package}-*] ${worksrcpath}/${submodule_target}
    }
}
# END abseil
]]>
0 https://l2dy.sourceforge.io/2022/04/30/macports-distfiles-without-git-submodule.html#comments https://l2dy.sourceforge.io/feed/2022/04/
R 时间戳转换为日期 https://l2dy.sourceforge.io/2022/04/24/r-datetime-to-date.html https://l2dy.sourceforge.io/2022/04/24/r-datetime-to-date.html Sun, 24 Apr 2022 15:30:27 +0000 Zero 如果时间戳(date-time)都是 UTC 时区的话,可以使用 base 的 as.Date 函数。

如果时间戳时区不一致或非 UTC 时区,用 lubridate 包的 as_date 可以获得在对应时区下的日期,但性能比 as.Date 稍差。

]]>
0 https://l2dy.sourceforge.io/2022/04/24/r-datetime-to-date.html#comments https://l2dy.sourceforge.io/feed/2022/04/
ggplot2 画百分比饼图 https://l2dy.sourceforge.io/2022/04/22/ggplot2-pie-chart-with-percentage.html https://l2dy.sourceforge.io/2022/04/22/ggplot2-pie-chart-with-percentage.html Fri, 22 Apr 2022 15:28:00 +0000 Zero 首先通过 count() 计算出各种 cut 的钻石各有多少个,存到名为 n 的列里,然后通过 mutate 计算出总和为1的占比。此时如果用 geom_col() 画图可以得出一个高度为1的堆积图。

library(tidyverse)

diamonds %>%
  count(cut) %>%
  mutate(p = n / sum(n)) %>%
  ggplot(aes("", p, fill = cut)) +
  geom_col() +
  scale_fill_brewer(type = "div")

p1.png

再加以 coord_polar() 变换转换为极座标,就可以画出一张饼图了。在此基础上多叠加一层 geom_text() 就实现了百分比的显示,非常灵活。

diamonds %>%
  count(cut) %>%
  mutate(p = n / sum(n)) %>%
  mutate(labels = scales::label_percent(accuracy = 0.01)(n / sum(n))) %>%
  ggplot(aes("", p, fill = cut)) +
  geom_col() +
  geom_text(aes(label = labels), position = position_stack(0.5)) +
  coord_polar("y") +
  scale_fill_brewer(type = "div")

p2.png

如果不想显示极座标轴和灰色背景,可以通过加上 theme_void() 来实现。对于重叠的文字则是可以用 ggrepel 打散,最终效果如图。

p3.png

]]>
0 https://l2dy.sourceforge.io/2022/04/22/ggplot2-pie-chart-with-percentage.html#comments https://l2dy.sourceforge.io/feed/2022/04/
展开 Tiddle 中的 JSON 字段 https://l2dy.sourceforge.io/2022/04/20/tidyverse-unnest-json.html https://l2dy.sourceforge.io/2022/04/20/tidyverse-unnest-json.html Wed, 20 Apr 2022 15:47:20 +0000 Zero tidyr 本身就提供了 unnest() 系列函数,可以用于展开嵌套的数据结构。配合 fromJSON() 使用就可以实现 JSON 字段的展开。

library(tidyverse)
library(jsonlite)

spread_all <- function(data, cols) {
  data %>%
    mutate(across(all_of(cols), function(x)
      map(x, fromJSON))) %>%
    unnest_wider(all_of(cols))
}

# 用法
df %>%
  spread_all(c("x", "y"))

为了加速 JSON 的解析,我们可以引入 furrr 包来实现并发解析。

library(furrr)
# make future_map parallel
plan(multisession)

spread_all <- function(data, cols) {
  data %>%
    mutate(across(all_of(cols), function(x)
      future_map(x, fromJSON))) %>%
    unnest_wider(all_of(cols))
}
]]>
0 https://l2dy.sourceforge.io/2022/04/20/tidyverse-unnest-json.html#comments https://l2dy.sourceforge.io/feed/2022/04/
R Markdown 跳过中间变量缓存结果 https://l2dy.sourceforge.io/2022/04/17/knitr-cache-skip-intermediate.html https://l2dy.sourceforge.io/2022/04/17/knitr-cache-skip-intermediate.html Sun, 17 Apr 2022 04:04:00 +0000 Zero 当要从一个很大的数据集中取子集进行分析,且不想生成中间状态的 csv 文件只用于快速生成报告时,你就需要用到 knitr 的缓存功能了。

knitr 默认的缓存就是 lazy 的,所以如果后面的代码只使用了这个子集的数据,直接在代码块上加上 cache = TRUE 配置就可以避免每次渲染都加载一次这个大数据集。如果这个代码块的数据和输出格式没有相关性的话,建议还加上 cache.path = "cache/" 这个配置,可以在切换输出格式的情况下也共用缓存。缓存的自动刷新条件可以用 cache.extra 控制,具体用法可参考 rmarkdown-cookbook 的文档。

然而,在数据集非常大的情况下,你可能会碰到 long vectors not supported yet 这个报错。这种情况下 knitr 默认的缓存机制已经不能满足需求了,我们需要更灵活的可以定制化的缓存。此时我们可以用 xfun::cache_rds() 来实现:

```{r res, cache.path = "cache/"}
res <- xfun::cache_rds({
  cars <- reda_csv(files)
  cars %>%
    filter(model = y)
}, name = "res.rds", hash = list(files, y))
```

xfun::cache_rds() 的第一个参数是要缓存的表达式,在首次执行完成后结果会被缓存并赋值给 res,再下一次执行时就可以直接从缓存中加载变量赋值给 res 了。

name 参数用于在非 knitr 环境(例如在 RStudio 中跑代码块)下和 knitr 环境复用同一个缓存文件,文件名需要和代码块的标签保持一致。注意还需要保证两种环境下 dir 参数一致,在 knitr 环境下 dir 参数默认值为 cache.path 的值,非 knitr 环境下默认为 cache/。这也是使用 xfun::cache_rds() 独有的优势,可以在 RStudio 的 notebook 环境下复用 knitr 的缓存。

hash 参数会影响缓存的文件名,所以一旦 hash 的内容发生变化就会重新计算,相当于 cache.extra

这样就实现了跳过中间生成的大数据集 cars 直接缓存最终结果,非常方便。

Update (2022-04-22): 截至目前 RStudio 仍未支持在 R Notebook 环境下复用 knitr 缓存,见 rstudio/rstudio#9291

]]>
0 https://l2dy.sourceforge.io/2022/04/17/knitr-cache-skip-intermediate.html#comments https://l2dy.sourceforge.io/feed/2022/04/
Tidyverse 读取多个同格式 csv 文件 https://l2dy.sourceforge.io/2022/04/17/tidyverse-read-multiple-csv-files.html https://l2dy.sourceforge.io/2022/04/17/tidyverse-read-multiple-csv-files.html Sun, 17 Apr 2022 03:44:00 +0000 Zero 如何将多个同样格式的 csv 文件合并读取到一个 tibble 里呢?很自然的会想到先用 read_csv() 分别读取每个文件,再用 reduce(rbind) 聚合到一起。这样确实能用,但对于大数据集来说性能很差。

readr 从 2.0.0 开始原生支持同时读取多个文件的功能,只需要把文件名字符串向量里传给 file 参数就行。实测在数据量很大的时候和 rbind 相比可以显著减少读取时间。这个功能在 readr 包的文档中没有描述,导致我走了不少弯路才发现 readr 原生就支持多文件读取。

另外 vroom 从 1.0.0 开始就原生支持了这个功能,用法一样不过 vroom 对于文件中的字符数据是懒加载的,在某些场景下可以提升性能。具体能否提升性能要看使用方式了,可以用 system.time({}) 实测一下。

]]>
0 https://l2dy.sourceforge.io/2022/04/17/tidyverse-read-multiple-csv-files.html#comments https://l2dy.sourceforge.io/feed/2022/04/
MacPorts 安装 Mamba 环境 https://l2dy.sourceforge.io/2022/04/08/bootstrap-mamba-with-macports.html https://l2dy.sourceforge.io/2022/04/08/bootstrap-mamba-with-macports.html Fri, 08 Apr 2022 15:11:00 +0000 Zero micromamba

首先使用 MacPorts 安装 micromamba$HOME/micromamba 并初始化 shell 配置。

sudo port install micromamba
micromamba shell init --shell=zsh --prefix=~/micromamba

执行完成后需要重开 shell 以加载 .zshrc,如果使用的 shell 不是 zsh 需要对应调整 --shell 参数。

此时 micromamba 已安装完成。如果不需要完整的 conda 功能,到这里就可以使用 micromamba 命令了。

mamba

为了兼容 conda 可以用 micromamba 安装 mamba 并重新初始化 shell 配置。

micromamba install mamba -n base -c conda-forge
mamba init zsh

重开一次 shell 之后就有完整的 mamba 环境了,可以运行 mambaconda 命令,但 activate 命令还需要用 micromamba

环境

除了安装 mamba 外,不建议使用 base 环境安装其他包。以下命令可以新创建一个名叫 sci 的环境并安装。

mamba create -n sci <list of packages>

如果觉得默认源包太少或太旧,可以启用 conda-forge 并设置为默认。源配置的调整建议在新环境创建前完成,否则依赖关系可能出问题。

conda config --add channels conda-forge
conda config --set channel_priority strict
]]>
0 https://l2dy.sourceforge.io/2022/04/08/bootstrap-mamba-with-macports.html#comments https://l2dy.sourceforge.io/feed/2022/04/
Rime 四叶草方案字频分布分析 https://l2dy.sourceforge.io/2022/04/05/word-freq.html https://l2dy.sourceforge.io/2022/04/05/word-freq.html Tue, 05 Apr 2022 04:07:00 +0000 Zero 说到输入法当然要用开源的了,不然谁知道自己的打字记录会在云端保留多久。我在用的是 Rime 输入法+四叶草拼音输入方案+小鹤双拼,不过四叶草字库里生僻字太多,翻页翻过头时就全变成无法识别的方块了(没有安装支持过于生僻字的字体)。

字的频度值越低,说明字越生僻。所以可以设置一个最低频度来控制候选词里的生僻字数。先把频度值都取出来:

awk -F\\t '$3!="" {print $3}' data/clover.base.dict.yaml > freq.txt

数据分析用 Jupyter 笔记本比较方便。安装上 VSCode 的 Jupyter 插件,在 VSCode 里选择有 seaborn 和 ipykernel 包的 Python 环境就可以开始分析了。

首先把需要的不需要的依赖都导入进来,免得后面再找。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

然后把之前提取出的词频文件用 pandas 导入

fl = pd.read_csv('freq.txt', names=['cnt'], header=None)
df = fl['cnt']

成功后就可以开始画图分析了,先画个分布图。

sns.displot(df)

然而执行了好几分钟还没跑出来结果。回头看一眼数据,最大值 579392145,最小值 0。这个分布太散直接拿来画图确实不合理,那就取个对数试试。为了后面好过滤就以 10 为底吧。

df_log = df.transform(np.log10)
sns.displot(df_log)

output1.png

这次 OK 了,可以看出个位数频度里有不少字,放大看下

df_less_than_10 = df.where(lambda x : x < 10).dropna()
sns.displot(df_less_than_10)

output2.png

三万多字集中在 1 上,回到 yaml 文件里一看全是生僻字,直接过滤了吧。

df_log_main = df_log.where(lambda x : x > 1).dropna()
sns.displot(df_log_main)

output3.png

这就好看多了。大部分字还是集中在 10e4 到 10e6 范围内。那就从频度 10000 往下翻看有没有需要保留的字。

$ cd data
$ less clover.base.dict.yaml
<用 ^I...$ 搜索定位到 1000 以下>
$ grep '扁' clover.base.dict.yaml
扁    bian    255939
扁    pian    661

看来保留到 600 字频应该就差不多了,剩下的要么是生僻字,要么是多音字很少见的读音。

diff --git a/src/clover-dict-gen b/src/clover-dict-gen
index 4ef8716..87b74a8 100755
--- a/src/clover-dict-gen
+++ b/src/clover-dict-gen
@@ -186,12 +186,13 @@ class DictGenerator:
         return (word_count, parse_count)
 
 
-    def getWordDictText(self):
+    def getWordDictText(self, min_freq = -1):
         """
             生成单字的 rime 字典文本
         """
         # 按频率倒序排序
         word_list = [(key[0], key[1], self.word_dict[key]) for key in self.word_dict]
+        word_list = list(filter(lambda w: w[2] > min_freq, word_list))
         word_list.sort(key = lambda w: w[2], reverse = True)
 
         # 生成文本
@@ -292,7 +293,7 @@ name: %s
 version: "1.0.0"
 sort: by_weight
 ...
-""" % word_dict_name + generator.getWordDictText()
+""" % word_dict_name + generator.getWordDictText(600)
 
     parse_dict_text = """
 # Rime 字典

搞定,重新 build 一下就好了。

]]>
0 https://l2dy.sourceforge.io/2022/04/05/word-freq.html#comments https://l2dy.sourceforge.io/feed/2022/04/
本地搭建多节点 k8s 测试集群 https://l2dy.sourceforge.io/2022/04/04/k8s-multinode.html https://l2dy.sourceforge.io/2022/04/04/k8s-multinode.html Mon, 04 Apr 2022 17:23:00 +0000 Zero 目前搭建本地 k8s 集群的方案还挺多的,有官方的 minikube,也有第三方的 k3sk3dKubeKey 等等。简单的测试其实用 k3s 搭个单节点测试就好,但遇到有反亲和规则的 Helm chart 就拉不起来了,所以要么多弄几台机器做分布式集群,要么就用套娃式的虚拟化方案做出多节点来。

现在手头就一台高配物理机,套娃就套娃吧。一开始看 minikube 在 https://github.com/kubernetes 下应该很靠谱才对,但是一个 MySQL 数据库都没拉起来,看报错是文件权限问题。不过 bitnami 的 chart 文档里已经给出了解决方案,启用 volumePermissions.enabled 就可以自动添加一个初始化容器执行 chmod。看起来还行,我把相关子 chart 都启用了这个配置,helm install 也成功了。可这 chart 它不讲武德,竟然在最后的 init 容器里又执行了一次 helm install,比我还能套娃。

这套娃 chart 代码在容器里不是很好改,还是老老实实看怎么让 fsGroup 生效吧。谷歌一下找到 #1990 (comment) 这个 issue,k8s 的 hostPath PV 实现不支持用 fsGroup 配置权限,minikube 和 k3d (rancher/local-path-provisioner#41) 同时中招。

查了好久也没找到合适的方案,看来只能试试非正当手段了。能不能在 hostPath 创建好目录之后,马上把权限改对呢?脏是脏了点,但能抓住老鼠就是好猫。正好 k3d 创建集群时能指定 --volume,直接把宿主机上的目录 mount 进去,然后 for 循环 chmod,总算是顺利把套娃的 helm install 也搞定了。

后记

全都搞定之后才发现 OpenEBS 似乎能解决问题,KubeKey All-in-One 模式也能一键安装,不过懒得折腾了。

后来还发现 minikube 提供的 StorageClass 还不支持多节点集群 (#12165),官方这么惨大家快去欺负(帮忙)。

]]>
0 https://l2dy.sourceforge.io/2022/04/04/k8s-multinode.html#comments https://l2dy.sourceforge.io/feed/2022/04/