【データ整理】別々に保存した複数銘柄のCSVを一括で1つの表にまとめる

準備・環境構築

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

銘柄ごとにCSVファイルをダウンロードし、フォルダに保存する作業を繰り返してきた方は多いはずです。

しかし、いざ複数銘柄を横断的に分析しようとすると、ファイルがバラバラで扱いにくいという問題に直面します。

「毎回Excelで手作業コピペしている」「ファイルが増えるたびに管理が破綻する」という悩みは定番です。

原因は、最初のデータ保存時点で「後から統合しやすい構造」を意識していないことにあります。

本記事では、pandasのconcatmergeを使い、フォルダ内の複数CSVを一括で1つのDataFrame(データフレーム)に統合する方法を解説します。

コピペで動くコードを2パターン用意しているので、自分のデータ構造に合わせて使い分けてください。

concatとmergeの違いと使い分け

concatは「縦に積む」操作

pd.concatは、同じカラム構成のDataFrameを縦方向に連結する関数です。

銘柄ごとに「日付・始値・高値・安値・終値・出来高」という同一構造のCSVがある場合に使います。

各ファイルのデータを上下に積み重ね、「どの行がどの銘柄か」を識別する列を追加するのが基本パターンです。

用途としては、全銘柄の終値を時系列で一覧したい場合や、銘柄横断のフィルタリングに適しています。

mergeは「横に結合する」操作

pd.mergeは、共通するキー列(通常は日付)を基準にDataFrameを横方向に結合する関数です。

「日付をキーに、各銘柄の終値を横に並べた比較表を作りたい」という場合に使います。

相関分析やポートフォリオ分析では、この横持ち形式が必須です。

使い分けの判断基準はシンプルで、「全行を保持したい→concat」「共通キーで列を増やしたい→merge」と覚えてください。

【コピペOK】concat方式:縦積み統合コード

まずは最も汎用性の高い、concat方式のコードです。


pip install pandas

import pathlib
from typing import Final

import pandas as pd

# ==============================
# 設定エリア
# ==============================
CSV_DIR: Final[str] = "./csv_data"
OUTPUT_FILE: Final[str] = "merged_vertical.csv"
ENCODING: Final[str] = "utf-8-sig"
TICKER_COLUMN: Final[str] = "銘柄コード"
DATE_COLUMN: Final[str] = "Date"


# ==============================
# CSVファイル一覧の取得
# ==============================
def get_csv_paths(directory: str) -> list[pathlib.Path]:
    '指定ディレクトリ内の全CSVファイルパスを取得する'
    dir_path = pathlib.Path(directory)
    if not dir_path.exists():
        raise FileNotFoundError(f"ディレクトリが見つかりません: {directory}")
    csv_files = sorted(dir_path.glob("*.csv"))
    if not csv_files:
        raise FileNotFoundError(f"CSVファイルが見つかりません: {directory}")
    print(f"検出ファイル数: {len(csv_files)}")
    return csv_files


# ==============================
# 単一CSVの読み込みと銘柄コード付与
# ==============================
def load_single_csv(file_path: pathlib.Path) -> pd.DataFrame:
    'CSVを読み込み、ファイル名から銘柄コード列を付与する'
    df = pd.read_csv(file_path, encoding=ENCODING)
    ticker = file_path.stem  # 拡張子を除いたファイル名
    df[TICKER_COLUMN] = ticker
    print(f"  {ticker}: {len(df)}行")
    return df


# ==============================
# 全CSVを縦方向に統合
# ==============================
def concat_all_csv(csv_paths: list[pathlib.Path]) -> pd.DataFrame:
    '全CSVを読み込みconcatで縦積みする'
    frames: list[pd.DataFrame] = []
    for path in csv_paths:
        df = load_single_csv(path)
        frames.append(df)
    merged = pd.concat(frames, ignore_index=True)
    return merged


# ==============================
# 日付列の型変換とソート
# ==============================
def format_and_sort(df: pd.DataFrame) -> pd.DataFrame:
    '日付列をdatetime型に変換し、銘柄コード・日付でソートする'
    if DATE_COLUMN in df.columns:
        df[DATE_COLUMN] = pd.to_datetime(df[DATE_COLUMN])
        df = df.sort_values([TICKER_COLUMN, DATE_COLUMN]).reset_index(drop=True)
    return df


# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    print("=== CSV縦積み統合 開始 ===")
    paths = get_csv_paths(CSV_DIR)
    merged_df = concat_all_csv(paths)
    merged_df = format_and_sort(merged_df)
    print(f"n統合後: {len(merged_df)}行 × {len(merged_df.columns)}列")
    print(merged_df.head(10).to_string(index=False))
    merged_df.to_csv(OUTPUT_FILE, index=False, encoding=ENCODING)
    print(f"n保存完了: {OUTPUT_FILE}")

コードの処理フロー解説

上記のコードは、以下の4ステップで構成されています。

* ステップ1 ファイル検索:pathlib.Path.glob("*.csv")で指定フォルダ内の全CSVファイルを自動検出します

* ステップ2 個別読み込み:各CSVをpd.read_csvで読み込み、ファイル名(拡張子なし)を銘柄コード列として付与します

* ステップ3 縦積み統合:pd.concatで全DataFrameを一括連結し、ignore_index=Trueでインデックスを振り直します

* ステップ4 整形・保存:日付列をdatetime型に変換してソートし、CSVとして出力します

CSV_DIRのパスを自分のフォルダに書き換えるだけで動作します。

【コピペOK】merge方式:横結合で比較表を作成

複数銘柄の終値を日付ベースで横に並べる、merge方式のコードです。


import pathlib
from functools import reduce
from typing import Final

import pandas as pd

# ==============================
# 設定エリア
# ==============================
CSV_DIR: Final[str] = "./csv_data"
OUTPUT_FILE: Final[str] = "merged_horizontal.csv"
ENCODING: Final[str] = "utf-8-sig"
DATE_COLUMN: Final[str] = "Date"
VALUE_COLUMN: Final[str] = "Close"
MERGE_METHOD: Final[str] = "outer"


# ==============================
# CSVファイル一覧の取得
# ==============================
def get_csv_paths(directory: str) -> list[pathlib.Path]:
    '指定ディレクトリ内の全CSVファイルパスを取得する'
    dir_path = pathlib.Path(directory)
    if not dir_path.exists():
        raise FileNotFoundError(f"ディレクトリが見つかりません: {directory}")
    csv_files = sorted(dir_path.glob("*.csv"))
    if not csv_files:
        raise FileNotFoundError(f"CSVファイルが見つかりません: {directory}")
    print(f"検出ファイル数: {len(csv_files)}")
    return csv_files


# ==============================
# 単一CSVから日付と対象列だけ抽出
# ==============================
def extract_column(file_path: pathlib.Path) -> pd.DataFrame:
    'CSVから日付列と指定列を抽出し、列名を銘柄コードにリネームする'
    df = pd.read_csv(file_path, encoding=ENCODING)
    ticker = file_path.stem
    if VALUE_COLUMN not in df.columns:
        raise KeyError(f"{VALUE_COLUMN}列が見つかりません: {file_path.name}")
    subset = df[[DATE_COLUMN, VALUE_COLUMN]].copy()
    subset[DATE_COLUMN] = pd.to_datetime(subset[DATE_COLUMN])
    subset = subset.rename(columns={VALUE_COLUMN: ticker})
    print(f"  {ticker}: {len(subset)}行")
    return subset


# ==============================
# 全CSVを横方向にmerge
# ==============================
def merge_all_csv(csv_paths: list[pathlib.Path]) -> pd.DataFrame:
    '日付をキーに全CSVを横結合する'
    frames = [extract_column(path) for path in csv_paths]
    merged = reduce(
        lambda left, right: pd.merge(left, right, on=DATE_COLUMN, how=MERGE_METHOD),
        frames,
    )
    merged = merged.sort_values(DATE_COLUMN).reset_index(drop=True)
    return merged


# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    print("=== CSV横結合 開始 ===")
    paths = get_csv_paths(CSV_DIR)
    merged_df = merge_all_csv(paths)
    print(f"n統合後: {len(merged_df)}行 × {len(merged_df.columns)}列")
    print(merged_df.head(10).to_string(index=False))
    merged_df.to_csv(OUTPUT_FILE, index=False, encoding=ENCODING)
    print(f"n保存完了: {OUTPUT_FILE}")

コードの処理フロー解説

上記のコードは、以下の3ステップで構成されています。

* ステップ1 列抽出:各CSVから日付列と終値(Close)列だけを取り出し、列名をファイル名(銘柄コード)にリネームします

* ステップ2 横結合:functools.reducepd.mergeを組み合わせ、日付をキーに全DataFrameを順番に結合します。how="outer"で全日付を保持します

* ステップ3 整形・保存:日付でソートしてCSV出力します。休場日など片方にしかない日付はNaNになります

VALUE_COLUMN"Volume"に変えれば、出来高の比較表も同じコードで作成できます。

統合後のデータ品質チェック

統合しただけで安心してはいけません。

データの欠損や重複がないかを確認する習慣をつけてください。

以下の3つのチェックを統合直後に実行することを推奨します。

* 欠損値の確認:merged_df.isnull().sum()で各列の欠損数を表示します。outer結合では休場日の差異でNaNが発生するため、fillna(method="ffill")で前日値補完するか、dropnaで除外するか判断してください

* 重複行の確認:merged_df.duplicated().sum()でゼロであることを確認します。同一ファイルを二重にフォルダへ入れてしまうミスは意外と多いです

* 行数の妥当性検証:concat方式の場合、統合後の行数は各ファイルの行数合計と一致するはずです。一致しなければ読み込みエラーが発生しています

よくあるエラーと対処法

UnicodeDecodeError: ‘utf-8’ codec can’t decode byte

CSVファイルの文字コード(Character Encoding)がUTF-8ではない場合に発生します。

日本語環境のExcelで保存したCSVはShift_JIS(CP932)であることが多いです。

以下を試してください。

* 設定エリアのENCODING"cp932"に変更してください

* それでも解決しない場合は"utf-8"を試してください

* 文字コードが不明な場合はchardetライブラリで自動検出できます。pip install chardetの後、chardet.detect(open(file, "rb").read())で確認してください

KeyError: ‘Close’

CSVファイルにCloseという列名が存在しない場合に発生します。

データ提供元によって列名が終値Adj Closecloseなど異なります。

以下を試してください。

* pd.read_csv直後にprint(df.columns.tolist())で実際の列名を確認してください

* 設定エリアのVALUE_COLUMNを実際の列名に書き換えてください

* 列名が日本語の場合は全角・半角の違いにも注意してください。"終値"" 終値"(先頭にスペース)は別物です

MergeError / 結合後に行数が爆発的に増える

日付列に重複がある場合、merge時に多対多結合(Many-to-Many Join)が発生し、行数が想定外に膨れ上がります。

1つの銘柄CSVに同じ日付の行が複数存在しているのが原因です。

以下を試してください。

* merge前に各DataFrameでdf.duplicated(subset=[DATE_COLUMN]).sum()を実行し、日付の重複を検出してください

* 重複がある場合はdf.drop_duplicates(subset=[DATE_COLUMN], keep="last")で最新行だけを残してください

* 元のCSVファイル自体を確認し、データ取得スクリプト側で重複出力を防ぐ修正を行ってください

まとめ

この記事では、複数銘柄のCSVファイルをpandasのconcatmergeで一括統合する方法を解説しました。

要点を整理します。

* pd.concatは同一構造のCSVを縦に積む操作で、銘柄コード列を付与して使います

* pd.mergeは日付をキーに横結合する操作で、銘柄比較や相関分析に適しています

* ファイル検索にはpathlib.Path.glob("*.csv")を使い、手動でのファイル名指定を排除してください

* 統合後は欠損値・重複行・行数の3点を必ずチェックしてください

* 文字コードと列名の不一致が最も多いエラー原因です。設定エリアの定数を書き換えるだけで対処できます

次のステップとして、統合後のDataFrameに対してテクニカル指標の一括計算や銘柄間の相関分析を行ってみてください。VALUE_COLUMN"Volume"に変えて出来高の比較表を作るだけでも、分析の幅が大きく広がります。

タイトルとURLをコピーしました