Pythonで定期計測したネット回線速度をプロバイダに提出した話

## はじめに
実家のインターネット回線が本当に瞬断しているのかを調査するため、回線速度を定期計測するPythonスクリプトを作成ました。定期計測することで、通信速度のスパイク的な低下やネットワークの瞬断を視覚的にとらえることができました。

本記事は、回線速度の測定結果をプロバイダに提出したというお話になります。もし、1日の回線速度を定点観測したい方がいましたら、本記事がお役に立つかもしれません。

![回線速度計測_下り.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/8f5a5e12-0141-1edc-1f93-1e2dadfc4986.png)


## 背景
私は普段、東京でWebエンジニアをしています。

2023年の年末に帰省したところ、父からある問題を相談されました。
実家では、ある特定の時間帯に回線速度が低下し、数分間インターネットが使用できない状況になるそうです。数分経過すると、インターネットの回線速度が回復するという現象が起こっていました。
私も帰省した際に、インターネットが一時的に利用できなくなる問題に直面しましたが、単発の現象で再現性がありませんでした。

父はこの問題を解決したく、契約先のプロバイダに問い合わせをしていましたが、プロバイダからは、「弊社の回線速度には問題がありません。」との回答が続き、議論が平行線をたどっているという状況でした。

私も、時折インターネットにつながりにくくなるという現象を肌感覚では認識できていましたが、第三者に説明する際の客観的な根拠がなく、プロバイダへの説明が難しいと感じていました。

![回線速度計測_関係者_平行線.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/d45f18e7-de0a-5a2b-52c1-5ddfaf580498.png)

## 課題
ここまでの課題を整理すると下記のようになります。

- 実家の回線速度低下を示す客観的な根拠がない
  - 本当に回線速度が低下しているのか分からない 
  - 回線速度低下が一日に何回発生しているのか分からない

そこで1週間実家に帰省している間に、私が目指すべきGoalを下記のように定義しました。

## Goal
- 実家の回線速度をプロバイダに把握してもらうこと
- 回線速度低下が本当に発生している場合、プロバイダに改善策の検討を依頼できること

Goalを達成するための手段として、1日の回線速度の変化を定量的な形で観測し、計測結果をプロバイダに共有する方針としました。

![回線速度計測_関係者.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/058a0e97-59d7-bd0c-672a-ecb55a08a907.png)

## 手段
具体的な手段は下記のとおりです。
1. 1日の回線速度を1分おきに計測し、1日の回線速度を数値データで記録する
    （SpeedTest CLIをバッチ実行するPythonスクリプトを作成）
2. 数値データから、グラフを作成する（今回はSpreadSheetで手作業）
3. グラフからネットワークの瞬断が確認できた場合、プロバイダに状況を共有する

## 実装
ここからは、回線速度を計測するためのPythonスクリプトの実装に入ります。

### 事前設定
- [speedtest-cli](https://pypi.org/project/speedtest-cli/) のPythonライブラリをpip install
- スクリプト実行中にPCが勝手にスリープしないようにするため、Windowsの電源オプションを設定
    <details>
    <summary>画像はこちらをクリック</summary>

    ![回線速度計測_スリープになるまでの時間.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/78da03a9-2314-c079-40ba-e5263cfdf61a.png)
    ![回線速度計測_電源プラン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/2a83f539-9310-78dc-bc07-0924c429fee0.png)
    
    </details>

### Pythonスクリプト
- 下記のPythonスクリプトを作成

```python:speed-test.py
import pandas as pd
import datetime as dt
import time
import speedtest


def create_file_if_not_exists(file_path):
    """記録用のcsvがなければ新規作成、あれば何もしない"""
    open(file_path, "a+")


def is_csv_empty(file_path):
    """空のCSVファイルであればTrueを返す"""
    with open(file_path, "r") as file:
        return not file.readline().strip()


def run_speed_test(s):
    """Speed-test Cli を実行する

    Args:
        s (Speedtest): 初期化済みのSpeedtestインスタンス

    Returns:
        DataFrame: 計測時刻とSpeedtestの実行結果を記録したdataframe (1回分の計測結果)
    """
    servers = []
    # If you want to test against a specific server
    # servers = [1234]

    threads = None
    # If you want to use a single threaded test
    # threads = 1

    s.get_servers(servers)
    s.get_best_server()
    s.download(threads=threads)
    s.upload(threads=threads)
    s.results.share()

    # 結果行にタイムスタンプ追加
    time_dict = {"datetime": dt.datetime.now().strftime("%Y/%m/%d %H:%M:%S")}
    results_dict = time_dict | s.results.dict()
    return pd.DataFrame.from_dict(results_dict, orient="index").T


def write_result_to_csv(df, csv_file):
    """CSVに計測結果を書き込む"""
    if is_csv_empty(csv_file):
        df.to_csv(csv_file, mode="a", encoding="shift-jis", index=True, header=True)
    else:
        df.to_csv(csv_file, mode="a", encoding="shift-jis", index=True, header=False)
    return


def measure_network_speed(result_csv):
    """回線速度を計測し、ファイルに書き込む

    Args:
        result_csv (str): 書き込み先のCSVファイルパス
    """
    start = time.time()
    print(current_time.strftime("%Y/%m/%d %H:%M:%S"), "run speed test")
    try:
        df = run_speed_test(st)
    except Exception as e:
        print("エラー:", e)
        return
    print("elapsed", int(time.time() - start), "[sec]")
    write_result_to_csv(df, result_csv)


if __name__ == "__main__":
    now = dt.datetime.now()

    ########## 設定項目 ##########
    RESULT_CSV = "./speed_test_result.csv"
    POLLING_INTERVAL_MINUTE = 1  # [min] # 1分より短くしないでください!

    dt_start = dt.datetime(now.year, now.month, now.day, 15, 00, 00)  # 計測開始時刻
    dt_end = dt.datetime(now.year, now.month, now.day, 21, 00, 00)  # 計測終了時刻
    #############################

    # ファイル初期化
    create_file_if_not_exists(RESULT_CSV)

    st = speedtest.Speedtest()
    current_time = dt.datetime.now()
    prev_time = current_time - dt.timedelta(minutes=POLLING_INTERVAL_MINUTE)

    # 終了時刻になるまで計測を繰りかえす
    while current_time < dt_end:
        current_time = dt.datetime.now()
        if current_time < dt_start:
            print("計測開始時刻前のためスキップします")
            time.sleep(10)
            continue
        if current_time < prev_time + dt.timedelta(minutes=POLLING_INTERVAL_MINUTE):
            print("skip polling")
            time.sleep(10)
            continue

        prev_time = current_time
        measure_network_speed(RESULT_CSV)

    print("現在時刻が計測終了時刻を超えたため、計測終了を終了します")

```
Pythonスクリプトの設定項目は下記の通りです。
  - RESULT_CSV (str): 計測結果書き込み先ファイルパス
  - dt_start (datetime): 計測開始時刻
    dt_end (datetime): 計測終了時刻
    ※ 私の環境では15:00 - 21:00 の6時間としました。

### 注意事項
:::note warn
- テザリングやモバイルルーターなど、通信量に上限がある環境では使用しないでください。（1日で数十GB程度）
- ネットワークに負荷がかかるので、計測期間は最低限にしてください
:::
- speedtest-cliは大量のデータを送受信するようです
    - speedtest-cliの計測値を見ると、1回あたり50MB程度のパケットが送信されているようでした。（出力CSVの`bytes_sent`, `bytes_received`を参照）

:::note warn
今回のPythonスクリプトを使用する場合は、使用者の責任において実行してください。
:::

### 出力
上記のPythonスクリプトを実行すると、下記のようなCSVが出力されました。

![回線速度計測_測定結果CSV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/656bee48-b396-9431-97c6-f70c9fa7b28e.png)

これをSpreadSheetでグラフ化し、上り、下りをグラフ化したものがこちら。縦軸は対数目盛となっています。

![回線速度計測_上り.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/69babce8-b832-c087-ad3d-ed7b3c3a5d10.png)

![回線速度計測_下り.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/ad5fdaca-5ec9-86e3-5a5b-82ba398f7b43.png)

下りのグラフ（青線）が分かりやすいのですが、18時ごろに下記のような特徴がみられました。
- グラフが途切れている（不通）となっている箇所がある
- 回線速度が500kbps以下になっている箇所がある
- これらは数分間持続している

## 結果
計測したところ、18時頃に回線速度が急激に低下している箇所が見られ、肌感覚での速度低下を客観的に証明できるものとなりました。
この測定結果をプロバイダに共有したところ、プロバイダ側でも回線速度を改めてチェックしていただけることになりました。

その後、実家で使用している中継器に問題があるのでは？とプロバイダから指摘を受けました。
元々実家で使用していた中継器をプロバイダ貸し出しの中継器に交換し、その後何かあれば別途相談するという形で終結しました。

プロバイダ貸し出しの中継器に変更した後でも、回線速度が低下する場合がありましたが、通信速度は実用上問題ないレベルまで回復したため、そのまま現在に至るという形になっています。


## 教訓
今回、測定結果をプロバイダに提示することで、平行線になっていた議論が前に進み、プロバイダ側にも調査をいただくことができました。

その中で、何も根拠がない状態で、「～に違いない」といった空中戦を行うことは、自分にも相手にもメリットがないなと感じました。

このことから、相手に問題を相談する際には、下記の2点を明確にすると話がスムーズに進むと感じました。
- 定量的なデータを提示することで、相手の納得感を高める
- 相手に依頼するアクションを提示することで、相手の行動を明確化する

![回線速度計測_関係者_依頼.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/52615474-05e1-b03d-e59c-4ee426f85964.png)

根拠を提示して、相手へのアクションを提示することは仕事にも応用できそうだなと感じました。