実践編Practice
開発活動の時系列を読み解く
公開リポジトリのコミット履歴を時系列データに変え、開発活動の推移を可視化します。トレンドと季節性を成分に分解し、先の活動量を簡単に予測するところまで、リポジトリマイニング・時系列分析・データ可視化を1本のパイプラインにつなぐ総合演習です。手を動かせるノートブックつきで進めます。
この教材は日本語のみで提供していますThis learning material is available in Japanese only.
課題設定The task
コミット履歴を、時間の流れとして読む
ソフトウェアの開発は、一定のペースでは進みません。リリース前に活動が高まり、長期休暇や年末年始には静かになる。担当者が代わると波形が変わることもあります。こうした開発活動の推移は、リポジトリのコミット履歴という形で残っています。この演習では、公開リポジトリの履歴を時間の流れにそって並べ、そこにある波を読み解きます。
やることは、次の一続きの流れです。
- データ入手:リポジトリマイニングで、各コミットの日時を集める。
- 時系列化:コミットを週ごとに数え、一定間隔の系列にする。コミットのない週は0で埋める。
- 可視化:折れ線でコミット数の推移を描き、移動平均でなだらかにして大きな流れを見る。
- 成分分解:系列をトレンド・季節・残差の3つに分けて、それぞれの動きを取り出す。
- 予測:末尾の数週を検証用に取り分け、ARIMA で先の活動量を見積もって当たり具合を測る。
最後に得られるのは、開発活動の週次系列と、その分解結果、そして先を見積もる小さな予測モデルです。1つの数字の羅列が、波として意味を持ちはじめます。
この演習のパイプライン
使う技術Techniques used
組み合わせる3つのモジュール
この演習は、これまで単体で学んだ次の3モジュールを1本につなぎます。データを整える下ごしらえには pandas を使いますが、その基礎は前処理のモジュールでも扱っています。まだ学んでいない技術があれば、先にそのモジュールで基礎を押さえてから戻ってくると、各ステップの意味がはっきりします。
リポジトリマイニング入門
- PyDriller で公開リポジトリを走査し、各コミットの日時を取り出す。Step 1(データ入手)の土台。
時系列分析
- コミットを週次系列に集計し、トレンド・季節性に分解して、ARIMA で先を予測する。時間を守った評価も学ぶ。Step 2・4・5で使う。
データ可視化
- 折れ線と移動平均で推移を描き、分解結果を図にして波を読み取る。Step 3で使う。
ノートブックで実行Run the notebook
このページのコードを、そのまま動かす
開発活動の時系列ノートブック
このページで読むコードと同じ内容を、上から順に実行できるノートブックにまとめてあります。Colab で開けば、インストール込みで環境構築なしにそのまま動かせます。手元の Jupyter で動かしたい場合は .ipynb をダウンロードしてください。
最初のセルで !pip install pydriller statsmodels pandas matplotlib を実行します。リポジトリの履歴取得はネットワーク接続が必要で、リポジトリの規模により数分かかることがあります。
データ入手:コミット日時を集める
Collect commit timestamps何を・なぜWhat & why
時系列分析の材料は「いつ何回起きたか」です。ここでは各コミットの日時だけを集めます。PyDriller は Git リポジトリを Python から走査するライブラリで、URL を渡すだけで公開リポジトリを一時的に取得し、コミットを1件ずつたどれます。各コミットの author_date(作者が変更を確定した日時)を取り出し、コミット1件を「1」と数えた系列を作ります。
題材には、小さめで実在する公開リポジトリを使います。ここでは PyDriller 自身のリポジトリ ishepard/pydriller を例にします。別のリポジトリで試すのも簡単です(発展課題を参照)。
from pydriller import Repository
import pandas as pd
url = "https://github.com/ishepard/pydriller"
# コミットを1件ずつたどり、作者の日時だけを集める
dates = []
for commit in Repository(url).traverse_commits():
dates.append(commit.author_date)
# 日時を索引にした系列を作る(各コミットを1と数える)
s = pd.Series(1, index=pd.to_datetime(dates, utc=True)).sort_index()
print(len(s), "commits")
print(s.index.min(), "->", s.index.max())
1032 commits
2018-02-15 13:22:31+00:00 -> 2024-05-27 09:41:08+00:00この Step のまとめ
traverse_commits() で履歴を走査し、各コミットの日時を集めて、日時を索引にした系列にしました。値はすべて1です。次はこれを「週ごとの回数」に数えなおして、一定間隔の時系列にします。
時系列化:一定間隔の系列にする
Build an evenly spaced series何を・なぜWhat & why
コミットは不規則な時刻に発生します。時系列分析は一定間隔にそろった系列を前提にするので、まず「週ごとのコミット数」に集計します。pandas の resample は、日時索引を指定した間隔でまとめる仕組みです。"W-MON" は月曜を区切りとした週次を表します。合計をとれば、その週のコミット数になります。
集計後は欠測のないことが大切です。コミットが1件もなかった週は、値がないのではなく「0回」であるべきだからです。resample は空の週にも0を入れますが、意図を明示するために asfreq で0埋めも重ねておきます。月次にしたいときは s.resample("MS").sum() のように区切りを変えるだけです。
# 週次のコミット数に集計する(週は月曜始まり)
weekly = s.resample("W-MON").sum()
# コミットのない週は0にする(欠測週を明示的に0埋め)
weekly = weekly.asfreq("W-MON", fill_value=0)
print(weekly.tail())
print("週数:", len(weekly))
2024-04-29 0
2024-05-06 3
2024-05-13 7
2024-05-20 2
2024-05-27 1
Freq: W-MON, dtype: int64
週数: 328この Step のまとめ
resample("W-MON").sum() で週次のコミット数にまとめ、asfreq で欠測週を0にしました。これで間隔のそろった、欠測のない時系列ができました。次はこの系列を目で見て、大きな流れをつかみます。
可視化:推移を折れ線と移動平均で見る
Visualize the trend何を・なぜWhat & why
まずは素直に折れ線で描きます。週次の生データはぎざぎざしていて、大きな流れが見えにくいものです。そこで移動平均を重ねます。移動平均は、各時点の前後いくつかの値を平均して系列をなだらかにする方法で、短期の細かい上下を打ち消し、長い波を浮かび上がらせます。ここでは8週幅の中心移動平均(center=True)を使います。
import matplotlib.pyplot as plt
# 8週の中心移動平均で平滑化する
ma = weekly.rolling(window=8, center=True).mean()
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(weekly.index, weekly, color="#B7C0BB", linewidth=1, label="週次コミット数")
ax.plot(ma.index, ma, color="#0E6F5C", linewidth=2, label="移動平均(8週)")
ax.set_ylabel("commits / week")
ax.legend()
fig.tight_layout()
plt.show()
(折れ線グラフが表示される。灰色の生系列はぎざぎざと上下し、
緑の移動平均はなだらかな山と谷を描く。活動が高まる時期と
静かな時期が交互に現れているのが読み取れる。)下のデモで、移動平均の窓幅を変えると系列の見え方がどう変わるかを試せます。窓を広げるほどなめらかになりますが、広げすぎると本当の変化まで消えてしまいます。
移動平均で平滑してみる
窓幅(何週分を平均するか)を動かすと、週次コミット数(灰色)に対する移動平均(緑)がどう変わるかを見られます。窓を広げるほど短期の細かい上下が消え、大きな波だけが残ります。これは上のコードの rolling(window=...).mean() と同じ考え方です。
この Step のまとめ
折れ線で推移を描き、移動平均で大きな流れを取り出しました。目で見ると波があるのは分かりますが、「トレンドなのか、季節の繰り返しなのか」までは区別できません。次のステップで、系列を成分に分けて切り分けます。
成分分解:トレンド・季節・残差に分ける
Decompose the series何を・なぜWhat & why
時系列は、いくつかの動きが重なってできていると考えられます。古典的な分解では、系列を次の3つに分けます。
- トレンド(trend):長い目で見た増減の傾向。プロジェクトが盛り上がっているか、落ち着いてきたか。
- 季節(seasonal):一定周期で繰り返すパターン。週次データなら、1年(約52週)ごとの繰り返しを想定できます。
- 残差(residual):トレンドと季節では説明できない、その他の変動。
加法モデルでは、観測値 = トレンド + 季節 + 残差 と考えます。statsmodels の seasonal_decompose は、移動平均でトレンドを推定し、周期ごとの平均で季節成分を求めて、この3つを取り出します。period=52 は1年周期を表します(この分解には最低でも周期の2倍、つまり約2年分の週次データが必要です)。
from statsmodels.tsa.seasonal import seasonal_decompose
# 年周期(約52週)を仮定して加法モデルで分解する
result = seasonal_decompose(weekly, model="additive", period=52)
print("トレンド(末尾):")
print(result.trend.dropna().tail(3).round(2))
result.plot() # 観測・トレンド・季節・残差の4段グラフ
plt.show()
トレンド(末尾):
2023-11-13 2.71
2023-11-20 2.63
2023-11-27 2.58
Freq: W-MON, Name: trend, dtype: float64※ period=52 のため、移動平均で求めるトレンドの両端26週は NaN になります(上の末尾は最後の観測 2024-05-27 の26週前で終わっています)。
この Step のまとめ
seasonal_decompose で系列をトレンド・季節・残差に分けました。トレンドを見れば長期の勢い、季節成分を見れば1年の中での繰り返し(たとえば夏や年末の停滞)が読めます。次は、この流れを使って先の活動量を見積もります。
予測:先の活動量を見積もる
Forecast future activity何を・なぜWhat & why
最後に、先の数週のコミット数を予測します。単純な方法としては移動平均や指数平滑もありますが、ここでは定番の ARIMA を使います。ARIMA(p, d, q) は、過去の値(自己回帰 AR)・差分の階数(I)・過去の予測誤差(移動平均 MA)を組み合わせて先を予測するモデルです。
予測の評価では、時間を守ることが決定的に大切です。未来のデータを使って過去を当てても意味がありません。そこで系列を時系列順に区切り、末尾の12週を検証用に取り分け、それより前だけで学習します。学習した後に12週先を予測し、取り分けておいた実測値と比べて誤差(MAE: 平均絶対誤差)を測ります。
from statsmodels.tsa.arima.model import ARIMA
import numpy as np
# 末尾12週を検証用に取り分ける(時間を守った分割)
train = weekly.iloc[:-12]
test = weekly.iloc[-12:]
# ARIMA(p, d, q) を学習し、12週先まで予測する
model = ARIMA(train, order=(1, 1, 1))
fit = model.fit()
forecast = fit.forecast(steps=12)
mae = np.mean(np.abs(forecast.values - test.values))
print("検証MAE:", round(mae, 2))
print("予測平均:", round(forecast.mean(), 2),
"実測平均:", round(test.mean(), 2))
検証MAE: 2.41
予測平均: 3.18 実測平均: 2.75MAE は「予測が平均して何コミットずれたか」を表します。週あたり数コミットの系列で MAE が 2 前後なら、大きな流れは追えているものの、個々の週のばらつきまでは当てきれていない、という読み方になります。order を変えたり、対数変換や差分を工夫したりすると精度は動きます。単純な予測(前の数週の平均をそのまま先の値にする)と比べて、どれだけ良くなったかを確かめるのが定石です。
時間を守る
時系列の評価では、データをランダムに分けてはいけません。必ず時間で区切り、過去で学習して未来を当てます。ランダム分割は未来の情報が学習に混ざり(リーク)、実力より良く見えてしまいます。
この Step のまとめ
末尾を検証用に取り分け、ARIMA で先の活動量を予測して MAE で評価しました。時間を守った分割にすることで、現実に近い当たり具合を測れます。最後に、この一連の結果から何が言えて、何に注意すべきかを整理します。
考察:波の意味と限界
Read the waves and the limits読み取れることWhat we can read
ここまでで、開発活動の週次系列と、その分解、そして小さな予測モデルが手に入りました。これらから、たとえば次のようなことが読み取れます。
読み取れることSignals
- トレンドの向きで、プロジェクトの勢い(成長・成熟・停滞)が分かる。
- 季節成分で、リリース前の高まりや長期休暇の停滞といった繰り返しが見える。
- 残差の大きな山は、大規模なマージや一時的な集中作業を示すことがある。
注意すべきことCautions
- コミット数は活動の一面にすぎない。1コミットの大きさはまちまちで、量の多さが価値や進捗と直結するわけではない。
- 履歴の書き換え(rebase やスカッシュ)や自動コミットは、系列をゆがめる。
- 季節性は年数が少ないと当てにならない。周期の2倍以上のデータが要る。
- 予測はあくまで過去の延長。方針転換や人員の変化は織り込めない。
まとめ方How to conclude
時系列は、数字を眺めるだけでは見えない「時間の中の構造」を取り出す道具です。トレンドで長い流れを、季節でリズムを、残差で異変を切り分ける。そのうえで、予測は「確実な未来」ではなく「過去がこう続くならこうなる」という仮定つきの見積もりだと心得ておくと、結果を落ち着いて解釈できます。数字の背後にある開発の営みを想像しながら読むことが、いちばん大切です。
発展課題Try more
この演習を広げる
基本の流れをつかんだら、次の課題で分析を深めてみてください。どれもノートブックの一部を書き換えるだけで試せます。
次に挑戦すること
- 別のリポジトリで比べる:
urlを変えて、活発なプロジェクトと落ち着いたプロジェクトの波形を並べる。規模の大きなリポジトリでは取得に時間がかかる点に注意。 - 切り口を変える:全体ではなく、作者別やファイル別・ディレクトリ別に週次系列を作り、誰が・どこが活動の中心かを時系列で見る。
- 異常検知:残差が大きく外れた週を「いつもと違う週」として拾い、その時期に何が起きたか(大規模マージ、リリース)を履歴で確かめる。
- 別の予測手法:季節性を明示的に扱う SARIMA や、休日・トレンド変化に強い Prophet を使い、ARIMA の結果と当たり具合を比べる。
まとめWrap-up
つないで見えた、開発の時間
この演習では、バラバラのコミットを時間の流れとして読み解きました。改めて振り返ると、次の技術を1本につないだことになります。
- リポジトリマイニング:PyDriller でコミット日時という素材を手に入れた。
- 時系列分析:週次に集計し、トレンド・季節・残差に分解し、ARIMA で先を予測した。
- データ可視化:折れ線と移動平均で、波の大きな流れを目で確かめた。
ひとつの数字の列が、トレンド・季節・残差という構造を持った「時間の物語」になりました。各技術の詳しい理論やAPIは、下の各モジュールに戻ると確認できます。
使ったモジュール
次の演習・段階へ
- 次の演習:A/Bテストで効果を検証する(演習一覧)
- ほかの実践編で、別の切り口のパイプラインも試す。
- ノートブックの
urlを気になるリポジトリに変えて、その開発の波を読んでみる。
おつかれさまでした
ノートブックを開き、url を自分の気になるリポジトリに変えて、もう一度上から実行してみてください。集計の間隔(週次・月次)や移動平均の窓幅、ARIMA の order を少し変えるだけで、見える波がどう動くかを確かめると、時系列分析の勘所がつかめます。