配列の同じ要素の連続を別の要素に置き換える【Python】

# 概要
発話区間検出などをしていると、発話が細切れになりすぎることを避けるため、一定長さ以下の沈黙は無視したくなることがあります。そのような場合に使える、一定長さ以下/以上の同じ要素の連続を別の要素に置き換える関数を作りました。
# ゴール
以下のような入出力をする関数`replace_sequence`を作ることが目標です。

```Python
def replace_sequence(arr, *, before = False, after = True, min_len = 0, max_len = 5):
  """処理を書く"""

# 2以上3以下のFalseの連続をTrueに置き換える
input_arr = [False, False, True, False, False, False, False, True]
output_arr = replace_sequence(input_arr, before = False, after = True, min_len = 2, max_len = 3)
print(output_arr)
#output: [True, True, True, False, False, False, False, True]
```

# 実装

以下のように実装してみました。

```Python
def replace_sequence(arr, *, before = False, after = True, min_len = 0, max_len = 5):

  replaced_arr = [] # 置き換え後の配列
  start = -1 # 開始位置を初期化
  
  for i, v in enumerate(arr):
    # 要素がbeforeと異なり、開始位置が設定されている場合
    if v != before and start >= 0:
      # before要素の連続個数をseq_lenに代入
      seq_len = i - start 
      # seq_lenがmin_len以上max_len以下の場合、seq_len個のafterを追加
      if min_len <= seq_len <= max_len:
        replaced_arr += [after for _ in range(seq_len)]
      # それ以外の場合、seq_len個のbeforeを追加
      else:
        replaced_arr += [before for _ in range(seq_len)]
      # 要素をそのまま追加
      replaced_arr.append(v)
      # 開始位置を破棄
      start = -1
    # vがbeforeと異なり、開始位置が設定されていない場合
    elif v != before and start == -1:
      # 要素をそのまま追加
      replaced_arr.append(v)
    # vがbeforeと同じで、開始位置が設定されていない場合
    elif v == before and start == -1:
      # 開始位置を設定
      start = i
    # vがbeforeと同じで、開始位置が設定されている場合
    elif v == before and start >= 0:
      # 何もしない
      continue
  # 最後の要素がbeforeの場合（末尾にbeforeの連続がある場合）
  if start >= 0:
    seq_len = len(arr) - start
    if min_len <= seq_len <= max_len:
      replaced_arr += [after for _ in range(seq_len)]
    else:
      replaced_arr += [before for _ in range(seq_len)]
  return replaced_arr  
```

テストします。

```Python
import unittest

class TesteReplaceSequence(unittest.TestCase):
  def test_replace_sequence(self):
    # 2連続のFalseを置き換える
    self.assertEqual(replace_sequence(
        [True, False, False, True]
        , before = False, after = True
        , min_len = 2, max_len = 2
      ),
      [True, True, True, True]
    )
    
    # 2連続以上のFalseは置き換えない
    self.assertEqual(replace_sequence(
        [True, False, False, True]
        , before = False, after = True
        , min_len = 0, max_len = 1
      ),
      [True, False, False, True]
    )

    # TrueをFalseに置き換える
    self.assertEqual(replace_sequence(
        [True, False, False, True]
        , before = True, after = False
        , min_len = 0, max_len = 1
      ),
      [False,False, False, False]
    )


unittest.main(argv=['first-arg-is-ignored'], exit=False) # juypter notebookで実行する場合

```

よさそうです（テストとしては全然網羅できていないですが、おそらくは）。

```
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

# 別実装
[@WolfMoon さまのコメント](https://qiita.com/shimajiroxyz/items/5db02e02f9f16536d070#comment-cb92f9e069f6dbe93c22)を参考に、rle, edit, elrの3段階による処理を実装してみました（コメントありがとうございます）。rle関数の戻り値を(value, count）のペアの配列にしていることを始め、少し、自分が理解しやすいように書き換えていますが、だいたいはそのままなので、詳細はコメントをご覧ください。

自分で思いついた実装に比べると、各段階の処理が単純なためunittestが書きやすく、保守性が優れている気がします。

```Python
class SequenceReplacer:
  @staticmethod
  def rle(arr):
    """ run length encoding
    使用法　value_counts = rle(arr)
    arr はリスト, value_counts はリストに出現する連(run)と連の長さのペアを要素とするリスト
    例
    arr = [False, False, True, False, False, False, False, True]
    rle(arr) は [(False, 2), (True, 1), (False, 4), (True, 1)] を返す
    rle('001111000000011') は [('0', 2), ('1', 4), ('0', 7), ('1', 2)] を返す
    """
  
    if len(arr) == 0:
      return []

    value_counts = []
    count = 0
    for current, next in zip(arr[:-1], arr[1:]):
      count += 1
      if current != next:
        value_counts.append((current, count))
        count = 0
    count += 1
    value_counts.append([arr[-1], count])

    return value_counts

  @staticmethod
  def edit(value_counts, before, after, min_len, max_len):
    editted_value_counts = value_counts.copy()
    for i, (value, count) in enumerate(value_counts):
      if min_len <= count <= max_len and value == before:
        editted_value_counts[i] = (after, count)
    return editted_value_counts

  @staticmethod
  def elr(value_counts):
    """ rle(arr) の逆
    使用法　arr = elr(value_counts)
    elr([(False, 2), (True, 1), (False, 4), (True, 1)]) は
    [False, False, True, False, False, False, False, True] を返す
    elr( [ ('0', 2), ('1', 4), ('0', 7), ('1', 2) ] ) は長さ 15 のリストを返すので，文字列にするには
    ''.join(elr([ ('0', 2), ('1', 4), ('0', 7), ('1', 2) ])) とすれば '001111000000011' を得る
    """
    arr = [ ]
    for value, count in value_counts:
      arr += [ value for _ in range(count) ]
    
    return arr
  
  @classmethod
  def exec(cls, arr, *, before = False, after = True, min_len = 0, max_len = 5, edit_func = None):
    """rle, edit, elrの3段階で系列の置換を実施
    入力
    arr: 置換元の配列
    before: 置換対象の要素
    after: beforeの置換後の要素
    min_len, max_len: min_len以上max_len以下のbeforeの連続を同じ長さのafterに置き換える
    edit_func: 独自ルールで編集したい場合に設定。入力・出力ともに(value, count)のリストであることが必要。
               edit_funcが設定されている場合、before, after, min_len, max_lenは無視される。設定されていない（None）場合、cls.editが使われる
    出力
    replaced_arr: 置換後の配列

    使用法
    exec([False, False, True, False, False, False, False, True], before = False, after = True, min_len = 2, max_len = 2)
    の戻り値は
    [True, True, True, False, False, False, False, True]
    （ちょうど2のFalseの連続をTrueに置換）
    """
    edit_func = edit_func or (lambda x: cls.edit(x, before, after, min_len, max_len))
    value_counts = cls.rle(arr)
    editted_value_counts = edit_func(value_counts)
    replaced_arr = cls.elr(editted_value_counts)

    return replaced_arr
```

```Python
# ちょうど2つのFalseの連続をTrueに置換
output = SequenceReplacer.exec([False, False, True, False, False, False, False, True], before = False, after = True, min_len = 2, max_len = 2)
print(output)

# 独自の編集ルールのテスト（結果は上に同じ）
output = SequenceReplacer.exec([False, False, True, False, False, False, False, True], edit_func = lambda x: SequenceReplacer.edit(x, False, True, 2, 2))
print(output)
```

```
[True, True, True, False, False, False, False, True]
[True, True, True, False, False, False, False, True]
```