1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Wordleの平均的な文字の埋まり方を可視化したページを作った

Last updated at Posted at 2022-02-12

はじめに

Wordleは、5文字の英単語を推測するゲームです

6回の推測ターンのうち何回でクリアできたか、また、どの箇所の文字から推測が当たっていったのかをシェアする機能があり、Twitterで多くのシェアを確認することができます
https://twitter.com/search?q=%23Wordle&src=typed_query&f=live

はじめはこれを見ながら、正解するまでのターン数をヒストグラムにしたらどうなるのだろうかと考えたのですが、それはすでに以下のTwitterアカウントできちんと作られています

そこで、新たにアイデアとして、各文字が平均的にどのように推測されているのかというのを可視化してみようと考え、実際のページを作ってみました

できたもの

スコアを選ぶと、そのスコアで正解した人の平均的な文字の埋まり方を可視化します
スクリーンショット 2022-02-12 20.35.51.png

また、各マスで選ばれた色の確率を次のように見ることもできるようになっています
スクリーンショット 2022-02-12 23.14.12.png

Pythonのstreamlitで作ったものをHerokuにデプロイしており、Herokuのscheduler(crontabのようなもの)で1時間に1回、最大1万件のツイートを取得しており、(正常に動いていれば)データが更新されます

システム構成

成績データの取得と保存を行う処理と、成績データから平均や確率の計算と可視化を行う処理の2つに分かれています

成績データ取得と保存

スクリプト

Wordleの推測の結果のデータをTwitter APIで取得します。
Wordleは1日1個ずつIDが振られており、そのIDとともにツイートされるので、'"#Wordle 238"'という文字列で検索を行い、その結果を取得します

class Twitter(): # 検索用のクラス
    def __init__(self):
        self.OAuth = OAuth1(API_KEY, API_KEY_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
        
    def search_query_by_request(self, query, max_results=10000, next_token=None):
        url = 'https://api.twitter.com/2/tweets/search/recent'
        #query += " exclude:nativeretweets filter:media"
        params = {'query': query, 'max_results': max_results, 'next_token': next_token}
        request = requests.get(url, params=params, auth=self.OAuth)
        requestJson = json.loads(request.text)
        return requestJson
    
    def search_query(self, query, max_results=10000):
        data = []
        next_token = None
        while len(data) < max_results:
            res = self.search_query_by_request(query, next_token=next_token)
            data += res['data']
            if 'next_token' not in res['meta']: break
            next_token = res['meta']['next_token']
        return {'data': data, 'meta': res['meta']}


t = Twitter()
res = t.search_query(f'"#Wordle {wordle_id}"') # wordle_idは例えば238などの数値

得られたツイート本文のうち、必要なのは四角の絵文字で表される部分だけなので、正規表現等で、⬜🟨🟩 を WYGの文字列に置き換えます(W: White, Y: Yellow, G: Green)

def parse_colors(text):
    p, s = "^^**", "**$$"
    text = text\
    .replace("⬛️", f"{p}W{s}")\
    .replace("", f"{p}W{s}")\
    .replace("", f"{p}W{s}")\
    .replace("\U0001f7e8",  f"{p}Y{s}")\
    .replace("\U0001f7e9", f"{p}G{s}")\
    .replace("\n", "")
    match = re.search("\^\^\*\*.*\*\*\$\$", text)
    if match is None: return None
    sub_text = re.sub("[\^\*\$]", "", match.group())
    if len([None for s in sub_text.strip() if re.match("[^WYG]", s)]) !=0: return None
    return sub_text

これを先程取得したツイート全体に適用して配列に格納していきます。

colors = []
for d in res['data']:
    color = parse_colors(d['text'])
    if color is not None and len(color) % 5 == 0: 
        colors.append(list(color))

そのようにすると、pd.DataFrame(colors)としたときに、以下のようなデータが出来上がります。

スクリーンショット 2022-02-12 20.00.57.png

Herokuでデータを保持しておくために、この状態でcolorsという名前のテーブルでDBに保存しておきます。

from sqlalchemy import create_engine
import sqlalchemy.orm
import sqlalchemy.ext.declarative

engine = create_engine(db_url)
engine.execute('DELETE FROM colors') # 一旦空っぽにする
pd.DataFrame(colors).to_sql("colors", con=engine, if_exists="replace", index=False)

crontabで定期実行するのはここまでです。

平均データと確率データの可視化

スクリプト

ページの表示の際に、平均的な埋まり方と各色の確率の可視化を行う部分を説明します

スコア(「4/6」とか)ごとに結果を分けてみたいため、DBに保存してある成績データのうち、埋まっている文字の数で区切って取り出すメソッドを定義しました。

def set_dataframe_per_score():
    engine = create_engine(db_url)
    res = engine.execute('SELECT * FROM colors').fetchall()
    df = pd.DataFrame(res)
    df_by_score = {"All": df}
    for score in scores:
        df_by_score[f"{score}/6"] = filter_dataframe_by_score(df, score)
    return df_by_score
    
def filter_dataframe_by_score(df, score):
    if score == 6: 
        return df[df["29"]=="G"]
    else:
        char_len = score * 5
        return df[df[f"{char_len-1}"] == "G"][df[f"{char_len}"].isin([None]) & df["29"].isin([None])] 

これにより、以下のようにして、スコアごとの結果の一覧が手に入ります

df_by_score = set_dataframe_per_score()
df = df_by_score[score] # scoreは"4/6"など

各ターンの各文字で最も頻度が高い色を調べるためには、以下のようにdf.mode()を利用することができます。

chars = [v for v in list(df.mode().values[0]) if isinstance(v, str)]
#=> 'WYWWWYYYWWWWYYGGGGGG'

arry = np.array([c for c in chars]).reshape(-1, 5)

あとは、この結果をpandasのstyleを指定しつつ表示しています

import streamlit as st

def highlight_color(v):
    if v is None: return None
    codes = {"G": "78B15A", "Y": "FBCB59", "W": "E6E7E8"}
    return f'color: #{codes[v]}; background-color: #{codes[v]};'

df_colored = pd.DataFrame(arry).style.applymap(highlight_color)
st.dataframe(df_colored)

次に、一番頻度の高い色を表示するだけではもったいないので、各マスにおける各色の確率を可視化してみることにしました

例えば1ターン目の1文字目の各色の頻度を調べるためには、以下のように、pandasのvalue_counts()メソッドを使うとちょうど良い感じに計算ができそうです

スクリーンショット 2022-02-12 20.17.53.png

この値をデータ数で割ることで、そのマスの特定の色の確率を調べることができます。
色を指定したときに、各マスごとにその色が選ばれる確率を表したDataFrameを返す関数を定義します

def prob_dataframe_by_color(df, color):
    arry = np.array([df[i].value_counts().get(color, 0)/ df.shape[0] for i in df.columns]).reshape(-1, 5)
    return pd.DataFrame(arry) 

そして、rgbaのアルファチャンネルで確率をヒートマップとして可視化したDataFrameを作成しました

def highlight_white(v): return f'background-color: rgba(230,231,232,{v});'
def highlight_yellow(v): return f'background-color: rgba(251,203,89,{v});'
def highlight_green(v): return f'background-color: rgba(120,177,90,{v});'

for color in ["W", "Y", "G"]:
    df_prob = prob_dataframe_by_color(df, color)[0:score_i]
    if color == "W":
        df_opacity = df_prob.style.applymap(highlight_white)
    elif color == "Y":
        df_opacity = df_prob.style.applymap(highlight_yellow)
    elif color == "G":
        df_opacity = df_prob.style.applymap(highlight_green)
    st.dataframe(df_opacity)

以上になります

おわりに

自分のスコアと照らし合わせて、どの部分から空いていったのかを見てみると面白いかもしれません

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?