2個のデータ列を交互に合わせるシンプルな方法・速度比較 - ぼんやり生成AIに質問していたらベンチマークするテストコードが出来上がっていた

「2個のデータ列を交互に合わせる」シンプルな方法は何なのか…とぼんやり思って、生成AIに質問していたら、どんどん話が進んで、「2個のデータ列を交互に合わせる」方法を比較するテストコードが出来上がったので、記載します。

処理速度を実際に計測してどの方法がよいかが分かる、正しく動作するテストコードが苦労することもなく得られました。生成AI(ChatGPT)、素晴らしい。

# 生成AIに質問(ChatGPT)

何もない状態から、生成AI(ChatGPT)に下記の質問をして、「2個のデータ列を交互に合わせる」方法を比較するテストコードが出来上がりました。

* Pythonで、1Dのデータ列を交互に分けて2つのデータ列にしたい  
* 分けた `data1`, `data2` のデータ列を、元の1Dデータ列に戻したい  
* もっと効率的でもっとシンプルな方法は？  
* `zip` を使わないで実現する方法は？  
* `zip` を使う方法（例：`restored = list(chain.from_iterable(zip(data1, data2)))`）と、  
  `zip` を使わない方法（例：`restored[::2] = data1, restored[1::2] = data2`）で、  
  どちらが高速で処理できるのか？  
* NumPyを使った高速ベクトル演算版の例が見たい  
* `timeit.timeit` に関数で渡す方法で、前述の3ケースの比較を行うコードがほしい  
* `# --------- 処理の確認 ---------` のセクションを追加して、処理結果が想定通りか確認するコードを追加したい  
* 結果のデータ列の末尾辺りを表示して、実際の値を確認したい  
* `# --------- 1. zip + chain.from_iterable ---------` の方法で、入力データ列のサイズが異なる場合の対処が出来ていない  
* `assert result_zip == result_slice == result_numpy, "出力結果が一致しません"` の部分について、  
  assertで中断しないようにしたい（処理は続行）  

## 質問・回答の全履歴

https://chatgpt.com/share/67fa869e-c4e4-8004-8255-350bd7181062

# テストコード

「2個のデータ列を交互に合わせる」方法を比較するテストコード: 
(生成AIが作成したコードに、出力例等のメモを追加記載したもの)

```py:test__2個のデータ列を交互に合わせる速度比較.py
# 2個のデータ列を交互に合わせる方法3つ、速度比較

# 生成AIに聞く: https://chatgpt.com/share/67fa869e-c4e4-8004-8255-350bd7181062

# 結果: 動作OK
# 出力例: 
"""
▼ 結果データ列の末尾10要素:
expected      : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
zip + chain   : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
スライス代入   : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
NumPy         : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
✔ すべての手法で結果が一致しました。

▼ ベンチマーク(100回実行)
データ列のサイズ: data1: 50001, data2: 50000
zip + chain:    0.407367 秒（0.004074 秒/回）
スライス代入:    0.175750 秒（0.001758 秒/回）
NumPy:          0.044295 秒（0.000443 秒/回）
"""

import timeit
from itertools import chain
import numpy as np

# --------- 共通のデータセットを準備 ---------
N = 100_000
data1_list = list(range(0, N, 2)) + [-1] # +[-1]:長さが異なる場合の模擬
data2_list = list(range(1, N, 2))
data1_np = np.array(data1_list)
data2_np = np.array(data2_list)

# --------- 1. zip + chain.from_iterable ---------
def method_zip_chain():
    # zip は短い方で止まる
    restored = list(chain.from_iterable(zip(data1_list, data2_list)))

    # 長さの差を考慮して、余りを追加
    if len(data1_list) > len(data2_list):
        restored.extend(data1_list[len(data2_list):])
    elif len(data2_list) > len(data1_list):
        restored.extend(data2_list[len(data1_list):])

    return restored

# --------- 2. スライス代入（リスト） ---------
def method_slice_assign():
    restored = [0] * (len(data1_list) + len(data2_list))
    restored[::2] = data1_list
    restored[1::2] = data2_list
    return restored

# --------- 3. NumPy（empty + スライス代入） ---------
def method_numpy():
    restored = np.empty(len(data1_np) + len(data2_np), dtype=data1_np.dtype)
    restored[::2] = data1_np
    restored[1::2] = data2_np
    return restored

# --------- 処理の確認 ---------
expected = list(range(N)) + [-1] # +[-1]:長さが異なる場合の模擬
result_zip   = method_zip_chain()
result_slice = method_slice_assign()
result_numpy = method_numpy().tolist()  # NumPy → list に変換して比較

# 末尾10要素の表示
print("▼ 結果データ列の末尾10要素:")
print("expected      :", expected[-10:])
print("zip + chain   :", result_zip[-10:])
print("スライス代入   :", result_slice[-10:])
print("NumPy         :", result_numpy[-10:])

if expected == result_zip == result_slice == result_numpy:
    print("✔ すべての手法で結果が一致しました。")
else:
    print("✖ 結果が一致しません！")

# --------- ベンチマーク ---------
number = 100

zip_time   = timeit.timeit(method_zip_chain, number=number)
slice_time = timeit.timeit(method_slice_assign, number=number)
numpy_time = timeit.timeit(method_numpy, number=number)

print(f"\n▼ ベンチマーク({number}回実行)")
print(f"データ列のサイズ: data1: {len(data1_list)}, data2: {len(data2_list)}")
print(f"zip + chain:    {zip_time:.6f} 秒（{zip_time/number:.6f} 秒/回）")
print(f"スライス代入:    {slice_time:.6f} 秒（{slice_time/number:.6f} 秒/回）")
print(f"NumPy:          {numpy_time:.6f} 秒（{numpy_time/number:.6f} 秒/回）")
```

# 実行結果

上記コードの出力例: 
```
▼ 結果データ列の末尾10要素:
expected      : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
zip + chain   : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
スライス代入   : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
NumPy         : [99991, 99992, 99993, 99994, 99995, 99996, 99997, 99998, 99999, -1]
✔ すべての手法で結果が一致しました。

▼ ベンチマーク(100回実行)
データ列のサイズ: data1: 50001, data2: 50000
zip + chain:    0.407367 秒（0.004074 秒/回）
スライス代入:    0.175750 秒（0.001758 秒/回）
NumPy:          0.044295 秒（0.000443 秒/回）
```

3番目「Numpy」の方法が、非常に高速。

# 最も「シンプル」かつ「高速処理」となる方法

前述のベンチマーク結果から、最も「シンプル」かつ「高速処理」となる方法は、「NumPy + スライス代入」の方法と分かりました。