機械学習で積雪の有無を予測してみた。


## やりたかったこと
機械学習初心者の僕が、機械学習の勉強のためにPythonの機械学習ライブラリ `scikit-learn` を使ってみたかった。
何を予測してみようかなーっと思ってしばらく考えたわけですが「特定の条件を与えられると、地面に雪が積もっているかどうかを予測する」とか面白いかなと思ってやり始めたわけです。

ちなみにPythonアプリを書くのは初めてです。
使いなれたRubyで何とかならんか、って思ったのですが、この分野ではやはりPythonが強いようで、変につまづいて苦労したくない怠け者なのでPythonで始めました。

## 学習用のデータを集める

さて、まずは機械学習エンジンに学習させるために、実際の積雪データを用意します。
ここでは気象庁の[気象データ・ダウンロードのサイト](http://www.data.jma.go.jp/gmd/risk/obsdl/#)から、実際の観測データを落とします。
今回は、積雪の多い富山県の砺波市の気象データを学習データとして使用しました。

```cvs:data_2013_2015.csv
ダウンロードした時刻：2016/03/20 20:31:19

,砺波,砺波,砺波,砺波,砺波,砺波,砺波,砺波,砺波,砺波,砺波,砺波,砺波,砺波
年月日時,気温(℃),気温(℃),気温(℃),積雪(cm),積雪(cm),積雪(cm),風速(m/s),風速(m/s),風速(m/s),風速(m/s),風速(m/s),降水量(mm),降水量(mm),降水量(mm)
,,,,,,,,,風向,風向,,,,
,,品質情報,均質番号,,品質情報,均質番号,,品質情報,,品質情報,均質番号,,品質情報,均質番号
2013/2/1 1:00:00,-3.3,8,1,3,8,1,0.4,8,西,8,1,0.0,8,1
2013/2/1 2:00:00,-3.7,8,1,3,8,1,0.3,8,北,8,1,0.0,8,1
2013/2/1 3:00:00,-4.0,8,1,3,8,1,0.2,8,静穏,8,1,0.0,8,1
2013/2/1 4:00:00,-4.8,8,1,3,8,1,0.9,8,南南東,8,1,0.0,8,1
...
```

こんな感じのデータです。
上記の気象庁のサイトでは必要な項目を選べるのですが、`気温` 、`積雪` 、`風速` 、`風向` 、`降水量` を選択しました。ただ、風速・風向は使いませんでしたが...

また、気象庁のサイトでは一度に落とせるデータ量に制限がかかっていますので、四回に分けて2004年から2015年の2月、3月のデータを落としました。

これを 

```
iconv -f Shift-JIS -t UTF-8 sample_data_sjis/data_2004_2006.csv >> sample_data/data.csv
```

のようにしてSJISからUTF-8に変換、かつ１つのファイルにまとめて不必要な行を削除したのが[こちら](https://github.com/hiroeorz/snow-forecast/blob/master/sample_data/data.csv)です。

### 学習の実行〜予測まで

あとは、scikit-learnを使って、学習を行い、学習したモデルを用いて予測してみます。
スクリプトは以下のとおり。

＊ちなみに、以下のPythonスクリプトや学習用データを含むアプリは、githubの[hiroeorz/snow-forecast](https://github.com/hiroeorz/snow-forecast)にあげています。

```python:snow_forecaster.py
import csv
from sklearn.svm import LinearSVC
from sklearn.ensemble import AdaBoostClassifier,ExtraTreesClassifier,GradientBoostingClassifier,RandomForestClassifier
from sklearn.decomposition import TruncatedSVD
from sklearn import datasets
from sklearn.cross_validation import cross_val_score

class SnowForecast:

    CLF_NAMES = ["LinearSVC","AdaBoostClassifier","ExtraTreesClassifier" ,
                 "GradientBoostingClassifier","RandomForestClassifier"]

    def __init__(self):
        u"""各インスタンス変数を初期化"""
        self.clf = None
        self.data = {"target" : [], "data" : []}
        self.weather_data = None
        self.days_data = {}
        self.days_snow = {}

    def load_csv(self):
        u"""学習用CSVファイルを読み込む"""
        with open("sample_data/data.csv", "r") as f:
            reader = csv.reader(f)
            accumulation_yesterday = 0
            temp_yeaterday = 0
            date_yesterday = ""
            
            for row in reader:
                if row[4] == "":
                    continue

                daytime = row[0]
                date = daytime.split(" ")[0]
                temp = int(float(row[1]))
                accumulation = int(row[4])
                wind_speed = float(row[7])
                precipitation = float(row[12])

                if date_yesterday != "":
                    # [温度, 降水量, 昨日の温度, 昨日の積雪量]
                    sample = [temp, precipitation, temp_yeaterday, accumulation_yesterday]
                    exist = self.accumulation_exist(accumulation)
                    self.data["data"].append(sample)
                    self.data["target"].append(exist)
                    self.days_data[daytime] = sample
                    self.days_snow[daytime] = exist

                if date_yesterday != date:
                    accumulation_yesterday = accumulation
                    temp_yeaterday = temp
                    date_yesterday = date

        return self.data

    def is_snow_exist(self, daytime_str):
        u"""雪が積もったなら1、積もらなければ0を返す."""
        return self.days_snow[daytime_str]

    def predict_with_date(self, daytime_str):
        u"""与えられた日付のデータを使って積雪の有無を予想する。"""
        sample = self.days_data[daytime_str]
        temp = sample[0]
        precipitation = sample[1]
        temp_yeaterday = sample[2]
        accumulation_yesterday = sample[3]
        return self.predict(temp, precipitation, temp_yeaterday, accumulation_yesterday)

    def predict(self, temp, precipitation, temp_yeaterday, accumulation_yesterday):
        u"""与えられたパラメータを使って積雪の有無を予想する。"""
        return self.clf.predict([[temp, precipitation, temp_yeaterday, accumulation_yesterday]])[0]

    def train_data(self):
        u"""学習を行うためのデータを返す。すでに読み込み済みならそれを返し、まだならCVSファイルから読み込む"""
        if self.weather_data is None:
            self.weather_data = self.load_csv()

        return self.weather_data

    def accumulation_exist(self, accumulation):
        u"""積雪量(cm)を受け取り、積雪があれば１、なければ0を返す"""
        if accumulation > 0:
            return 1
        else:
            return 0

    def best_score_clf(self):
        u"""各学習モデルのタイプ別にスコアを計算し、もっともスコアの高いタイプのオブジェクトをインスタンス変数にとっておく."""
        features = self._features()
        labels = self._labels()

        # 今回は特徴量の算出に4量しか使わないので特徴量の削減は行わない。よって以下はコメントにしておく。
        # lsa = TruncatedSVD(3)
        # reduced_features = lsa.fit_transform(features)

        best = LinearSVC()
        best_name = self.CLF_NAMES[0]
        best_score = 0

        for clf_name in self.CLF_NAMES:
            clf    = eval("%s()" % clf_name) 
            scores = cross_val_score(clf, features, labels, cv=5) # 特徴量削減した場合は reduced_features を使う
            score  = sum(scores) / len(scores)  #モデルの正解率を計測
            print("%sのスコア:%s" % (clf_name,score))
            if score >= best_score:
                best = clf
                best_name = clf_name
                best_score = score

        print("------\n使用するモデル: %s" % best_name)
        return clf

    def train(self):
        u"""学習を実行する。実際の学習の前にどのモデルを使うかを判定し、自動的に選択させる。"""
        self.clf = self.best_score_clf()
        self.clf.fit(self._features(), self._labels())

    def _features(self):
        u"""学習データを返す。"""
        weather = self.train_data()
        return weather["data"]

    def _labels(self):
        u"""結果のラベルを返す。"""
        weather = self.train_data()
        return weather["target"]

    def judge(self, datetime_str):
        u"""日付文字列を受け取って積雪判定を行う。"""
        print("------")
        result = forecaster.predict_with_date(datetime_str)
        print("%s: 予想:%s 実際:%s" % (datetime_str, result, forecaster.is_snow_exist(datetime_str)))

        if result == 1:
            print("雪が積もります")
        else:
            print("雪は積もらないです")

if __name__ == "__main__":
    forecaster = SnowForecast()
    forecaster.train()
    
    #####################################################
    # 日付を指定して、学習に使用したパラメータを与えて判定する。
    #####################################################
    forecaster.judge("2006/2/19 00:00:00")
    forecaster.judge("2012/2/2 00:00:00")
    forecaster.judge("2014/2/2 13:00:00")
    forecaster.judge("2015/2/28 00:00:00")
    
    #######################################
    # パラメータを直接与えて予測させてみる。
    #######################################
    print("------")
    temp = 0.0
    precipitation = 0
    temp_yeaterday = 3.0
    accumulation_yesterday = 2 
    result = forecaster.predict(temp, precipitation, temp_yeaterday, accumulation_yesterday)
    print("[温度:%s] [降水量:%s] [昨日の温度:%s] [昨日の積雪量:%s]" %
          (temp, precipitation, temp_yeaterday, accumulation_yesterday))
    
    print("判定結果: %s" % result)
    
    if result == 1:
        print("雪が積もります")
    else:
        print("雪は積もらないです")
        
    #########################################################
    # パラメータを直接与えて予測させてみる(昨日の温度を-3.0℃に変更)。
    #########################################################
    print("------")
    temp_yeaterday = -3.0
    result = forecaster.predict(temp, precipitation, temp_yeaterday, accumulation_yesterday)
    print("[温度:%s] [降水量:%s] [昨日の温度:%s] [昨日の積雪量:%s]" %
          (temp, precipitation, temp_yeaterday, accumulation_yesterday))
    
    print("判定結果: %s" % result)
    
    if result == 1:
        print("雪が積もります")
    else:
        print("雪は積もらないです")
        
    print("------")
```

なんせPythonは初めてなので、おかしなところがあったら指摘していただけると助かります。

実行は以下のように

```
$ python snow_forecaster.py
```

実行結果は以下のようになりました。

```
LinearSVCのスコア:0.965627801273
AdaBoostClassifierのスコア:0.969820996581
ExtraTreesClassifierのスコア:0.961194223678
GradientBoostingClassifierのスコア:0.966826266875
RandomForestClassifierのスコア:0.958078728911
------
使用するモデル: AdaBoostClassifier
------
2006/2/19 00:00:00: 予想:1 実際:1
雪が積もります
------
2012/2/2 00:00:00: 予想:1 実際:1
雪が積もります
------
2014/2/2 13:00:00: 予想:0 実際:0
雪は積もらないです
------
2015/2/28 00:00:00: 予想:0 実際:0
雪は積もらないです
------
[温度:0.0] [降水量:0] [昨日の温度:3.0] [昨日の積雪量:2]
判定結果: 0
雪は積もらないです
------
[温度:0.0] [降水量:0] [昨日の温度:-3.0] [昨日の積雪量:2]
判定結果: 1
雪が積もります
------
```

最初に幾つかのモデルでスコアを計算し、最もスコアの良いモデルで学習を実行します。ここでは `AdaBoostClassifier` を採用したようです。

ちなみに、モデルの選択をする部分のコードは[機械学習クソ素人の俺がプロダクトをリリースするまでの2ヶ月で覚えたこと](http://qiita.com/puriketu99/items/c519a95c0b16ea63c1ac)のコードを使わせていただきました。

で、あとは学習の実行と判定を行います。
後半部分で、温度、降水量、昨日の積雪量を同じにして、昨日気温を変えてみています。昨日2cmの積雪があって昨日気温が3.0℃だと、本日は「積雪がない」と判定していますが、昨日気温を -3.0℃に変更して予想させてみると、「積雪がある」との判定に変わりました。昨日雪が積もっていて、温度が低ければ溶けずに残る可能性が高いので、これは直感的にも正しそうです。

## 感想など

理論も何も理解していませんが、とりあえず初心者でも数時間で機械学習アプリを試してみることができました。ちなみに積雪予測に関しては、最初本日分データだけで予測させてようとしていましたがうまくいかず、昨日のデータを与えると急に予測精度が97%近くに上がりました。確かに昨日の時点で雪が積もっていれば翌日も積もっている可能性が高いわけで、こういったところでどのようなパラメータを与えるか、を現実的なところから持ってくると精度が上がるなーと思いました。
面白いので、もうちょっと色々と試して、多少慣れてきたら理論の方もちょっと勉強してみようと思います。