Добавить столбцы из переменного числа файлов в базовый файл

Вопрос или проблема

Я имею дело с серией bed файлов, которые выглядят следующим образом:

chr1 100 110 0.5
chr1 150 175 0.2
chr1 200 300 1.5

Колонки соответствуют хромосоме, началу, концу и оценке. У меня есть несколько различных файлов с разными оценками в каждом, и мне бы хотелось объединить их следующим образом:

> cat a.bed
chr1 100 110 0.5
chr1 150 175 0.2
chr1 200 300 1.5

> cat b.bed
chr1 100 110 0.4
chr1 150 175 0.7
chr1 200 300 0.9

> cat c.bed
chr1 100 110 1.5
chr1 150 175 1.2
chr1 200 300 0.1

> cat combined.bed
chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

Все колонки с оценками (последняя колонка файла) добавляются в один файл. Я нашел этот ответ, который может объединить колонку из одного дополнительного файла в существующий файл, но мне хотелось бы иметь команду, которая может добавлять переменное количество колонок. Поэтому, если у меня есть 10 bed файлов для объединения, я хотел бы иметь команду, которая может обработать их все вместе и создать один файл с 10 колонками оценок.

Каждый файл должен содержать одинаковое количество строк, и у каждой записи должны быть одинаковые координаты во всех файлах, так что конфликтов здесь быть не должно. Однако в каждом из файлов может быть много записей (100К или больше в общем), поэтому я хотел бы избежать многократной обработки каждой из них.

Есть ли способ сделать это изящно? Это будет в скрипте, так что нет необходимости в одиночной команде.

Используя любые paste и awk, при условии, что все файлы имеют одинаковые ключевые значения и нет пустых файлов:

$ paste *.bed | awk -v ORS= '{print $1, $2, $3; for (i=4; i<=NF; i+=4) print OFS $i; print RS}'
chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

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

$ printf 'Chr Start Stop'; printf ' %s' *.bed; printf '\n'
Chr Start Stop a.bed b.bed c.bed

Альтернативно, просто используя любой awk, даже если у вас нет одинаковых ключей во всех файлах и даже если некоторые файлы пустые и добавляя заголовок:

$ cat tst.awk
{ key = $1 OFS $2 OFS $3 }
!seen[key]++ {
    keys[++numKeys] = key
}
{
    vals[key,FILENAME] = $4
}
END {
    numFiles = ARGC - 1

    printf "Chr" OFS "Start" OFS "Stop"
    for ( fileNr=1; fileNr<=numFiles; fileNr++ ) {
        fileName = ARGV[fileNr]
        printf "%s%s", OFS, fileName
    }
    print ""

    for ( keyNr=1; keyNr<=numKeys; keyNr++ ) {
        key = keys[keyNr]
        printf "%s", key
        for ( fileNr=1; fileNr<=numFiles; fileNr++ ) {
            fileName = ARGV[fileNr]
            val = vals[key,fileName]
            printf "%s%s", OFS, val
        }
        print ""
    }
}

$ awk -f tst.awk a.bed b.bed c.bed
Chr Start Stop a.bed b.bed c.bed
chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

Направление вывода в column -t было бы одним из многих альтернатив, если вы хотите табличный вывод, например:

$ awk -f tst.awk a.bed b.bed c.bed | column -t
Chr   Start  Stop  a.bed  b.bed  c.bed
chr1  100    110   0.5    0.4    1.5
chr1  150    175   0.2    0.7    1.2
chr1  200    300   1.5    0.9    0.1

Следующее использует Miller (mlr) для переименования первых трех колонок, не имеющих имен, в chr, start, и stop, а затем добавляет дополнительное поле под названием file, содержащее имя файла, из которого была прочитана текущая запись. Затем применяется операция reshape “long-to-wide”, чтобы преобразовать набор данных, используя недавно добавленное поле file как название для значения в 4-й (все еще безымянной) колонке.

mlr --nidx \
    label chr,start,stop then \
    put '$file = FILENAME' then \
    reshape -s file,4 \
    [abc].bed

Предполагается, что входные данные разделены пробелами, как в вопросе, но они не обязательно должны быть отсортированы. Команда генерирует выходящие данные, разделенные пробелами:

chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

(Команда label на самом деле совершенно необязательна, если вам не нужен вывод в каком-либо формате, поддерживающем заголовки, как показано ниже.)

Для красивого форматирования используйте --n2p вместо --nidx:

$ mlr --n2p label chr,start,stop then put '$file = FILENAME' then reshape -s file,4 [abc].bed
chr  start stop a.bed b.bed c.bed
chr1 100   110  0.5   0.4   1.5
chr1 150   175  0.2   0.7   1.2
chr1 200   300  1.5   0.9   0.1

Используйте --n2c для CSV вывода.

$ mlr --n2c label chr,start,stop then put '$file = FILENAME' then reshape -s file,4 [abc].bed
chr,start,stop,a.bed,b.bed,c.bed
chr1,100,110,0.5,0.4,1.5
chr1,150,175,0.2,0.7,1.2
chr1,200,300,1.5,0.9,0.1

Используя Raku (ранее известный как Perl_6)

Создание хэша с Chr/start/stop в качестве ключа, добавление значения:

~$ raku -ne 'BEGIN my %hash; %hash.push: .split(/ \s+ <?before <[.0..9-]>+ $ > /, 2); END .say for %hash.sort;'  *.bed
chr1 100 110 => [0.5 0.4 1.5]
chr1 150 175 => [0.2 0.7 1.2]
chr1 200 300 => [1.5 0.9 0.1]

Очистка вывода (использование put вместо say дает ключ/значение, разделенные табуляцией):

~$ raku -ne 'BEGIN my %hash; %hash.push: .split(/ \s+ <?before <[.0..9-]>+ $ > /, 2); END .put for %hash.sort;'  *.bed
chr1 100 110    0.5 0.4 1.5
chr1 150 175    0.2 0.7 1.2
chr1 200 300    1.5 0.9 0.1

То же самое, что и выше, за исключением объединения пар ключ/значение с использованием пробелов:

~$ raku -ne 'BEGIN my %hash; %hash.push: .split(/ \s+ <?before <[.0..9-]>+ $ > /, 2); END .kv.join(" ").put for %hash.sort;'  *.bed
chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

Raku – это язык программирования из семейства Perl, который имеет чистый синтаксис регулярных выражений. Выше .split используется для разделения каждой входной строки на 2 фрагмента, удаляя пробелы перед последней колонкой. Они добавляются в хэш, поэтому значения накапливаются. При выводе вы добавляете .sort в конец кода (сортировка по умолчанию по ключам).

В основном, функция .split ищет шаблон / \s+ <?before <[.0..9-]>+ $ > /, который представляет пробелы перед любой комбинацией десятичных цифр 0..9, точки ., и знака минус - в конце строки $ (опустите знак минус если отрицательные числа в этой колонке невозможны). Технически, синтаксис <?before … > является нотацией безразмерного положительного предварительного поиска Raku (“X перед Y”). Другой подход ниже.

Отделение последней колонки без использования регулярных выражений:

~$ raku -ne 'BEGIN my %hash; 
             %hash.push: .flip.split(/ \s+ /, 2).reverse.map: *.flip; 
             END .kv.join(" ").put for %hash.sort;'  *.bed
chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

РЕДАКЦИЯ: Добавление строки заголовка с именами файлов в качестве столбцов значений:

~$ raku -ne 'BEGIN my (%fh, %hash);  
               %hash.push: "Chr start stop" => $*ARGFILES.Str unless %fh{$*ARGFILES.Str}++; 
               %hash.push: .split(/ \s+ <?before <[.0..9-]>+ $ > /, 2); 
             END .kv.join(" ").put for %hash.sort;'  *.bed

Возвращает:

Chr start stop a.bed b.bed c.bed
chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

https://docs.raku.org/language/regexes
https://docs.raku.org/type/Str#routine_split
https://raku.org

Ответ или решение

Вопрос о добавлении столбцов из переменного числа файлов в базовый файл представляет собой классическую задачу обработки данных, часто встречающуюся в биоинформатике и других областях, где используется формат bed (Browser Extensible Data). Формат bed-файлов обычно включает в себя информацию о расположении геномных компонентов, таких как хромосомы и их координаты, и часто используется для хранения количественных данных, таких как считывания или интенсивности сигналов.

Теория

Основная задача состоит в том, чтобы объединить несколько bed-файлов, каждый из которых содержит одинаковые геномные координаты, но различающиеся в значениях, в один файл. Это требует агрегирования данных по ключевым параметрам, таким как хромосома, начальная и конечная координаты, и добавление дополнительной информации о значениях из каждого файла. Таким образом, первый, второй и третий столбцы должны совпадать во всех файлах, а из последнего столбца каждого файла извлекается значение, которое добавляется как новый столбец в итоговый файл.

Пример

Рассмотрим пример. Допустим, у нас есть три bed-файла:

  • a.bed:

    chr1 100 110 0.5
    chr1 150 175 0.2
    chr1 200 300 1.5
  • b.bed:

    chr1 100 110 0.4
    chr1 150 175 0.7
    chr1 200 300 0.9
  • c.bed:

    chr1 100 110 1.5
    chr1 150 175 1.2
    chr1 200 300 0.1

Итоговый объединенный файл combined.bed должен выглядеть следующим образом:

chr1 100 110 0.5 0.4 1.5
chr1 150 175 0.2 0.7 1.2
chr1 200 300 1.5 0.9 0.1

Применение

Для решения этой задачи в Unix или Linux системах можно использовать командные средства, такие как paste, awk и другие утилиты обработки текста. В зависимости от ваших требований к эффективности и читаемости кода, вы можете выбрать различные подходы. Вот несколько примеров:

  1. Использование paste и awk:

    Объединение файлов командой paste, которая может соединять строки из нескольких файлов по горизонтали. Затем awk используется, чтобы извлекать и печатать нужные столбцы:

    paste *.bed | awk -v ORS= '{print $1, $2, $3; for (i=4; i<=NF; i+=4) print OFS $i; print RS}'

    Эта команда объединяет все bed-файлы (предполагается, что они находятся в текущем каталоге) и выводит объединенные строки, извлекая каждый четвертый столбец из вывода команды paste.

  2. Использование скрипта на awk:

    Более сложное решение с использованием awk позволяет обработать ситуацию, когда некоторые файлы могут быть пустыми или порядок строк может варьироваться:

    $ cat tst.awk
    { key = $1 OFS $2 OFS $3 }
    !seen[key]++ {
       keys[++numKeys] = key
    }
    {
       vals[key,FILENAME] = $4
    }
    END {
       numFiles = ARGC - 1
    
       printf "Chr" OFS "Start" OFS "Stop"
       for ( fileNr=1; fileNr<=numFiles; fileNr++ ) {
           fileName = ARGV[fileNr]
           printf "%s%s", OFS, fileName
       }
       print ""
    
       for ( keyNr=1; keyNr<=numKeys; keyNr++ ) {
           key = keys[keyNr]
           printf "%s", key
           for ( fileNr=1; fileNr<=numFiles; fileNr++ ) {
               fileName = ARGV[fileNr]
               val = vals[key,fileName]
               printf "%s%s", OFS, val
           }
           print ""
       }
    }

    Запуск этого скрипта с заданными файлами (awk -f tst.awk a.bed b.bed c.bed) собирает данные из всех указанных файлов и печатает их в формате с заголовком.

  3. Использование mlr (Miller):

    mlr, или Miller, предлагает более мощный и гибкий подход к обработке данных, позволяя манипулировать столбцами и выполнять операции "длинный-к-широкому" (long-to-wide):

    mlr --nidx label chr,start,stop then put '$file = FILENAME' then reshape -s file,4 [abc].bed

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

Резюме

Существуют различные подходы к задаче объединения bed-файлов в зависимости от ваших предпочтений и доступных инструментов. В предложенных примерах было использовано три основные утилиты: paste, awk, и mlr, каждая из которых предлагает свой уникальный сценарий использования. Выбор конкретного инструмента будет зависеть от специфики проекта: объема данных, необходимых метаданных и требований к производительности. В конечном итоге, знание этих инструментов расширяет возможности обработки и анализа данных, делая процессы более эффективными и менее подверженными ошибкам.

Оцените материал
Добавить комментарий

Капча загружается...