new-value-analysis 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- new_value_analysis/__init__.py +14 -0
- new_value_analysis/action_finder/__init__.py +8 -0
- new_value_analysis/action_finder/find_topicnum.py +91 -0
- new_value_analysis/action_finder/select_action_number.py +33 -0
- new_value_analysis/action_finder/topic_modeling.py +34 -0
- new_value_analysis/actor_finder/__init__.py +9 -0
- new_value_analysis/actor_finder/doc2vec.py +79 -0
- new_value_analysis/actor_finder/find_right_silhouette.py +47 -0
- new_value_analysis/actor_finder/silhouette_plot.py +55 -0
- new_value_analysis/actor_finder/visualize_dendrogram.py +50 -0
- new_value_analysis/opportunity_area_analysis/__init__.py +7 -0
- new_value_analysis/opportunity_area_analysis/opportunity_plot.py +153 -0
- new_value_analysis/opportunity_area_analysis/satisfaction_scaling.py +27 -0
- new_value_analysis-0.1.0.dist-info/METADATA +42 -0
- new_value_analysis-0.1.0.dist-info/RECORD +18 -0
- new_value_analysis-0.1.0.dist-info/WHEEL +5 -0
- new_value_analysis-0.1.0.dist-info/licenses/LICENSE +21 -0
- new_value_analysis-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""new_value_analysis
|
|
2
|
+
|
|
3
|
+
New Value Analysis(신규가치분석)용 유틸리티 패키지입니다.
|
|
4
|
+
|
|
5
|
+
구성
|
|
6
|
+
- action_finder: 토픽 모델링 기반 Action 추출/할당
|
|
7
|
+
- actor_finder: 문서 임베딩(Doc2Vec) + 군집 진단
|
|
8
|
+
- opportunity_area_analysis: 기회영역 스케일링/시각화
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from . import action_finder, actor_finder, opportunity_area_analysis
|
|
12
|
+
|
|
13
|
+
__all__ = ["action_finder", "actor_finder", "opportunity_area_analysis"]
|
|
14
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""토픽 개수(토픽 수) 탐색/평가 관련 함수 모음.
|
|
2
|
+
|
|
3
|
+
- Perplexity, Coherence 등 지표를 이용해 적절한 토픽 개수를 탐색합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def lda_perplexity_coherence_plot(
|
|
7
|
+
corpus,
|
|
8
|
+
word_dict,
|
|
9
|
+
all_documents,
|
|
10
|
+
fixed_topn=10, # 각 토픽에서 고려할 상위 단어 개수(10개로 고정)
|
|
11
|
+
topic_range=range(2,10),
|
|
12
|
+
passes=10,
|
|
13
|
+
iterations=20,
|
|
14
|
+
random_state=2025,
|
|
15
|
+
figsize=(12,5)
|
|
16
|
+
):
|
|
17
|
+
import gensim
|
|
18
|
+
from gensim.models import CoherenceModel
|
|
19
|
+
from tqdm.auto import tqdm
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
|
|
22
|
+
perplexity_values = []
|
|
23
|
+
coherence_values = []
|
|
24
|
+
|
|
25
|
+
# 2 ~ 9까지의 토픽 개수에 대해 perplexity & coherence 계산
|
|
26
|
+
for i in tqdm(topic_range):
|
|
27
|
+
# 1. LDA 모델 생성(코퍼스,단어사전,생성할 토픽의 개수,토픽재할당기준 2가지, random_state)
|
|
28
|
+
ldamodel = gensim.models.ldamodel.LdaModel(
|
|
29
|
+
corpus, # 각 문서를 단어 빈도수(BOW) 형태로 표현한 코퍼스
|
|
30
|
+
id2word = word_dict, # 단어 사전 : 각 단어의 고유 ID와 실제 단어를 매핑하는 딕셔너리 객체(초기 할당에 활용)
|
|
31
|
+
num_topics = i, # 생성할 토픽의
|
|
32
|
+
|
|
33
|
+
# passes, iterations : 두 인자를 통해 LDA모델은 문서(리뷰)와 단어의 토픽 할당을 더욱 정교하게 조정
|
|
34
|
+
passes = passes, # 전체 코퍼스를 대상으로 토픽 재할당 과정을 10번 반복
|
|
35
|
+
# passes 값이 높을수록 모델은 전체 데이터에 대해 여러번 학습하며
|
|
36
|
+
# 전반적인 토픽 구조를 더 안정적으로 추정할 수 있으나, 계산 비용이 증가함
|
|
37
|
+
|
|
38
|
+
iterations = iterations, # 각 문서(리뷰) 내에서 토픽 할당을 최적화하는 내부 알고리즘을 20번 반복 수행
|
|
39
|
+
# iterations 값이 높으면 각 문서의 토픽 할당이 더욱 세밀하게 조정되지만,
|
|
40
|
+
# 과도한 반복은 과적합이나 계산 시간 증가 위험이 있음
|
|
41
|
+
|
|
42
|
+
random_state = random_state # 난수 시드 값 : 결과 재현성을 위해 고정
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 2. Perplexity 계산(perplexity_score 변수에 담아주기)
|
|
46
|
+
perplexity_score = ldamodel.log_perplexity(corpus)
|
|
47
|
+
|
|
48
|
+
# 3. 2번에서 구한 값 perplexity_values 에 추가하기
|
|
49
|
+
perplexity_values.append(perplexity_score)
|
|
50
|
+
|
|
51
|
+
# 4. Coherence 모델 초기화
|
|
52
|
+
coherence_model = CoherenceModel(
|
|
53
|
+
model = ldamodel, # 평가 대상 LDA 모델 : 이 모델에서 생성된 토픽들의 일관성을 측정함
|
|
54
|
+
texts = all_documents, # 토큰화된 문서 리스트 : 모델 생성에 사용된 문서(리뷰)들을 기반으로 토픽 간
|
|
55
|
+
# 단어 관련성을 평가함
|
|
56
|
+
dictionary = word_dict, # 단어 사전 : 각 단어와 고유 ID의 매핑 정보를 제공, 텍스트 내 단어들이 유효한지
|
|
57
|
+
# 확인하는 역할
|
|
58
|
+
topn = fixed_topn # 각 토픽에서 고려할 상위 단어의 개수 : 토픽의 대표 단어들을 선택하여
|
|
59
|
+
# 일관성 점수를 산출하는데 사용됨
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 5. Coherence 계산(coherence_score 변수에 담아주기)
|
|
63
|
+
coherence_score = coherence_model.get_coherence()
|
|
64
|
+
|
|
65
|
+
# 6. 5번에서 구한 값 coherence_values 에 추가하기
|
|
66
|
+
coherence_values.append(coherence_score)
|
|
67
|
+
|
|
68
|
+
x = list(topic_range) # 토픽 개수(2~9)
|
|
69
|
+
|
|
70
|
+
# 하나의 Figure 안에 여러 개의 서브 플롯(axes)을 생성
|
|
71
|
+
fig, ax = plt.subplots(
|
|
72
|
+
1,2, # 1행 2열의 서브 플롯을 생성
|
|
73
|
+
figsize=figsize # 전체 Figure 크기를 가로 12인치, 세로 5인치로 설정
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Perplexity 시각화
|
|
77
|
+
# ax[0] : 0번째 서브 플롯
|
|
78
|
+
ax[0].plot(x, perplexity_values, marker='o', linestyle='-')
|
|
79
|
+
ax[0].set_xlabel('Topic Counts')
|
|
80
|
+
ax[0].set_ylabel('Perplexity scores')
|
|
81
|
+
ax[0].set_title('Perplexity Changes')
|
|
82
|
+
|
|
83
|
+
# Coherence 시각화
|
|
84
|
+
# ax[1] : 1번째 서브 플롯
|
|
85
|
+
ax[1].plot(x, coherence_values, marker='o', linestyle='-', color='orange')
|
|
86
|
+
ax[1].set_xlabel('Topic Counts')
|
|
87
|
+
ax[1].set_ylabel('Coherence Scores')
|
|
88
|
+
ax[1].set_title('Coherence Changes')
|
|
89
|
+
|
|
90
|
+
plt.tight_layout() # 서브 플롯 간의 레이아웃 간격을 자동으로 조정하여 겹치지 않도록
|
|
91
|
+
plt.show()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""문서(리뷰)별 토픽 분포에서 최빈/최대 확률 토픽(행동 번호)을 할당하는 함수 모음.
|
|
2
|
+
|
|
3
|
+
- 문서별 토픽 분포를 순회하며 가장 높은 확률의 토픽 번호를 선택합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def assign_action_number(ldamodel, corpus):
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
# 각 문서(리뷰)에서 가장 높은 확률을 가진 토픽 번호(액션 번호)를 저장할 리스트
|
|
10
|
+
action_number_assignment = []
|
|
11
|
+
|
|
12
|
+
# 모든 문서(리뷰)에 대해 토픽 분포를 순회하며, 가장 높은 확률의 토픽(행동) 번호를 선택
|
|
13
|
+
for doc_topic_distribution in ldamodel.get_document_topics(corpus):
|
|
14
|
+
# ldamodel.get_document_topics(corpus) : 각 문서(리뷰)의 토픽 분포
|
|
15
|
+
# print(doc_topic_distribution)
|
|
16
|
+
|
|
17
|
+
topic_ids = [] # 문서(리뷰)에 할당된 토픽 번호들을 저장할 리스트
|
|
18
|
+
topic_probabilities = [] # 각 토픽 확률들을 저장할 리스트
|
|
19
|
+
|
|
20
|
+
# 각(토픽,확률) 튜플 분해
|
|
21
|
+
for topic_info in doc_topic_distribution:
|
|
22
|
+
# doc_topic_distribution : 토픽 분포의 토픽과 토픽 확률에 대한 튜플
|
|
23
|
+
topic_ids.append(topic_info[0]) # 토픽 번호
|
|
24
|
+
topic_probabilities.append(topic_info[1]) # 해당 토픽의 확률 값
|
|
25
|
+
|
|
26
|
+
# 각 토픽에서 가장 높은 확률을 가진 토픽의 인덱스 찾기
|
|
27
|
+
max_index = np.argmax(topic_probabilities)
|
|
28
|
+
selected_action = topic_ids[max_index] # 가장 높은 확률의 토픽 번호 선택
|
|
29
|
+
|
|
30
|
+
# 선택 된 토픽 번호(액션 번호)를 결과 리스트에 추가
|
|
31
|
+
action_number_assignment.append(selected_action)
|
|
32
|
+
|
|
33
|
+
return action_number_assignment
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""토픽 모델링(LDA) 학습 관련 함수 모음.
|
|
2
|
+
|
|
3
|
+
- LDA 모델 학습 및 관련 결과를 생성하는 기능을 제공합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def LDA_train(
|
|
7
|
+
corpus,
|
|
8
|
+
word_dict,
|
|
9
|
+
topic_num=3, # 생성할 토픽의 개수를 3으로 설정(임의로 설정)
|
|
10
|
+
passes=10,
|
|
11
|
+
iterations=20,
|
|
12
|
+
random_state=2025
|
|
13
|
+
):
|
|
14
|
+
import gensim
|
|
15
|
+
|
|
16
|
+
# LDA 모델 생성 : 코퍼스 내 문서(리뷰)들을 대상으로 토픽을 추출
|
|
17
|
+
ldamodel = gensim.models.ldamodel.LdaModel(
|
|
18
|
+
corpus, # 각 문서를 단어 빈도수(BOW) 형태로 표현한 코퍼스
|
|
19
|
+
id2word = word_dict, # 단어 사전 : 각 단어의 고유 ID와 실제 단어를 매핑하는 딕셔너리 객체(초기 할당에 활용)
|
|
20
|
+
num_topics = topic_num, # 생성할 토픽의
|
|
21
|
+
|
|
22
|
+
# passes, iterations : 두 인자를 통해 LDA모델은 문서(리뷰)와 단어의 토픽 할당을 더욱 정교하게 조정
|
|
23
|
+
passes = passes, # 전체 코퍼스를 대상으로 토픽 재할당 과정을 10번 반복
|
|
24
|
+
# passes 값이 높을수록 모델은 전체 데이터에 대해 여러번 학습하며
|
|
25
|
+
# 전반적인 토픽 구조를 더 안정적으로 추정할 수 있으나, 계산 비용이 증가함
|
|
26
|
+
|
|
27
|
+
iterations = iterations, # 각 문서(리뷰) 내에서 토픽 할당을 최적화하는 내부 알고리즘을 20번 반복 수행
|
|
28
|
+
# iterations 값이 높으면 각 문서의 토픽 할당이 더욱 세밀하게 조정되지만,
|
|
29
|
+
# 과도한 반복은 과적합이나 계산 시간 증가 위험이 있음
|
|
30
|
+
|
|
31
|
+
random_state = random_state # 난수 시드 값 : 결과 재현성을 위해 고정
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return ldamodel
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Doc2Vec 기반 문서 임베딩 학습 함수 모음.
|
|
2
|
+
|
|
3
|
+
- 토큰화된 문서(리뷰)로 Doc2Vec 모델을 학습하고 문서 벡터를 추출합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def train_doc2vec_module(
|
|
10
|
+
df,
|
|
11
|
+
token_col="tagged_review",
|
|
12
|
+
vector_size=100,
|
|
13
|
+
alpha=0.025,
|
|
14
|
+
min_alpha=0.001,
|
|
15
|
+
window=8,
|
|
16
|
+
epochs=5,
|
|
17
|
+
tag_prefix="document"
|
|
18
|
+
):
|
|
19
|
+
# 자연어처리, 토픽 모델링을 위한 라이브러리
|
|
20
|
+
import gensim
|
|
21
|
+
from gensim.models.doc2vec import TaggedDocument # 문서 태깅 클래스
|
|
22
|
+
from gensim.models import doc2vec
|
|
23
|
+
"""
|
|
24
|
+
df[token_col]에 토큰 리스트가 들어있다고 가정하고,
|
|
25
|
+
Doc2Vec 학습 + 문서벡터 리스트를 반환하는 모듈.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
model : gensim.models.doc2vec.Doc2Vec
|
|
30
|
+
vector_list : List[np.ndarray]
|
|
31
|
+
tagged_corpus_list : List[TaggedDocument]
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# TaggedDocument 예시
|
|
35
|
+
TaggedDocument(
|
|
36
|
+
tags = ['document'], # 각 문서를 구분할 수 있는 태그(문서 ID)
|
|
37
|
+
words = df[token_col][1] # 해당 문서를 구성하는 단어(토큰) 목록
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# tags의 경우, 리스트로 패킹해서 여러 문서 태그를 부여할 수도 있지만,
|
|
41
|
+
# 보통은 문서별로 하나의 태그(예 : 'document 1')만 사용하는 편이 많음
|
|
42
|
+
|
|
43
|
+
tagged_corpus_list = [] # 문서 태깅 결과를 담아둘 리스트
|
|
44
|
+
|
|
45
|
+
# 각 문서에 접근하여 문서 ID 태깅
|
|
46
|
+
for i, tokens in enumerate(df[token_col]):
|
|
47
|
+
tag = f"{tag_prefix} {i}"
|
|
48
|
+
tagged_doc = TaggedDocument(
|
|
49
|
+
tags = [tag],
|
|
50
|
+
words = tokens
|
|
51
|
+
)
|
|
52
|
+
tagged_corpus_list.append(tagged_doc)
|
|
53
|
+
|
|
54
|
+
# 1. doc2vec 모델 초기화
|
|
55
|
+
model = doc2vec.Doc2Vec(
|
|
56
|
+
vector_size = vector_size, # 학습할 벡터(임베딩)의 차원 수(예 : 100차원)
|
|
57
|
+
alpha = alpha, # 초기 학습률(learning rate)
|
|
58
|
+
min_alpha = min_alpha, # 학습 진행 중에 점차 감소시킬 최소 학습률
|
|
59
|
+
window = window # 주변 단어(window) 범위 설정(앞뒤 8개 단어)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 2. 단어 사전(vocabulary) 생성
|
|
63
|
+
# TaggedDocument 리스트를 토대로 전체에 등장하는 단어들을 파악하고 모델에 등록
|
|
64
|
+
model.build_vocab(tagged_corpus_list)
|
|
65
|
+
|
|
66
|
+
# doc2vec 모델 학습 - 문서 벡터와 단어 벡터를 기반으로 학습
|
|
67
|
+
model.train(
|
|
68
|
+
tagged_corpus_list,
|
|
69
|
+
total_examples = model.corpus_count, # 전체 문서 개수
|
|
70
|
+
epochs = epochs # 학습 반복 횟수(너무 크면 과적합의 가능성 / 학습속도 느려짐)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
vector_list = [] # 문서 벡터들을 담아둘 리스트
|
|
74
|
+
|
|
75
|
+
for i in range(len(df)):
|
|
76
|
+
doc2vec_vector = model.dv[f'{tag_prefix} {i}'] # 각 문서 벡터에 접근
|
|
77
|
+
vector_list.append(doc2vec_vector)
|
|
78
|
+
|
|
79
|
+
return model, vector_list, tagged_corpus_list
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""계층적 군집(AgglomerativeClustering)에서 실루엣 점수 기반 최적 군집 수 탐색.
|
|
2
|
+
|
|
3
|
+
- 군집 수(n_clusters)를 변화시키며 실루엣 점수를 계산하고 적절한 값을 찾습니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def agglomerative_silhouette_module(
|
|
7
|
+
df,
|
|
8
|
+
vector_col="vector",
|
|
9
|
+
n_clusters=2,
|
|
10
|
+
linkage="ward",
|
|
11
|
+
n_cluster_range=range(2, 10),
|
|
12
|
+
plot=True
|
|
13
|
+
):
|
|
14
|
+
from sklearn.cluster import AgglomerativeClustering # 병합 군집, 어글로머레이티브
|
|
15
|
+
from sklearn.metrics.cluster import silhouette_score # 실루엣 계수 평균값 계산 도구
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
# 1. 클러스터 초기화
|
|
19
|
+
cluster = AgglomerativeClustering(n_clusters=n_clusters, linkage=linkage)
|
|
20
|
+
|
|
21
|
+
# 2. 클러스터링(군집화)
|
|
22
|
+
cluster_label = cluster.fit_predict(list(df[vector_col])) # 군집 알고리즘에서 요구하는 입력포맷에 맞에 형변환
|
|
23
|
+
|
|
24
|
+
# 3. 평균 실루엣 지수 계산
|
|
25
|
+
silhouette_avg = silhouette_score(list(df[vector_col]), cluster_label)
|
|
26
|
+
|
|
27
|
+
# -1 ~ 1 사이의 값, 1에 가까울수록 잘 군집하였다고 판단
|
|
28
|
+
|
|
29
|
+
# 각 클러스터별 평균 실루엣 지수 확인
|
|
30
|
+
n_cluster = n_cluster_range # 클러스터 범위
|
|
31
|
+
scores = [] # 각 클러스터 별 실루엣 지수를 담아둘 리스트
|
|
32
|
+
|
|
33
|
+
for n in n_cluster:
|
|
34
|
+
cluster = AgglomerativeClustering(n_clusters=n, linkage=linkage) # 1. 클러스터 초기화
|
|
35
|
+
cluster_labels = cluster.fit_predict(list(df[vector_col])) # 2. 클러스터링
|
|
36
|
+
silhouette_avg_n = silhouette_score(list(df[vector_col]), cluster_labels) # 3. 평균 실루엣 지수 계산
|
|
37
|
+
|
|
38
|
+
scores.append(silhouette_avg_n)
|
|
39
|
+
|
|
40
|
+
# 각 클러스터별 평균 실루엣 지수 시각화
|
|
41
|
+
if plot:
|
|
42
|
+
plt.plot(list(n_cluster), scores)
|
|
43
|
+
plt.xlabel('Number of Clusters')
|
|
44
|
+
plt.ylabel('Silhouette Score')
|
|
45
|
+
plt.show()
|
|
46
|
+
|
|
47
|
+
# 군집 개수 3이 가장 높은 평균 실루엣 지수를 보임 (데이터에 따라 달라짐)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""KMeans 기반 실루엣 플롯 시각화 함수 모음.
|
|
2
|
+
|
|
3
|
+
- 여러 K 값에 대해 실루엣 플롯을 그려 군집 품질을 비교합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
import numpy as np
|
|
8
|
+
import matplotlib.cm as cm
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
from sklearn.datasets import make_blobs
|
|
11
|
+
from sklearn.cluster import KMeans
|
|
12
|
+
from sklearn.metrics import silhouette_samples, silhouette_score
|
|
13
|
+
|
|
14
|
+
def visualize_silhouette(cluster_lists, X_features):
|
|
15
|
+
# 입력값으로 클러스터링 갯수들을 리스트로 받아서, 각 갯수별로 클러스터링을 적용하고 실루엣 개수를 구함
|
|
16
|
+
n_cols = len(cluster_lists)
|
|
17
|
+
|
|
18
|
+
# plt.subplots()으로 리스트에 기재된 클러스터링 수만큼의 sub figures를 가지는 axs 생성
|
|
19
|
+
fig, axs = plt.subplots(figsize=(4*n_cols, 4), nrows=1, ncols=n_cols)
|
|
20
|
+
|
|
21
|
+
# 리스트에 기재된 클러스터링 갯수들을 차례로 iteration 수행하면서 실루엣 개수 시각화
|
|
22
|
+
for ind, n_cluster in enumerate(cluster_lists):
|
|
23
|
+
|
|
24
|
+
# KMeans 클러스터링 수행하고, 실루엣 스코어와 개별 데이터의 실루엣 값 계산.
|
|
25
|
+
clusterer = KMeans(n_clusters = n_cluster, max_iter=500, random_state=0)
|
|
26
|
+
cluster_labels = clusterer.fit_predict(X_features)
|
|
27
|
+
|
|
28
|
+
sil_avg = silhouette_score(X_features, cluster_labels)
|
|
29
|
+
sil_values = silhouette_samples(X_features, cluster_labels)
|
|
30
|
+
|
|
31
|
+
y_lower = 10
|
|
32
|
+
axs[ind].set_title('Number of Cluster : '+ str(n_cluster)+'\n' \
|
|
33
|
+
'Silhouette Score :' + str(round(sil_avg,3)) )
|
|
34
|
+
axs[ind].set_xlabel("The silhouette coefficient values")
|
|
35
|
+
axs[ind].set_ylabel("Cluster label")
|
|
36
|
+
axs[ind].set_xlim([-0.1, 1])
|
|
37
|
+
axs[ind].set_ylim([0, len(X_features) + (n_cluster + 1) * 10])
|
|
38
|
+
axs[ind].set_yticks([])
|
|
39
|
+
axs[ind].set_xticks([0, 0.2, 0.4, 0.6, 0.8, 1])
|
|
40
|
+
|
|
41
|
+
# 클러스터링 갯수별로 fill_betweenx( )형태의 막대 그래프 표현.
|
|
42
|
+
for i in range(n_cluster):
|
|
43
|
+
ith_cluster_sil_values = sil_values[cluster_labels==i]
|
|
44
|
+
ith_cluster_sil_values.sort()
|
|
45
|
+
|
|
46
|
+
size_cluster_i = ith_cluster_sil_values.shape[0]
|
|
47
|
+
y_upper = y_lower + size_cluster_i
|
|
48
|
+
|
|
49
|
+
color = cm.nipy_spectral(float(i) / n_cluster)
|
|
50
|
+
axs[ind].fill_betweenx(np.arange(y_lower, y_upper), 0, ith_cluster_sil_values, \
|
|
51
|
+
facecolor=color, edgecolor=color, alpha=0.7)
|
|
52
|
+
axs[ind].text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
|
|
53
|
+
y_lower = y_upper + 10
|
|
54
|
+
|
|
55
|
+
axs[ind].axvline(x=sil_avg, color="red", linestyle="--")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""계층적 군집(덴드로그램) 시각화 함수 모음.
|
|
2
|
+
|
|
3
|
+
- linkage 결과를 기반으로 덴드로그램을 출력합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def plot_dendrogram(
|
|
7
|
+
df,
|
|
8
|
+
vector_col="vector",
|
|
9
|
+
method="ward",
|
|
10
|
+
figsize=(15, 8),
|
|
11
|
+
orientation="top",
|
|
12
|
+
distance_sort="descending",
|
|
13
|
+
show_leaf_counts=True,
|
|
14
|
+
color_threshold=18,
|
|
15
|
+
show=True
|
|
16
|
+
):
|
|
17
|
+
from scipy.cluster.hierarchy import dendrogram, linkage
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
|
|
20
|
+
# dendrogram : 계층적 군집 결과(linked)를 트리 형태로 시각화
|
|
21
|
+
# linkage : 계층적 군집(Hierarchical Clustering)을 수행
|
|
22
|
+
|
|
23
|
+
# 계층적 군집 수행
|
|
24
|
+
linked = linkage(
|
|
25
|
+
list(df[vector_col]), # 군집화 할 데이터 (군집 알고리즘에서 요구하는 입력포맷에 맞게 형변환)
|
|
26
|
+
# linkage : 2차원 배열형태 데이터 요구(지금 데이터는 vector리스트 담겨있기때문에 그대로 사용해도됨)
|
|
27
|
+
method # 군집 방식 지정('ward' -> 군집 내 분산을 최소화)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# 덴드로그램 시각화
|
|
31
|
+
plt.figure(figsize=figsize)
|
|
32
|
+
|
|
33
|
+
dendrogram(
|
|
34
|
+
linked, # 계층적 군집 결과
|
|
35
|
+
orientation=orientation, # 덴드로그램의 방향(위에서 아래로 그리기(left,right,bottom))
|
|
36
|
+
distance_sort=distance_sort, # 클러스터 간 거리를 기준으로 내림차순 정렬
|
|
37
|
+
show_leaf_counts=show_leaf_counts, # 리프 노드(최종 말단)마다 샘플 개수 표시 여부
|
|
38
|
+
color_threshold=color_threshold # y축 기준 -> 18 이상은 동일 색상(군집)으로 묶이지 않도록 설정
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if show:
|
|
42
|
+
plt.show()
|
|
43
|
+
|
|
44
|
+
# y축 값이 낮은 지점에서 클러스터링 된 문서들은 서로 비슷한 내용일 가능성이 높음
|
|
45
|
+
# y축 값이 높은 곳에서 합쳐지는 문서들은 거리가 멀었다가 뒤늦게 합쳐진 것이므로, 서로 상이한 주제를 가질 수 있음
|
|
46
|
+
|
|
47
|
+
# 덴드로그램에서 threshold를 기준으로 크게 2개의 군집으로 나뉨
|
|
48
|
+
# 약 18지점에서 자르면 2개의 큰 군집으로 나누어지고, 더 아래로 자르면 3~4개 이상의 세부 군집으로 나눠볼 수도 있음(분석가의 몫)
|
|
49
|
+
|
|
50
|
+
return linked
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Opportunity Area(기회영역) 시각화 함수 모음.
|
|
2
|
+
|
|
3
|
+
- 스케일링된 점수를 기반으로 Opportunity Area Plot을 생성합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# opportunity_plot.py
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
from matplotlib.lines import Line2D
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class OpportunityPlotConfig:
|
|
20
|
+
# 컬럼명
|
|
21
|
+
col_satisfaction: str = "satisfaction"
|
|
22
|
+
col_importance: str = "importance"
|
|
23
|
+
col_action: str = "Action"
|
|
24
|
+
|
|
25
|
+
# 축 범위
|
|
26
|
+
xlim: Tuple[float, float] = (1, 10)
|
|
27
|
+
ylim: Tuple[float, float] = (1, 10)
|
|
28
|
+
|
|
29
|
+
# 그림/스타일
|
|
30
|
+
figsize: Tuple[int, int] = (10, 8)
|
|
31
|
+
point_size: int = 35
|
|
32
|
+
edgecolor: str = "black"
|
|
33
|
+
label_fontsize: int = 10
|
|
34
|
+
show_grid: bool = True
|
|
35
|
+
|
|
36
|
+
# 평균선 색
|
|
37
|
+
line1_color: str = "r"
|
|
38
|
+
line2_color: str = "g"
|
|
39
|
+
|
|
40
|
+
# 범례
|
|
41
|
+
show_legend: bool = True
|
|
42
|
+
legend_title: str = "Actions"
|
|
43
|
+
legend_fontsize: int = 8
|
|
44
|
+
legend_title_fontsize: int = 10
|
|
45
|
+
legend_loc: str = "upper left"
|
|
46
|
+
legend_bbox_to_anchor: Tuple[float, float] = (1, 1)
|
|
47
|
+
|
|
48
|
+
# 저장 옵션
|
|
49
|
+
save_path: Optional[str] = "./data/Opportunity_area.png"
|
|
50
|
+
dpi: int = 300
|
|
51
|
+
bbox_inches: str = "tight"
|
|
52
|
+
|
|
53
|
+
# 색상 고정(재현성)
|
|
54
|
+
random_seed: Optional[int] = 42
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def plot_opportunity_area(
|
|
58
|
+
df: pd.DataFrame,
|
|
59
|
+
config: OpportunityPlotConfig = OpportunityPlotConfig(),
|
|
60
|
+
show: bool = True
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
DataFrame을 입력으로 받아 Opportunity Area(중요도-만족도) 산점도를 그립니다.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
df : pd.DataFrame
|
|
68
|
+
satisfaction, importance, Action 컬럼을 포함한 데이터프레임
|
|
69
|
+
config : OpportunityPlotConfig
|
|
70
|
+
컬럼명/스타일/저장옵션 설정
|
|
71
|
+
show : bool
|
|
72
|
+
True면 plt.show(), False면 figure 닫고 반환만
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
(fig, ax)
|
|
77
|
+
"""
|
|
78
|
+
# 1) 컬럼 추출
|
|
79
|
+
sat = df[config.col_satisfaction].to_numpy()
|
|
80
|
+
imp = df[config.col_importance].to_numpy()
|
|
81
|
+
act = df[config.col_action].astype(str).to_numpy()
|
|
82
|
+
|
|
83
|
+
# 2) 색상 생성(재현성)
|
|
84
|
+
rng = np.random.default_rng(config.random_seed) if config.random_seed is not None else np.random.default_rng()
|
|
85
|
+
colors = rng.random((len(act), 3))
|
|
86
|
+
|
|
87
|
+
# 3) figure 생성
|
|
88
|
+
fig, ax = plt.subplots(figsize=config.figsize)
|
|
89
|
+
|
|
90
|
+
# 4) 산점도
|
|
91
|
+
ax.scatter(
|
|
92
|
+
imp, sat,
|
|
93
|
+
s=config.point_size,
|
|
94
|
+
c=colors,
|
|
95
|
+
edgecolors=config.edgecolor
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
ax.set_xlabel("Importance")
|
|
99
|
+
ax.set_ylabel("Satisfaction")
|
|
100
|
+
ax.set_xlim(*config.xlim)
|
|
101
|
+
ax.set_ylim(*config.ylim)
|
|
102
|
+
|
|
103
|
+
# 5) 평균선(원본 로직 유지: 대각선 형태)
|
|
104
|
+
ax.plot(
|
|
105
|
+
[config.xlim[0], config.xlim[1]],
|
|
106
|
+
[sat.mean(), config.ylim[1]],
|
|
107
|
+
config.line1_color
|
|
108
|
+
)
|
|
109
|
+
ax.plot(
|
|
110
|
+
[imp.mean(), config.xlim[1]],
|
|
111
|
+
[config.ylim[0], config.ylim[1]],
|
|
112
|
+
config.line2_color
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# 6) 텍스트 라벨
|
|
116
|
+
for i, a in enumerate(act):
|
|
117
|
+
ax.text(imp[i], sat[i], a, fontsize=config.label_fontsize, ha="left")
|
|
118
|
+
|
|
119
|
+
# 7) 범례 (더미 scatter 대신 handles 직접 생성 → 경고 없음)
|
|
120
|
+
if config.show_legend:
|
|
121
|
+
handles = [
|
|
122
|
+
Line2D(
|
|
123
|
+
[0], [0],
|
|
124
|
+
marker="o", linestyle="",
|
|
125
|
+
markerfacecolor=colors[i],
|
|
126
|
+
markeredgecolor=config.edgecolor,
|
|
127
|
+
markersize=6,
|
|
128
|
+
label=act[i]
|
|
129
|
+
)
|
|
130
|
+
for i in range(len(act))
|
|
131
|
+
]
|
|
132
|
+
ax.legend(
|
|
133
|
+
handles=handles,
|
|
134
|
+
title=config.legend_title,
|
|
135
|
+
fontsize=config.legend_fontsize,
|
|
136
|
+
title_fontsize=config.legend_title_fontsize,
|
|
137
|
+
loc=config.legend_loc,
|
|
138
|
+
bbox_to_anchor=config.legend_bbox_to_anchor
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if config.show_grid:
|
|
142
|
+
ax.grid(True)
|
|
143
|
+
|
|
144
|
+
# 8) 저장 (경로가 지정된 경우만)
|
|
145
|
+
if config.save_path:
|
|
146
|
+
fig.savefig(config.save_path, dpi=config.dpi, bbox_inches=config.bbox_inches)
|
|
147
|
+
|
|
148
|
+
if show:
|
|
149
|
+
plt.show()
|
|
150
|
+
else:
|
|
151
|
+
plt.close(fig)
|
|
152
|
+
|
|
153
|
+
return fig, ax
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Opportunity Area 분석을 위한 점수 스케일링 함수 모음.
|
|
2
|
+
|
|
3
|
+
- 만족도/중요도 등의 점수를 Min-Max 스케일링 등으로 정규화합니다.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# sentiutils/scaling.py
|
|
7
|
+
import numpy as np
|
|
8
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
9
|
+
from typing import Dict, List, Union
|
|
10
|
+
|
|
11
|
+
def minmax_scale_scores(
|
|
12
|
+
scores: Union[Dict, List],
|
|
13
|
+
feature_range: tuple = (1, 10),
|
|
14
|
+
round_digits: int = 4
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
MinMaxScaler 기반 점수 스케일링 유틸
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(scores, dict):
|
|
20
|
+
scores = list(scores.values())
|
|
21
|
+
|
|
22
|
+
scores_array = np.array(scores).reshape(-1, 1)
|
|
23
|
+
|
|
24
|
+
scaler = MinMaxScaler(feature_range=feature_range)
|
|
25
|
+
scaled_scores = scaler.fit_transform(scores_array)
|
|
26
|
+
|
|
27
|
+
return [round(float(x), round_digits) for x in scaled_scores.flatten()]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: new_value_analysis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: New Value Analysis: actor/action/opportunity 분석 유틸리티 패키지
|
|
5
|
+
Author: New Value Analysis contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/smhrdGit/new_value_analysis
|
|
8
|
+
Project-URL: Repository, https://github.com/smhrdGit/new_value_analysis
|
|
9
|
+
Project-URL: Issues, https://github.com/smhrdGit/new_value_analysis/issues
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy>=1.21
|
|
14
|
+
Requires-Dist: pandas>=1.5
|
|
15
|
+
Requires-Dist: gensim>=4.3
|
|
16
|
+
Requires-Dist: scikit-learn>=1.2
|
|
17
|
+
Requires-Dist: matplotlib>=3.7
|
|
18
|
+
Requires-Dist: scipy>=1.10
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# New Value Analysis
|
|
22
|
+
|
|
23
|
+
`new_value_analysis`는 신규가치분석(New Value Analysis)을 위한 파이썬 유틸리티 패키지입니다.
|
|
24
|
+
|
|
25
|
+
## 구성
|
|
26
|
+
- `action_finder`: 토픽 모델링(LDA) 기반 Action(행동) 추출/할당
|
|
27
|
+
- `actor_finder`: Doc2Vec 임베딩 + 군집 진단(실루엣/덴드로그램)
|
|
28
|
+
- `opportunity_area_analysis`: Opportunity Area 스케일링/시각화
|
|
29
|
+
|
|
30
|
+
## 설치(로컬 개발 모드)
|
|
31
|
+
```bash
|
|
32
|
+
pip install -U pip
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 사용 예시
|
|
37
|
+
```python
|
|
38
|
+
from new_value_analysis import action_finder, actor_finder, opportunity_area_analysis
|
|
39
|
+
|
|
40
|
+
# 예: actor_finder 내부 함수 사용
|
|
41
|
+
# model, vectors, tagged = actor_finder.train_doc2vec_module(df, token_col="tagged_review")
|
|
42
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
new_value_analysis/__init__.py,sha256=NbX9b0Et4pRNDa6JT1Pm6_Z2m_YVioB44H64WD9Qfcs,461
|
|
2
|
+
new_value_analysis/action_finder/__init__.py,sha256=OJOjsMv-j9sGwwiu_2e3VDZZ0XqusitXSAhdhIrUFyg,255
|
|
3
|
+
new_value_analysis/action_finder/find_topicnum.py,sha256=CbGj-uP_a_BmEp-wHJjrMqs7KTgoWQDU2uuoxkI2lUs,4494
|
|
4
|
+
new_value_analysis/action_finder/select_action_number.py,sha256=ySpa9L0FAdFC3U62qr6D-itjGraBXHbawPRhywgrKBQ,1674
|
|
5
|
+
new_value_analysis/action_finder/topic_modeling.py,sha256=B0nf9ffu-8UBKh80AdevPXpvFSH48NlewuKd536heUM,1753
|
|
6
|
+
new_value_analysis/actor_finder/__init__.py,sha256=HWORMWO33lTe7dTKUflMvs6AFN1akeBJo7fLvXaLRvQ,294
|
|
7
|
+
new_value_analysis/actor_finder/doc2vec.py,sha256=lSl_iSjfY1-VvO5r_I3FFbKhTfZF-93WTyE6lEwzLjk,2801
|
|
8
|
+
new_value_analysis/actor_finder/find_right_silhouette.py,sha256=EZONLNDggr4q1zXbGTkH0hlA5Iud1jMwY5Sevhx3hoc,1973
|
|
9
|
+
new_value_analysis/actor_finder/silhouette_plot.py,sha256=BCy8h5Suce8Xfv3QS1XiwTi2JcJt_0JOCPyGR9xK1AQ,2589
|
|
10
|
+
new_value_analysis/actor_finder/visualize_dendrogram.py,sha256=0cuZkxC26gdJ9-1GdfZ6ttbFrWheJOWmpvMve4Tba1E,2196
|
|
11
|
+
new_value_analysis/opportunity_area_analysis/__init__.py,sha256=cPuB7XRX0xTPy43N3WWk37MzM8YMY09EcwbW-3d98pI,233
|
|
12
|
+
new_value_analysis/opportunity_area_analysis/opportunity_plot.py,sha256=gpoj1g3M1cWiJlCAWXvCjuCLIy-5UGAA_zygWTJyAv0,4090
|
|
13
|
+
new_value_analysis/opportunity_area_analysis/satisfaction_scaling.py,sha256=azsF46YjTaXAj7Q8UBqv_HtMlsUECWuHs6DeHndPlso,796
|
|
14
|
+
new_value_analysis-0.1.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
|
|
15
|
+
new_value_analysis-0.1.0.dist-info/METADATA,sha256=ShBHnRlzlUtrxsgN3ybcTOJc4FDyhQfM3fDmcH_0CIY,1447
|
|
16
|
+
new_value_analysis-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
17
|
+
new_value_analysis-0.1.0.dist-info/top_level.txt,sha256=faC8m2gJvXl0zyiDG6eEtklFNwyI0OVpsnget5KrGNk,19
|
|
18
|
+
new_value_analysis-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
new_value_analysis
|