東京都人口統計のデータ

Takami Torao
  • このエントリーをはてなブックマークに追加

ダウンロード

以下の 2 つの CSV ファイルは東京都の人口(推計)-過去の推計-の「次の国勢調査人口が公表されるまでの人口推計」から入手できる (人が読む形式の) Excel 形式の統計表1から抽出した数値をプログラムから利用できる形式で出力したものである (いつ時点のデータかは version カラムを参照)。

population.csv
カラム 対応する集計
point_of_time 集計時点
region 地域
male 人口(男)
male 人口(女)
area 面積 (km²)
difference 前月との増減
households (参考) 世帯数
version ファイル更新日時
population_shift.csv
カラム 対応する集計
month この変動値の月
nationality 国籍(all,japanese,foreign)
region 地域
move_in 転入(都外)
move_out 転出(都外)
metro_move_in 転入(都内)
metro_move_out 転出(都内)
birth 出生
death 死亡
other その他
jp_total (別掲)日本人全体の増減
version ファイル更新日時

データベース作成スクリプト

Excel 形式の統計表から SQLite3 形式のデータベースを作成する Python 3 スクリプト。カレントディレクトリに存在する *.xls ファイルを読み込んで population.db を作成/更新する。

$ ls *.xls
-rw-r--r-- 1 torao torao  95744 Jun 13 02:34 js15aa0000.xls
-rw-r--r-- 1 torao torao  96256 Jun 13 02:34 js15ba0000.xls
-rw-r--r-- 1 torao torao  96256 Jun 13 02:34 js15ca0000.xls
...
$ pip3 install xlrd
$ python3 mkpopldb.py
$ sqlite3 population.db "SELECT * FROM POPULATION WHERE region='総数'" -header
id|point_of_time|region|male|female|area|difference|households|version
8065|2015-10-01|総数|6666690|6848581|2190.93|8927|6701122|2020-06-13 02:34:43.162439
8137|2015-11-01|総数|6673467|6855056|2190.93|13252|6711813|2020-06-13 02:34:43.164432
8209|2015-12-01|総数|6674623|6856697|2190.93|2797|6712627|2020-06-13 02:34:43.167424
...

前述の 2 つの CSV ファイルはこの Python スクリプトで作成したデータベースをエクスポートしたもの。

$ sqlite3 population.db "SELECT * FROM population" -header -csv > population.csv
$ sqlite3 population.db "SELECT * FROM population_shift" -header -csv > population_shift.csv

処理方針。

  • 文字列は正規化 (空白文字の削除、全角の半角化) する。
  • 他のカラムから算出可能な数値は保存しない代わりに、サムチェックを兼ねて正しく算出可能かを検証している。
  • 集計表のバージョンによって表記が変わる部分は日付で判断して分岐する。
# -*- coding: utf-8 -*-
# pip3 install xlrd
#
# If you got a "Fatal Python error: config_get_locale_encoding: failed to get the locale encoding: nl_langinfo(CODESET)
# failed", execute export LC_CTYPE="ja_JP.UTF-8".
import datetime
import glob
import os
import re
import sqlite3
import xlrd

NAT_ALL = "all"
NAT_JAPANESE = "japanese"
NAT_FOREIGN = "foreign"


def import_xls(xls, db):
  # 集計シートの読み込み
  modified = datetime.datetime.fromtimestamp(os.stat(xls).st_mtime)
  sheets = read_xls_as_array(xls)
  if len(sheets) != 1:
    error(xls, "想定外のシート数です %d" % (len(sheets)))
  sheet = sheets[0]

  # 集計対象の日付を取得
  date = guess_month(xls, sheet)

  con = sqlite3.connect(db)
  c = con.cursor()

  # 人口統計の投入
  c.execute(
    "CREATE TABLE IF NOT EXISTS population(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, point_of_time DATE NOT NULL, region TEXT NOT NULL, male INTEGER NOT NULL, female INTEGER NOT NULL, area REAL NOT NULL, difference INTEGER NOT NULL, households INTEGER NOT NULL, version TIMESTAMP NOT NULL)")
  c.execute("CREATE UNIQUE INDEX IF NOT EXISTS population_idx00 ON population(point_of_time,region)")
  regions = 0
  for cols in parse_population(xls, sheet, date):
    regions += 1
    c.execute(
      "INSERT OR REPLACE INTO population(point_of_time,region,male,female,area,difference,households,version) VALUES(?,?,?,?,?,?,?,?)",
      (date, cols[0], cols[1], cols[2], cols[3], cols[4], cols[5], modified))

  # 前月人口変動統計の投入 (全て、日本人、外国人)
  month = date + datetime.timedelta(days=-1)
  month = datetime.date(month.year, month.month, 1)
  c.execute(
    " CREATE TABLE IF NOT EXISTS population_shift(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, month DATE NOT NULL, nationality TEXT NOT NULL, region TEXT NOT NULL, move_in INTEGER NOT NULL, move_out INTEGER NOT NULL, metro_move_in INTEGER NOT NULL, metro_move_out INTEGER NOT NULL, birth INTEGER NOT NULL, death INTEGER NOT NULL, other INTEGER NOT NULL, jp_total INTEGER NOT NULL, version TIMESTAMP NOT NULL)""")
  c.execute("CREATE UNIQUE INDEX IF NOT EXISTS population_shift_idx00 ON population_shift(month,nationality,region)")
  for nat in [NAT_ALL, NAT_JAPANESE, NAT_FOREIGN]:
    count = 0
    for cols in parse_population_shift(xls, sheet, date, nat):
      count += 1
      c.execute(
        "INSERT OR REPLACE INTO population_shift(month,nationality,region,move_in,move_out,metro_move_in,metro_move_out,birth,death,other,jp_total,version) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
        (month, nat, cols[0], cols[1], cols[2], cols[3], cols[4], cols[5], cols[6], cols[7], cols[8], modified))
    if regions != count:
      error(xls, "地域の数が異なります: [%s] %d != %d" % (nat, count, regions))

  con.commit()
  con.close()

  print("%s: %d region" % (date, regions))


def parse_population(xls, sheet, date):
  # カラムの意味するところが想定と違っていないかを検証
  header = header_index(xls, sheet, 0)
  actual = [merged_cell_value(sheet, header, i, 1, 3) for i in range(len(sheet[header]))]
  expected = ["地域", "", "人口総数", "男", "女", "性比", "人口総数に対する割合", "面積", "人口密度", "前月人口との増減",
              "(参考)世帯数" if date < datetime.date(2019, 4, 1) else "<参考値>世帯数"]
  for col, (a, e) in enumerate(zip(actual, expected)):
    if not a.startswith(e):
      error(xls, "(%d行%d列) 想定外のヘッダです: %s != %s" % (header + 1, col + 1, a, e))

  # 人口推移の終了行を取得
  _end = lambda row, col, s: type(s) is str and re.match("\\d+月中の人口の動き", s)
  end = search(sheet, _end, header + 3)
  if end is None:
    error(xls, "%d 行目から始まる人口推移の停止地点が不明です" % (header + 1))
  end = end[0]

  # データの解釈と検証 (算出可能なものは保持しない代わりに sumcheck の意味で検証する)
  total = None
  verified = []
  for row in range(header + 3, end):
    region = merged_cell_value(sheet, row, 0, 2, 1)
    if region == "<参考>前年同月及び前々年同月との比較":
      break
    if region != "" and region != "地域" and "下線の数字を修正しました" not in region:
      for i in range(2, len(expected)):
        if type(sheet[row][i]) is not float:
          error(xls, "(%d行%d列) 不正なデータ型が含まれています: [%s] \"%s\" is %s"
                % (row + 1, i + 1, region, sheet[row][i], type(sheet[row][i])))
      male = int(sheet[row][3])
      female = int(sheet[row][4])
      if region == "総数":
        total = male + female
      if int(sheet[row][2]) != male + female:
        error(xls, "(%d行%d列) 男女数と総数が一致しません: [%s] %d + %d != %d"
              % (row + 1, i + 1, region, male, female, sheet[row][2]))
      gender_rate = round(float(male) / float(female) * 100.0 * 10.0) / 10.0
      if abs(gender_rate - sheet[row][5]) >= 0.1:
        error(xls, "(%d行%d列) 男女数と性比が一致しません: [%s] %d / %d = %f != %f"
              % (row + 1, i + 1, region, male, female, gender_rate, sheet[row][5]))
      population_rate = round((male + female) * 100.0 / total * 100.0) / 100.0
      if abs(population_rate - round(sheet[row][6] * 100.0) / 100.0) >= 0.01:
        error(xls, "(%d行%d列) 人口総数割合が一致しません: [%s] %d / %d = %f != %f"
              % (row + 1, i + 1, region, male + female, total, population_rate, sheet[row][6]))
      area = sheet[row][7]
      population_density = round(float(male + female) / area)
      if population_density != sheet[row][8]:
        error(xls, "(%d行%d列) 総数と人口密度が一致しません: [%s] %d / %f = %d != %d"
              % (row + 1, i + 1, region, male + female, area, population_density, sheet[row][8]))
      verified.append((region, male, female, area, int(sheet[row][9]), int(sheet[row][10])))

  # 重複を検証して削除
  modified = True
  while modified and len(verified) > 0:
    modified = False
    for i in range(len(verified)):
      for j in range(i + 1, len(verified)):
        if verified[i][0] == verified[j][0]:
          for k in range(1, len(verified[i])):
            if verified[i][k] != verified[j][k]:
              error("重複している統計 %s が一致しません: %s != %s"
                    % (verified[i][0], verified[i][k], verified[j][k]))
          verified.pop(j)
          modified = True
          break
      else:
        continue
      break

  # 総数が一致しているかを検証 (面積は合計が一致しない)
  verify_cumulative(xls, verified, [1, 2, 4, 5])

  return verified


def parse_population_shift(xls, sheet, date, nationality):
  # 開始位置を取得
  def _eval(row, col, s):
    return type(s) is not str or (nationality == NAT_ALL and re.match("\\d+月中の人口の動き", s)) or (
        nationality == NAT_JAPANESE and re.search("\\d+月中の人口の動き(日本人)", s) or (
        nationality == NAT_FOREIGN and re.match("\\d+月中の人口の動き(外国人)", s)))

  header = search(sheet, lambda row, col, s: type(s) is str and re.match("\\d+月中の人口の動き", s))
  if header is None:
    error(xls, "人口推移表のヘッダが見つかりません")
  header = header[0] + 1

  # カラムの意味するところが想定と違っていないかを検証
  actual = [merged_cell_value(sheet, header, i, 1, 3) for i in range(len(sheet[header]))]
  expected = ["地域", "", "全体の増減", "他県との移動増減", "転入", "転出", "都内間移動増減", "転入", "転出", "自然動態増減", "出生",
              "死亡", "その他の増減"]
  if nationality == NAT_ALL:
    expected.append("(別掲)日本人全体の増減")
  for col, (a, e) in enumerate(zip(actual, expected)):
    if a != e:
      error(xls, "(%d行%d列) 想定外のヘッダです: %s != %s" % (header, col, a, e))

  # 人口推移の開始行を取得
  start = search(sheet, lambda row, col, s: (col == 0 or col == 1) and s == "総数", header + 1)
  if start is None:
    error(xls, "(%d行) 人口推移の開始地点が不明です" % (header + 1))
  start = start[0]

  # 人口推移の終了行を取得
  end = search(sheet, lambda row, col, s: col == 2 and s == "", start + 1)
  if end is None:
    error(xls, "(%d行) 人口推移の停止地点が不明です" % (header + 1))
  end = end[0]

  # データの解釈と検証 (算出可能なものは保持しない代わりに checksum の意味で検証する)
  verified = []
  for row in range(start, end):
    region = merged_cell_value(sheet, row, 0, 2, 1)
    if not re.fullmatch(".*[市区町村部庁数]", region):
      error(xls, "(%d行) 予期しない地域名です: \"%s\"" % (row + 1, region))

    cols = sheet[row]
    for i in range(2, len(cols)):
      cols[i] = 0 if cols[i] == "-" else cols[i]

    # 転入出 (他県)
    move_in = cols[4]
    move_out = cols[5]
    if cols[3] != move_in - move_out:
      error(xls, "(%d行) 転入出(他県)と増減が一致しません: [%s] %d - %d = %d != %d"
            % (row + 1, region, move_in, move_out, move_in + move_out, cols[3]))

    # 転入出 (都内)
    metro_move_in = cols[7]
    metro_move_out = cols[8]
    if cols[6] != metro_move_in - metro_move_out:
      error(xls, "(%d行) 転入出(都内)と増減が一致しません: [%s] %d - %d = %d != %d"
            % (row + 1, region, metro_move_in, metro_move_out, metro_move_in + metro_move_out, cols[6]))

    # 自然動態
    birth = cols[10]
    death = cols[11]
    if cols[9] != birth - death:
      error(xls, "(%d行) 出生/死亡と増減が一致しません: [%s] %d - %d = %d != %d"
            % (row + 1, region, birth, death, birth - death, cols[9]))

    # 全体の増減
    if cols[2] != cols[3] + cols[6] + cols[9] + cols[12]:
      error(xls, "(%d行) 全体の増減が一致しません: [%s] %d + %d + %d + %d = %d != %d"
            % (row + 1, region, cols[3], cols[6], cols[9], cols[12], cols[6] + cols[9] + cols[12], cols[2]))

    verified.append((region, move_in, move_out, metro_move_in, metro_move_out, birth, death, cols[12], cols[13]))

  # 総数が一致しているかを検証
  verify_cumulative(xls, verified, [1, 2, 3, 4, 5, 6, 7, 8])

  return verified


def verify_cumulative(xls, rows, columns):
  # 総数が一致しているかを検証
  verify_cumulative_row(xls, rows, "区部", lambda s: re.fullmatch(".*区", s), columns)
  verify_cumulative_row(xls, rows, "市部", lambda s: re.fullmatch(".*市", s), columns)
  verify_cumulative_row(xls, rows, "町村部", lambda s: s in ["郡部", "島部"], columns)
  verify_cumulative_row(xls, rows, "郡部", lambda s: s in ["瑞穂町", "日の出町", "檜原村", "奥多摩町"], columns)
  verify_cumulative_row(xls, rows, "島部", lambda s: re.fullmatch(".*庁", s), columns)
  verify_cumulative_row(xls, rows, "大島支庁", lambda s: s in ["大島町", "利島村", "新島村", "神津島村"], columns)
  verify_cumulative_row(xls, rows, "三宅支庁", lambda s: s in ["三宅村", "御蔵島村"], columns)
  verify_cumulative_row(xls, rows, "八丈支庁", lambda s: s in ["八丈町", "青ヶ島村"], columns)
  verify_cumulative_row(xls, rows, "小笠原支庁", lambda s: s in ["小笠原村"], columns)
  verify_cumulative_row(xls, rows, "総数", lambda s: s in ["区部", "市部", "郡部", "島部"], columns)


def verify_cumulative_row(xls, rows, region, matcher, columns):
  """
  総数が一致しているかを検証
  """
  expected = None
  for cols in rows:
    if cols[0] == region:
      expected = cols
      break
  if expected is None:
    error(xls, "総数検証の %s が見つかりません" % (region,))
  actual = [0] * len(expected)
  for cols in rows:
    if matcher(cols[0]):
      for i in range(1, len(actual)):
        actual[i] += cols[i]
  for i in columns:
    if actual[i] != expected[i]:
      error(xls, "合計が一致しません: [%s] %s != %s" % (expected[0], expected[i], actual[i]))


def guess_month(xls, sheet):
  cols = sheet[header_index(xls, sheet, 0) - 1]
  for col in cols:
    matcher = re.fullmatch("(平成|令和)(\\d+|元)年(\\d+)月(\\d+)日現在", col)
    if matcher is not None:
      era = matcher[1]
      y = 1 if matcher[2] == "元" else int(matcher[2])
      m = int(matcher[3])
      d = int(matcher[4])
      if era == "平成":
        y += 1988
      elif era == "令和":
        y += 2018
      else:
        error(xls, "不正な年号")
      return datetime.date(y, m, d)
  error(xls, "年月日を検出できません")


def header_index(xls, sheet, offset=0):
  pos = search(sheet, lambda row, col, s: s == "地域", offset)
  if pos is None:
    error(xls, "集計表のヘッダ (cell[row][0]='地域') が見つかりません")
  return pos[0]


def search(sheet, eval, row_offset=0, col_offset=0):
  for row in range(row_offset, len(sheet)):
    for col in range(col_offset, len(sheet[row])):
      if eval(row, col, sheet[row][col]):
        return row, col
  return None


def read_xls_as_array(xls):
  """
  Excel ファイルを読み込んで全てのシートを 2 次元配列として取得する。
  """
  wb = xlrd.open_workbook(xls)
  sheets = [[[cell_to_value(sheet.cell(row, col)) for col in range(sheet.ncols)] for row in range(sheet.nrows)]
            for sheet in wb.sheets()]
  return sheets


def merged_cell_value(sheet, row, col, x, y):
  ss = []
  for i in range(y):
    for j in range(x):
      ss.append(sheet[row + i][col + j])
  return normalize(" ".join(ss))


def cell_to_value(cell):
  return cell.value if cell.ctype != 1 else normalize(cell.value)


def normalize(s):
  s = re.sub("[  \u3000\r\n\t]+", "", s).strip()  # 空白文字の削除
  s = s.translate(str.maketrans({chr(0xFF01 + i): chr(0x21 + i) for i in range(94)}))  # 全角→半角変換処理
  return s


def error(xls, msg, rows=None):
  print("ERROR [%s]: %s" % (xls, msg), file=sys.stderr)
  if rows is not None:
    for row in len(rows):
      print(",".join(rows[row]), file=sys.stderr)
  sys.exit(1)


if __name__ == "__main__":
  files = sorted(glob.glob("./*.xls"))
  for xls in files:
    import_xls(xls, "population.db")

参照

  1. 東京都の人口 (推計)