데이터 사이언티스트에게 요구되는 인재상과 그에 대한 대비책이 명확하지 않아, 채용 공고를 읽으며 '요구되기 쉬운 인재상'을 추정하게 되었습니다. 하지만 채용 공고의 수가 많아 하나하나 읽는 것은 힘든 작업입니다. 그래서 LLM(대규모 언어 모델)을 사용하여 효율화하고 있습니다.
인재상은 자유 기술 형식이기 때문에, '자체적으로 움직일 수 있는 분'과 '주도적으로 행동할 수 있는 분'처럼 같은 의미라도 표현이 다양합니다. 단순히 단어를 세는 것만으로는 실태를 파악하기 어려워, 이전 기사('요구하는 인재상을 LLM으로 추출. 임베딩과 클러스터링으로 건수를 세워봤다')에서는 다음과 같은 순서로 진행했습니다.
- Claude를 사용하여 채용 공고의 지원 조건을 바탕으로 '필요 요소'를 JSON 형태로 추출합니다.
- 요소 레이블을 임베딩(Embedding, 벡터)으로 변환하고, 의미가 비슷한 것들을 응집형 클러스터링(Agglomerative Clustering)으로 같은 '그룹'에 묶습니다.
- 그룹별로 몇 건의 채용 공고에서 등장하는지 계산하고, Claude를 사용하여 그룹에 이름을 붙입니다.
채용 공고 145건을 분석하여 레이블이 2,074개, 그룹이 357개 만들어졌습니다 (5건 이상의 채용 공고에 등장하는 그룹은 100개입니다).
이를 통해 '어떤 요소가 많은지'는 셀 수 있게 되었지만, 요소를 하나씩 세는 것만으로는 한 건의 채용 공고가 어떤 요소들의 조합을 요구하고 있는지 파악하기 어렵습니다. 그래서 이번에는 이전 결과를 사용하여, 채용 공고 1건 각각의 인재상 전체를 유형별로 분류(클러스터링)합니다.
- 채용 공고 145건을 요구하는 인재상의 유사도에 따라 유형 분류(클러스터링)합니다.
- 유형 수를 미리 정하지 않고, 비슷한 채용 공고부터 순서대로 연결해 나가는 방법(응집형 클러스터링)을 시도합니다.
- 유형 수를 결정할 때의 기준(실루엣 계수, 안정성)을 살펴봅니다.
- 각 유형별로 '전체 대비 자주 등장하는 요소'를 조사하여 어떤 유형인지 읽어냅니다.
이 글에서는 다음 내용은 다루지 않습니다.
-
LLM에 의한 요소 추출, 요소 임베딩, 요소 그룹화 (이전 기사의 결과를 그대로 사용합니다)
-
유형 이름 붙이기 (이 글에서는 '유형 1'과 같이 번호로만 남깁니다)
-
Python의 기본 및 pandas의 기본(표 읽기나 열 계산)을 아는 분
-
클러스터링은 처음이거나 들어본 정도인 분
-
이전 기사를 읽지 않아도, 이 글만으로 따라갈 수 있도록 작성했습니다.
수식은 거의 사용하지 않고, '무엇을 하는지'와 '왜 그렇게 하는지'를 중심으로 설명합니다. 어려운 용어는 등장하는 단계에서 설명하겠습니다.
데이터에 대하여
- 원 데이터는 채용 사이트의 채용 공고 145건(지원 조건란)입니다. 이전 기사에서 진행한 처리(요소 추출 및 그룹화) 결과를 사용합니다. 입력 파일은 공개하지 않습니다.
- 채용공고 ID는
JOB_0001과 같은 순번으로 대체했습니다. - '입력 파일 형태'의 예와 각 단계별 작은 표의 예시는 설명용으로 만든 더미 데이터입니다. 그 외 출력물은 실제 데이터를 실행한 결과입니다.
총 7단계로 구성되어 있습니다. 각각 무엇을 넣고, 무엇이 가능한지 정리합니다.
| 단계 | 할 일 | 입력하는 것 | 가능한 것 |
|---|
| 0 | 준비 | 없음 | 라이브러리와 설정값 |
| ... | | | |
각 단계의 코드는 위에서부터 순서대로 연결하면 하나의 파일(job_clustering.py)이 됩니다. | | | |
이전 기사에서 만든 다음 두 파일을 data/ 폴더에 두고 사용합니다.
elements.jsonl
은 채용 공고별로 추출된 요소를 한 줄에 한 건의 공고로 저장한 것입니다.
{"job_id": "JOB_0001", "elements": [{"label": "SQL을 사용한 데이터 추출 실무 경험", "type": "경험", "strength": "필수"}, {"label": "주도적으로 과제를 찾아 움직일 수 있는 분", "type": "자세", "strength": "환영"}]}
final_summary.csv
는 그룹 이름과 그 그룹에 포함된 레이블 목록입니다. 이 글에서 사용하는 열은 group_name과 labels ( / 구분) 뿐입니다.
group_name,labels
SQL 실무 경험,"SQL을 사용한 데이터 추출 실무 경험 / SQL을 통한 데이터 가공 경험"
과제 발견·해결력,"주도적으로 과제를 찾아 움직일 수 있는 분 / 스스로 과제를 설정하여 해결할 수 있는 역량"
요소에는 타입(스킬/경험/자세)과 강도(필수/환영)가 붙어 있지만, 이 글에서는 사용하지 않습니다. '그 그룹이 채용 공고에 있는지 여부'만 확인합니다.
할 일: 라이브러리를 불러오고, 나중에 조정할 설정값들을 코드의 맨 앞에 모아서 작성합니다.
미리 모으는 이유: '5건 미만은 제외', '타입 수는 8'과 같은 숫자들을 코드 여기저기에 적어 놓으면, 값이 바뀌었을 때 수정하는 것을 잊기 쉽습니다. 위쪽에 모아두면 값을 바꾸고 테스트하기가 훨씬 수월합니다.
라이브러리는 다음 명령어로 설치할 수 있습니다.
pip install pandas numpy scipy scikit-learn matplotlib
import json
import numpy as np
import pandas as pd
...
설정의 의미는 다음과 같습니다.
| 설정 | 값 |
|---|
MIN_SUPPORT | 5 |
K_RANGE | 4~12 |
K_FINAL | 8 |
MIN_CLUSTER_SIZE | 5 |
MIN_IN_RATE | 0.3 |
그래프의 일본어 문자가 깨지지 않도록 사용할 폰트를 순서대로 찾아 설정했습니다 (찾지 못하면 글자가 사각형으로 표시됩니다).
RNG = np.random.default_rng(0)
은 난수 시드(seed)를 고정하는 것입니다. 단계 4의 '안정성'은 난수를 사용하기 때문에, 고정하지 않으면 실행할 때마다 숫자가 조금씩 달라지기 때문입니다.
할 일: 이전 두 파일을 읽어와 '어떤 공고에, 어떤 그룹이 있는지'를 한 줄씩 나열한 목록을 만듭니다.
필요한 파일: elements.jsonl (공고별 라벨)과 final_summary.csv (라벨이 어느 그룹에 속하는지).
만들 수 있는 것: 공고 ID와 그룹 이름 두 열로만 이루어진 표입니다. 예를 들어, 다음과 같이 바뀝니다.
| 공고 ID | 해당 공고의 라벨 | → | 라벨이 포함된 그룹 |
|---|
| JOB_0001 | SQL을 사용한 데이터 추출 실무 경험 | → | SQL 실무 경험 |
| JOB_0001 | 주도적으로 과제를 찾아 움직일 수 있는 분 | → | 과제 발견・해결 능력 |
이렇게 하는 이유: 여기서는 임베딩(embedding)이나 클러스터링(clustering)을 다시 수행하지 않고, 저장된 결과를 불러옵니다. 다시 수행하면 임계값(threshold)이나 난수의 차이로 인해 그룹의 구분이 약간 달라져서, 이름 붙인 그룹과 불일치할 위험이 있기 때문입니다.
with open(ELEMENTS_PATH, encoding="utf-8") as f:
records = [json.loads(line) for line in f if line.strip()]
print(f"공고 수 (추출 완료):{len(records)}건")
...
실행 결과입니다.
공고 수 (추출 완료):145건
라벨 2074개 → 그룹 357개
job_id group
...
읽을 수 있는 내용은 다음과 같습니다.
- 공고는 145건이고, 라벨은 2074개이며, 그룹은 357개입니다. 라벨 그대로라면 표기상의 차이 때문에 종류가 너무 많지만, 그룹으로 묶으면 357개로 줄어듭니다.
assert
은 '딕셔너리에 없는 라벨이 있다면 여기서 중단한다'는 확인 절차입니다. 조용히 누락된 채 진행하면 나중에 원인을 찾기가 매우 어려워집니다. - drop_duplicates()
으로, 같은 공고 내에서 동일한 그룹을 한 번만 기록했습니다. 1건의 공고가 같은 그룹을 3번 적혀 있어도, '그 공고에 존재한다'는 사실에는 변함이 없기 때문입니다.
이 단계부터는 '그룹'이라는 단어가 주역이 되므로, 여기서 다시 설명합니다.
지난 글에서 공고문에서 추출한 짧은 문장(라벨)이 2,074개 있었습니다. 라벨은 같은 의미라도 표현 방식이 다릅니다. 그래서 의미가 비슷한 라벨들을 하나로 묶은 것이 그룹입니다. 그룹화된 후, Claude가 'SQL 실무 경험'과 같은 이름을 붙였습니다.
| 라벨 (채용 공고에 적힌 표현) | 그룹 (요약 후 이름) |
|---|
| SQL을 사용한 데이터 추출 실무 경험 | → |
| SQL에 의한 데이터 가공 경험 | → |
즉, 그룹은 **'채용 공고가 요구하는 요소의 종류'**를 의미합니다. 2,074개의 라벨이 357개의 그룹으로 정리되었습니다. 이 글에서는 그룹을 '체크 항목'처럼 사용합니다. 즉, '이 채용 공고가 그 체크 항목을 요구하는지'를 확인하는 이미지입니다.
수행할 작업: 단계 1의 목록(채용 공고별로 특정 그룹을 한 줄씩 나열한 것)을 채용 공고를 행, 그룹을 열로 하는 표로 재구성합니다. 해당 채용 공고에 그 그룹이 있으면 1, 없으면 0을 넣습니다.
이렇게 하는 이유: 클러스터링은 '숫자의 배열이 비슷한지'를 통해 채용 공고들 간의 근접성을 측정합니다. 따라서 각 채용 공고 하나하나를 '숫자의 배열'로 표현할 필요가 있습니다. 0/1 표로 만들면, 한 행 자체가 그 채용 공고의 인재상(人物像)을 나타내는 숫자 배열이 됩니다.
예시: 채용 공고가 2건, 그룹이 2개일 경우, 다음과 같이 바뀝니다 (설명을 위한 더미 데이터입니다).
먼저, 재구성하기 전의 목록(단계 1의 형태)입니다.
| 채용공고ID | 그룹 |
|---|
| JOB_A | SQL실무경험 |
| ... | |
| 이것을 재구성하면 다음과 같은 0/1 표가 됩니다. | |
| 채용공고ID | SQL실무경험 | 과제발견·해결력 |
|---|
| JOB_A | 1 | 1 |
| JOB_B | 1 | 0 |
JOB_A는 [1, 1],
JOB_B는 [1, 0]
이라는 숫자 배열이 되었습니다. 실제 표는 145행 × 357열입니다.
적은 그룹을 제외하는 이유: 5건 미만의 채용 공고에만 나타나는 그룹은 우연히 1~2건의 채용 공고에 있었을 뿐, 유형을 구분할 단서가 되지 못합니다. 따라서 MIN_SUPPORT (5건) 미만인 열을 제외합니다.
job_ids = [r["job_id"] for r in records]
# crosstab으로 '채용공고 × 그룹'의 건수표를 만들고, clip(upper=1)로 0/1로 만듭니다
X_all = pd.crosstab(long_df["job_id"], long_df["group"]).clip(upper=1)
...
pd.crosstab은 '행×열' 기준으로 쌍의 개수를 세는 함수입니다. 이를 통해 채용공고 × 그룹 표가 만들어집니다. -
clip(upper=1)
은 1보다 큰 값을 1로 만듭니다. 0/1 표를 만들기 위한 예방적 처리입니다. -
reindex(job_ids, fill_value=0)
은 요소가 하나도 추출되지 않은 채용 공고도 행으로 남기는 처리입니다.
그룹 357개 → 5건 이상인 100개로 축소
그룹이 남지 않은 채용공고: 0건
[]
...
- 357개의 그룹 중, 5건 이상의 채용 공고에 나타나는 것은 100개였습니다. 나머지 257개는 소수의 채용 공고에만 나타나는 그룹입니다.
- 100개로 축소했음에도 불구하고 모든 채용 공고에 최소한 하나의 그룹이 남아있었습니다. 제외된 채용 공고는 0건입니다 (
[]와 표시되어 있습니다). - 만약 그룹이 하나도 남지 않는 채용 공고가 있다면, 다음 '코사인 거리(Cosine Distance)'를 계산할 수 없습니다. 따라서 여기서 확인하여 제거하는 처리를 넣은 것입니다.
이렇게 해서, 145행 × 100열의 0/1 표(X)가 완성되었습니다.
수행할 작업: 0/1 표의 열(그룹)마다 '그 그룹이 얼마나 희귀한지'에 따른 가중치를 부여합니다.
넣을 것: 단계 2의 0/1 표입니다.
얻게 될 것: 형태는 같은 표이지만, '1' 부분이 그룹의 희귀성에 따른 크기의 숫자가 됩니다.
이렇게 하는 이유: 예를 들어 '데이터 분석·활용 경험과 지향'은 거의 모든 채용 공고에 나타납니다. 모두가 가지고 있는 것은 채용 공고들끼리 구별하는 단서가 되지 못합니다. 반대로, '음성 기술의 연구 개발 경험'처럼 소수의 채용 공고에만 나타나는 그룹은 구별하는 단서가 됩니다. 따라서 희귀한 그룹일수록 큰 가중치를 주어 이후 '거리(Distance)' 계산에 강하게 영향을 미치게 합니다.
이러한 개념을 IDF (Inverse Document Frequency)라고 부릅니다. '자주 나오는 단어는 약하게, 희귀한 단어는 강하게' 처리하는, 문서 분석에서 자주 사용되는 가중치입니다. 가중치는 다음 공식으로 계산합니다.
idf = log(채용공고 수 ÷ 그룹의 건수) + 1
+1
이는 모든 채용 공고에 등장하는 그룹의 가중치가 0이 되어 완전히 무시되는 것을 방지하기 위함입니다.
# idf = log(채용공고 수 ÷ 그룹 건수) + 1
# 「+1」은, 모든 채용 공고에 등장하는 그룹의 가중치가 0이 되지 않도록 하기 위함
n_jobs = len(X)
...
【가중치 높은 그룹 (희귀)】
group
DB 설계・운영 스킬 4.37
...
숫자를 확인해 보겠습니다. 단 5건의 채용 공고에만 등장하는 그룹은 log(145 ÷ 5) + 1 ≒ 4.37이고, 107건의 채용 공고에 등장하는 '데이터 분석・활용 경험 및 지향'은 log(145 ÷ 107) + 1 ≒ 1.30입니다. 희귀한 그룹이 자주 나오는 그룹보다 3배 이상의 가중치를 가지고 있습니다.
수행할 작업: 채용 공고 간의 유사도를 측정하여, 비슷한 순서대로 연결해 나갑니다. 그 후, 유형의 개수(k)를 4~12로 변경하면서 분리 정도를 숫자로 확인합니다.
입력 데이터: Step 3의 가중치 표(Xw)입니다.
결과물: k별 숫자 표와 해당 그래프입니다.
이렇게 하는 이유: 이번에는 '유형이 몇 개일지'를 미리 정할 수 없습니다. 따라서 먼저 '몇 개로 나누는 것이 좋을지'에 대한 참고 기준을 숫자로 확인하는 것입니다.
| 용어 | 설명 |
|---|
| 클러스터링 (Clustering) | 비슷한 것끼리 자동으로 묶음으로 분리하는 방법입니다. 이 묶음을 '클러스터(Cluster)'라고 부릅니다. 본 글에서는 클러스터 = 유형입니다. |
| 응집형 클러스터링 (Agglomerative Clustering) | 가장 유사한 2건부터 연결하기 시작하여, 비슷한 순서대로 계속 연결해 나가는 방법입니다. 마지막에는 전부가 하나의 그룹이 됩니다. 연결을 중간에 멈춘 개수가 유형 수가 됩니다. 사전에 유형 수를 정할 필요가 없다는 것이 장점입니다. |
| 코사인 거리 (Cosine Distance) | 채용 공고를 숫자의 배열(벡터, Arrow)로 간주하고, 벡터의 방향이 얼마나 다른지를 측정하는 거리입니다. 예를 들어, 채용 공고 A가 'SQL・과제 발견', 채용 공고 B가 'SQL・과제 발견・음성'이라면, 공통 요소가 많아 가깝습니다(거리는 작음). 만약 채용 공고 C가 '영어 논문・이미지 인식'이고 A와 공통 요소가 없다면, 거리는 최대값인 1이 됩니다. 요소의 개수 적고 많은지를 보는 것이 아니라, 조합의 유사성을 볼 수 있습니다. |
| 평균 연결법 (Average Linkage Method) | 연결된 그룹들 사이의 거리를 멤버 간 거리의 평균으로 측정하는 방식입니다. |
| 실루엣 계수 (Silhouette Coefficient) | 분리 정도가 얼마나 좋은지를 나타내는 숫자입니다. -1부터 1까지의 값이며, 1에 가까울수록 유형 내부가 가깝고 유형 간이 멀다(깔끔하게 분리되어 있다)는 것을 의미합니다. |
| 안정성 (ARI: Adjusted Rand Index) | 채용 공고를 80%만 골라 다시 만들어도 같은 분리 방식이 나오는지 보는 숫자입니다. 두 가지 분리 방식이 얼마나 일치하는지를 ARI라는 지표로 측정하며, 30번 반복한 평균을 확인합니다. 1이면 완벽하게 일치하고, 0 근처는 우연 수준과 같습니다. |
def make_linkage(M):
# 코사인 거리: 라벨의 개수가 아닌 '어떤 조합인가(방향)'가 유사한지를 봄
dist = pdist(M.values, metric="cosine")
...
코드의 핵심 포인트는 다음과 같습니다.
pdist(..., metric="cosine")를 사용하여 채용 공고 간의 코사인 거리를 측정합니다.
method="average" (평균 연결법)을 사용했습니다. 0/1 데이터에 적합하며, 극단적으로 치우친 유형이 되기 어렵다고 판단하여 선택했습니다(필자의 판단입니다).
ward 방식은 사용하지 않았습니다. Ward 방법은 유클리드 거리를 사용하는 것이 전제되기 때문입니다.
fcluster(..., criterion="maxclust")를 사용하여 연결된 결과를 'k개로 나눌 지점'에서 멈추고, 각 채용 공고에 유형 번호를 부여합니다.
실행 결과는 다음과 같습니다.
k silhouette stability_ARI 최소 유형 개수
4 0.062 0.651 6
5 0.059 0.616 4
...
읽어낼 수 있는 내용은 다음과 같습니다.
- 실루엣 계수는 어떤 k에서도 0.06~0.11로 낮습니다. 유형의 경계가 그다지 명확하지 않다고 해석할 수 있습니다(필자의 해석입니다).
- 안정성 (ARI)는 k=4와 k=5에서 0.6대, k=6 이후는 0.4 전후입니다. k를 늘릴수록 재구성했을 때의 분리 방식이 바뀌기 쉬워집니다.
- k=5 이후부터는 최소 유형이 4건으로 유지됩니다. 소수의 채용 공고만으로 이루어진 작은 유형들이 계속해서 잘려 나오고 있습니다.
어떤 숫자도 결정적인 지표가 될 수 없습니다. 그래서 미리 가늠했던 6~10개 중 k=8을 선택했습니다. 최종적으로는 '타입의 특징을 읽어내서 이름을 붙일 수 있을지'로 판단하는 방식입니다.
할 일: 단계 4에서 연결한 과정을 나무 모양의 그림(덴드로그램(Dendrogram))으로 그립니다.
넣을 것: 단계 3의 가중치 표입니다.
얻을 수 있는 것: 어떤 채용 공고(묶음)가 어느 높이에서 연결되었는지 보여주는 그림입니다.
왜 보는가: 이 그림을 보면 '어디를 잘라야 자연스럽게 그룹이 나뉠지' 눈으로 알 수 있습니다.
그림 읽는 법: 아래에 나열된 것이 채용 공고(묶음)이며, 가로선 높이가 연결되었을 때의 거리입니다. 낮은 위치에서 연결된 것일수록 비슷합니다. 높은 위치까지 연결되지 않고 남은 것은 서로 유사하지 않다는 의미입니다.
Z_full = make_linkage(Xw)
fig, ax = plt.subplots(figsize=(10, 3.5))
# truncate_mode="lastp":마지막 p번의 결합만 표시합니다 (145개의 잎을 모두 그리면 읽기 어렵기 때문)
...
채용 공고가 145건이므로 전부 그리자면 보기 어렵습니다. truncate_mode="lastp"는 마지막 p번의 연결만을 그리도록 지정하는 것입니다. 여기서는 마지막 20번을 그리고 있습니다.
아래 괄호 안의 숫자는 그곳에 모여 있는 채용 공고 건수입니다. 세로축은 코사인 거리(Cosine Distance)이며, 위쪽에서 연결된 부분이 거의 0.8~0.92 범위에 몰려있습니다. 거리가 0.8 이상이라는 것은 공통 요소가 상당히 적다는 상태를 의미합니다. 어느 높이에서 잘라도 '여기서 크게 단차가 있다'는 지점이 없습니다. 이는 단계 4에서 실루엣 계수(Silhouette Coefficient)가 낮았다는 것과 일치합니다.
즉, 이번 데이터는 '뚜렷하게 나뉜 타입'이라기보다는, '연속적으로 조금씩 다른 채용 공고를 임시로 몇 개로 나눈' 성격이 강하다고 생각됩니다. 이 점은 결과 해석 시 염두에 두어야 합니다.
할 일: 덴드로그램을 '8개로 나뉘는 지점'에서 잘라 각 채용 공고에 타입 번호를 매깁니다.
넣을 것: 단계 3의 가중치 표와 타입 수 K_FINAL (8)입니다.
얻을 수 있는 것: 채용 공고 145건 각각에 타입 번호(1~7, 또는 0)가 붙은 목록입니다.
번호 매기는 법: 건수가 많은 순서대로 타입 1, 타입 2...로 재배치합니다. MIN_CLUSTER_SIZE (5건) 미만인 작은 타입은 무리하게 어딘가에 넣지 않고 **'기타'(타입 0)**로 남겨둡니다. 넣어버리면 그 타입의 윤곽이 흐려지기 때문입니다.
_, raw_labels = cut(Xw, K_FINAL)
# 건수가 많은 순서대로 타입 번호를 재배치합니다. MIN_CLUSTER_SIZE 건 미만인 타입은 0 (기타)로 처리
sizes = pd.Series(raw_labels).value_counts()
...
k=8로 자르니, 5건 이상의 타입이 총 7개와 '기타' 4개가 되었습니다. 건수는 다음과 같습니다.
타입 1이 전체의 40% 이상을 차지하고 있습니다. 큰 타입을 형성하는 그룹에는 '그 외에 들어가지 않은 채용 공고'가 모이기 쉽다는 점에 주의해야 합니다.
할 일: 타입별로 '전체 대비 많이 나오는 그룹'을 조사합니다. 이를 보고, 해당 타입이 어떤 인재상을 요구하고 있는지 읽어냅니다.
넣을 것: 단계 2의 0/1 표 (X)와 단계 6의 타입 번호입니다.
얻을 수 있는 것: 타입별 특징 그룹 순위표입니다.
왜 '리프트(Lift)'를 사용하는가: 단순히 '타입 내 등장 비율(출현율)'이 높은 순서대로 나열하면, 전체에서도 자주 나오는 그룹(예: '데이터 분석/활용 경험 및 지향')은 모든 타입에서 상위에 오게 되어 타입 간의 차이를 알 수 없습니다. 그래서 리프트 = 타입 내 등장률 ÷ 전체 등장률을 사용합니다.
예를 들어, 전체에서는 10%의 채용 공고에만 나오는 그룹이 특정 타입에서는 50%의 채용 공고에 나온다면, 리프트는 5가 됩니다. 이는 '전체보다 5배 더 많이 나올 가능성이 있다'는 의미로, 해당 타입의 특징이라고 할 수 있습니다. 자주 나오는 공통 그룹은 어떤 타입에서도 리프트가 1에 가까워지기 때문에 특징으로는 나타나지 않습니다. 이것이 리프트를 사용하는 목적입니다.
리프트 (Lift) = 타입 내 등장률 ÷ 전체 등장률. 2라면 "전체보다 2배 더 나오기 쉬움"
overall = X.mean()
def type_profile(tid, top_n=TOP_N):
...
실행 결과 (타입별 상위 8개 그룹)
==== 타입1 (62건) ====
타입 내 등장률 전체 등장률 리프트
group
...
출력 결과를 바탕으로 각 타입의 특징을 표로 정리했습니다. '읽어낼 수 있는 경향'은 특징 그룹을 보고 필자가 가정한 것이며, 코드가 도출한 결과가 아닙니다.
| 타입 | 건수 | 특히 두드러지게 나타나는 그룹(리프트) | 읽어낼 수 있는 경향(필자의 추론) |
|---|
| 1 | 62 | 과제 발견・해결력 (2.05), 커뮤니케이션 능력 (1.66) | 기술적 요구사항은 전체와 거의 같습니다. 차이가 나는 부분은 업무 진행 방식이며, 가장 큰 표준적인 타입입니다 |
| ... | | | |
| 결과를 읽을 때 주의할 점 | | | |
작은 타입일수록 리프트가 크게 나올 수 있습니다. 예를 들어 타입 7은 6건이므로, 1건의 차이가 등장률을 약 17포인트 움직입니다. 리프트가 20을 넘어도 "6건 중 5건에 있음"이라는 의미입니다. -
이전 단계에서 보았듯이, 타입 간의 경계는 명확하지 않습니다. '타입 1~7은 확실히 다른 인물상'이라고 읽기보다는, '채용 공고를 특징의 출현 방식에 따라 임시로 나눈 것'이라고 해석하는 것이 안전합니다.
-
원본이 된 그룹은 LLM으로 추출하여 정리한 것입니다. 타입 구분의 방식에는 이전의 추출이나 그룹화 과정도 영향을 미칩니다. 지난 기사에서도 1위 그룹에만 196개의 라벨이 들어가 의미의 폭이 너무 넓거나, 1건짜리 그룹이 많다는 등 세분화(granularity) 측면의 과제가 발견되었습니다.
-
채용 공고 × 그룹의 0/1 표에 IDF 가중치를 적용하고, 코사인 거리와 평균 연결법의 응집형 클러스터링으로, 채용 공고 145건을 타입별로 분류했습니다.
-
타입 수의 기준(실루엣 계수, 안정성)은 결정적이지 않았으며, 최종적으로는 특징 그룹을 보고 해석할 수 있는지 여부로 k=8을 선택했습니다.
-
7개의 타입과 소수의 '기타'로 나뉘었습니다. 가장 큰 타입 1은 공통적인 요구사항이 중심이며, 작은 타입(4~7)에는 연구 개발, 클라이언트 대상 분석, 선진적인 개발력 등의 특징이 보였습니다.
-
계층적 트리 다이어그램이 평평하고 실루엣 계수도 낮기 때문에, '뚜렷하게 구분된 타입'은 아니라는 점에 주의가 필요합니다.