盆暗の学習記録

データサイエンス ,エンジニアリング,ビジネスについて日々学んだことの備忘録としていく予定です。初心者であり独学なので内容には誤りが含まれる可能性が大いにあります。

データ分析時のメモリ使用量を減らす方法

最近少し覚えたことをまとめます。

(基本的にpythonのコードと共に述べていきますが、Rの場合についても少し触れていきます。)

不要なオブジェクトの削除

「使わなくなったオブジェクトを削除してメモリを開放する」という考えです。

del

例えば以下のコードを試してみます。

import gc
import psutil
import numpy as np
import sys
nrows = 500000
ncols = 100
print('データを生成します')
X = np.random.rand(nrows, ncols)
print(f'データのサイズ: {sys.getsizeof(X) / 1024**2: .1f} MB')
mem_before = psutil.virtual_memory().used
print(f'del前のメモリ使用量: {mem_before / 1024**3 :.1f} GB')
del X
mem_after = psutil.virtual_memory().used
mem_diff = mem_before - mem_after
print(f'del後のメモリ使用量: {mem_after / 1024**3 :.1f} GB ({mem_diff / 1024**2 :.1f} MB reduced)')

上のコードを実行すると、以下のような結果になります。

データを生成します
データのサイズ:  381.5 MB
del前のメモリ使用量: 11.5 GB
del後のメモリ使用量: 11.2 GB (362.4 MB reduced)

手動でのガベージコレクションは不要

delのあとに明示的にガベージコレクションgc.collect())を使う方法も考えられますが、基本的に自動でのガベージコレクションが働くのでやらなくて問題ないみたいです。

また、ガベージコレクションを行ったとしても、メモリーがOSに返されるとは限らないようです1

Rの場合

一方でRはオブジェクトを削除しただけではメモリ使用量が変わらないため、手動でのガベージコレクションが有効なようです2

rm(X)
gc(reset = TRUE)

データ型の最適化

numpyにはuint8float32int64などの細かいデータ型が用意されています。

例えば以下のようにnp.iinfo()np.finfo()のメソッドを使うと、そのデータ型でもてる最小値・最大値などの情報を見ることができます。

import numpy as np
print(np.iinfo(np.int8))
print(np.iinfo(np.int16))
print(np.iinfo(np.int32))
print(np.iinfo(np.int64))
print(np.finfo(np.float16))
print(np.finfo(np.float32))
print(np.finfo(np.float64))
Machine parameters for int8
---------------------------------------------------------------
min = -128
max = 127
---------------------------------------------------------------

Machine parameters for int16
---------------------------------------------------------------
min = -32768
max = 32767
---------------------------------------------------------------
(以下略)

8bitのint8は-128~127の範囲の値しか表現できませんが、例えば二値変数(0, 1)や月(1~12)や日(1~31)のデータなどであればこのくらいの表現力で十分ですよね。なので最適化しましょう、というのがこのアプローチの考え方です。

自動で型変換するコード

こんな感じのやつを使っています。

import pandas as pd
import numpy as np

def reduce_mem_usage(df: pd.DataFrame) -> pd.DataFrame:
    """データフレームのすべてのカラムのデータ型を最適化してメモリ使用量を抑えるやつ"""
    start_mem = df.memory_usage().sum() / 1024**2
    print(f'Memory usage of dataframe is {start_mem:.2f} MB')
    df = df.apply(_optimize_dtype, axis=0)
    end_mem = df.memory_usage().sum() / 1024**2
    print(f'Memory usage after optimization is: {end_mem:.2f} MB')
    print(f'Decreased by {100 * (start_mem - end_mem) / start_mem:.1f}%')
    return df


def _optimize_dtype(col):
    """あるカラム/変数のデータ型を最適化するやつ"""
    col_type = col.dtype
    if col_type != 'object' and col_type != 'datetime64[ns]':
        c_min = col.min()
        c_max = col.max()
        # intにできるなら変換するが、NAがあればfloatのままにする(intはNAを保持できない)
        is_int = False
        if _is_int(col):
            if col.isnull().sum() == 0:
                is_int = True
            else:
                print(f'{col.name} was not convert to int because it has NA')
        # bit数の最適化
        if is_int:
            if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                col = col.astype(np.int8)
            elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                col = col.astype(np.int16)
            elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                col = col.astype(np.int32)
            elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                col = col.astype(np.int64)
        else:
            if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                # feather-formatがfloat16を受け入れないためfloat32にする
                col = col.astype(np.float32)
            elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                col = col.astype(np.float32)
            else:
                col = col.astype(np.float64)
    return col


def _is_int(col):
    """あるカラム/変数が整数かどうかを判定する"""
    fractional, _ = np.modf(col)
    if np.sum(fractional) == 0: # 小数部がすべて0
        return True
    else:
        return False

使用例は次のような感じです。

import numpy as np
import pandas as pd
import sys

# データを生成
nrows = 500_000
ncols = 100
X = [np.random.randint(low=0, high=50, size=nrows) for x in range(ncols)]
X = pd.DataFrame(np.array(X).T)
print('データを生成 ---------------------------')
print('dtypes: ')
print(X.dtypes.value_counts().to_frame(name='rows'))
print(f'データ型変換前のオブジェクトのサイズ: {sys.getsizeof(X) / 1024**2 : .1f} MB')

print('\nデータ型を変換 -------------------------')
X = reduce_mem_usage(X)
print('dtypes: ')
print(X.dtypes.value_counts().to_frame(name='rows'))
print(f'データ型変換後のオブジェクトのサイズ: {sys.getsizeof(X) / 1024**2 : .1f} MB')
データを生成 ---------------------------
dtypes:
       rows
int64   100
データ型変換前のオブジェクトのサイズ:  381.5 MB

データ型を変換 -------------------------
Memory usage of dataframe is 381.47 MB
Memory usage after optimization is: 47.68 MB
Decreased by 87.5%
dtypes:
      rows
int8   100
データ型変換後のオブジェクトのサイズ:  47.7 MB

このコードはkaggleのkernelを元にえじさんが改造したものを自分用に改造したものです。

えじさんのものからの変更点は

  • loggerに関する部分をprintに置き換え(私がまだloggerに慣れていないため)
  • floatであっても小数点以下が全部0でNAの無いカラムはintに変換する
  • forループでなくapply()を使う(applyの方が何倍か速い3

です。

疎行列クラスを使う

ダミー変数(One Hot Encodingした列)が多いデータの場合、非ゼロ要素の情報しか保持しない疎行列用のクラスを使うことでメモリ使用量を大幅に削減できます。

pythonの場合はscipy.sparseを、Rの場合は{Matrix}パッケージを使うことで効率よく疎行列を保持できます。

import pandas as pd
import numpy as np
import sys
import scipy.sparse
nrows = 500000
ncols = 500
categories = pd.Series(np.random.randint(low=0, high=ncols, size=nrows))
X = pd.get_dummies(categories)
print(f'dtypes: {X.dtypes.value_counts().to_dict()}')
print(f'データのサイズ: {sys.getsizeof(X) / 1024**2: .1f} MB')
X_csr = scipy.sparse.csr_matrix(X, shape=X.shape)
print(f'csr_matrixのサイズ: {sys.getsizeof(X_csr): .1f} Byte')
dtypes: {dtype('uint8'): 500}
データのサイズ:  238.4 MB
csr_matrixのサイズ:  56.0 Byte

  1. アプリケーションのメモリー使用量の最適化

  2. Rでメモリを解放したい - まずは蝋の翼から。

  3. 使用例で使った50万行x100列のデータの変換を10回行って実行時間の平均をとると、applyを使う場合1.62±0.01秒、forループの場合19.668±0.292秒でした。

pythonで塗り分け地図を描く

f:id:nigimitama:20190928225127p:plain

基本的なplotの仕方を覚えたのでメモ。

geopandasによるplot

データの用意

市区町村レベルの行政区域データは国土数値情報の行政区域データから、 町丁レベルの行政区域データはe-Statの境界データダウンロードの小地域データから取得できます。

e-Statの小地域データを例にやってみます

input_path = "shapefile" # 展開先フォルダ名

# e-stat 国勢調査 小地域(町丁・字等別) 東京都全域
shapefile_url = "https://www.e-stat.go.jp/gis/statmap-search/data?dlserveyId=A002005212015&code=13&coordSys=1&format=shape&downloadType=5"
shapefile_name = "tokyo.zip"

# ダウンロード
from urllib.request import urlretrieve
urlretrieve(url=shapefile_url, filename=shapefile_name)

# 解凍
import zipfile
with zipfile.ZipFile(shapefile_name) as existing_zip:
    existing_zip.extractall(input_path)

# ファイル名を取得
import os
files = os.listdir(input_path)
shapefile = [file for file in files if ".shp" in file][0]
print(f"downloaded shapefile: {shapefile}")

# 読み込み
import geopandas as gpd
shapefile_path = os.path.join(input_path, shapefile)
df = gpd.read_file(shapefile_path, encoding='cp932')
print(f"{shapefile_path} is loaded")

# 東京都の島嶼部を除く
import pandas as pd
islands = ['大島町', '利島村', '新島村', '神津島村', '三宅村', '御蔵島村', '八丈町', '青ヶ島村', '小笠原村']
is_not_islands = [df["CITY_NAME"] != island for island in islands]
is_not_islands = pd.concat(is_not_islands, axis=1).all(axis=1)
df = df.loc[is_not_islands, :]

# 陸地だけにする
df = df.loc[df["HCODE"] == 8101, :]

こんな感じのデータです(詳細はe-Statから定義書をご参照ください)

f:id:nigimitama:20190928224557p:plain

plot

全体図

島嶼部を除いた、いつもの東京都の図。

# 全体図
df.plot(figsize=[10,10])

f:id:nigimitama:20190928224655p:plain

塗り分け図

# 人口で塗り分け
df.plot(column="SETAI", legend=True, figsize=[30,10], cmap="Oranges")

f:id:nigimitama:20190928224751p:plain

# 人口密度で塗り分け
df["pop_density"] = df["JINKO"] / df["AREA"]
df.plot(column="pop_density", legend=True, figsize=[30,10], cmap="Blues")

f:id:nigimitama:20190928224833p:plain

一部の地域ですごく高い値がある関係で、ほとんどの地域で薄い色に…

値のラベルをつけた図

# 値ラベル用にgeometryから当該ポリゴン内のある地点を取得
df["coords"] = df["geometry"].apply(lambda x: x.representative_point().coords[:])
df["coords"] = [coords[0] for coords in df["coords"]]

# 文京区の人口密度
temp = df.query("CITY_NAME == '文京区'")
temp.plot(column="pop_density", legend=True, figsize=[30,10], cmap="Blues")

# 値ラベル
import matplotlib.pyplot as plt
for i, row in temp.iterrows():
    plt.annotate(s=round(row["pop_density"], 2), xy=row["coords"], horizontalalignment="center")

f:id:nigimitama:20190928225127p:plain

[WSL Ubuntu]pythonで塗り分け地図を描くための環境構築

f:id:nigimitama:20190928230707p:plain

Ubuntuへのgeopandasというライブラリのインストールまでの流れと、塗り分け図のプロットの例を書いていきます。

私のPCの環境について

WindowsですがWSLを使います。

geopandasのインストール

windowsへのインストールはめちゃくちゃ面倒くさい上に何度やっても失敗したのでUbuntuに入れます。

sudo apt update
sudo apt upgrade

# pipがない場合、インストール
sudo apt install python3-pip
# Geopandasの依存ライブラリをインストール
sudo pip3 install numpy pandas shapely fiona pyproj six descartes
# Install Geopandas
sudo pip3 install git+https://github.com/geopandas/geopandas.git

jupyterが入っていない場合は

sudo pip3 install jupyterlab

でjupyterを入れ、$ jupyter labで起動します。

起動して

import matplotlib.pyplot as plt
%matplotlib inline

import geopandas as gpd
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
world.plot()

と書いて

f:id:nigimitama:20190928222903p:plain

のように図が出ればインストールは成功です。

記事冒頭の図は

world.plot(column="pop_est", figsize=[10,10], cmap="Oranges")

というコードで描くことができます。色が濃い国は人口が多い国です。

f:id:nigimitama:20190928230724p:plain

Chromeの標準フォントを変更してギザギザ文字を無くす

f:id:nigimitama:20190825120233p:plain

はじめに

WindowsMacに比べるとデフォルトのフォントが汚いことが多いですが,CSSが適切に設定されていないWebサイトのフォントについてはChrome側のデフォルトのフォント設定を変更することで解決できるみたいです。

やり方をメモしておきます。

設定方法

Chromeの「設定」→「フォントをカスタマイズ」から設定できます。

f:id:nigimitama:20190825124607p:plain

お好みのフォント・フォントサイズを設定してください。

f:id:nigimitama:20190825124654p:plain

おすすめフォント

游ゴシックにすると文字が細すぎて見づらいので,「源ノ角ゴシック(source han sans)」というオープンソースのフォントにしたらいい感じでした

f:id:nigimitama:20190825122852p:plain

源ノ角ゴシックCode (Source Han Code JP)というプログラミング用のもあるので固定幅フォントはRictyの代わりにこちらにしてもいいかも。

游ゴシックの場合、Medium(やや太字)に設定するのがおすすめです。

pythonによるe-stat APIからのデータ取得

前回に引き続いて,公的データの取得に関してメモ。

Rだと便利なライブラリがあるが,Pythonだとやや面倒かも。

賃金構造基本統計調査 雇用形態別DBを例に説明していく

アプリケーションIDの用意

e-Statでユーザー登録し,アプリケーションIDを取得してコードに貼り付けておく

# 読み込み
app_id = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

APIアクセス用メソッドの定義

共通する処理なのでメソッドにしておく

import pandas as pd
import urllib
import json
# GETリクエストの送信からjson受け取り,dictへの変換までの流れを行うメソッド
def get_estat_data(api_method: str, params: dict, api_version="3.0", return_format="json") -> dict:
    url = f"https://api.e-stat.go.jp/rest/{api_version}/app/{return_format}/{api_method}?{urllib.parse.urlencode(params)}"
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as res:
        response = res.read()
    return json.loads(response.decode())

統計情報の取得

普通に人力で探す場合

APIのgetStatsListを使う場合

  • APIで調べることもできる
    • メソッドのURLはgetStatsList
  • 全件取得する場合はパラメータにappIdのみを入れる

  • パラメータを設定することで,条件で絞り込んで取得できる

    • searchWordに値を入れればキーワードで検索することもできる
params = {
    "appId": app_id,
    "searchWord": "賃金構造基本統計調査 雇用形態別" # キーワードで検索
}
stats_list = get_estat_data(api_method="getStatsList", params=params)
stats = stats_list["GET_STATS_LIST"]["DATALIST_INF"]["TABLE_INF"]
stats_df = pd.io.json.json_normalize(stats)
stats_df[["@id","STAT_NAME.$","STATISTICS_NAME","TITLE","CYCLE","SURVEY_DATE"]].head()

@idの「0003082750」が目的のstatsDataId

f:id:nigimitama:20190810165015p:plain

メタ情報の取得:getMetaInfo

  • 一度に取得できるデータの上限は10万レコード
  • 縦持ちテーブルでデータがDLされるので,この上限にはすぐに達してしまう
    • リクエストパラメータで指定できるように,予め必要なデータ項目(変数,カラム)を調べておく必要がある
    • getMetaInfoで調べられる
  • getMetaInfostatsDataIdが必須パラメータなので,先程取得したIDを入れる

メタ情報の取得

stats_data_id = "0003082750"
params = {"appId": app_id, "statsDataId": stats_data_id}
meta_info = get_estat_data(api_method="getMetaInfo", params=params)

CLASS_INF:データに関する情報

class_info = meta_info["GET_META_INFO"]["METADATA_INF"]["CLASS_INF"]["CLASS_OBJ"]

# 中身を見やすいように整形
tab = pd.io.json.json_normalize(class_info[0], record_path="CLASS")
cat01 = pd.io.json.json_normalize(class_info[1], record_path="CLASS")
cat02 = pd.io.json.json_normalize(class_info[2], record_path="CLASS")
cat03 = pd.io.json.json_normalize(class_info[3], record_path="CLASS")
cat04 = pd.io.json.json_normalize(class_info[4], record_path="CLASS")
cat05 = pd.io.json.json_normalize(class_info[5], record_path="CLASS")
cat06 = pd.io.json.json_normalize(class_info[6], record_path="CLASS")
cat07 = pd.io.json.json_normalize(class_info[7], record_path="CLASS")
cat08 = pd.io.json.json_normalize(class_info[8], record_path="CLASS")

f:id:nigimitama:20190810165736p:plain

f:id:nigimitama:20190810170015p:plain

これらの中身を見て,必要なデータを取得するためのコードを把握しておく。

統計データの取得:getStatsData

params = {
    "appId": app_id,
    "statsDataId": stats_data_id,
    "cdTab": tab.loc[4, "@code"],
    "cdCat01": cat01.loc[0,"@code"],
    "cdCat02": cat02.loc[0,"@code"]
}
stats_data = get_estat_data(api_method="getStatsData", params=params)

DATA_INF:統計データ

# DATA_INF: 統計データ
data_info = stats_data["GET_STATS_DATA"]["STATISTICAL_DATA"]["DATA_INF"]

ここにはNOTEVALUEが入っている(data_info.keys()で確認できる)

f:id:nigimitama:20190810173732p:plain

NOTEは統計表上の欠損値の記号とその意味についての注釈文

# NOTE:欠損値の記号などについての注意書き
data_info["NOTE"]

f:id:nigimitama:20190810173812p:plain

VALUEは目的の統計データ

# VALUE: 統計データ
data_df = pd.io.json.json_normalize(data_info, record_path="VALUE")
data_df.head()

f:id:nigimitama:20190810174118p:plain

データは正規化されていて,例えば性別の「男性」が「01」というコードに変換されたような形で入っている。

CLASS_INFで取得したデータとJOINさせて整形していく必要がある(これが面倒…)

pythonによるRESAS APIからのデータ取得

今までRでしか取得したことがなく,Pythonでは初めてだったのでメモ

使い方の流れ

  1. 利用登録してAPIキーを取得
  2. RESAS-API - API概要を見て,取得するデータを決める
  3. 取得したいデータのURLにGETリクエストを送れば(アクセスすれば)JSON形式でデータを取得できる
    • urllibライブラリのurllib.request.Request()メソッドで取得できる
    • パラメータ(?cityCode=11362みたいなやつ)を付加する場合はurllib.parse.urlencode()を使う
  4. 取得したJSONデータを整形する
    • jsonライブラリのjson.loads()pythonの辞書型に変換
    • pandaspd.io.json.json_normalize()などで辞書型をDataFrame型に変換

コードの例

RESASデータ取得用の関数

import urllib
import pandas as pd
import json
api_key = {"X-API-KEY": "ここに取得したAPIキーを入力"}


def get_resas_data(params: dict, api_path: str, api_key: dict) -> dict:
    api_endpoint = "https://opendata.resas-portal.go.jp/"
    url = api_endpoint + api_path
    url = (url + "?" + urllib.parse.urlencode(params)) if (params != None) else url
    req = urllib.request.Request(url, headers = api_key)
    with urllib.request.urlopen(req) as res:
        response = res.read()
    resas_data_dict = json.loads(response.decode())
    return resas_data_dict

例:都道府県一覧の取得

# 都道府県一覧の取得
resas_data = get_resas_data(params=None, api_path="api/v1/prefectures", api_key=api_key)
prefectures = pd.io.json.json_normalize(resas_data['result'])

f:id:nigimitama:20190804205207p:plain

例:市区町村一覧の取得

cities = pd.DataFrame()
for i in range(1,48):
    resas_data = get_resas_data(params={"prefCode": i}, api_path="api/v1/cities", api_key=api_key)
    cities_i = pd.io.json.json_normalize(resas_data['result'])
    cities = pd.concat([cities, cities_i], axis=0)

f:id:nigimitama:20190804205142p:plain

例:出生数・死亡数/転入数・転出数の取得

都道府県や市区町村とは異なり,こういうデータは返ってくるJSONがネスト(入れ子)していて整形が面倒。

pd.io.json.json_normalize()やpandasの前処理関数(例えばpd.DataFrame.pivot())を駆使して整形していく。

例えば,次のように,リストと文字列が入っている辞書型データは,pd.io.json.json_normalize()できれいにデータフレームに変換できる

# 辞書の入ったリストと文字列を含む辞書型データ
data_dict = {'data': [{'value': 34780, 'year': 1995},
                        {'value': 36035, 'year': 2000},
                        {'value': 46769, 'year': 2005}],
                        'label': '総人口'}
# record_pathにリストのキーを指定,metaにそれ以外のキーを指定
pd.io.json.json_normalize(data_dict, record_path="data", meta=["label"])

f:id:nigimitama:20190804204500p:plain

データの取得

といった知識をもとに書いたコードがこれ

api_path = "api/v1/population/sum/estimate"
params={"prefCode": 13, "cityCode": 13101}
resas_data = get_resas_data(params=params, api_path=api_path, api_key=api_key)
data_list = resas_data["result"]["data"]
for i in range(len(data_list)):
    resas_df_i = pd.io.json.json_normalize(data_list[i], record_path="data", meta=["label"])
    resas_df_i = resas_df_i.pivot(index="year", columns="label", values="value").reset_index()
    resas_df = resas_df_i if i == 0 else pd.merge(resas_df, resas_df_i, on="year")

f:id:nigimitama:20190804204844p:plain

WSLのvimの背景ハイライトがおかしいのを直す方法

問題

WSLでvimを起動し,Ctrl + DCtrl + Uなどを押すとハイライトがバグってしまいます。

f:id:nigimitama:20190727222034p:plain

デフォルトのカラースキームだと目立たないバグですが,自分でデフォルト以外のカラースキームを設定していると色がめちゃくちゃになってしまうこともあります。

解決策

背景色・文字色を指定する

~/.vimrc

highlight Normal ctermfg=white ctermbg=black

のような値を追記するというもの。

これにより,背景色(ctermbg)と文字色(ctermfg)を任意の値に固定します。

:help cterm-colorsコマンドを使うとvimで設定できる色の名前一覧を見ることができるので,これを見ながら自分が使用しているカラースキームに合わせた色に設定するとよさそうです。

(非推奨)Background Color Eraseオプションを無効にする

~/.vimrc

set t_ut=""

を設定する方法。

こちらはカラースキームを気にしなくていい対処法ですが,バグったハイライトが一瞬でて正常に戻る感じなので,バグがチラつく感じ。

一応こちらも書いておきますが,前節の対処法のほうがいいです。

参考