クイズ大会アプリをstreamlitで作ってみた話

# はじめに
忘年会シーズンが近いこともあり、会社でクイズ大会をすることになりました。上司から「問題は俺が作るから、君はGoogle Formでもなんでもいいから、回答できるものを作ってくれ」と言われたので、回答・集計用のアプリをStreamlitで作ってみた話をしたいと思います。

:::note info
この記事はSnowflake Advent Calendar 2023 11日目の記事です。
:::

# 概要
忘年会のクイズ大会ということもあり、スマホからの複数人からの回答を同時に受け付ける必要があるので、StreamlitからSnowflakeのテーブルに回答を出力し、再度SnowflakeからStreamlitへ回答者の順位を返すアプリを下記のような構成で作りました。

![アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634247/be9d38a4-bc0e-5572-a446-edbeedc31a9d.png)

# 0.事前準備
Snowflakeにはあらかじめ2つのテーブルを用意しておきます。
1. 回答結果を登録するテーブル（今回は、"RESULT"テーブル）
1. 正解を登録しておくテーブル（今回は、"ANSWER"テーブル）

# 1.Localでアプリを開発
（3~4時間でザクっと作ってしまったので粗めですが）Streamlitのコードを紹介します。
回答ページと結果発表ページをタブで分けることで、1つのアプリでクイズ大会を乗り切れるようになっています。
ポイントとしては、Streamlit Community Cloudで実行する際にもSnowflakeとの接続情報を取得するために、Secrets managementを使います。

https://docs.streamlit.io/streamlit-community-cloud/deploy-your-app/secrets-management

Snowflakeとの接続情報を記載した下記のtomlファイルは、後ほどStreamlit Community Cloud上でも登録します。

``` .streamlit/secrets.toml
[connections.snowflake]
account = <アカウント名>
user = <ユーザー名>
password = <パスワード>
warehouse = <ウェアハウス名>
database = <データベース名>
role = <ロール名>
```

実行コードはこちらです。
私はこれらに加えて、画像と動画も入れたりしてちょっと遊んだりもしてみました。


``` app.py
import numpy as np
import pandas as pd
import streamlit as st

from snowflake.snowpark.session import Session
session = Session.builder.configs(st.secrets.connections.snowflake).create()

st.header('クイズ大会')

# 回答のページと結果発表のページをtabで分けて用意
tab1, tab2 = st.tabs(['回答ページ', '結果発表'])

with tab1:

     answer = []
     st.subheader('参加者情報')
     answer.append(st.text_input('氏名', ''))

# 4択問題の回答箇所
     st.subheader('張り切っていってみよ〜✊')
     for iii in range(1,11):
          answer.append(st.selectbox(f'解答{iii}', ['1','2','3','4']))
                    
     renames = ['名前','解答1','解答2','解答3','解答4','解答5'
                 ,'解答6','解答7','解答8','解答9','解答10']
     df_answer = pd.DataFrame(answer, index=renames).T

# 既に用意している回答と結合して正解数を計算
     query = "select * from QUIZ.QUIZ_APP.ANSWER;"
     df_collect = session.sql(query).to_pandas()
     df = pd.merge(df_answer.T.reset_index().rename(columns={'index':'ID'})
                    , df_collect, on='ID')
     answer.append((df[0] == df.ANSWER).sum())     

# Snowflakeへデータを登録する用のクエリを作成
     query = f'''insert into QUIZ.QUIZ_APP.RESULT
          select \n
     '''
     for iii in range(len(answer)):
          if iii == 0:
               query = query + f"'{answer[iii]}'\n"
          else:
               query = query + f", '{answer[iii]}'\n"

# 回答の登録
     st.write('以下の解答で送信するよ')
     st.write(df_answer)

# st.buttonを利用してボタンを押したら回答が登録される仕組み
     st.write('問題なかったら、下記ボタンから解答を送信してね！')
     transfer = st.button('解答を送信する')

     if transfer:
          session.sql(query).collect()
          st.subheader(f'「{answer[0]}」さんの解答を受け付けました！')
          st.balloons()       


with tab2:
     st.subheader('🥁結果発表🥁')
# 結果・順位を取得
     query ='''select 
                    *, rank() over( order by collect_answer desc) as rank
               from 
                    QUIZ.QUIZ_APP.RESULT 
               order by 
                    collect_answer desc'''
     df_result =  session.sql(query).to_pandas()
     df_result.rename(columns={'NAME':'名前','COLLECT_ANSWER':'正解数', 'RANK':'順位'}, inplace=True)

# 結果発表
     open_under_4 = st.button(f'4位以下の順位は〜〜')
     if open_under_4:
          st.snow()
          st.write(df_result[df_result['順位'] >= 4].set_index('順位')[['名前', '正解数']])
     
     column1, column2, column3 = st.columns(3)
     with column1:
          open_top2 = st.button(f'2位の人は〜〜', key='No2')
          if open_top2:
               st.snow()
               df_2 = df_result[df_result['順位'] == 2].set_index('順位')[['名前', '正解数']]
               st.write(f'2位は')
               for iii in range(df_2.shape[0]):
                    st.write(f'##### {df_2.iloc[iii].名前} さん')
               st.write(f'でした🎉')
     
     with column2:
          open_top1 = st.button(f'1位の人は〜〜', key='No1')

     with column3:
          open_top3 = st.button(f'3位の人は〜〜', key='No3')
          if open_top2 == True:
               open_top3 = True
          if open_top3:
               st.snow()
               df_3 = df_result[df_result['順位'] == 3].set_index('順位')[['名前', '正解数']]
               st.write(f'3位は')
               for iii in range(df_3.shape[0]):
                    st.write(f'##### {df_3.iloc[iii].名前} さん')
               st.write(f'でした🎉')

     if open_top1:
          st.snow()
          df_1 = df_result[df_result['順位'] == 1].set_index('順位')[['名前', '正解数']]
          for iii in range(df_1.shape[0]):
               st.subheader(f'1位は　🎉　{df_1.iloc[iii].名前}　🎉　さんでした！')
          
     open_all_result = st.button(f'全員の順位は〜〜')
     if open_all_result:
          st.snow()
          st.write(df_result.set_index('順位')[['名前', '正解数']])
```

# 2.Streamlit Community Cloudからアプリをデプロイ
私の場合は、コード類をGitHubにあげておき、それらをStreamlit Community Cloudから公開するようにしました。
具体的な方法は「[30 days of Stremlit]( https://blog.streamlit.io/30-days-of-streamlit/)」の day6 と day7 に書かれています。（2023年12月11日現在）
追加で、先ほど作ったtomlファイルの中身をStreamlit Communitu Cloudの中でコピペする必要があるくらいです[(参考ドキュメント)]( https://docs.streamlit.io/streamlit-community-cloud/deploy-your-app/secrets-management)。

# おわりに
というわけで、クイズ大会用のネタアプリは完成しました！順位発表の部分は好みが出るかな、と思いますので、思い思いのネタを詰めてやるのが楽しかったです。
また、なかなか使い所が難しかったst.snow()やst.baloon()を効果的に使えたのも個人的に満足です。
ちなみに私はクイズ大会本番前に、テスト用の回答結果を削除しておこうと、テーブルをtruncateする際にうっかり正解データを消し飛ばしてしまっていたらしく、結果発表ではテストさんが1位で参加者は全員2位という不甲斐ない結果に終わりました（笑）
（その場で正解数を計算するという大惨事を乗り越え、会自体は無事に終えましたが）