第2回 金融データ活用チャレンジ 米国国勢調査局データの特徴量化
一般社団法人 金融データ活用推進協会(FDUA)が主催する「第2回 金融データ活用チャレンジ」に参加しました。「金融データの活用可能性を深化させる業界を挙げた超実践プログラム」ということだったので、米国金融機関のデータサイエンティストになったつもりで、米国国勢調査局データを特徴量化し、より精緻な予測モデル構築を試みました。その中で工夫したことや考えたことを共有できたらと思います。
1. 課題
「第2回 金融データ活用チャレンジ」の課題は、米国小企業庁のデータをもとに生成した人工的なデータを使って、企業向けローンの返済可否予測を行うというもの。企業に関する情報(所在地や産業分類、従業員数等)やローンに関する情報(期間や金額、保証金額等)など19個の特徴量が配布されました。これに加えてオープン且つ無料の外部データも利用可能です。
所在地データには州名と都市名の2つがあります。人口密度や貧困率といったデータを所在地データと紐づけることができれば、より精緻な予測モデルを構築できるかもしれません。
2. データクレンジング
データ分析実務あるあるですが、今回配布された所在地データは表記ゆれなどがある汚いデータだったため、1つ1つのデータをきれいにしていきます。具体的には、スペルミスの修正、都市名のフォーマット統一(最初の文字を大文字、残りを小文字)、括弧内のテキストの削除、略語の展開(N.はNorthなど)、余分な空白の削除を行います。
# スペルミスの修正
corrections = {
'ALSIO VIEJO': 'ALISO VIEJO',
'ALTANTA': 'ATLANTA',
'CENTAL FALLS': 'CENTRAL FALLS',
'COLORADO SRINGS': 'COLORADO SPRINGS',
'ELK GROVE VILLIAGE': 'ELK GROVE VILLAGE',
'FOUNTAIN HILL': 'FOUNTAIN HILLS',
'FOUNTAIN HILLSS': 'FOUNTAIN HILLS',
'Glemdale': 'Glendale',
'GODDLETTSVILLE': 'GOODLETTSVILLE',
'LAS CRUSES': 'LAS CRUCES',
'Lomg Island City': 'Long Island City',
'Mount Larel': 'Mount Laurel',
'NEWHEBRON': 'NEW HEBRON',
'NESLONVILLE': 'NELSONVILLE',
'NORTH BRUNWICK': 'NORTH BRUNSWICK',
'PFUGERVILL': 'PFLUGERVILLE',
'PHILADELHIA': 'PHILADELPHIA',
'PLEANSANTON': 'PLEASANTON',
'PORTLAMD': 'PORTLAND',
'QUINCYL': 'QUINCY',
'SARATOGA SPRING': 'SARATOGA SPRINGS',
'SARATOGA SPRINGSS': 'SARATOGA SPRINGS',
'SCHENECTEDY': 'SCHENECTADY',
'SHACKOPEE': 'SHAKOPEE',
'Stewartstwon': 'Stewartstown',
'WARNER ROBBINS': 'WARNER ROBINS',
'WEEHAKEN': 'WEEHAWKEN',
'WISCONSIN RAPIDSS': 'WISCONSIN RAPIDS',
}
df['City'] = df['City'].replace(corrections)
# 都市名のフォーマット統一(最初の文字を大文字、残りを小文字)
df['City'] = df['City'].str.title()
# 括弧内のテキストの削除
df['City'] = df['City'].str.replace('\(.*', '', regex=True)
# 略語の展開
abbreviations = {
'Co\.': 'County ',
'E\.': 'East ',
'Ft\.': 'Fort ',
'Mt\.': 'Mount ',
'N\.': 'North ',
'S\.': 'South ',
'So\.': 'South ',
'Southth\.': 'Southern ',
'St\.': 'Saint ',
'Twp\.': 'Township ',
'W\.': 'West '
}
for abbr, full in abbreviations.items():
df['City'] = df['City'].str.replace(abbr, full, regex=True)
# 余分な空白の削除
df['City'] = df['City'].str.replace(r'\s+', ' ', regex=True)
df['City'] = df['City'].str.strip()
3. 緯度経度
きれいになった所在地データをもとに緯度経度データを取得します。無料で緯度経度データを取得できるGeoPyというライブラリを使用します。データ取得に時間がかかるので、重複する所在地データは削除しておき、後で統合することにしました。
# 州と都市から緯度と経度を取得する関数
def get_lat_lng(city, state, attempts=5, timeout=10):
geolocator = Nominatim(user_agent='fdua_competition')
try:
# 州と都市を指定して地理的位置情報を取得
location = geolocator.geocode(f"{city}, {state}", timeout=timeout)
if location:
return location.latitude, location.longitude
else:
return None, None
except GeocoderUnavailable:
if attempts > 0:
time.sleep(3) # 3秒待機してからリトライ
return get_lat_lng(city, state, attempts - 1, timeout)
else:
print(f'Error: {city}, {state}') # エラーメッセージを出力
return None, None
# 緯度と経度の取得
lat_lng = []
for index, row in tqdm(df.iterrows(), total=df.shape[0]):
lat_lng.append(get_lat_lng(row['City'], row['State']))
df['Lat'], df['Lng'] = zip(*lat_lng)
4. County(郡) / 米国国勢調査局データ
ローン対象企業がどのCounty(郡)に属するのか調べます。それにはTIGER/Line Shapefilesと呼ばれる米国国勢調査局が提供する地理的データを利用します。こちらのZipファイルとして圧縮されている複数のファイルを解凍して同じフォルダに格納します。
地理的データはGeoPandasというライブラリを使うことで簡単に扱えます。次のコードのようにPandasっぽくファイルを読み込んだり、表示したりすることができます。
# TIGER/Line Shapefilesの読込み
path = os.path.join(DIR, f'input/tl_2015_us_county/tl_2015_us_county.shp')
gdf = gpd.read_file(path)
display(gdf)
特定の緯度経度がどのCounty(郡)に含まれるのか、地理的データを使って調べます。
# County(郡)の特定
def find_county(lat, lng, gdf):
# 緯度と経度からポイントを生成
point = Point(lng, lat)
# ポイントに一致する可能性のあるCounty(郡)のインデックスを取得
possible_matches_index = list(gdf.sindex.intersection(point.bounds))
# 取得したインデックスに基づいて、可能性のあるCounty(郡)を取得
possible_matches = gdf.iloc[possible_matches_index]
# ポイントを含むCounty(郡)を正確に特定
precise_matches = possible_matches[possible_matches.contains(point)]
# 正確な一致が見つかった場合
if not precise_matches.empty:
row = precise_matches.iloc[0] # 最初の一致した郡を取得
# GEOID、名前、土地面積を辞書形式で返す
return {'GEOID': '0500000US' + row['GEOID'], 'NAME': row['NAME'], 'ALAND': row['ALAND']}
# 一致する郡が見つからなかった場合、全ての値をNoneとして返す
return {'GEOID': None, 'NAME': None, 'ALAND': None}
# DataFrameの各行に対してfind_county関数を適用し、County(郡)の情報を取得
county_info = df.apply(lambda x: find_county(x['Lat'], x['Lng'], gdf), axis=1)
# GEOID、名前、土地面積を新たなカラムとしてDataFrameに追加
df['Geoid'] = county_info.apply(lambda x: x['GEOID']) # GEOID
df['Name'] = county_info.apply(lambda x: x['NAME']) # County(郡)の名前
df['Aland'] = county_info.apply(lambda x: x['ALAND']) # 土地面積
地理的データにはCounty(郡)を示すID(GEOID)やCounty(郡)名、土地面積が含まれています。GEOIDは、米国国勢調査局が定めた地理的な場所を一意に識別するためのコードです。GEOIDを使用することで、他のデータの統合が簡単にできるようになります。また、土地面積は特徴量作成に利用することにします。
5. Comparative Economic Characteristics (CP03) / 米国国勢調査局データ
County(郡)の経済的な特徴を示すデータを準備します。米国国勢調査局のウェブサイトには様々な有益なデータがありますが、ここではComparative Economic Characteristics (CP03)というデータを使います。
先ほどの地理的データと違ってcsv形式なので扱いは簡単ですが、非常に多くの指標があり、指標の意味を確認するのが大変です・・・。
# CP03の読込み
cp03 = pd.read_csv(os.path.join(DIR, 'input/acscp5y2015_cp03/ACSCP5Y2015.CP03-Data.csv'), low_memory=False, skiprows=1)
cp03
County(郡)のデータテーブルとCP03のデータテーブルをGEOIDを使って統合します。CP03というデータテーブルには多くの指標が含まれるので、以下の指標を使うこととします。
16歳以上の人口
失業率
世帯収入の中央値
給付を受けた世帯数
貧困率
また、「土地面積」と「16歳以上の人口」から「人口密度」を計算して特徴量として使用することにします。
# 使用するカラムの選択
cols = [
'Geography', # 地理的区分
'2011-2015 Estimates!!EMPLOYMENT STATUS!!Population 16 years and over', # 16歳以上の人口
'2011-2015 Estimates!!EMPLOYMENT STATUS!!Civilian labor force!!Unemployment Rate', # 失業率
'2011-2015 Estimates!!INCOME AND BENEFITS (IN 2015 INFLATION-ADJUSTED DOLLARS)!!Total households!!Median household income (dollars)', # 世帯収入の中央値
'2011-2015 Estimates!!INCOME AND BENEFITS (IN 2015 INFLATION-ADJUSTED DOLLARS)!!With Food Stamp/SNAP benefits in the past 12 months', # 給付を受けた世帯数
'2011-2015 Estimates!!PERCENTAGE OF FAMILIES AND PEOPLE WHOSE INCOME IN THE PAST 12 MONTHS IS BELOW THE POVERTY LEVEL!!All people', # 貧困率
]
cp03 = cp03[cols] # 選択したカラムでデータフレームを更新
cp03.columns = ['Geoid', 'Pop16Plus', 'UnempRate', 'MedHhInc', 'FoodStamp', 'PovRate'] # カラム名の変更
# データフレームの結合
df = pd.merge(df, cp03, on=['Geoid'], how='left')
# 欠損値を州ごとの平均値で補完し、それでも欠損値がある場合は0で補完
for col in ['Pop16Plus', 'UnempRate', 'MedHhInc', 'FoodStamp', 'PovRate']:
df[col] = df[col].fillna(df.groupby('State')[col].transform('mean')).fillna(0)
# 人口密度(16歳以上の人口 / 土地面積)
df['PopDensity'] = df['Pop16Plus'] / (df['Aland'] + 1)
# 人口密度の欠損値を0で補完
df['PopDensity'] = df['PopDensity'].fillna(0)
以上の処理をまとめたノートブックはこちらです。
6. 結果と考察
(1) 結果
「第2回 金融データ活用チャレンジ」は2月15日(木)に終了。順位は155位でした・・・。_| ̄|○
データクレンジングや米国国勢調査局データの特徴量化をやればやるほどスコアが下がってしまい・・・。このアプローチは良くなかったようです。特徴量化のプロセスが楽しすぎて、予測に有効なアプローチか否かという検証であったり、頭を切り替えて別のアプローチを試すといったことが疎かになっていました。
(2) 考察
人工的なデータを使ったデータ分析コンテストでは外部データを活用してもうまくいかないことが多いと考えた方が良いのかもしれません。
このようなデータ生成では敵対的生成ネットワーク(GAN)が使用されます。GANは、2つのニューラルネットワーク、「生成モデル」と「識別モデル」が互いに競争しながら学習を進めることで、実データと見分けがつかないほど高品質なデータの生成を実現しようというものです。
仮に因果関係のある外部データを活用して予測モデルを構築したとしても、データ生成においてそのような因果関係が考慮されていないのであれば、全く意味のない外部データということになりそうです。また、生成データを用意する主催者側がそのような因果関係を考慮してデータを生成しようとしても、実データと因果関係のある外部データは無数にあることから、そのような対応を行うとは考えにくいです。
データの揺らぎや欠損値などに対するデータクレンジングについても、生成モデル起因のものか、生成したデータに事後的に操作を加えたものなのかによって、有効なアプローチは変わってくるのかもしれません。前者の場合はデータクレンジングを行うことで有益な情報が少なくなる恐れがあります。
7. 終わりに
精緻な予測モデル構築にはつながりませんでしたが、上記のアプローチなどを通して学べたことが多いデータ分析コンテストだったと思います。主催して頂いた金融データ活用推進協会の関係者の皆様には感謝の言葉しかありません。
実際のデータにはその背後にある複雑な事象を読み解いていくという楽しみがありますが、生成された人工データにはそのような複雑な事象が必ずしも再現されません。このため、データ分析コンテストで人工データを使う場合、有効な分析アプローチが出尽くしてしまうと、そこから先は運による要素が大きくなってしまうという弊害がありそうです。また、データ分析コンテストを通した学びの機会(今回の場合は金融ビジネスに対する学びの機会)が限定的なものになっていないか心配だったりもします。なかなか難しいとは思いますが、次回、「第3回 金融データ活用チャレンジ」では、日本の金融ビジネスに関連した実データ/実データに近い人工データを用いた課題を期待したいです。次回も参加したいと思います。
この記事が気に入ったらサポートをしてみませんか?