複数の行と列に基づいてグループ化

複数の行と列に基づいてグループ化

データの例を以下に示します。最初の2つの列はIDで、3番目の列は頻度です。

1 2 99
2 3 62
4 5 80
4 4 98
5 5 79
6 1 98

最初と2番目の列は、同一のIDまたは重複したIDです。たとえば、1、2、3、6は同じ人です。 1==2、2==3などなので、1==3です。したがって、データをこのように分割できます。

人1

1 2 99
2 3 62
6 1 98

人2

4 5 80
4 4 98
5 5 79

上記のようにデータをどのように分割できますか?ここでは、行全体を比較する必要があります。これは私にとって混乱した部分です。次に、各グループ内の3番目の列の頻度に基づいてIDを選択したいと思います。ここでは、他のファイルからこれらのIDを削除するために最も頻度の低い動物を取得します。推奨される最終出力は次のとおりです。

2 3 62
6 1 98
4 5 80
5 5 79 

私は答えを探していますが、私にとっては複雑に見えます。たぶん、データを分割するよりも良い方法があるかもしれません。どんなアイデアでもお願いします。

答え1

最初の質問を解決するには、すべてのUnixシステムのbourne派生シェルでawk + ​​sortを使用して入力を分割する方法は次のとおりです(私はshebangでbashを使用しますが、bashである必要はありません)。

$ cat tst.sh
#!/usr/bin/env bash

awk '{ print $0 ORS $2, $1, $3 }' "${@:--}" |
sort -n -k1,1 -k2,2 |
awk '
    !seen[($1 > $2 ? $1 FS $2 : $2 FS $1)]++ {
        out = ""

        for ( i=1; i<=2; i++ ) {
            if ( $i in map ) {
                out = map[$i]
                break
            }
        }

        if ( out == "" ) {
            out = "person_" (++numPeople)
        }

        for ( i=1; i<=2; i++ ) {
            map[$i] = out
        }

        print >> out
        close(out)
    }
'

次の行を含むように公開した入力例を変更する必要があります。私のコメント分割が機能していることを実際にテストするには、次の手順を実行します。

$ cat file
1 2 99
2 3 62
4 5 80
4 4 98
5 5 79
6 1 98
7 8 99
9 10 98
9 7 97

$ ./tst.sh file

$ head person*
==> person_1 <==
1 2 99
1 6 98
2 3 62

==> person_2 <==
4 4 98
4 5 80
5 5 79

==> person_3 <==
7 8 99
7 9 97
9 10 98

1 2 x上記はと同じであるため、各行の最初の2つのIDの順序は重要ではないと仮定します2 1 x

答え2

「分割データ」部分への部分的な答えです。 GNUawk機能を使用してください。配列の配列だからgawk解決策があります。

1列と2列のIDを重ねてグループ化し、各人に一意のIDを付与し、次の名前のファイルで印刷しますperson_ID

gawk '
#id=array of arrays with unique ids UID and alias
#IDs aID as taken from the file: id[UID][aID]

#manually create first entry in line 1
NR==1 { id[1][$1]=1 ; id[1][$2]=1 ; next }

#on other input: scan array id for a match in aIDs
#use related UID it if match is found
FNR==NR {
  for (i=1 ; i<=length(id) ; i++ ) {
    if ($1 in id[i] || $2 in id[i] ) {
      id[i][$1]=i ; id[i][$2]=i ; next }
  }
#if no match was found, create a new UID:
  new=length(id)+1
  id[new][$1]=new ; id[new][$2]=new
}

#rerun through id arrays to check for doubles:
FNR!=NR && !b {
  for ( i in id ) {
    for ( j in id[i] ) {
      if ( seen[j] ) {
        for ( k in id[i] ) { id[seen[j]][k]=id[seen[j]][k] }
        delete id[i]
        }
       else { seen[j]=i }
    }
  }
  delete seen
#adjust UIDs as they may be out of order now to new id array nid,
#delete old id array:
  for ( i in id ) {++n ; for (j in id[i]) { nid[n][j]=id[i][j] } }
  delete id
  b=!b
}

#write to separate files per UID
FNR!=NR {
  for (i in nid) {
    if ($1 in nid[i] || $2 in nid[i] ) { print > "person_"i }
  }
}

#This is just to print the aID vs UID map
END {
  for (i in nid) {
    print "aIDs for person UID=",i ; b=1
    for (j in nid[i]) {
      if (b) {printf j ; b=0}
      else {printf ","j}
    }
  print ""
  }
}
' infile infile

ここで、行削除の問題には、次の非常に簡単なアプローチをお勧めします。

上記で作成されたファイルをインポートし、person_i各ファイルのフィールド3の最小値を持つ行を選択します。次の行をdelete_meファイルに書き込み、元のファイルでリバース grep を使用します。

for file in person_* ; do
  sort -n -k3 ${file} | head -n1
done > delete_me
grep -xvf delete_me original

sort同一または類似の番号の場合は最小限の選択のみで行われるため、別途の精製作業は行われません。一致に完全な行を含める必要があるかどうかを確認するには、-xforを使用します(そうでない場合はと一致します)。grep1 2 31 2 31 2 33


次のバリアントでは、ID ごとに人をグループ化し、列 3 で最大値を持つ行をフィルタリングして一意の人を見つけます。入力ファイルを2番目に読み込むと、最大行がない行のみが印刷されます。したがって、追加ファイルがない単一のスクリプトソリューションは次のとおりです。

#id=array of arrays with unique ids UID and alias
#IDs aID as taken from the file: id[UID][aID]

#manually create first entry in line 1, print to "person_1"
NR==1 { id[1][$1]=1 ; id[1][$2]=1 ; next }
#manually create first entry for line deletion selection
NR==1 { max[1]=$3 ; line[1]=$0 }

#on other input: scan array id for a match in aIDs
#use related UID it if match is found
FNR==NR {
  for (i=1 ; i<=length(id) ; i++ ) {
    if ($1 in id[i] || $2 in id[i] ) {
      id[i][$1]=i ; id[i][$2]=i
#select line for deletion
      if ($3>=max[i]) { max[i]=$3 ; line[i]=$0 }
      ; next
    }
  }
#if no match was found, create a new UID:
  new=length(id)+1
  id[new][$1]=new ; id[new][$2]=new
  max[new]=$3 ; line[new]=$0
}

#rerun through id arrays to check for doubles:
FNR!=NR && !b {
  for ( i in id ) {
    for ( j in id[i] ) {
      if ( seen[j] ) {
        for ( k in id[i] ) { id[seen[j]][k]=id[seen[j]][k] }
        delete id[i]
#adjust line deletion selection:
        if (max[seen[j]]>=max[i]) { delete line[i]} else {delete line[seen[j]]}
      }
       else { seen[j]=i }
    }
  }
  delete seen
#adjust UIDs as they may be out of order now:
  for ( i in id ) {++n ; for (j in id[i]) { nid[n][j]=id[i][j] } }
  delete id
#swap line deletion marker from array element to index for better processability
  for (i in line) { line[line[i]]="" ; delete line[i]}
  delete max
#set flag for running this block once only
  b=!b
}

#write to separate files per UID (commented out)
#FNR!=NR {
#  for (i in nid) {
#    if ($1 in nid[i] || $2 in nid[i] ) {print > "person_"i}
#  }
#}
#print lines that have not been selected for deletion
#to STDOUT use the alternative to print it to a separate file
FNR!=NR && ! ($0 in line)
#alternative:
#FNR!=NR && ! ($0 in line) {print > "myoutfile"}


#This is just to print the aID vs UID map
END { print "\n------\nUID vs aID map:\n"
  for (i in nid) {
    print "aIDs for person UID=",i ; b=1
    for (j in nid[i]) {
      if (b) {printf j ; b=0}
      else {printf ","j}
    }
  print ""
  }
}

次に実行しますawk -f script.awk infile infile。つまり、infileを2回読みます。

関連情報