正式名称:Python mini-Hack-a-thon
第1回(2010年9月25日)の説明:
2009年の6月からZope/Ploneの開発者で集まってもくもくと開発したり色々相談したりとかやっていたんですが、全然Zope/Ploneに限定したことをやっていない気がしてきたので、試しに名前を変えてみました。スプリントのゆるい版みたいな感じで各自自分でやりたいことを持ってきて、勝手に開発を進めています。参加費は無料です。
第100回(2019年6月22日)の説明:
基本的に毎月開催です。スプリントのゆるい版みたいな感じで各自自分でやりたいことを持ってきて、勝手に開発を進めています。参加費は無料です。初めての方も常連さんもぜひご参加ください。
参加者のバックグランドはweb開発からデータ分析、機械学習まで幅広く、レベルも初心者から猛者まで様々。上の説明にもあるように、各自が「もくもく」と自習したり開発したりする会。言語の縛りはないが、Pythonで何かする人が多い。1日の終わりに成果発表LTがあり、毎回楽しい。2019年6月22日(土)に100回目を迎えた。
以下では、その100回の歴史をデータから振り返る。なお、2019年6月22日の第100回にて「もくもく」した成果にコメントやコードを追加しリファクタリングしたものである。
import requests
import json
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime, timedelta, timezone
%matplotlib inline
現在はconnpassだが、第12回まではATNDにて募集されていた。「Python mini Hack-a-thon」というキーワードと主催者名「takanori」を指定して検索してみる。
res_atnd = requests.get('http://api.atnd.org/events/?keyword=Python,mini,Hack-a-thon&owner_nickname=takanori&count=100&format=json')
res_atnd.status_code
res_atnd_dict = json.loads(res_atnd.text)
#res_atnd_dict
検索結果は過不足ないようなので、pandasのDataFrameに加工する。
pyhack_atnd = pd.DataFrame()
for i, e in enumerate(res_atnd_dict['events']):
pyhack_atnd = pd.concat([pyhack_atnd, pd.DataFrame(e['event'], index=['event_id'])])
i
加工結果を確認する。
pyhack_atnd.head()
pyhack_atnd.title
pyhack_atnd.dtypes
pyhack_atnd.describe(include='all')
第5回の雪山合宿2011の参加者数が0になっている。関係者によると、この回の募集はこくちーずで行われたとのこと。
pyhack_atnd.loc[pyhack_atnd.accepted == 0,]
pyhack_atnd.loc[pyhack_atnd.event_id==11947, 'accepted'] = 7
pyhack_atnd.sort_values('started_at').reset_index(drop=True).head()
第13回以降。試行錯誤の結果「python mini Hack-a-thon」というキーワードとグループID「14」を指定して検索すると漏れがない。
res_cnps = requests.get('https://connpass.com/api/v1/event/?keyword=Python,mini,Hack-a-thon&series_id=14&count=1000')
res_cnps.status_code
res_cnps_dict = json.loads(res_cnps.text)
#res_cnps_dict
ATNDのデータと同様にpandasのDataFrameに加工する。
pyhack_cnps_org = pd.DataFrame()
for i, e in enumerate(res_cnps_dict['events']):
pyhack_cnps_org = pd.concat([pyhack_cnps_org, pd.DataFrame(e)])
i
pyhack_cnps_org.head()
pyhack_cnps_org.title
pyhack_cnps_org.dtypes
加工時に発生した重複行(series
が原因)とPyHack以外のイベントを除く。
ちなみに、もくもく会は「Python mini Hack-a-thon」、合宿は「Python mini hack-a-thon」と表記揺れがある(Hackとhack)。
pyhack_cnps = pyhack_cnps_org.loc[(pyhack_cnps_org.series==14) &
(pyhack_cnps_org.hash_tag == 'pyhack') &
pyhack_cnps_org.title.apply(lambda x: re.search('Python mini (H|h)ack-a-thon', x) != None)]\
.sort_values('started_at').reset_index(drop=True)
pyhack_cnps.head()
確認のため、イベントのタイトルを表示してみる。
pyhack_cnps.title
各カラムの概要を確認する。
pyhack_cnps.describe(include='all')
APIの仕様書だけでなく、加工後のデータのカラム名も比較する(APIの仕様書を比べて、名前が同じ変数はデータ内容も同じであることは確認済み)。
pyhack_atnd.columns
pyhack_cnps.columns
共通するものを抽出し、それらだけを残す。
cols = pyhack_cnps.columns[[x in pyhack_atnd.columns.to_list() for x in pyhack_cnps.columns.to_list()]].to_list()
cols
pyhack_all = pd.concat([pyhack_atnd.loc[:, cols], pyhack_cnps.loc[:, cols]])\
.sort_values('started_at').reset_index(drop=True)
結合結果を確認する。
pyhack_all.head()
pyhack_all.dtypes
pyhack_all.describe(include='all')
pyhack_all.title
開始日時started_at
などが文字列になっているのでdatetimeに変換しておく。
# 開始日時(東京時間)
pyhack_all['started_at'] = pd.to_datetime(pyhack_all.started_at, utc=True).dt.tz_convert('Asia/Tokyo')
# 終了日時(東京時間)
pyhack_all['ended_at'] = pd.to_datetime(pyhack_all.ended_at, utc=True).dt.tz_convert('Asia/Tokyo')
# 変更日時(東京時間)
pyhack_all['updated_at'] = pd.to_datetime(pyhack_all.updated_at, utc=True).dt.tz_convert('Asia/Tokyo')
開始日時から年、月、日をそれぞれ切り出す。
pyhack_all['started_at_year'] = pd.to_numeric(pyhack_all.started_at.dt.strftime('%Y'))
pyhack_all['started_at_month'] = pd.to_numeric(pyhack_all.started_at.dt.strftime('%m'))
pyhack_all['started_at_day'] = pd.to_numeric(pyhack_all.started_at.dt.strftime('%d'))
データの期間を確認すると、101回目(2019年7月12日からの夏山合宿)以降も含まれている。
min(pyhack_all.started_at)
max(pyhack_all.started_at)
101回目以降(最初の分析時点では未来のイベント)を除く。
# 現在の日時(東京時間)
now = pd.to_datetime(datetime.utcnow(), utc=True).tz_convert('Asia/Tokyo')
日付を指定する場合は以下のようにする。ただし、時刻がUTC(世界標準時 = ロンドンの冬時間)で午前0時(東京では朝9時)になるので、厳密には時刻も指定する方がよい。
pd.to_datetime('2019-06-22', utc=True).tz_convert('Asia/Tokyo')
pyhack = pyhack_all.loc[pyhack_all.started_at <= now]
pyhack.tail()
回数を表すカラムを追加する(ほとんどの合宿には回数が明示されていないので)。
pyhack = pyhack.assign(number=pyhack.index + 1)
pyhack.head()
イベントの種類を表すカラムを追加する。
pyhack = pyhack.assign(type=['もくもく会' if event == None
else event[0] for event in pyhack.title.apply(lambda x: re.search('(夏|雪)山合宿', x))])
pyhack.head()
別ファイル(darkskykey.py
)に記述したDark Sky APIのシークレットキーを読み込む。
from darkskykey import *
res_weather_001 = requests.get(f"https://api.darksky.net/forecast/{darkskykey}/35.6995556,139.7807378,2010-09-25T11:00:00+0900?exclude=flags&lang=ja")
res_weather_001_dict = json.loads(res_weather_001.text)
#res_weather_dict
res_weather_001_dict['currently']['summary']
res_weather_005 = requests.get(f"https://api.darksky.net/forecast/{darkskykey}/36.5531173,138.3497436,2011-02-25T12:00:00+0900?exclude=flags&lang=ja")
json.loads(res_weather_005.text)['currently']['summary']
菅平(or長野)と東京が区別されているかの確認。
res_weather_005_tokyo = requests.get(f"https://api.darksky.net/forecast/{darkskykey}/35.6995556,139.7807378,2011-02-25T12:00:00+0900?exclude=flags&lang=ja")
json.loads(res_weather_005_tokyo.text)['currently']['summary']
weather = []
for i, dt in enumerate(pyhack.started_at):
lat = pyhack.lat[i]
lon = pyhack.lon[i]
dt_str = re.sub('\s', 'T', str(dt)) # datetime型をstr型に変換して、APIのフォーマットに合わせる。
res_weather = requests.get(f'https://api.darksky.net/forecast/{darkskykey}/{lat},{lon},{dt_str}?exclude=flags&lang=ja')
weather.append(json.loads(res_weather.text)['currently']['summary'])
# 最初の10回分を確認。
weather[0:10]
pyhack['weather'] = pd.Series(weather)
pyhack.head()
参加者数(キャンセル待ち以外)accepted
を定員(合計)limit
で割ったもの。参加者タイプが複数ある場合、キャンセル待ちが発生していても充足率が100%未満ということが起こり得る。
pyhack = pyhack.assign(filled=lambda x: x.accepted / x.limit)
pyhack.head()
pyhack.dtypes
pyhack.describe(include='all')
当初は夏山合宿がなかったので、雪山合宿の方が夏山より2回多い。
type_counts = pyhack.type.value_counts()
type_counts
type_counts.plot(kind='bar')
曇り(薄曇り含む)が多い。ただし、気象用語の定義は実感とは異なるようなので、晴れていると感じても分類上は「薄曇り」である可能性がある。
pyhack.weather.value_counts()
会場名の文字列を整形していないので、表記揺れが別々にカウントされている。例えば、マウンテンパパは本来は16回である。
pyhack.place.value_counts()
住所の方が表記揺れは少ない。
pyhack.address.value_counts()
緯度経度は桁数を揃えれば、一意にカウントできそうだが、一部は取得元が異なるせいか同じ住所でも若干のズレがある模様。
pd.Series(pyhack.lat + '-'+ pyhack.lon).value_counts()
月日をカウントすると偏りはないが、合宿の開催される冬場と7月には複数回開催された日がある。
pyhack.started_at.dt.strftime('%m月%d日').value_counts()
月別に見ると9月がやや少ない(6回)。
pyhack.started_at_month.value_counts().sort_index()
2012年から2014年までは9月に開催されていなかったためと思われる(一方、2012年と2013年は8月に2回)。
pd.pivot_table(pyhack, values='number', index=['started_at_year'], columns=['started_at_month'],
aggfunc=len, fill_value=0, margins=True)
日だけをカウントすると、22日が最多で月の中頃が多い模様(週末開催なので月の両端にはなりにくい?)。
pyhack.started_at_day.value_counts().sort_index()
pyhack.started_at_day.hist()
同じ日に複数の記念日が設定されている。
pyhack.catch
「、」で切り分けて数える。
pd.Series(re.sub('\([^\(\)]+\)', '', '、'.join(pyhack.catch)).split('、')).value_counts()
「玉の輿の日」(という記念日を作るのはどうかと思うが)と「野菜の日」を確認すると、合宿の期間中に該当したため他より多くなったらしい。
pyhack.loc[pyhack.catch.apply(lambda x: re.search('玉の輿の日', x) != None)]
pyhack.loc[pyhack.catch.apply(lambda x: re.search('野菜の日', x) != None)]
その他、気になる人がいそうな記念日を調べてみる。
pyhack.loc[pyhack.catch.apply(lambda x: re.search('カレーの日', x) != None)]
pyhack.loc[pyhack.catch.apply(lambda x: re.search('地ビールの日', x) != None)]
pyhack.loc[pyhack.catch.apply(lambda x: re.search('世界ビールの日', x) != None)]
pyhack.loc[pyhack.catch.apply(lambda x: re.search('クラシック音楽の日', x) != None)]
今日(6月22日)は何の日?
pyhack.loc[pyhack.started_at.dt.strftime('%Y-%m-%d') == '2019-06-22', 'catch']
pyhack.accepted
これをグラフにする。
# カラーマップ
colmap = plt.cm.get_cmap('winter')
# 合宿を表すフラグ
camp_idx = pyhack.type.isin(['夏山合宿', '雪山合宿'])
fig = plt.figure(figsize=(16,6), dpi=300)
ax = fig.add_subplot(111)
# 棒グラフを描く。
ax.bar(x=pyhack.index, height=pyhack.accepted,
color=[colmap(x) for x in pyhack.filled], alpha=0.7, label='参加') # バーの色は充足率を表す。
ax.bar(x=pyhack.index, height=pyhack.waiting,
bottom=pyhack.accepted, color='red', alpha=0.7, label='キャンセル待ち') # 参加者の上にキャンセル待ちを乗せる。
ax.scatter(x=pyhack.index[camp_idx], y=(pyhack.accepted+pyhack.waiting)[camp_idx],
marker='*', color='orange', label = '合宿') # 合宿の回にマークを付ける。
plt.xticks(np.arange(4,100,5), np.arange(4,100,5)+1)
plt.title('Python mini Hack-a-thon 参加者数推移')
ax.legend()
# 凡例のカラーバーを表示させる。
plt.scatter(x=pyhack.index, y=pyhack.accepted,
c=pyhack.filled, cmap='winter', s=0, vmin=0, vmax=1) # ダミー(サイズ0)
cbar = plt.colorbar()
cbar.set_label('充足率')
cbar_ticks = [str(int(float(t * 100)))+'%' for t in cbar.get_ticks()]
cbar.ax.set_yticklabels(cbar_ticks)
plt.show()
第55回以降はほぼ毎回キャンセル待ちが発生している。
# TODO: Bokehを用いて上のグラフをインタラクティヴにする。
第100回にてもくもくした成果をLTとして当日発表したところ好評を得た。ノートブックの仕上げと公開は第101回の夏山合宿になってしまったが、「歴史」をデータで示すことができ、当日のもくもくとその後の追加作業を含めて学ぶことが多かった。
このようなイベントを継続して開催されている運営の方々には感謝しかない。これからもできる限り参加して、楽しくもくもくしていきたい。今後ともよろしくお願いします!