大学の評定平均(GPA)を簡単に計算するサービスをリリースしてみた

## はじめに
山形大学理学部4年のHagianです．[Python Advent Calendar 2021](https://qiita.com/advent-calendar/2021/python)の18日目を担当します．

大学で履修した情報数学の講義でPythonに出会って早2年，そろそろ何かリリースしてみたいなあ…というのが今年の漠然とした目標でした．そんな中，今年の9月にGPA計算機を開発しリリースすることができました。本記事ではその解説をしていきます．

&darr;リリースしたサービスはこちら&darr;

[GPA Calculator for Yamagata University](https://calc.remh.dev/)

## きっかけ

開発しようと思ったきっかけはいくつかありますが，1番は**新型コロナウイルス感染症の流行**です．昨年度から感染状況によってオンライン授業 + できるだけキャンパスに立ち入らないようにする…みたいな期間が長くありました．成績配布も紙媒体で行われていましたが，オンラインのシステムで発表されたあと，しばらく経ってからしか手元に来ない状況が起きました．

そこで困ったことが1つ．GPA (評定平均) やGPS (評定の総和) がわからないのです．

>GPAとは
履修登録した科目についてそれぞれの単位数にグレードポイント (GP; 4, 3, 2, 1, 0のいずれか) をかけ，その合計ポイントGPSを，それぞれの単位数の総和で割ったものとなっています (引用：[GPA制度&mdash;山形大学](http://www.yamagata-u.ac.jp/gakumu/yuss/gpa.html))．

弊学の学務情報システム上では評定(S, A, B, C, F)や評点(0~100点)は表示されるようになっています．しかしGPAやGPSは自分で計算するか，紙で配布されるのを待つしかありません．早く知りたければ自分で計算すればいいのですが，履修科目数が多いとちょっと手間がかかります．
長くなりましたが，その**手間をなくそう！**というのが開発のきっかけです．

## システム概要
**開発環境**

* MacBook Pro 2017
* macOS Big Sur 11.3.1
* Python 3.9.6
* VS Code 1.59.1

**フレームワーク**

* Streamlit

https://streamlit.io/

**使用モジュール**

* math
* re

**クラウドプラットフォーム**

* Heroku

https://www.heroku.com/home

* Github Pages

https://docs.github.com/ja/pages

**その他**

* Google Domains

https://domains.google.com/?hl=ja

学務情報システムに表示された成績をコピペするだけでGPAやGPSが計算できるような流れを目指しました．

## 開発
コードはPythonで記述しました。PythonにはStreamlitというライブラリ (フレームワーク) があり，これを用いるとPythonファイルからWebUI付きのアプリを簡単に作ることができます．

https://qiita.com/tags/streamlit

学務情報システムでは次のように成績が表示されます (リストはイメージです)．

| No. | 科目大区分 | 科目中区分 | 科目小区分 | 科目名 | 単位数 | 修得年度 | 修得学期 | 評価 (評点) | 評語 (評定) | 合否 | 
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
| 1 | 専門教育科目 | 理学専門科目 | コースカリキュラム | 情報数学Ｃ | 2.0 | 2019 |  後期 | 85 | Ａ | 合 |

成績の凡例以外をコピーし，GPA計算機のテキストエリアにペーストすると計算できるように実装しました．

![スクリーンショット 2021-12-16 12.02.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1021972/9a65a1ca-73ff-391f-8d46-69693e9517f4.png)

### 実装のポイント
以下，コードの一部を使って説明します．

```python:GPACalculator.py
def main():
    # (中略)
    text = st.text_area(label="ここに成績を貼り付けてください。",value="")
    text = re.sub("\t",",",text)
    lines = re.split("合|否|認",text)
```

ペーストしたテキストの処理に少し手こずりました．まず，表内のデータはタブで区切られていることがわかったので，正規表現モジュールを使ってカンマ区切りに直しました．次にデータ1行をどのように判定するかです．表示は複数行ですが，コピペすると1行のテキストになってしまうことがわかりました．そこで合否の文字で1行が終わっているので，その漢字を判定に用いました．

```python:GPACalculator.py
    # (前略)
    # 評定を数値化
            if "Ｓ" in l[1]:
                out = 4
            elif "Ａ" in l[1]:
                out = 3
            elif "Ｂ" in l[1]:
                out = 2
            elif "Ｃ" in l[1]:
                out = 1
            elif "Ｎ" in l[1]:
                out = -1
            else:
                out = 0
```

forループで1行のデータを単位数と評定だけにしたあと，上記のようなif文で評定を数値に変換しました．コードにある通り評定は**全角英字**だったため，ここを失念して何回かハマりました．

```python:GPACalculator.py
grade_point_average = math.floor(grade_point_sum/degree_count*10**2)/(10**2)
```

全ての成績について変換・計算したあと，GPAを計算しました．GPAは**小数点以下第3位を切り捨てて計算する**ので，上記のように丸め処理を行いました．以下の記事を参考にしました．

https://qiita.com/ny7760/items/1918c43f206fb6c372eb

### エラー回避
実装に向けてローカルテストをしていた際，2つのエラーが発生することがわかりました．

* ```ZeroDivisionError```
* ```UnboundLocalError```

これらのエラーが出ると，実行が止まってしまいうまく動作しません．そこで```try-except```文を用いて2つとも回避しています．
エラー原因についてですが，テキストエリアに何も入っていない状態 (=初期状態) でロードすると，計算するものが何もなく，```main()```関数内のローカル変数が定義前に出現してしまうことに起因しているようです．

## リリース
クラウドプラットフォームはHerokuを選びました．無料プランではアクセスがない状態で一定時間経過すると止まってしまいますが，再起動までの時間もそこまでかからなかったので問題はなさそうだと感じました．
Heroku上でWebアプリを動かすのに必要なファイル類は以下のように記述しました．

**Procfile**

```:Procfile
web: streamlit run --server.enableCORS false --server.port $PORT GPACalculator.py
```

**runtime.txt**

```:runtime.txt
python-3.9.6
```

**requirements.txt**

```:requirements.txt
streamlit==0.88.0
```

### Read Meファイル
使い方を説明したファイルをどこにアップロードするか考えましたが，Github Pagesを使うことで解決しました．

前期の成績発表前にリリースすることを目標としていたので，9月13日の夜にリリース，Twitterで告知を行いました．

## 反響

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【告知】<br>山形大生向けに、成績を計算するプログラムを作りました！<br>学務情報システムから成績をコピペするだけで、GPS・GPA・総単位数が計算できます！<a href="https://t.co/TeeEc7xrzJ">https://t.co/TeeEc7xrzJ</a></p>&mdash; みのすけ (@4voltex) <a href="https://twitter.com/4voltex/status/1437416065579970563?ref_src=twsrc%5Etfw">September 13, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

リリース告知ツイートは3ヶ月経過したいま，50リツイートを超えました．たくさんのフォロワーから感謝の言葉をもらい，必要性を再度実感しました．

### ライセンス
リリース後，コードはGithubにて公開することにしました．著作権関連はどうするか迷いましたが，ソースコードはMITライセンス，その他文書はCC4.0ライセンスのもと公開しています．

## Herokuプラン変更・独自ドメイン適用
リリースから2ヶ月ほど経過した11月，Herokuのプランを見直し，無料プランからHobbyプラン($7/月，動作時間無制限)にアップグレードしました．理由としては，無料プランでは月550時間しかアプリを動作させることができません．9月，10月の2ヶ月とも月末までに使用時間のアラートメール (リミットの8割到達) を受け取っていて，来年3月の成績公開時に不安があったためです．

またHobbyプランにアップグレードしたのを機に，独自ドメインの適用も行いました．

### 独自ドメイン関連の設定
以下のQiita記事を参考にしました．

https://qiita.com/sengoku/items/39660d25e67debd7d131

https://qiita.com/shinshin86/items/87dd3adc6879fc35ea6e

#### ドメインの取得
まずドメインを取得しました．今回はGoogle Domainsを利用しました．ドメイン名はなんでもいいんですが，

* できるだけ短いもの
* ```.dev```ドメインが欲しい...

という点から選び，```remh.dev```というドメインを取得しました．使用料金は ¥1400/年 です．

#### サブドメインの設定
次にサブドメインを設定しました．サブドメインを用いるとGithub PagesとGPA Calculatorで使い分けられるので便利です．

#### 注意点
.devドメインは**HTTPS接続必須**ですので．HTTPSに関する設定を行いました．

```:.htaccess
RewriteEngine on
RewriteCond %{HTTP:X-Forwarded-Proto} ^http$
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
```

上記のように```.htaccess```というファイルに設定を記述し，Herokuへデプロイしました．
Github Pagesは設定から「Enforce HTTPS」にチェックを入れました．

### 独自ドメイン関連まとめ
**費用**

* Heroku Hobbyプラン：月額 $7
* Google Domains：年額 ¥1400

*****
1年あたりの費用：およそ ¥11000 ($1 = ¥114で計算)

## その他
話が前後しますが，10月下旬にソースコードを修正し，PEP8に準拠した形式にしました．また随時メンテナンスを行い，Pythonやフレームワークの最新版を適用させるようにしています．

## 今後の予定
利用規約やプライバシーポリシーのページを設ける予定です．あとは収益化できたらいいなぁと思っています．収益は管理費用に充てたいと考えています．

## おわりに
以下にGithubリポジトリへのリンクを張っておきます．

https://github.com/PyRadiolarus/yucalc-gpa