AtCoderの大学別レート分布を出してみた

# はじめに
「[PythonのWebスクレイピングでAtCoderのレート分布を出してみた](https://qiita.com/saba/items/8955f228c32fe0f4e4bb)」の続きです。
今回は年代別ではなく所属大学別にレート分布を出しました。
基本的な部分は前のものと同じです。

# 所属について
AtCoder の所属はユーザーが好きなものを書けるため、所属大学が同じでも表記が異なることがあります。例えば、同じ東京大学所属でも、所属を「東京大学」にしている人と「The University of Tokyo」にしている人がいます。
このような表記揺れに対応するために、まずは各大学の表記例を以下のスプレッドシートにて集めました。
https://docs.google.com/spreadsheets/d/1I1jQHw1A7oZv-At2XjDwuAG2IKZS3Q-pbEWUjg76XLM/edit?usp=sharing
追加してくれた方々ありがとうございます。


# 実装
まずスプレッドシートにより集計された大学別所属名を csv ファイルとしてダウンロードして、そのファイルを読み込みます。空白は検索時に "+" に置き換える必要があるので、 replace() で置換しておきます。

```python
# 所属名一覧
Affiliation_dict = defaultdict(list)  # {大学名: [所属名のリスト], ...}

with open("AtCoder大学名表記一覧 - シート1.csv") as f:
    reader = list(csv.reader(f))
    for daigaku in reader[5:]:
        for syozoku in daigaku[2:]:
            if syozoku:
                Affiliation_dict[daigaku[1]].append(syozoku.replace(" ", "+"))

# {'東京大学': ['The+University+of+Tokyo', '東京大学', 'UT', 'UTokyo'], ...}
```

今回は所属別のレート分布のため検索対象に日本語の文字列が入ります。そのため、日本語での検索に対応させる必要があります。「[PythonでURLエンコード・デコード（urllib.parse.quote, unquote）](https://note.nkmk.me/python-urllib-parse-quote-unquote/)」を参考にすると、

```python
if is_japanese(aff):  # 所属が日本語文字列か調べる
    Affiliation = parse.quote(Affiliation)  # URLエンコード
```

このように urllib.parse.quote() を使ってあげると日本語の文字列をURLエンコードしてあげることができます。
以下本コードです。

```python
from urllib import request, parse
from bs4 import BeautifulSoup
from collections import defaultdict
import csv
import re


# 日本語かどうか
def is_japanese(string):
    p = re.compile("[ぁ-んァ-ン一-龥]+")
    return p.search(string) is not None


# 所属名一覧
Affiliation_dict = defaultdict(list)  # {大学名: [所属名のリスト], ...}

with open("AtCoder大学名表記一覧 - シート1.csv") as f:
    reader = list(csv.reader(f))
    for daigaku in reader[6:]:
        for syozoku in daigaku[2:]:
            if syozoku:
                Affiliation_dict[daigaku[1]].append(syozoku.replace(" ", "+"))

print(Affiliation_dict)

num = {}  # {大学名: [灰色の人数, 茶色の人数, ...]} の形式で人数を入れる

for univ in Affiliation_dict:
    print("-{}-".format(univ))

    univ_num = [0] * 10  # 大学ごとの[灰色の人数, 茶色の人数, ...]

    for aff in Affiliation_dict[univ]:
        print("--{}--".format(aff))

        if is_japanese(aff):  # 所属が日本語文字列か調べる
            Affiliation = parse.quote(aff)  # URLエンコード
        else:
            Affiliation = aff

        for rating in range(0, 4000, 400):
            print(rating)

            RatingLowerBound = rating
            if rating == 3600:  # 金以上
                RatingUpperBound = 9999
            else:
                RatingUpperBound = rating + 399

            # フィルタを URL にセットする
            url_filter = "?f.Affiliation=" + Affiliation + \
                         "&f.RatingLowerBound=" + str(RatingLowerBound) + \
                         "&f.RatingUpperBound=" + str(RatingUpperBound) + \
                         "&page="

            url = "https://atcoder.jp/ranking"
            html = request.urlopen(url + url_filter + "0")
            soup = BeautifulSoup(html, "html.parser")

            ul = soup.find_all("ul")

            a = []

            # 指定したフィルタでのページ数を調べる
            page = 0
            for tag in ul:
                try:
                    string_ = tag.get("class")

                    if "pagination" in string_:
                        a = tag.find_all("a")
                        break

                except:
                    pass

            for tag in a:
                try:
                    string_ = tag.get("href")

                    if "ranking" in string_:
                        page = max(page, int(tag.string))

                except:
                    pass

            # フィルタ内順位
            rank = []

            # 順位の最大値を調べるために、順位表の最後のページを見る
            html = request.urlopen(url + url_filter + str(page))
            soup = BeautifulSoup(html, "html.parser")

            td = soup.find_all("span")

            for tag in td:
                try:
                    string_ = tag.get("class").pop(0)

                    if string_ == "small":
                        rank.append(int(tag.string[1:-1]))

                except:
                    pass

            if rank:
                # フィルタ内順位の最大値がその人数
                univ_num[rating // 400] += max(rank) + rank.count(max(rank)) - 1

        num[univ] = univ_num

# 人数が多い順にソートする
num_sort = sorted(num.items(), key=lambda x: sum(x[1]), reverse=True)

dict_sort = {}

for i in num_sort:
    dict_sort[i[0]] = i[1]

print(dict_sort)

# CSV として書き出し
# 横軸が大学名、縦軸が色（上から灰色, 茶色, ...の順）
with open("rating_univ.csv", "w", encoding="shift_jis") as f:
    writer = csv.DictWriter(f, dict_sort.keys())
    writer.writeheader()
    for i in range(10):
        row = {}
        for k, v in dict_sort.items():
            row[k] = v[i]
        writer.writerow(row)
```

# 結果
2019年8月10日8時00分時点での所属集計結果を元にして大学別レート分布を作成しました。数が多いため、10人以上いる大学のみになります。

・出力された csv ファイルを Excel にインポートして色付け
<img width="1315" alt="スクリーンショット 2019-08-10 8.18.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/7be06244-6859-7679-e08d-322290b4a878.png">
<img width="1295" alt="スクリーンショット 2019-08-10 8.18.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/b97727b5-91df-16d6-d0bb-3224ee0d0edc.png">


・100% 積み上げ縦棒
<img width="1146" alt="スクリーンショット 2019-08-10 8.19.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/d05242da-1f97-3b38-c1f7-5a5fd2866331.png">


・積み上げ縦棒
<img width="1105" alt="スクリーンショット 2019-08-10 8.19.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365958/bcd4325a-0284-9fff-d8fe-ac34f57e1b5b.png">