TeXによるデータベース構築
目次
目的
私は鉄緑会という学習塾で中高生に英語を教えていて,その講義資料をTeXで作成しています。高校3年生の講義では主に東京大学の過去問を扱い,その解説をするのですが,その過程でTeXにより問題のデータベースを作成したいという要求が生じました。理由としては,授業で扱う問題というのは毎年固定されたものではなく,差し替えが行われるからです。1つのTeXファイルに各問題の解説を列挙していく形式では,問題の差し替えの度にファイルを修正する必要があります。さらに,仮に1つのファイルだけを編集すればよいならまだましですが,詳しくは次の「データベースの内容」にある通り,1つの問題に関する情報が複数のファイルに存在することは運用上避けられません。具体的には,1つの問題の情報は,①問題冊子②解答冊子③解説冊子④解答用紙⑤講評の5つのファイルに散らばっています。これらのファイルの情報を問題改訂の度に変えるというのはミスの原因にもなりますし,それを防ごうとすると問題を改訂する意欲が失われ,「本当はこの問題は差し替えたいけど,面倒だから今年はそのままにしよう」というようなことにも繋がってしまいます。TeXによるデータベース化により,動的に内容を変化させることができれば,ファイルの書き換えという非本質的な時間を節約し,テキストの内容そのものに時間をかけることができ,教材の質の向上に繋がります。
同様の趣旨の記事として,参考にy_minodaさんの記事を貼っておきます。
データベースの内容
データベースに登録する内容ですが,1つのレコードは,問題,解答,解答用紙,配点,採点基準,目標解答時間,解説,講評の8つのフィールドからなります。簡単にそれぞれについて説明すると,
- 問題・解答:過去問の問題本文とそれに対する模範解答です。
- 解答用紙:東京大学の英語の入試問題は字数指定問題や記号問題など様々な形式があるため,数学の白紙の解答用紙や理科の罫線のみが入った解答用紙とは異なり,問題ごとに個別に作成する必要があります。
- 配点・採点基準:小問ごとの配点や採点基準です。
- 目標解答時間:解説冊子に載せる目標解答時間です。
- 解説:演習後の講義で使う解説資料です。
- 講評:生徒の答案を採点し,翌週に配布する採点所感,多かったミスなどを記載した資料です。
使用する問題情報の一元化
まずは,使用する問題の情報を一元化します。つまり,問題冊子.texの中で\input{東京大学-2000-1A}...し, 解説冊子.texの中でも\input{東京大学-2000-1A}...などとするのではなく,1つのファイル(ここでは使用問題設定.sty)に,それぞれの授業でどの問題を使用するのかという一覧を作成します。カンマ区切りで使用する問題を順に指定していきます。
%\使用問題設定{週}{第1問,第2問,第3問...} \使用問題設定{01}{東京大学-2000-1A,東京大学-2001-1B,東京大学-2002-3,東京大学-2022-4A,東京大学-2023-4B}% \使用問題設定{02}{東京大学-1986-1A,東京大学-1999-1B,東京大学-1973-2A,東京大学-2004-2B,東京大学-2010-3,東京大学-2012-5}% ...
\使用問題設定は以下のように定義しておきます。
また,準備として\@namedefの完全展開版である,\e@namedefを以下のように定義します。
\def\e@namedef#1{\expandafter\edef\csname#1\endcsname} %latex.ltx内の\@namedefの定義内の\defを\edefに置き換えたもの %\def\@namedef#1{\expandafter\def\csname #1\endcsname
\newcounter{回別問題数}% \def\使用問題設定#1#2{%%%\使用問題設定{週}{第1問,第2問,第3問...}% \setcounter{回別問題数}{0}% \expandafter\@for\expandafter\list\expandafter:\expandafter=#2\do{% \stepcounter{回別問題数}% \e@namedef{第#1回使用問題\arabic{回別問題数}}{\list}% \global\e@namedef{第#1回問題数}{\arabic{回別問題数}}% }% }%
問題冊子,解答冊子,解説冊子などを作成するときには,このファイルを参照し,教材を改訂するときには,この1つのファイルのみを変更すれば済むというようにします。
データベースの作成
データベース自体は,以下のようなディレクトリツリー構造をとるものとします。
├東京大学/ ├1970/ ├東京大学-1970-1A.tex ├東京大学-1970-1B.tex ├東京大学-1970-2A.tex ├東京大学-1970-2B.tex ├東京大学-1970-3.tex ├東京大学-1970-4A.tex ├東京大学-1970-4B.tex ├東京大学-1970-5.tex ... ├2023/ ├東京大学-2023-1A.tex ├東京大学-2023-1B.tex ├東京大学-2023-2A.tex ├東京大学-2023-2B.tex ├東京大学-2023-3.tex ├東京大学-2023-4A.tex ├東京大学-2023-4B.tex ├東京大学-2023-5.tex ├慶應義塾大学医学部/ ├2023/ ├慶應義塾大学医学部-2023-1.tex ├慶應義塾大学医学部-2023-2.tex ├慶應義塾大学医学部-2023-3.tex ├慶應義塾大学医学部-2023-4.tex
さらに,この1つ1つのファイルの中に上述した8つのフィールドが存在しています。
たとえば,東京大学-2023-1A.texというファイルは以下のようになっています。
\問題登録{hoge} \解答登録{huga} \配点登録{hige} \採点基準登録{hogehoge} \目標解答時間登録{12分} \解答用紙登録{piyo} \解説登録{puyo} \講評登録{piyora}
それぞれの命令は以下のように定義しておきます。
\long\def\問題登録#1{\long\@namedef{\currfilebase-問題}{#1}} \long\def\解答登録#1{\long\@namedef{\currfilebase-解答}{#1}} \long\def\配点登録#1{\long\@namedef{\currfilebase-配点}{#1}} \long\def\目標解答時間登録#1{\long\@namedef{\currfilebase-目標解答時間}{#1}} \long\def\解答用紙登録#1{\long\@namedef{\currfilebase-解答用紙}{#1}} \long\def\解説登録#1{\long\@namedef{\currfilebase-解説}{#1}} \long\def\講評登録#1{\long\@namedef{\currfilebase-講評}{#1}} \long\def\採点基準登録#1{\long\@namedef{\currfilebase-採点基準}{#1}}
currfileパッケージをロードし,子ファイルのファイル名を\currfilebaseにより取得しています。\jobnameだと元締めファイルのファイル名になってしまいます。
データベースを呼び出す側のファイル
データベースを呼び出すファイル,たとえば問題冊子を生成するファイルは以下のようになっています。本来はデータベースはこのファイルとは別のディレクトリに存在しますが,簡単にするため,データベースは元締めファイルの直下にあるものとします。
\begin{document} \出典読み込み%ここでその週に使う問題のデータベースを読み込む %\使用問題{大問番号}{その回に使う問題の通し番号}{その問題を読み込んだ後に行う処理} \使用問題{1}{1}{\newpage}% \使用問題{1}{2}{}% \使用問題{2}{3}{\newpage}% \使用問題{3}{4}{\newpage}% \使用問題{3}{5}{}% \end{document}
膨大なデータベースをすべて読み込むとコンパイル時間が長くなってしまうので,\出典読み込み という命令でその回に必要なデータベースだけを読み込むようにしておきます。\出典読み込み という命令の定義は以下のようになっています。\newcounter{週}によりカウンタを定義し,元締めファイル側で,そのファイルで呼び出したい週の数値をセットします。
\def\parse出典#1{% \@parse出典#1\@nil } \def\@parse出典#1-#2-#3\@nil{% \input{./#1/#2/#1-#2-#3.tex} } \newcounter{週} \def\出典読み込み{% その週の出典のすべてのTeXソースを読み込み,問題・解答・解説などの情報を@namedef{第01回使用問題1-問題}のように定義していく \fornext<k>[1]{\@nameuse{第\ifnum\c@週<10 0\fi\the\c@週 回問題数}}{% \expandafter\expandafter\expandafter\parse出典\expandafter\expandafter\expandafter{\csname 第\ifnum\c@週<10 0\fi\the\c@週 回使用問題\arabic{k}\endcsname} } }
つぎに,\使用問題の定義ですが,たとえば,第1回であれば使用問題設定.styに登録した, 東京大学-2000-1A, 東京大学-2001-1B, 東京大学-2002-3, 東京大学-2022-4A, 東京大学-2023-4Bの5つの問題を使用しますが,実際の入試問題のように東京大学-2000-1Aと東京大学-2000-1Bは同じ大問に含めたいです。また,第1問と第2問の間には\newpageを含めたいなどの微調整をできるようにします。簡潔にするため,大問は3問からなるものとします。
\def\第1問使用問題{\noindent 第1問\par} \def\第2問使用問題{\noindent 第2問\par} \def\第3問使用問題{\noindent 第3問\par} \long\def\使用問題#1#2#3{% \if#11\relax \appto\第1問使用問題{% \csname\csname 第\ifnum\c@週<10 0\fi\the\c@週 回使用問題#2\endcsname-問題\endcsname% #3% }% \fi \if#12\relax \appto\第2問使用問題{% \csname\csname 第\ifnum\c@週<10 0\fi\the\c@週 回使用問題#2\endcsname-問題\endcsname% #3% }% \fi \if#13\relax \appto\第3問使用問題{% \csname\csname 第\ifnum\c@週<10 0\fi\the\c@週 回使用問題#2\endcsname-問題\endcsname% #3% }% \fi }
\apptoを使うことで,\第○問使用問題という命令のあとに付け足していきます。なお,\apptoを使うにはetoolbox.styを読み込む必要があります。
おわりに
このようにTeXによりデータベースを作成し,内容に応じて必要なファイルをロードしながら,TeXソース(トークン列)をメモリ上で動的に構築していくことで,教材改訂の手間を軽減することができます。このような非本質的な時間を減らすことで,教材の内容そのものにこだわる時間や生徒と接する時間を増やすことができます。
共通テストの選択肢問題のTeXによる自動採点
この記事はTeX & LaTeX Advent Calendar 2023の22日目の記事です。21日目はujimushi at SradJPさんでした。23日目はwtsnjpさんです。
目的
学習塾や学校などの教育機関において,共通テスト等における選択肢問題を採点する場面は多く見られます。記述問題ではなく,選択肢問題であるという特性を考えるとこれを目視で手採点し,合計点を足し合わせるというのは非効率的であり,また正確性を考慮しても自動化することが好ましいです。採点時間も大幅に短縮されるため,生徒側にとってもすぐに返却されるというメリットがあり,まさに,今年のテーマである「(La)TeXで幸せになる方法」であるといえます。マークシートの読み取りやPDFの結合などTeX 以外の操作も含めた全体のワークフローをパッケージ化(SwiftにてmacOSで動くアプリケーションソフトを実装予定)し,配布することで,TeX に明るくない講師を含めてすべての人が恩恵を受けることが出来ます。
(更新:2024/1/31に実装完了)
作りたいもの
実際に生じた例として,1988年度の大学入試センター試験(試行)の英語の試験を実施し,自動採点により以下のような返却答案を配布することを考えます。

ワークフロー
全体のワークフローは以上のようですが,すべてを解説するとあまりにも長くなりすぎるので,本記事ではTeXによる自動採点の部分に主に焦点を当てて解説することとします。
個別化されたマークシート答案の作成
「型」となるマークシートに受験番号と氏名だけ異なる解答用紙を作成するには,doraTeXさんの以下の記事が参考になります。
マークシートの読み取り
マークシートの読み取りには社内版マークシートリーダー(非公開)を用いています。
TeXによる自動採点
共通テストの選択肢の出題方式として,次の3通りがあります。
- 単答問題:最もシンプルな問題形式。1つの問題につき1つの選択肢を選ぶ。
- 完答問題:複数の問題(本試験ではすべて2つ)をすべて正解したときにのみ配点が与えられる。
- 順不同問題:連続する複数の選択肢と解答の選択肢群が集合としてどの程度一致しているかにより配点が与えられる。
例えば,設問番号53~56の正解選択肢が4, 5, 9, 0であるとき,0, 5, 9, 4や9, 0, 5, 4と答えたものには満点を与え,9, 0, 1, 2のように答えた場合には2問分の配点を与える。また,4, 4, 4, 4のように答えた場合は当然1問分の配点しか与えない。
模範解答の登録
まずは模範解答.texに模範解答を登録します。
%\模範解答登録{問題番号}{正解の選択肢}{配点}のように登録 \模範解答登録{1}{2}{6}% \模範解答登録{2}{1}{2}% ... \模範解答登録{55}{9}{5}% \模範解答登録{56}{0}{5}% %上で登録したもののうち,完答問題に該当するものを %\完答問題登録{問題番号1, 問題番号2, 問題番号3, ...}のように完答問題のセットごとに登録します。 \完答問題登録{28,29} ... \完答問題登録{36,37} %同様に,順不同問題に該当するものを %\順不同問題登録{問題番号1, 問題番号2, 問題番号3, ...}のように順不同問題のセットごとに登録します。 \順不同問題登録{53,54,55,56}
また,準備として\@namedefの完全展開版である,\e@namedefを以下のように定義します。
\def\e@namedef#1{\expandafter\edef\csname#1\endcsname} %latex.ltx内の\@namedefの定義内の\defを\edefに置き換えたもの %\def\@namedef#1{\expandafter\def\csname #1\endcsname
%\模範解答登録の定義 \def\模範解答登録#1#2#3{% \e@namedef{問#1解答}{#2}% \e@namedef{問#1配点}{#3}% \e@namedef{問#1順不同解答}{#2}%これは順不同問題にしか使わないが,面倒なのですべての問題において定義しておく }
次に,\完答問題登録の定義を確認します。完答問題の集合(以下,「完答集合」)に含まれるそれぞれの問題から完答集合への逆引きを内部で作ります。
例えば
\完答問題登録{28,29}
を実行すると,その中で
\e@namedef{完答集合28}{28,29}
および
\e@namedef{完答集合29}{28,29}
が実行されます。
%%%完答問題 \newcounter{完答問題に必要な得点数}% \def\完答問題登録#1{% \setcounter{完答問題に必要な得点数}{0}% %配列のそれぞれの要素に対してループ \expandafter\@for\expandafter\@完答問題\expandafter:\expandafter=#1\do{%% \e@namedef{完答集合\@完答問題}{#1}% \stepcounter{完答問題に必要な得点数}% \ifnum\arabic{完答問題に必要な得点数}=\@ne \else \e@namedef{問\@完答問題 配点}{0}%完答問題のセットのうち最初の問題以外の配点を0に上書き \fi }%%loop \e@namedef{#1の完答問題数}{\arabic{完答問題に必要な得点数}}% }
次に,順不同問題の定義に移ります。ここでは,順不同問題の集合に属する問題から順不同問題の正解の選択肢群への写像を作成します。 例えば,
\順不同問題登録{53,54,55,56} →\e@namedef{順不同集合53}{53,54,55,56} \e@namedef{順不同集合54}{53,54,55,56} \e@namedef{順不同集合55}{53,54,55,56} \e@namedef{順不同集合56}{53,54,55,56} \e@namedef{正解集合53,54,55,56}{4,5,9,0} \e@namedef{問56から正解集合への写像}{\正解集合53,54,55,56}
のように展開されます。ただし,\問○○から正解集合への写像はその集合内で最も問題番号が大きい問題のときのみ生成されるようにします。これは後で採点をする際に,最後の問題が来るまで採点を保留するためのものです。
%%%順不同問題 \newcounter{順不同集合内の問題数}% \def\順不同問題登録#1{% \@tempcnta =\z@ \setcounter{順不同集合内の問題数}{0}% %配列のそれぞれの要素に対してループ \@for\@順不同問題:=#1\do{%% \e@namedef{順不同集合\@順不同問題}{#1}% \stepcounter{順不同集合内の問題数}% %%%正解集合との逆引きを作成 \@ifundefined{正解集合#1}{%未定義の場合 \e@namedef{正解集合#1}{\@nameuse{問\@順不同問題 解答}}% }{%定義済の場合 \e@namedef{正解集合#1}{\@nameuse{正解集合#1},\@nameuse{問\@順不同問題 解答}} } \e@namedef{\@順不同問題 の問題数}{\arabic{順不同集合内の問題数}}% }%%ここまでループ \@for\@順不同問題:=#1\do{%% \advance\@tempcnta by\@ne \ifnum\value{順不同集合内の問題数}=\@tempcnta%集合内で最後の問題のときのみ生成 \e@namedef{問\@@順不同問題 から正解集合の写像}{\csname 正解集合#1\endcsname} \fi } }
採点の部分
次に,採点の部分の実装の説明をします。 まず,マークシートを読み取った結果を登録します。
\def\読み取り結果#1#2{% \setcounter{問題番号}{0}% \setcounter{出席番号}{#1}% \stepcounter{生徒数}% %それぞれの生徒に対し,記入解答を登録する \@for\@生徒解答:=#2\do \stepcounter{問題番号}% \e@namedef{\arabic{出席番号}番の人の解答記入数値\arabic{問題番号}}{% \ifx\@生徒解答\@empty -\else\@生徒解答\fi}%無記入の場合には-に置き換える }%%ここまでループ \setcounter{問題数}{\arabic{問題番号}}% }
単答問題
これは単純です。記入した値と解答の数字を確認し,この2つの値が等しい場合にのみ配点を与えます。なお,正答率を計算するため,正誤のフラグも用意しています。
\ifnum\@nameuse{\arabic{出席番号}番の人の解答記入数値\arabic{k}}=\@nameuse{問\arabic{k}解答}\relax%%正解のとき \e@namedef{\arabic{出席番号}番の問\arabic{k}の正誤}{1}%正誤フラグ \e@namedef{\arabic{出席番号}番の問\arabic{k}の配点}{\@nameuse{問\arabic{k}配点}}%配点を与える \advance \@tempcnta by \@nameuse{問\arabic{k}配点}%合計点を計算 \else%不正解 \e@namedef{\arabic{出席番号}番の問\arabic{k}の正誤}{0}%正誤フラグに0を代入 \e@namedef{\arabic{出席番号}番の問\arabic{k}の配点}{0}%配点を与えない \fi
完答問題
先ほどの単答問題よりは複雑ですが,まだ簡単です。kを現在採点する問題の番号を保持するindexとして,\@ifundefined{完答集合\arabic{k}}{完答問題以外の処理}{完答問題の処理}により,これが定義されているときは完答問題の処理を行うようにします。
アイディアとしては,模範解答.texで用意した\完答集合に対してループを回し,この集合の要素の数(つまりその数だけ正解して初めて配点が与えられるような問題数)と正解した問題数が一致したときにのみ配点を与えます。
\@tempcnta = \z@%完答問題の問題数のカウンタ \@tempcntb = \z@%完答問題の正解数のカウンタ \expandafter\@for\expandafter\@完答問題\expandafter:\expandafter=\csname 完答集合\arabic{k}\endcsname\do{%ただしkは現在の問題番号を保持するindex %記入が-のとき \expandafter\expandafter\expandafter\ifx\csname\arabic{出席番号}番の人の解答記入数値\@完答問題\endcsname-\relax \e@namedef{\arabic{出席番号}番の問\arabic{k}の正誤}{0}%正誤フラグに0を代入 \e@namedef{\arabic{出席番号}番の問\arabic{k}の配点}{0}%配点を与えない \else%%%記入が-ではないとき \advance \@tempcnta by \@ne \ifnum\csname\arabic{出席番号}番の人の解答記入数値\@完答問題\endcsname=\@nameuse{問\@完答問題 解答}\relax%%正解のとき \e@namedef{\arabic{出席番号}番の問\@完答問題 の正誤}{1}%正誤フラグ \advance \@tempcntab by \@ne \else%%%不正解のとき \e@namedef{\arabic{出席番号}番の問\@完答問題 の正誤}{0}%正誤フラグ \fi \fi%%-かどうかの場合分け終了 }%%完答問題内のループ終了 \ifnum\@tempcnta=\@tempcntb\relax \e@namedef{\arabic{出席番号}番の問\arabic{k}の配点}{\@nameuse{問\arabic{k}配点}}%すべての問題に正解したときのみ配点を与える \fi
順不同問題
続いて,最も難関な順不同問題の処理を行います。方針として,順不同問題のそれぞれについてループを回し,解答と比較します。よって,計算量のオーダーとしては,n2のオーダーになります。このとき,正解した際に解答を☃に置き換えることがポイントです。こうすることで,同じ解答に対して重複して加点されるのを防ぎます。

\@ifundefined{問\arabic{k}から正解集合の写像}{\relax}{%順不同問題の最後の問題のときのみ処理 \@tempcntb = \z@%%順不同集合内の問題数 \expandafter\@for\expandafter\@順不同\expandafter:\expandafter=\csname 順不同集合\arabic{k}\endcsname\do{%ループ1開始 \expandafter\expandafter\expandafter\ifx\csname\arabic{出席番号}番の人の解答記入数値\@順不同\endcsname-\relax%はじめに記入が-の場合の処理 \e@namedef{\arabic{出席番号}番の問\@順不同 の正誤}{0}%正誤フラグに0を代入 \e@namedef{\arabic{出席番号}番の問\@順不同 の配点}{0}%配点を与えない \else%%%記入が-ではないときの処理 \expandafter\@for\expandafter\@順不同解答\expandafter:\expandafter=\csname 順不同集合\arabic{k}\endcsname\do{%ループ2開始 \expandafter\expandafter\expandafter\ifx\csname\arabic{出席番号}番の問\@順不同 の正誤\endcsname1\relax \else %すでに正解の場合はなにもしない \expandafter\expandafter\expandafter\ifx\expandafter\csname\expandafter\arabic\expandafter{\expandafter 出\expandafter 席\expandafter 番\expandafter 号\expandafter}\expandafter 番\expandafter の\expandafter 人\expandafter の\expandafter 解\expandafter 答\expandafter 記\expandafter 入\expandafter 数\expandafter 値\expandafter\@順不同\expandafter\endcsname\csname 問\@順不同解答 順不同解答\endcsname\relax%%正解のとき \e@namedef{問\@順不同解答 順不同解答}{☃}%正解した場合は解答を☃に置き換え,もうヒットしないようにする(4,5,9,0が正解のとき,4,4,4,4という解答に満点を与えるのを防ぐため) %%可読性向上のため,\expandafter\patchcmd\expandafter{\csname 問\arabic{k}解答\endcsname}{\@正解集合}{☃}{}{}としてもよい(ただし,順不同解答集合が「相異なる1桁の数値のみの羅列」であることが保証されているものとします) \advance \@tempcnta by \@nameuse{問\@順不同 配点}%合計点を計算 \e@namedef{\arabic{出席番号}番の問\@順不同 の正誤}{1}%正誤フラグ \advance \@tempcntb by \@ne \e@namedef{\arabic{出席番号}番の問\@順不同 番の正解数}{\the\@tempcntb} \else%%不正解のとき \e@namedef{\arabic{出席番号}番の問\@順不同 の正誤}{0}%正誤フラグ \fi \fi%%すでに正解の場合はなにもしない }%ループ2終了 \fi%記入が-かどうかの場合分け終了 }%ループ1終了 %%答えをリセット \expandafter\@for\expandafter\@@順不同解答\expandafter:\expandafter=\csname 順不同集合\arabic{k}\endcsname\do{% \e@namedef{問\@@順不同解答 順不同解答}{\@nameuse{問\@@順不同解答 解答}}%一応答えを保存しておくため,解答とは別に順不同用の解答を用意し,こちらを☃に変えている }% }%
採点データの集計
\データ集計にて平均点・標準偏差・順位・偏差値をはじめ,設問別得点率のレーダーチャート・得点分布・成績優秀者などの採点データを集計。具体的な実装はここでは省略しますが,レーダーチャートの作り方として参考にdoraTeXさんの記事を貼っておきます。
本体となるTeXファイル
\begin{document} \def\何位まで掲載するか{6}%成績優秀者を何位まで掲載するか %第1引数は出席番号,第2引数はマークシートリーダの読み取り結果のカンマ区切りの配列 \読み取り結果{1}{2,1,3,4,3,4,3,4,2,1,3,4,2,2,3,3,4,3,1,4,4,4,1,3,2,1,4,5,2,1,3,5,3,1,2,1,5,1,2,4,1,4,4,2,2,2,1,3,4,1,1,3,3,5,7,9} %採点の実行 \正誤判定{1} ... \正誤判定{40} %平均・偏差値などの採点データの集計 \データ集計 %それぞれの生徒の個人成績表を出力 \個人成績表{1} %出席番号1の生徒の個人成績表を出力 ... \個人成績表{40}% \end{document}