4

I have a list

2
2
2
3
2
2
2
4
2
2
2

I want to print the values which are at least 2 times larger than the values 3 steps above and below in the same column.

The output should be

4

How to do that? I have a similar question asked here, just to better illustrate I write it here, thanks.

20171006 Update : Sorry for oversimplifying my actual input file, it is actually a table instead of a list that I need to select in multiple columns (column 2, 3, 4 etc.) and print out column 1. How could I incorporate the column informations in such script?

A 2 2 2
B 2 2 2
C 2 2 2
D 3 3 3
E 2 2 2
F 2 2 2
G 2 2 2
H 4 4 4
I 2 2 2 
J 2 2 2 
K 2 2 2 

And to get

H
3
  • 2
    all 3 values above/below or only 3rd value above/below? – Sundeep Oct 5 '17 at 6:24
  • 3rd above and 3rd below – Johnny Tam Oct 5 '17 at 8:20
  • If you're happy with one or several of the answers, upvote them. If one is solving your issue, accepting it would be the best way of saying "Thank You!" :-) – Kusalananda Oct 7 '17 at 7:56
5

You could do that in awk. You'd need to save the previous 6 lines to compare the 3rd last line with 6th last line and the current one. For that, the common trick is to use a ring buffer which is an array indexed by NR%6 where 6 is the number of lines you want to keep.

awk '
  NR > 6 {
    x = saved[NR%6]; y = saved[(NR - 3) % 6]; z = $0
    if (y >= 2*x && y >= 2*z) print y
  }
  {saved[NR % 6] = $0}'  < file

For your edit: save the key and value to compare:

awk -v key=1 -v value=2 '
  NR > 6 {
    x = saved_value[NR%6]; y = saved_value[(NR - 3) % 6]; z = $value
    if (y >= 2*x && y >= 2*z) print saved_key[(NR - 3) % 6]
  }
  {saved_key[NR % 6] = $key; saved_value[NR % 6] = $value}'  < file

where key is the index of column you want to print and value the column with the values you want to compare.

Or based on whatever metric you'd like based on those columns 2, 3, 4 like the average:

awk '
  {metric = ($2 + $3 + $4) / 3}
  NR > 6 {
    x = saved_metric[NR%6]; y = saved_metric[(NR - 3) % 6]; z = $metric
    if (y >= 2*x && y >= 2*z) print saved_key[(NR - 3) % 6]
  }
  {saved_key[NR % 6] = $key; saved_metric[NR % 6] = $metric}'  < file
9
  • Sorry for oversimplifying my actual input file, it is actually a table instead of a list that I need to select in multiple columns (column 2, 3, 4 etc.) and print out column 1. How could I incorporate the column informations in such script? – Johnny Tam Oct 5 '17 at 16:17
  • @JohnnyTam, see edit. – Stéphane Chazelas Oct 5 '17 at 17:07
  • After I changed the name of file to my input table name in the second script, the error "No such file or directory" while running it. Did I do something wrong? – Johnny Tam Oct 7 '17 at 6:38
  • @JohnnyTam, sorry there was a typo in the code (missing -v in front of value=2). Fixed now. – Stéphane Chazelas Oct 7 '17 at 6:47
  • Thank you so much! It worked!!!! How about if I want to modify it so it just considers the value above OR below (i.e. not AND), I just change the && to || in this part (y >= 2*x && y >= 2*z)? I tried but failed, values larger than the value above OR below were not printed out. Is it I have to modify something more? Thank you! – Johnny Tam Oct 7 '17 at 7:45
5

Without reading the entire file in memory:

paste <(tail -n+4 file.txt | head -n-3) <(head -n-6 file.txt) <(tail -n+7 file.txt) |
    awk '$1 >= 2*$2 && $1 >= 2*$3 {print $1}'

This needs a shell that can handle <(...) constructs (f.i. AT&T ksh, bash or zsh) and a head implementation that supports negative offsets.

Explanation: the paste command above puts on the same line the current value, the value 3 steps above, and the value 3 steps below; the awk command checks the 2 times larger conditions.

2
  • Error "Missing name for redirect." appeared, is it related to the problem of shell which cannot handle <(...) constructs (f.i. AT&T ksh, bash or zsh) and head? – Johnny Tam Oct 7 '17 at 6:29
  • @JohnnyTam Yes, you need a (slightly) smarter shell. – Satō Katsura Oct 7 '17 at 6:54
2

Awk solution:

awk 'function mean(sum){ 
          m=sum/3; return (int(m) == m)? m: int(m)+1 
     }
     { a[NR]=$0 }
     END{ 
         for(i=4;i<=NR-3;i++) 
             if (a[i]>=mean(a[i-3]+a[i-2]+a[i-1])*2 &&
                a[i]>=mean(a[i+3]+a[i+2]+a[i+1])*2) 
             print a[i] 
     }' file

  • a[NR]=$0 - collecting all values into array a indexed with record numbers NR

The output:

4
0
2

If you only need compare with 3rd value above/below only, then that's easier with the below awk command.

awk -vRS='' '{for (i=4;i<=NF;i++) if ($i>=$(i-3)*2 && $i>=$(i+3)*2 ) print $i}' infile

Or in short.

awk -vRS='' '{for (i=4;i<=NF;i++) if ($i>=$(i%3)*2) print $i}' infile
0
1

So you would have to keep a sliding window of 3 + 1 + 3 input lines to be able to do this.

awk -vn=3 -va=2 'BEGIN { N=2*n+1 } { t=(NR-n)%N; m=NR%N; b=(NR+n)%N; w[b]=$0 } NR >= N && w[m] >= a*w[t] && w[m] >= a*w[b] { print w[m] }' file

This would print all values fulfill the criteria, with the added bonus that you can easily modify the distance by tweaking the variable n on the command line (change -vn=3) and the weight by changing a (change -va=2).

The code stores the last N values where N is 2*n + 1 in a cyclic buffer w. If the middle value, w[m], is bigger than a times the first value in the buffer, w[t] (t for "top"), and a times bigger than the last value in the buffer, w[b] (b for "bottom"), then it is printed.


The awk script unravelled:

BEGIN   { N = 2*n + 1 }

        {
            t = (NR - n)%N
            m = NR%N
            b = (NR + n)%N
            w[b] = $0
        }

NR >= N && w[m] >= a*w[t] && w[m] >= a*w[b] { print w[m] }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.