как обрезать файл - удалить столбцы с одинаковым значением - PullRequest
7 голосов
/ 15 июня 2011

Я бы хотел, чтобы вы помогли обрезать файл, удалив столбцы с одинаковым значением.

# the file I have (tab-delimited, millions of columns)
jack 1 5 9
john 3 5 0
lisa 4 5 7

# the file I want (remove the columns with the same value in all lines)
jack 1 9
john 3 0
lisa 4 7

Не могли бы вы дать мне какие-либо указания по этой проблеме?Я предпочитаю решение sed или awk, или, возможно, решение perl.

Заранее спасибо.Лучшее,

Ответы [ 8 ]

5 голосов
/ 16 июня 2011

Вот быстрый скрипт на Perl, чтобы выяснить, какие столбцы можно вырезать.

open FH, "file" or die $!;
my @baseline = split /\t/,<FH>;         #snag the first row
my @linemap = 0..$#baseline;            #list all equivalent columns (all of them)

while(<FH>) {                           #loop over the file
    my @line = split /\t/;
    @linemap = grep {$baseline[$_] eq $line[$_]}  @linemap; #filter out any that aren't equal
}
print join " ", @linemap;
print "\n";

Вы можете использовать многие из приведенных выше рекомендаций для фактического удаления столбцов. Моим фаворитом, вероятно, будет реализация cut, отчасти потому, что вышеприведенный скрипт на perl может быть изменен, чтобы дать вам точную команду (или даже запустить ее для вас).

@linemap = map {$_+1} @linemap;                   #Cut is 1-index based
print "cut --complement -f ".join(",",@linemap)." file\n";
3 голосов
/ 16 июня 2011

Если вы знаете, какой столбец нужно предварительно вырезать, тогда будет полезен cut:

cut --complement -d' ' -f 3 filename
3 голосов
/ 16 июня 2011
#!/usr/bin/perl
$/="\t";
open(R,"<","/tmp/filename") || die;
while (<R>)
{
  next if (($. % 4) == 3);
  print;
}

Ну, это предполагалось, что это был третий столбец.Если это значение:

#!/usr/bin/perl
$/="\t";
open(R,"<","/tmp/filename") || die;
while (<R>)
{
  next if (($_ == 5);
  print;
}

После редактирования вопроса желания ОП становятся понятными.Как насчет:

#!/usr/bin/perl
open(R,"<","/tmp/filename") || die;
my $first = 1;
my (@cols);
while (<R>)
{
  my (@this) = split(/\t/);
  if ($. == 1)
  {
    @cols = @this;
  }
  else
  {
    for(my $x=0;$x<=$#cols;$x++)
    {
      if (defined($cols[$x]) && !($cols[$x] ~~ $this[$x]))
      {
        $cols[$x] = undef;
      }
    }
  }
  next if (($_ == 5));
#  print;
}
close(R);
my(@del);
print "Deleting columns: ";
for(my $x=0;$x<=$#cols;$x++)
{
  if (defined($cols[$x]))
  {
    print "$x ($cols[$x]), ";
    push(@del,$x-int(@del));
  }
}
print "\n";

open(R,"<","/tmp/filename") || die;
while (<R>)
{
  chomp;
  my (@this) = split(/\t/);

  foreach my $col (@del)
  {
    splice(@this,$col,1);
  }

  print join("\t",@this)."\n";
}
close(R);
2 голосов
/ 16 июня 2011

Как я понимаю, вы хотите просмотреть каждую строку и проверить, не имеют ли значения в каком-либо столбце никакого отклонения, а затем в этом случае вы можете удалить этот столбец.Если это так, у меня есть предложение, но не готовый сценарий, но я думаю, что вы сможете понять это.Вы должны посмотреть на cut.Это извлекает части линии.Вы можете использовать его для извлечения, т. Е. Столбца один, затем запустить uniq для выводимых данных, а затем, если после уникального значения только одно значение, это означает, что все значения в этом столбце идентичны.Таким образом, вы можете собрать количество столбцов, которые не имеют дисперсии.Вам понадобится сценарий оболочки, чтобы увидеть, сколько столбцов у вас в файле (я думаю, с использованием head -n 1 и подсчет количества разделителей) и запустить такую ​​процедуру для каждого столбца, сохраняя номера столбцов в массиве, а затем в конце обработки вырезать строку, чтобы удалить столбцыкоторые не представляют интереса.Конечно, это не awk или perl, но должно работать и будет использовать только традиционные инструменты Unix.Ну, вы можете использовать их в Perl-скрипте, если хотите:)

Ну, и если я неправильно понял вопрос, возможно, сокращение будет по-прежнему полезным :), похоже, это один из менее известных инструментов.

1 голос
/ 16 июня 2011

Основная проблема здесь в том, что вы сказали «миллионы столбцов» и не указали, сколько строк. Чтобы проверить каждое значение в каждой строке по отношению к его аналогу в каждом другом столбце ... вы просматриваете множество проверок.

Конечно, вы сможете уменьшить количество столбцов по мере продвижения, но вам все равно придется проверять каждый столбец до последнего ряда. Итак ... большая обработка.

Мы можем создать начальный хеш с двух первых строк:

use strict;
use warnings;

open my $fh, '<', "inputfile.txt" or die;
my %matches;
my $line = <$fh>;
my $nextline = <$fh>;
my $i=0;
while ($line =~ s/\t(\d+)//) {
    my $num1 = $1;
    if ($nextline =~ s/\t(\d+)//) {
       if ($1 == $num1) { $matches{$i} = $num1 }
    } else {
       die "Mismatched line at line $.";
    }
    $i++;
}

Затем с помощью этого «начального» хеша вы можете прочитать остальные строки и удалить несоответствующие значения из хеша, например:

while($line = <$fh>) {
    my $i = 0;
    while ($line =~ s/\t(\d+)//) {
        if (defined $matches{$i}) {
            $matches{$i} = undef if ($matches{$i} != $1);
        }
        $i++;
    }
}

Можно представить решение, в котором можно убрать все строки, которые уже оказались уникальными, но для этого вам нужно создать массив строк или сделать регулярное выражение, и я не уверен это не займет столько же времени, сколько простое прохождение через строку.

Затем, после обработки всех строк, у вас будет хеш со значениями дублированных чисел, чтобы вы могли заново открыть файл и выполнить печать:

open my $fh, '<', "inputfile.txt" or die;
open my $outfile, '>', "outfile.txt" or die;
while ($line = <$fh>) {
    my $i = 0;
    if ($line =~ s/^([^\t]+)(?=\t)//) {
        print $outfile $1;
    } else { warn "Missing header at line $.\n"; }
    while ($line =~ s/(\t\d+)//) {
        if (defined $matches{$i}) { print $1 }
        $i++;
    }
    print "\n";
}

Это довольно сложная операция, и этот код не проверен. Это даст вам подсказку к решению, вероятно, потребуется время, чтобы обработать весь файл. Я предлагаю запустить несколько тестов, чтобы проверить, работает ли он с вашими данными, и настроить его.

Если у вас есть только несколько совпадающих столбцов, гораздо проще просто извлечь их из строки, но я не решаюсь использовать split на таких длинных строках. Что-то вроде:

while ($line = <$fh>) {
    my @line = split /\t/, $line;
    for my $key (sort { $b <=> $a } keys %matches) {
        splice @line, $key + 1, 1;
    }
    $line = join ("\t", @line);
    $line =~ s/\n*$/\n/; # awkward way to make sure to get a single newline
    print $outfile $line;
}

Обратите внимание, что нам нужно отсортировать ключи в порядке убывания, чтобы мы обрезали значения с конца. В противном случае мы испортим уникальность последующих номеров массивов.

В любом случае, это может быть один из способов. Хотя это довольно большая операция. Я бы держал резервные копии. ;)

1 голос
/ 16 июня 2011

Не полностью протестировано, но, похоже, это работает для предоставленного тестового набора, обратите внимание, что он уничтожает исходный файл ...

#!/bin/bash

#change 4 below to match number of columns
for i in {2..4}; do
    cut -f $i input | sort | uniq -c > tmp
    while read a b; do
        if [ $a -ge 2 ]; then
            awk -vfield=$i '{$field="_";print}' input > tmp2
            $(mv tmp2 input)
        fi
    done < tmp
done

$ cat input
jack    1   5   9
john    3   5   0
lisa    4   5   7

$ ./cnt.sh 

$ cat input
jack 1 _ 9
john 3 _ 0
lisa 4 _ 7

Использование _ для более четкого вывода ...

1 голос
/ 16 июня 2011

Вы можете выбрать столбец для вырезания, как

# using bash/awk
# I had used 1000000 here, as you had written millions of columns but you should adjust it
for cols in `seq 2 1000000` ; do
    cut -d DELIMITER -f $cols FILE | awk -v c=$cols '{s+=$0} END {if (s/NR==$0) {printf("%i,",c)}}'
done | sed 's/,$//' > tmplist
cut --complement -d DELIMITER -f `cat tmplist` FILE

Но он может быть ДЕЙСТВИТЕЛЬНО медленным, потому что он не оптимизирован и читает файл несколько раз ... так что будьте осторожны с большими файлами.

Или вы можете прочитать весь файл один раз с помощью awk и выбрать столбцы для дампа, а затем использовать команду cut.

cut --complement -d DELIMITER -f `awk '{for (i=1;i<=NF;i++) {sums[i]+=$i}} END {for (i=1;i<=NF; i++) {if (sums[i]/NR==$i) {printf("%i,",c)}}}' FILE | sed 's/,$//'` FILE

HTH

1 голос
/ 16 июня 2011

Насколько я могу судить, вам нужно сделать эту многопроходную программу, которая бы соответствовала вашим потребностям, не перегружая память. Для начала загрузите одну строку файла в массив.

open FH,'datafile.txt' or die "$!";
my @mask;
my @first_line= split(/\s+/,<FH>);

Тогда вы захотите последовательно прочитать в других строках

while(my @next_line= split(/\s+/,<FH>)) {
/* compare each member of @first_line to @next_line
 * any match, make a mark in mask to true
 */

Когда вы дойдете до конца файла, вернитесь наверх и используйте маску, чтобы определить, какие столбцы печатать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...