MENU

PythonでCSV列名の揺れを吸収して集計する方法

当ページのリンクには広告(PR)が含まれていることがあります。
PythonでCSV列名の揺れを吸収して集計する方法のアイキャッチ

同じ売上データでも、出力元によって「部門」が「部署」になったり、列名の前後に空白が付いたりします。集計コードで列が見つからないときは、入力の列名を一度表示すると原因を確認できます。

全てを似た名前として推測すると、「売上金額」と「入金額」まで混ざりかねません。文字の正規化と、業務上の別名の対応を分ける方法なら、どこを同じ意味として扱ったかを残せます。

目次

そろえる範囲を決める

この例では、NFKCで全角英数字などの表記を正規化し、stripで前後の空白を除きます。その後、「部署」「department」を「部門」、「amount」を「金額」に対応させます。

NFKCはUnicodeの互換正規化です。日本語の同義語を理解する処理ではありません。社内の略語や違う意味の金額をまとめる判断は、別名表に明示します。また、製品コードなどでは字形の違いに意味がある場合もあるため、この変換を全てのセルへ広げません。

架空のCSVを用意する

次をinput.csvとしてUTF-8で保存します。金額は円の整数という例です。

部署, 金額 
A,100
B,70
A,50

期待する集計はAが150、Bが70です。実データで試す前に、列の順番を変えた場合や、同じ意味の列を二つ入れた場合も確認できます。

列名の衝突を止めてから集計する

次をnormalize_csv.pyとして保存し、input.csvと同じフォルダーで実行します。Python標準ライブラリだけを使います。

import csv
import unicodedata
from collections import defaultdict
from pathlib import Path

aliases = {"部署": "部門", "department": "部門", "amount": "金額"}

def canonical(name):
    name = unicodedata.normalize("NFKC", name).strip()
    return aliases.get(name, name)

totals = defaultdict(int)
with Path("input.csv").open(encoding="utf-8-sig", newline="") as f:
    rows = csv.reader(f)
    original = next(rows, None)
    if original is None:
        raise ValueError("ヘッダーがありません")
    columns = [canonical(name) for name in original]
    if len(columns) != len(set(columns)):
        raise ValueError(f"列名が重複しました: {columns}")
    if set(columns) != {"部門", "金額"}:
        raise ValueError(f"想定外の列構成です: {columns}")
    print(list(zip(original, columns)))
    for line_no, values in enumerate(rows, start=2):
        if len(values) != len(columns):
            raise ValueError(f"{line_no}行目の列数が違います")
        row = dict(zip(columns, values))
        department = row["部門"].strip()
        if not department:
            raise ValueError(f"{line_no}行目の部門が空です")
        try:
            amount = int(row["金額"])
        except ValueError as error:
            raise ValueError(f"{line_no}行目の金額が整数ではありません") from error
        totals[department] += amount

with Path("normalized-total.csv").open("x", encoding="utf-8-sig", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["部門", "金額"])
    writer.writerows(sorted(totals.items()))
print(dict(sorted(totals.items())))

処理前には、元の列名と変換後の列名の対応が表示されます。集計結果は別のCSVへ保存し、同名ファイルが既にあれば上書きせず止まります。

止まった入力を確認する

「部門」と「部署」が両方ある入力は、変換後にどちらも「部門」になります。このコードは片方を勝手に選ばず、重複として止めます。列名を変更するだけの処理でも、衝突を見落とすと値を失うためです。

この例は部門と金額の二列だけを受け付けます。備考列もある業務では、許可する列を仕様として追加し、必要列の不足と追加列を分けて確認する形へ変えます。金額のカンマ、通貨記号、小数も自動で推測せず、入力側の約束に合わせて変換します。

確認用のデータでは、正常入力の合計に加え、列名衝突、想定外の列、列数の不一致が止まることを確かめます。別名表を増やしたときには、以前の列名が別の意味へ変わっていないかも読み直します。

関連する作業

列名がそろった後は、複数ファイルの結合や合計・平均の計算へ進めます。

参考資料

この記事を書いた人

Watanabeのアバター Watanabe サイト運営者

2020年よりブログ開始。
SEOが思いのほか性にあっていたようで現在に至る。
モットーは「勝率の高い選択をする」
AIは活用するが吉、最後は人間が息を吹き込む。
アートと科学を追求し、日々精進。
―――
収益:6~7桁をウゴウゴ。
サイト:ペラサイト~中規模サイトまで運営中。
案件:1000円以上の案件をメインに取組中。
打ち手:ブラックSEO~ホワイトSEOまで
―――

目次