arithmetic-timeattack

アーキテクチャ

何を解いた設計か

九九版と長さ版を独立に作った結果、約8割が同一コードになった。 認証・ランキング・キャッシュ・排他制御・キー配置・未送信データの退避—— どれも単元とは無関係な仕組みなのに、単元ごとに複製されていた。 3つ目を作れば、バグ修正のたびに3か所を直すことになる。

そこで 共通エンジン(Core)と単元定義(Unit)に分離した。 新しい単元を作るときに書くのは、原則 Unit.gs 1枚だけになる。

共通エンジン ── 認証 / 出題の配信と再採点 / 記録 / ランキング / 分析の器 / 画面
      ↑
   単元定義 ── モード一覧 / 1問の作り方 / 分析の軸

依存の向き

Core は Unit を知らない。Unit が Core の期待する形に合わせる。 逆向きの依存を作らないことで、単元を足しても Core が肥大しない。

Core.gs  ──uses──▶  UNIT (グローバル定数)
index.html  ──uses──▶  boot() が返す設定(単元名・モード一覧)

出題の統一形式

すべての単元が、この1つの形で問題を表す。これが分離の要。

{
  t:   'A',                       // 問題型。分析の軸になり、画面側の表示分岐にも使う
  q:   ['1200', 'm'],             // 出題の表示トークン
  f:   ['km', 'm'],               // 答えの欄と、その単位
  ans: { km: 1, m: 200 },         // 正解
  tag: '1200m',                   // 重複回避と分析用の識別子
  ruler: { from:0, span:85, at:64 },  // 任意。あれば目盛りを描く
  rows: [['73','÷','8','='], ['八','_','_'], ['73','-','72','=','_']],  // 任意。並べ方を単元が決める
  veil: 2                             // 任意。n番目以降の欄がある段を、そこに着くまで覆う
}

式の並べ方を単元が持つ

既定の描画は 「式 = 欄」に固定されている。q が左辺に並び、答えの欄は必ず右端に集まる。 この形では作れないものが2つある。

共通画面に単元ごとの分岐を足して解くと、依存が逆流する。 以前 index.html が 「モード5は2桁」という長さ単元固有の分岐を持っていたのと同じ失敗になる。 そこで digitCap と同じく単元が宣言する形にした。問題が rows を持てば、 共通画面はその並びのとおりに描くだけで、意味は知らない。

veil: n は「n番目以降の欄がある段を、そこに着くまで覆う」。 覆いの向こうに前の段の答えを使って書いた式を置けるようにするためのもので、 先に読めると手続きを飛ばして答えが出てしまう段を隠す。

このため、覆いより手前の欄は正解でないと越えられない。既定の欄移動は 「上限桁まで打てば進む」だが、それだと誤った値のまま次の段が開き、 その段の式と画面の中で食い違う。打ち切った時点で1回のまちがいとして落とす。

UNIT.slotColor は欄キーごとの「目立たせる色」で、宣言された欄に 色・下線の太さ・薄い塗りをまとめて付ける。色だけに意味を持たせないという決まりを 共通画面の側で守るため、3つは分けて指定できない(単元が色だけを宣言することはできない)。

UNIT.glyph は欄キーごとの字形の表で、宣言された欄の数字だけその字で描く (九九の商を漢数字にするなど)。打つのは数字キーのままで、判定も記録も数字。 digitCap と同じく宣言であって、答えは含まない。

判定の方式

方式 使う場面 例
欄ごとの一致 位に分けること自体が目標 1200m = □km□m
合計での一致 値を読むことが目標 まきじゃく(1m87cm も 187 も正解)

UNIT.byTotal(item) が true を返す型は合計で判定する。 空欄は 0 として扱うので、0km400m と「km欄を空にして400」が両方通る。

自動判定と情報漏れ

答えの桁数が確定していれば、Enter なしで自動確定できる。 ただし桁数そのものがヒントになるため、出題側で桁数を揃える責任を負う。

確定に使う桁数は UNIT.digitCap で宣言する。 Core はこの宣言だけを見て、 その問題の実際の答え(ans)は参照しない。

digitCap: { 1: { '': 2 }, 5: { cm: 2 } }   // モードid → 欄キー → 桁数

実際の答えの桁数で確定させると2つ壊れる。1つは、桁数の大小が 「早く確定する/しない」という形でヒントに変わること。もう1つが重い。 答えが1桁の問題は1打で確定し、2桁の問題は2打かかる。 主指標である所要msに、問題の種類ごとの打鍵数の差が混入する。

九九がこれを踏んでいた。9×9 の積は1〜81 で、1桁の答えが81通り中23通り(28%)。 1桁の答えを多く含む段(1・2の段)だけが速く見えていたが、 それは想起が速いのではなく打鍵が1回少ないだけだった。 weak_child は段ごとの平均想起時間を主指標にしているので、 この下駄は分析の結論を直接ゆがめる。

digitCap が無ければ従来の「正解の桁数 or 欄の上限」で確定する。 既存単元との互換のために残してあるが、新しい単元では必ず宣言する。

この宣言を Unit 側に置いたのは依存の向きを保つため。 以前は index.html に 「モード5は2桁、モード6は3桁」という長さ単元固有の分岐が直接書かれていた。 共通画面が特定の単元のモード番号を知っている状態で、 他の単元が ruler を使うと壊れる。宣言に置き換えて解消した。

セキュリティ

脅威 対策
スコアの改竄 点数を送らせない。解答だけ送り、サーバーがシードから問題を再生成して再採点
他人になりすまし トークンにメールを入れて署名し、送信時に照合
点数の水増し 出題番号の重複・範囲外・負の ms を弾く(itemsValid_)
制限時間の詐称 制限時間をセッション開始時の値としてトークンに固定
シートの直接編集 デプロイを「実行=自分/アクセス=ドメイン内」にし、児童に権限を渡さない
教師画面への侵入 教師ドメイン(@edu.nishi.or.jp)・所有者・config.teachers のみ

完全ではない。開発者ツールを開ける児童は突破しうる。 log に1問ごとの所要msが残るので、全問200ms未満のような異常値は事後に検出できる。

教師の判定はドメインの完全一致で行う

教師は @edu.nishi.or.jp、児童は @kyoiku.edu.nishi.or.jp。 児童のドメインは教師ドメインのサブドメインになっている。

tanaka@edu.nishi.or.jp          → 教師
a1234@kyoiku.edu.nishi.or.jp    → 児童(教師ではない)

indexOf('edu.nishi.or.jp') >= 0 や endsWith('edu.nishi.or.jp') で書くと、 児童が全員教師になる。weak_child(誰がどの型で遅いか)まで児童の画面から開けてしまい、 README が「児童に見せない前提」としているものが崩れる。

@ の右側を切り出して TEACHER_DOMAIN と完全一致で比べる(isTeacherDomain_)。 config.teachers は例外の口として残す。別ドメインの教師(支援員・非常勤など)を1件ずつ足す。

判定はサーバー側だけで行い、教師画面の URL は教師と判定したときにしか 児童画面へ渡さない。児童のブラウザにリンクの文字列自体が届かない。

モードの公開

どのモードを児童に見せるかは、教師がクラスごとに指定する。 class_config シートが class | mode_1 | mode_2 | … | seq_off を持ち、 チェックの入ったモードだけが児童の一覧に出る。

以前はこれを「フラグ」で持っていた。単元が flags を宣言し、 モードが flag: 'calc' のように1つのフラグを参照する形で、 フラグは学年で自動的に開き、クラスごとに上書きできた。 この形は、モードよりフラグが粗いため要求に応えられない。 複数のモードが同じフラグを共有するので片方だけ閉じることができず、 flag: null のモードには閉じる手段がそもそも無い。 実際、4単元20モードのうち単独で閉じられるものは1つも無かった。

学年による自動の解禁も廃した。担任が触っていないのにモードが開いている状態を作らない。 どのモードを出すかは進度の判断で、学年から機械的に決まるものではない。 初期状態は全モードがオフなので、設置後に教師画面で開く操作が要る。

Unit.gs は公開の可否を持たない。単元は「どんなモードがあるか」だけを宣言し、 「どれを使うか」は現場が決める。

公開が0件のときだけ、児童画面が取り直す

boot() は起動時の1回しか呼ばれない。教師が授業中に公開しても、 画面を開いたままの端末は再読み込みするまで空のまま残る。 帯活動の途中で40人に「もう一回ひらいて」と言わせるのは、公開の操作としては使えない。

そこで 公開が0件のときだけ 15秒ごとに boot() を取り直す (index.html の watchRun)。1件でも開けば見張りは止まり、以後は通信が増えない。 プレイ中の画面と、ふたを閉じた端末(document.hidden)では叩かない。 負荷は「まだ開いていない端末の数」で頭打ちになり、教師が公開した瞬間に0へ戻る。 40台が同じ授業の頭に開くと巡回の秒が揃うので、間隔に0〜5秒のばらつきを足してある。

10分(40回)で打ち切る。閉じたまま放置された端末が一日中叩き続けるのを防ぐため。 打ち切ったあとは案内文を「さいど よみこんで ください」に替え、 タブに戻ってきた操作(visibilitychange)を「まだ待っている」の合図として数え直す。

順次開放

型を積み上げる単元(あまりのあるわり算の ①九九さがし → ②誤答判別 → ③わり算)は、 前のモードを一定回数やるまで次を開けない。Unit.gs が宣言する。

{ id: 2, needs: { mode: 1, tries: 3 } }

Core は needs という形だけを見る。単元名も型名も入らない。

条件を「回数」にしてある。「①で n 問以上」のような成績条件にすると、 到達バッジを却下したのと同じ欠陥が戻る——閾値の適正値が学級ごとに違い、 上位層はすぐ開き、下位層は最後のモードに永久に到達しない。 回数なら遅い子でも必ず到達し、順序だけが担保される。

class_config の seq_off にチェックを入れると、そのクラスだけ順序を外せる。 授業で全員に同じモードをやらせる場面があるので、担任が上書きできる口を残す。 この列は needs を持つ単元の教師画面にだけ出る。

教師画面(teacher.html)の作り

教師が触る設定

共通の設定(制限時間・閾値など)は BASE_DEFAULTS にある。 単元固有の設定は UNIT.settings で宣言すると、教師画面に欄が自動で生まれる。

settings: [{ key: 'ruler_tape', label: 'テープの太さ', min: 60, max: 200 }]

Core も teacher.html も key の意味を知らない。宣言した単元にだけ欄が出る (長さの巻き尺設定は九九の教師画面に出ない)。欄を直接 teacher.html に 書き足すと、共通画面が単元を知ることになり依存が逆流する。

設定は表示にだけ効かせる。 gen は rand と mode しか受け取らない。 教師が変えた値で出題が変わる作りにすると、サーバーの再採点と食い違ってスコアが壊れる。 長さの巻き尺で「表示範囲(span)」を設定項目にしていないのはこのため。 span は目盛りの窓と答えの値域の両方を決めているので、表示だけを変えられない。

背景の床(成長する図形)

最初のメニューの背景に、練習を重ねるほど育つ非周期の床を敷ける。 単元が settings で floor_on(onoff)と floor_per_answer(1正答あたりの育ち)を宣言したときだけ動く。 図形は単元が UNIT.floorPattern で選ぶ(FLOOR_PATTERNS_ の id。既定は penrose)。 今は九九(八角の星)・あまりのあるわり算(ペンローズ)・長さのたんい(巻き尺の渦)・おもさ(円弧の曼荼羅)・円と球(円と球:起点2つ)が宣言している。 タイルは穴(h)を持てる。画面に掛かるかはタイルの外接する四角で判定する。 タイルは育つ順の値(o、0〜1)も持てる。o を持つ図形は、育ち始める点からの距離ではなく o の順に育つ(起点が複数の図形。 円と球は、円の模様と球の模様それぞれの中の距離の順位を枚数で割った値で、2つが同じ割合ずつ交互に育つ)。①の先のにじみは枚数(次の60枚)で付ける。 生成器は design リポジトリの growing-figures/generators.js を common/index.html に写したもの。直すときは向こうを正本にする。宣言の無い単元では uset_ が 0 を返し、キャンバスは隠れたまま何も計算しない。 図形・育ち方・色の正本は design リポジトリの growing-figures/。ここにあるのはその実装。

モードごとの育ちの倍率

最も練習させたいモードほど、1正答で図形が多く育つようにする。教師画面の全般設定「モードごとの育ちの倍率」で 0.5〜3倍(既定1倍)。 保存先は config の grow_w(”モード:倍率” をカンマでつなぐ)。単元は UNIT.growWeights で既定を宣言できる(あまりのあるわり算は「あまりのある わり算」を2倍)。 growWeights_ が「単元の既定 → 教師の設定」の順に重ねて 0.5〜3 に丸める。

協力モード

クラス全員の本番の正答で1つの図形を育てる、教室の大画面(教師画面の「協力プレイ」タブ)だけに出す仕組み。教師画面のタブは「公開モード|協力プレイ|全般設定|分析」の順。 児童の画面はいつものまま(左上に自分の色のエンブレムと「協力プレイ中」が出るだけ。協力していない間は、終わった回があれば同じ場所に灰色の「前回の協力プレイ」)。

両画面に同じものが要る部品(色の割り当て・起点・割当・枚数の見込み・縮尺の定数)は common/coop_shared.html に1つだけ置き、 index.html と teacher.html が <?!= include('coop_shared') ?> で同じものを挿入する(貼るファイルは5つになる。写しを2か所で直し忘れる事故を無くすため)。 菱形のパス(floorPoly_)は床の FLOOR_UI 区画のものを両画面で共用する(教師画面には床のプレビュー用に同じ区画が埋め込まれている)。 印は画面(section)の外に置くので、メニュー・結果・プレイのどこに居ても出る(明るい画面では白い札)。 教師が始めたときに開いたままの画面にもすぐ出すため、児童側は画面を触っている間だけ10秒+ばらつきごとに coopPeek(自分の色に必要な分だけ返す軽い問い合わせ)を聞く。無操作が5分続く端末・閉じた端末は聞かず、 次の操作や画面への復帰で即1回聞いて再開する(恒常的な通信を増やさない)。変化がある時だけ印を描き直す。

起動の速さ

GAS はサンドボックスの起動と boot() の1往復(300〜800ms)が終わるまで応答しない。 以前はその間 show() が呼ばれず、児童は真っ白な画面を見ていた。 実時間以上に遅く感じる構造で、遅さの体感の大半がこれだった。

対策は2つ。

控えは単元ごとに分けて持つ(キーに UNIT.id を入れる。doGet がテンプレートとして評価するときに APP_ID として埋め込む)。 以前はデプロイURL(location.pathname)で分けていたが、GAS のウェブアプリはどの単元も iframe の中の同じパス(/userCodeAppPanel)で動くので分けられず、 あまりのあるわり算を開くと九九の控えが先に描かれ、サーバーの応答が失敗すると九九のまま残っていた。未送信の記録(km_pending_<単元>)も同じ理由で単元ごとに分ける。

画面を描く処理は、DOM要素が無くても落ちないように書く。 起動時の1行が例外を投げると、 以降が全部止まって白画面になり、原因も見えない。実際 $('apptitle') という 存在しない id への参照(正しくは class="apptitle")が1つあり、これで全画面が死んでいた。

転送量について

QN = 200(1セッションの出題数)は多く見えるが、実測で1セッション7〜15KB。 ruler を積んでも問題にならない。遅さの原因をここに求めない。

同時アクセス(40人想定)

代償:設定変更の反映が最大5分遅れる。教師画面の保存でキャッシュを破棄して即時化できる。

上限は「同時実行30」。これが設計を決めている

Apps Script の同時実行は1ユーザーあたり30。このアプリは「実行=自分(所有者)」で デプロイする(児童にスプレッドシートの編集権を渡さないため、これ以外に選べない)。 つまり40人ぶんの実行が全部、所有者ひとりの30枠を奪い合う。

学級の人数が枠を超えているので、余裕は「1実行あたりの短さ」でしか作れない。 守っている規則は3つ。

  1. シートの全読を人数ぶん繰り返さない。 summary と daily は メール別・クラス別に畳んだ索引をスクリプトキャッシュで共有する (summaryIndex_ / top3_)。読むのは最初の1実行だけ
  2. 待つ実行を作らない。 ロック待ちの実行も枠を占有する。 準備処理は取れなければ諦め(tryLock(0))、記録は10秒で諦めて端末に送り返す。 教室で先に壊れるのは記録ではなく「始められないこと」
  3. 開いただけの実行で書き込まない。 書式の指定もシートへの書き込みなので、 起動経路から外す

枠を超えた実行がどう失敗するかはクライアントからは見えないので、 失敗を握り潰さないことも同じくらい重要。起動は指数バックオフで取り直し、 問題列の到着待ちは15秒で打ち切る(common/index.html)。

データモデル

すべての単元で同じ。単元ごとに別のスプレッドシートを持つ。

シート 書き手 内容
config 教師 制限時間・閾値・教師メール
roster 教師 email / 学年 / 組 / 番号 / 氏名
class_config 教師 クラスごとに公開するモード(mode_1…)と順次開放の解除(seq_off)
log アプリ 1試行1行。誤答と遅答だけ問題単位で圧縮して記録
daily アプリ 当日ランキング(学級 × モード × 制限時間)
summary アプリ 累計と自己ベスト(児童 × モード × 制限時間)
weak_child 集計 児童 × 問題型 の平均想起時間
weak_class 集計 問題型ごとの誤答数と、よくある誤答の一覧
coop アプリ 協力モードの履歴(id / クラス / 図形 / seed / 組 / 名・番号 / 開始・終了 / 状態 / 合計)
coop_log アプリ 協力モードの提出イベント(session / t / email / correct / mode)
coop_save 教師 協力モードの保存データ(再生そのものの payload JSON)

記録・分析の対象は、名簿に email と氏名の両方がある児童だけ。 名簿にないアカウント(名簿未登録の児童・氏名が空の行)はゲストとして 全モードを使えるが、トークンを発行しないので送信も通らず、シートには何も書かれない。 集計も同じ名簿条件で log を絞るので、名簿から氏名を消した児童の過去分は weak_child / mistakes からも消える(log の行自体は残る)。

1試行1行にした理由:問題単位で全部残すと40人×3回×5日で週18,000行になり、 1年で破綻する。正解は集計値で持ち、誤答と閾値超えの遅答だけを1セルに圧縮すれば 週600行に収まる。

主指標を所要msにした理由:既習事項の習熟を測るので、正答率は早期に天井に張り付き 児童間の差が消える。差が出るのは想起にかかる時間。

分析の軸

UNIT.types が分析の列を決める。九九なら「段」、長さなら「問題型」。 長さ版は加えてよくある誤答の実物を集計する。 1200m を「12km 0m」とする1000倍のずれのように、 誤りが型を持つ単元では、誤答の中身が指導の的を直接示す。

2種類の評価

順位だけだと、上位3名以外には何も残らない。個人内評価を並置する。

  何を測る 記号 更新
学級内 当日の学級順位1〜3位 🥇🥈🥉(絵文字) 当日リセット
個人内 自己ベストを塗り替えた回数 ★n(ミントの文字、1〜99) 累積・下がらない

記号の種類(絵文字と文字)で見た目を分けてある。

到達バッジ(一定点数で段階が上がる)にしなかった理由: 閾値の適正値が学級ごとに違い、上位層はすぐ天井、下位層は永遠に最低段階になる。 それは学級内順位と同じ欠陥を、個人内評価の顔をして持ち込むことになる。 更新回数なら遅い子でも伸びれば必ず増える。

★は本番だけ加算する。練習で増やすと意味が薄れる。

練習モードの記録

練習は原則として記録しない。ただし 時間制限あり かつ ランダム出題のときだけ、本番とは別枠のハイスコアを残す。

記録する練習(時間制限あり)は startSession を通す。 クライアントに 点数を自己申告させる経路を作らないため。practice: true を渡し、サーバーが再採点する。

記録しない練習(むせいげん)は nextPracticeItem で1問ずつ受け取る。 採点も記録もしないので startSession のようなトークン・キャッシュは不要。 それでも出題自体は必ずサーバーが作る。クライアントに出題ロジックを 持たせると docs/ADD_UNIT.md が明記する既知バグ(サーバーとの二重管理)を再発するため。

練習で範囲を絞る機能(practiceNarrow / genNarrow)は削除した。 以前この2つを UNIT に定義できる予約があったが、index.html 側の受け口 (段選択画面・絞り込み引数)が存在せず、機能していなかった。 復活させる場合は絞った練習を記録しない前提で、選択画面と nextPracticeItem への引数から作り直すこと。

シートの読みやすさ

生ログは機械可読を優先した形なので、教師がそのまま読むには向かない。 以下を分離した。

fieldsByType はオプション。答えが単一値でよい単元(九九など)は省略でき、 その場合は値をそのまま表示する。

ハブ

単元が増えると入口が分からなくなる。ハブはリンク集に徹する。

意図的に入れていないもの:児童の画面に出す進捗表示、達成バッジ、履歴。 ハブが重くなるほど「入口」としての速さが失われる。

単元横断分析(教師の一部だけ)

Analysis.gs / analysis.html。要件は docs/ANALYSIS_REQUIREMENTS.md。 入口の速さを守るため、児童の経路(doGet → links、boot)には一切載せない。

ハブを学年ごとに分けるとき

ハブを学年(低・高など)ごとに分けて管理する場合、名簿の正本も学年ごとのハブに分かれる。 各単元の roster は縦積みで取り込む:

={IMPORTRANGE("低学年ハブのURL","roster!A:E");IMPORTRANGE("高学年ハブのURL","roster!A:E")}

縦積みでは各ソースのヘッダ行が途中に混ざる。名簿を読む処理は必ず rosterRows_()(Core.gs)を経由する。@ を含まない行(ヘッダ・空行)はここで捨て、 呼び出し側が自分で判定しないこと(読み取り口を1つにしておけば、積み方が変わっても 単元は無関係でいられる)。

同じ児童が複数ハブに載ったとき:同じメールの行は1人に畳む。正本は先に現れた行。 ただし先の行が氏名なしで後の行が氏名ありなら後の行を使う (記録・分析の対象になる児童を消さない)。クラス別の人数も重複カウントしない。 同学年の違う児童(別ハブの3-1同士など)はそのまま併合される—— 実際に別の集団を同じクラス表記で載せる設計なら、クラス側で区別する。

名簿は年度ごとに残す

同じ児童でも年度が違えば別の記録・集計対象(去年は3-1、今年は4-2)。 名簿は年度ごとに分けて残す:

年度替わりは自動:毎朝走る rolloverRoster トリガーが 4/1 にだけ動き、 roster の中身を roster_<前年度> へ「値」として退避し、roster を空に戻す。 A1 が IMPORTRANGE などの式なら式を戻すので、ハブ側が新年度に更新されれば 名簿はそのまま流れ替わる。roster_<前年度> が既にあれば何もしない(手動で 残しておいた年度名簿を上書きしない)。

ぶんせきの過去年度表示は roster_<年度> があればそこから名簿を読む。 無い年度は log の行が持つ「記録時点の学年・組・氏名」から名簿を組み立てるので、 アーカイブが無くても過去年度は正しいクラス所属で見られる (ただしその年度に一度も記録の無い児童は組み立てられない)。

Unit.gs の契約検査

単元が増えると、宣言の欠落や gen の返り値のずれが入り込む。 validateUnit_() が宣言と実働の両方を点検する(エディタからは checkUnit()、 setup() の末尾でも走る。教師画面の読み込み時にも同じ結果が警告として出る)。

「注意:」で始まる行は設計上許容されるもの(digitCap の未宣言欄など)で、 残りは直すべきもの。テスト側では tests/contract.test.cjs が apps/ 配下の 全単元にこの検査を掛けるので、新しい単元は置くだけで契約テストに入る。

版ずれの検知

設置は5ファイルの手動貼り付けなので、「Core.gs だけ古い・index.html だけ新しい」 の組み合わせが起きる。Core.gs は応答に ENGINE_VER を載せ、画面側は自分の WANT_VER と比べて、ちがっていれば画面に警告を出す。

WANT_VER は index.html・teacher.html それぞれに手書きしてある (<?!= ENGINE_VER ?> で埋めると稼働中の Core.gs の値が入り、応答の版と 必ず一致して検知が死ぬ — 検知の対象が「貼ったファイル群の混在」だから、 比べる相手はファイル側に書かれた値でなくてはならない)。 片方にだけ依存を増やす変更(画面が Core の新しい応答を前提にする、またはその逆)では ENGINE_VER と両画面の WANT_VER の3か所を一緒に上げる (ずれたままにすると tests/cache.test.cjs が検知する)。

拡張するとき

新しい単元を足す手順は docs/ADD_UNIT.md を見る。 Unit.gs を1枚書き、Core と index をコピーして貼るだけで動く。 単元を足したら node tests/contract.test.cjs(または setup() / checkUnit())で 宣言と出題の形を機械検査できる。

Core を変えるときは、単元固有の知識を Core に入れないこと。 「長さのときだけ」という分岐が Core に現れたら、それは Unit に置くべきもの。

名簿や設定など、シートの読み取り口を増やさないこと。 rosterRows_() のように1つの関数に寄せておくと、データの取り込み方が変わったとき (例:名簿の縦積み)直す場所が1箇所で済む。

遅いの判定

「遅い」は秒の固定閾値で決めない。同じ学年・同じ型の児童の分布と比べる(Core.gs の slowBand_ ほか)。 型ごとに打鍵数・手順の数が違うので、1つの秒数では型によって赤が出すぎたり出なかったりする。

段 やり方 理由 根拠
1回ごとの時間 対数に直す 反応時間は遅い側に長く裾を引く。平均が裾に引っぱられる Ratcliff (1993):外れ値の処理として、観測を捨てない変換(対数・逆数)を推奨
児童の代表値 対数の平均=幾何平均 ときどき大きく遅れた回(よそ見・数え直し)で代表値が動かない 同上
学年・型の中心と広がり 中央値と MAD×1.4826 平均・SD は数人の極端な値に引っぱられ、その極端な値自体を見落とす。MAD は崩壊点 0.5 Leys et al. (2013)
段階 頑健な z で ≤0/≤1/≤2/>2 ×1.4826 で正規分布なら SD と同じ目盛りになる。偏差値=50−10z(速いほど高い) 同上
1回の中の飛びぬけて遅い問題(slow_items) 対数で 中央値+2.5×MAD 超 論文の既定値 Leys et al. (2013)
本人の中で遅い型(◆) 本人の偏差値の中央より10以上低く、かつその型の偏差値の標準誤差の2倍を超える 個人内の差は2つの値の誤差が両方乗り、見かけの凸凹が出やすい McDermott, Fantuzzo & Glutting (1990) ほか、下位検査のプロフィール分析への批判

根拠の無い値(実データで見直す)

この方式の限界

参考文献