Pythonで選挙データを分析してみよう!②〜埼玉投票情報 比例代表情報 読み込み編〜

このシリーズでは、2025/07/21に行われた参院選挙結果を分析するためのプログラムを、一つずつ紐解いて解説していきます。
今回は第2弾です。 前回は小選挙区の投票結果を扱いましたが、今回は比例代表のデータ整形に挑戦します。使用するスクリプトは cleanup_voting_info_by_proportional_representation.py です。

作成したコードは Github に公開しているので、興味があれば、見てみてください。
※コードは、予告なく、改変/削除されることがあります。

基本的な考え方は前回と同じですが、データの内容が少し違うので、そのあたりをどうやって吸収しているのか見ていきましょう。

今回のデータも一筋縄ではいかないExcelファイル

まずは、今回処理する比例代表のデータを見てみます。

プログラムで読み込むとこんな感じです。

令和07年07月20日執行,,,,,,,,,,,,,,,,,,,参投2表
参議院比例代表選出議員選挙 投票調べ(国内+在外),,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,
結果,,,,,,,,,,,,,,,,,,,埼玉県選挙管理委員会
市区町村等,有権者数,,,投票者数,,,棄権者数,,,投票率(%),投票率(%),投票率(%),順位,,前回(R04.07)投票率(%),,,確定時刻,
,男,女,計,男,女,計,男,女,計,男,女,平均,,,男,女,平均,時,分
県 計,3034433.0,3092955.0,6127388.0,1751352.0,1733695.0,3485047.0,1283081.0,1359260.0,2642341.0,57.72,56.05,56.88,,,50.56,49.94,50.25,2.0,13.0
市 計,2832752.0,2889668.0,5722420.0,1634795.0,1620286.0,3255081.0,1197957.0,12693...

ご覧の通り、前回の選挙区データと非常によく似た構造をしていますね。

  • ファイルの先頭にタイトル情報
  • 複数行にまたがる複雑なヘッダー
  • 「県 計」などの集計行の混在

これらを、前回同様にPythonスクリプトで華麗にさばいていきます。

データクレンジングのプロセス

基本的な流れは選挙区データと同じです。違う部分に注目しながら見ていきましょう。

Step 1: Excelファイルの全シートを読み込む

ここは前回と全く同じアプローチです。pd.ExcelFile を使ってExcelファイル内の全シートを効率的に読み込みます。

import pandas as pd
import os

# Excelファイルを指定
# ※実際のコードでは `current_dir` が定義されています
file_path = os.path.join(current_dir, "埼玉投票情報_比例代表.xls")

# Excelファイル内の全シート名を取得して、それぞれをデータフレームとして読み込む
excel_file = pd.ExcelFile(file_path, engine='xlrd')
target_sheets = excel_file.sheet_names
sheet_data = {}
for sheet_name in target_sheets:
    df = pd.read_excel(file_path, sheet_name=sheet_name, engine='xlrd')
    sheet_data[sheet_name] = df

同じ処理を共通化できるのは、プログラムの良いところですね。

Step 2: 比例代表データに合わせたキーワードで不要行を削除

ここが今回のスクリプトの肝であり、前回のスクリプトとの違いが光る部分です。不要な行を判定する関数 is_header_or_unwanted_row の中身を見てみましょう。

def is_header_or_unwanted_row(row_data):
    """
    行がヘッダーや不要な行かどうかを判定する関数
    """
    row_str = ' '.join([str(val) for val in row_data if pd.notna(val)])
    
    # タイトル行のパターンに「比例代表」関連のキーワードを追加
    title_patterns = [
        '参議院議員選挙',
        '比例代表選出議員選挙', #
        '政党別',             # このあたりが比例代表データ特有
        '名簿登載者',           #
        '選挙管理委員会',
        '執行'
    ]
    
    # ヘッダー行のパターンも同様に調整
    header_patterns = [
        '政党等名',           #
        '名簿登載者名',         #
        '市区町村',
        '得票数'
    ]
    
    unit_patterns = ['(票)', '計', 'Unnamed']
    
    # いずれかのキーワードが含まれていればTrueを返す
    for pattern in title_patterns + header_patterns + unit_patterns:
        if pattern in row_str:
            return True
    
    return False

# データフレームの各行に関数を適用し、不要な行を除外する
# (この部分の呼び出し方は前回と同じ)

基本的なロジックは同じですが、title_patternsheader_patterns のリストに '比例代表選出議員選挙''政党等名' といった、比例代表のデータに特有のキーワードが追加されているのが分かります。

このように、処理の骨格は同じまま、判定に使うキーワードリストをデータに合わせて変えるだけで、異なる種類のファイルにも柔軟に対応できるわけです。これもまた、メンテナンス性の高いプログラムの良い点ですね。

Step 3: 列の整形と命名

行の次は列の番です。この処理も前回とほぼ同じですが、最終的な列名がデータの内容に合わせて変わります。

# 空の列や不要な列を削除(処理は前回と同じ)
df = df.dropna(axis=1, how='all')
# ...

# 列名を分かりやすい日本語に付け替える
column_mapping = {
    df.columns[0]: "市区町村名",
    df.columns[1]: "男性有権者数",
    df.columns[2]: "女性有権者数", 
    # ... 以下、データの内容に合わせた列名が続く
}
df = df.rename(columns=column_mapping)

最終的にどのようなデータにしたいのか、というゴールに合わせて列名を定義することで、後工程の分析が非常にやりやすくなります。

整形後のデータがこちら

これらの処理を経て、比例代表のデータも、選挙区データと同じようにスッキリと見やすい形式に生まれ変わりました。

,市区町村名,男性有権者数,女性有権者数,有権者数計,男性投票者数,女性投票者数,投票者数計,...
0,さいたま市西区,52,53,105,28,29,57,...
1,さいたま市北区,35,36,71,20,20,40,...
2,さいたま市大宮区,45,47,92,26,27,53,...
...

(数値はダミーです)

これで、選挙区のデータと比例代表のデータを同じように扱って、横断的な分析を進める準備が整いました。

まとめ

今回は、比例代表の投票結果データを整形するプロセスを見てきました。

前回の選挙区データ整形と非常に似たアプローチでしたが、判定ロジックの核心部分(キーワードリスト)を少し変えるだけで、異なるデータにも対応できるという、プログラムの柔軟性や拡張性の高さを感じていただけたのではないでしょうか。

こうして一つずつデータを綺麗にしていくことで、分析の幅はどんどん広がっていきますね。