⚙️

DQバリデーションコードをリファクタリング | 上長いないから冗長コードをAIで直してみた

に公開

初めに

現在とあるプロジェクトに参加していますが私の所属はSES企業。
OJTで手取り足取り教えてくれる先輩はもちろんいません。

本来スキルの高い先輩のコードを見て試行錯誤を繰り返すことがエンジニアとしての王道的な成長だと思っていますが、現場の環境的にそれが難しく、どうすれば自分の技術を磨けるのか悩んでいます。

そこで、先輩エンジニアの上手なコードをたくさん学習しているであろうAI先生に相談してみました。

気づきが多くていいやり方だなと感じつつも、「こんなコード見たことないが?」 というものも多く、実務で活躍されている方の意見が欲しいなと改めて感じています。

もし読んでいて「自分ならこう書く」と思う部分があれば、ぜひコメントで教えてもらえると嬉しいです!

学び

  • 基礎的なやり方が悪いわけではなさそう
  • 本番環境では直列処理が一般的
  • DRY, Fail Fastというソフトウェア設計原則を体感できた

AIに聞いたこと

#1

  • やっていること
    • 既存int列で計算後、それを利用してフィルタリング
    • フィルタリング前の計算結果を残したいため新規列を作成し追加
  • 気になっていること
    • 見た目が長くて冗長に見える
    • 同じdfに連続してカラムを追加していてぱっと見何しているかわかりづらい気がする
    • 自分が知らない方法で効率よくできるのではないか
before.py
process_df["A"] = abs(process_df["B"] - process_df["C"])
process_df["D"] = abs(process_df["E"] - process_df["F"])
process_df["G"] = process_df["A"] <= 0.1
process_df["H"] = process_df["B"] <= 0.1
  • 結論
    • 改善案はあるもののパフォーマンスや処理内容自体には問題なさそう
    • sub(), abs()などを使用したほうがfill_valueを設定できNaNに対して堅牢
      • 後続処理でNaNなら○○、という利用をしているからそのままでもよさげ?
    • df.assignを利用して列追加をまとめて行えそう
  • 対処法
    • df.assign利用
    • 今後工程が増えることが予想されるなら計算部分を関数化してdf.pipeを使用
after_simple.py
process_df = process_df.assign(
    A = (process_df["B"] - process_df["C"]).abs(),
    D = (process_df["E"] - process_df["F"]).abs(),
    G = lambda d: d["A"] <= 0.1,
    H = lambda d: d["B"] <= 0.1
)
after_func.py
def add_diff_cols(df):
    df2 = df.copy()
    df2["A"] = (df2["B"] - df2["C"]).abs()
    df2["D"] = (df2["E"] - df2["F"]).abs()
    return df2

def add_flags(df, thr=0.1):
    df2 = df.copy()
    df2["G"] = df2["A"] <= thr
    df2["H"] = df2["B"] <= thr
    return df2

process_df = (
    process_df
    .pipe(add_diff_cols)
    .pipe(add_flags, thr=0.1)
)

#2

  • やっていること
    • カラム変更が正しく行われているか目視で確認するために変更前後でログ出力している
    • 既存データと新規データの兼ね合いで存在するカラムが異なるが、後続処理で使用するカラムは統一したい(変更箇所を最小限にしたい)
    • 中間dfがどのような値を保持しているか見るために中間dfと出力dfを分離
  • 気になっていること
    • 結局目視だから抜け漏れ発生しそう
    • 縦にコードが長く読みづらい
    • そもそも中間dfと出力dfを分ける意味があるのか
before.py
print("======== adjusted_process_df: 調整後 ========")
# 実際はカラム名が長いため横ではなく縦に並べている
display(adjusted_process_df[[
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "J",
    "K"
]])

# 後段処理では L,M を使用しているため、J,K をL,M にコピー
output_df = adjusted_process_df.copy()
output_df["L"] = output_df["J"]
output_df["M"] = output_df["K"]
print("======== output_df: 調整済CSV保存するデータフレーム ========")
display(output_df[[
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "L",
    "M"
]])
  • 結論
    • DRY原則に引っかかるからNG
      • Don't Repeat Yourself 同じ処理を複数個所に書かない
    • 本番環境ならdfを分けると冗長だから直列処理が望ましい
      • 再実行可能、再現性保証する必要性はあり
      • copy()をするとメモリが多く必要になることも考慮必要
      • 直列でもログに出せば今と同等の処理可能
      • 不安ならparquetなどで軽量化して保存も一案
    • 単純な処理ならテスト環境で動かし、問題ないなら本番環境ではログ出力も削除
      • 今回の例では、不安なら列を比較する処理を書けばOK
  • 対処
    • 重複カラムを変数で保持し再利用、変数となってるカラムを追加。
      • 問題発生時に一か所だけいじれば治るようにすることが目的
    • 直列処理に変更
      • 本番環境での実装のため
    • 新しいカラムに既存カラムをコピーする処理だったため、renameで対処
      • fail fastの原則にのっとりカラムがないならエラーを先に吐くようにバリデーション
after.py
base_cols = ["A","B","C","D","E","F","G","H","I",]
rename_map = {"J": "L", "K": "M"}
old_cols = ["J","K"]
new_cols = ["L","M"]
before_cols = base_cols + old_cols
after_cols = base_cols + new_cols

# カラム存在確認。J,Kないならエラー、あるならコピーと同等の処理が走るため目視確認不要
# L,M は上書きされても問題なし
expected = set(rename_map.keys())
actual   = set(adjusted_process_df.columns)
missing  = sorted(expected - actual)
if missing:
    raise KeyError(f"rename したい列が存在しません: missing={missing}")

print("======== adjusted_process_df: before ========")
display(adjusted_process_df[before_cols])
print("======== adjusted_process_df: after ========")
adjusted_process_df = adjusted_process_df.rename(columns=rename_map)
display(adjusted_process_df[after_cols])

感想

AIが発達していて本当によかったと思いました(小並感)。
ただ、本音を言えば、優秀な上司のもとで成長したかったです(小並感)。

プログラミングはまだしも、上流工程やら設計やらをどう独学でキャッチアップすればいいのか...

もしこの記事を読んで、「こうするといいよ」などの指導やアドバイスをいただけたら、とても嬉しいです。

Discussion