Raspberry Piで温度を測定・比較し、グラフを自動生成する

Qiita 4回目の投稿になります。
IT業界未経験で言葉の定義が間違っている可能性があります。お気づきの点があればアドバイスいただけると助かります。

私の家は築25年の木造建築の一軒家で、冬は1階がとても寒く、夏は2階がそこそこ暑くなってしまいます。
また、木造建築であるためか、家の中の温度変化がゆるやかに感じています。
例えば、６月の朝9時~11時くらいは涼しいのですが、夕方19時くらいは家の中がじめじめします。

そこで、いろいろな季節・場所で温度データを測定したく、RasberryPiで温度データの取得・データの加工を行ってみました。

##はじめに
本当は複数ヶ所で同時刻に温度を測定し比較したかったのですが、予算の都合でRasberryPiを1つしか入手できなかったため、私の家の近くの気象庁のデータと測定データを比較することにしました。

1. DHT11という温度センサーで60分ごとに温度を測定する
2. BeautifulSoupで気象庁の気象データをスクレイピングする
3. matplotlibでグラフを作成する
を行いました。

なお、スクレイピングとはウェブサイトから特定の情報を抽出することです。詳細は[こちら](https://ja.wikipedia.org/wiki/%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0)を確認ください。


##1. DHT11という温度センサーで60分ごとに温度を測定する
#### 1-1.回路を組み、サンプルスクリプトをインストールする
[Raspberry PiとDHT11で温度・湿度を測る](https://qiita.com/mininobu/items/1ba0223af84be153b850)と[ラズパイZeroとDHT11で温度湿度ロガーを作る](https://qiita.com/yuhkan/items/2737749d4532150026ee)
を参考に、回路を組みサンプルスクリプトを作成しました。

使用したのはRaspberry Pi 3B+で、回路はこんな感じです。
![IMG_20200617_094254.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/522259/ae8c1bc5-b2b7-4ef0-cc51-a2d1fff81ab8.jpeg)



サンプルスクリプトの各関数に関しては、[温湿度センサー（DHT11）の詳しい説明](https://www.souichi.club/technology/dht11-datasheet/#parsedatapulluplengths%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)で理解しました。

#### 1-2.サンプルスクリプトを用い、データをcsvに保管するコードを作成する
気象庁のデータをスクレイピングするため、スクレイピングするデータとセンサーで取得するデータの時間を合わせる必要があります。

サンプルスクリプトのcollect_inputとparse_data_pull_up_lengthsでエラーハンドリングを行っているため、サンプルスクリプトのexample.pyを書き換え、測定したデータをcsvファイルに保管できるようにしました。
また、is_varlid()関数を実施して、適切に取得できたかチェックしているようなのですが、この関数で失敗と返る場合が結構あります。

count変数で失敗した回数を取得したところ、
・ 1回で成功 ... 70%
・ 17回目で成功 ... 15%
・ 33回目で成功 ... 10%
・ それ以上 ... 5%
(すみません。統計はとってないです。ただの主観です。)
となり、失敗する場合は2のn乗連続で失敗しているように思われます。
今回は60分に1度データを取得するので、「過去の失敗した回数から考えられる十分大きな試行回数」を2の10乗足す1(=1029)としました。

<details>
<summary>**全コードをクリックで展開**</summary>
<div>

```pyhon
import RPi.GPIO as GPIO
import dht11
import time
import datetime
import os
import numpy as np
import pandas as pd

def add_data(filepath):
    # 所定のpathにcsvファイルが存在しているか確認
    if os.path.isfile(filepath):
        # 存在している場合は読み込み
        df = pd.read_csv(filepath, index_col=0)
    else:
        # 存在していない場合は新規作成
        df = pd.DataFrame([], columns=["year","month","day","hour","sensor_temprature", \
                                       "scraping_temprature","read_try"])
    # 試行回数をカウントする変数を作成
    count=0
    # is_valid()関数がTrueになるまで、最大で1029回の温度データの読み込みを行う
    while count<=1029:
        result = instance.read()
        count+=1
        if result.is_valid():
           # web scrapingのために、年・月・日もDataFrameに格納
            year = datetime.datetime.now().year
            month = datetime.datetime.now().month
            day = datetime.datetime.now().day
            hour = datetime.datetime.now().hour
            temp = result.temperature
            hum = result.humidity
            data=[year,month,day,hour,temp,hum,"",count]
            s = pd.Series(data, index=df.columns)
            df = df.append(s, ignore_index=True)
            break
    return df

# initialize GPIO
GPIO.setwarnings(True)
GPIO.setmode(GPIO.BCM)

instance = dht11.DHT11(pin=14)

filepath = '/home/pi/Desktop/DHT11_Python/data.csv'
df=add_data(filepath)
df.to_csv(filepath)
print(df)
```
</div>
</details>


#### 1-3.60分に一度、温度センサーが起動するように設定する
[Raspberry Pi で systemd を使ってプログラムを自動実行する](https://qiita.com/molchiro/items/ee32a11b81fa1dc2fd8d)を参考に、定刻に起動するよう設定しました。
なお、[Arch manual pages(SYSTEMD.TIME(7))](https://jlk.fjfi.cvut.cz/arch/manpages/man/systemd.time.7.html)下部の"hourly"の通り設定しています。

##2. BeautifulSoupで気象庁の気象データをスクレイピングする
[Pythonで過去の気象データをスクレイピング](https://qrunch.net/@predora005/entries/tNPJgBIVfejaZbkM?ref=qrunch)を参考に、気象庁のデータをスクレイピングできるようコードを書きました。
注意点は、beautiful soupやラズパイ・pipのバージョンにより記述方法が少し変更になる点です。

参考urlと今回のスクレイピング対象ページの違いは、

- 参考リンクのページはテーブルが2つ存在しているのに対し、今回のターゲットとなるページはテーブルが一つしか存在していない。<br>よって、テーブル1が存在しない前提でコードを書き直す必要がある。
- 参考リンクのページは列名が1行しか存在しないが、今回のターゲットとなるページは列名が2行にわたっている。<br>pandasでDataFrameを作成する際に都合がよくないので、手動で列名を設定した。


今回のスクレイピング対象のスクリーンショット
![対象の画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/522259/01b578c6-90fa-4655-a076-c8a7c2432a83.png)


リンクのスクレイピング対象のスクリーンショット
![リンクの画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/522259/acf1eb58-a5d0-73cb-5bd1-b90376807b5b.png)



その他、コードを書く上での変化点は、

- 1ページで1日分の温度データしか取得できない。<br>よって、ページを移動できるよう年・月・日に変数を設定し、全ての日付にアクセスできるようにした。
- 本日付の気象データをスクレイピングしようとすると、ページが存在せずにエラーとなってしまう。<br>よって、本日付のページはスクレイピングされないように設定した。


※ 少々汚いコードです。。。


<details>
<summary>**全コードをクリックで展開**</summary>
<div>

```Python
from bs4 import BeautifulSoup
import pandas as pd
import numpy as np
import requests
import datetime
import os
import sys

# precとblockのコードを変更すると、地域が変更される。
# m_prec=44は東京都を表し、m_block=1133は府中を表す。
def scraping_weather(m_year,m_month,m_day,m_prec=44,m_block=1133):
    url ="https://www.data.jma.go.jp/obd/stats/etrn/view/hourly_a1.php?prec_no={prec}&block_no={block}&year={year}&month={month}&day={day}&view="
    url = url.format(prec=m_prec,block=m_block,year=m_year,month=m_month,day=m_day)
    html=requests.get(url)
    soup = BeautifulSoup(html.content, "html.parser")

    # id='tablefix2'の<table>を抽出  
    table = soup.find('table', id='tablefix1')  

    # table2内の全thを抽出  
    th_all = table.find_all('th')  
    # 列タイトルを手動で格納する  
    table_column = ["時", "降水量","気温", "風速・風向(m/s)","日照時間","雪(cm)","風速","風向","降雪","積雪"]

    # <table>内の全trを抽出。  
    tr_all = table.find_all('tr')  

    # 先頭のtrは抽出済みなので飛ばす  
    tr_all = tr_all[1:]  

    # 行と列の個数を算出し、ndarrayを作成  
    number_of_cols = len(table_column)  
    number_of_rows = len(tr_all)  
    table_data = np.zeros((number_of_rows, number_of_cols), dtype=np.float32)  

    # 各行のデータをndarrayに格納する  
    for r, tr in enumerate(tr_all):  
        td_all = tr.find_all('td')  
        for c, td in enumerate(td_all):  
            try:  
                table_data[r,c] = td.string  
            except ValueError:  
                table_data[r,c] = np.nan  

    # 抽出したデータのDataFrameを生成する  
    df = pd.DataFrame(data=table_data, columns=table_column)  
    return df

def combine_scraping_data(df):
    date_before=str(0)
    # 全データを探索
    for i in range(len(df)):
        # web scraping が過去に実施されているかどうか確認
        if np.isnan(df.loc[i,"scraping_temprature"]):
            year = df.loc[i,"year"]
            month = df.loc[i,"month"]
            day = df.loc[i,"day"]
            
            # 本日付のスクレイピングデータは存在しないので、本日と同じデータの場合はスキップ
            if ((day == datetime.date.today().day)&(month == datetime.date.today().month)):
                continue
            
            #　日付をstr型で代入
            date_now = str(year)+str(month)+str(day)
            
            # 前の行と日付が変更になったかどうか確認
            if date_now != date_before:
                # 変更になっている場合はスクレイピングを実施し、DataFrameに格納
                t_df =  scraping_weather(year,month,day)
                # date_before変数の日付を更新
                date_before=date_now
            for j in range(len(t_df)):
                # スクレイピングしたデータとセンサーデータを突き合わせ、スクレイピングした温度の代入
                if df.loc[i,"hour"] == t_df.loc[j,"時"]:
                    df.loc[i,"scraping_temprature"] = t_df.loc[j,"気温"]
    return df


filepath = '/home/pi/Desktop/DHT11_Python/data.csv'

if os.path.isfile(filepath):
    df = pd.read_csv(filepath, index_col=0)
else:
    print("No data. This python wll be stopped.")
    sys.exit()


for i in range(len(df)):
    if df.loc[i,'hour']==0:
        df.loc[i,'hour'] =24
        df.loc[i,'day'] -=1

df = combine_scraping_data(df)

df.to_csv(filepath)
print(df['scraping_temprature'])

```
</div>
</details>


##3. matplotlibでグラフを作成する
matplotlibでスクレピングデータのグラフを作成し、所定のファイルに保存します。
グラフは[matplotlib で折れ線グラフを描く](https://pythondatascience.plavox.info/matplotlib/%E6%8A%98%E3%82%8C%E7%B7%9A%E3%82%B0%E3%83%A9%E3%83%95)を参考にプロットしました。

resultフォルダの中に各日付のフォルダを作成し、その中にcsvファイルとグラフ(pngファイル)を格納することにしました。図にするとこんな感じです。

```
├── creating_graph.py
└── result
    ├── 6_10
    │   ├── 6_10.csv
    │   └── 6_10.png
    └── 6_11
        ├── 6_11.csv
        └── 6_11.png

```
また、グラフを作成した後に、元データから対象データを削除することにしました。

<details>
<summary>**全コードをクリックで展開**</summary>
<div>

```Python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os


filepath = '/home/pi/Desktop/DHT11_Python/data.csv'

# csvファイルをDataFrameで読み込む
if os.path.isfile(filepath):
    df = pd.read_csv(filepath, index_col=0)
else:
    print("No data. This python wll be stopped.")
    sys.exit()

# 日ごとのグラフを作成するので、月・日のデータをリスト化しループを回す
for g_month in df["month"].unique():
    for g_day in df["day"].unique():
        # 対象データをt_dfに読み込む
        t_df = df[(df['day']==g_day)&(df['month']==g_month)]
        # スクレイピングデータが一つでも存在しない場合はスキップする
        if t_df["scraping_temprature"].isnull().any():
            continue
        
        # 新規フォルダを作成する
        result_filepath = '/home/pi/Desktop/DHT11_Python/dht11_creating_graph/result/{month}_{day}'
        result_filepath = result_filepath.format(month=g_month, day=g_day)
        os.makedirs(result_filepath, exist_ok=True)
        
        # csvファイルを保存するためのパスを作成し、保存する
        result_filename = '/{month}_{day}.csv'
        result_filename = result_filename.format(month=g_month, day=g_day)
        t_df.to_csv(result_filepath+result_filename)
        
        # グラフを保管するためのパスを作成する
        result_graphname = '/{month}_{day}.png'
        result_graphname = result_graphname.format(month=g_month, day=g_day)
        
        # グラフを作成する
        x=t_df['hour']
        y1=t_df['sensor_temprature']
        y2=t_df['scraping_temprature']
        
        fig = plt.figure()
        p1 = plt.plot(x, y1, linewidth=2)
        p2 = plt.plot(x, y2, linewidth=2, linestyle="dashed")
        plt.legend((p1[0], p2[0]), ('sensor_temprature', 'scraping_temprature'), loc=2)
        plt.xlabel("hour")
        plt.ylabel("temprature")
        
        # グラフを保存する
        fig.savefig(result_filepath+result_graphname)
        
        # 元データから、グラフを作成済みのデータを削除する
        df = df[(df['day']!=g_day)|(df['month']!=g_month)]
        # 元データのインデックスを振り直す
        df.reset_index(inplace=True, drop=True)

# 元データを保存する
df.to_csv(filepath)

```
</div>
</details>

##　結果
つっこみどころは多いのですが、2階の暑い部屋の6月14日のデータと1階のそこまで暑くない部屋の6月16日のデータを取得できました。
下のグラフを見てもらえると、1階のグラフでは8時から18時くらいに部屋の温度がスクレイピングした温度より低くなっているのに対し、2階のグラフでは部屋の温度がスクレイピングした温度より常に高いことがわかります。

2階の暑い部屋のグラフ
![6_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/522259/52e13cde-6700-3cbb-cf6f-03083bb1d60a.png)

1階のそこまで暑くない部屋のグラフ
![6_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/522259/46edd1ee-0695-4e64-697c-582575c221ee.png)

#### つっこみどころ

- ２階の温度は雨の日に測定し、1階の温度は晴れの日に測定している。そもそも前提条件が違うのでは？
- 温度センサー自体の測定誤差を考慮するとどうなるのか？
- 測定場所の風の当たり具合、日の当たり具合で誤差がでるのでは？
- 温度センサー自体が光っているが、この光で温度が上がり測定値がぶれないか？
- 温度センサーが傾いてしまった場合、測定結果に誤差が生じないか？

参考写真
![IMG_20200617_094254.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/522259/ae8c1bc5-b2b7-4ef0-cc51-a2d1fff81ab8.jpeg)

## おわりに
測定結果に疑義はありますが、温度データのスクレイピングとグラフの作成をある程度自動化できたので、今回は終了とします。
気になる点があれば連絡もらえると嬉しいです！