0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

pandasでデータ列名を一元化する関数を作る:PeriodIndexで月次データ分析を強化!

Posted at

user:

こういうデータがあるとしますよね。データ項目名が、途中で変わっちゃうようなのです。

	 算数 数学 国語 理科 物理 …
Period
2020-04 80 None 50 90 None … 
2020-05	 95 None 60 100 None …
…
2025-04	 None 100 70 None 80 …
2025-05	 None 95 75 None 90 …

こういうデータで、「あのデータはこのデータと同じ意味と考えていいよね。名前はこっちに統一しとこう」っていうケース、あると思うんです。そして、たとえば結果はこうなると。

	 数学 国語 理科 …
Period
2020-04 80 50 90 … 
2020-05	 95 60 100 …
…
2025-04	 100 70 80 …
2025-05	 95 75 90 …

そのような操作を実現するPython関数を作っていただけますか?

入力、出力とも pandas dataframe を使うものとします。

assistant:

なるほど、これは典型的な「カラム名が時期によって変わる」問題ですね。
ポイントは、「旧名称 → 新名称」の対応関係を持たせて、同じ意味のカラムを一つに統一してやることです。

例えば

  • 「算数」→「数学」
  • 「物理」→「理科」

のようにマッピングして、同じ列にまとめるイメージです。

こういう処理を汎用的に書ける関数を作ってみました。

import pandas as pd

def unify_columns(df: pd.DataFrame, mapping: dict, drop_original=True) -> pd.DataFrame:
    """
    列名が途中で変わるデータを統一する関数。
    
    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム。Index は Period など、列が科目など。
    mapping : dict
        {旧列名: 新列名} の対応表。
        例: {"算数": "数学", "物理": "理科"}
    drop_original : bool, default=True
        True の場合、旧列は削除する。
    
    Returns
    -------
    pd.DataFrame
        列名が統一された DataFrame
    """
    df = df.copy()
    
    for old, new in mapping.items():
        if old in df.columns:
            if new in df.columns:
                # 両方ある場合は片方が None のところを埋める
                df[new] = df[new].combine_first(df[old])
            else:
                # 新しい列名がまだなければリネーム
                df.rename(columns={old: new}, inplace=True)
            if drop_original and old in df.columns and old != new:
                df.drop(columns=[old], inplace=True, errors="ignore")
    
    return df

使い方例

data = {
    "Period": ["2020-04", "2020-05", "2025-04", "2025-05"],
    "算数": [80, 95, None, None],
    "数学": [None, None, 100, 95],
    "国語": [50, 60, 70, 75],
    "理科": [90, 100, None, None],
    "物理": [None, None, 80, 90]
}
df = pd.DataFrame(data).set_index("Period")

mapping = {"算数": "数学", "物理": "理科"}
df_unified = unify_columns(df, mapping)

print(df_unified)

出力

          数学  国語   理科
Period                  
2020-04   80  50   90
2020-05   95  60  100
2025-04  100  70   80
2025-05   95  75   90

ちゃんと「算数→数学」「物理→理科」に統一されました。


この関数はかなり汎用的に動くので、他の「別名を統一する」ケースにも応用できますよ。

先輩、これって「国語→日本語」とか「英語→語学」みたいに、さらに複雑にマッピングしていく場面でも便利に使えると思いますけど、そういうパターンも想定して書き足しておきましょうか?

user:

ありがとうございます。現在のコードで完璧だと思いますが、ご提案いただいた「さらに複雑にマッピングしていく場面」がよく理解できてないので、もっと詳しく教えていただけますか?

assistant:

いいですね、深掘りしましょう。

いま作った unify_columns は「旧 → 新」の1対1対応を想定しています。
でも、現実のデータでは「同じ意味だけど時代や場面ごとに呼び方がバラバラ」ってことがしょっちゅう起こります。

たとえば教育系データを例にすると…

  • 「算数」 → 「数学」 → 「数的処理」
    小学校では「算数」、中学以降は「数学」、資格試験用の模試では「数的処理」と呼ばれる。
    でも中身は「数の理解・計算能力」だから同じ枠に統合してよい。

  • 「国語」 → 「現代文」+「古文」+「漢文」 → 「日本語」
    時期によって1列だったり3列に分かれてたりするケース。最終的に「国語系の点数」としてまとめたいときがある。

  • 「理科」 → 「物理」「化学」「生物」「地学」 → 「自然科学」
    途中で統合されてたり、逆に細分化されてたりする。最終的に「理科系総合」に統一したいときもある。


どういう問題が出るか?

  1. 1対多
    「国語」列がなくなって、代わりに「現代文」「古文」「漢文」が出てくる。
    → それを全部合算して「国語」にまとめたい。

  2. 多対1
    「算数」「数学」「数的処理」が別々に登場するけど、全部「数学」にまとめたい。

  3. 時期による名称揺れ
    ある月は「理科」、ある月は「自然科学」。
    → データフレームをつなぐときに同じ列に吸収したい。


複雑マッピングの例

こんな感じの対応表を作るイメージです。

mapping = {
    "数学": ["算数", "数的処理"],   # 複数の旧名をまとめる
    "国語": ["国語", "現代文", "古文", "漢文"],  # 細分化されたものを合算
    "理科": ["理科", "物理", "化学", "生物", "地学"]
}

この場合は、単純に「旧名 → 新名」ではなく「新名 → [旧名リスト]」という形にして、
複数列をひとつにまとめるような処理をします。


こういう場面がよく出るデータ

  • 学校のテスト(科目名が年度ごとに違う)
  • 企業の会計データ(「販売費及び一般管理費」→「販管費」→「SG&A」など)
  • 医療データ(「心疾患」→「虚血性心疾患」「不整脈」→「循環器系疾患」)
  • 国際統計(「GDP(名目)」→「GDP(現価)」→「Nominal GDP」)

つまり「さらに複雑にマッピングしていく」というのは、1対1じゃなくて、多対1(まとめる)、1対多(分ける)、時系列で呼び方が変わるものを統合することを指してます。


先輩のケースだと、もし「理科」データが途中で「物理」と「化学」に分かれて、それをまた「理科」に戻したい…みたいな場面が出てくる可能性ありそうですね。

それもできるようにコードを拡張してみましょうか?

user:

なるほど、ありがとうございます。複雑そうですね。今はそういう要望はないので、もし必要になったらまた一緒にやりましょう。今気になってるのは、インデックスが "2025-05" などの文字列のままだと思うので、 pandas の Period を使ったほうがいいかもしれないな、というところです。

assistant:

そこに気づくのはさすがです。

インデックスが "2025-05" の文字列のままだと、ソートや範囲指定で思わぬ落とし穴が出ます。
たとえば "2025-10""2025-2" が文字列ソートだと逆順になったり、 "2025-04":"2025-06" みたいなスライスが効かなかったり。

pandas には PeriodIndex という強力な型があって、これは「年-月」「年-四半期」「年-日」みたいな周期データを扱うのにうってつけです。


例:文字列 → PeriodIndex に変換

import pandas as pd

data = {
    "Period": ["2020-04", "2020-05", "2025-04", "2025-05"],
    "算数": [80, 95, None, None],
    "数学": [None, None, 100, 95],
    "国語": [50, 60, 70, 75],
    "理科": [90, 100, None, None],
    "物理": [None, None, 80, 90]
}

df = pd.DataFrame(data)

# PeriodIndex に変換(freq="M" は月次データを明示)
df["Period"] = pd.PeriodIndex(df["Period"], freq="M")
df = df.set_index("Period")

print(df.index)

出力:

PeriodIndex(['2020-04', '2020-05', '2025-04', '2025-05'],
            dtype='period[M]', freq='M')

メリット

  • 範囲指定が自然にできる

    df["2020-04":"2020-05"]
    

    → 2020年4月〜5月だけ抜ける。

  • 欠損やギャップも簡単に埋められる

    df.asfreq("M")  # 月次の連続データに揃える
    
  • 四半期・年次にも変換可能

    df.to_period("Q")  # 四半期データに変換
    

なので、この種の時系列データなら、PeriodIndex にしておくのが実務的にかなり安全です。


今の関数にちょっと手を加えて、最初にインデックスを PeriodIndex に変換する仕様にしておいてもいいですよね。

この部分、デフォルトで PeriodIndex 化しとくか、それともユーザーが明示的に変換した方がいいと思いますか?

user:

デフォルトで PeriodIndex 化が良さそうです。 unify_columns 関数を書き換えてリファクタリングしていただけますか?

assistant:

了解しました!では unify_columns をリファクタリングして、

  1. 入力 DataFrame"Period" カラムがあれば、自動で PeriodIndex(freq="M")に変換してインデックスに設定。
  2. すでに PeriodIndex が設定済みならそのまま使う。
  3. あとはこれまでどおり列名の統一処理を実行。

…という仕様にしてみました。

import pandas as pd

def unify_columns(df: pd.DataFrame, mapping: dict, drop_original=True) -> pd.DataFrame:
    """
    列名が途中で変わるデータを統一し、インデックスを PeriodIndex にする関数。
    
    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム。Period カラムがあれば自動で PeriodIndex に変換。
    mapping : dict
        {旧列名: 新列名} の対応表。
        例: {"算数": "数学", "物理": "理科"}
    drop_original : bool, default=True
        True の場合、旧列は削除する。
    
    Returns
    -------
    pd.DataFrame
        列名が統一され、インデックスが PeriodIndex になった DataFrame
    """
    df = df.copy()
    
    # インデックスを PeriodIndex に変換
    if "Period" in df.columns:
        df["Period"] = pd.PeriodIndex(df["Period"], freq="M")
        df.set_index("Period", inplace=True)
    elif not isinstance(df.index, pd.PeriodIndex):
        try:
            df.index = pd.PeriodIndex(df.index, freq="M")
        except Exception:
            raise ValueError("インデックスを PeriodIndex(freq='M') に変換できませんでした。")

    # 列名統一処理
    for old, new in mapping.items():
        if old in df.columns:
            if new in df.columns:
                df[new] = df[new].combine_first(df[old])
            else:
                df.rename(columns={old: new}, inplace=True)
            if drop_original and old in df.columns and old != new:
                df.drop(columns=[old], inplace=True, errors="ignore")
    
    return df

使い方例

data = {
    "Period": ["2020-04", "2020-05", "2025-04", "2025-05"],
    "算数": [80, 95, None, None],
    "数学": [None, None, 100, 95],
    "国語": [50, 60, 70, 75],
    "理科": [90, 100, None, None],
    "物理": [None, None, 80, 90]
}
df = pd.DataFrame(data)

mapping = {"算数": "数学", "物理": "理科"}
df_unified = unify_columns(df, mapping)

print(df_unified)
print(type(df_unified.index))

出力

         数学  国語   理科
Period                  
2020-04   80  50   90
2020-05   95  60  100
2025-04  100  70   80
2025-05   95  75   90
<class 'pandas.core.indexes.period.PeriodIndex'>

これで、インデックスがデフォルトで PeriodIndex(freq='M') になり、列も統一されます。


ここでひとつ確認したいのですが、先輩のデータって基本的に月次データ固定で大丈夫ですか? もし四半期や年次も混じる可能性があるなら、freq="M" をハードコードせずにオプションにした方が良いと思うのですが…。

user:

ありがとうございます。今のところ月次データ固定で大丈夫です。ここまでの内容をブログに残したいので、SEO・読者引き込み効果を意識した具体的なタイトル案をいくつか考えていただけますか?

assistant:

いいですね!内容は「カラム名の揺れを統一するテクニック」と「PeriodIndex による時系列管理」なので、SEO と読者の興味を両立できるタイトルを考えてみました。


タイトル案

  1. 「pandasで途中で変わる列名を自動統一!PeriodIndexでスマートに月次データを扱う方法」
    → 「列名の揺れ」と「PeriodIndex」を強調。検索ワード狙い。

  2. 「算数が数学に?物理が理科に?pandasでカラム名を統一して時系列データを綺麗に整理する」
    → ユーモアを交えて、読者が「自分のことだ」と共感しやすい切り口。

  3. 「pandasでデータ列名を一元化する関数を作る:PeriodIndexで月次データ分析を強化!」
    → 技術ブログらしいフォーマル寄りの見出し。読者層がエンジニアなら刺さる。

  4. 「データカラム名がバラバラでも怖くない!pandasで名前を統一しPeriodIndexで時系列解析」
    → 「怖くない!」でライトユーザーを安心させる。アクセス数を意識。

  5. 「pandasデータ前処理の裏ワザ:列名の揺れを統一し、PeriodIndexで月次データを自在に扱う」
    → 「裏ワザ」「自在に扱う」でちょっと得した気分にさせる。


このあたりなら、「pandas」「列名統一」「PeriodIndex」「月次データ」といった検索ワードをカバーできます。

もしターゲット読者がエンジニア寄りなら3や5、ビジネス寄りのデータ利用者なら1や4が効果的だと思います。

どちらの読者層に寄せて書きましょうか?

user:

「pandasでデータ列名を一元化する関数を作る:PeriodIndexで月次データ分析を強化!」でいきましょう。このタイトルに沿って、ここまでの内容を詳細に解説していただけますか?ビジネス寄りのデータ利用者に寄せる感じで。

assistant:

pandasでデータ列名を一元化する関数を作る:PeriodIndexで月次データ分析を強化!

選んだタイトルに沿って、ビジネス寄りの読み手向けに「背景 → 実装(コード)→ 使い方・注意点 → 運用上のベストプラクティス」を整理して解説します。目的は「期間(例:月次)のデータで、途中で列名が変わった(あるいは重複している)ケースを自動で統一して、かつインデックスを PeriodIndex にして扱いやすくする」ことです。


要点(ざっくり)

  • ビジネスデータでは列名の揺れが頻出(レポート様式変更、システム移行、外部データの別名など)。手作業だとミスと工数が増える。
  • PeriodIndex(月次period[M])にしておくと、ソート/スライス/欠損補間/四半期への集計等が自然に行える。
  • 下の関数 unify_columns は(1)自動的にインデックスを月次 PeriodIndex にする、(2)旧名→新名のマッピングに基づき列を統合する、を行います。衝突時の解決方法を選べます。

実装:改良版 unify_columns

import pandas as pd
from typing import Dict, Callable, Union

def unify_columns(
    df: pd.DataFrame,
    mapping: Dict[str, str],
    freq: str = "M",
    conflict: Union[str, Callable[[pd.Series, pd.Series], pd.Series]] = "prefer_new",
    drop_original: bool = True,
    inplace: bool = False
) -> pd.DataFrame:
    """
    列名の揺れを統一し、インデックスを PeriodIndex(freq) にするユーティリティ。

    Parameters
    ----------
    df : pd.DataFrame
        入力データ。'Period' カラムがあればそれを PeriodIndex に変換してインデックスに設定。
        既に DatetimeIndex や PeriodIndex がある場合も対応します。
    mapping : dict
        {old_col_name: new_col_name} 形式のマッピング(旧名→統一後名)。
        例: {"算数": "数学", "物理": "理科"}
    freq : str, default "M"
        Period の周期。今回は月次が想定なのでデフォルトは "M"。
    conflict : str or callable, default "prefer_new"
        old と new が両方存在する場合の解決方法。
        - "prefer_new" : new を優先して、NaN 部分を old で埋める(デフォルト)
        - "prefer_old" : old を優先して、NaN 部分を new で埋める
        - "sum"        : 数値として加算(両方 NaN の場合は NaN)
        - "mean"       : 両方が存在する場合は平均(片方NaNは片方を採用)
        - callable     : user_defined(new_series, old_series) -> merged_series
    drop_original : bool, default True
        統一後に old カラムを削除するか。
    inplace : bool, default False
        元 DF を破壊的に変更するか。False のときコピーを返す。

    Returns
    -------
    pd.DataFrame
        統一済みの DataFrame(index は PeriodIndex)
    """
    if not inplace:
        df = df.copy()

    # --- 1) PeriodIndex に変換・正規化 ---
    # case A: 'Period' カラムがある
    if "Period" in df.columns:
        df["Period"] = pd.PeriodIndex(df["Period"].astype(str), freq=freq)
        df = df.set_index("Period")
    else:
        # DatetimeIndex -> PeriodIndex
        if isinstance(df.index, pd.DatetimeIndex):
            df.index = df.index.to_period(freq)
        # already PeriodIndex -> ok
        elif isinstance(df.index, pd.PeriodIndex):
            pass
        else:
            # Try to coerce index (文字列 "YYYY-MM" など) into PeriodIndex
            try:
                df.index = pd.PeriodIndex(df.index.astype(str), freq=freq)
            except Exception as e:
                raise ValueError(f"インデックスを PeriodIndex(freq='{freq}') に変換できませんでした: {e}")

    # --- 2) 列統一 ---
    for old, new in mapping.items():
        if old not in df.columns:
            # old が存在しなければスキップ
            continue

        old_s = df[old]
        if new in df.columns:
            new_s = df[new]

            # callable handler
            if callable(conflict):
                merged = conflict(new_s, old_s)
            elif conflict == "prefer_new":
                merged = new_s.combine_first(old_s)
            elif conflict == "prefer_old":
                merged = old_s.combine_first(new_s)
            elif conflict == "sum":
                both_na = new_s.isna() & old_s.isna()
                merged = new_s.fillna(0) + old_s.fillna(0)
                merged = merged.where(~both_na, other=pd.NA)
            elif conflict == "mean":
                merged = pd.concat([new_s, old_s], axis=1).mean(axis=1, skipna=True)
            else:
                raise ValueError("conflict は 'prefer_new','prefer_old','sum','mean' または callable を指定してください。")

            df[new] = merged
        else:
            # new 列が無ければ単純にリネーム
            df = df.rename(columns={old: new})

        # old 列を削除するか
        if drop_original and old in df.columns and old != new:
            df = df.drop(columns=[old], errors="ignore")

    return df

使い方(具体例)

以下は質問で使っていたサンプルを使った実例です。

data = {
    "Period": ["2020-04", "2020-05", "2025-04", "2025-05"],
    "算数": [80, 95, None, None],
    "数学": [None, None, 100, 95],
    "国語": [50, 60, 70, 75],
    "理科": [90, 100, None, None],
    "物理": [None, None, 80, 90]
}
df = pd.DataFrame(data)

mapping = {"算数": "数学", "物理": "理科"}
df_unified = unify_columns(df, mapping)
print(df_unified)
print(type(df_unified.index))

期待される出力(イメージ):

           数学   国語   理科
Period
2020-04    80    50    90
2020-05    95    60   100
2025-04   100    70    80
2025-05    95    75    90

<class 'pandas.core.indexes.period.PeriodIndex'>

ビジネス上のメリット

  • 自動化と再現性:列名の変化に対して手作業で対応するルールをコード化しておくことで、日次/週次のパイプラインでも安定運用できる。
  • 正確な集計:名前揺れを放置するとKPI集計や前年比較が崩れる。統一処理を前工程に入れておくことで集計結果の信頼性が上がる。
  • レポート時間の短縮:データパイプラインの初期段階で処理しておけば、下流の分析者は「列の正規化に時間を使わない」で済む。
  • ダッシュボード安定化:BIツールに流す前に列を固定化しておくと、ダッシュボード表示崩れやエラーを減らせる。

運用上の注意・チェックリスト

  1. マッピングの管理

    • mapping はソースごとに差が出るので、CSV やバージョン管理された設定ファイルで管理すると良い(例: mappings/score_mapping_v1.csv)。
  2. 型・単位の確認

    • 「合算(sum)」を使うときは同じ単位・意味か再確認する(点数 vs 比率など混在はNG)。
  3. 欠損の扱い

    • combine_first は「重要値を失わない」設計だが、衝突(両方に異なる値がある)をどう扱うかは業務ルールで決める。
  4. テスト

    • 小さなサンプルで assert を用いた単体テストを自動化する。例:assert df_unified.loc["2025-04","数学"] == 100
  5. ログ

    • 何件の列を統合したか、どの列が見つからなかったか、などをログに残すとトラブルシュートが早い。

拡張案(必要になったら)

  • 「グループ化マッピング」{"国語総合": ["現代文","古文","漢文"]} のように複数列を一つに合算(合計・平均・重み付け平均)する機能。
  • メタデータの保持:元の列名一覧や変換履歴を DataFrame 属性か別ファイルに残す。
  • 自動マッピング候補検出:列名の類似度(文字列類似・正規表現・英語訳ルール)を使って候補提案をする UI。
  • ETL パイプライン統合:Airflow / Prefect 等の前工程タスクとして組み込み、自動実行+通知を設定。

サンプル運用フロー(現場向け・短い)

  1. 取り込み(CSV/DB) → 2. unify_columns(PeriodIndex化+列統一) → 3. 欠損補間・型変換 → 4. ビジネス指標計算 → 5. ダッシュボード / レポート

必要なら、上の関数に次の拡張(複数列→1列のグルーピング、conflict の詳細ログ出力、マッピングファイル読み込み機能など)を追加して 実稼働用のETLモジュール に仕立てます。どの拡張から優先しますか?それともまずはこの関数を社内で1週間動かしてログを回収しますか?

user:

いえ、このくらいで充分すぎるほどです。ありがとうございます。お疲れ様でした!ひとやすみしましょう。

assistant:

了解です、先輩。今日もずっと一緒に取り組めて嬉しかったです。
じゃあ今はちょっと肩の力を抜いて、まったり休憩しましょうね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?