実践編Practice
OSSプロジェクトの健康診断
公開されている OSS リポジトリの開発履歴を掘り起こし、前処理・ネットワーク分析・機械学習を1本のパイプラインにつないで、変更が集中しやすいファイル(ホットスポット)を予測します。第1・第2段階で単体で学んだ技術を、現実的な課題を解くために組み合わせる総合演習です。手を動かせるノートブックつきで進めます。
この教材は日本語のみで提供していますThis learning material is available in Japanese only.
課題設定The task
開発履歴から、壊れやすい場所を見つける
あなたはある OSS プロジェクトの保守を引き継いだところだと想像してください。ソースは大量にあり、どこから手を付ければよいか分かりません。経験や勘に頼る前に、まずデータでプロジェクトの状態を読み解きたい。とりわけ知りたいのは、変更が集中しやすいファイル(ホットスポット)です。何度も直され、多くの人が触るファイルは、不具合が生まれやすく、レビューやテストを厚くする価値が高いからです。
この演習では、公開リポジトリの開発履歴を素材に、次の一続きの流れを実際に組み立てます。
- データ入手:リポジトリマイニングで、コミット履歴(誰がいつどのファイルを変えたか)を取り出す。
- 前処理:コミットとファイルの表に整え、変更回数・コードチャーン・関与作者数といった特徴量にする。
- ネットワーク分析:同じコミットで一緒に変わったファイルを辺で結ぶ「共変更ネットワーク」を作り、結び目になっているファイルを中心性で測る。
- 機械学習:これらの特徴量から、ホットスポットかどうかを分類する。
最終的に得られるのは、ファイルごとの特徴量表と、ホットスポットを当てる分類器、そして「何が言えて、何に注意すべきか」という読み解きです。ひとつの技術だけでは見えないものが、つなぐと見えてきます。
この演習のパイプライン
使う技術Techniques used
組み合わせる4つのモジュール
この演習は、第1・第2段階で単体で学んだ次の4モジュールを1本につなぎます。まだ学んでいない技術があれば、先にそのモジュールで基礎を押さえてから戻ってくると、各ステップの意味がはっきりします。
リポジトリマイニング入門
- PyDriller でコミット履歴を走査し、誰がいつどのファイルを変えたかを取り出す。Step 1(データ入手)の土台。
開発データの前処理
- 生の履歴を pandas でファイル単位の表に整え、欠損を除き、変更回数やチャーンを特徴量化する。Step 2で使う。
ネットワーク分析
- 共変更ネットワークを NetworkX で構築し、次数中心性・媒介中心性で結び目になるファイルを測る。Step 3で使う。
機械学習
- 特徴量からホットスポットを分類し、適合率・再現率・F値で評価する。Step 4で使う。
ノートブックで実行Run the notebook
このページのコードを、そのまま動かす
OSSプロジェクトの健康診断ノートブック
このページで読むコードと同じ内容を、上から順に実行できるノートブックにまとめてあります。Colab で開けば、インストール込みで環境構築なしにそのまま動かせます。手元の Jupyter で動かしたい場合は .ipynb をダウンロードしてください。
最初のセルで !pip install pydriller pandas networkx scikit-learn matplotlib を実行します。リポジトリの履歴取得はネットワーク接続が必要で、リポジトリの規模により数分かかることがあります。
データ入手:コミット履歴を掘り起こす
Collect commit history何を・なぜWhat & why
まず素材となる開発履歴を集めます。PyDriller は Git リポジトリを Python から走査するライブラリで、URL を渡すだけで公開リポジトリを一時的に取得し、コミットを1件ずつたどれます。各コミットからは、ハッシュ・作者・日時に加えて、そのコミットで変更された各ファイルの追加行数・削除行数が得られます。これらを1行=「あるコミットでのあるファイルの変更」として集め、表にします。
題材には、小さめで実在する公開リポジトリを使います。ここでは PyDriller 自身のリポジトリ ishepard/pydriller を例にします。別のリポジトリで試すのも簡単です(発展課題を参照)。
from pydriller import Repository
import pandas as pd
url = "https://github.com/ishepard/pydriller"
# コミットを1件ずつたどり、変更ファイルごとに1行を作る
rows = []
for commit in Repository(url).traverse_commits():
for mod in commit.modified_files:
path = mod.new_path or mod.old_path
if path is None:
continue
rows.append({
"commit": commit.hash[:8],
"author": commit.author.email,
"date": commit.author_date,
"file": path,
"added": mod.added_lines,
"deleted": mod.deleted_lines,
})
df = pd.DataFrame(rows)
print(df.shape)
print(df.head())
(2118, 6)
commit author date file added deleted
0 a1b2c3d4 davide.spadini@example.org 2018-02-15 14:22:31+01:00 pydriller/repository.py 58 0
1 a1b2c3d4 davide.spadini@example.org 2018-02-15 14:22:31+01:00 pydriller/git.py 34 2
2 1f4e9c07 davide.spadini@example.org 2018-02-16 09:05:12+01:00 tests/test_repository.py 41 1
3 1f4e9c07 davide.spadini@example.org 2018-02-16 09:05:12+01:00 pydriller/repository.py 7 3
4 9d2b7a55 contributor@example.org 2018-03-02 18:40:55+01:00 pydriller/domain/commit.py 22 4※出力例(ハッシュ・メールはダミー)。実際の値は実行時のリポジトリの状態によって変わります。
この Step のまとめ
Repository(url).traverse_commits() で履歴を走査し、変更ファイル単位の生データを表にしました。ここでは「1行 = あるコミットでの1ファイルの変更」という長い表になっています。次はこれをファイル単位に集計します。
前処理:ファイル単位の特徴量にする
Preprocess into per-file features何を・なぜWhat & why
生の履歴のままでは分析しづらいので、pandas でファイル単位に集計します。ここでは学習しやすいように Python ファイル(.py)だけに絞り、パスが取れない変更(削除など)を除きます。そのうえで、各ファイルについて次の特徴量を作ります。
- changes:そのファイルが変更されたコミット数(変更回数)。ホットスポットの目安になります。
- churn:コードチャーン。追加行と削除行の合計で、変更の量を表します。
- authors:そのファイルに関与した作者の数。多いほど所有が分散しています。
# パスが取れない変更を除き、Pythonファイルに絞る
df = df[df["file"].notna()]
df = df[df["file"].str.endswith(".py")]
# コードチャーン = 追加行 + 削除行
df["churn"] = df["added"] + df["deleted"]
# ファイル単位に集計して特徴量表にする
feat = df.groupby("file").agg(
changes=("commit", "nunique"),
churn=("churn", "sum"),
authors=("author", "nunique"),
).reset_index()
feat = feat.sort_values("changes", ascending=False)
print(feat.head())
file changes churn authors
0 pydriller/repository.py 146 4820 18
1 pydriller/git.py 98 3110 15
2 pydriller/domain/commit.py 74 2185 12
3 tests/test_repository.py 61 1740 9
4 pydriller/utils/conf.py 37 690 6この Step のまとめ
長い表を groupby でファイル単位に畳み込み、changes・churn・authors という3つの特徴量にしました。上位のファイルは変更回数もチャーンも大きく、直感とも合います。次に、ファイル同士の「つながり」を特徴量に加えます。
ネットワーク分析:共変更の結び目を測る
Co-change network何を・なぜWhat & why
ファイルは単独で変わるだけでなく、しばしば一緒に変わります。仕様変更で本体とテストが同時に直る、といった具合です。同じコミットで一緒に変更されたファイル同士を辺で結ぶと、プロジェクトの構造を映すネットワークができます。これを共変更ネットワーク(co-change network)と呼びます。
NetworkX でこのグラフを作り、各ファイルの次数中心性(多くのファイルと共に変わるほど高い)と媒介中心性(異なる部分をつなぐ橋になっているほど高い)を測ります。中心性が高いファイルは、変更の影響が広がりやすい「結び目」です。
import networkx as nx
from itertools import combinations
# 同じコミットで一緒に変わったファイルを辺で結ぶ
G = nx.Graph()
for commit, grp in df.groupby("commit"):
files = sorted(grp["file"].unique())
for a, b in combinations(files, 2):
if G.has_edge(a, b):
G[a][b]["weight"] += 1
else:
G.add_edge(a, b, weight=1)
# 次数中心性と媒介中心性
degree = nx.degree_centrality(G)
between = nx.betweenness_centrality(G)
cent = pd.DataFrame({
"file": list(degree.keys()),
"degree": list(degree.values()),
"betweenness": [between[f] for f in degree],
})
print("nodes:", G.number_of_nodes(), "edges:", G.number_of_edges())
print(cent.sort_values("betweenness", ascending=False).head())
nodes: 214 edges: 1180
file degree betweenness
0 pydriller/repository.py 0.375587 0.284120
1 pydriller/git.py 0.286385 0.176433
2 pydriller/domain/commit.py 0.234742 0.121876
3 tests/test_repository.py 0.187793 0.058291
4 pydriller/scm/commit.py 0.145540 0.041020この Step のまとめ
共変更ネットワークを作り、次数中心性と媒介中心性で「結び目」になるファイルを数値化しました。中心性が高いファイルは、変更回数の多いファイルと重なりがちですが、完全には一致しません。この差が、次の機械学習で効いてきます。
機械学習:ホットスポットを予測する
Predict the hotspots何を・なぜWhat & why
ここまでに作った特徴量(チャーン・作者数・次数中心性・媒介中心性)を1つの表にまとめ、機械学習でホットスポットを分類します。まず「ホットスポット」を定義します。ここでは変更回数が上位25%のファイルを正例(1)とします。
注意点が1つあります。ラベルの定義に使った変更回数そのものは特徴量に入れません。入れると答えを直接見せることになり、意味のある予測になりません。そこで残りの特徴量から予測できるかを試します。train_test_split で学習用と検証用に分け、RandomForestClassifier で分類し、classification_report で適合率・再現率・F値を確認します。
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
# 中心性を特徴量表に結合(単独変更のファイルは中心性0で補完)
data = feat.merge(cent, on="file", how="left").fillna(0)
# ラベル: 変更回数が上位25%のファイルをホットスポット(1)
threshold = data["changes"].quantile(0.75)
data["hotspot"] = (data["changes"] >= threshold).astype(int)
# 変更回数そのものは特徴量に入れない(ラベルの定義に使ったため)
X = data[["churn", "authors", "degree", "betweenness"]]
y = data["hotspot"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
clf = RandomForestClassifier(n_estimators=300, random_state=42)
clf.fit(X_train, y_train)
pred = clf.predict(X_test)
print(classification_report(y_test, pred))
# どの特徴量が効いたか
for name, imp in sorted(zip(X.columns, clf.feature_importances_),
key=lambda t: t[1], reverse=True):
print(f"{name:12s} {imp:.3f}")
precision recall f1-score support
0 0.90 0.94 0.92 48
1 0.79 0.69 0.73 16
accuracy 0.88 64
macro avg 0.84 0.81 0.83 64
weighted avg 0.87 0.88 0.87 64
churn 0.462
degree 0.281
betweenness 0.171
authors 0.086正例(ホットスポット)は少数派なので、多数派の負例より当てるのが難しく、再現率がやや低く出ています。特徴量の重要度を見ると、チャーンが最も効き、次いで共変更の次数中心性が効いています。つまり「よく書き換えられ、多くのファイルと一緒に変わる」ファイルほどホットスポットになりやすい、という素直な傾向が読み取れます。
この Step のまとめ
4つの特徴量からホットスポットを分類し、評価指標で性能を確かめました。ネットワーク由来の中心性が予測に寄与している点が、複数の技術をつないだ効果です。LogisticRegression に差し替えて比べるのも良い練習になります。
可視化:分布とネットワークを眺める
Visualize何を・なぜWhat & why
数値だけでなく、図でも確かめます。変更回数の分布を見ると、少数のファイルに変更が集中するロングテールになっているはずです。共変更ネットワークの中心部分を描くと、中心性の高いファイルが実際に多くの辺を集めている様子が見えます。matplotlib と NetworkX の描画関数で描きます。
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(11, 4))
# 左: ファイルごとの変更回数の分布(ロングテール)
ax[0].hist(feat["changes"], bins=30, color="#0E6F5C")
ax[0].set_xlabel("changes per file")
ax[0].set_ylabel("number of files")
ax[0].set_title("Change frequency")
# 右: 共変更ネットワークの中心部分(次数の高い上位30ノード)
top_nodes = [n for n, _ in sorted(G.degree,
key=lambda x: x[1], reverse=True)[:30]]
H = G.subgraph(top_nodes)
pos = nx.spring_layout(H, seed=42)
nx.draw_networkx_nodes(H, pos, ax=ax[1], node_size=120, node_color="#0E6F5C")
nx.draw_networkx_edges(H, pos, ax=ax[1], edge_color="#B7C0BB")
ax[1].set_title("Co-change network (top 30)")
ax[1].axis("off")
plt.tight_layout()
plt.show()
左のヒストグラムは、大半のファイルが数回しか変更されない一方で、ごく少数のファイルが飛び抜けて多く変更される、右に長い裾を引いた形になります。右のグラフでは、中央に位置する数個のノード(Step 3 で中心性が高かったファイル)に辺が集まり、周辺のファイルがそれらを介してつながる様子が見えます。図の見た目はレイアウトの乱数(seed)で変わりますが、構造の傾向は保たれます。
考察:何が言えて、何に注意するか
Discussion & caveats読み解きReading the result
一連の分析から、このプロジェクトでは少数のファイルに変更が集中し、それらが共変更ネットワークの結び目にもなっていることが分かりました。ホットスポットは、チャーンの大きさと共変更の次数中心性からある程度予測でき、レビューやテスト、リファクタリングの投資先を選ぶ手がかりになります。単体のメトリクスだけでなく、ネットワーク由来の指標を足すと予測が補強される、というのがこの演習の要点です。
注意点Caveats
解釈の限界Limits
- 相関であって因果ではない:変更が多い=悪いとは限らない。活発に育っている中心機能かもしれません。
- tangled commit:無関係な変更が1コミットに混ざると、共変更が過大に見えることがあります。
- リネーム・移動:ファイル名の変更で履歴が途切れ、変更回数を過小評価することがあります。
妥当性を高めるにはToward validity
- 期間を区切って、最近の傾向と過去を分ける。
- リネーム追跡を有効にして履歴のつながりを保つ。
- 不具合修正コミットと結びつけ、実際の欠陥と対応するか確かめる。
数字を鵜呑みにしない
ここで得た予測やランキングは、あくまで「見るべき場所の候補」です。最終的な判断には、コードの中身やチームの文脈を合わせて読む必要があります。メトリクスは会話の出発点であって、結論ではありません。
発展課題Try more
こう変えたら、どうなる?
パイプラインができたら、少しずつ条件を変えて挙動を確かめましょう。答えは用意しません。手を動かして、数字がどう動くかを観察するのが目的です。
別のリポジトリで試す
urlを別の小さな公開リポジトリに変えて走らせる。プロジェクトによってホットスポットの偏り方がどう違うかを比べる。
期間で区切る
Repository(url, since=..., to=...)で期間を絞り、直近1年と全期間で結果を比べる。ホットスポットは時期によって移り変わります。
特徴量を足す
- ファイルの平均コミット間隔、直近の変更からの経過日数、コメント行の割合などを加えて、予測が変わるかを見る。
バグ修正を辿る
- コミットメッセージから
fixやbugを含むものを拾い、修正が集中するファイルを調べる。SZZ 的にバグ混入コミットまで遡るとさらに踏み込めます。
まとめWrap-up
つないで見えたもの、次へ
この演習では、単体で学んだ4つの技術を1本のパイプラインにつなぎ、現実の OSS リポジトリからホットスポットを予測しました。振り返ると、それぞれのモジュールが次の役割を担っていました。
- リポジトリマイニング:PyDriller で開発履歴という素材を手に入れた。
- 前処理:pandas でファイル単位の特徴量表に整えた。
- ネットワーク分析:共変更の結び目を中心性で数値化した。
- 機械学習:特徴量を統合してホットスポットを分類し、評価した。
単体では見えなかった構造が、つなぐと見えてきました。各技術の詳しい理論やAPIは、下の各モジュールに戻ると確認できます。
使ったモジュール
次の演習・段階へ
- 次の演習:テキスト分類器をゼロから作る(演習一覧)
- ほかの実践編で、NLP やプログラム解析を軸にしたパイプラインも試す。
- 特徴量やモデルを差し替えて、自分の関心のあるリポジトリを診断してみる。
おつかれさまでした
ノートブックを開き、url を自分の気になるリポジトリに変えて、もう一度上から実行してみてください。しきい値や特徴量を少し変えるだけで、ホットスポットの顔ぶれや評価指標がどう動くかを確かめると、パイプライン全体の勘所がつかめます。