GPT-4で人員チーミング・JOBアサイン最適化問題を解決する組み分け帽子を開発させた(HRJOBAssigner dev by ChatGPT-4)

# 概要
ハリーポッターの組み分け帽子、ご存じでしょうか。

そんな魔法みたいな帽子＝システムをいつか作れればいいな、と夢見ていました。

そこで、今回、GPT-4へ要求定義をし、問答を重ねたところ、この問題化・言語化すら難しいものを解決できていると思われるコードを書かせることに成功しました。

# 経緯
人員配置問題は、組織の人々を悩ませつづける、人類史が始まってからの悩みの種です。

私が見る限り、機械的な最適配置に基づくチーミングは、いまだ、どこの企業も完全なアルゴリズム・ソリューションを開発できていないのが現状かと思います。
なぜならば人員は、それぞれ役割や役職があり、性格傾向や適性や発達障害傾向や精神疾病、肉体的傷病、その他複雑な事情、といった先天的基質的要素や後天的事情要素を十人十色で持っており、それらの要素を互いに相互作用させた相性エッジ値が、クラスタとして完璧なハーモニクス（すなわち、グラフのエネルギーを最大に到達させた状態）を形成させ、最高の出力を出させなければならない、非常に問題提起が難しい問題だからです。

私は、この問題をChatGPT-4が答えとなるコードを書くのに必要な推論能力・演算能力を潜在的に獲得しているのでは、と推察したので、少しでも組織運営者、および、組織の下で働かれている方々の負担も減らすことへ、貢献できる種を生み出せていることができていれば幸いです。

そしてやがて、このシステムを国や自治体や企業や法人やその他組織運営者が使用することで、チームの出力を最大化するに必要な欠落人員をあぶりだす＝>雇用創出を促す＝>国民同士のハーモニクスの最大化＝>国民の最大幸福の実現へ通じる種ともなっていれば幸いです。

# お知らせ
HR TEAM ASSIGNER を販売開始しました。
近日、以下の私の記事のプログラムの、HR JOB ASSIGNER 版を販売予定です。
- [HR TEAM ASSIGNER](https://note.com/shurihandoku/n/n1bbd74ef847c)

# 概念図
人1人それぞれに特徴ベクトルを持ち、人と人の間には特徴ベクトル同士で演算された相性値なるもの（これは、何らかの根拠もしくはシステムの思想で定義された値）をもつソシオグラフを出力出来るシステムが、未来に出来たと仮定し、そのソシオグラフのエッジ値のみを入力とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/290f77be-c9e6-4420-ba0d-26012150ed51.png)

# 制限事項
最適解に到達している保証はないため、ご容赦ください。
まだ発展途上なので、開発を始めてもよいですし、私も開発を続けます。

人と人との相性値＝エッジとみなせます。今回は、1人の基質・事情要素の実数列ベクトルへのパラメタイズ⇒相性値の定式化もすでに何らかのシステムにより、完成したことを前提とし、エッジを乱数として、以下のスクリプトを書かせました。
# 結果
以下全て、ChatGPTが出力した最終結果となるスクリプトの実行結果、ChatGPTが出力した設計書です。
### 人員(Person)-チーム(Team)低次元写像)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/7be4b768-9cff-6f89-8234-9e56e7d2eba0.png)

### チーム(Team)-JOB
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/10538463-ad4a-1f5a-9255-925b1cc6ee80.png)

### ファイル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/fb445d5f-1003-1028-0f9a-d0b6dddd83c0.png)

```teams.csv
Team,Energy Value,Assigned Job,Element 1,Element 2,Element 3,Element 4,Element 5,Element 6,Element 7,Element 8,Element 9,Element 10
Team 1,6.767948111600772,Job 9,2.202377933087427,2.687965142576683,2.1115970083308606,1.841770326495304,1.2169324274666526,3.074860843979545,1.617227248432609,1.8926575611154894,2.68258536345418,1.2445890970575952
Team 2,43.83730714190114,Job 4,11.985758991151041,16.32656945108003,13.380233659130518,14.129526266377402,11.321062462292526,13.54470381694819,13.729200352478248,14.938993326957604,14.185326165222174,14.432832168850725
Team 3,26.733542510153278,Job 6,8.185695272100496,7.893269883579512,9.575981585283843,7.86026806334735,6.4135989016491735,8.129704892207283,7.383165353197087,10.121416873564645,9.251909601467094,9.061531179090244
Team 4,15.64365069947192,Job 1,6.298359291718393,5.024860140404059,5.839049380467318,3.587148768730801,3.6291599026376966,3.4652417974356777,6.2505268058973025,5.399892577692256,4.36328753573241,4.516438687440106
Team 5,0.0,Job 10,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
Team 6,8.92945012264168,Job 2,2.4765182118841764,2.514043534818291,2.620433799729747,3.4886719779610305,3.0081698324981825,2.280704143059062,3.054421187498168,2.9386136649105703,2.62573210369196,3.0222361594519525
Team 7,3.2904355710620177,Job 5,1.3163462407929678,0.6591121698345317,1.0030886140847621,1.303286898358543,0.7162094970637604,0.8875820201839696,1.2832836253560285,0.8823516130767319,0.7196916154161992,1.3080333796781953
Team 8,5.568195537794409,Job 7,1.2697211588554513,1.795562506194496,1.4367176097984777,1.9085369466708602,1.3534031651565601,1.406796438012634,1.4523781387098267,2.6044393753134516,2.2451996811847605,1.64854165060085
Team 9,28.204425556468582,Job 8,8.829036560043981,7.5161618578425,8.4630763852964,9.622665170553784,10.184194165826923,8.828908734531593,8.840826024829806,9.05244849500689,8.503045471324208,9.096672322180734
Team 10,3.332442026230085,Job 3,0.8815673636619835,1.1749016700609807,1.0722509875277448,0.7139125274231505,0.8995060695212856,0.9350659640086525,1.1589331351657315,0.24146472325743462,1.9281729704631667,0.6967638592750373

```
```personnel.csv
Role,Name,Team,Distance
PM,Person_0,Team 6,0.7984017347637568
PM,Person_1,Team 4,1.0926126088511012
PM,Person_2,Team 3,0.4903033848831987
PM,Person_3,Team 4,0.6242085545720975
PM,Person_4,Team 2,0.2731522141561052
PM,Person_5,Team 6,0.5522074222318395
PM,Person_6,Team 9,0.7867250777735731
PM,Person_7,Team 3,0.7810243651825797
PM,Person_8,Team 2,0.8227666422786554
PM,Person_9,Team 2,0.536439230478523
PM,Person_10,Unassigned,0.4897786013939271
PM,Person_11,Team 3,0.625601125187984
PM,Person_12,Team 2,0.6861468347562235
PM,Person_13,Team 1,0.20746304357298082
PM,Person_14,Team 2,0.3188660199577896
PM,Person_15,Unassigned,0.6545959270998329
PM,Person_16,Team 7,0.48239132641992316
PM,Person_17,Team 4,0.29869314489426174
PM,Person_18,Unassigned,0.4097794011953554
PM,Person_19,Team 8,0.10572090753310792
PM,Person_20,Team 2,0.706885706794009
PM,Person_21,Team 3,0.2671233802112887
PM,Person_22,Team 9,0.5070469075565086
PM,Person_23,Team 4,0.49288995385719425
PM,Person_24,Team 2,0.4434653895679771
PL,Person_25,Team 9,0.6717282827983687
PL,Person_26,Team 1,0.44733682889413123
PL,Person_27,Team 9,0.4567418443575675
PL,Person_28,Team 4,0.6942093436298953
PL,Person_29,Team 2,0.708926053526934
PL,Person_30,Team 9,0.6326990735935429
PL,Person_31,Team 4,0.7145266374527907
PL,Person_32,Team 9,0.2551633237990506
PL,Person_33,Unassigned,0.5054678645097398
PL,Person_34,Team 8,0.23522060636963268
PL,Person_35,Team 2,0.6465117027317286
PL,Person_36,Team 3,0.6069750101882494
PL,Person_37,Team 2,0.22723925721616453
PL,Person_38,Unassigned,0.6486972970612168
PL,Person_39,Team 9,0.6292281988489249
PL,Person_40,Team 6,0.47089256188325856
PL,Person_41,Team 2,0.32209840776903326
PL,Person_42,Team 2,0.824552719546667
PL,Person_43,Team 2,0.7348836258213796
PL,Person_44,Team 3,0.7977668906393716
PL,Person_45,Team 1,0.6881501157253176
PL,Person_46,Unassigned,0.3596854894248986
PL,Person_47,Team 8,0.5370533629990253
PL,Person_48,Team 9,0.18352043179569005
PL,Person_49,Unassigned,0.3792045605426886
PL Assistant,Person_50,Team 10,0.1439189497999444
PL Assistant,Person_51,Team 9,0.39491088913015954
PL Assistant,Person_52,Team 6,0.49738475228001094
PL Assistant,Person_53,Team 9,0.48669297916456034
PL Assistant,Person_54,Team 9,0.39881563990075036
PL Assistant,Person_55,Team 4,0.2845076344039277
PL Assistant,Person_56,Team 9,0.6662389493021725
PL Assistant,Person_57,Team 4,0.6128080114495539
PL Assistant,Person_58,Team 3,0.31388191490604117
PL Assistant,Person_59,Team 1,0.6846531393761486
PL Assistant,Person_60,Team 2,0.7551523685614068
PL Assistant,Person_61,Team 2,0.575450309746403
PL Assistant,Person_62,Team 2,0.562263134630319
PL Assistant,Person_63,Team 9,0.4924480695870272
PL Assistant,Person_64,Team 3,0.5666707617418756
PL Assistant,Person_65,Team 2,0.6755843830546133
PL Assistant,Person_66,Team 3,0.594882200186483
PL Assistant,Person_67,Team 4,0.6454173992643082
PL Assistant,Person_68,Team 2,0.5018206306130737
PL Assistant,Person_69,Team 3,0.7860032111754011
PL Assistant,Person_70,Team 9,0.4866528560002603
PL Assistant,Person_71,Team 3,0.7185108406905568
PL Assistant,Person_72,Team 2,0.7378339953469086
PL Assistant,Person_73,Team 8,0.3186246126298088
PL Assistant,Person_74,Team 9,0.5175925421895594
Member,Person_75,Unassigned,0.3596854894248978
Member,Person_76,Team 2,0.45318516488009636
Member,Person_77,Team 6,0.7368065410782038
Member,Person_78,Team 3,0.2470326540429778
Member,Person_79,Team 2,0.5099422022317608
Member,Person_80,Team 3,0.6013622897249711
Member,Person_81,Team 7,0.49298300817462576
Member,Person_82,Team 2,0.7611233990277733
Member,Person_83,Team 3,0.6070565809263716
Member,Person_84,Team 2,0.484734041640102
Member,Person_85,Unassigned,0.4898612252897405
Member,Person_86,Team 4,0.730263411991248
Member,Person_87,Team 9,0.7029136274181866
Member,Person_88,Team 2,0.08996484303309334
Member,Person_89,Team 9,0.7486012670686006
Member,Person_90,Team 9,0.5481520859431103
Member,Person_91,Team 2,0.2556589515216452
Member,Person_92,Team 2,0.5204519603152855
Member,Person_93,Team 6,0.17156155381650057
Member,Person_94,Team 3,0.7894986091695136
Member,Person_95,Team 10,0.14391894979994424
Member,Person_96,Unassigned,0.5310892296738643
Member,Person_97,Team 3,0.3929236934569961
Member,Person_98,Team 3,0.3716320508436023
Member,Person_99,Team 2,0.44164990593384673
```
# Pythonスクリプト(chatGPT-4が出力したコードの集積)
```HRJOBASSINGER_20230723_v4.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.cm as cm
from sklearn.decomposition import PCA
from sklearn_extra.cluster import KMedoids
from scipy.optimize import linear_sum_assignment

# 初期設定
NUM_PEOPLE = 100
NUM_CLUSTERS = 10
DIMENSIONS = 10
PEOPLE_VECTORS = np.random.rand(NUM_PEOPLE, DIMENSIONS)  # 修正: 3からDIMENSIONSに変更
JOBS_VECTORS = np.random.rand(10, DIMENSIONS)  # 修正: 3からDIMENSIONSに変更

# チームの役職の定義
ROLES = ['PM', 'PL', 'PL Assistant', 'Member']

# チームタイプごとの役職の人数
TEAM_TYPE_1 = [1, 2, 1, 2]
TEAM_TYPE_2 = [1, 1, 2, 1]

# 相性のランダムな行列
COMPATIBILITY_MATRIX = np.random.rand(NUM_PEOPLE, NUM_PEOPLE)
def assign_roles(num_people):
    assigned_roles = [num_people // len(TEAM_TYPE_1)] * len(TEAM_TYPE_1)
    remaining = num_people % len(TEAM_TYPE_1)
    for i in range(remaining):
        assigned_roles[i] += 1
    roles = np.repeat(ROLES, assigned_roles)
    return roles

class HRJobAssigner:
    def __init__(self):
        self.data = np.random.rand(NUM_PEOPLE, DIMENSIONS)
        self.transformed_data = self._reduce_dimensionality()

    def _reduce_dimensionality(self):
        pca = PCA(n_components=3)
        return pca.fit_transform(self.data)

    def _get_optimal_threshold(self, distances, target_ratio=0.9):
        sorted_distances = np.sort(distances)
        target_idx = int(len(distances) * target_ratio)
        return sorted_distances[target_idx]

    def assign_teams(self):
        kmedoids = KMedoids(n_clusters=NUM_CLUSTERS, init='random', random_state=0)
        kmedoids.fit(COMPATIBILITY_MATRIX)
        labels = kmedoids.labels_
        centers = np.array([np.mean(self.data[labels == i], axis=0) for i in range(NUM_CLUSTERS)])  # 修正

        # ここで centers が形状 (NUM_CLUSTERS, DIMENSIONS) であることを確認
        assert centers.shape == (NUM_CLUSTERS, DIMENSIONS)
        
        distances = np.array([np.linalg.norm(PEOPLE_VECTORS[i] - centers[labels[i]]) for i in range(NUM_PEOPLE)])
        threshold = self._get_optimal_threshold(distances)

        assign_mask = distances < threshold
        labels[~assign_mask] = -1

        return labels, centers

    def assign_jobs_to_teams(self, team_vectors):
        cost_matrix = np.empty((NUM_CLUSTERS, NUM_CLUSTERS))
        for i, team in enumerate(team_vectors):
            for j, job in enumerate(JOBS_VECTORS):
                cost_matrix[i, j] = np.linalg.norm(team - job)
        row_ind, col_ind = linear_sum_assignment(cost_matrix)
        return col_ind

    def visualize_3d_graph(self, assigned_teams):
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
        colors = [cm.rainbow(i/NUM_CLUSTERS) for i in range(NUM_CLUSTERS)]
        for i in range(NUM_CLUSTERS):
            cluster_data = PEOPLE_VECTORS[assigned_teams == i]
            ax.scatter(cluster_data[:, 0], cluster_data[:, 1], cluster_data[:, 2], color=colors[i], label=f'Team {i + 1}')
            centroid = np.mean(cluster_data, axis=0)
            for person in cluster_data:
                distance = np.linalg.norm(person - centroid)
                ax.plot([centroid[0], person[0]], [centroid[1], person[1]], [centroid[2], person[2]], 'gray', linewidth=0.5)
                ax.text((centroid[0] + person[0]) / 2, (centroid[1] + person[1]) / 2, (centroid[2] + person[2]) / 2, f"{distance:.2f}", color='black')
        ax.set_xlabel('X Label')
        ax.set_ylabel('Y Label')
        ax.set_zlabel('Z Label')
        ax.legend()
        plt.savefig("3D_graph.png")
        plt.show()

    def visualize_teams_jobs_3d(self, team_vectors, assigned_jobs):
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
        for i, vec in enumerate(team_vectors):
            ax.scatter(vec[0], vec[1], vec[2], label=f'Team {i + 1}', color=cm.rainbow(i/NUM_CLUSTERS))
        for j, vec in enumerate(JOBS_VECTORS[assigned_jobs]):
            ax.scatter(vec[0], vec[1], vec[2], label=f'Job {j + 1}', marker='x', color=cm.rainbow(j/NUM_CLUSTERS))
            ax.plot([team_vectors[j][0], vec[0]], [team_vectors[j][1], vec[1]], [team_vectors[j][2], vec[2]], linestyle='--', color=cm.rainbow(j/NUM_CLUSTERS))
        ax.set_xlabel('X Label')
        ax.set_ylabel('Y Label')
        ax.set_zlabel('Z Label')
        ax.legend()
        plt.savefig("teams_jobs_assignments_3d.png")
        plt.show()

    def _transform_centers(self, centers):
        """クラスタの中心を3次元に変換"""
        pca = PCA(n_components=3)
        pca.fit(self.data)
        return pca.transform(centers)

    def generate_csv(self, labels, centers, assigned_jobs):
        transformed_centers = self._transform_centers(centers)  # 中心を3次元に変換

        distances = []
        for i in range(NUM_PEOPLE):
            if labels[i] == -1:
                all_dists = [np.linalg.norm(self.transformed_data[i] - center) for center in transformed_centers]
                distances.append(min(all_dists))
            else:
                distances.append(np.linalg.norm(self.transformed_data[i] - transformed_centers[labels[i]]))
        df_personnel = pd.DataFrame({
            'Role': assign_roles(NUM_PEOPLE),
            'Name': [f'Person_{i}' for i in range(NUM_PEOPLE)],
            'Team': [f'Team {label + 1}' if label != -1 else 'Unassigned' for label in labels],
            'Distance': distances
        })
        df_personnel.to_csv('personnel.csv', index=False)
        team_energy = [np.sum(self.data[labels == i], axis=0) for i in range(NUM_CLUSTERS)]
        df_teams = pd.DataFrame({
            'Team': [f'Team {i + 1}' for i in range(NUM_CLUSTERS)],
            'Energy Value': [np.linalg.norm(e) for e in team_energy],
            'Assigned Job': [f'Job {j + 1}' for j in assigned_jobs]
        })
        for i in range(DIMENSIONS):
            df_teams[f'Element {i + 1}'] = [e[i] for e in team_energy]
        df_teams.to_csv('teams.csv', index=False)

    def get_total_cost(self, assigned_teams, team_vectors):
        # チーム内の距離の合計
        team_cost = np.sum([np.linalg.norm(PEOPLE_VECTORS[assigned_teams == i] - team_vectors[i]) for i in range(NUM_CLUSTERS) if np.any(assigned_teams == i)])
        
        # チームとジョブの距離の合計
        assigned_jobs = self.assign_jobs_to_teams(team_vectors)
        job_cost = np.sum([np.linalg.norm(team_vectors[i] - JOBS_VECTORS[assigned_jobs[i]]) for i in range(NUM_CLUSTERS)])

        return team_cost + job_cost

    # HRJobAssignerクラスのoptimizeメソッドを修正します。
    def optimize(self, max_iterations=100, tol=1e-4):
        previous_cost = float('inf')
        for iteration in range(max_iterations):
            assigned_teams, centers = self.assign_teams()
            team_vectors = np.array([np.mean(PEOPLE_VECTORS[assigned_teams == i], axis=0) if np.any(assigned_teams == i) else np.zeros(DIMENSIONS) for i in range(NUM_CLUSTERS)])
            
            current_cost = self.get_total_cost(assigned_teams, team_vectors)
            if abs(current_cost - previous_cost) < tol:
                print(f"Convergence achieved after {iteration} iterations.")
                break

            previous_cost = current_cost
        
        # 最適化が完了した後で出力を行うように移動
        assigned_jobs = self.assign_jobs_to_teams(team_vectors)
        self.visualize_3d_graph(assigned_teams)
        self.visualize_teams_jobs_3d(team_vectors, assigned_jobs)
        self.generate_csv(assigned_teams, team_vectors, assigned_jobs)
        return assigned_teams, team_vectors, assigned_jobs
    
# 最後の部分のコードを修正して、optimizeメソッドの呼び出しのみを行う
assigner = HRJobAssigner()
assigner.optimize()
```


# 要件定義書・詳細設計書
![要件定義書・詳細設計書.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/7b5240f3-8d3b-1ce4-008c-b48c9e9c2e5d.png)

# おわり

# おまけ（開発会議・問答・記録)
![screencapture-chat-openai-c-479b90cc-c29c-4f20-8f59-90e5673055b1-2023-07-24-01_53_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/215da9dc-6df9-6844-ca18-764db1b8adf9.png)
![screencapture-chat-openai-c-479b90cc-c29c-4f20-8f59-90e5673055b1-2023-07-24-01_53_39-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/657d3f02-c740-6878-4f68-7ec9cc2fd926.png)
![screencapture-chat-openai-c-479b90cc-c29c-4f20-8f59-90e5673055b1-2023-07-24-01_53_39-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/d5fa56af-391c-b419-84af-b6a2d96a9ac2.png)
![screencapture-chat-openai-c-479b90cc-c29c-4f20-8f59-90e5673055b1-2023-07-24-01_53_39-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559034/b14a6e8a-7123-8afc-53e9-a922fc4fda0b.png)