機械学習で人材採用を効率化！マッチングシステムの作り方

## はじめに
組織内での効果的な人財配置は、プロジェクトの成功に直結する重要な要素です。本記事では、機械学習とグラフデータベースを活用して、プロジェクトの要件に最適な人財を効率的にマッチングするシステムの構築方法を解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/0e100ed0-c936-8a59-cc8d-a15343e8948e.png)

## システムの要件と目的

このシステムは以下の要件と目的を満たすように設計されています：

1. **高精度なマッチング**: プロジェクトの要件と人財のスキルセットを適切にマッチングし、最適な人財を推薦する。
2. **多角的な評価**: スキル、経験年数、過去のプロジェクト成功率、プロジェクトの複雑さなど、多様な要素を考慮したマッチングを行う。
3. **柔軟性と拡張性**: 新しいスキルや技術の登場に対応できる柔軟な設計とし、将来的な拡張も容易にする。
4. **解釈可能性**: マッチングの根拠を理解し、説明できるようにする。
5. **効率的なデータ管理**: グラフデータベースを用いて、人財とプロジェクトの複雑な関係性を効率的に管理する。
6. **自動化と意思決定支援**: 人財配置プロセスの一部を自動化し、人事担当者の意思決定を支援する。

## グラフデータベースの活用

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/d68e6d63-a07b-8302-c1c4-57066f7d0abc.png)


本システムでは、グラフデータベースを使用してデータを管理します。グラフデータベースを選択した理由と主な利点は以下の通りです：

1. **複雑な関係性の表現**: 人財、スキル、プロジェクト間の多様な関係を自然に表現できる。
2. **柔軟なスキーマ**: 新しいスキルやプロジェクト要件の追加が容易。
3. **高速な関係性の探索**: 「特定のスキルを持つ人財」や「類似プロジェクトの経験者」などの複雑なクエリを効率的に実行できる。

例えば、以下のようなCypherクエリで、特定のスキルセットを持つ人財を効率的に検索できます：

```cypher
MATCH (p:Person)-[:HAS_SKILL]->(s:Skill)
WHERE s.name IN ['Python', 'Machine Learning']
RETURN p.name, COUNT(DISTINCT s) as skill_count
ORDER BY skill_count DESC
LIMIT 5
```

## 技術選択：ランダムフォレスト回帰の採用理由
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/90cabf99-c0c9-ac1e-496d-a2a6de8693de.png)

本システムでは、機械学習モデルとしてランダムフォレスト回帰を採用しています。この選択には以下の理由があります：

1. **高い予測精度**: 多数の決定木を組み合わせることで、高い予測精度を実現。
2. **過学習の抑制**: 個々の決定木の過学習を抑制し、汎化性能の高いモデルを構築可能。
3. **非線形関係の捕捉**: 複雑な非線形関係を自動的に学習し、モデル化が可能。
4. **特徴量の重要度評価**: モデルの解釈可能性が高く、各特徴量の重要度を容易に評価可能。
5. **欠損値や外れ値への耐性**: データの前処理の負担が比較的小さく、実データでの運用に適している。

他のモデルとの比較：

- **XGBoost**: ランダムフォレストよりも高い予測精度を示す場合がありますが、過学習のリスクが高く、チューニングにより多くの労力が必要です。
- **SVM**: 高次元データに強いですが、大規模データセットでの計算コストが高く、解釈可能性が低いです。
- **線形回帰**: 単純で解釈しやすいですが、非線形関係を捉えることができません。

これらの特性を総合的に判断し、ランダムフォレスト回帰が人財マッチングタスクに最適であると判断しました。

## 実装の説明

### 1. 必要なライブラリのインポート

```python
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import mean_squared_error, r2_score
```

### 2. データの準備

実際のシステムでは、グラフデータベースからデータを取得することを想定しています。ここでは簡略化のため、すでに取得されたデータを使用します。

```python
# サンプルデータ（実際にはグラフDBから取得）
persons = [
    {'id': 1, 'skills': ['Python', 'Machine Learning', 'Data Analysis'], 'experience': 5, 'project_success': 0.8},
    {'id': 2, 'skills': ['Java', 'Spring Framework', 'SQL'], 'experience': 3, 'project_success': 0.7},
    # ... 他の人財データ ...
]

projects = [
    {'id': 1, 'required_skills': ['Python', 'Machine Learning'], 'duration': 6, 'complexity': 0.7},
    {'id': 2, 'required_skills': ['Java', 'Spring Framework'], 'duration': 4, 'complexity': 0.6},
    # ... 他のプロジェクトデータ ...
]
```

### 3. 特徴量エンジニアリング

人財とプロジェクトのマッチング度を数値化します。

```python
def create_feature_vector(person, project):
    person_skills = set(person['skills'])
    project_skills = set(project['required_skills'])
    
    skill_match_ratio = len(person_skills & project_skills) / len(project_skills)
    skill_coverage = len(person_skills) / len(all_skills)
    experience_ratio = person['experience'] / max(p['experience'] for p in persons)
    
    return [
        skill_match_ratio,
        skill_coverage,
        experience_ratio,
        person['project_success'],
        project['duration'] / max(p['duration'] for p in projects),
        project['complexity']
    ]
    
def calculate_match_score(person, project):
    matched_skills = set(project['required_skills']) & set(person['skills'])
    skill_match_ratio = len(matched_skills) / len(project['required_skills'])
    experience_bonus = min(person['experience'] / 10, 0.5)
    return skill_match_ratio + experience_bonus

# 全スキルのセットを作成
all_skills = set()
for person in persons:
    all_skills.update(person['skills'])
for project in projects:
    all_skills.update(project['required_skills'])

# 訓練データの作成
X = []
y = []

for person in persons:
    for project in projects:
        X.append(create_feature_vector(person, project))
        # マッチングスコアの計算（簡略化のため、スキルマッチと経験のみを考慮）
        matched_skills = set(project['required_skills']) & set(person['skills'])
        skill_match_ratio = len(matched_skills) / len(project['required_skills'])
        experience_bonus = min(person['experience'] / 10, 0.5)
        y.append(skill_match_ratio + experience_bonus)

X = np.array(X)
y = np.array(y)
```

### 4. モデルの構築と最適化

ランダムフォレスト回帰モデルを構築し、グリッドサーチでハイパーパラメータを最適化します。

```python
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

param_grid = {
    'n_estimators': [100, 200, 300],
    'max_depth': [None, 10, 20, 30],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

rf = RandomForestRegressor(random_state=42)
grid_search = GridSearchCV(rf, param_grid, cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
grid_search.fit(X_train, y_train)

best_model = grid_search.best_estimator_
```

### 5. モデルの評価

```python
y_pred = best_model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"Best parameters: {grid_search.best_params_}")
print(f"Mean squared error: {mse:.4f}")
print(f"R-squared score: {r2:.4f}")
```

### 6. 新しいプロジェクトに対する予測

```python
def predict_best_match(model, project, candidates):
    best_candidate = None
    best_score = -float('inf')
    
    for candidate in candidates:
        features = create_feature_vector(candidate, project)
        score = model.predict([features])[0]
        
        if score > best_score:
            best_score = score
            best_candidate = candidate
    
    return best_candidate, best_score

# 新プロジェクトの例
new_project = {
    'id': 5,
    'required_skills': ['Python', 'Deep Learning', 'TensorFlow'],
    'duration': 8,
    'complexity': 0.85
}

best_match, match_score = predict_best_match(best_model, new_project, persons)

print("\nBest match for the new project:")
print(f"Candidate ID: {best_match['id']}")
print(f"Skills: {', '.join(best_match['skills'])}")
print(f"Experience: {best_match['experience']} years")
print(f"Predicted Match Score: {match_score:.2f}")
```

### 7. 特徴量の重要度の分析

```python
feature_importance = best_model.feature_importances_
feature_names = ['Skill Match Ratio', 'Skill Coverage', 'Experience Ratio', 'Project Success', 'Project Duration', 'Project Complexity']

print("\nFeature Importance:")
for name, importance in zip(feature_names, feature_importance):
    print(f"{name}: {importance:.4f}")
```

## 結果の解釈と今後の展望

モデルの性能評価（R-squared スコア、MSE）、特徴量の重要度分析、新プロジェクトに対する推薦結果の妥当性を慎重に検討することが重要です。

今後の展望としては、以下のような点が考えられます：

1. より多様な特徴量の追加（例：ソフトスキル、地理的要因、チーム適合性など）
2. 大規模データセットでの検証
3. 深層学習モデルの適用可能性の検討（例：スキルの埋め込み表現を学習するためのWord2Vecライクなアプローチ）
4. 定期的なモデルの再訓練と新しいスキルへの対応方法の確立
5. 人事担当者のフィードバックを取り入れたモデルの改善

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/bf476644-ed7d-4365-dc6f-6d1374dd8318.png)

本記事では、機械学習とグラフデータベースを用いた効率的な人財マッチングシステムの構築方法を解説しました。グラフデータベースを活用したデータ管理と、ランダムフォレスト回帰を用いた予測モデルにより、高精度で解釈可能な人財推薦が可能になります。

このシステムは、組織の人財配置プロセスを効率化し、プロジェクトの成功確率を高めることが期待できます。ただし、実際の運用では、このシステムを意思決定支援ツールとして位置づけ、人間の判断と組み合わせて使用することが重要です。

継続的な改善と検証を行うことで、より効果的な人財マッチングシステムの構築が可能になるでしょう。

## 参考情報

### シーケンス図
```mermaid
sequenceDiagram
    participant User as ユーザー
    participant System as システム
    participant GraphDB as グラフデータベース
    participant FE as 特徴量エンジニアリング
    participant ML as 機械学習モデル
    participant Output as 結果出力

    User->>System: 新プロジェクト情報入力
    System->>GraphDB: 人財データ取得要求
    GraphDB-->>System: 人財データ返送
    System->>FE: 特徴量生成要求
    FE->>FE: スキルマッチ率計算
    FE->>FE: 経験年数比率計算
    FE->>FE: プロジェクト成功率取得
    FE-->>System: 特徴量ベクトル返送
    System->>ML: 予測要求
    ML->>ML: ランダムフォレスト回帰実行
    ML-->>System: 予測結果返送
    System->>Output: 結果整形要求
    Output->>Output: 最適マッチング特定
    Output->>Output: スコアリスト作成
    Output-->>System: 整形済み結果返送
    System-->>User: マッチング結果表示
```

### 実装 (全部)
```python
import numpy as np
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import mean_squared_error, r2_score
import sklearn

print(f"scikit-learn version: {sklearn.__version__}")

# 1. データの拡張
persons = [
    {'id': 1, 'skills': ['Python', 'Machine Learning', 'Data Analysis'], 'experience': 5, 'project_success': 0.8},
    {'id': 2, 'skills': ['Java', 'Spring Framework', 'SQL'], 'experience': 3, 'project_success': 0.7},
    {'id': 3, 'skills': ['JavaScript', 'React', 'Node.js'], 'experience': 4, 'project_success': 0.75},
    {'id': 4, 'skills': ['Python', 'Django', 'PostgreSQL'], 'experience': 2, 'project_success': 0.6},
    {'id': 5, 'skills': ['C++', 'Algorithms', 'Data Structures'], 'experience': 7, 'project_success': 0.85},
    # データを追加
    {'id': 6, 'skills': ['Python', 'TensorFlow', 'Deep Learning'], 'experience': 3, 'project_success': 0.72},
    {'id': 7, 'skills': ['Java', 'Kotlin', 'Android'], 'experience': 5, 'project_success': 0.78},
    {'id': 8, 'skills': ['JavaScript', 'Vue.js', 'Node.js'], 'experience': 4, 'project_success': 0.76},
    {'id': 9, 'skills': ['Python', 'Flask', 'MongoDB'], 'experience': 3, 'project_success': 0.7},
    {'id': 10, 'skills': ['C#', '.NET', 'SQL Server'], 'experience': 6, 'project_success': 0.82},
]

projects = [
    {'id': 1, 'required_skills': ['Python', 'Machine Learning'], 'duration': 6, 'complexity': 0.7},
    {'id': 2, 'required_skills': ['Java', 'Spring Framework'], 'duration': 4, 'complexity': 0.6},
    {'id': 3, 'required_skills': ['JavaScript', 'React'], 'duration': 3, 'complexity': 0.5},
    {'id': 4, 'required_skills': ['Python', 'Django', 'SQL'], 'duration': 5, 'complexity': 0.65},
    # プロジェクトデータを追加
    {'id': 5, 'required_skills': ['Python', 'TensorFlow', 'Deep Learning'], 'duration': 8, 'complexity': 0.8},
    {'id': 6, 'required_skills': ['Java', 'Android'], 'duration': 5, 'complexity': 0.7},
    {'id': 7, 'required_skills': ['JavaScript', 'Vue.js', 'Node.js'], 'duration': 4, 'complexity': 0.6},
    {'id': 8, 'required_skills': ['Python', 'Flask', 'MongoDB'], 'duration': 3, 'complexity': 0.55},
]

# 2. 特徴量エンジニアリングの改善
def create_feature_vector(person, project):
    person_skills = set(person['skills'])
    project_skills = set(project['required_skills'])
    
    skill_match_ratio = len(person_skills & project_skills) / len(project_skills)
    skill_coverage = len(person_skills) / len(all_skills)
    experience_ratio = person['experience'] / max(p['experience'] for p in persons)
    
    return [
        skill_match_ratio,
        skill_coverage,
        experience_ratio,
        person['project_success'],
        project['duration'] / max(p['duration'] for p in projects),
        project['complexity']
    ]

def calculate_match_score(person, project):
    matched_skills = set(project['required_skills']) & set(person['skills'])
    skill_match_ratio = len(matched_skills) / len(project['required_skills'])
    experience_bonus = min(person['experience'] / 10, 0.5)
    return skill_match_ratio + experience_bonus

# データセットの作成
X = []
y = []
all_skills = set()

for person in persons:
    all_skills.update(person['skills'])

for person in persons:
    for project in projects:
        X.append(create_feature_vector(person, project))
        y.append(calculate_match_score(person, project))

X = np.array(X)
y = np.array(y)

# 3. モデルの選択と最適化
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('rf', RandomForestRegressor(random_state=42))
])

param_grid = {
    'rf__n_estimators': [100, 200, 300],
    'rf__max_depth': [None, 10, 20, 30],
    'rf__min_samples_split': [2, 5, 10],
    'rf__min_samples_leaf': [1, 2, 4]
}

grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
grid_search.fit(X_train, y_train)

best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test)

mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"Best parameters: {grid_search.best_params_}")
print(f"Mean squared error: {mse:.4f}")
print(f"R-squared score: {r2:.4f}")

# 4. 新しいプロジェクトに対する予測
new_project = {
    'id': 9,
    'required_skills': ['Python', 'Deep Learning', 'TensorFlow'],
    'duration': 8,
    'complexity': 0.85
}

def predict_best_match(model, project, candidates):
    best_candidate = None
    best_score = -float('inf')
    
    for candidate in candidates:
        features = create_feature_vector(candidate, project)
        score = model.predict([features])[0]
        
        if score > best_score:
            best_score = score
            best_candidate = candidate
    
    return best_candidate, best_score

best_match, match_score = predict_best_match(best_model, new_project, persons)

print("\nBest match for the new project:")
print(f"Candidate ID: {best_match['id']}")
print(f"Skills: {', '.join(best_match['skills'])}")
print(f"Experience: {best_match['experience']} years")
print(f"Predicted Match Score: {match_score:.2f}")

print("\nAll candidates scores:")
for candidate in persons:
    features = create_feature_vector(candidate, new_project)
    score = best_model.predict([features])[0]
    print(f"Candidate ID: {candidate['id']}, Predicted Score: {score:.2f}")

# 5. 特徴量の重要度の確認
feature_importance = best_model.named_steps['rf'].feature_importances_
feature_names = ['Skill Match Ratio', 'Skill Coverage', 'Experience Ratio', 'Project Success', 'Project Duration', 'Project Complexity']

print("\nFeature Importance:")
for name, importance in zip(feature_names, feature_importance):
    print(f"{name}: {importance:.4f}")
```


### 動作確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/891b076d-32c2-2bb0-8a34-47b280005314.png)

Python version: 3.10.12 (main, Sep 11 2024, 15:47:36) [GCC 11.4.0]
NumPy version: 1.26.4
Pandas version: 2.2.2
Scikit-learn version: 1.5.2

システムの実装後、以下の手順で動作確認を行いました。

#### 1. モデルの学習と評価

ランダムフォレスト回帰モデルを学習データで訓練し、テストデータで評価しました。

```python
Best parameters: {'max_depth': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'n_estimators': 200}
Mean squared error: 0.0062
R-squared score: 0.9586
```

- **解釈**: R-squared scoreが0.9586と非常に高く、モデルが人財マッチングのパターンを良く捉えていることを示しています。Mean squared errorも0.0062と低く、予測の精度が高いことが分かります。

#### 2. 新しいプロジェクトに対する予測

学習済みモデルを使用して、新しいプロジェクトに対する最適な人財マッチングを予測しました。

```python
新プロジェクト:
ID: 5
必要スキル: ['Python', 'Deep Learning', 'TensorFlow']
期間: 8ヶ月
複雑度: 0.85

最適マッチング結果:
候補者ID: 6
スキル: Python, TensorFlow, Deep Learning
経験年数: 3年
予測マッチスコア: 1.31
```

- **解釈**: モデルは、新プロジェクトの要件に完全に一致するスキルセットを持つ候補者6を正しく識別しました。

- **入力データからの説明**:
  候補者6の詳細情報（入力データより）:
  ```python
  {
    'id': 6, 
    'skills': ['Python', 'TensorFlow', 'Deep Learning'], 
    'experience': 3, 
    'project_success': 0.72
  }
  ```
  新プロジェクトが要求するスキル（Python, Deep Learning, TensorFlow）を候補者6が全て保有しています。これが高いマッチスコアの主な理由です。経験年数は3年とやや少ないものの、プロジェクト成功率が0.72と比較的高いことも評価されています。スキルの完全一致が、経験年数の少なさを補って余りある評価となっています。

#### 3. 全候補者のスコア

同じプロジェクトに対する全候補者のスコアも算出しました。

```python
候補者ID: 1, 予測スコア: 0.80
候補者ID: 2, 予測スコア: 0.30
候補者ID: 3, 予測スコア: 0.40
候補者ID: 4, 予測スコア: 0.65
候補者ID: 5, 予測スコア: 0.50
候補者ID: 6, 予測スコア: 1.31
候補者ID: 7, 予測スコア: 0.50
候補者ID: 8, 予測スコア: 0.40
候補者ID: 9, 予測スコア: 0.67
候補者ID: 10, 予測スコア: 0.50
```

- **解釈**: 候補者6が突出して高いスコアを示しており、モデルが明確に最適な候補者を識別できていることが分かります。

- **他の候補者との比較**:
  - 候補者1（スコア: 0.80）: Pythonと機械学習のスキルを持っていますが、Deep LearningとTensorFlowが欠けています。
  - 候補者4（スコア: 0.65）: Pythonスキルを持っていますが、Deep LearningとTensorFlowが欠けています。
  - 候補者9（スコア: 0.67）: PythonとMongoDBのスキルを持っていますが、Deep LearningとTensorFlowが欠けています。

  これらの候補者は部分的にスキルが一致していますが、候補者6ほど完全には要件を満たしていません。

#### 4. 特徴量の重要度

モデルが各特徴量をどの程度重視しているかを分析しました。

```python
特徴量の重要度:
スキルマッチ率: 0.8875
プロジェクト成功率: 0.0549
経験年数比率: 0.0512
プロジェクト期間: 0.0035
プロジェクト複雑度: 0.0029
スキルカバレッジ: 0.0000
```

- **解釈**: スキルマッチ率が圧倒的に重要視されていることが分かります。これは候補者6が高いスコアを獲得した主な理由を裏付けています。一方で、プロジェクトの期間や複雑度、スキルカバレッジはあまり重視されていないようです。

#### 総合評価

1. モデルの性能は非常に高く、人財マッチングタスクに適していると言えます。
2. 新プロジェクトに対する予測結果も、入力データと照らし合わせて妥当なものとなっています。
3. ただし、スキルマッチ率に大きく依存しており、他の要素の影響が小さい点は検討が必要です。

#### 今後の改善点

1. より多様な特徴量の追加（例：ソフトスキル、過去のプロジェクト経験の詳細など）
2. スキルマッチ率以外の要素の重要性を高める方法の検討
3. より大規模なデータセットでの検証
4. 定期的なモデルの再訓練と、新しいスキルへの対応方法の確立
5. 経験年数や過去のプロジェクト成功率がより反映されるようなモデルの調整

これらの結果から、本システムは高い精度で人財マッチングを行えることが確認できました。特に、要求されるスキルセットと候補者のスキルの一致度を正確に評価できています。ただし、実際の運用に向けては、スキル以外の要素もバランスよく考慮できるようさらなる改善が必要です。また、人事担当者の知見を取り入れながら、モデルの予測結果を補完していくことが重要となるでしょう。