読者です 読者をやめる 読者になる 読者になる

唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

Weblio語彙力診断のスコア分布を調べてみた

最近Twitterで流行っていた日本語の語彙力診断の結果のツイートの分布が変という話がありました
qiita.com

たまにTwitterでみかけるWeblioの英単語の語彙力診断だとどんな分布なのか気になったので調べてみました
uwl.weblio.jp

以下のような形式のツイートを検索して、スコアと回答時間を収集しました


ちなみに同じユーザーが複数回ツイートしている場合があるので平均を取りました

ツイートのスコアのユーザーごとの平均をヒストグラムにしたのが以下です
スコアが増えるとユーザー数は減っているように見えます(個人的には英語の勉強が好きな人のほうが診断を受けたがるので、上の人のほうが多いかと思っていました)

f:id:sucrose:20160821205408p:plain

次に回答時間(秒)の分布を調べてみました
こちらは正規分布に近い形になっているように見えます
Weblio語彙力診断の説明に「約2分半で完了します」と書いてありますがグラフを見ると半分強ぐらいがこの時間で終わっていそうです
f:id:sucrose:20160821204550p:plain

スコアと回答時間に関係があるのかどうか、ということで散布図も出してみました
スコアが高いほど回答時間が短そう、という関係が見て取れます
f:id:sucrose:20160821204710p:plain

ソースコード

検索APIの一回に取ってくる件数のオプションがTweepyのドキュメント(古い?)だとrppになっていますが公式APIの方だとcountになっているので注意
API Reference — tweepy 3.5.0 documentation
dev.twitter.com

# -*- coding: utf-8 -*-

import tweepy
import time
import collections
import datetime
import pandas as pd
import seaborn as sns
import matplotlib.font_manager
import re

#APIキーなどの設定
consumer_key = ''
consumer_secret = ''
access_token = ''
access_token_secret = ''

auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
api = tweepy.API(auth)

#検索結果を取ってくる
statuses = tweepy.Cursor(api.search, u'語彙力診断結果 スコア 推定語彙数', count=100).items(200)

data = collections.defaultdict(list)

#データを整形
for status in statuses:
    match = re.search(ur'スコア「(.+?)」', status.text)
    if match:
        data['screen_name'].append(status.user.screen_name)
        data['score'].append(float(match.group(1)))
    
    match = re.search(ur'回答時間「(?:(.+?)分)?(.+?)秒」', status.text)
    if match:
        seconds = 0
        
        groups = match.groups()
        if len(groups) == 2:
            seconds += int(groups[0]) * 60
        seconds += int(groups[1])
        
        data['seconds'].append(seconds)

df = pd.DataFrame(data)

from pylab import *
import numpy as np

#フォント指定をしているので注意
prop = matplotlib.font_manager.FontProperties(fname=r'C:\Windows\Fonts\meiryo.ttc', size=15)


title(u'Weblio語彙力診断のスコア別ツイート数', fontproperties=prop)
xlabel(u'score')
ylabel(u'count')
hist(df.groupby('screen_name').mean().score, bins=np.arange(0, 325, 25), histtype='stepfilled')
show()

title(u'Weblio語彙力診断の回答時間別ツイート数', fontproperties=prop)
xlabel(u'seconds')
ylabel(u'count')
hist(df.groupby('screen_name').mean().seconds, histtype='stepfilled')
show()

title(u'Weblio語彙力診断のスコアと回答時間', fontproperties=prop)
xlabel(u'score')
ylabel(u'seconds')
scatter(df.groupby('screen_name').mean().score, df.groupby('screen_name').mean().seconds)
show()

BigQueryで有り金全部溶かさないように、テーブルの日付ごとのパーティション(partition)機能について調べてみた

BigQueryではクエリのたびに対象のテーブルをフルスキャンします
スキャンしたテーブルのサイズによって料金が請求されるので、コストの削減のために日付などの単位でテーブルを分割するのがベストプラクティスとして知られています

qiita.com

テーブルを日付ごとに分割する利点

  • クエリのときのテーブルのサイズが小さくなるので、費用が下がる
  • テーブルを最後に更新してから90日以上たつとストレージ代が安くなる
  • テーブルが分かれているので、特定の日のデータだけ入れ直すのが簡単
  • expireを設定すれば一定期間で自動的に古い方から消えていくようにできる

以前このブログでも紹介しましたが、最近テーブルを日付ごとにパーティションする機能がサポートされました
sucrose.hatenablog.com

パーティション機能の挙動を知りたかったのでいろいろ調べてまとめてみました
以下に書いてあるmkやloadなどのbqコマンドの、テーブルのスキーマ指定は省略してあるので必要だったら足してください

テーブルの作成

パーティション機能を使ったテーブル

通常のテーブルを作成するコマンドに--time_partitioning_type=DAYを加えればよいです
ちなみに現在は日付単位(DAY)でしかパーティションをサポートしてないらしい

bq mk --time_partitioning_type=DAY mydataset.table1

パーティションの保存期間を指定する

パーティションが3日(259200秒)で消えるようなテーブルを作成するコマンド

bq mk --time_partitioning_type=DAY  --time_partitioning_expiration=259200 mydataset.table1

データのload

パーティション無指定でパーティション機能が有効なテーブルにloadする場合の挙動

パーティションを指定せずにloadするとその日のパーティションに入れられる
Webコンソールからデータを保存する場合も同様

bq load mydataset.table1 data.csv

特定のパーティションにloadする

テーブル名の後ろに$日付を指定

bq load 'mydataset.table1$20160713' data.csv

置換する場合の挙動

--replaceをつけて、パーティションを指定せずにloadしたときはすべてのパーティションが消えて新しくloadした当日のパーティションだけになってしまいます(危ない)

bq load --replace mydataset.table1 data.csv

パーティションを指定するとそのパーティションだけが置き換えられます

bq load --replace 'mydataset.table1$20160713' data.csv

クエリ

パーティションの確認

以下のクエリでパーティションの情報の一覧が表示されます
パーティションの作成日時などがわかります

SELECT * FROM [mydataset.table1$__PARTITIONS_SUMMARY__] 

期間の指定

1日分だけクエリに使う場合にはFROM [テーブル名$日付]でよい
特定の期間の場合には_PARTITIONTIMEカラムに擬似的にパーティションのタイムスタンプが入っているので、これを参照することでクエリに使われるテーブルのデータ量が削減されます

SELECT
    *
FROM
    [mydataset.table1]
WHERE
    _PARTITIONTIME BETWEEN TIMESTAMP('2016-07-01') AND TIMESTAMP('2016-07-30');

_PARTITIONTIMEと比較する時は_PARTITIONTIMEに関数を適用せずに比較対象に関数を適用したほうがパフォーマンスがよいとのこと

ちなみに日付ごとに分けてテーブルを作っている場合にはTABLE_DATE_RANGE()関数を使ってテーブルを指定します

FROM TABLE_DATE_RANGE([mydataset.table1], TIMESTAMP('2016-07-01'), TIMESTAMP('2016-07-30'))

期間を無指定の時の挙動

すべてのパーティションからデータを取得してきます
誤って全期間のテーブルをフルスキャンしてしまってクエリ代がひどいことになる事故が怖いですね

Long-term storage pricing

90日以上更新されていないテーブル(パーティション機能を使っていれば古いパーティションも)はストレージ代が安くなります

日付ごとに分かれたテーブルからパーティションを使ったテーブルへの移行

以下のようなコマンドでパーティションが使われたテーブルに移行できます

bq partition mydataset.sharded_ mydataset.partitioned

コマンドのヘルプにはコピーすると書いてあり、パーティションの作成日時も新たな日時になっているので、おそらくまた90日以上たたないとLong-term storage pricingの割引は効かなそう
長期間のデータを移行すると急にストレージ代が増えてしまうかも

まとめ

パーティション機能を公式でサポートして便利になりました

今のところ日付別にテーブルを分ける方法よりも劇的に便利になるわけではないので、日付別にテーブルを作成する方法で問題なく動いているのであればすぐに移行するモチベーションはなさそう(コマンド一発でテーブルは移行できる)

日付でテーブルを分けたりパーティション機能を使ったりすることの良い点悪い点

  • 日付で分ける
    • 明示的に書かない限り1日分のテーブルにしかクエリを発行しない
    • テーブルが増えて管理しづらくなる
    • 途中の日付からカラムを追加できる
  • パーティション機能
    • 明示的に書かないと全パーティションにクエリが発行されてしまう
    • テーブルの数が一つで管理しやすい。データをloadするのも単純にテーブル名だけ指定してloadしつづければよい