Удалить элемент из списка на основе следующего элемента в том же списке

Я только начал изучать Python, и здесь у меня есть отсортированный список последовательностей белков (всего 59 000 последовательностей), и некоторые из них перекрываются. Я сделал список игрушек здесь, например:

ABCDE
ABCDEFG
ABCDEFGH
ABCDEFGHIJKLMNO
CEST
DBTSFDE
DBTSFDEO
EOEUDNBNUW
EOEUDNBNUWD
EAEUDNBNUW
FEOEUDNBNUW
FG
FGH

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

ABCDEFGHIJKLMNO
CEST
DBTSFDEO
EAEUDNBNUW
FEOEUDNBNUWD
FGH

Как мне это сделать? Мой код выглядит так:

with open('toy.txt' ,'r') as f:
    pattern = f.read().splitlines()
    print pattern

    for i in range(0, len(pattern)):
        if pattern[i] in pattern[i+1]:
            pattern.remove(pattern[i])
        print pattern

И я получил сообщение об ошибке:

['ABCDE', 'ABCDEFG', 'ABCDEFGH', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDE', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FG', 'FGH']
['ABCDEFG', 'ABCDEFGH', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDE', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FG', 'FGH']
['ABCDEFG', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDE', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FG', 'FGH']
['ABCDEFG', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDE', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FG', 'FGH']
['ABCDEFG', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FG', 'FGH']
['ABCDEFG', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FG', 'FGH']
['ABCDEFG', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FG', 'FGH']
['ABCDEFG', 'ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FGH']
Traceback (most recent call last):
  File "test.py", line 8, in <module>
    if pattern[i] in pattern[i+1]:
IndexError: list index out of range

11 ответов

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

Ошибка произошла из-за того, что вы изменяли один и тот же список при проверке индекса с помощью range() ,

Таким образом, при увеличении i Переменная вы удаляете элемент из списка, который в какой-то момент вызывает index error неизбежно.

Таким образом, вот рабочая версия вашего исходного кода с некоторыми изменениями,

pattern = ["ABCDE","ABCDEFG","ABCDEFGH","ABCDEFGHIJKLMNO","CEST","DBTSFDE","DBTSFDEO","EOEUDNBNUW","EAEUDNBNUW","FG","FGH"]
output_pattern = []


for i in range(0, (len(pattern)-1)):
    if not pattern[i] in pattern[i+1]:
        output_pattern.append(pattern[i]) 

# Adding the last item
output_pattern.append(pattern[-1])   
print (output_pattern)

>>>> ['ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FGH']    

Обратите внимание, что этот код будет работать, если ваш список предварительно отсортирован, как вы упоминали в разделе комментариев.

Что делает этот код?

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

Что мне делать с последним предметом?

Поскольку список отсортирован, вы можете считать последний элемент всегда уникальным. Вот почему я использую

output_pattern.append(pattern[-1])

который добавляет последний элемент исходного списка.

Важная заметка

Этот ответ был написан в ответ на первоначальный вопрос ОП, где он хотел сохранить более продолжительное совпадение, и я цитирую, основываясь на следующем пункте в том же списке. Как заявляет @Chris_Rands, если ваши проблемы связаны с биологической задачей и вам необходимо найти какое-либо совпадение, это решение не подходит для ваших нужд.

Пример, где этот код не сможет распознать потенциальное перекрытие,

pattern = ["ACD", "AD", "BACD"]

где он будет выводить тот же результат, не удаляя возможные "ACD" перекрытия. Теперь, как пояснение, это подразумевало бы гораздо более сложный алгоритм, и я сначала подумал, что он выходит за рамки требований вопроса. Если когда-нибудь это ваш случай, я могу ошибаться, но я действительно считаю, что реализация C++ кажется более подходящей. взгляните на алгоритм CD-Hit, предложенный @Chris_Rands в разделе комментариев.

Вы могли бы использовать groupby() а также max() чтобы помочь здесь:

from itertools import groupby

with open('toy.txt') as f_input:
    for key, group in groupby(f_input, lambda x: x[:2]):
        print(max(group, key=lambda x: len(x)).strip())

Это будет отображать:

ABCDEFGHIJKLMNO
CEST
DBTSFDEO
EOEUDNBNUW
EAEUDNBNUW
FGH

groupby() работает, возвращая список совпадающих элементов на основе функции, в данном случае последовательных строк с одинаковыми первыми 2 символами. max() Затем функция берет этот список и возвращает элемент списка с самой длинной длиной.

# assuming list is sorted:
pattern = ["ABCDE",
"ABCDEFG",
"ABCDEFGH",
"ABCDEFGHIJKLMNO",
"CEST",
"DBTSFDE",
"DBTSFDEO",
"EOEUDNBNUW",
"EAEUDNBNUW",
"FG",
"FGH"]

pattern = list(reversed(pattern))

def iterate_patterns():
    while pattern:
        i = pattern.pop()
        throw_it_away = False
        for p in pattern:
            if p.startswith(i):
                throw_it_away = True
                break
        if throw_it_away == False:
            yield i

print(list(iterate_patterns()))

Выход:

['ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FGH']

Код

import collections as ct


def read_file(filepath):
    """Yield a generator of lines from a file."""
    with open(filepath, "r") as f:
        for line in f:
            yield line.strip()


def find_longest_sequences(seqs):
    """Return a dict of the long common sequences."""
    seqs = tuple(seqs) 
    dd = ct.defaultdict(list)
    [dd[k].append(seq) for seq in seqs for k in seqs if k in seq]
    return {max(v, key=len) for v in dd.values()}


data = read_file("test.txt")
find_longest_sequences(data)

Выход

{'ABCDEFGHIJKLMNO',
 'CEST',
 'DBTSFDEO',
 'EAEUDNBNUW',
 'EOEUDNBNUWD',
 'FEOEUDNBNUW'}

подробности

Мы используем read_file выдавать каждую строку файла.

find_longest_sequences создает defaultdict, который группирует похожие последовательности вместе. Итерирует данные с двумя циклами:

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

Набор значений состоит из результирующего dict, и самые длинные последовательности возвращаются.

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

  1. FGH пересекается с ABCDEFGHIJKLMNO и, таким образом, не является действительным выводом.
  2. FEOEUDNBNUWD не оригинальная последовательность Постобработка необходима для перекрывающихся последовательностей.

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

class Tree:
  def __init__(self, val=None):
    self.left, self.value, self.right = None, val, None
  def insert_val(self, _val):
    if self.value is None or _val.startswith(self.value):
       self.value = _val
    else:
       if _val < self.value:
          getattr(self.left, 'insert_val', lambda x:setattr(self, 'left', Tree(x)))(_val)
       else:
          getattr(self.right, 'insert_val', lambda x:setattr(self, 'right', Tree(x)))(_val)
  def flatten(self):
     return [*getattr(self.left, 'flatten', lambda :[])(), self.value, *getattr(self.right, 'flatten', lambda :[])()]

t = Tree()
for i in open('filename.txt'):
  t.insert_val(i.strip('\n'))
print(t.flatten())

Выход:

['ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EAEUDNBNUW', 'EOEUDNBNUW', 'FGH']

Простой способ - обработать входной файл по одной строке за раз, сравнить каждую строку с предыдущей и сохранить предыдущую, если она не содержится в текущей.

Код может быть таким простым, как:

with open('toy.txt' ,'r') as f:
    old = next(f).strip()               # keep first line after stripping EOL 

    for pattern in f:
        pattern = pattern.strip()       # strip end of line...
        if old not in pattern:
            print old                   # keep old if it is not contained in current line
        old = pattern                   # and store current line for next iteration
    print old                           # do not forget last line
with open('demo.txt') as f:
    lines = f.readlines()

l_lines = len(lines)

n_lst = []

for i, line in enumerate(lines):
    line = line.strip()
    if i == l_lines - 1:
        if lines[-2] not in line:
            n_lst.append(line)
        break
    if line not in lines[i + 1]:
        n_lst.append(line)

print(n_lst)

Выход

['ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EOEUDNBNUW', 'EAEUDNBNUW', 'FGH']

Это приведет вас туда, где вы хотите быть:

with open('toy.txt' ,'r') as f:
    lines = f.readlines()
    data = set(lines)
    print(sorted([i for i in lines if len([j for j in data if j.startswith(i)])==1]))

#['ABCDEFGHIJKLMNO', 'CEST', 'DBTSFDEO', 'EAEUDNBNUW', 'EOEUDNBNUW', 'FGH']

я добавил set только в случае нескольких вхождений одного и того же текста.

Кенни, ты почти понял, но есть две проблемы, на которые указал @scharette:

  1. for Цикл и удаление элемента списка не должны идти вместе. Исправление заключается в использовании while цикл и явно увеличить индекс. while цикл менее эффективен, потому что он вызывает len() несколько раз вместо одного, но это то, что нужно, чтобы получить правильный результат.
  2. IndexError, Это происходит только в самой последней строке. Мой способ справиться с этой проблемой - игнорировать ошибку.

После этого я изменил ваш код:

with open('toy.txt' ,'r') as f:
    pattern = f.read().splitlines()
    print pattern

    try:
        i = 0
        while i < len(pattern):
            if pattern[i] in pattern[i+1]:
                pattern.remove(pattern[i])
            print pattern
            i += 1
    except IndexError:
        pass

Не совсем соответствует вашим ожиданиям, но, учитывая, что вы заявляете, что это отсортировано (и это не так, EOEUDNBNUWD EAEUDNBNUW) и что я не знаю почему ты скучаешь EOEUDNBNUWD Я не уверен, правильно ли изложены ваши ожидания или я неправильно понял ваш вопрос.

(ах, да, я вижу понятие перекрытия бросает гаечный ключ в sort а также startswith подход).

Возможно, для ОП было бы неплохо перефразировать этот конкретный аспект, я прочитал комментарий @DSM, не понимая его проблемы. Теперь я делаю.

li = sorted([i.strip() for i in """
ABCDE
ABCDEFG
ABCDEFGH
ABCDEFGHIJKLMNO
CEST
DBTSFDE
DBTSFDEO
EOEUDNBNUW
EOEUDNBNUWD
EAEUDNBNUW
FEOEUDNBNUW
FG
FGH""".splitlines() if i.strip()])

def get_iter(li):
    prev = ""
    for i in li:
        if not i.startswith(prev):
            yield(prev)
        prev = i
    yield prev

for v in get_iter(li):
    print(v)

выход:

ABCDEFGHIJKLMNO
CEST
DBTSFDEO
EAEUDNBNUW
EOEUDNBNUWD
FEOEUDNBNUW
FGH

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

Вот еще один вариант рабочего решения:

with open('toy.txt', 'r') as infile:
    input_lines = reversed(map(lambda s: s.strip(), infile.readlines()))

output = []
for pattern in input_lines:
    if len(output) == 0 or not output[-1].startswith(pattern):        
        output.append(pattern)

print('\n'.join(reversed(output)))
Другие вопросы по тегам