itertoolsのcombinations関数で組み合わせ【Python tips】

Python組合せ Python

 

こんにちは、monachan_papaです。

今回は私の推しのちんどん屋さんについての検証をPythonでしてみたいと思います。
何をするかというと、、、、

ちんどん屋さんの出動メンバ組合せ

ちんどん屋さんはたいてい3人だったり、4人だったりのチームで街頭宣伝や賑やかしをするのが古くからのスタイルであり、スタンダードです。
私の推しのちんどん屋さんも同じく、このスタイルをとっています。
そして、推しのちんどん屋さんは、何と!
総勢10名のメンバ(ただし、メインのゴールデンメンバは7名)を擁しておられます。つまり、この中から数名選抜し出動するというわけなんです。

この選抜メンバが誰かな?!というのが、毎回楽しみであります!
今日、ふと思ったことがあります。

  • 選抜メンバの組合せ総数はどれだけだろう?
  • 今までどんな組合せで何通り見てきたのだろうか?
  • まだ見たことのない組合せは?

このマニアックで素敵すぎる疑問を解決するために、私は立ち上がった!
さて、解決のための前提条件、検証方法を以下に挙げます。

  • 今まで見てきた組合せはすべて記憶しているし、データとしても持っている。
  • メインのゴールデンメンバ7名から3名を選抜する組み合わせを本検証とする。

以下のように進みます。では、いってみましょう!

 

組合せとは?

組合せといったら、高校数学に出てくる順列、組合せが真っ先に出てくるかと思います。
まずは、簡単に定義をはっきりさせましょう。下表に示します。

順列 組合せ
nPk nCk
異なるn個からk個選び並べる 異なるn個からk個選び並べる
順番を考慮する! 順番は関係ねぇ!

懐かしい人もいるでしょうね。
組合せで最重要なのは順番は関係ねぇ!ということです。

例えば、A、B、C、Dから3つ選ぶとき、ABC、ACB、BAC、BCA、CAB、CBAは同じということです。

 

組合せの算出方法

さあ、やっとこさ、Pythonの出番です!
組合せ算出にとても便利なライブラリがあるんです。
itertoolsモジュールというのものが標準で入っていて、この中のcombinations関数を使えば、楽勝で片付けることができるのです。

とりあえず、importしましょう!

import itertools

怒られなかったら、うまくいっているハズです!

 

itertools.combinations関数について

関数の説明をします。

itertools.combinaitons(引数1, 引数2)

  • 引数1: リストなどのイテレータを指定
  • 引数2: 選択する要素数

めちゃくちゃ簡単です!

ではそっそく、引数1のイテレータを準備しましょう。

ゴールデンメンバの7名、ついに来た!!

names = 'うっしー 駒子 ワッキー マーサ スージー 公乃 すずこ'.split()
names

▼出力結果

['うっしー', '駒子', 'ワッキー', 'マーサ', 'スージー', '公乃', 'すずこ']

しっかり、7名いらっしゃいました!
この中から3名選抜するわけなので、引数2は3となります。

 

組合せ総数と全通りの表示

ガンガンいきますぜ!組合せ総数と、出動メンバの全通りを表示します。

 

関数の戻り値は、リスト型に変換するのがポイントです!

com = list(itertools.combinations(names, 3))
print(f'組合せ総数: {len(com)}通り')
com

▼出力結果

組合せ総数: 35通り

[('うっしー', '駒子', 'ワッキー'),
 ('うっしー', '駒子', 'マーサ'),
 ('うっしー', '駒子', 'スージー'),
 ('うっしー', '駒子', '公乃'),
 ('うっしー', '駒子', 'すずこ'),
 ('うっしー', 'ワッキー', 'マーサ'),
 ('うっしー', 'ワッキー', 'スージー'),
 ('うっしー', 'ワッキー', '公乃'),
 ('うっしー', 'ワッキー', 'すずこ'),
 ('うっしー', 'マーサ', 'スージー'),
 ('うっしー', 'マーサ', '公乃'),
 ('うっしー', 'マーサ', 'すずこ'),
 ('うっしー', 'スージー', '公乃'),
 ('うっしー', 'スージー', 'すずこ'),
 ('うっしー', '公乃', 'すずこ'),
 ('駒子', 'ワッキー', 'マーサ'),
 ('駒子', 'ワッキー', 'スージー'),
 ('駒子', 'ワッキー', '公乃'),
 ('駒子', 'ワッキー', 'すずこ'),
 ('駒子', 'マーサ', 'スージー'),
 ('駒子', 'マーサ', '公乃'),
 ('駒子', 'マーサ', 'すずこ'),
 ('駒子', 'スージー', '公乃'),
 ('駒子', 'スージー', 'すずこ'),
 ('駒子', '公乃', 'すずこ'),
 ('ワッキー', 'マーサ', 'スージー'),
 ('ワッキー', 'マーサ', '公乃'),
 ('ワッキー', 'マーサ', 'すずこ'),
 ('ワッキー', 'スージー', '公乃'),
 ('ワッキー', 'スージー', 'すずこ'),
 ('ワッキー', '公乃', 'すずこ'),
 ('マーサ', 'スージー', '公乃'),
 ('マーサ', 'スージー', 'すずこ'),
 ('マーサ', '公乃', 'すずこ'),
 ('スージー', '公乃', 'すずこ')]

35通り。

この数字は世の中が良くなれば実際、一年間で現実的に部隊編成可能な数字だと思います。 ちんどん屋さんのスケジュールとか諸々の調整がもし、うまくいったら不可能ではない数字です。
また、各メンバは35回のうち、15回出動すれば実現可能なことが分かります。

そして、私も最終的にすべての選抜メンバ組合せ、いつかコンプリートできるかと思うと夢が膨らみます!

余談ですが、もしこれがフルメンバ10名から3名選抜となると、不可能な数字なんです。
120通りになるんですよ!さすがに無理でしょう。

 

見果てぬ夢、「組合せの差分」をとって現実に近づけろ!

では、最後の検証です!見果てぬ夢は、分析しまくるしかない。
私はあとどれだけの選抜メンバに遭遇していないのか?まず、それを知らねばなるまい。

以下、ゴールデンメンバ7人から3人選抜した場合限定の私が出会った記録のデータです。
4人選抜時とか入れたらコンプリートに近づきますが、小生も男です!
男らしく対象外とします!すべて3人選抜時限定です!

exp = [
    ['マーサ', 'スージー', 'すずこ'],
    ['ワッキー', '公乃', 'すずこ'],
    ['駒子', 'ワッキー', 'すずこ'],
    ['うっしー', 'スージー', 'すずこ'],
    ['うっしー', '駒子', 'スージー'],
    ['駒子', 'マーサ', '公乃'],
    ['駒子', '公乃', 'すずこ'],
    ['駒子', 'スージー', 'すずこ'],
    ['うっしー', '駒子', 'マーサ'],
    ['うっしー', '駒子', 'スージー'],
    ['うっしー', 'ワッキー', '公乃'],
    ['駒子', 'ワッキー', 'スージー'],
]

print(f'私が遭遇した3人選抜一覧数: {len(exp)}')

▼出力結果

私が遭遇した3人選抜一覧数: 12

さて、私がまだ遭遇していない選抜メンバは、単純に 35-12=23 というわけにはいきません。
総組合せから、私のデータを引くことに変わりはありません。しかし、まずは、、、

データの正規化をしないといけません!

そして、解を導き出すために、集合演算の差集合を利用する。そのための正規化をします。

まずは私が遭遇した3人選抜一覧。差集合をとるためには、いくつか問題点があります。
以下のように対応すれば実現ができます!

  • リストの中の各リストの要素を昇順ソートする(正しく差分がとれないため)
  • リストの中の各リストをタプル化する(タプル化しないと、集合型にできないため)
  • set関数で集合型に変換(重複項目が現在あるが、このとき排除できる)

では、一気に片付けましょう!

exp_ = [tuple(sorted(e)) for e in exp]
exp_ = set(exp_)
print(f'私が遭遇した3人選抜一覧数: {len(exp_)}')
exp_

▼出力結果

私が遭遇した3人選抜一覧数: 11

{('うっしー', 'すずこ', 'スージー'),
 ('うっしー', 'スージー', '駒子'),
 ('うっしー', 'マーサ', '駒子'),
 ('うっしー', 'ワッキー', '公乃'),
 ('すずこ', 'スージー', 'マーサ'),
 ('すずこ', 'スージー', '駒子'),
 ('すずこ', 'ワッキー', '公乃'),
 ('すずこ', 'ワッキー', '駒子'),
 ('すずこ', '公乃', '駒子'),
 ('スージー', 'ワッキー', '駒子'),
 ('マーサ', '公乃', '駒子')}

重複項目が1項目ありましたね。同じ組合せで遭遇したことがあったということはラッキーですが、ここでは正しく分析するためにグッと我慢します!
型変換もうまくいきました。

つづいて、同様に一番初めに用意した総組合せの方も正規化をします。
正規化はたった1行でいきます!内包表記はとても便利ですね。

ここでのポイントは、、、、

ソートをかけることにより、もともとタプルだったものがリスト化されてしまうで、またタプルに戻してあげましょう!

com_ = {tuple(sorted(c)) for c in com}
print(f'組合せ総数: {len(com)}通り')
com_
組合せ総数: 35通り

{('うっしー', 'すずこ', 'スージー'),
 ('うっしー', 'すずこ', 'マーサ'),
 ('うっしー', 'すずこ', 'ワッキー'),
 ('うっしー', 'すずこ', '公乃'),
 ('うっしー', 'すずこ', '駒子'),
 ('うっしー', 'スージー', 'マーサ'),
 ('うっしー', 'スージー', 'ワッキー'),
 ('うっしー', 'スージー', '公乃'),
 ('うっしー', 'スージー', '駒子'),
 ('うっしー', 'マーサ', 'ワッキー'),
 ('うっしー', 'マーサ', '公乃'),
 ('うっしー', 'マーサ', '駒子'),
 ('うっしー', 'ワッキー', '公乃'),
 ('うっしー', 'ワッキー', '駒子'),
 ('うっしー', '公乃', '駒子'),
 ('すずこ', 'スージー', 'マーサ'),
 ('すずこ', 'スージー', 'ワッキー'),
 ('すずこ', 'スージー', '公乃'),
 ('すずこ', 'スージー', '駒子'),
 ('すずこ', 'マーサ', 'ワッキー'),
 ('すずこ', 'マーサ', '公乃'),
 ('すずこ', 'マーサ', '駒子'),
 ('すずこ', 'ワッキー', '公乃'),
 ('すずこ', 'ワッキー', '駒子'),
 ('すずこ', '公乃', '駒子'),
 ('スージー', 'マーサ', 'ワッキー'),
 ('スージー', 'マーサ', '公乃'),
 ('スージー', 'マーサ', '駒子'),
 ('スージー', 'ワッキー', '公乃'),
 ('スージー', 'ワッキー', '駒子'),
 ('スージー', '公乃', '駒子'),
 ('マーサ', 'ワッキー', '公乃'),
 ('マーサ', 'ワッキー', '駒子'),
 ('マーサ', '公乃', '駒子'),
 ('ワッキー', '公乃', '駒子')}

さあ、準備がすべて整いました!
いけ〜!

ret = com_ - exp_
print(f'まだ遭遇していない組合せ数: {len(ret)}通り')
ret

▼出力結果

まだ遭遇していない組合せ数: 24通り

{('うっしー', 'すずこ', 'マーサ'),
 ('うっしー', 'すずこ', 'ワッキー'),
 ('うっしー', 'すずこ', '公乃'),
 ('うっしー', 'すずこ', '駒子'),
 ('うっしー', 'スージー', 'マーサ'),
 ('うっしー', 'スージー', 'ワッキー'),
 ('うっしー', 'スージー', '公乃'),
 ('うっしー', 'マーサ', 'ワッキー'),
 ('うっしー', 'マーサ', '公乃'),
 ('うっしー', 'ワッキー', '駒子'),
 ('うっしー', '公乃', '駒子'),
 ('すずこ', 'スージー', 'ワッキー'),
 ('すずこ', 'スージー', '公乃'),
 ('すずこ', 'マーサ', 'ワッキー'),
 ('すずこ', 'マーサ', '公乃'),
 ('すずこ', 'マーサ', '駒子'),
 ('スージー', 'マーサ', 'ワッキー'),
 ('スージー', 'マーサ', '公乃'),
 ('スージー', 'マーサ', '駒子'),
 ('スージー', 'ワッキー', '公乃'),
 ('スージー', '公乃', '駒子'),
 ('マーサ', 'ワッキー', '公乃'),
 ('マーサ', 'ワッキー', '駒子'),
 ('ワッキー', '公乃', '駒子')}

まだ見ぬ24通り。

うむ!まだまだ先は長いが、頑張ろう。
そして、ちんどん追っかけ歴も4年目に突入してきたので、色んなデータが蓄積されてきました。

こうやって、振り返ると感慨深いものがあるなあと思った、秋の夜。

 

コメント

タイトルとURLをコピーしました