mantrog

京大漫トロピーのブログです

あらすじ割り振りDX計画

会誌制作もDXしたあああああい!!

ということで1353です。突然ですが新入生の皆さんは弊サークルがどんな活動をしているかご存知でしょうか?
麻雀?ボドゲ」まあ9割くらいそうですが(コロナで今はそれらも難しい)、実はちゃんとした活動もしています。それが会誌製作です。
漫トロの会誌は年2回発行(昨年は1回)していて、特に秋の会誌はその年の漫画ランキングをはじめとして非常にボリューミーな内容になっています。しかし、濃い内容に比例するように非常に会誌製作に手間がかかっているのも事実です。

具体的なタスクとして、

  • その年の漫画ランキングの提出
  • 締め切りに遅れた人への催促
  • クロスレビューの原稿提出
  • 締め切りを守らない人への催促
  • 座談会
  • 座談会文字起こし
  • 締め切りを破るどうしようもない人にパンチする
  • 原稿をフォーマットに流し込む
  • 印刷所との調整
  • 校正作業
  • この後に及んで締め切りを破るクズを処刑する
  • 折り作業 etc

ざっと挙げただけでもこれだけあります(実際はもっと多い)。年々会員の負担が増えてきているのも事実で、できることなら製作の工数を削減したいですよね。
前置きが長くなりましたが、
「タスクの一部をDXして工数を削減しよう!」
というのが今回の記事の趣旨になります。

あらすじ割り当て業務をDXする

今回DXするタスクは、あらすじの担当者の割り当て作業になります。私たちの会誌ではランキングの上位30位の漫画に対して、あらすじを載せます(下図参照)。

f:id:mantropy:20210525221324j:plain


シチョウくんも過去の記事で会誌のあらすじに触れていましたね。
mantropy.hatenablog.com

基本的に各漫画を高く評価している人がその漫画のあらすじを書きますが、どの作品にどの会員を割り当てるかは運営代が決めます。ただでさえ他の仕事で忙しい運営代の負担を減らすために、最適化の手法を用いて運営代の意思決定を支援します!

漫トロのランキングシステム

最適化の解説を行う前に、漫トロのランキングシステムについて簡単に説明します。まず各会員がその年に面白かった漫画を1~30位までランク付けします。順位の高さによって得点がつけられ、1位:30点、2位:29点、……30位:1点という形になります。例えば『対ありでした。 〜お嬢様は格闘ゲームなんてしない〜』は僕の個人ランキングで2位になっているので、この作品に29点加算されることになります。

これを全ての会員に行い得点を集計したものが、総合ランキングになります。会誌ネタバレを防ぐため、この記事では極力総合ランキングの内容には触れませんが、興味のある人は是非会誌を買ってください。twitterとかにDMを送ってね❤️

最適化する

話を戻して最適化について解説します。
最適化は大きく分けて下の3つのステップからなります。

  1. 決定変数を決める
  2. 目的関数を決める
  3. 制約条件を決める

ぶっちゃけ上の3つが決まればあとはソルバーがなんとかしてくれます。便利な時代ですね。それでは各ステップを見ていきましょう。

決定変数を決める

決定変数とは文字通り、最適化の過程において最終的に解を求める変数になります。
今回の問題で欲しいのは、「どの会員がどの作品のあらすじを担当するか」ということですよね。
その解を得るためにちょっと複雑ですが、i番目の会員がj番目の漫画のあらすじを担当するか否かを示す0 or1の変数を導入します(0:担当しない, 1:担当する)。
 x_{i, j} : i番目の会員がj番目の作品のあらすじを書くか否か


目的関数を決める

前述の通りなるべく作品に思い入れがある人にあらすじを書いて欲しいですよね。そこで目的関数を設定するにあたって、どの漫画をどれだけ評価しているかを示す重み行列を導入します。仰々しい表現ですが要は各会員が総合ランキング30位以内の漫画をどれだけ評価しているかということです。

 w_{i, j} : i番目の会員がj番目の作品にどれだけ得点を入れたか


そうすると目的関数は以下のように書けます。
maximize \displaystyle \sum_{i=1}^n \sum_{j=1}^m x_{i_j} w_{i_j}
nは会員数, mは作品数(今回は30)

制約条件を決める

最後に制約条件を決めましょう。条件として考えられるのは以下の二つです。

  1. 一人あたりの担当作品数がなるべく均等になるようにする
  2. 各作品ごとにあらすじ担当者が必ず一人割り振られるようにする

1人あたりの担当作品数の上限値と下限値を設定すると1の条件式は以下の通りになります。


全てのiに対して
最低担当作品数  \leq  \displaystyle \sum_{j=1}^m x_{i_j}
最高担当作品数  \geq  \displaystyle \sum_{j=1}^m x_{i_j}


続いて2つ目の条件式は各作品に対して一人の担当者を割り振るので

全てのjに対して
  \displaystyle \sum_{i=1}^n x_{i_j} =1


これで必要な式は全て出揃いました。

実装する

これをpythonで実装していきます。まず重み行列の作成ですが、レニが毎年ランキングデータをまとめてくれているので、この情報を用います。ですが人が作ったデータを勝手に記事にするのもあれなので、重み行列の作成については割愛します。
最適化はpythonpulpパッケージを用いて行います。関数はこんな感じ、

~
import numpy as np
import pandas as pd
import pulp as pp


def synopsis_distributer(weight_array, model_name, member_array, comic_array, output_file_path):
    #会員の人数、あらすじを書く漫画の作品数を取得する
    member_num, comic_num = weight_array.shape

    #一人あたり最低あらすじ数
    synopsis_pp_min = comic_num // member_num
    #一人あたり最高あらすじ数
    synopsis_pp_max = synopsis_pp_min + 1
    #均等にあらすじ担当が割り切れる時
    if comic_num % member_num == 0:
      synopsis_pp_max = synopsis_pp_min

    #問題を定義する,今回は最大値問題を設定する
    model = pp.LpProblem(name=model_name, sense=pp.LpMaximize)

    #決定変数を定義する
    x = pp.LpVariable.dicts("x", [(i, j) for i in range(member_num) for j in range(comic_num)], cat="Binary")
    
    #目的関数
    model += pp.lpSum(x[(i, j)] * weight_array[i][j] for i in range(member_num) for j in range(comic_num))

    #制約条件
    #第一条件
    for i in range(member_num):
        model += pp.lpSum(x[(i, j)] for j in range(comic_num)) >= synopsis_pp_min
        model += pp.lpSum(x[(i, j)] for j in range(comic_num)) <= synopsis_pp_max

    #第二条件
    for j in range(comic_num):
      model += pp.lpSum(x[(i, j)] for i in range(member_num)) == 1

    #モデルを解く
    model.resolve()

    #最適化された配列を作成する
    best_array = np.zeros(weight_array.shape)
    for i in range(member_num):
      for j in range(comic_num):
        best_array[i][j] = x[(i, j)].value()
    #結果をデータフレームに格納する
    best_synopsis_distribution = pd.DataFrame(best_array, index=member_array, columns=comic_array)
    #結果をcsvで保存する
    best_synopsis_distribution.to_csv(output_file_path)
    return best_synopsis_distribution


~

結果

スクリプトを実行すると下の写真のように各会員のあらすじ担当作品が出力されます。

f:id:mantropy:20210525214758p:plain
本来はヘッダーに漫画タイトルが存在しますが、会誌のネタバレ防止のため黒塗りしています

出力結果において僕のあらすじの担当作品は「対あり」と『往生際の意味を知れ!』となっています。

これらは僕の個人ランキングにおいて2位と3位の作品であり、この結果からおそらく最適なあらすじ担当の組み合わせが出力されていると考えられます。やったね!!

最後に

これにてあらすじ割り振り業務のDXが完了しました。もう運営代が頭を悩ますことはなくなるので、ちょっとだけ業務が楽になりますね。普段老害と呼ばれているので、たまには益になることができて良かった。

ちなみにですが、ちゃんと担当を割り振ったところで何人かは締め切りをぶっちして代筆が立てられるので、最適化しても正直インパクトは薄いです。というか会誌製作の業務が大変な理由の半分くらいは会員が締め切りを守らないこと。DXとかビッグデータ活用とかの現場ってこんな状況ばっかりなんでしょうね、悲しいなあ。なのでタスクを軽くしたいなら締め切りを破る奴を減らすのが一番手っ取り早いです。ということで、
みんな締め切りは守ろうね!!