Python-эквивалент лексикографической и числовой сортировки bash

Итак, я работал над скриптом Python, который объединяет некоторую информацию в формат «кровать». Это означает, что я работаю с функциями генома, мой первый столбец — это имя каркаса (строка), второй — начальная позиция на этом каркасе (целое число), а третий столбец — позиция остановки (целое число), другие столбцы содержать другую информацию, не относящуюся к моему вопросу. Моя проблема в том, что мой вывод не отсортирован.

Теперь я знаю, что могу сортировать файлы с помощью этой команды bash:

$sort -k1,1 -k2,2n -k3,3n infile > outfile

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

Фрагмент моих данных (я хочу отсортировать по столбцам 1, 2 и 3 (в таком порядке)):

Scf_3R  8599253 8621866 FBgn0000014 FBgn0191744 -0.097558026153
Scf_3R  8497493 8503049 FBgn0000015 FBgn0025043 0.437973284047
Scf_3L  16209309    16236428    FBgn0000017 FBgn0184183 -1.19105585707
Scf_2L  10630469    10632308    FBgn0000018 FBgn0193617 0.073153454539
Scf_3R  12087670    12124207    FBgn0000024 FBgn0022516 -0.023946795475
Scf_X   14395665    14422243    FBgn0000028 FBgn0187465 0.00300558969397
Scf_3R  25163062    25165316    FBgn0000032 FBgn0189058 0.530118698187
Scf_3R  19757441    19808894    FBgn0000036 FBgn0189822 -0.282508464261

person Joëlle    schedule 15.06.2016    source источник
comment
sort не является командой bash. Это часть GNU coreutils. То, что вы запускаете ее из bash, не делает ее командой bash, вы можете запускать любую программу из bash.   -  person totoro    schedule 15.06.2016
comment
Я этого не знал, я запускаю его из командной строки; например это баш. По крайней мере, на мой невежественный взгляд. Спасибо за исправление!   -  person Joëlle    schedule 15.06.2016
comment
Пожалуйста, покажите фрагмент ваших данных.   -  person totoro    schedule 15.06.2016
comment
Спасибо за фрагмент данных, есть ответ, но я могу загрузить его позже.   -  person Dilettant    schedule 15.06.2016
comment
Совет: сортировка Python гарантированно будет стабильной, т. е. если у вас есть два элемента x, y, которые сравниваются равными, их относительный порядок сохраняется в процессе сортировки. Это означает, что для сортировки по нескольким ключам можно просто сортировать по каждому ключу отдельно, от последнего к первому. В вашем случае: data.sort(key=lambda x: int(x[2])); data.sort(key=lambda x: int(x[1])); data.sort(key=lambda x: x[0]). Сначала будут строки по значению x[0] затем путем численного сравнения значений x[1] и, наконец, численного сравнения x[2]s.   -  person Bakuriu    schedule 15.06.2016
comment
@Bakuriu: вы могли бы сделать это одним sort() вызовом: data.sort(key=lambda x: (x[0], int(x[1]), int(x[2])))?   -  person jfs    schedule 15.06.2016
comment
@ J.F.Sebastian Да, это моя точка зрения. большинство людей не знают, что сортировку по кортежу значений можно эмулировать несколькими вызовами сортировки из-за стабильности алгоритма.   -  person Bakuriu    schedule 15.06.2016


Ответы (3)


Загрузить данные, отсортировать их с помощью sorted, записать в новый файл.

# Load data 
lists = list()
with open(filename, 'r') as f:
    for line in f:
        lists.append(line.rstrip().split())

# Sort data
results = sorted(lists, key=lambda x:(x[0], int(x[1]), int(x[2])))

# Write to a file
import csv
with open(filename, 'w') as f:
    writer = csv.writer(f, delimiter='\t')
    writer.writerows(results)
person SparkAndShine    schedule 15.06.2016

Чтобы отсортировать по собственным критериям сортировки, просто передайте соответствующую функцию key:

with open('infile', 'rb') as file:
    lines = file.readlines()

def sort_key(line):
    fields = line.split()
    try:
        return fields[0], int(fields[1]), int(fields[2])
    except (IndexError, ValueError):
        return () # sort invalid lines together
lines.sort(key=sort_key)

with open('outfile', 'wb') as file:
    file.writelines(lines)

Предполагается, что в конце входного файла есть новая строка (при необходимости добавьте ее).

Код сортирует текстовые данные по значениям байтов (это нормально, если первый столбец - ASCII), откройте файл в текстовом режиме (используйте io.open() на Python 2), если это не так (для сортировки по значениям кодовой точки Unicode) . Результат команды sort в оболочке может зависеть от локали. Вы можете использовать подборщик PyICU в Python.

Если вам нужно отсортировать файлы, которые не помещаются в памяти, см. раздел Сортировка текстового файла с помощью Python.

person jfs    schedule 15.06.2016
comment
Браво. Кратко, что означает полную обработку ошибок в виде группировки в функции sort_key, без лямбда ;-) и даже показанных дополнительных направлений. Спасибо за эти строки. - person Dilettant; 15.06.2016

Решение @sparkandshine кажется кратким и точным для конкретного шаблона сортировки. Также тот, который предложил мне @j-f-sebastian, выглядит превосходно, лаконично и с важными подсказками/ссылками на стратегии интернационализации и сортировки вне памяти.

Возможно следующий более подробный пример предлагает дополнительную полезную информацию для OP или кого-то с похожими задачами, чтобы адаптироваться к их потребностям. См. комментарии в основном коде, соответствующем pep8:

#! /usr/bin/env python
"""Provide a show case for hierarchical sort, that offers flexible
hierarchical lexcical, numeric column sort mixes at runtime.
Hopefully this draft solution offers ideas for helping migrate
the sort shell level operation into a pythonic solution - YMMV."""
from __future__ import print_function

from functools import partial  # We use this to tailor the key function


def text_in_lines_gen(text_in_lines):
    """Mock generator simulating a line source for the data."""
    for line in text_in_lines.split('\n'):
        if line:
            yield line.split()


def sort_hier_gen(iterable_lines, hier_sort_spec):
    """Given iterator of text lines, sort all lines based on
    sort specification in hier_sort_spec.

    Every entry in hier_sort_spec is expected to be a pair with first value
    integer for index in columns of text blocks lines and second entry
    type of sorting in ('int', 'float') numeric or any other for text
    (lexical) ordering regime."""

    num_codes = ('int', 'float')
    converter_map = dict(zip(num_codes, (int, float)))

    # Extract facts from sort spec, prepare processing:
    key_ordered = tuple(k for k, _ in hier_sort_spec)

    # Prepare key function: Step 1 ...
    def _key_in(selected, r):
        """Inject the indexing into the key at sort time
        via partial application, as key function in sort
        has single argument only."""
        return tuple(r[k] for k in selected)

    _key = partial(_key_in, key_ordered)  # ... step 2

    convert_these_by = {}
    for k, t in hier_sort_spec:
        if t in num_codes:
            convert_these_by[k] = converter_map[t]

    if not convert_these_by:  # early out
        for row in sorted(iterable_lines, key=_key):
            yield row
    else:
        def flow_converter(row_iter, converter_map):
            """Row based converter - Don't block the flow ;-)."""
            for row in row_iter:
                for k, convert in converter_map.items():
                    row[k] = convert(row[k])
                yield row

        for row in sorted(flow_converter(iterable_lines,
                          convert_these_by), key=_key):
            yield row


def main():
    """Drive the hierarchical text-int-int sort."""

    data_1 = """Scf_3R  8599253 8621866 FBgn0000014 FBgn0191744 -0.097558026153
    Scf_3R  8497493 8503049 FBgn0000015 FBgn0025043 0.437973284047
    Scf_3L  16209309    16236428    FBgn0000017 FBgn0184183 -1.19105585707
    Scf_2L  10630469    10632308    FBgn0000018 FBgn0193617 0.073153454539
    Scf_3R  12087670    12124207    FBgn0000024 FBgn0022516 -0.023946795475
    Scf_X   14395665    14422243    FBgn0000028 FBgn0187465 0.00300558969397
    Scf_3R  25163062    25165316    FBgn0000032 FBgn0189058 0.530118698187
    Scf_3R  19757441    19808894    FBgn0000036 FBgn0189822 -0.282508464261"""

    bar = []
    x = 0
    for a in range(3, 0, -1):
        for b in range(3, 0, -1):
            for c in range(3, 0, -1):
                x += 1
                bar.append('a_%d %d %0.1f %d' % (a, b, c * 1.1, x))
    data_2 = '\n'.join(bar)

    hier_sort_spec = ((0, 't'), (1, 'int'), (2, 'int'))
    print("# Test data set 1 and sort spec={0}:".format(hier_sort_spec))
    for sorted_row in sort_hier_gen(text_in_lines_gen(data_1), hier_sort_spec):
        print(sorted_row)

    hier_sort_spec = ((0, 't'), (1, None), (2, False))
    print("# Test data set 1 and sort spec={0}:".format(hier_sort_spec))
    for sorted_row in sort_hier_gen(text_in_lines_gen(data_1), hier_sort_spec):
        print(sorted_row)

    hier_sort_spec = ((0, 't'), (2, 'float'), (1, 'int'))
    print("# Test data set 2 and sort spec={0}:".format(hier_sort_spec))
    for sorted_row in sort_hier_gen(text_in_lines_gen(data_2), hier_sort_spec):
        print(sorted_row)

if __name__ == '__main__':
    main()

На моей машине три тестовых примера (включая примеры данных вопросов) дают:

Первый:

# Test data set 1 and sort spec=((0, 't'), (1, 'int'), (2, 'int')):
['Scf_2L', 10630469, 10632308, 'FBgn0000018', 'FBgn0193617', '0.073153454539']
['Scf_3L', 16209309, 16236428, 'FBgn0000017', 'FBgn0184183', '-1.19105585707']
['Scf_3R', 8497493, 8503049, 'FBgn0000015', 'FBgn0025043', '0.437973284047']
['Scf_3R', 8599253, 8621866, 'FBgn0000014', 'FBgn0191744', '-0.097558026153']
['Scf_3R', 12087670, 12124207, 'FBgn0000024', 'FBgn0022516', '-0.023946795475']
['Scf_3R', 19757441, 19808894, 'FBgn0000036', 'FBgn0189822', '-0.282508464261']
['Scf_3R', 25163062, 25165316, 'FBgn0000032', 'FBgn0189058', '0.530118698187']
['Scf_X', 14395665, 14422243, 'FBgn0000028', 'FBgn0187465', '0.00300558969397']

Второй:

# Test data set 1 and sort spec=((0, 't'), (1, None), (2, False)):
['Scf_2L', '10630469', '10632308', 'FBgn0000018', 'FBgn0193617', '0.073153454539']
['Scf_3L', '16209309', '16236428', 'FBgn0000017', 'FBgn0184183', '-1.19105585707']
['Scf_3R', '12087670', '12124207', 'FBgn0000024', 'FBgn0022516', '-0.023946795475']
['Scf_3R', '19757441', '19808894', 'FBgn0000036', 'FBgn0189822', '-0.282508464261']
['Scf_3R', '25163062', '25165316', 'FBgn0000032', 'FBgn0189058', '0.530118698187']
['Scf_3R', '8497493', '8503049', 'FBgn0000015', 'FBgn0025043', '0.437973284047']
['Scf_3R', '8599253', '8621866', 'FBgn0000014', 'FBgn0191744', '-0.097558026153']
['Scf_X', '14395665', '14422243', 'FBgn0000028', 'FBgn0187465', '0.00300558969397']

В третьих:

# Test data set 2 and sort spec=((0, 't'), (2, 'float'), (1, 'int')):
['a_1', 1, 1.1, '27']
['a_1', 2, 1.1, '24']
['a_1', 3, 1.1, '21']
['a_1', 1, 2.2, '26']
['a_1', 2, 2.2, '23']
['a_1', 3, 2.2, '20']
['a_1', 1, 3.3, '25']
['a_1', 2, 3.3, '22']
['a_1', 3, 3.3, '19']
['a_2', 1, 1.1, '18']
['a_2', 2, 1.1, '15']
['a_2', 3, 1.1, '12']
['a_2', 1, 2.2, '17']
['a_2', 2, 2.2, '14']
['a_2', 3, 2.2, '11']
['a_2', 1, 3.3, '16']
['a_2', 2, 3.3, '13']
['a_2', 3, 3.3, '10']
['a_3', 1, 1.1, '9']
['a_3', 2, 1.1, '6']
['a_3', 3, 1.1, '3']
['a_3', 1, 2.2, '8']
['a_3', 2, 2.2, '5']
['a_3', 3, 2.2, '2']
['a_3', 1, 3.3, '7']
['a_3', 2, 3.3, '4']
['a_3', 3, 3.3, '1']

Обновлено, чтобы использовать в основном генераторы только для одной копии данных «вокруг», так как это необходимо в любом случае (в памяти) для глобальной сортировки, но нет необходимости иметь больше копий ;-)

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

Одно последнее обновление удалило оставшуюся негенерирующую копию копии в случае, когда преобразование реализовано, путем определения локальной функции генератора преобразований на основе строк. ХТН.

person Dilettant    schedule 15.06.2016