クイズ統計学入門(1) 〜出題傾向を知るための問題文の数理的解析〜

  • 2
    いいね
  • 0
    コメント

※ このエントリは、クイズやる人 Advent Calendar 2016に寄稿したものです。

はじめに

みなさん、クイズやってますか? やってますよね??
そして、クイズといえば、やっぱり華は早押しクイズ! 皆さま、オンラインにオフラインに、日々いろんな形で早押しクイズに勤しんでいらっしゃることと思います。

※ とりあえずここまで読んで「何言ってんだこいつ」と思われた方は、極めて正常かつ一般な方でありますため、以降の内容は読まない、もしくは超流し読みにされるのが宜しいかと思われます。一方、「そらそうよ」と思われた方は、このままお進みください。

さて、早押しクイズをやっていると、こんな気になること、ありますよね?

  • パラレル(いわゆる「ですが問題」)とか、語源問題(「◯◯語で□□という意味の〜」という形の問題)って、だいたい何問に1問ぐらいの割合で飛んでくるんだろう……?
  • 「この単語が聞こえてきたらもう答えはこれで確定!」っていうキーワード、知りたいなあ……。

こういう疑問、クイズプレイヤーなら一度は抱きますよね? てか、抱けよ☆ これらに対して、技術的・数理的な手法で真っ向から答えてしまおう、というのが、この記事の趣旨であり、目的であります。

さて、以降この記事には、その性質上、ソースコードや数式がごちゃごちゃ出てきます。もちろん、これらをすべて省いた上でいきなり結論を書くこともできますが、少しくらいは「それっぽさ」を出したいというのがヒミツキチラボセンター王もしくは卒論Night登壇者である自分のこだわりでもありますため(クソ面倒)、ややこしい記述もあえて一通り書きます。もちろん、クイズそのものに関わる本質的なところはなるべく平易な説明に努めますが、専門的な箇所(特に技術的側面)については詳しい説明は割愛させていただきます、ご了承くださいませ。とにかく、わけわからないところはバンバン飛ばして読んで下さい

あと、念のためではありますが、ここに載せるプログラムコードは、ちゃんと動作確認していませんので、思わぬ不具合を起こす可能性があります。そうした不具合等に対し、筆者は一切の責任を負いかねますので、ご利用は自己責任でお願い致します。また、結果や考察につきましても、あくまで筆者の個人的な所感に過ぎませんので、これらを真に受けすぎないよう、お願い致します。

では、皆さまを、アカデミックなクイズ統計学の入口へとお連れしましょう。

【注】ここでは、以降の手順によって、HTML → SQLデータベース → CSVファイル、というフローでクイズ問題データの整理を進めますが、もちろん、SQLデータベースを省略して、HTML → CSVファイル、でも全く問題ありません。もっと言えば、別にプログラムを書く必要すらなく、HTMLをExcelなどにうまいことコピペする方法でもできると思いますし、作業効率を考えればそっちの方が良いでしょう。以降の記述は、

  • なんとなく、一度データベースに入れておいた方が、後々を考えるとより色々と遊びやすそうだし整理しやすそう
  • 個人的にExcelを使うのが非常に苦手なので、なるべくExcelを使わない方法でやりたい

という個人的なノリや事情によるものですので、データベース不要と言われればその通りですし、Excelで十分だろうと言われればそれもその通りです。以降は、あくまで、クイズ活動歴が浅い私が試してみたことの記録ですので、本質的には冗長な箇所や回り道をしている箇所もあることをご承知おきください。

分析するクイズ問題データの準備

問題文と答えの抽出とデータベースへの格納

さて、ここからクイズ問題文を分析していくわけですが、当然、分析対象であるクイズ問題が無いと始まりません。今回は、クイズの杜様にて公開されているクイズ問題のうち、最もスタンダードなクイズ問題文と思われる「abcシリーズ」の問題のみを使用しました。

クイズ問題はHTML形式で公開されているわけですが、このままだとちょっと扱いづらいので、まずはこのHTMLファイルから、問題文とその答えをきれいに抽出するところから始めます。ついでに、あとで使いやすくするために、データベースにも突っ込んじゃいましょう。

abcの問題HTMLファイルをabcquizディレクトリにすべて保存したら、まずHTMLファイルの文字コードをUTF-8に統一しておきましょう。abcquizディレクトリにすべてのHTMLファイルを保存してある場合、以下のコマンドで文字コードを一括変換できます。

nkf -w --overwrite ./abcquiz/*.htm

文字コードの変換が完了したら、Pythonスクリプト書いて、走らせます。ここで、下記のPythonスクリプトで使っているMySQLdbおよびbs4というパッケージはデフォルトでは入っていませんので、これも別途インストールしてあげる必要があります。例えば、pipコマンドを使うと、こんな感じでインストールできます。

sudo pip install MySQL-python
sudo pip install beautifulsoup4

そしたら、次のPythonプログラムを実行します。なお、以降のPythonスクリプトは、Python 2.x系での動作を前提としています。

extract_abcquiz_into_mysqldb.py
# coding: utf-8

import os
import re
import codecs
import MySQLdb
from bs4 import BeautifulSoup

# MySQL操作用オブジェクト
connector = MySQLdb.connect(host="localhost", db="quiz", user="(ユーザ名)", passwd="(パスワード)", charset="utf8")
cursor = connector.cursor()

# 指定フォルダ内のHTMLファイル名をすべて探索
file_names = os.listdir("./abcquiz/")
# for文で自身の要素removeが生じる場合は全件スライスのコピーで回す
for f in file_names[:]:
    if ".htm" not in f:
        file_names.remove(f)

# 問題文を抽出し読み込む(すでにunicode型になっている)
for f in file_names:
    open_file = codecs.open("./abcquiz/" + f, "r", "utf8")
    whole_soup = BeautifulSoup(open_file)
    # まず各問題ごとにsoupを分割する
    each_quiz_soups = whole_soup.find_all("tr")
    for qs in each_quiz_soups:
        current_quiz = {"question":u"", "answer":u""}
        # さらにtdタグを抽出する
        tds = qs.find_all("td")
        is_next_of_question = False
        for td in tds:
            # 最後が「?」で終わるtdセルは問題文なので抽出する
            if re.match(ur".*\?$", td.text):
                current_quiz["question"] = td.text
                is_next_of_question = True
                continue
            # 問題文の次にくるtdセルは答え
            if is_next_of_question:
                current_quiz["answer"] = td.text
                break
        sql_query = u"insert into abcquiz (question, answer) values ('" + current_quiz["question"] + u"', '" + current_quiz["answer"] + u"')"
        try:
            cursor.execute(sql_query)
        except:
            continue

connector.commit()
cursor.close()
connector.close()

そうすると、HTMLファイルからすべての問題文と答えのペアが抽出され、quizデータベースのabcquizテーブルに、問題文はquestion、答えはanswerという列で保存されます。なお、MySQLのインストールや、MySQLのユーザーアカウントの準備、quizデータベースおよびabcquizテーブルの作成などは事前に行う必要があります。

これで、11184問のクイズ問題データベースがとりあえず完成しました。

CSVファイル形式にエクスポート

データベースに入れたまま分析にかけるのはちょっとやりづらいので、一度格納したクイズたちを、分析しやすい形に整形して取り出しておきましょう。自分は、こういうときはCSVファイル形式をよく使うので、今回はCSVファイル形式にエクスポートしてやります。もちろん、CSVじゃなくても、JSONでもなんでもいいと思います。

まず、MySQLにログインします。

mysql -u (ユーザ名) -p

パスワードを入力してログインできたら、quizデータベースへの移動と、abcquizテーブルの内容のCSVファイルへのエクスポートを行います。

use quiz;
select * from abcquiz into outfile 'abcquiz.csv' fields terminated by ',';

なお、CSVファイルを生成しようとすると、secure-file-privとかなんとかいうエラーが出てしまって、うまくいかないことがあります。ここらの解決法はケースバイケースでして、書き出すと長くなるのでここでは割愛しますが、secure-file-privの値がNULLになってしまっているケースが多いみたいなので、「読み込まれるmy.cnfの中でsecure_file_privの値を決め打ちすればよい」という方向性で対処すれば、だいたいうまくいくと思います。

(参考) Mysql making --secure-file-priv option to NULL - Stack Overflow
http://stackoverflow.com/questions/37543177/mysql-making-secure-file-priv-option-to-null

さて、ここまでの処理で、次のようなCSVファイル、つまり、問題文と答えが半角カンマで仕切られた形で書かれた11184行のテキストファイルが生成されます。

元素記号Bの元素はホウ素ですが、元素記号Cの元素は何でしょう?,炭素
少ない賃金や分け前のことを、ある鳥にたとえて「何の涙」というでしょう?,雀(すずめ)の涙
鎌倉幕府の初代将軍は源頼朝ですが、室町幕府の初代将軍は誰でしょう?,足利尊氏(あしかが・たかうじ)
「やせっぽち」という意味のあだ名で呼ばれている、現在のサッカー日本代表の監督は誰でしょう?,ジーコ
アメリカの国旗に使われていて、日本の国旗に使われていない色は何でしょう?,青
 ・
 ・
 ・

これを素材として、次のチャプターでは実際にいろいろ分析をしていきます。

パターンマッチングによる問題分析

では、ちょっとした分析をしてみましょう。ここでは、記事の冒頭に挙げたクイズプレイヤーあるある疑問のひとつ、

  • パラレルとか、語源問題って、だいたい何問に1問ぐらいの割合で飛んでくるんだろう……?

に対する答えを出してみようと思います。ここでは、パラレル問題および語源問題の定義を以下のようにしてしまいます。

  • パラレル問題とは、問題文中の任意の場所に 「ですが」 という文字列が出現する問題文を持つ問題のことである。
  • 語源問題とは、問題文中の任意の場所に 「語で(任意の文字列)という意味」 という形の文字列パターン表現が出現する問題文を持つ問題のことである。

では、さきほど生成したCSVファイルを読み込み、これらのパターンにマッチする問題数をカウントし、全体に対する出現比率を算出するプログラムを作ってみます。

count_parallel_gogen_question.py
# coding: utf-8

import csv
import re

# 事前に作成したCSVファイルから問題文を読み込む(CSVの1列目)
questions = []
with open("./abcquiz.csv", "r") as f:
    reader = csv.reader(f)
    for row in reader:
        questions.append(row[0].decode("utf8"))

# 探索する文字列パターンを正規表現で指定
parallel_pattern = re.compile(ur"ですが")
gogen_pattern = re.compile(ur"語で.*という意味")

# 指定したパターンにマッチする問題の数をカウントする
parallel_num = 0
gogen_num = 0
for q in questions:
    if re.search(parallel_pattern, q):
        parallel_num += 1
    if re.search(gogen_pattern, q):
        gogen_num += 1

# カウント結果に基づき、出現比率を算出
parallel_probability = float(parallel_num) / float(len(questions))
gogen_probability = float(gogen_num) / float(len(questions))

# 結果の出力
print u"総問題数 = ", unicode(len(questions))
print u"パラレル問題数 = ", unicode(parallel_num), u"(出題比率 = ", u"{:.2f}".format(parallel_probability * 100.0), u"%,", u"{:.2f}".format(1.0 / parallel_probability), u"問に1問の割合)"
print u"語源問題数 = ", unicode(gogen_num), u"(出題比率 = ", u"{:.2f}".format(gogen_probability * 100.0), u"%,", u"{:.2f}".format(1.0 / gogen_probability), u"問に1問の割合)"

こいつを走らせると、こんな結果が画面に出力されてきます。

総問題数 =  11184
パラレル問題数 =  1083 (出題比率 =  9.68 %, 10.33 問に1問の割合)
語源問題数 =  244 (出題比率 =  2.18 %, 45.84 問に1問の割合)

はい、さっきの疑問に対する答えが出ましたね!

  • パラレル問題は、10.33問に1問の割合で出題される
  • 語源問題は、45.84問に1問の割合で出題される

念のため、ちゃんとパラレル問題や語源問題をちゃんと拾えているのかを確認してみます。さっきのコードにある、パラレル問題をカウントする箇所に、問題文を画面に出力するprint文を1行を追記します。

    if re.search(parallel_pattern, q):
        parallel_num += 1
        print q

すると、パラレル問題と判定された問題文が、画面にずらずらと表示されてきますので、見てみます。

元素記号Bの元素はホウ素ですが、元素記号Cの元素は何でしょう?
鎌倉幕府の初代将軍は源頼朝ですが、室町幕府の初代将軍は誰でしょう?
秋田新幹線の愛称は「こまち」ですが、山形新幹線の愛称は何でしょう?
スポーツの別名で、「氷上の格闘技」と称されるのはアイスホッケーですが、「氷上のチェス」と称されるのは何でしょう?
世界最大の魚類はジンベイザメですが、世界最大の両生類は何でしょう?
 ・
 ・
 ・

思惑通り、ちゃんとパラレル問題が検出されてカウントされていますね。なお、このリストをずっと眺めていくと、途中に、こんな問題文も出てきます。

料理の薬味に使う植物で、ネギはユリ科、ショウガはショウガ科ですが、ワサビは何科でしょう?

「ですが」の前に2つの要素が出現し、3つ目に出てくる要素を問う、いわゆる「3パラ問題」ですね。今回のパラレル問題の定義では、こうした3パラ問題もカウント対象になっています。定義を工夫すれば、3パラ問題のみを抽出することもできると思います。

同様に、語源問題についても確認してみましょう。さっき追記したprint文を、語源問題をカウントしている箇所に移動させてやります。

    if re.search(gogen_pattern, q):
        gogen_num += 1
        print q

そして実行。

ドイツ語で「木のケーキ」という意味がある、文字通り木の年輪を模したケーキは何でしょう?
スペイン語で「再征服」という意味がある、キリスト教徒がイスラム教徒に占領されたイベリア半島を奪回した運動を何というでしょう?
フランス語で「食器戸棚」という意味がある、パーティーなどで行う立食形式のことを何でしょう?
ロシア語で「情報公開」という意味がある、開かれた政治や経済を展開しようとした旧ソ連の政策は何でしょう?
フランス語で「完全な」という意味があり、フルーツ、チョコレートなどの種類がある、冷たく甘いデザートは何でしょう?
 ・
 ・
 ・

うん、こちらもいけてますね! 大丈夫そうです。なお、こちらもリストを眺めていくと、ちょっと毛色の違う形の問題文が見つかったりします。

一度に刈り取りと脱穀を行えることから、英語で「兼ね備えた」という意味の名を持つ農機具は何でしょう?
細長いシュークリームにチョコレートをかけた洋菓子で、フランス語で「稲妻」という意味の名前を持つものは何でしょう?

「◯◯語で□□という意味」という表現が途中に出てくるパターンですね。こちらは、語源を知っていることがそこまでアドバンテージにならないこともあるので(たとえば、上に挙げた問題は、英語やフランス語の意味を知っていなくても、その前で押すことがわりと可能な問題かと思います)、いわゆる「語源問題」と呼ぶにはちょっと弱いものもあるかもしれません。

あと、ちょっと予想していなかった形の問題文がひとつ検知されました。

英語では「Confidencial(コンフィデンシャル)」と訳される、手紙に添える、宛名の人だけに読んで欲しいという意味の言葉は何でしょう?

この問題、今回定義した語源問題の定義である 「語で(任意の文字列)という意味」 というパターンが、確かに含まれています。しかし、実質的な形式としては、他の言語への翻訳が前フリに登場する形となっており、語源問題とは言えない問題かもしれませんね。まあ、今回はざっくりと出題比率を調べたかっただけなので、この1問が結果の大勢に影響を与えるわけではありませんが、より詳細な分析をしたい場合は、考慮していく必要があるでしょう。

おわりに

というわけで、本稿では、abcシリーズの過去問を題材として、その技術的な取り扱いや、実際の分析結果とその考察について取り上げました。結果として、

  • パラレル問題は、10.33問に1問の割合で出題される
  • 語源問題は、45.84問に1問の割合で出題される

というものが得られましたが、今回は、あくまで10年前後前のabcシリーズの問題のみを扱っておりますので、近年のクイズ出題傾向では、この比率はだいぶ変わってきている可能性もあると考えられます。

さて、冒頭にクイズプレイヤーあるある疑問を挙げましたが、いま解決してきた疑問の他に、もうひとつ、ありましたよね?

  • 「この単語が聞こえてきたらもう答えはこれで確定!」っていうキーワード、知りたいなあ……。

本当は、こっちの疑問に対する解決の話題が本丸だったんですが、ちょっと記事も長くなってしまいそうですし、今日はここで一区切りとさせていただき、別の日に「クイズ統計学(2)」としてあらためて公開したいと思っています。今回の記事で行った分析は、問題文データさえあればExcelのCOUNTIF関数なんかでも十分に可能な内容だと思いますが、次回は、専門的なプログラミング手法を使わないと解けないような内容を、がっつり扱っていく予定です。よろしければ、またお付き合いくださいませ。

ここまで読んでくださった皆さまに、心から感謝を申し上げまして、「クイズ統計学(1)」を締めたいと思います。それでは、また「クイズ統計学(2)」にてお会いしましょう。