【R Shiny】理解度チェック・小テストの作り方|選択式問題と自動採点を実装

Shiny

R Shinyでは、グラフやシミュレーションだけでなく、学生が問題に回答し、その場で正誤判定を確認できる学習アプリも作成できます。

例えば、薬理学の授業で薬物動態について説明した後に、選択式の問題を出題し、学生が自分の理解度を確認できる仕組みを作成できます。

今回は、R Shinyを使って選択式の理解度チェックを作成する方法を紹介します。

具体的には、次の機能を実装します。

  • 選択式問題を表示する
  • 学生が選択肢を選ぶ
  • 「回答する」ボタンを押す
  • 正解・不正解を自動判定する
  • 正解と解説を表示する
  • 複数の問題をまとめて採点する
  • 得点を表示する

今回のサンプルでは、学生の氏名や回答履歴を保存する機能は実装しません。回答をCSVファイルなどに保存する方法は、次の記事で扱います。

R Shinyを初めて使用する方は、まず【R Shiny入門】R Shinyの基本とできることをご覧ください。R Shinyの概要や、どのようなアプリを作成できるのかを紹介しています。

また、R Shinyの基本的なアプリの作り方では、uiとserverの役割や、基本的な画面の作成方法を解説しています。

前回の記事「【R Shiny】HTML教材の作り方と組み込み方法」では、HTMLファイルを作成し、R Shinyに表示する方法を紹介しました。

HTML教材と理解度チェックを組み合わせることで、教材の閲覧から理解度の確認までを1つのアプリで行うことも可能です。

今回はHTMLを使用せず、Shinyの標準機能だけで問題を作成します。そのため、前回の記事を読んでいなくても実行できます。

1. R Shinyで理解度チェックを作る仕組み

通常のWebページでは、文章や画像を表示することが中心になります。

一方、Shinyでは、利用者の入力に応じてRの処理を実行し、その結果を画面に表示できます。

理解度チェックの場合、次の流れになります。

問題を表示
    ↓
学生が回答を選択
    ↓
「回答する」をクリック
    ↓
Shinyが回答を取得
    ↓
Rで正誤を判定
    ↓
判定結果と解説を表示

この仕組みを実現するために、今回は主に次の関数を使用します。

関数役割
radioButtons()選択肢を表示する
actionButton()回答ボタンを作成する
observeEvent()ボタンが押されたときに処理を実行する
reactiveVal()判定結果などを保持する
renderUI()判定結果を画面に表示する
uiOutput()renderUI() の表示場所を指定する

これらの関数を組み合わせることで、比較的少ないコードで理解度チェックを実装できます。

2. 必要な環境

今回使用するのは、R、RStudio、Shinyパッケージです。

Shinyパッケージをまだインストールしていない場合は、RStudioのConsoleで次のコードを実行してください。

# Shinyをインストールする
# 最初の1回だけ実行すればよい

install.packages("shiny")

すでにShinyをインストールしている場合、この操作は不要です。

今回は追加パッケージを使用しません。

3. まずは1問だけの理解度チェックを作る

最初に、1問だけの簡単な理解度チェックを作成します。

題材は、薬物動態の消失半減期です。

問題:線形の1-コンパートメントモデルにおいて、クリアランスが一定のまま分布容積が2倍になると、消失半減期はどうなるでしょうか?

正解は「2倍になる」です。

① アプリ用フォルダを作成する

デスクトップなどに、次のフォルダを作成してください。

Shiny_Quiz

この中に app.R を保存します。

Shiny_Quiz
└── app.R

デスクトップ全体をShinyアプリのフォルダとして指定するのではなく、専用フォルダを使用してください。

② app.Rにコードを入力する

RStudioで新しいR Scriptを作成し、以下のコードをすべてコピーしてください。

# ==================================================
# R Shiny 理解度チェック:1問版
# ==================================================

# Shinyパッケージを読み込む
library(shiny)


# --------------------------------------------------
# 1. 画面を作成する
# --------------------------------------------------

ui <- fluidPage(

  # アプリのタイトル
  titlePanel("薬物動態 理解度チェック"),

  # 問題の見出し
  h3("問題1:消失半減期"),

  # 問題文
  p(
    "線形の1-コンパートメントモデルにおいて、",
    "クリアランスが一定のまま分布容積が2倍になると、",
    "消失半減期はどうなりますか?"
  ),

  # ----------------------------------------------
  # 選択肢を表示する
  # ----------------------------------------------

  radioButtons(

    # 回答を識別するための名前
    inputId = "answer",

    # 選択肢の上に表示する説明
    label = "正しいものを1つ選んでください",

    # 選択肢
    choices = c(
      "半分になる",
      "変化しない",
      "2倍になる",
      "4倍になる"
    ),

    # 最初は何も選択しない
    selected = character(0)

  ),

  # ----------------------------------------------
  # 回答ボタン
  # ----------------------------------------------

  actionButton(

    # ボタンを識別する名前
    inputId = "submit",

    # ボタンに表示する文字
    label = "回答する"

  ),

  # 区切り線
  hr(),

  # 判定結果を表示する場所
  uiOutput("feedback")

)


# --------------------------------------------------
# 2. サーバー側の処理
# --------------------------------------------------

server <- function(input, output, session) {


  # ----------------------------------------------
  # 判定結果を保存するための変数
  # ----------------------------------------------

  # NULLは「まだ判定していない」ことを表す
  feedback_result <- reactiveVal(NULL)


  # ----------------------------------------------
  # 回答ボタンが押されたときの処理
  # ----------------------------------------------

  observeEvent(input$submit, {

    # 学生が選択した回答を取得する
    answer <- input$answer


    # 何も選択していない場合
    if (is.null(answer) || length(answer) == 0) {

      feedback_result(
        list(
          status = "unanswered",
          message = "選択肢を選んでください。"
        )
      )

      # ここで処理を終了する
      return()

    }


    # 正解かどうかを判定する
    if (identical(answer, "2倍になる")) {

      # 正解の場合
      feedback_result(
        list(
          status = "correct",
          message = paste0(
            "正解です。消失半減期は ",
            "t1/2 = 0.693 × Vd / CL ",
            "で表されます。CLが一定なら、",
            "Vdが2倍になると半減期も2倍になります。"
          )
        )
      )

    } else {

      # 不正解の場合
      feedback_result(
        list(
          status = "incorrect",
          message = paste0(
            "不正解です。正解は「2倍になる」です。",
            "消失半減期はVdに比例し、",
            "CLに反比例します。"
          )
        )
      )

    }

  })


  # ----------------------------------------------
  # 判定結果を画面に表示する
  # ----------------------------------------------

  output$feedback <- renderUI({

    # 保存された判定結果を取得する
    result <- feedback_result()

    # まだ回答ボタンが押されていない場合
    if (is.null(result)) {
      return(NULL)
    }


    # 未回答の場合
    if (result$status == "unanswered") {

      return(
        div(
          style = "color: #a05a00;",
          result$message
        )
      )

    }


    # 正解の場合
    if (result$status == "correct") {

      return(
        div(
          style = paste(
            "background-color: #e7f5e9;",
            "padding: 15px;",
            "border-radius: 6px;"
          ),

          h4("正解!"),

          p(result$message)
        )
      )

    }


    # 不正解の場合
    div(
      style = paste(
        "background-color: #fff0f0;",
        "padding: 15px;",
        "border-radius: 6px;"
      ),

      h4("不正解"),

      p(result$message)
    )

  })

}


# --------------------------------------------------
# 3. アプリを起動する
# --------------------------------------------------

shinyApp(
  ui = ui,
  server = server
)

③ アプリを実行する

コードを app.R として Shiny_Quiz フォルダに保存してください。

RStudioで app.R を開き、「Run App」をクリックします。

問題文と4つの選択肢が表示されれば成功です。

次の操作を試してください。

  1. 何も選ばずに「回答する」をクリックする
  2. 不正解の選択肢を選んで回答する
  3. 正解の「2倍になる」を選んで回答する

それぞれ、未回答のメッセージ、不正解の解説、正解の解説が表示されることを確認してください。

4. radioButtons()とは?

今回のアプリで、選択肢を表示しているのが radioButtons() です。

radioButtons(

  inputId = "answer",

  label = "正しいものを1つ選んでください",

  choices = c(
    "半分になる",
    "変化しない",
    "2倍になる",
    "4倍になる"
  ),

  selected = character(0)

)

radioButtons() は、複数の選択肢の中から1つを選択するための関数です。

主な引数は次のとおりです。

引数意味
inputId入力欄を識別する名前
label入力欄の説明
choices選択肢
selected最初に選択しておく値

inputIdの役割

今回のコードでは、次のように指定しています。

inputId = "answer"

この場合、学生が選択した回答は、サーバー側で次のように取得できます。

input$answer

例えば「2倍になる」を選択すると、input$answer は "2倍になる" という値になります。

このように、Shinyでは inputId を使って画面の入力とRの処理を対応させます。

selected = character(0)の意味

通常、radioButtons() は最初の選択肢があらかじめ選択されます。

しかし、理解度チェックでは、学生が自分で選択するまで未回答の状態にしておきたい場合があります。

そこで、次のように指定しています。

selected = character(0)

これにより、アプリを起動した時点では、どの選択肢も選ばれていない状態になります。

5. actionButton()とobserveEvent()の役割

今回のアプリでは、「回答する」ボタンを押したときに正誤判定を実行します。

ボタンは次のコードで作成しています。

actionButton(
  inputId = "submit",
  label = "回答する"
)

そして、サーバー側で次のように処理しています。

observeEvent(input$submit, {

  # ボタンが押されたときに実行する処理

})

observeEvent() は、指定したイベントが発生したときに処理を実行するための関数です。

今回の場合は、input$submit が回答ボタンに対応しています。

そのため、学生が選択肢を変更しただけでは採点せず、「回答する」をクリックした時点で採点します。

このように、入力の変更と採点の実行を分けられることが重要です。

6. reactiveVal()とは?

今回のコードでは、判定結果を保持するために reactiveVal() を使用しています。

feedback_result <- reactiveVal(NULL)

reactiveVal() は、Shinyのリアクティブな値を1つ保持するための関数です。

値を保存するときは、次のように記述します。

feedback_result(
  list(
    status = "correct",
    message = "正解です。"
  )
)

値を取得するときは、次のように記述します。

result <- feedback_result()

ここで重要なのは、feedback_result と feedback_result() の違いです。

feedback_result はリアクティブな値を管理するためのオブジェクトで、feedback_result() は現在の値を取得する操作です。

また、feedback_result(新しい値) とすると、その値を更新できます。

値が更新されると、その値を参照している renderUI() などの処理が再実行されます。

7. renderUI()とuiOutput()とは?

Shinyでは、Rの処理結果に応じて画面の内容を変更できます。

今回使用しているのが、renderUI() と uiOutput() です。

画面側では、次のように記述しています。

uiOutput("feedback")

これは、判定結果を表示する場所です。

一方、サーバー側では次のように記述します。

output$feedback <- renderUI({

  h4("正解!")

  p("消失半減期は2倍になります。")

})

ただし、この例では renderUI() 内で最後に評価された p() だけが返されます。

見出しと文章を両方表示したい場合は、次のように tagList() でまとめます。

output$feedback <- renderUI({

  tagList(

    h4("正解!"),

    p("消失半減期は2倍になります。")

  )

})

このように、複数の画面要素をまとめて返す場合は tagList() や div() を使用します。

今回の完成コードでは div() を使い、背景色や余白も指定しています。

8. 複数問題の理解度チェックを作成する

ここまでは1問だけのアプリでした。

次に、3問の理解度チェックを作成し、まとめて採点する方法を紹介します。

今回の問題は次の3問です。

問題1: クリアランスが一定で分布容積が2倍になると、消失半減期はどうなるか?

問題2: 分布容積が一定でクリアランスが2倍になると、消失半減期はどうなるか?

問題3: 線形の1-コンパートメントモデルにおいて、消失半減期を決定する組み合わせはどれか?

それぞれ1点、合計3点満点とします。

複数問題版の完成コード

先ほどの app.R を上書きする前に、1問版を残したい場合は、別のフォルダにコピーしておいてください。

次のコードを app.R にコピーします。

# ==================================================
# R Shiny 理解度チェック:3問版
# ==================================================

# Shinyパッケージを読み込む
library(shiny)


# --------------------------------------------------
# 1. 問題と正解を設定する
# --------------------------------------------------

# 各問題の正解を保存する
correct_answers <- c(

  q1 = "2倍になる",

  q2 = "半分になる",

  q3 = "CLとVd"

)


# 各問題の解説を保存する
explanations <- c(

  q1 = paste0(
    "t1/2 = 0.693 × Vd / CL なので、",
    "CLが一定ならVdが2倍になると半減期も2倍になります。"
  ),

  q2 = paste0(
    "t1/2 = 0.693 × Vd / CL なので、",
    "Vdが一定ならCLが2倍になると半減期は半分になります。"
  ),

  q3 = paste0(
    "線形の1-コンパートメントモデルでは、",
    "消失半減期はCLとVdによって決まります。"
  )

)


# --------------------------------------------------
# 2. 画面を作成する
# --------------------------------------------------

ui <- fluidPage(

  # アプリのタイトル
  titlePanel("薬物動態 理解度チェック(3問版)"),

  p(
    "以下の3問に回答し、",
    "「まとめて採点する」をクリックしてください。"
  ),

  hr(),


  # ----------------------------------------------
  # 問題1
  # ----------------------------------------------

  h3("問題1"),

  p(
    "CLが一定のままVdが2倍になると、",
    "消失半減期はどうなりますか?"
  ),

  radioButtons(
    inputId = "q1",
    label = NULL,
    choices = c(
      "半分になる",
      "変化しない",
      "2倍になる",
      "4倍になる"
    ),
    selected = character(0)
  ),

  hr(),


  # ----------------------------------------------
  # 問題2
  # ----------------------------------------------

  h3("問題2"),

  p(
    "Vdが一定のままCLが2倍になると、",
    "消失半減期はどうなりますか?"
  ),

  radioButtons(
    inputId = "q2",
    label = NULL,
    choices = c(
      "半分になる",
      "変化しない",
      "2倍になる",
      "4倍になる"
    ),
    selected = character(0)
  ),

  hr(),


  # ----------------------------------------------
  # 問題3
  # ----------------------------------------------

  h3("問題3"),

  p(
    "線形の1-コンパートメントモデルにおいて、",
    "消失半減期を決定する組み合わせはどれですか?"
  ),

  radioButtons(
    inputId = "q3",
    label = NULL,
    choices = c(
      "CLとVd",
      "投与量と投与速度",
      "吸収速度定数と投与量",
      "最高血中濃度と投与間隔"
    ),
    selected = character(0)
  ),

  hr(),


  # ----------------------------------------------
  # 採点ボタン
  # ----------------------------------------------

  actionButton(
    inputId = "grade",
    label = "まとめて採点する"
  ),

  hr(),


  # ----------------------------------------------
  # 採点結果の表示場所
  # ----------------------------------------------

  uiOutput("score_result")

)


# --------------------------------------------------
# 3. サーバー側の処理
# --------------------------------------------------

server <- function(input, output, session) {


  # ----------------------------------------------
  # 採点結果を保持する
  # ----------------------------------------------

  grade_result <- reactiveVal(NULL)


  # ----------------------------------------------
  # 採点ボタンが押されたときの処理
  # ----------------------------------------------

  observeEvent(input$grade, {

    # 各問題の回答を取得する
    answers <- c(
      q1 = input$q1,
      q2 = input$q2,
      q3 = input$q3
    )


    # ------------------------------------------
    # 未回答があるか確認する
    # ------------------------------------------

    # 未回答の場合、radioButtonsの値はNULLになる
    # c()ではNULLが取り除かれるため、
    # 個別に確認する

    unanswered <- c(
      is.null(input$q1) || length(input$q1) == 0,
      is.null(input$q2) || length(input$q2) == 0,
      is.null(input$q3) || length(input$q3) == 0
    )


    # 1問でも未回答なら採点しない
    if (any(unanswered)) {

      grade_result(
        list(
          status = "unanswered",
          message = "未回答の問題があります。すべて回答してください。"
        )
      )

      return()

    }


    # ------------------------------------------
    # 正誤を判定する
    # ------------------------------------------

    # 各問題の回答と正解を比較する
    correct <- answers == correct_answers


    # TRUEの数を合計して得点を計算する
    score <- sum(correct)


    # ------------------------------------------
    # 採点結果を保存する
    # ------------------------------------------

    grade_result(
      list(
        status = "graded",
        score = score,
        correct = correct,
        answers = answers
      )
    )

  })


  # ----------------------------------------------
  # 採点結果を画面に表示する
  # ----------------------------------------------

  output$score_result <- renderUI({

    # 採点結果を取得する
    result <- grade_result()


    # まだ採点していない場合
    if (is.null(result)) {
      return(NULL)
    }


    # 未回答がある場合
    if (result$status == "unanswered") {

      return(
        div(
          style = "color: #a05a00;",
          result$message
        )
      )

    }


    # ------------------------------------------
    # 各問題の結果を作成する
    # ------------------------------------------

    question_results <- lapply(
      seq_along(correct_answers),
      function(i) {

        # 問題番号
        question_number <- i

        # 正誤
        is_correct <- result$correct[i]

        # 正誤に応じた文字列
        judgment <- if (is_correct) {
          "正解"
        } else {
          "不正解"
        }

        # 問題ごとの表示
        div(

          style = paste(
            "border: 1px solid #dddddd;",
            "padding: 12px;",
            "margin-bottom: 12px;",
            "border-radius: 6px;"
          ),

          h4(
            paste0(
              "問題",
              question_number,
              ":",
              judgment
            )
          ),

          p(
            paste0(
              "あなたの回答:",
              result$answers[i]
            )
          ),

          p(
            paste0(
              "正解:",
              correct_answers[i]
            )
          ),

          p(
            paste0(
              "解説:",
              explanations[i]
            )
          )

        )

      }
    )


    # ------------------------------------------
    # 合計得点と問題ごとの結果を表示
    # ------------------------------------------

    tagList(

      h3(
        paste0(
          "得点:",
          result$score,
          " / 3点"
        )
      ),

      question_results

    )

  })

}


# --------------------------------------------------
# 4. アプリを起動する
# --------------------------------------------------

shinyApp(
  ui = ui,
  server = server
)

実行結果

アプリを起動すると、3問の選択式問題が表示されます。

すべての問題に回答して「まとめて採点する」をクリックすると、合計得点と各問題の正誤、正解、解説が表示されます。

1問でも未回答がある場合は、採点せずにメッセージを表示します。

なお、採点後に選択肢を変更しただけでは得点は更新されません。もう一度「まとめて採点する」をクリックすると、新しい回答で採点されます。

9. 複数問題の採点処理を理解する

3問版では、正解を次のようにまとめて設定しています。

correct_answers <- c(

  q1 = "2倍になる",

  q2 = "半分になる",

  q3 = "CLとVd"

)

これは、各問題の正解を名前付きベクトルとして保存したものです。

学生の回答も同じ順番で取得します。

answers <- c(
  q1 = input$q1,
  q2 = input$q2,
  q3 = input$q3
)

そして、次のコードで正誤を判定します。

correct <- answers == correct_answers

例えば、3問のうち問題1と問題3が正解だった場合、次のような結果になります。

TRUE FALSE TRUE

Rでは、TRUE は1、FALSE は0として合計できます。

そのため、次のコードで得点を計算できます。

score <- sum(correct)

この例では2点になります。

このように、複数問題の採点は、回答と正解を比較することで実装できます。

10. 問題数を増やすには?

今回の3問版を応用すれば、5問や10問の理解度チェックも作成できます。

基本的には、次の部分を追加します。

  • radioButtons() で新しい問題を表示する
  • correct_answers に正解を追加する
  • explanations に解説を追加する
  • answers に新しい入力を追加する
  • 未回答チェックに新しい問題を追加する

ただし、問題数が多くなると、コードが長くなります。

その場合は、問題文、選択肢、正解、解説をデータフレームなどで管理し、画面を自動生成する方法も考えられます。

今回の記事では、Shinyの基本的な仕組みを理解するため、問題を1つずつ記述する方法を採用しました。

11. GoogleフォームとR Shinyの比較:どちらを使うべきか?

ここまでR Shinyを使った理解度チェックの作成方法を紹介しましたが、選択式問題を出題して自動採点するだけであれば、Googleフォームでも同様のことができます。

Googleフォームは、プログラミングを必要とせず、画面操作だけで問題を作成できます。正解や配点、回答後のフィードバックを設定でき、学生の回答を記録する機能も備えています。

そのため、通常の小テストであれば、Googleフォームの方が簡単で便利な場合が多いでしょう。

では、あえてR Shinyで理解度チェックを作成するメリットは何でしょうか?

GoogleフォームとR Shinyの機能比較

項目GoogleフォームR Shiny
問題の作成画面操作で簡単Rコードの作成が必要
選択式問題対応対応
記述式問題対応実装可能
自動採点標準機能で対応実装可能
正解・解説の表示対応実装可能
回答・得点の記録標準機能で対応保存処理の実装が必要
回答日時の記録標準機能で対応実装可能
解答所要時間の測定標準機能では限定的実装可能
問題ごとの解答時間標準機能では困難実装可能
問題・選択肢の順序変更シャッフル可能自由に実装可能
回答に応じた条件分岐セクション分岐に対応より柔軟な分岐が可能
グラフや計算との連携限定的高い自由度
回答結果の集計標準機能で対応Rで自由に解析可能
学生への配布URL共有で容易アプリの公開環境が必要
維持管理比較的容易開発者による管理が必要

Googleフォームでは、正解の設定、配点、フィードバック、採点結果の公開方法などを標準機能で設定できます。また、回答の集計やGoogleスプレッドシートへの出力にも対応しています。

一方、Shinyでは、これらの機能を自分で実装する必要があります。https://support.google.com/docs/answer/7032287?hl=ja

メリット① 解答時間を測定できる

R Shinyの利点の1つは、解答時間を細かく測定できることです。

例えば、問題が表示された時刻と、学生が回答ボタンを押した時刻を記録すれば、その差から解答時間を計算できます。

さらに、問題ごとに表示時刻と回答時刻を記録すれば、各問題の解答時間を測定できます。

例えば、次のようなデータを取得することが考えられます。

問題回答正誤解答時間
問題12倍になる正解8.4秒
問題2変化しない不正解15.2秒
問題3CLとVd正解6.8秒

※この表は説明用の架空データです。

Googleフォームでも回答の送信時刻は記録されますが、標準機能だけで問題ごとの解答時間を測定することは困難です。

Shinyでは、Rの時刻取得機能を利用して、このような処理を実装できます。

ただし、今回のサンプルコードには解答時間の測定機能を実装していません。また、測定した時間を後から確認するには、別途保存処理が必要です。

さらに、ブラウザ上で問題が実際に表示された瞬間を厳密に測定したい場合は、JavaScriptなどを組み合わせる必要があります。Rのサーバー側で取得した時刻との差には、通信や描画の遅延が含まれる場合があります。

メリット② 回答に応じて問題を変更できる

Googleフォームでも、回答に応じて移動先のセクションを変更することができます。

しかし、R Shinyでは、回答内容、得点、解答時間、計算結果などを組み合わせて、より柔軟に次の問題を決定できます。

例えば、次のような学習アプリが考えられます。

  • 正解した場合は、より難しい問題を出題する
  • 不正解だった場合は、基礎的な問題に戻る
  • 解答時間が長い場合は、補足説明を表示する
  • 一定の得点に達した場合は、次の学習段階に進む

このように、学生の回答に応じて出題内容を変える仕組みは、適応型学習にも応用できます。

メリット③ シミュレーションと問題を連携できる

R Shinyの大きな利点は、Rによる計算処理やグラフ表示と、理解度チェックを同じアプリ内で連携できることです。

例えば、薬物動態の教材であれば、学生がクリアランスや分布容積を変更し、血中濃度推移の変化を確認した後に、その結果に関する問題を出題できます。

また、学生ごとに異なるパラメータを設定し、それぞれの計算結果に基づいて正解を判定することも可能です。

このような、計算結果によって正解が変わる問題は、固定された選択肢を中心とするGoogleフォームよりも、Shinyの方が実装しやすい場合があります。

メリット④ 回答データを自由に解析できる

Googleフォームでも、回答結果をGoogleスプレッドシートに出力し、集計や解析を行うことができます。

したがって、単純な正答率や平均点の計算だけであれば、Shinyを使用する必要はありません。

一方、Shinyでは、回答内容に加えて、解答時間、パラメータの変更履歴、試行回数、操作順序などを記録するように設計できます。

これらを組み合わせることで、正誤だけでなく、学生がどのような過程で回答に至ったかを分析できる可能性があります。

ただし、こうした詳細なログの取得には追加の実装が必要であり、学生の個人情報や学習履歴を扱う場合は、適切な説明、アクセス管理、保存方法の検討も必要です。

Googleフォームの方が優れている点

ここまでShinyの利点を紹介しましたが、Googleフォームには明確なメリットがあります。

最も大きいのは、作問から配布、回答の収集までが非常に簡単なことです。

プログラミングを必要とせず、問題文、選択肢、正解、配点などを画面操作で設定できます。

また、回答を自動的に保存できるため、ShinyのようにCSVファイルへの書き込みやデータベースを自分で実装する必要がありません。

さらに、Googleフォームは、問題や選択肢のシャッフル、回答の必須設定、回答結果の集計などにも対応しています。

一方、Shinyでは、アプリを学生に公開するためのサーバー環境も必要です。

大学の授業で多数の学生に利用してもらう場合には、アクセス集中時の動作やデータの保存先、セキュリティなども考慮しなければなりません。

結局、どちらを使うべきか?

通常の小テストやアンケートであれば、Googleフォームを推奨します。

選択式問題を出題し、回答を集め、自動採点して結果を確認するだけであれば、Googleフォームで十分な場合が多いでしょう。

一方、解答時間の測定、学生の回答に応じた出題、シミュレーションとの連携などが必要な場合は、R Shinyを検討する価値があります。

特に、薬物動態のシミュレーションや薬理学の仮想実験など、学生が操作しながら学習する教材では、Shinyの柔軟性を活用できる可能性があります。

重要なのは、Shinyを使うこと自体を目的にするのではなく、学習目標に応じて適切な方法を選択することです。

12. 今回のアプリの限界

今回のサンプルは、理解度チェックの基本的な仕組みを学ぶためのものです。

実際の授業で使用する場合は、いくつか注意点があります。

回答は保存されない

今回のアプリでは、回答や得点をファイルに保存していません。

アプリを閉じたり、セッションが終了したりすると、回答結果は保持されません。

回答を記録したい場合は、CSVファイルやデータベースなどを利用する必要があります。

学生の本人確認はできない

今回のアプリには、学生のログイン機能や本人確認機能がありません。

そのため、誰が回答したのかを特定することはできません。

厳密な試験には適していない

今回のサンプルでは、正解をRコード内に設定しています。

また、回答回数の制限や不正行為対策も実装していません。

そのため、成績評価に使用する正式な試験ではなく、学生が自分の理解度を確認するための練習問題として利用することを想定しています。

複数の学生が利用する場合

Shinyでは、利用者ごとにセッションが作成されます。

今回の reactiveVal() は server 関数内で作成しているため、通常は各セッションで独立した判定結果を保持します。

ただし、回答を共通のCSVファイルなどに保存する場合は、同時書き込みやデータ管理について別途考慮する必要があります。

13. まとめ

今回は、R Shinyを使用して理解度チェックを作成する方法を紹介しました。

まず、radioButtons() を使用して選択式の問題を表示しました。

次に、actionButton() と observeEvent() を組み合わせ、回答ボタンが押されたときに正誤判定を実行しました。

さらに、reactiveVal() で判定結果を保持し、renderUI() と uiOutput() を使用して結果と解説を表示しました。

最後に、3問の理解度チェックを作成し、まとめて採点して得点を表示する方法を紹介しました。

今回の重要なポイントは、次の4つです。

  1. radioButtons() で選択式の回答を取得できる
  2. observeEvent() で回答ボタンが押されたときに処理を実行できる
  3. reactiveVal() で判定結果を保持できる
  4. 複数の回答を正解と比較することで自動採点できる

今回のアプリは、HTMLを使用せずに作成しています。

そのため、前回の記事で紹介したHTML教材と組み合わせれば、教材を読んでから問題に回答する学習アプリにも発展させられます。

次回は、R Shinyで学生の回答や得点をCSVファイルに自動保存する方法を紹介します。

回答日時、選択した回答、得点などを記録し、教員が後から確認できる仕組みを作成します。

コメント

タイトルとURLをコピーしました