Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

データ分析: 欠損値補完について2 #5

Closed
victor-von-pooh opened this issue Mar 8, 2024 · 46 comments
Closed

データ分析: 欠損値補完について2 #5

victor-von-pooh opened this issue Mar 8, 2024 · 46 comments

Comments

@victor-von-pooh
Copy link

victor-von-pooh commented Mar 8, 2024

こちらより

欠損値補完の処理として, メタ情報を考慮した補完を行いたい.

2011年1月1日〜2021年12月31日までの11年間のデータを用いて, 以下のメタ情報を付加した重回帰分析によって補完する方法を考えてみる.

  • 月齢データ
    • 黄経差
    • 月齢
    • こよみ
    • 気象庁
    • MIRC
  • 気象データ
    • 降水量
    • 気温

重回帰分析

重回帰分析は, 説明変数 $x_i (i = 1, ..., n)$ と目的変数 $y$ について, 偏回帰係数 $a_i (i = 0, 1, ..., n)$ をモデルの学習によって決定し,

$$y = a_0 + a_1x_1 + \cdots + a_nx_n$$

という回帰式を作ることを目的とする分析である.

復習資料:

  1. 機械学習入門③重回帰分析を学ぼう
  2. tus-os-2023/Data_Analysis
  3. Scikit-Learn, Linear Regression

今回は, 目的変数を時刻 $t$ の潮位に, 説明変数を時刻 $t$ の,

  • 黄経差, 月齢
    • 1日の間は定数とする
    • 次の日の値と差を取り, 24分割する
  • こよみ, 気象庁, MIRC
    • 各ラベルを整数変換する(例えば, 中潮 $\rightarrow$ 3 など )
  • 降水量(mm)
    • --0 に変換する
  • 気温(℃)

の計7個とする.

対応していただくこと

まずは2種類のファイル

  • tide/data/Moon_2011-2021/Mooncal_2011-2021.csv
  • tide/data/Shimonoseki_2011-2021/Shimonoseki.csv

を呼び出し, tide/data/Moji_Tide_2011-2021/csv/9010_2011-2021.csv と上手く時刻毎に結合された DataFrame を作成してみましょう.

@tottocoslowlifer
Copy link
Owner

ありがとうございます.
Moji_analysis.ipynbを更新しましたので,ご確認の程よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

@tottocoslowlifer 確認しました. 非常に良くできていると思いました.
3点だけコメントさせて頂きます.

  • アップロードの際はセルの実行を一度リセットしてから全て実行する
  • 黄経差, 月齢については2つの方法でデータを取得できるようにする
    • 1日の間は定数とする
    • 次の日の値と差を取り, 24分割する
  • 最後のセルにて, 潮位のデータが1時間ずれてしまっている

2つ目については関数を作成するのが良いかなと思います.

3つ目は少し問題があります. 理由としては, 潮位のデータの1日が0〜23時なのに対し, 下関の気象データは1〜24時になっているからです.

以上のご対応をよろしくお願い致します.

@tottocoslowlifer
Copy link
Owner

対応が完了いたしました.
ご確認の程よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

お疲れ様です. ご対応いただきありがとうございます.
大変申し訳ございませんが, 色々と考えた方がいいことがあるみたいなので, このセクションを最初からやり直しましょう.

まず, 前提として関数はセクション冒頭にまとめましょう. この際, 関数間は2行空けます. また, 返り値は基本的には用意しましょう.

やっていただくこと

順を追ってやってみましょう.

  1. 3つの DataFrame を作成する関数を作成する
  2. 門司の DataFrame と月齢の DataFrame を統合する
  3. 2で作ったデータをフラットにし, 気象の DataFrame と合わせる

それぞれでの注意事項を説明します.

3つの DataFrame を作成する関数を作成する

  • 門司の DataFrame
  • 月齢の DataFrame
  • 気象の DataFrame

これらを呼び出す関数を作成します.

門司の DataFrame

6個目のセルの出力と同じものが出てくるような関数を用意しましょう.
基本的に6個目のセルまでにやったことをまとめれば良いので,

def call_moji_tide() -> pd.DataFrame:
    df = pd.read_csv("../data/Moji_Tide_2011-2021/csv/9010_2011-2021.csv").drop("9010(門司の観測値コード)", axis=1)

    date_cols = ["20xx年", "xx月", "xx日"]

    df["年月日"] = [
        datetime.date(
            2000 + int(df["20xx年"][i]),
            int(df["xx月"][i]),
            int(df["xx日"][i])
        )for i in range(len(df))
    ]
    df = df.drop(date_cols, axis=1)

    date_keys = []
    for i in range(len(df)):
        if df["年月日"][i] not in date_keys:
            date_keys.append(df["年月日"][i])

    am_cols = [f"{i}時の潮高値(cm)" for i in range(12)]
    pm_cols = [f"{i}時の潮高値(cm)" for i in range(12,24)]

    new_df_list = []
    for i in range(len(date_keys)):
        tmp = df[df["年月日"] == date_keys[i]].reset_index().drop("index", axis=1)
        am_data = [item for item in tmp[tmp["1(午前)/2(午後)"] == 1][am_cols].values[0]]
        pm_data = [item for item in tmp[tmp["1(午前)/2(午後)"] == 2][am_cols].values[0]]
        new_df_list.append(am_data + pm_data)

    time_cols = am_cols + pm_cols
    df = pd.concat([pd.DataFrame(date_keys, columns=["年月日"]), pd.DataFrame(new_df_list, columns=time_cols)], axis=1)

    date_dropped_df = df.drop("年月日", axis=1)
    index = list(date_dropped_df.dropna(how="all").index)

    return df.iloc[index].reset_index().drop("index", axis=1)

こんな感じで良いと思います.

月齢の DataFrame

こちらは,

  1. df = pd.read_csv("../data/Moon_2011-2021/Mooncal_2011-2021.csv") で生の DataFrame を呼び出す
  2. ["こよみ", "気象庁", "MIRC"] の列について, それぞれのデータを {"小潮": 1, "中潮": 2, "大潮": 3, "若潮": 4, "長潮": 5} のようにラベリングし, 整数値を格納する
    1. ラベリング用の関数を作ると楽かも
      def get_label(df: pd.DataFrame, col: str) -> pd.DataFrame:
          labels_dict = {"小潮": 1, "中潮": 2, "大潮": 3, "若潮": 4, "長潮": 5}
      
          col_list = df[col].to_list()
          labels = [labels_dict[item] for item in col_list]
          df = df.drop(col, axis=1)
          df[col] = labels
      
          return df
  3. ["年月日"]datetime.date の型に直す

という作業をしましょう.

スクリーンショット 2024-03-12 21 04 48

こんな感じの DataFrame ができると良いかと思います.

気象の DataFrame

こちらは,

  1. df = pd.read_csv("../data/Shimonoseki_2011-2021/Shimonoseki.csv").rename(columns={"Unnamed: 0": "年月日時"}) で生の DataFrame を呼び出す
  2. ["年月日時"]datetime.datetime の型に直す
    1. datetime.datedatetime.time を用意し, datetime.datetime.combine() メソッドを使うと簡単
    2. この際, 24時の表記を翌日の0時に直す
    3. datetime.timedelta を使うと楽
  3. ["降水量(mm)", "気温(℃)"] について, float 型で扱えるようにデータそのものを書き換える
    1. -- は0, /// は欠損値, その他はその値(float 型)
    2. 欠損値を float 型として使うには float("nan") とする
    3. その他の中に 17.8) などのエラーが含まれるのでこれをちゃんと通るように処理するのがポイント

という作業をしましょう.
こちらも

スクリーンショット 2024-03-12 21 21 55

こんな感じの DataFrame ができると良いかと思います.

最後に

一つのセルで,

df = call_moji_tide()
moon_df = call_mooncal()
shimonoseki_df = call_shimonoseki()

のように呼び出せると良いと思います.

門司の DataFrame と月齢の DataFrame を統合する

merged_df = pd.merge(df, moon_df, on="年月日", how="inner")

おそらくこの1行で出来ます.

2で作ったデータをフラットにし, 気象の DataFrame と合わせる

ここで, データを一元化する関数を作成しましょう.
引数に how などを入れて, 同じ関数内に if 文の分岐を入れ,

  • 1日の間は定数とする
  • 次の日の値と差を取り, 24分割する

これらを選択できるようにしましょう.

スクリーンショット 2024-03-12 21 29 51

こんな感じの DataFrame ができると良いかと思います.

@victor-von-pooh
Copy link
Author

すみません, 補足です.

次の日の値と差を取り, 24分割する

こちらについて, 最終日(2021-12-31)は欠損値ではなく前日と同じ差分を足して埋めてみましょう.

@tottocoslowlifer
Copy link
Owner

お疲れ様です.先ほど,notebookをコミットいたしました.

最後の

2で作ったデータをフラットにし, 気象の DataFrame と合わせる

に取り組んでいるところなのですが, 30個目のセルのように,meta_df年月日時の型がTimestamp型?になってしまい,行き詰っているところです.

よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

@tottocoslowlifer
ありがとうございます. 迅速な対応で助かります.

全体的によく書けています. 関数の中身も的確です◎

以下コメントになります.

コメント

まずはコードを確認してのコメントになります.

1個目のセル

import datetime as dt とエイリアスをつけることがあるのは知らなかったです.
冗長な書き方が防げて良いですね◎

現在, import numpy as np がどこにも使われていないようなので, この1文はコメントアウトしてしまいましょうか(一応後で出てくるかもしれないため).

24個目のセル

from datetime import timedelta は無くても良いかなと思います.
timedelta を使うのが call_shimonoseki() 関数の上から9行目の1箇所だけなので, ここを

dt_ymd = dt.date(dt_ymd.year, dt_ymd.month, dt_ymd.day) + dt.timedelta(days=1)

とするのが良いと思います.
どうしても必要であれば, 1個目のセルに入れましょう.

call_mooncal() 関数及び call_shimonoseki() 関数は非常に良く書けていて素晴らしいです◎

一方で, full_merge() 関数ですが,

  • call_shimonoseki() 関数の最後からもう1行空ける
  • 引数内の moon_df がどこにも出ていない
  • df も入っているけれど, 上から5行目の1箇所のみしか出てきておらず, ここも merged_df["年月日"] で代用可(なので要らない)
  • そもそも, 門司のデータと月のデータを結合しているのに(27個目のセル), 門司のデータと下関のデータを結合する必要はない?(関数そのものが不要?)

という感じだと思います.
ただし, ここで使われた処理のアイデアは良く, これを応用して merged_df をフラット(1時間毎の DataFrame)にしましょう.

26個目のセル

ここは確認として moon_df を呼び出したのかと思われますが, アップロードのときは無くても大丈夫です.
もしくは, 3個セルを使って dfshimonoseki_df も表示させると良いかなと思います(moon_df のみあるのが不自然という話でした).

29個目以降のセル

これらは今調整中と見ています. 出来次第関数は24個目のセルの中に入れていきましょう.

質問に対するコメント

30個目のセルのように,meta_df年月日時の型がTimestamp型?になってしまい,行き詰っているところです.

こちらに対する回答ですが, Timestamp 型になることが理想でした(なので間違ってはいないです).
ではどのようにすれば良いかということですが, 27個目のセル同様, pd.merge(df_1, df_2, on="年月日時", how="inner") を使ってみましょう.
この df_1 には merged_df をフラットにした DataFrame を, df_2 には shimonoseki_df を入れるという感じで繋げます.
このとき, pd.merge() の2つの引数 onhow はそれぞれ,

  • on : df_1, df_2 に共通するカラムを基準に同じ要素に基づいてマージする, カラム名を指定
  • how : どのような入れ方をするか, "inner" だと共通の要素のみに集約する(積集合), "outer" だと少なくとも片方に入っている要素を全て入れる(和集合)

という役割を担っています.

現在, merged_df["年月日"] にも shimonoseki_df["年月日時"] にも, Timestamp 型で要素が含まれているのでこのカラムを基準にマージできることを目指しましょう.

@tottocoslowlifer
Copy link
Owner

お疲れ様です.
月齢データと門司データを合わせてフラット化したもの(moon_to_flat(...))に対し、最後に気象データ(shimonoseki_df)をマージしようとしているのですが,
スクリーンショット 2024-03-14 14 13 12
というエラーが出てしまいました.

一方で,
スクリーンショット 2024-03-14 14 21 44

ひとつ下のセル(31と書かれたもの)を見る限り,型は揃っているように見えます.

何が原因なのでしょうか.
よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

victor-von-pooh commented Mar 15, 2024

@tottocoslowlifer
ご対応ありがとうございます. 以下コメントになります.

コメント

いくつかポイントがあります.

  • 関数の目的とプログラム内容が一致していない
  • for 文や if 文によるネストが深くなりすぎている
  • 引数にないグローバル変数を使っている
  • flat_merge() 関数と moon_to_flat() 関数を分けない

まず, flat_merge() 関数の引数に shimonoseki_df を使う必要はないと思います. 使い方としては, 関数内1行目〜6行目で shimonoseki_df年月日時 をデータとしてまとめていることかと思われますが,

meta_append = []

for i in range(len(shimonoseki_df)):
    ymd = shimonoseki_df.loc[i,"年月日時"]
    if dt.datetime.date(ymd) in list(df["年月日"]):
        meta_append.append([ymd])

この中にもいくつか注意事項があります.

  1. if dt.datetime.date(ymd) in list(df["年月日"]): の1行の中に df があるのは NG
  2. この部分は,
    meta_append = [[shimonoseki_df.loc[i,"年月日時"]] for i in range(len(shimonoseki_df)) if dt.datetime.date(shimonoseki_df.loc[i,"年月日時"]) in list(merged_df["年月日"])]
    の1行で書ける
  3. この状態の meta_append は要らない

という感じです.

続いて, 一旦 moon_to_flat() 関数の話をします. こちらは手順として flat_merge() 関数と地続きなので, 関数を分ける必要はないと思います. また, how については, 1 or 2 というのはあまり良くないです. 理由としては, それ以外を指定してしまった場合の処理が考慮されていないからです. ここは, 差分を取る場合は "diff" とし, それ以外はそのまま入れるというような感じにしましょう.

ここまでを受けて, 一つの関数 flatten() を作成する手順を提案します.

flatten() 関数作成に向けて

3つに分けて紹介します.

  1. ["年月日", "潮位"] に関して
  2. ["こよみ", "気象庁", "MIRC"] に関して
  3. ["黄経差", "月齢"] に関して

["年月日", "潮位"] に関して

まず, merged_df は,

スクリーンショット 2024-03-15 15 34 25

のようになっているので, "年月日" のカラムと "n時の潮高値(cm)" と書かれているカラムを利用します.

time_cols = [f"{i}時の潮高値(cm)" for i in range(24)]

times = []
data = []

まずこのように初期状態を用意します. ここに, merged_df の長さ分の for 文を使って append していきます.

for i in range(len(merged_df)):
    times += [dt.datetime.combine(merged_df["年月日"][i], dt.time(time)) for time in range(24)]
    data += merged_df[time_cols].iloc[i].to_list()

これで出来上がりです. 以下にまとめてみたので, 一旦どのような DataFrame が出来ているか確認してみましょう.

time_cols = [f"{i}時の潮高値(cm)" for i in range(24)]

times = []
data = []

for i in range(len(merged_df)):
    times += [dt.datetime.combine(merged_df["年月日"][i], dt.time(time)) for time in range(24)]
    data += merged_df[time_cols].iloc[i].to_list()

pd.DataFrame({"年月日時": times, "潮位": data})

スクリーンショット 2024-03-15 15 42 28

このようになるかと思われます.

["こよみ", "気象庁", "MIRC"] に関して

こちらは特に時間毎の値を変更する必要はないので, そのまま引き伸ばすようにしましょう.

label_cols = ["こよみ", "気象庁", "MIRC"]
labels = []

for col in label_cols:
    tmp = []
    for i in range(len(merged_df)):
        tmp += [merged_df[col][i] for _ in range(24)]
    labels.append(tmp)

pd.DataFrame({"こよみ": labels[0], "気象庁": labels[1], "MIRC": labels[2]})

スクリーンショット 2024-03-15 15 45 20

["黄経差", "月齢"] に関して

ここは, how という引数を使って分岐を入れます. まず, 黄経差と月齢のデータリストを表す変数 emd, mp を定義します.

emd = []
mp = []

続いて, how="diff" 以外はデータを引き伸ばすようにするので, if 文の処理を how="diff" の場合に, else 文の中をそのままの場合にします.

how="diff" の場合,

for i in range(len(merged_df)):
    if i == len(merged_df) - 1:
        emd_diff = (merged_df["黄経差"][i] - merged_df["黄経差"][i - 1]) / 24
        mp_diff = (merged_df["月齢"][i] - merged_df["月齢"][i - 1]) / 24

    else:
        emd_diff = (merged_df["黄経差"][i + 1] - merged_df["黄経差"][i]) / 24
        mp_diff = (merged_df["月齢"][i + 1] - merged_df["月齢"][i]) / 24

    emd += [merged_df["黄経差"][i] + emd_diff * j for j in range(24)]
    mp += [merged_df["月齢"][i] + mp_diff * j for j in range(24)]

という処理をします. この for 文内の if 文の分岐では, 最終日については前日との差をそのまま使うようにしています.

それ以外の場合,

for i in range(len(merged_df)):
    emd += [merged_df["黄経差"][i] for _ in range(24)]
    mp += [merged_df["月齢"][i] for _ in range(24)]

で良いと思います. まとめると,

how = "diff"

emd = []
mp = []

if how == "diff":
    for i in range(len(merged_df)):
        if i == len(merged_df) - 1:
            emd_diff = (merged_df["黄経差"][i] - merged_df["黄経差"][i - 1]) / 24
            mp_diff = (merged_df["月齢"][i] - merged_df["月齢"][i - 1]) / 24

        else:
            emd_diff = (merged_df["黄経差"][i + 1] - merged_df["黄経差"][i]) / 24
            mp_diff = (merged_df["月齢"][i + 1] - merged_df["月齢"][i]) / 24

        emd += [merged_df["黄経差"][i] + emd_diff * j for j in range(24)]
        mp += [merged_df["月齢"][i] + mp_diff * j for j in range(24)]

else:
    for i in range(len(merged_df)):
        emd += [merged_df["黄経差"][i] for _ in range(24)]
        mp += [merged_df["月齢"][i] for _ in range(24)]

pd.DataFrame({"黄経差": emd, "月齢": mp})

スクリーンショット 2024-03-15 15 54 45

というようになると思います.

flatten() 関数

今までのをまとめます.

def flatten(df: pd.DataFrame, how: str="same") -> pd.DataFrame:
    time_cols = [f"{i}時の潮高値(cm)" for i in range(24)]
    label_cols = ["こよみ", "気象庁", "MIRC"]

    times = []
    data = []
    labels = []
    emd = []
    mp = []

    for i in range(len(df)):
        times += [dt.datetime.combine(df["年月日"][i], dt.time(time)) for time in range(24)]
        data += df[time_cols].iloc[i].to_list()

    for col in label_cols:
        tmp = []
        for i in range(len(df)):
            tmp += [df[col][i] for _ in range(24)]
        labels.append(tmp)

    if how == "diff":
        for i in range(len(df)):
            if i == len(df) - 1:
                emd_diff = (df["黄経差"][i] - df["黄経差"][i - 1]) / 24
                mp_diff = (df["月齢"][i] - df["月齢"][i - 1]) / 24

            else:
                emd_diff = (df["黄経差"][i + 1] - df["黄経差"][i]) / 24
                mp_diff = (df["月齢"][i + 1] - df["月齢"][i]) / 24

            emd += [df["黄経差"][i] + emd_diff * j for j in range(24)]
            mp += [df["月齢"][i] + mp_diff * j for j in range(24)]

    else:
        for i in range(len(df)):
            emd += [df["黄経差"][i] for _ in range(24)]
            mp += [df["月齢"][i] for _ in range(24)]

    return pd.DataFrame(
        {
            "年月日時": times,
            "潮位": data,
            "黄経差": emd,
            "月齢": mp,
            "こよみ": labels[0],
            "気象庁": labels[1],
            "MIRC": labels[2],
        }
    )

そして, このようにします.

df = call_moji_tide()
moon_df = call_mooncal()
shimonoseki_df = call_shimonoseki()

merged_df = pd.merge(df, moon_df, on="年月日", how="inner")
flattened_df_same = flatten(merged_df)
flattened_df_diff = flatten(merged_df, how="diff")

スクリーンショット 2024-03-15 16 08 50

質問に対するコメント

print(type(merged_df["年月日"][0]))

これを見ると,

<class 'datetime.date'>

となっているようですね. また,

print(type(shimonoseki_df["年月日時"][0]))

を見ると,

<class 'datetime.datetime'>

のようになっています. すなわち, 門司のデータと月齢データをマージした時点では, merged_df["年月日"] のデータ及び shimonoseki_df["年月日時"] のデータの型は <class 'pandas._libs.tslibs.timestamps.Timestamp'> ではなく, datetime のクラスになっているようですね. つまり, Timestamp 型というのは間違いだったようです.

また,

ひとつ下のセル(31と書かれたもの)を見る限り,型は揃っているように見えます.

こちらについては,

shimonoseki_df["年月日時"]

を実行すると,

スクリーンショット 2024-03-15 16 33 26

と出てきており, dtype: object になっています. 一方,

moon_to_flat(2,meta_df,merged_df)["年月日時"]

を実行すると,

スクリーンショット 2024-03-15 16 35 04

なので dtype: datetime64[ns] と気象データとは異なりますね. 型が完全に一致しなかったということですね.

上のコメントを施せばおそらくマージできるようになると思いますが, flatten() 関数を作らない場合はまた別途相談しましょう.

@tottocoslowlifer
Copy link
Owner

ありがとうございます.
コメントアウトを加えた上で再度コミットいたしましたので,ご確認の程よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

victor-von-pooh commented Mar 15, 2024

確認できました. ご対応ありがとうございます.

それでは, flattened_df_same で良いので, shimonoseki_df とマージしてみましょう.

preprocessed_df = pd.merge(flattened_df_same, shimonoseki_df, on="年月日時", how="inner")
preprocessed_df

これを実行し, エラーが出ないかを確認しましょう. これができたら重回帰分析にかけてみましょう.

X: ["黄経差", "月齢", "こよみ", "気象庁", "MIRC", "降水量(mm)", "気温(℃)"]
y: ["潮位"]

この際, ["潮位", "降水量(mm)", "気温(℃)"] には欠損値が含まれていることに注意しましょう.

@tottocoslowlifer
Copy link
Owner

先ほどと同じエラーが出てしまいました.
スクリーンショット 2024-03-15 17 21 35

@victor-von-pooh
Copy link
Author

@tottocoslowlifer
原因を調査してみました. どうやら, call_shimonoseki() 関数内の

for i in range(len(df)):
    time = df.loc[i, "年月日時"]
    dt_ymd = dt.datetime.strptime(time.split("日")[0]+"日", "%Y年%m月%d日")
    dt_time = time.split("日")[1]

    if dt_time == "24時":
        dt_ymd = dt.date(dt_ymd.year, dt_ymd.month, dt_ymd.day) + dt.timedelta(days=1)
        dt_time = "0時"
    dt_time = dt.datetime.strptime(dt_time, "%H時")

    dt_ymd = dt.date(dt_ymd.year, dt_ymd.month, dt_ymd.day)
    dt_time = dt.time(dt_time.hour)
    df.loc[i, "年月日時"] = dt.datetime.combine(dt_ymd, dt_time)

の最後の行で, 直接値を書き換えていたのが良くなかったみたいです. 例えば, for 文の前に

times = []

というのを用意し, 上記最後の行を

times.append(dt.datetime.combine(dt_ymd, dt_time))

のようにして一旦リストとして持っておいて, 後で DataFrame にするというのが良いかなと思います. なるべく call_shimonoseki() 関数の原型を崩さないように, かつフォーマットを揃えるように次のように書き換えてみました.

def call_shimonoseki() -> pd.DataFrame:
    df = pd.read_csv("../data/Shimonoseki_2011-2021/Shimonoseki.csv").rename(columns={"Unnamed: 0": "年月日時"})

    times = []
    for i in range(len(df)):
        time = df.loc[i, "年月日時"]
        dt_ymd = dt.datetime.strptime(time.split("日")[0]+"日", "%Y年%m月%d日")
        dt_time = time.split("日")[1]

        if dt_time == "24時":
            dt_ymd = dt.date(dt_ymd.year, dt_ymd.month, dt_ymd.day) + dt.timedelta(days=1)
            dt_time = "0時"
        dt_time = dt.datetime.strptime(dt_time, "%H時")

        dt_ymd = dt.date(dt_ymd.year, dt_ymd.month, dt_ymd.day)
        dt_time = dt.time(dt_time.hour)
        times.append(dt.datetime.combine(dt_ymd, dt_time))

    rain = []
    temp = []
    item_dict = {"降水量(mm)": rain, "気温(℃)": temp}
    for i in range(len(df)):
        for item in ["降水量(mm)", "気温(℃)"]:
            if df.loc[i, item] == "--":
                item_dict[item].append(float(0))
            elif df.loc[i, item] == "///":
                item_dict[item].append(float("nan"))
            else:
                df.loc[i, item] = "".join(re.findall(r"\d+\.\d+", str(df.loc[i, item])))
                if df.loc[i, item] == "":
                    df.loc[i, item] =- float("nan")
                item_dict[item].append(float(df.loc[i, item]))

    return pd.DataFrame({"年月日時": times, "降水量(mm)": rain, "気温(℃)": temp})

書き方に質問があればお答えします. もし納得いただけたら, 同様にして call_mooncal() 関数も直してみましょう.

これでしっかりとデータの結合が出来るはずです. よろしくお願いいたします.

@tottocoslowlifer
Copy link
Owner

できました!!!

@victor-von-pooh
Copy link
Author

お疲れ様です.

call_mooncal() 関数の中で, ["こよみ", "気象庁", "MIRC"] のラベリングが抜けてしまっているようですので, そちらもご対応よろしくお願い致します.

引き続き, こちらの重回帰分析にかけるところまでをよろしくお願い致します.

@tottocoslowlifer
Copy link
Owner

お疲れ様です.

  1. 訓練データには欠損値を含まない期間のものを使用する
  2. テストデータに関しても,欠損値を含むものは取り除く

という方向性で進めております.
セル31で, 上の2を実装したつもりなのですが,セル32のコメントアウトの箇所を戻すと,以下のようなエラーが出ます.

スクリーンショット 2024-03-19 15 59 23

欠損値を含むデータを取り除ききれていないということなのでしょうか.
よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

ありがとうございます.
いくつかコメントがあります.

コメント

24個目のセル

非常に細かいことで申し訳ありません. 各所の黄経差を表す単語, "longtitude" $\rightarrow$ "longitude" に直しましょう.

25個目のセル

ここは消してしまいましょう.

27, 28個目のセル

このままで大丈夫なのですが, "date" の情報は DataFrame どうしのマージの際に必要だったものなので,

preprocessed_df_diff = pd.merge(flattened_df_diff, shimonoseki_df, on="date", how="inner").drop("date", axis=1)

のようにして削除してしまいましょう.

重回帰分析のセクションについて

こちらは「欠損値補完」の「メタ情報を用いた補完」の中に入っている項目なので, 「#」は4つにしましょう.

29個目のセル

欠損値の確認は大事ですね◎

30個目のセル

ヒストグラムを出していただいていますが, 意図があまりわかりませんでした. ここから相関係数の算出や, PCA(主成分分析)などをして特徴量の重要度を割り出すなどをしないのであれば不要な気もします.

31個目のセル

いただいた質問ですが, なぜ2015〜2016年の欠損値が無いデータで学習する際にエラーが起きるかというと, この期間の中に "rainfall(mm)", temperature(℃) の欠損値があるからです. これらを削除しない限りは上手くいきません. また, 欠損値は落としてから train_test_split() をしましょう.

訓練データには欠損値を含まない期間のものを使用する

こちらですが, 基本的にデータは使えるだけ使いましょう. なぜなら, 今の欠損値補完は時系列データとしてでなく, テーブルデータとして見ているからです. 時間依存が無いので, より多くのデータを用いることができます.

手順としては,

  1. "rainfall(mm)", temperature(℃) の欠損が見られるデータを全て削除する
  2. "tide level" の欠損が見られるデータを全て削除する
  3. X, y を作る
    X = preprocessed_df_same[["longtitude", "moon phase", "calendar", "JMA", "MIRC", "rainfall(mm)", "temperature(℃)"]]
    y = preprocessed_df_same["tide level"]
  4. 重回帰分析にかける
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.50, random_state=8192)
    regressor = LinearRegression().fit(X_train, y_train)

train_test_split()random_state を好きな値で構いませんので設定しましょう(shuffle=False は要らないです).

@tottocoslowlifer
Copy link
Owner

ありがとうございます.
コミットができましたので,ご確認の程よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

victor-von-pooh commented Mar 19, 2024

@tottocoslowlifer
ご対応ありがとうございます. 非常に綺麗なコーディングです◎
2点コメントがあります.

  • 「重回帰分析」のセクションマークダウンで, 「#」を4つにする
  • 30個目のセルの X= の部分に空白を入れる

これを直して次の作業をしていただきます.

作業内容

作ったモデル regressorX_train を使って predict() し, y_train との誤差を算出しましょう. 誤差については平均絶対誤差と平均二乗誤差を使ってみましょう.

また, ここまでを関数化してみましょう. train_predict() などとし, 引数を Xy にして, 関数内で2種類の誤差について print() し, 返り値にモデルを持ってきましょう. すなわち, return regressor とします.

予測値についてはそれぞれ四捨五入で整数値に変換してから誤差を計算しましょう.

@tottocoslowlifer
Copy link
Owner

ありがとうございます.
再度コミットいたしました.

@victor-von-pooh
Copy link
Author

victor-von-pooh commented Mar 19, 2024

@tottocoslowlifer
ありがとうございます. コードが簡潔で素晴らしいです◎
すみません, 追加で少し補正したいことがあります.

  • train_predict() 関数の引数に, split_rate というのを入れ, train_test_split()test_size を自由に変えられるようにする
  • y_train_pred = regressor.predict(X_train) の下に y_train_pred の各要素を整数値に四捨五入する処理を追加する
  • 30個目のセルの最後は, regressor = train_predict(X, y, split_rate) とする

これが終わったら, 今までのことについて質問があればお答えします.

@tottocoslowlifer
Copy link
Owner

完了いたしました.
質問に関しては,その都度,聞かせていただいていたので,今は特にありません.
ありがとうございます.

@victor-von-pooh
Copy link
Author

victor-von-pooh commented Mar 19, 2024

@tottocoslowlifer
ありがとうございます. 最後に1点のみ, 忘れていたことがありましたのでご対応のほどよろしくお願い致します.

  • train_predict() 関数で, train データを使わずに test データにする

次のように変えれば大丈夫です.

def train_predict(X, y, split_rate):
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=split_rate, random_state=316)
    regressor = LinearRegression().fit(X_train, y_train)
    
    y_pred = regressor.predict(X_test)
    for i in range(len(y_pred)):
        y_pred[i] = round(y_pred[i])
    
    mae = np.mean(abs(y_pred - y_test))
    mse = np.mean((y_pred - y_test) ** 2)
    print("MAE train: ", mae)
    print("MSE train: ", mse)

    return regressor

@tottocoslowlifer
Copy link
Owner

ありがとうございます.コミットいたしました.
一点だけ質問なのですが,重回帰分析の前に
標準化を行うべきケースと行わなくてもよいケース
の違いがわからずにいます.

「説明変数の数字のスケールが異なる場合は標準化を行う」という認識でいたのですが,今回の場合なら必要ないのでしょうか.

よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

ご対応ありがとうございました.

一点だけ質問なのですが,重回帰分析の前に
標準化を行うべきケースと行わなくてもよいケース
の違いがわからずにいます.

非常に良い質問だと思います. こちらについては, 「重回帰分析『だから』標準化が要らない」と回答させていただきます. 他の手法, 例えばニューラルネットワークなどであれば, そのスケールの違いから学習が困難になる場合が多いのですが, 重回帰分析では,

$$ y = \boldsymbol{a} \cdot \boldsymbol{X}, \quad \boldsymbol{a} = \left( a_1, a_2, \cdots, a_n \right), \quad \boldsymbol{X} = \left( x_1, x_2, \cdots, x_{n-1}, 1 \right) $$

と表され, 偏回帰係数 $\boldsymbol{a}$ を学習します. このとき, $a_n$ は切片を表します. 仮に $x_i$ についてスケーリングするとしましょう. $std_i$ をこの $x_i$ についての標準偏差, $\bar{x_i}$$x_i$ についての平均とすると, 標準化の式は

$$ standard_i = \frac{x_i - \bar{x_i}}{std_i} $$

となります. ここで, $standard_i$ が新しい $x_i$ の代わりになるのですが, 一方で

$$ x_i = standard_i \times std_i + \bar{x_i} $$

と表せ, $x_i$ の標準偏差及び平均は標本空間内では定数と見做せるため, ここに $a_i$ をかけると

$$ a_i \times x_i = a_i \times \left( standard_i \times std_i + \bar{x_i} \right) = \left( a_i \times std_i \right) \times standard_i + a_i \times \bar{x_i} $$

となり, 式全体への寄与としては, $a_i$ に定数 $std_i$ をかけ, $a_n$$a_i \times \bar{x_i}$ を足すだけになります. つまり, ただ式を線形変換しただけなので, 結果の予測値については影響を及ぼさないということです. これが重回帰分析の強みであり, 今回の欠損値補完のために手法として選んだ理由になります.

@tottocoslowlifer
Copy link
Owner

なるほど,重回帰分析の場合は,標準化の操作がスケールへ何ら影響しないということであっていますでしょうか.

ご回答ありがとうございました.

@victor-von-pooh
Copy link
Author

重回帰分析の場合は,標準化の操作がスケールへ何ら影響しないということであっていますでしょうか.

標準化をすることはデータの前処理としての過程であり, 特徴量のスケールを変更しているので, この考え方は少し違うように感じます. 「標準化をする」 = 「特徴量のスケールを揃える」です. 一方で, 重回帰分析による予測の際は, このスケールの「違い」についての影響を受けないということです.

@tottocoslowlifer
Copy link
Owner

ありがとうございます.
表現が不適切でした.
標準化の操作によりスケールを変更することが,結果に影響しないということでしょうか?

@victor-von-pooh
Copy link
Author

標準化の操作によりスケールを変更することが,結果に影響しないということでしょうか?

そうですね, それは正しいと思います.

@tottocoslowlifer
Copy link
Owner

分かりました.ありがとうございます.

他の事柄に関しては,現時点では質問がないです.
よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

victor-von-pooh commented Mar 19, 2024

ありがとうございます.

それでは次の話に参りたいと思いますが, 今回の重回帰分析の結果を見て「思ったほど良くない」と感じていただけると嬉しいです. では, 本当にこの方法でモデルを作ったことが良くなかったのかを考察してみましょう.

振り返り

まず, 「メタ情報を用いた補完」では, 3種類の DataFrame の情報を用いてそれらを結合し, 不適切なデータを取り除いて, 次のように説明変数と目的変数を設定しました.

X : ["longitude", "moon phase", "calendar", "JMA", "MIRC", "rainfall(mm)", "temperature(℃)"]
y : ["tide level"]

この Xy を重回帰分析で学習させました. しかし, よく考えると説明変数の中には目的変数の値に直接影響させるファクターはなく, 補助的なものが多く含まれているだけといった感じです.

過去の情報を使ってみる

説明変数の中に, 過去(例えば直近10時間)の潮位のデータを特徴量として加えてみるということを考えます. 具体的には, preprocessed_df を作った後, "tide level" を1時間ずつずらしたカラムを用意し(上の例だと10個シフト), 欠損するデータを全て省いてから説明変数を次の n + 7 個にします.

X : ["tide level shift 1h", "tide level shift 2h", ..., "tide level shift nh", "longitude", "moon phase", "calendar", "JMA", "MIRC", "rainfall(mm)", "temperature(℃)"]

作業内容

  • 「#」を4つで新しいセクションを作る(名前は自由)
  • 再度 preprocessed_df_same, preprocessed_df_diff を作る(26, 27個目のセルをまとめて次のようにすればOK)
    preprocessed_df_same = pd.merge(flattened_df_same, shimonoseki_df, on="date", how="inner").drop("date", axis=1)
    preprocessed_df_diff = pd.merge(flattened_df_diff, shimonoseki_df, on="date", how="inner").drop("date", axis=1)
  • "tide level" を基準に n 時間シフトさせて n 個のカラムを生成する関数を作成する(DataFrame の .shift() メソッドが使える)
  • 欠損値を落として train_predict() 関数にかける

追加で1点

先ほどの, train_predict() 関数の print() 部分にある train の文字を削除していただきたいです.

print("MAE: ", mae)
print("MSE: ", mse)

@tottocoslowlifer
Copy link
Owner

対応が完了いたしました.
ご確認の程よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

victor-von-pooh commented Mar 20, 2024

@tottocoslowlifer
ありがとうございます. 以下コメントになります.

コメント

精度も非常に高くなっていて, 処理としては概ね正しいです. 一方で最後のところで, 今まで使っていた降水量等のデータがなくなっているので, コード面でいくつか直していただきたいです. ご対応をよろしくお願い致します.

24個目のセル

tide_shift() 関数について, 以下の変更を加えてみましょう.

  • 引数を tide_shift(df: pd.DataFrame, num: int) とする
  • 1行目で, 代入の操作をしているが, DataFrame は = だとコピー後のものに変更を加えると元の DataFrame にも影響が出るため, ここでは意味をなさない, 代入操作が必要であれば, pre_df = df.copy() とする
  • 関数下の
    for i in range(num):
        pre_df = pre_df.dropna(subset=[f"tide level shift {i+1}h"])
    ここは, せっかく X を用意したので
    pre_df = pre_df.dropna(subset=X)
    とする
  • 返り値は, return pre_df, X とする(リストにする必要はない)

32個目のセル

次のようにしましょう.

preprocessed_df_same_shift, new_cols = tide_shift(preprocessed_df_same, 10)
cols = ["longitude", "moon phase", "calendar", "JMA", "MIRC", "rainfall(mm)", "temperature(℃)"] + new_cols
X = preprocessed_df_same_shift[cols]
y = preprocessed_df_same_shift["tide level"]
regressor = train_predict(X, y, 0.50)

@tottocoslowlifer
Copy link
Owner

夜分に申し訳ありません.
対応が完了いたしました.

@victor-von-pooh
Copy link
Author

@tottocoslowlifer ご対応ありがとうございました.
それではこの issue で最後のステップになりますが, 上で作った regressor を使って, 元の潮位のデータを全て補完してみましょう.

@tottocoslowlifer
Copy link
Owner

お疲れ様です.
方向性を全く理解できていない状態なので,補完の操作の流れを示していただけると幸いです.
申し訳ありません.よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

方向性を全く理解できていない状態なので

こちらは,

  1. 重回帰分析のモデルを使って欠損値補完をするということの意味がわからない
  2. 欠損値補完をするロジックは理解しているが, それをコードに書き起こせない

でいうとどちらでしょうか?

@tottocoslowlifer
Copy link
Owner

1です.

@victor-von-pooh
Copy link
Author

ありがとうございます.

まず, 機械学習というものは, 次のような分析方法です(出典: 機械学習 | 用語解説 | 野村総合研究所(NRI)).

データを分析する方法の1つで, データから「機械」が自動で「学習」し, データの背景にあるルールやパターンを発見する方法. 近年では, 学習した成果に基づいて「予測・判断」することが重視されるようになった.

機械学習や深層学習などの本質は, 学習させること自体ではなく, そこから機械が導いたロジックに基づいて「予測・判断」をすることにあります. 今回の場合, なぜ重回帰分析をしたのかを考えていただきたいです. 重回帰分析は, 教師あり学習の「回帰」問題で扱われる代表的なモデル構造であり, 説明変数から目的変数を予測する機械学習アルゴリズムです. 今までの「メタ情報を用いた補完」では, いくつかの説明変数を用意し, それに対する線形写像である(ことが望ましい)目的変数を予測するということを重回帰分析を用いて行いました.

説明変数:

X(ある日のある時刻における) = [
    "黄経差", "月齢", "こよみ", "気象庁",
    "MIRC", "降水量", "気温",
    "1時間前の潮位", "2時間前の潮位", "3時間前の潮位",
    "4時間前の潮位", "5時間前の潮位", "6時間前の潮位",
    "7時間前の潮位", "8時間前の潮位", "9時間前の潮位", "10時間前の潮位",
]

目的変数:

y(ある日のある時刻における) = ["潮位"]

さて, これである日のある時刻の説明変数が揃っている場合, これらの情報を基にその時刻の潮位を機械が教えてくれるようになりました. これが現在の状況です.

今の notebook の一番最後のセルの一番最後の regressor という変数は, 平均絶対誤差が 2.80, 平均二乗誤差が 13.11 でこの潮位を予測できるモデルとなっています. すなわち, 説明変数分のデータがあれば欠損された潮位のデータを, 本来の値から3前後だけしかズレないで予測してくれるということです. 元の潮位のデータは28個目のセルより97個欠損していることが分かります. これら97個の時刻における説明変数のデータを用いてこのモデルにかけることで, 欠損されたデータを補うことができるということになります.

コーディングに移る前に, 今一度やってきたことでわからないことが無いかを確認いただき, 上記の説明に納得していただきたく存じます.

@tottocoslowlifer
Copy link
Owner

ありがとうございます.

ある日のある時刻の説明変数が揃っている場合, これらの情報を基にその時刻の潮位を機械が教えてくれる

とあるのですが,欠損値を含むデータにregressorを用いる際,

例えば $y$ にあたる潮位データが欠けていて, さらにその行の説明変数にあたるデータも欠損値を含む
というケースはないのでしょうか.(1時間前の潮位もnanであるケース,降水量もnanであるケースなど)

それとも,処理の上で

pre_df = pre_df.dropna(subset=["rainfall(mm)", "temperature(℃)"])
pre_df = pre_df.dropna(subset=X)

としてこれらのケースを除くのでしょうか.

よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

ありがとうございます. 良いご質問です.

結論から言うと, それはご自身で確認いただきたいところです. 潮位のデータとその他のデータが同時に欠損している場合が無いかを調べ, 無ければそのまま regressor に入れる, ある場合はもう一度重回帰分析のモデルを作り直すのが良いでしょう. 今回の「欠損値補完」のセクションでは, 「統計的情報を用いた補完」を含めて欠損値を補完することが目的です.

@tottocoslowlifer
Copy link
Owner

お疲れ様です.
調べてみた結果,tide levelのデータが連続して欠損している箇所が複数,ありました.

そのため,これらの箇所周辺で"tide level shift nh"が欠損値ばかりになり,説明変数として機能しないような気がします.

重回帰分析モデルの説明変数を再度,見直すべきでしょうか.

@victor-von-pooh
Copy link
Author

潮位のデータのみが連続して欠損している場合は, 一つずつ補ってずらすというように再帰的にモデルに入れてみると良いと思います. モデルの説明変数を見直す必要はありません.

@tottocoslowlifer
Copy link
Owner

ありがとうございます.
取り組んでみたのですが行き詰まってしまいました.
スクリーンショット 2024-03-22 23 43 44
スクリーンショット 2024-03-22 23 43 59

お手数ですが,ZOOMミーティングを設定させていただけたら幸いです.
よろしくお願いいたします.

@victor-von-pooh
Copy link
Author

承知致しました. それではまたカレンダー内の「空」の部分に予定を入れておいてください. Zoom の情報は以下の通りになります.

https://tus-ac-jp.zoom.us/j/92156768099?pwd=VU1MOFdiNFVkdzhBZlNTVXRRWnVpUT09

ミーティング ID: 921 5676 8099
パスコード: 302739

よろしくお願い致します.

@victor-von-pooh
Copy link
Author

欠損値の補完が完了したため close とします. お疲れ様でした.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants