icuniicuの長いツイート

どうも、@icuniicuと申す者です。Twitterに書きづらいツイートをもっと自由に呟きます。

マッチングアプリSILKのシミュレーションを書いてみた

プログラミングの強みの1つとして、シミュレーションができることだと思っていた。自分でも何かシミュレーションを書いてみようと思い、SILKを題材とした。

SILKに登録した経緯

SILKは男子大学生と社会人女性をマッチングするアプリだ。
一般的にマッチングアプリでは男性数が多すぎることが多い。しかしSILKでは男子大学生しか登録することができないので、その問題が解決されているだろうと踏んで登録した。また、自分は比較的幼い見た目なので、年下好きに魅力的に映るのではという期待もあった。

SILKを使った感想

最初の数日は少し楽しかった。数人の女性が自分のプロフィールを閲覧していた。また1人とマッチした(しかしチャットに返信が来ることはなかった)。

1週間も経つと、誰も僕のプロフィールを閲覧しなくなっていた。それでも毎日女性にいいね!を押し続け、反応を待った。3週間が経とうとしているが、誰ともマッチしないどころか、誰も僕のプロフィールを閲覧することもない。

モチベーションも切れ始めている。

SILKの数字

女性数と男性数の比はどれくらいなのだろうか。アプリ内で女性の数を数えてみると、220人程度だった。しかし男性数は推測するしかない。
SILKのホームページには「事前登録者数15000人」との記載があった。なるほど、事前登録者が全員使っていなかったとしても、1万人くらいいるわけか。まてよ、女性が220人だから...、男女比1:50もある!!

サービス開始直後は人数が出そろっていなかったため、僕のプロフィールも多少閲覧された。しかし人数が1:50程度に安定してしまった今、ほとんど閲覧されないことも納得がいく。

シミュレーションの設定

女性200人、男性10000人に設定した。
好みのタイプを一人一人に設定し(マッチョ、グラマー、クール、カワイイ)、その好みの異性に対していいね!をするようにした。
お互いにいいね!を押したらマッチする。お互いにチャットする余裕が残っていればチャットできる。

細かい設定は書く元気がない。なぜなら現在はSILKそのものにモチベーションがほとんどないからだ。(いないと思うが)細かい設定を知りたい人はソースコードを頑張って読んで欲しい。ソースコードにコメントあまりついてなくてすみません。

シミュレーションの結果

魅力的な人からどんどんマッチしてゆく。一方で魅力がない人はマッチしない。
至極当然な結果が出た。

チャットまで辿り着けていない人の推移。day1~day30

両方とも時間が経つほどチャットまで行ける人数が増えている。しかしやはり男性はそのスピードが遅い。このシミュレーションでは95%は誰ともチャットしていない。女性は80%くらいの人がチャットまで辿り着いている。

f:id:ilohaspeach:20190103113844p:plainf:id:ilohaspeach:20190103113847p:plain
チャットできていない人数の推移

マッチすらできなかった人は、どのような人なのか?day1~day30

男性は横軸にマッチョ度、縦軸にクール度、円の大きさをカワイイ度に設定した。しかし人数が多すぎてカワイイ度は目視できない。すみません。
マッチョな人、クールな人の人数がday30ではほんの少し減っていることが確認できると思う。

女性は横軸にグラマー度、縦軸にクール度、円の大きさをカワイイ度に設定した。グラマーで、クールな人は、day30には残っていない。

f:id:ilohaspeach:20190103114037p:plainf:id:ilohaspeach:20190103114042p:plain
マッチできなかった人の特性

総合力がある人はちゃんとチャットまで辿り着いたのか?

魅力の合計得点を指標に、全員の総合スコアを計算した。総合スコアが高ければちゃんとチャットまで辿り着いているはずだ。
結果を見ても、大体その傾向があるということが分かる。

f:id:ilohaspeach:20190103115213p:plainf:id:ilohaspeach:20190103115217p:plain
総合力がある人はチャットまで辿り着いているのか





考察

シミュレーションを書いていくうちにマッチングサービスの課題をいくつか発見した。

①一番魅力的な人に票が集中してしまう

普通、表示されている異性の中から一番魅力的な人を選んでいいね!することになる。もし多くの人が同じ人を魅力的だと感じてしまうなら、その人にいいね!が集まりすぎてしまう。例えば、SILKの男性陣が同じ女性をいいね!と思ったら、その一人に10000件のいいね!が届いてしまう。実際にその人とチャットできる人はほんの数人なのにも関わらず、である。
ここに非効率が生まれてしまう。

②選ぶ相手が多すぎると、ベストな相手を選べない

男性側は全ての女性を苦なく閲覧できる。なんてったって200人しかいないのだから。
一方で女性は10000人の男性を閲覧しなくては、ベストな男性を選ぶことができない。SILKではここに問題を抱えている。

例えば最強のイケメンがいたとしても、女性側は10000人のプロフィールを見てられないので、そこから見つけ出せるか分からない。そこはSILKがどのような順にプロフィールを表示するかの問題、確率の問題になる。

あ、書いてて気づいたけど、ICUだから低く表示されてる説出てきた。ICUだから女性から選ばれない説も出てきた。学歴は表示されるからな~。

マッチングアプリwithではAIを使って自分に合いそうな人をリコメンドしてくれる。選ぶ人数が増えるほど、ベストな相手を見つけやすくすることに価値がある。

③人は異性の望む形に生まれるとは限らない

マッチョ、グラマー、クール、カワイイの需要と供給が上手く満たされないのが、マッチングを遅らせている。マッチング数を至上命題とするなら、人が異性の望む形になっていないというのが問題になる。
ある程度個人が努力して需要に合わせていくことはできるが...。

ここに関しては見た目ベースで人を判断するマッチング市場の問題とも言えるかもしれない。

シミュレーションの感想

シミュレーションは僕の好きなように状況を設定できる。その意味で出来レースに感じてしまうかもしれない。しかしシミュレーションには意味があると感じた。

状況を整理することができた

シミュレーションを書くためには、SILKのマッチングゲームの内容をきちんと把握しなくてはならない。どこが制約になっているのか、どこに問題があるのか、メタな視点で考えることができた。

自分がチャットまで辿り着けないことに納得できた。

相当魅力的な男性でも、確率が悪ければマッチしないかもしれない。チャットしていないからといって、僕に魅力がないというわけではないと確認して安心した。(しかしこのシミュレーションは僕が「最強のイケメン」ではないということは暗示している笑)


まとめ

僕と似た感情を抱く男子大学生は少なくない。なぜなら男性の登録者数は10000人もいるだろうから!それにシミュレーションに示されているように、相当数の男子大学生はチャットまで辿り着いていないだろう。近いうちに不満を抱えた男子大学生が退会することになると思う。
一方で好みの大学生を見つけた女性陣も退会したり、使わなくなったりする。すると...?

希望を持つが、誰ともマッチしない男子大学生が登録しているだけのアプリになる。

重要なのは女性数を増やすことだと思う。男性が大学生からしか登録できないのに対して、女性にはその制限がない。人口比率だけ見れば女性数の方が圧倒的に多いのだから、そこは頼むわ!

今のところ、SILKの魅力は「男性登録者から、男子大学生以外を排除している」点にしかない。それ以外の点では他のマッチングアプリに及ばない。(UI/UXあたりでも使いにくさが目立つし、検索機能も付いていないし、相手との相性を計算する機能もない...強さが全く感じられない)

SILKがこれから強くなれるかは、まず女性登録者数を稼げるかという点、UI/UXを他社と同等くらいのレベルまで上げられるかという点、そして男子大学生限定というユニークさをどこまで活かせるかという点にあると思う。

なんかいろいろ書いたけど、モチベーションもないし、僕はそろそろ使わなくなると思う。

でも最強美女/イケメンなら死ぬほどいいね!来るだろうから、無料だし(1/31まで)、やってみてもいいと思う!

コード

人間のクラスを定義した。

import random
import numpy as np


class Human:
    # chat means the one s/he is chatting with.
    # the maximum number of chats at a time is determined by numbers below.
    max_num_chat_mean = 3
    max_num_chat_sd = 1

    max_like_per_day = 3

    def __init__(self):
        # to be overridden
        self.preference = -1

        self.strike_zone = random.randrange(30, 100, 1) / 100

        # glamor, cool, kawaii for female. macho, cool, kawaii for male.
        self.charm = [random.random(), random.random(), random.random()]

        self.score = (self.charm[0] + self.charm[1] + self.charm[2]) / 3

        self.like_set = set()

        self.liked_set = set()

        self.match_set = set()

        # the list of the males who a female is now communicating with
        self.chat_set = set()

        random_number = round(abs(
            np.random.normal(
                Human.max_num_chat_mean,
                Human.max_num_chat_sd
                )
            ))

        if random_number == 0:
            self.max_num_chat = 1
        else:
            self.max_num_chat = random_number

    def like(self, opposite_sex_list):
        opposite_sex_list = opposite_sex_list + list(self.liked_set)
        candidate = set([i for i in opposite_sex_list if i.charm[self.preference] > self.strike_zone])
        candidate = candidate.difference(self.like_set)
        candidate = sorted(
            candidate,
            key=lambda x: x.charm[self.preference],
            reverse=True
        )

        candidate_like_today = set(candidate[0:Human.max_like_per_day])

        self.like_set = self.like_set.union(candidate_like_today)

        for human in candidate_like_today:
            human.liked_set.add(self)

    def match(self):
        for opposite_sex in self.like_set:
            if self in opposite_sex.like_set:
                self.match_set.add(opposite_sex)
                opposite_sex.match_set.add(self)

    def chat(self):
        num_to_add = self.max_num_chat - len(self.chat_set)
        if num_to_add == 0:
            pass

        humans_to_add = sorted(
            self.match_set,
            key=lambda x: x.charm[self.preference],
            reverse=True
            )

        for human in humans_to_add:
            if not human.is_occupied() and not self.is_occupied():
                self.chat_set.add(human)
                human.chat_set.add(self)

    def is_occupied(self):
        if len(self.chat_set) >= self.max_num_chat:
            return True
        else:
            return False


class Female(Human):
    number = 0

    # 0: macho, 1: cool, 2: kawaii
    # preference_seed is the probability that a female prefers each category.
    preference_seed = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2]

    def __init__(self):
        super().__init__()
        self.preference = random.choice(Female.preference_seed)

        self.id = Female.number
        Female.number += 1


class Male(Human):
    number = 0

    # 0: glamor, 1: cool, 2: kawaii
    preference_seed = [0, 0, 0, 0, 1, 1, 1, 2, 2, 2]

    def __init__(self):
        super().__init__()
        self.preference = random.choice(Male.preference_seed)

        self.id = Male.number
        Male.number += 1

実際に動かすコードをこちらに書いた。

import random
from Human import Male, Female
import matplotlib.pyplot as plt


def main():
    num_of_female = 200
    num_of_male = 10000

    female_list = []
    male_list = []

    for i in range(0, num_of_female):
        female_list.append(Female())

    for i in range(0, num_of_male):
        male_list.append(Male())

    human_list = female_list + male_list

    # the number of people which a person browse per day
    num_browse = 200

    day_start = 1
    day_end = 30

    today = day_start

    male_without_match = []
    male_without_chat = []
    female_without_match = []
    female_without_chat = []

    male_without_match_time_series = []
    male_without_chat_time_series = []
    female_without_match_time_series = []
    female_without_chat_time_series = []

    while(today <= day_end):
        male_without_match = []
        male_without_chat = []
        female_without_match = []
        female_without_chat = []

        random.shuffle(male_list)
        random.shuffle(female_list)

        for male in male_list:
            random.shuffle(female_list)
            male.like(female_list[0:num_browse])

        for female in female_list:
            random.shuffle(male_list)
            female.like(male_list[0:num_browse])

        for human in human_list:
            human.match()

        for human in human_list:
            human.chat()

        print("*****day" + str(today) + "*****")

        for male in male_list:
            if len(male.match_set) == 0:
                male_without_match.append(male)
            if len(male.chat_set) == 0:
                male_without_chat.append(male)

        for female in female_list:
            if len(female.match_set) == 0:
                female_without_match.append(female)
            if len(female.chat_set) == 0:
                female_without_chat.append(female)

        male_without_match_time_series.append(male_without_match)
        male_without_chat_time_series.append(male_without_chat)
        female_without_match_time_series.append(female_without_match)
        female_without_chat_time_series.append(female_without_chat)

        today += 1

    # Plotting
    # See the difference of before and after
    # What kind of males were able to chat with females?
    original_macho = [i.charm[0] for i in male_list]
    original_cool = [i.charm[1] for i in male_list]
    original_kawaii = [i.charm[2] for i in male_list]

    plt.subplot(1, 2, 1)
    plt.xlabel("macho level")
    plt.ylabel("cool level")
    plt.title("before")
    plt.ylim(0, 1)
    plt.xlim(0, 1)
    plt.scatter(original_macho, original_cool, alpha=100 / num_of_male, s=[i*100 for i in original_kawaii])

    left_macho = [i.charm[0] for i in male_without_match]
    left_cool = [i.charm[1] for i in male_without_match]
    left_kawaii = [i.charm[2] for i in male_without_match]

    plt.subplot(1, 2, 2)
    plt.title("after")
    plt.ylim(0, 1)
    plt.xlim(0, 1)
    plt.scatter(left_macho, left_cool, alpha=100 / num_of_male, s=[i*100 for i in left_kawaii])
    plt.suptitle("the number of males binned by their charm")
    plt.show()

    original_glamor = [i.charm[0] for i in female_list]
    original_cool = [i.charm[1] for i in female_list]
    original_kawaii = [i.charm[2] for i in female_list]

    plt.subplot(1, 2, 1)

    plt.scatter(original_glamor, original_cool, alpha=100 / num_of_female, s=[i*100 for i in original_kawaii], c='red')
    plt.xlabel("glamor level")
    plt.ylabel("cool level")
    plt.title("before")
    plt.ylim(0, 1)
    plt.xlim(0, 1)
    left_glamor = [i.charm[0] for i in female_without_match]
    left_cool = [i.charm[1] for i in female_without_match]
    left_kawaii = [i.charm[2] for i in female_without_match]

    plt.subplot(1, 2, 2)
    plt.title("after")
    plt.ylim(0, 1)
    plt.xlim(0, 1)
    plt.scatter(left_glamor, left_cool, alpha=100 / num_of_female, s=[i*100 for i in left_kawaii], c='red')
    plt.suptitle("the number of females binned by their charm")
    plt.show()

    # score means the sum of their attractiveness
    # we're gonna see the difference of the number of people
    # before and after the simulation, sorted by their score.
    original_score = [i.score for i in male_list]
    left_score = [i.score for i in male_without_match]

    plt.subplot(1, 2, 1)
    plt.title("before")
    plt.ylim(0, num_of_male / 3)
    hist_original = plt.hist(original_score, bins=10, range=(0, 1))

    plt.subplot(1, 2, 2)
    plt.title("after")
    plt.ylim(0, num_of_male / 3)
    hist_left = plt.hist(left_score, bins=10, range=(0, 1))

    plt.suptitle("the number of males binned by their attractiveness")
    plt.show()

    original_score = [i.score for i in female_list]
    left_score = [i.score for i in female_without_match]

    plt.subplot(1, 2, 1)
    plt.title("before")
    plt.ylim(0, num_of_female / 3)
    plt.xlabel("attractiveness")
    hist_original_f = plt.hist(original_score, bins=10, range=(0, 1), color='red')

    plt.subplot(1, 2, 2)
    plt.title("after")
    plt.ylim(0, num_of_female / 3)
    plt.xlabel("attractiveness")
    hist_left_f = plt.hist(left_score, bins=10, range=(0, 1), color='red')

    plt.suptitle("the number of females binned by their attractiveness")
    plt.show()

    decrease_rate = hist_left[0] / hist_original[0]
    plt.bar(range(1, 11), decrease_rate, tick_label=["level" + str(i) for i in range(1, 11)])
    plt.title("rate of males who remain unable to chat")
    plt.show()

    decrease_rate = hist_left_f[0] / hist_original_f[0]
    plt.bar(range(1, 11), decrease_rate, color='red', tick_label=["level" + str(i) for i in range(1, 11)])
    plt.title("rate of females who remain unable to chat")
    plt.show()

    # time series
    num_of_male_without_chat_time_series = [
        len(i) for i in male_without_chat_time_series
    ]

    num_of_female_without_chat_time_series = [
        len(i) for i in female_without_chat_time_series
    ]

    plt.plot(range(1, day_end + 1), num_of_male_without_chat_time_series)
    plt.ylim(500, num_of_male * 1.1)
    plt.show()

    plt.plot(range(1, day_end + 1), num_of_female_without_chat_time_series, color='red')
    plt.ylim(0, num_of_female * 1.1)
    plt.show()


main()