pd.concat([left, right], axis=1)で横に結合すると、行は上からの順番ではなく、インデックスのラベルで対応します。 ずれたときは、同じ行番号が同じ人・同じ明細を意味するかを先に確認してください。ignore_index=Trueだけで行の対応は直りません。
対象は、CSVやExcelをDataFrameに読み込めるようになり、別表の列を横へ追加したい人です。この記事は、受講者一覧と確認テストの結果を使った架空の教材です。実在の成績処理や受講者の評価は行いません。「3行になったから直った」と判断しないための練習にします。
一次資料の確認日:2026年10月19日。pandas公式concat、reset_index、set_index、reindexを参照しました。公式文書は3.0.6表示です。以下のコードはPython 3.12.14・pandas 2.2.3で実行し、本文の期待値と照合しています。現行版のすべての条件を実測したという意味ではありません。
横結合のずれを、行の意味から切り分ける
axis=1では、インデックスが行の対応表になる
DataFrameの左端に見える10、20、30などは、必ずしも受講IDではありません。CSVを読んだときに付いた番号、抽出前の位置、以前の処理で指定したラベルということもあります。まず、left.indexとright.indexを別々に表示し、その番号が何を表すかを説明できる状態にします。
今回の左表はインデックスが10、20、30、右表は0、1、2です。どちらも3行ありますが、共通のラベルはありません。このまま横に連結すると、左だけの3行と右だけの3行が並ぶため、結果は6行になります。空欄が増えたのは、同じラベルの相手がいないことを示す手掛かりです。
Excelの画面では、二つの表の一行目が同じ人に見えるかもしれません。しかし、一方だけを得点順に並べ替えたり、未受験者を除外したりすると、その前提は崩れます。行の位置は、並べ替えや絞り込みで変わりうる情報です。横に置く前に、位置以外の根拠がないかを探してください。
axis=0は下へ積む方向、axis=1は列を横に並べる方向です。方向を正しく選んでも、対応の基準まで正しく選べたことにはなりません。上下の連結をしたい場合は、Excelの複数シートと列名を確認する手順の対象です。本稿は左右の行の対応へ絞ります。
症状を記録するときは「NaNが出る」だけでなく、「左3行・右3行から6行になり、左側の列と右側の列に分かれて欠損が出る」と書きます。その記録があれば、元から値が欠けている問題と、連結時に相手が見つからない問題を分けやすくなります。
ignore_indexは、横結合の行番号を捨てる指定ではない
ignore_index=Trueは、連結する方向のラベルを連番にします。横方向のaxis=1なら、対象は列のラベルです。行のインデックスを無視して一行目同士をつなぐ指定ではありません。この点を取り違えると、列名が0、1、2などへ変わったのに、行のずれは残る結果になります。
縦に連結するときにignore_index=Trueで行番号が振り直される経験があると、同じ指定が横でも行へ効くように感じがちです。覚える対象を「行番号を消す便利な引数」から、「連結する軸のラベルを付け直す引数」へ変えると、向きを変えたときも考え直せます。
今回の教材では、左と右をそのまま連結した結果も、横結合にignore_indexを付けた結果も6行です。後者では列名の情報が失われます。列名まで消えると、受講IDなのか得点なのかを後から読み取りにくくなるため、原因が分かる前に引数を追加する使い方は避けたいところです。
列名を変えたい要件と、行の対応を直したい要件は分けてメモします。「列名が重複して扱いにくい」「行が別人に対応している」は異なる問題です。二つを同時に直すより、まずIDの対応を決め、その後で列の命名をそろえたほうが、どの変更が何を直したかを追えます。
エラー文が出ずに表ができた場合も、期待したIDの組を確かめます。pandasは、実務上の「同じ受講者」という意味まで自動で判断してくれるわけではありません。ラベルに基づいて整列できたことと、資料の意味に合っていることを、人が別に確認する必要があります。
reset_indexで3行へ戻っても、別人の結果を付けない
左右それぞれにreset_index(drop=True)を使うと、行のラベルを0、1、2へそろえられます。今回も横結合の結果は3行になります。しかし、左の並びはS01、S02、S03、右はS03、S01、S04です。同じ位置をつなぐと、S01の横へS03の80点が付いてしまいます。
「行数が元に戻った」「欠損が消えた」という二つの見た目は、どちらも成立します。それでも対応は誤りです。この反例があるため、リセットを一般的な解決方法として先に適用しません。両方の表で同じ位置が同じ記録を指すと確認できた場合にだけ、位置に基づく連結を選びます。
位置を使える例は、同じ元表から行を落とさずに二つの列群へ分け、片方だけを並べ替えていない場合です。その場合も、元の行番号や識別子を残して対応を照合できます。反対に、別々に集めたCSV、回答の届いた順の表、片側だけ抽出した表では、位置を信頼する理由が弱くなります。
drop=Trueは元のインデックスを列として残さない指定です。元の番号が資料の出所を追う手掛かりなら、消してよいかを先に考えます。不要な採番と、利用者を識別する業務上のIDを同じ扱いにしないでください。元データのコピーを残してから、小さな例で結果を見ると説明しやすくなります。
読み込み時点でIDの先頭ゼロが失われているなら、順番の問題を直してもIDの対応は直りません。先頭ゼロと文字化けを分けるCSVの確認を先に進めます。IDの意味と型を確かめる作業は、結合の引数を選ぶ前の準備です。
同じIDを使う前に、空欄と重複を確かめる
受講IDを行のラベルへ移せば、それだけで何でも解決するわけではありません。IDが欠けた行、同じIDが複数回出る行があると、一対一で横へ並べるという今回の前提から外れます。まず、どちらの表でもIDが空でないことと、一つずつであることを確認します。
同じ人が複数回テストを受けているなら、重複は入力ミスとは限りません。受講IDと受験回、受講IDと実施日など、どの組が一件を表すかを決める必要があります。今回は一人につき一回の確認結果という教材条件に固定します。複数回の成績から一つを選ぶ処理は含めません。
重複を見つけて先頭行だけ残すと、偶然古い結果を選ぶ可能性があります。最新を採用する要件がある場合は、日付が最新の行を残すときの同日時・欠損確認を参考に、選ぶ条件を別に設計します。行を消すために条件を後付けしないことが大切です。
IDの前後の空白も、見た目では分かりにくい違いです。ただし、何でもtrimすればよいとは決めません。管理上、空白が入力の誤りと確認できるか、コードの意味が変わらないかを確かめます。教材はこの点を混ぜないため、空白のない文字列IDだけを用意しています。
ここで止める条件を決めておくと、後の連結結果へ不明な値を持ち込みにくくなります。「重複が出たら処理を保留」「欠損IDを別一覧に出す」など、異常を見つけた後にどうするかまでが確認手順です。エラーを消すためだけにIDを連番へ置き換えると、元の対応を失います。
outerとinnerは、残すIDの範囲を変える
IDで行をそろえた場合、外側を残すjoin="outer"では、いずれかの表にあるIDが残ります。今回ならS01、S02、S03、S04の4種類です。片側にしかないIDも見えるため、照合漏れや、対象一覧にない結果を見つける入口になります。
join="inner"では両側に共通するIDだけが残り、今回ならS01とS03の2行です。欠損のない表になっても、S02とS04について確認しなくてよくなったわけではありません。「両方にある人だけを調べる」目的なら適していますが、一覧の全員へ結果を付ける目的とは異なります。
NaNを見せないことを目標にinnerへ変えると、調査すべき人を表から隠すことがあります。先に「左表の3人を残すのか」「両方にいる人を残すのか」「片側だけも確認するのか」を文章で決めましょう。引数は、その文章に合わせて選ぶ手段です。
| 方法 | 行の対応 | 今回の見え方 | 選ぶ前の条件 |
|---|---|---|---|
| そのままaxis=1 | 元の行index | 6行へ分離 | 元のindexが共通の意味を持つか |
| 横結合にignore_index | 元の行indexは残る | 6行のまま列名が連番 | 列名を振り直す要件があるか |
| 両側reset_index | 上からの位置 | 3行でも別IDが対応 | 位置の一致を証明できるか |
| IDをindexにしてouter | 受講ID | 4IDと片側の欠損 | IDが欠損なし・一意か |
| IDをindexにしてinner | 共通の受講ID | S01とS03の2行 | 片側だけのIDを除く目的か |
この表は処理時間や精度の順位ではありません。同じ6行の架空入力から何が残るかを比較しています。速度の計測は行っておらず、データ量が増えた場合の性能もここからは判断できません。
二つの表をID順に並べ替えるだけでも、対応が直るとは限りません。今回の右表をS01、S03、S04の順にすると、左のS01、S02、S03とは二行目から異なります。両側を昇順にしたという条件は、両側に同じIDがそろっているという条件ではありません。並べ替えは見やすくする処理と考え、対応の照合を省かないでください。
見出しにも注意が必要です。元の二表にはどちらも受講ID列があるため、そのまま横へ置いた表は、同じ列名を二つ持ちます。列名の見た目だけでは左右を区別できません。教材のpositionでは左ID・右IDへ分け、どちらのIDが同じ行へ来たかを確認できるようにしています。確認用の表では、同じ意味に見える列も出所を明示すると原因を追いやすくなります。
修正案をレビューする人には、「行数3になりました」だけでなく、「S01と70、S03と80が対応し、S02の結果とS04の所属は未確認として残しました」と伝えられます。これなら、実行画面を開かなくても期待する対応を読み取れます。元の一覧にないIDがあることを、修正に失敗した情報として隠す必要はありません。
架空の二表を実行し、件数とIDの対応を照合する
左3行・右3行で、誤対応が見える教材を用意する
左表は受講者一覧、右表は確認テストの結果です。左にあるS02は今回の結果表にありません。右にあるS04は今回の受講者一覧にありません。これは退会や欠席などの理由を表す実データではなく、片側だけのIDを残して確認するための設定です。
得点がないことを0点と解釈しないでください。未受験なのか、入力が遅れているのか、別のIDで登録されたのかは、この二表だけでは分かりません。本稿の正解は「結果未確認と記録すること」であり、受講者の状態や成績を推定することではありません。
次のコードは、ネットワーク通信、ファイルの読み書き、実在する表の更新を行いません。メモリー内に二つのDataFrameを作るだけです。最初のブロックを実行してから、次のブロックへ進みます。期待値を先に書いているので、結果が違う場合は後ろの処理へ進まず照合できます。
import pandas as pd
left = pd.DataFrame({
"受講ID": ["S01", "S02", "S03"],
"講座": ["Python", "Python", "Dify"],
}, index=[10, 20, 30])
right = pd.DataFrame({
"受講ID": ["S03", "S01", "S04"],
"確認点": [80, 70, 90],
}, index=[0, 1, 2])
raw = pd.concat([left, right], axis=1)
ignored = pd.concat([left, right], axis=1, ignore_index=True)
position = pd.concat([
left.reset_index(drop=True).rename(columns={"受講ID": "左ID"}),
right.reset_index(drop=True).rename(columns={"受講ID": "右ID"}),
], axis=1)
assert len(raw) == 6
assert len(ignored) == 6
assert ignored.columns.tolist() == [0, 1, 2, 3]
assert len(position) == 3
assert position.loc[0, "左ID"] == "S01"
assert position.loc[0, "右ID"] == "S03"
print(position[["左ID", "右ID", "確認点"]].to_string(index=False))
表示される組は、S01とS03と80、S02とS01と70、S03とS04と90です。三つとも意図した同一IDの組ではありません。表の高さをそろえる修正が、内容の修正にならないことを、この小さな出力で確認します。
受講IDをindexに移し、片側のIDを残す
次は元のleftとrightを使い、受講IDを行のラベルへ移します。先ほど作ったpositionからIDを消して修正するのではありません。誤った結合結果を出発点にせず、元の二表の対応をやり直す流れです。途中の表の名前を分けておくと、どの段階を見ているかが分かります。
verify_integrity=Trueをset_indexへ指定し、今回のIDが重複していないことを確認します。加えて欠損も先に確認します。concatの同名の引数は連結する軸を検査するため、横結合の列ラベルの検査と混同しません。どの関数で、どのラベルを検査するかまで読みます。
assert left["受講ID"].notna().all()
assert right["受講ID"].notna().all()
labeled_left = left.set_index("受講ID", verify_integrity=True)
labeled_right = right.set_index("受講ID", verify_integrity=True)
aligned = pd.concat([labeled_left, labeled_right], axis=1, join="outer")
left_only = labeled_left.index.difference(labeled_right.index).tolist()
right_only = labeled_right.index.difference(labeled_left.index).tolist()
for_left = aligned.reindex(labeled_left.index)
common = pd.concat([labeled_left, labeled_right], axis=1, join="inner")
assert set(aligned.index) == {"S01", "S02", "S03", "S04"}
assert aligned.loc["S01", "確認点"] == 70
assert aligned.loc["S03", "確認点"] == 80
assert pd.isna(aligned.loc["S02", "確認点"])
assert pd.isna(aligned.loc["S04", "講座"])
assert left_only == ["S02"] and right_only == ["S04"]
assert for_left.index.tolist() == ["S01", "S02", "S03"]
assert set(common.index) == {"S01", "S03"}
print(aligned.to_string())
print("左だけ:", left_only, "右だけ:", right_only)
S01の70とS03の80が対応していれば、最初に決めた期待値と一致します。S04の講座が欠損になるのも教材では想定どおりです。欠損が一つでも出たら失敗と決めず、どのIDの、どの側の値が見つからないかまで読み取ります。
左の一覧へ戻す前に、落ちるIDを別に報告する
for_leftは、左にあったS01、S02、S03の順番で見たい場合の表です。reindexで左側のIDへそろえる前に、right_onlyのS04を記録しています。左の一覧から外れる情報を黙って捨てないためです。右だけの記録が不要かどうかは、元資料を作った人へ確認する材料になります。
left_onlyのS02も同時に残します。こちらは左の一覧にいるのに確認点が見つからない記録です。「左にいないS04」と「結果がないS02」は、どちらも未一致でも、問い合わせる資料が異なります。欠損の数だけ一つにまとめるより、向きの分かる二つのリストにしたほうが次の行動を選べます。
業務で使うときの確認メモには、対象一覧の日付、結果表の日付、両側の件数、共通ID数、片側だけのID数を残します。今回は左3、右3、共通2、左だけ1、右だけ1です。元のIDが一意という前提のもとで、共通2と左だけ1を足すと左3へ戻ることも照合できます。
| 確認項目 | 期待値 | 値が違ったときに見る場所 |
|---|---|---|
| 元の二表 | 左3行・右3行 | 入力した6行と抽出条件 |
| 共通ID | S01・S03の2種類 | IDの型・空白・元資料の対象 |
| 左だけ | S02 | 結果表に載っていない理由を確認 |
| 右だけ | S04 | 受講者一覧の対象と版を確認 |
| 得点の対応 | S01=70、S03=80 | 行数ではなくIDごとの値 |
より多いIDを対象一覧と照合する場合は、isinで型と対象漏れを確認する教材も参考になります。表を作れたあとに何が抜けたかを調べるという考え方は共通ですが、今回の主題は横結合で同じ行へ置く根拠です。
ID列で結びたい処理と、列を並べる処理を選び分ける
元表のID列を指定して関係を結びたい場合には、mergeも選択肢になります。concatが間違いでmergeが正解という一律の関係ではありません。すでに共通のインデックスでそろった列群を並べるのか、ID列の値に基づいて表同士を結びたいのかで、書き方を選びます。
mergeでは、キーの重複によって行数が増える場面を別に確認できます。mergeで重複キーと結合漏れを調べる記事では、そちらの問題を扱っています。本稿のIDを一意に固定した例だけから、多対多の表まで同じ方法で安全に処理できるとは考えないでください。
同じIDの二つの値を比べたいだけなら、結合後の検査の目的も変わります。片方が新しい版で、値が変わった箇所を探すなら、compareのインデックスとIDのそろえ方を使って、差分と不一致IDを分けて確認できます。目的が違えば、欲しい出力も異なります。
今回の二つのコードは、誤対応と修正後の対応を比較する教材です。実データの名寄せ、表記ゆれの自動補完、成績の採用条件、出欠判定は実装していません。IDがない表を名前だけでつなぐときの問題も含めず、共有する識別子がある前提に限定しています。
練習を広げるなら、右表の順番だけを変えて、IDでそろえた結果の対応が変わらないかを確かめます。次に右へ同じIDを追加し、どの検査で止まるかを見ます。一度に型、順番、ID、件数を変えるより、一条件ずつ変えたほうが、変更と結果の関係を説明しやすくなります。
質問するなら、行番号とIDを分けた小さな表を持っていく
学習の到達点は、axisの数字を覚えるだけではありません。なぜ元の横結合は6行になるか、なぜリセット後は3行でも誤りか、S02とS04をどこへ記録するかを説明できることです。コードの見た目ではなく、対応の根拠を説明できれば、別の資料にも考え方を移せます。
相談するときは、実在の個人情報を渡す前に、今回のようにIDと数値だけを変えた小さな表を作ります。「期待するS01の値は70」「実際は80になった」「左右のindexはこれ」と示せると、質問の対象が具体的になります。資料全体を丸ごと共有しなくても、対応のずれは再現できることがあります。
独学で原因を説明できるなら、別の小さな表で確認を続ける方法があります。前提を見落として何度も同じ修正を繰り返すなら、質問支援の範囲が合う講座も比較材料になります。Python Winnerの講座と申込前の確認点では、講座選びの入口を整理しています。
2026年10月19日に確認した公式Pythonマスターは税込297,000円、3ヶ月、30分のマンツーマンレッスン18回です。文字列処理、CSV、データテーブルなどの範囲があります。ただし、このconcat教材や特定の業務表を添削してもらえるか、希望コースの総額と支援期限は申込前に確認してください。
よくある質問
Q. axis=1にignore_index=Trueを付ければ行のずれは直りますか?
横結合では列ラベルを連番にする指定です。行の対応には元のインデックスが使われるため、今回の6行はそのまま残ります。
Q. reset_indexで同じ行数になったら正しいですか?
行数だけでは判断できません。別々の順番の表を位置でつなぐと、違うIDの値が同じ行に置かれます。対応させるIDと値を照合します。
Q. innerで欠損を消してもよいですか?
共通のIDだけを残す目的なら選択肢です。左全員の結果を調べる目的では、片側だけのIDが消える点を先に確認してください。
Q. 結果のない受講IDへ0点を入れますか?
今回の教材では入れません。未受験、登録漏れ、別IDなどの理由は表だけでは分からないため、結果未確認として扱います。
Q. この記事のコードはファイルを書き換えますか?
いいえ。架空のDataFrameをメモリー内で作り、期待値を照合するだけです。読込、保存、外部送信、実在の評価処理は行いません。