基于唯一 ID 转置数据 - awk
Transposing data based on unique ID - awk
我真的希望你能帮上忙。我是 (g)awk 的新手,过去两周我一直在与它作斗争。
我的原始文件如下 - 有一列具有唯一 ID,另一列具有唯一名称。随后的列是各种课程,每个字段包含(当不为空时)每门课程和每个学生的标记。所以每个学生每门课程只有一个分数:
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55
2 George 63
4 Alex 64
1 John 74
3 Emma 63
2 George 64
4 Alex 60
2 George 29
3 Emma 69
1 John 67
3 Emma 80
4 Alex 57
2 George 91
1 John 81
1 John 34
3 Emma 75
2 George 89
4 Alex 49
3 Emma 78
4 Alex 69
5 TERRY 67
6 HELEN 39
这就是我想要实现的 - 根据唯一 ID 转置数据,即标记,并将标记放在每个相应课程的下方,如下所示:
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55 69 64 60 49
2 George 29 64 89 91 63
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
5 TERRY 67
6 HELLEN 39
这是我到目前为止设法得到的:
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55
2 George 29
3 Emma 63
4 Alex 57
5 TERRY
6 HELLEN
1 John 69
2 George 64
3 Emma 80
4 Alex 69
5 TERRY 67
6 HELLEN
1 John 64
2 George 89
3 Emma 75
4 Alex 64
5 TERRY
6 HELLEN 39
...and so on
根据我在 awk 上已经知道的知识来实现对我来说真的有点棘手(请注意,我对基于 sed/perl e.t.c. 的解决方案不感兴趣)。
如果要提供一些帮助(最好不是单行),我可能会要求描述一下,因为我对解决方案和方法本身都很感兴趣。
非常感谢任何帮助。
编辑
这是我为到达最后阶段(以及卡住的地方)而编写的代码
#!/bin/bash
files3="*.csv"
for j in $files3
do
#echo "processing $j..."
fi13=$(awk -F" " '(NR==1){field13=;}{print field13}' ./work1/test1YA.csv)
fi14=$(awk -F" " '(NR==1){field14=;}{print field14}' ./work1/test1YA.csv)
fi15=$(awk -F" " '(NR==1){field15=;}{print field15}' ./work1/test1YA.csv)
fi16=$(awk -F" " '(NR==1){field16=;}{print field16}' ./work1/test1YA.csv)
# awk -F" " 'BEGIN{OFS=" ";RS="\n"}{print ,,,,,,,,,,,}' "$j" >> ./work1/test2YA.csv
awk -F" " -v f13="$fi13" -v f14="$fi14" -v f15="$fi15" -v f16="$fi16" '{if(==f13){=;===""}if(==f14){=;===""}if(==f15){=;===""}if(==f16){=;===""}{print ,,,,,,,,,,,,,,,}}' "$j" >> ./work1/test2YA.csv
done;
awk -F" " 'BEGIN{print "ID","Title","FirstName","MiddleName","LastName","FinalMarks","Status","Username","Campus","Code","Programme","Year","course1","course2","course3","course4"}{print}' ./work1/test2YA.csv >> ./work1/test3YA.csv
这是 gnu awk 的解决方案:
course.awk
BEGIN { # setup field width for constant field splitting
FIELDWIDTHS = "2 2 12 7 1 7 1 7 1 7 1 7"
# setup sort order (by id)
PROCINFO["sorted_in"] = "@ind_num_asc"
}
NR == 1 { # print header
print
next
}
{
# add ids to names
names[ ] =
# store under id and course number the mark if it is present
for( c = 1; c <= 5; c++ ) {
field = 2+ (c*2)
if( $(field) !~ /^ *$/ ) {
marks[ , c ] = $(field)
}
}
}
END {
# output
for( id in names ) {
printf("%-4s%-12s%7s %7s %7s %7s %7s\n",id, names[ id ], marks[ id, 1], marks[ id, 2], marks[ id, 3], marks[ id, 4], marks[ id, 5])
}
}
这样使用:awk -f course.awk your_file
.
输入不是制表符分隔,而是固定列宽的事实有点不雅:
- 使用
FIELDWIDTHS
和 %Ns
,其中 N 从 FIELDWIDTHS 派生
- FIELDWIDTHS 考虑到 ID 和名称、Course1 和 Course2 之间的空列,...
- 检查是否存在标记:
if( $(field) !~ /^ *$/ )
检查字段是否不完全由空格组成。
这可能是 awk 中的近似值:
NR==1{
for(x=1;x<=NF;x++)
{
head=head $x"\t";
}
print head
}
NR>1{
for(i=3;i<=NF;i++)
{
students["\t"]=students["\t"] "\t"$i;
}
}
END{
for (stu in students)
{
print stu,students[stu];
}
}
Id Name Course1 Course2 Course3 Course4 Course5
5 TERRY 67
4 Alex 64 60 57 49 69
1 John 55 74 67 81 34
6 HELEN 39
3 Emma 63 69 80 75 78
2 George 63 64 29 91 89
相同的想法,也许更简单
$ awk 'BEGIN{ FIELDWIDTHS="16 8 8 8 8 8"}
NR==1{print;next}
NR>1{keys[];
for(i=2;i<=6;i++)
{gsub(" ","",$i);
if($i) a[,i]=$i}}
END{for(k in keys)
{printf "%16s",k;
for(i=2;i<=6;i++) printf "%-8s",a[k,i];
print ""}}' file
Id Name Course1 Course2 Course3 Course4 Course5
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
6 HELEN 39
5 TERRY 67
1 John 55 67 81 74 34
2 George 29 64 89 91 63
您也可以通过管道对输出进行排序 sort -n
... | sort -n
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55 67 81 74 34
2 George 29 64 89 91 63
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
5 TERRY 67
6 HELEN 39
使用 GNU awk 用于 FIELDWIDTHS、二维数组和 sorted_in:
$ cat tst.awk
NR==1 {
print
split([=10=],f,/\S+\s*/,s)
for (i=1;i in s;i++) {
w[i] = length(s[i])
FIELDWIDTHS = FIELDWIDTHS (i>1?" ":"") w[i]
}
next
}
{
sub(/\s*$/," ")
for (i=1;i<=NF;i++) {
if ($i ~ /\S/) {
val[][i] = $i
}
}
}
END {
PROCINFO["sorted_in"] = "@ind_num_asc"
for (id in val) {
for (i=1;i<=NF;i++) {
printf "%*s", w[i], val[id][i]
}
print ""
}
}
.
$ awk -f tst.awk file
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55 67 81 74 34
2 George 29 64 89 91 63
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
5 TERRY 67
6 HELEN 39
这是我对此的看法。这适用于普通的 awk(不使用 FIELDWIDTHS),它会自动调整到不同数量的字段(即添加一个 Course7
列,你应该没问题)。此外,您可以将它指向多个文件,它应该分别处理每个文件。
#!/usr/bin/awk -f
# Initialize variables on the first record of each input file
# (and also print the header)
#
FNR <= 1 {
print
delete name
delete score
next
}
# Process each line.
#
{
id = substr([=10=], 0, 16) #
name[id] # Store the unique identifier in an array
pos = 0 #
# Step through the score fields until we hit the end of the line,
# storing scores in another array.
do {
score[id, pos] += substr([=10=],17+pos*8,8) +0
printf("id='%s' pos=%s value=%s total=%s\n", id, pos, substr([=10=],17+pos*8,8)+0, score[id, pos] );
} while (17+(++pos)*8 < length())
}
# Keep track of our maximum number of fields
pos>max { max=pos }
# Finally, generate our (randomly sorted) output.
END {
for (id in name) { # Step through the records...
printf("%-12s", id);
for (i=0; i<max; i++) { # Step through the fields...
if (score[id, i]==0) score[id, i]=""
printf("%-8s", score[id, i]);
}
printf("\n")
}
}
它有点长,但我认为更容易理解它的作用。
我真的希望你能帮上忙。我是 (g)awk 的新手,过去两周我一直在与它作斗争。
我的原始文件如下 - 有一列具有唯一 ID,另一列具有唯一名称。随后的列是各种课程,每个字段包含(当不为空时)每门课程和每个学生的标记。所以每个学生每门课程只有一个分数:
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55
2 George 63
4 Alex 64
1 John 74
3 Emma 63
2 George 64
4 Alex 60
2 George 29
3 Emma 69
1 John 67
3 Emma 80
4 Alex 57
2 George 91
1 John 81
1 John 34
3 Emma 75
2 George 89
4 Alex 49
3 Emma 78
4 Alex 69
5 TERRY 67
6 HELEN 39
这就是我想要实现的 - 根据唯一 ID 转置数据,即标记,并将标记放在每个相应课程的下方,如下所示:
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55 69 64 60 49
2 George 29 64 89 91 63
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
5 TERRY 67
6 HELLEN 39
这是我到目前为止设法得到的:
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55
2 George 29
3 Emma 63
4 Alex 57
5 TERRY
6 HELLEN
1 John 69
2 George 64
3 Emma 80
4 Alex 69
5 TERRY 67
6 HELLEN
1 John 64
2 George 89
3 Emma 75
4 Alex 64
5 TERRY
6 HELLEN 39
...and so on
根据我在 awk 上已经知道的知识来实现对我来说真的有点棘手(请注意,我对基于 sed/perl e.t.c. 的解决方案不感兴趣)。 如果要提供一些帮助(最好不是单行),我可能会要求描述一下,因为我对解决方案和方法本身都很感兴趣。
非常感谢任何帮助。
编辑 这是我为到达最后阶段(以及卡住的地方)而编写的代码
#!/bin/bash
files3="*.csv"
for j in $files3
do
#echo "processing $j..."
fi13=$(awk -F" " '(NR==1){field13=;}{print field13}' ./work1/test1YA.csv)
fi14=$(awk -F" " '(NR==1){field14=;}{print field14}' ./work1/test1YA.csv)
fi15=$(awk -F" " '(NR==1){field15=;}{print field15}' ./work1/test1YA.csv)
fi16=$(awk -F" " '(NR==1){field16=;}{print field16}' ./work1/test1YA.csv)
# awk -F" " 'BEGIN{OFS=" ";RS="\n"}{print ,,,,,,,,,,,}' "$j" >> ./work1/test2YA.csv
awk -F" " -v f13="$fi13" -v f14="$fi14" -v f15="$fi15" -v f16="$fi16" '{if(==f13){=;===""}if(==f14){=;===""}if(==f15){=;===""}if(==f16){=;===""}{print ,,,,,,,,,,,,,,,}}' "$j" >> ./work1/test2YA.csv
done;
awk -F" " 'BEGIN{print "ID","Title","FirstName","MiddleName","LastName","FinalMarks","Status","Username","Campus","Code","Programme","Year","course1","course2","course3","course4"}{print}' ./work1/test2YA.csv >> ./work1/test3YA.csv
这是 gnu awk 的解决方案:
course.awk
BEGIN { # setup field width for constant field splitting
FIELDWIDTHS = "2 2 12 7 1 7 1 7 1 7 1 7"
# setup sort order (by id)
PROCINFO["sorted_in"] = "@ind_num_asc"
}
NR == 1 { # print header
print
next
}
{
# add ids to names
names[ ] =
# store under id and course number the mark if it is present
for( c = 1; c <= 5; c++ ) {
field = 2+ (c*2)
if( $(field) !~ /^ *$/ ) {
marks[ , c ] = $(field)
}
}
}
END {
# output
for( id in names ) {
printf("%-4s%-12s%7s %7s %7s %7s %7s\n",id, names[ id ], marks[ id, 1], marks[ id, 2], marks[ id, 3], marks[ id, 4], marks[ id, 5])
}
}
这样使用:awk -f course.awk your_file
.
输入不是制表符分隔,而是固定列宽的事实有点不雅:
- 使用
FIELDWIDTHS
和%Ns
,其中 N 从 FIELDWIDTHS 派生
- FIELDWIDTHS 考虑到 ID 和名称、Course1 和 Course2 之间的空列,...
- 检查是否存在标记:
if( $(field) !~ /^ *$/ )
检查字段是否不完全由空格组成。
这可能是 awk 中的近似值:
NR==1{
for(x=1;x<=NF;x++)
{
head=head $x"\t";
}
print head
}
NR>1{
for(i=3;i<=NF;i++)
{
students["\t"]=students["\t"] "\t"$i;
}
}
END{
for (stu in students)
{
print stu,students[stu];
}
}
Id Name Course1 Course2 Course3 Course4 Course5
5 TERRY 67
4 Alex 64 60 57 49 69
1 John 55 74 67 81 34
6 HELEN 39
3 Emma 63 69 80 75 78
2 George 63 64 29 91 89
相同的想法,也许更简单
$ awk 'BEGIN{ FIELDWIDTHS="16 8 8 8 8 8"}
NR==1{print;next}
NR>1{keys[];
for(i=2;i<=6;i++)
{gsub(" ","",$i);
if($i) a[,i]=$i}}
END{for(k in keys)
{printf "%16s",k;
for(i=2;i<=6;i++) printf "%-8s",a[k,i];
print ""}}' file
Id Name Course1 Course2 Course3 Course4 Course5
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
6 HELEN 39
5 TERRY 67
1 John 55 67 81 74 34
2 George 29 64 89 91 63
您也可以通过管道对输出进行排序 sort -n
... | sort -n
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55 67 81 74 34
2 George 29 64 89 91 63
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
5 TERRY 67
6 HELEN 39
使用 GNU awk 用于 FIELDWIDTHS、二维数组和 sorted_in:
$ cat tst.awk
NR==1 {
print
split([=10=],f,/\S+\s*/,s)
for (i=1;i in s;i++) {
w[i] = length(s[i])
FIELDWIDTHS = FIELDWIDTHS (i>1?" ":"") w[i]
}
next
}
{
sub(/\s*$/," ")
for (i=1;i<=NF;i++) {
if ($i ~ /\S/) {
val[][i] = $i
}
}
}
END {
PROCINFO["sorted_in"] = "@ind_num_asc"
for (id in val) {
for (i=1;i<=NF;i++) {
printf "%*s", w[i], val[id][i]
}
print ""
}
}
.
$ awk -f tst.awk file
Id Name Course1 Course2 Course3 Course4 Course5
1 John 55 67 81 74 34
2 George 29 64 89 91 63
3 Emma 63 80 75 78 69
4 Alex 57 69 64 60 49
5 TERRY 67
6 HELEN 39
这是我对此的看法。这适用于普通的 awk(不使用 FIELDWIDTHS),它会自动调整到不同数量的字段(即添加一个 Course7
列,你应该没问题)。此外,您可以将它指向多个文件,它应该分别处理每个文件。
#!/usr/bin/awk -f
# Initialize variables on the first record of each input file
# (and also print the header)
#
FNR <= 1 {
print
delete name
delete score
next
}
# Process each line.
#
{
id = substr([=10=], 0, 16) #
name[id] # Store the unique identifier in an array
pos = 0 #
# Step through the score fields until we hit the end of the line,
# storing scores in another array.
do {
score[id, pos] += substr([=10=],17+pos*8,8) +0
printf("id='%s' pos=%s value=%s total=%s\n", id, pos, substr([=10=],17+pos*8,8)+0, score[id, pos] );
} while (17+(++pos)*8 < length())
}
# Keep track of our maximum number of fields
pos>max { max=pos }
# Finally, generate our (randomly sorted) output.
END {
for (id in name) { # Step through the records...
printf("%-12s", id);
for (i=0; i<max; i++) { # Step through the fields...
if (score[id, i]==0) score[id, i]=""
printf("%-8s", score[id, i]);
}
printf("\n")
}
}
它有点长,但我认为更容易理解它的作用。