暗无天日

=============>DarkSun的个人博客

使用awk查找并修复数据中一对多的不一致问题

https://www.datafix.com.au/BASHing/2021-03-17.html 上看到的一个 awk 小技巧。

所谓“一对多”的不一致问题是指这么一种情况:属性1与属性2本来应该是 1:1 或者 N:1 的关系,但是由于数据错误导致同一个属性1有了多个属性2与之对应。

例如下面数据中, 每个 item 本来应该只有唯一的一个 class,但是实际上 bananapotato 有多个 class 与之对应。

saleID	date	item	class	kg
001	2021-01-02	capsicum	vegetable	11.9
002	2021-01-02	banana	fruit	12.7
003	2021-01-02	capsicum	vegetable	3.7
004	2021-01-02	potato	vegetable	4.1
005	2021-01-02	capsicum	vegetable	6.0
006	2021-01-02	potato	fruit	13.0
007	2021-01-02	banana	vegetable	9.1
008	2021-01-02	potato	vegetable	15.0
009	2021-01-02	apple	fruit	5.6
010	2021-01-02	banana	fruit	7.7
011	2021-01-02	pumpkin	vegetable	8.3
012	2021-01-02	pumpkin	vegetable	5.6
013	2021-01-02	apple	fruit	3.5
014	2021-01-02	pumpkin	vegetable	5.3
015	2021-01-02	capsicum	vegetable	10.3
016	2021-01-03	apple	fruit	12.2
017	2021-01-03	pumpkin	vegetable	12.6
018	2021-01-03	potato	vegetable	4.4
019	2021-01-03	apple	fruit	12.5
020	2021-01-03	pumpkin	vegetable	11.6
021	2021-01-03	banana	vegetable	14.5
022	2021-01-03	capsicum	vegetable	4.1
023	2021-01-03	banana		5.9
024	2021-01-03	potato	vegetable	4.8
025	2021-01-03	apple	fruit	15.6

为了找出一对多的关系,我们需要使用二维数组来保存 属性1属性2 的关系,然后判断 属性1 这个一维数组中是否包含数组个数大于1的数值就行了(原文的判断方法比这要复杂,我简化了一下)。awk 程序如下

awk '{b[$3][$4]++}
END {for (i in b) {if (length(b[i])>1){
            for (j in b[i]){
                print b[i][j] "|" i FS j}}}}' /tmp/test.txt

4|potato vegetable
1|potato fruit
2|banana vegetable
1|banana 5.9
2|banana fruit

这里第一列是每个 属性1 属性2 对的数量,第二列是重复的 属性1 属性2 对的值。

为了方便复用,我们可以定义一个 one2many 的函数:

one2many() {
    sep="$1"                    # 数据分隔符
    one="$2"                    # 属性1
    many="$3"                   # 属性2
    file="$4"                   # 数据文件路径
    awk -F"${sep}" -v one="${one}" -v many="${many}" '$one != "" {b[$one][$many]++}
END {for (i in b)
         {if (length(b[i]) > 1) {
            for (j in b[i])
                {print b[i][j] FS i FS j}}}}' "${file}"
}

这个函数与上面命令不同之处在于通过 -F 指定数据分隔符,通过 -v 将函数参数传递给 awk 变量,使用 FS 变量替代 | 作为数量与重复属性对之间的分隔符。

那么,我们可以直接使用该函数进行数据检测:

<<one2many>>
one2many " " 3 4 /tmp/test.txt
4 potato vegetable
1 potato fruit
2 banana vegetable
1 banana 5.9
2 banana fruit

修复相对来说就比较简单了,我们首先创建一个查询表,用来查询 属性1 对应的 属性2,这个查询可以从 one2many 函数的结果中截取:

<<one2many>>
one2many " " 3 4 /tmp/test.txt |sed -n '1p;5p' |tee /tmp/lookup
4 potato vegetable
2 banana fruit

然后在 awk 中查询 属性1 的值若在查询表中则将 属性2 的值直接改为查询表中的对应结果:

awk 'FNR==NR {lookup[$1]=$2; next} # 通过FNR==NR判断是否在遍历查询表文件
$3 in lookup {$4=lookup[$3]} 1' /tmp/lookup /tmp/test.txt # awk 最后那个1表示执行awk 的默认操作,即输出$0
saleID  date    item    class   kg
001     2021-01-02      capsicum        vegetable       11.9
002     2021-01-02      banana  fruit   12.7
003     2021-01-02      capsicum        vegetable       3.7
004     2021-01-02      potato  vegetable       4.1
005     2021-01-02      capsicum        vegetable       6.0
006     2021-01-02      potato  fruit   13.0
007     2021-01-02      banana  vegetable       9.1
008     2021-01-02      potato  vegetable       15.0
009     2021-01-02      apple   fruit   5.6
010     2021-01-02      banana  fruit   7.7
011     2021-01-02      pumpkin vegetable       8.3
012     2021-01-02      pumpkin vegetable       5.6
013     2021-01-02      apple   fruit   3.5
014     2021-01-02      pumpkin vegetable       5.3
015     2021-01-02      capsicum        vegetable       10.3
016     2021-01-03      apple   fruit   12.2
017     2021-01-03      pumpkin vegetable       12.6
018     2021-01-03      potato  vegetable       4.4
019     2021-01-03      apple   fruit   12.5
020     2021-01-03      pumpkin vegetable       11.6
021     2021-01-03      banana  vegetable       14.5
022     2021-01-03      capsicum        vegetable       4.1
023     2021-01-03      banana          5.9
024     2021-01-03      potato  vegetable       4.8
025     2021-01-03      apple   fruit   15.6