pandasでIDごとの最新行を残すには、日時を変換してから並べ替え、drop_duplicates(subset=ID列, keep='last') を使います。ただし、日時が読めない行や、同じ最新日時なのに値が違う行は、並び順だけで選べません。そのIDを保留に分けてから採用行を決めましょう。
対象は、CSVをDataFrameへ読み込めるようになり、更新履歴から現在の一覧を作りたい社会人です。本稿は「品目ごとの最新在庫」を扱う教材です。履歴を合計したい場合や、同じ伝票に複数の明細がある場合は、そのまま当てはめられません。
仕様と講座情報の確認日:2026年10月10日。pandas公式のdrop_duplicates、sort_values、to_datetime、duplicatedを参照しました。公式文書は3.0.6表示です。以下は編集部が作成した架空データであり、実在の在庫や受講成果ではありません。
最新行を選ぶ前に、IDと日時の意味を決める
keep=’last’だけでは最新行にならない
ファイルの最後にある行が、最後に更新された記録とは限りません。担当者が先月の控えを追加したり、支店ごとのCSVを順番に連結したりすると、古い記録が後ろへ来ます。keep='last' は、その後ろの行を残す指定です。更新日時の列を見て、自動で時間の前後を判断してくれる設定ではありません。
例えば品目A101の数量が、10月8日は8、10月9日は10だったとします。9日の行、8日の行という順で表へ入っているなら、並べ替え前のlastは数量8を残します。処理は正常に終わり、IDの重複もなくなります。それでも「現在の在庫」という目的には合いません。エラーの有無と採用した記録の意味を分けて見ます。
日時を昇順に並べると、同じIDの中では新しい日時が後ろへ来ます。そこでlastを使えば、新しい行を選べます。降順に並べるならfirstを使う組み合わせになります。覚えるべきなのは特定の一行コードではなく、「どちらの端へ最新日時を置いたか」です。並び順と残す側をセットで説明してください。
一方、全列が一致するコピーを一つへまとめる操作は、最新状態の選択とは異なります。日時だけ違う二行は、更新前後の記録として両方必要なことがあります。まず完全なコピーを整理し、その後にIDごとの状態を選ぶ、と目的を段階に分けると、何を失ったか追いやすくなります。
日付がインデックスに入っていても、drop_duplicatesの重複判定にインデックスは含まれません。必要な日時を列として確認し、判定に使う列を明示します。「画面の左側に日時が見えるから、日時も比較されている」と考えないことが大切です。表示の形とAPIの判定対象は一致するとは限りません。
| 方法 | 残る位置 | 必要な前提 |
|---|---|---|
| 並べ替えずkeep='last' | 入力順の最後 | 入力順が更新順と確認できる場合だけ |
| 日時昇順+keep='last' | 日時が新しい側 | 日時が有効で、同時刻の競合がない |
| 日時降順+keep='first' | 日時が新しい側 | 同じく日時と同時刻の確認が必要 |
| 引数なしのdrop_duplicates | 全列が一致する行の先頭 | 完全コピーの整理が目的 |
| keep=False | 重複したグループは残らない | 最新行を一つ残す目的には使わない |
「一つのIDで一行」にしてよい表かを確認する
IDが同じだから同じ物を表す、と決めつけると、必要な行を落としてしまいます。注文IDには商品Aと商品Bが含まれる場合があります。店舗コードが同じでも、棚や保管場所が違う在庫かもしれません。一行が何を表すかを文章にしてから、subset の列を選びます。
本稿の前提は「一つの品目IDにつき、各時点の在庫数量を一行で記録する」です。そのため品目IDを一つ残す単位にできます。複数倉庫の在庫なら、品目IDと倉庫IDの組み合わせが必要になる可能性があります。列を増やせば安全になるのではなく、業務上の単位に合わせる必要があります。
さらに、数量がその時点の残高なのか、その日に入出庫した増減なのかを確認してください。残高10の後に残高12が記録されたなら、現在値として12を選べます。入庫10、出庫2という履歴なら、最後の2だけを残すと情報を失います。同じ「数量」列でも、意味が違えば最新行選択の手順は使えません。
更新日時の定義も一つにします。品目の状態が変わった日時と、ファイルを出力した日時は別です。毎晩同じ台帳を出力しているなら、出力日時の新しさはデータ内容の新しさを示さないことがあります。「更新日時」はどの操作で変わる値か、元のシステムや担当者へ確認します。
取消や無効を示す列があるときは、その列も仕様の一部です。最新行が取消なら、古い有効行へ勝手に戻してよいとは限りません。本稿の三列だけの教材に取消処理を混ぜることはせず、実データへ移す際に追加の状態列を確認する前提にします。見慣れない列を除いてコードを動かすより、意味を確かめてから設計し直します。
取り込み段階の欠損・完全重複を広く調べたい場合は、CSV集計の欠損と重複を残して調べる練習を先に読めます。本稿は、その後に現在の状態を一つ選ぶ工程へ進むための記事です。集計診断と状態の選択を別の工程として残すと、誤りの場所を特定しやすくなります。
日時の変換に失敗したIDを、古い行へ戻さない
文字列の日時には、空欄、形式違い、実在しない日付が混ざることがあります。見た目が年月日でも、そのままの並べ替えは文字の順序です。教材では年月日と時分を同じ形式へそろえ、to_datetime で日時型にしてから使います。別の形式を受け入れる場合は、推測で混ぜず、入力仕様を決め直してください。
errors='coerce' は、読めない日時をNaTへ変える指定です。これを「問題を修復する指定」と捉えると、欠損を気付かず通してしまいます。NaTになった場所は、元の文字列と照合するための目印です。変換後に欠損件数を数え、どのIDが含まれるかを取り出します。
例ではA202に、正常な10月9日の数量20と、日時が「未入力」の数量22があります。未入力の行だけを除き、数量20を最新として採用するのは避けます。数量22の更新が9日より後だった可能性を否定できないためです。日付が不明な行が一つでもあれば、そのIDの記録全体を保留します。
これはデータを捨てる判断ではありません。A202の二行を残して、日時を確認できる担当者へ戻すための分離です。後で元記録から「10月10日」と確かめられたなら、修正内容と確認者を記録し、改めて同じ処理へ入れます。コード内で適当な日付を補うと、採用理由を説明できなくなります。
品目IDが空欄の場合は、どのグループを保留すべきかも分かりません。教材のコードは処理全体を止めます。実務ではIDなし行を別管理する方法もありますが、空欄同士を同じ品目としてまとめる処理は避けてください。似ている商品名からIDを推定することも、この教材の範囲には含めません。
日付の誤変換をもっと詳しく調べたい場合は、pandasの日付変換でNaTが出るときの確認へ進めます。どの日付を採用するかは本稿で、元の文字列が日時へ正しく変換されたかは関連記事で確認する、と役割を分けています。
最新日時が同じで数量が違う場合は、並び順で決めない
A303に10月9日10時の数量30と数量35がある場合、日時だけでは新旧を決められません。CSVの後ろにある方を選んでも、正しさの根拠になりません。ファイルを結合する順番が変われば、採用する値も変わるからです。最新日時にある値が競合したIDを見つけ、確認へ回します。
同じ日時・同じ数量なら、教材の三列では同じ記録として扱えます。A404の数量40が二回届いた例は、一方をコピー行として未採用へ分けます。ただし実データでは、担当者や状態、倉庫などの列も違っているかもしれません。「数量が同じなら完全重複」と一般化せず、期待する列を最初に確かめます。
教材では、まずIDごとの最大日時を求め、その日時にある数量の種類を数えます。二種類以上あれば、当該ID全体を保留します。古い日時で値が違うことは、通常の履歴にも起こるため、この検査とは分けます。最大日時だけの競合を調べることで、過去の正常な変更まで異常扱いしません。
別の基準で決められる現場もあります。たとえば更新番号がシステム側で一意に増えるなら、日時と更新番号の組み合わせを使える可能性があります。優先する送信元が定義されている場合もあります。その基準が正式な仕様であることを確かめ、処理前に文章へ残してからコードへ入れます。
入力行番号を最後の判断材料に足す方法は、処理結果を固定することには役立ちます。しかし、どの値が正しいかを確かめる代わりにはなりません。安定した並べ替えも同様です。同じ入力から同じ結果が出ることと、正しい在庫を選んだことを混同しないでください。
| 状態 | 本稿の判断 | 理由 |
|---|---|---|
| 同じIDで日時が違う | 有効な最新日時を選ぶ | 更新履歴という前提に合う |
| 同じ最新日時・同じ数量 | 一つ採用、コピーは未採用 | 三列の内容が同じ |
| 同じ最新日時・異なる数量 | ID全体を保留 | 日時だけでは正しい値が決まらない |
| 日時不明の行があるID | ID全体を保留 | 不明な行が最新の可能性がある |
| IDが空欄 | 教材では全体を停止 | 同じ品目の範囲を決められない |
元行を追跡し、採用以外の記録も保存する
最新行だけの表を作ると、元の履歴が見えなくなります。「数量8の行はどこへ行ったか」と聞かれたときに答えられるよう、教材では入力順を表す _row を追加します。これは更新順を示す列ではなく、読み込んだ記録を追跡するための番号です。ファイルを読み直すと順が変わる可能性もあります。
出力は、現在値として採用する selected、判断できず保留する held、古い履歴やコピーの not_selected に分けます。未採用という名前にしておくと、元の記録自体が無価値だったと誤解しにくくなります。履歴を確認する用途には、古い記録も必要です。
保留には理由も付けます。「日時不明を含むID」と「最新日時の数量競合」は、担当者に聞く内容が違います。同じIDが両方に当てはまる場合、教材では日時不明の確認を先にします。有効な最新日時を決められない段階で、数量の勝者を選ばないようにするためです。
実務へ移す場合は、元ファイル名、シート名、読込日時なども合わせて保存すると、記録を戻しやすくなります。複数シートからまとめる入口は、pandasでExcelの複数シートを結合する確認手順で扱っています。最新行の選択へ入る前に、対象外の集計表を混ぜていないか確認できます。
保留ファイルの中身には、元データと同じ取扱いが必要です。練習には架空のIDと数量を使い、実在の顧客名や注文履歴を公開のノートへ貼り付けないようにします。自動化の練習結果を見せる場合も、元行を追える仕組みの説明と、実データの共有を分けて考えてください。
架空の8行で採用・保留・未採用を検算する
一括で動く教材コードと、8行の内訳
以下は品目ID、更新日時、数量の三列だけを受け入れる教材です。列の不足・追加・同名列の重複、空欄ID、数量の欠損があれば止めます。追加列が不要と決めつけて捨てないため、実データに状態や倉庫の列がある場合は、その意味を確認してから検査条件を拡張してください。
実行例はPython 3.12.14・pandas 2.2.3で確認しました。ファイルの保存や実データの削除は行いません。入力表を別の変数へコピーし、結果として三つの表を返します。日時仕様はタイムゾーンを持たない同一地域の時刻、形式は YYYY-MM-DD HH:MM という前提です。
import pandas as pd
records = pd.DataFrame([
["A101", "2026-10-09 10:00", 10],
["A101", "2026-10-08 10:00", 8],
["A202", "2026-10-09 10:00", 20],
["A202", "未入力", 22],
["A303", "2026-10-09 10:00", 30],
["A303", "2026-10-09 10:00", 35],
["A404", "2026-10-09 10:00", 40],
["A404", "2026-10-09 10:00", 40],
], columns=["item_id", "updated_at", "quantity"])
def choose_latest(records):
expected = ["item_id", "updated_at", "quantity"]
if records.columns.duplicated().any():
raise ValueError("列名が重複しています")
if set(records.columns) != set(expected):
raise ValueError("必要列の不足、または未確認の追加列があります")
df = records[expected].copy().reset_index(drop=True)
if df["item_id"].isna().any() or df["item_id"].astype(str).str.strip().eq("").any():
raise ValueError("品目IDがありません")
if df["quantity"].isna().any():
raise ValueError("数量がありません")
df["_row"] = range(len(df))
text = df["updated_at"].astype("string")
format_ok = text.str.fullmatch(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}").fillna(False)
df["_time"] = pd.to_datetime(
text.where(format_ok), format="%Y-%m-%d %H:%M", errors="coerce"
)
unknown_ids = set(df.loc[df["_time"].isna(), "item_id"])
good = df.loc[~df["item_id"].isin(unknown_ids)].copy()
at_latest = good["_time"].eq(good.groupby("item_id")["_time"].transform("max"))
kinds = good.loc[at_latest].groupby("item_id")["quantity"].nunique(dropna=False)
conflict_ids = set(kinds[kinds.gt(1)].index)
held_ids = unknown_ids | conflict_ids
held = df.loc[df["item_id"].isin(held_ids)].copy()
held["reason"] = held["item_id"].map(
lambda key: "日時不明を含むID" if key in unknown_ids else "最新日時の数量競合"
)
eligible = df.loc[~df["item_id"].isin(held_ids)]
selected = (eligible.drop_duplicates(subset=expected)
.sort_values(["item_id", "_time"])
.drop_duplicates(subset="item_id", keep="last"))
not_selected = eligible.loc[~eligible["_row"].isin(selected["_row"])]
return selected, held, not_selected
selected, held, not_selected = choose_latest(records)
print(selected[["item_id", "quantity"]].to_string(index=False))
print("採用・保留・未採用:", len(selected), len(held), len(not_selected))
assert len(selected) + len(held) + len(not_selected) == len(records)
assert selected["item_id"].is_unique
assert selected.set_index("item_id")["quantity"].to_dict() == {"A101": 10, "A404": 40}
assert set(held["item_id"]) == {"A202", "A303"}
出力はA101の数量10、A404の数量40です。採用2行、保留4行、未採用2行になります。A202は正常日時の一行も含めて保留です。A303の数量30と35も両方保留です。A101の古い数量8と、A404のコピー一行が未採用へ入り、元の8行が三つの表に分かれます。
この結果は「全品目の現在値が完成した」状態ではありません。四つの品目のうち二つだけを確認できた一覧です。表示や報告では、採用したID数と保留ID数を併記します。保留があることを隠して現在在庫の全体表として渡すと、数量20や35が消えたように見えてしまいます。
行数だけでなく、選ばれた数量と保留IDを照合する
合計行数の式が合うことは、記録がどこかへ分配された確認です。それだけでは、正しい側に入ったか分かりません。教材ではA101の10とA404の40を具体的に照合し、A202とA303が保留に含まれることも調べます。代表的な値の確認を加えると、並べ替えを忘れた誤りを発見できます。
採用表のIDが一意という確認も必要です。同じ品目が二行残っていれば、現在値として集計する際に二重計上するかもしれません。ただし一意であることだけで、粒度の判断が正しかったとはいえません。倉庫別の必要な行を落としても、IDは一意になるためです。最初に決めた表の意味へ戻って判断します。
三つの表で _row が重ならないことも確認できます。採用表と未採用表に同じ行が入ると、合計件数だけを合わせても追跡が崩れます。元行の集合を比較して、重複も欠落もないことを確認する考え方です。件数、行の集合、選んだ値という三つの視点を持つと、計算結果を説明しやすくなります。
全履歴の数量合計と最新表の数量合計を、同じになるはずだと検算しないでください。この教材では数量は各時点の残高です。履歴を足すことに意味がなく、最新表は一部の品目を保留にしています。どの合計を比較すべきかは、処理前後の表が表す意味によって変わります。
| 確認項目 | 期待する結果 | 見つけたい誤り |
|---|---|---|
| 三つの表の行数 | 2+4+2=8 | 記録の欠落・二重分配 |
| A101の採用数量 | 10 | 入力順lastで8を残す誤り |
| 保留ID | A202とA303 | 日時不明や同時刻競合の見逃し |
| 採用IDの重複 | なし | 現在表への複数行混入 |
| A404のコピー | 採用1・未採用1 | コピーの二重計上 |
入力順を変える練習で、隠れた優先順位を見つける
実務では、同じ記録が違う順番で届くことがあります。教材の表を並べ替えても、採用する品目と数量、保留するIDが同じになるか確認します。物理的なコピー行のどちらを採用したかは変わっても、内容が同じなら現在値の判断は同じです。比較する対象を「入力の何行目」だけにしないことがポイントです。
shuffled = records.sample(frac=1, random_state=7).reset_index(drop=True)
s2, h2, n2 = choose_latest(shuffled)
assert s2.set_index("item_id")["quantity"].to_dict() == {"A101": 10, "A404": 40}
assert set(h2["item_id"]) == {"A202", "A303"}
assert (len(s2), len(h2), len(n2)) == (2, 4, 2)
次にA303の二行の順番を入れ替えると、日時だけでlastを選ぶ単純なコードは数量30と35を入れ替えて採用します。本稿の手順はどちらも保留です。この差を一度自分で表示してみると、「動いたコード」に暗黙の判断が入り込んでいることを理解できます。
日時不明のA202を、実在する10月10日の記録へ修正した練習もできます。その場合はA202の数量22が採用候補になります。教材上の修正は学習用ですが、実務では訂正前後の値と根拠を残します。保留が解消されたことと、記録を勝手に埋めたことを区別してください。
別の練習として、A101にもっと古い日時の数量99を末尾へ追加します。採用数量が99へ変わったなら、時間の新しさではなく入力の後ろを選んでいる可能性があります。極端な値を一つ混ぜると、目視でも誤りを追いやすくなります。期待値を先に書いてから実行する順序が役立ちます。
結合や集計へ渡すとき、保留をゼロとして扱わない
最新表を別の台帳へ結合するときは、採用がない品目を数量ゼロへ置き換えないようにします。A202とA303は在庫ゼロではなく、現在値が未確定です。値がない理由を残さず欠損を0へ埋めると、在庫切れと確認待ちを区別できなくなります。見た目を整える前に、状態の意味を決めます。
結合後は品目数と行数を確認します。相手側の品目台帳に同じIDが二行あれば、採用表が二行から三行以上へ増えることがあります。pandasのmergeで行数が増える原因と確認手順では、こうした結合先の重複を別の小さな教材で確認できます。
集計担当者へ渡すなら、「採用2品目・保留2品目」「保留理由の確認先」を添えます。数量の表だけを送って、足りないIDは受け取った側に推測してもらう状態を避けます。保留表に回答期限や確認者を付けることは、コードの一部とは別に、運用の分担として決められます。
また、入力ファイルを上書きして処理を繰り返さないようにします。今回の採用表だけを翌日の入力にすると、保留の元行や古い履歴を失います。元の履歴、処理した条件、三つの出力を別々に残せば、仕様を変えたときにも同じ元データから結果を作り直せます。
最初は8行で一連の確認を終え、その後に行数を増やします。大きい表で処理時間を測る前に、同時刻、日時不明、完全コピー、通常更新という四つのパターンを説明できるようにしてください。規模が増えても、採用の根拠を人へ示す必要がなくなるわけではありません。
自習で進める範囲と、講師へ相談する範囲を分ける
この教材を自習で進めるなら、まずA101の二行だけで並べ替えとlastの組み合わせを確認します。次にA202を足して保留、A303を足して競合、最後にA404のコピーを扱います。最初から全コードを暗記するより、各パターンがなぜ別の出力へ入るかを説明する方が、実データへの応用につながります。
講師へ相談したいのは、関数名よりも判断が曖昧なところです。「倉庫別IDをどう作るか」「取消の最新行を残すか」「タイムゾーンが違う日時をどう比較するか」といった仕様を持参します。どの値を正しいとするかは、pandasだけでは決められないためです。
Pythonを基礎から質問しながら学ぶ選択肢として、WinスクールのPython Winnerがあります。公式の講座案内ではPythonやデータ分析などのコースと個別指導を確認できます。本稿の教材と同じ課題の添削が受けられるとは確認していません。相談では、自分の受講候補にデータ処理の演習やレビューが含まれるかを聞いてください。
料金・講座の支援範囲は、選ぶコースの現行案内と相談で確認します。無料の受講相談への参加と、有料講座への申込みは別です。仕事で使いたい入力形式や、週に確保できる練習時間を整理してから相談すると、必要な講座範囲を比べやすくなります。
学習先を選ぶ基準全体は、AI学習はChatGPTとPythonのどちらから始めるかでも整理しています。データの誤りをコードで追いたい人は、本稿のような検算の練習を重ねると、何を講師へ確認したいか具体的になります。
- 一行の意味と品目IDを確認する。
- 日時不明を含むIDを保留へ分ける。
- 同じ最新日時の数量競合を確認する。
- 残ったIDを日時順に並べて採用する。
- 採用・保留・未採用の件数と値を照合する。
よくある質問
Q. keep='last'だけで最新の日付の行が残りますか?
残るのは現在の入力順の最後です。日時を有効な日時型へ変換し、昇順に並べてからlastを使います。同日時の値競合や日時不明は別に確認します。
Q. NaTの行を削除すれば最新表を作れますか?
日時不明の行が最新の可能性があります。本稿では、その行だけでなく同じIDの正常日時の行も保留にし、古い状態を現在値として採用しません。
Q. 同じ最新日時で数量が違う場合はどうしますか?
本稿では当該ID全体を保留します。更新番号や正式な送信元の優先順位など、日時以外の判断基準を確認できた場合に仕様を追加します。
Q. 履歴全体の合計と最新表の合計は一致しますか?
本稿の数量は時点ごとの残高なので、履歴全体を足す検算は適しません。元行の分配、採用したIDと数量、保留理由を照合します。
Q. この記事のコードは実データにそのまま使えますか?
三列の架空在庫データを前提にした教材です。倉庫、取消、タイムゾーン、追加列など実データの意味を確認し、検査条件を調整してから小さなサンプルで検証してください。