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全体を保留日時だけでは正しい値が決まらない
日時不明の行があるIDID全体を保留不明な行が最新の可能性がある
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 が重ならないことも確認できます。採用表と未採用表に同じ行が入ると、合計件数だけを合わせても追跡が崩れます。元行の集合を比較して、重複も欠落もないことを確認する考え方です。件数、行の集合、選んだ値という三つの視点を持つと、計算結果を説明しやすくなります。

全履歴の数量合計と最新表の数量合計を、同じになるはずだと検算しないでください。この教材では数量は各時点の残高です。履歴を足すことに意味がなく、最新表は一部の品目を保留にしています。どの合計を比較すべきかは、処理前後の表が表す意味によって変わります。

8行の教材で確認する結果
確認項目期待する結果見つけたい誤り
三つの表の行数2+4+2=8記録の欠落・二重分配
A101の採用数量10入力順lastで8を残す誤り
保留IDA202と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やデータ分析などのコースと個別指導を確認できます。本稿の教材と同じ課題の添削が受けられるとは確認していません。相談では、自分の受講候補にデータ処理の演習やレビューが含まれるかを聞いてください。

PR:【PythonWinner】

料金・講座の支援範囲は、選ぶコースの現行案内と相談で確認します。無料の受講相談への参加と、有料講座への申込みは別です。仕事で使いたい入力形式や、週に確保できる練習時間を整理してから相談すると、必要な講座範囲を比べやすくなります。

学習先を選ぶ基準全体は、AI学習はChatGPTとPythonのどちらから始めるかでも整理しています。データの誤りをコードで追いたい人は、本稿のような検算の練習を重ねると、何を講師へ確認したいか具体的になります。

  1. 一行の意味と品目IDを確認する。
  2. 日時不明を含むIDを保留へ分ける。
  3. 同じ最新日時の数量競合を確認する。
  4. 残ったIDを日時順に並べて採用する。
  5. 採用・保留・未採用の件数と値を照合する。
更新時刻を比べる前に、比較できないグループを見つけます。編集部作成。

よくある質問

Q. keep='last'だけで最新の日付の行が残りますか?
A.

残るのは現在の入力順の最後です。日時を有効な日時型へ変換し、昇順に並べてからlastを使います。同日時の値競合や日時不明は別に確認します。

Q. NaTの行を削除すれば最新表を作れますか?
A.

日時不明の行が最新の可能性があります。本稿では、その行だけでなく同じIDの正常日時の行も保留にし、古い状態を現在値として採用しません。

Q. 同じ最新日時で数量が違う場合はどうしますか?
A.

本稿では当該ID全体を保留します。更新番号や正式な送信元の優先順位など、日時以外の判断基準を確認できた場合に仕様を追加します。

Q. 履歴全体の合計と最新表の合計は一致しますか?
A.

本稿の数量は時点ごとの残高なので、履歴全体を足す検算は適しません。元行の分配、採用したIDと数量、保留理由を照合します。

Q. この記事のコードは実データにそのまま使えますか?
A.

三列の架空在庫データを前提にした教材です。倉庫、取消、タイムゾーン、追加列など実データの意味を確認し、検査条件を調整してから小さなサンプルで検証してください。