Excel の重複行を見つけて確認用に分離する
4列すべての値が完全一致する行を見つけ、元の行番号とともに新しい Excel ファイルへまとめます。最初に出現した行も含め、重複グループごとに確認します。
複数の既知の日付形式を ISO 形式の YYYY-MM-DD に変換し、元の CSV は変更せずに保持します。許可された形式に一致しない値や、実在しない暦日については、別の確認用ファイルに出力します。
この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English
対象読者複数の日付形式が混在した CSV ファイルを受け取り、曖昧な値を自動推測せずに再現可能なクリーンアップ処理を行いたい人向けのガイドです。
日付のクリーンアップは、受け入れる形式を明示すると安全になります。この例では6種類の数値形式を認識し、正常に解析できたすべての値を YYYY-MM-DD に変換します。日付の自動推測は使用しません。
| 受け入れる入力例 | 意味 | Python 形式 |
|---|---|---|
| 2026-09-01 | 年-月-日 | %Y-%m-%d |
| 09/02/2026 | 月/日/年 | %m/%d/%Y |
| 2026/09/03 | 年/月/日 | %Y/%m/%d |
| 04-09-2026 | 日-月-年 | %d-%m-%Y |
| 2026.09.05 | 年.月.日 | %Y.%m.%d |
| 20260906 | 年月日 | %Y%m%d |
スラッシュやハイフンを使った形式の解釈は運用上の方針です。このチュートリアルでは、09/02/2026 は 2026年9月2日、04-09-2026 は 2026年9月4日を意味します。入力元が異なる規則を使用している場合は、実データを処理する前に受け入れる形式を変更してください。
次のデータセットは合成データであり、この記事のために作成したものです。作業フォルダに mixed_dates.csv として保存してください。6種類の異なる受け入れ形式で記録された有効な日付が6件と、失敗するはずの値が2件含まれています。
record_id,event_date,note
R001,2026-09-01,ISO format
R002,09/02/2026,US slash format
R003,2026/09/03,year first with slashes
R004,04-09-2026,day first with hyphens
R005,2026.09.05,dot separated
R006,20260906,compact numeric
R007,2026-02-30,invalid calendar date
R008,Sep 7 2026,unsupported text format
データレコードは8件あります。最初の6件は正常に解析できるはずです。R007 は認識されるパターンを使用していますが、2月30日は実在する日付ではありません。R008 は、受け入れ形式の一覧に意図的に含めていない英語の月名を使用しています。
最初の6つの値は、すべて 2026年9月1日から9月6日までの連続した日付を表しています。したがって、6つの標準化された値が生成されるはずです。残りの2件は、確認用ファイルに元のテキストをそのまま保持します。
| record_id | 元の値 | 想定される標準化値 | 状態 |
|---|---|---|---|
| R001 | 2026-09-01 | 2026-09-01 | PARSED |
| R002 | 09/02/2026 | 2026-09-02 | PARSED |
| R003 | 2026/09/03 | 2026-09-03 | PARSED |
| R004 | 04-09-2026 | 2026-09-04 | PARSED |
| R005 | 2026.09.05 | 2026-09-05 | PARSED |
| R006 | 20260906 | 2026-09-06 | PARSED |
| R007 | 2026-02-30 | UNPARSED | |
| R008 | Sep 7 2026 | UNPARSED |
したがって、想定件数は全8件、解析成功6件、未解析2件です。クリーンアップ後の CSV にも8件すべてが残り、未解析行の standardized_date は空欄、status は UNPARSED になります。
次のスクリプトを date_format_cleanup.py として保存してください。元の CSV を読み込み、完全なクリーンアップ済みコピーを書き出すとともに、日付を標準化できなかったレコードだけを含む小さな確認用 CSV も出力します。
import csv
from datetime import datetime
from pathlib import Path
SOURCE = Path("mixed_dates.csv")
OUTPUT_DIR = Path("outputs") / "date_cleanup_result"
CLEANED = OUTPUT_DIR / "cleaned_dates.csv"
UNPARSED = OUTPUT_DIR / "unparsed_dates.csv"
DATE_COLUMN = "event_date"
FORMATS = (
"%Y-%m-%d",
"%m/%d/%Y",
"%Y/%m/%d",
"%d-%m-%Y",
"%Y.%m.%d",
"%Y%m%d",
)
def parse_date(value: str) -> str | None:
text = value.strip()
for format_string in FORMATS:
try:
parsed = datetime.strptime(text, format_string)
except ValueError:
continue
return parsed.strftime("%Y-%m-%d")
return None
def main() -> None:
if not SOURCE.is_file():
raise FileNotFoundError(f"Source CSV not found: {SOURCE}")
OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir() # Stop rather than reuse an existing result folder.
with SOURCE.open("r", encoding="utf-8-sig", newline="") as source:
reader = csv.DictReader(source)
if reader.fieldnames is None:
raise ValueError("CSV has no header row.")
if DATE_COLUMN not in reader.fieldnames:
raise ValueError(f"Missing required column: {DATE_COLUMN}")
if "standardized_date" in reader.fieldnames or "date_status" in reader.fieldnames:
raise ValueError("Output column name already exists in source CSV.")
cleaned_fields = [*reader.fieldnames, "standardized_date", "date_status"]
review_fields = ["row_number", *reader.fieldnames]
with CLEANED.open("x", encoding="utf-8", newline="") as cleaned_stream, \
UNPARSED.open("x", encoding="utf-8", newline="") as review_stream:
cleaned_writer = csv.DictWriter(cleaned_stream, fieldnames=cleaned_fields)
review_writer = csv.DictWriter(review_stream, fieldnames=review_fields)
cleaned_writer.writeheader()
review_writer.writeheader()
total = 0
parsed_count = 0
unparsed_count = 0
for row_number, row in enumerate(reader, start=2):
total += 1
original_value = row[DATE_COLUMN]
standardized = parse_date(original_value)
output_row = dict(row)
if standardized is None:
output_row["standardized_date"] = ""
output_row["date_status"] = "UNPARSED"
review_writer.writerow({"row_number": row_number, **row})
unparsed_count += 1
else:
output_row["standardized_date"] = standardized
output_row["date_status"] = "PARSED"
parsed_count += 1
cleaned_writer.writerow(output_row)
if parsed_count + unparsed_count != total:
raise RuntimeError("Record counts do not balance.")
print(f"Processed {total} records.")
print(f"Parsed: {parsed_count}; unparsed: {unparsed_count}.")
print(f"Cleaned CSV: {CLEANED.as_posix()}")
print(f"Review CSV: {UNPARSED.as_posix()}")
if __name__ == "__main__":
main()
python date_format_cleanup.pycleaned_dates.csv には、元の3列に standardized_date と date_status を加えた列が含まれるはずです。元の8件すべてのレコードが残ります。失敗した2行も削除されません。
| record_id | event_date | standardized_date | date_status |
|---|---|---|---|
| R001 | 2026-09-01 | 2026-09-01 | PARSED |
| R002 | 09/02/2026 | 2026-09-02 | PARSED |
| R003 | 2026/09/03 | 2026-09-03 | PARSED |
| R004 | 04-09-2026 | 2026-09-04 | PARSED |
| R005 | 2026.09.05 | 2026-09-05 | PARSED |
| R006 | 20260906 | 2026-09-06 | PARSED |
| R007 | 2026-02-30 | UNPARSED | |
| R008 | Sep 7 2026 | UNPARSED |
unparsed_dates.csv には、データ行が正確に2行含まれるはずです。元の CSV ヘッダーが物理的な1行目なので、R007 は CSV の8行目、R008 は CSV の9行目です。
| row_number | record_id | event_date |
|---|---|---|
| 8 | R007 | 2026-02-30 |
| 9 | R008 | Sep 7 2026 |
以下の想定コンソール出力は、合成入力とスクリプトから手作業で導出したものです。実際の実行ログではありません。
Processed 8 records.
Parsed: 6; unparsed: 2.
Cleaned CSV: outputs/date_cleanup_result/cleaned_dates.csv
Review CSV: outputs/date_cleanup_result/unparsed_dates.csv| 症状 | 確認すること |
|---|---|
| FileNotFoundError | SOURCE とターミナルの現在の作業ディレクトリを確認します。 |
| 必須列が見つからない | ヘッダーに event_date が正確に含まれていることを確認するか、DATE_COLUMN を意図的に変更します。 |
| 未解析値が多すぎる | 入力元の規則を確認し、そのデータセットについて文書化され、かつ曖昧でない形式だけを追加します。 |
| 月と日の順序が想定と逆になる | スラッシュまたはハイフン形式の日付が月優先か日優先かを、形式に追加する前に確認します。 |
| FileExistsError | 結果フォルダがすでに存在します。上書きせず、内容を確認して新しい出力先を選択します。 |
| 正しく見える日付でも失敗する | 文字列に空白が含まれている、FORMATS にない形式である、または暦日そのものが無効である可能性があります。 |
失敗率が高いからといって、多数の推測的な形式を追加しないでください。そうすると、検出可能だった不良データが、誤って解釈された日付として静かに通過する可能性があります。
この例は暦日のみを扱います。時刻、タイムゾーン、Excel シリアル日付、ローカライズされた月名、2桁の年、追加の説明テキストを含む値は解析しません。これらの場合は明示的なルールが必要です。
datetime.strptime は暦日を検証するため、2026-02-30 のように形式上は妥当に見える値でも拒否されます。これは有用ですが、正常に解析できた日付が入力値として事実上正しいことを証明するわけではありません。たとえば 2026-09-21 の代わりに 2026-09-12 と入力した誤記でも、有効な暦日として解析されます。
複数の受け入れパターンが同じ文字列を異なる方法で解釈できる場合、FORMATS の順序が重要になります。本番のクリーンアップ処理では、入力元の規則を文書化し、元の値を保持し、失敗を報告し、解析だけに依存せず曖昧な形式を確認する必要があります。
2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · 標準ライブラリ: csv, datetime, pathlib · 未実行
説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。