최근 팀원들과 함께 전주시 공모전에 나갔었는데요, 저는 네트워크 분석을 담당했습니다. 결과적으로 수상은 하지 못했지만, 텍스트마이닝에 대해서 좀 더 공부할 수 있었습니다.
오늘은 연관규칙 분석과 동시출현빈도를 기준으로 네트워크 분석을 진행하는 법에 대해서 포스팅해보겠습니다. 공모전에서는 두 가지를 모두 사용해봤지만, 시각적인 결과를 고려하여 동시출현빈도만을 보고서에 포함시켰어요.
연관규칙 분석
연관규칙 분석은 주로 매출 데이터를 분석할 때 사용합니다. 어떤 상품이 함께 구매되는지 파악하는 것인데요, 이를 텍스트에 적용한다면 어떤 키워드가 함께 나타나는지를 파악할 수 있습니다.
연관규칙 분석을 위해서는 단어 2차원 리스트가 있어야 합니다. 데이터프레임 내 문장 컬럼을 단어 2차원 리스트로 바꾸기 위해서는 아래와 같이 전처리를 실시합니다.
# 단어 2차원 리스트 생성
def BoW(df, words = list(), stopwords = list()):
okt = Okt()
for i,j in enumerate(df['review']):
temp = []
for w in okt.nouns(j):
# 1글자 이상 & 불용어에 포함되지 않는 명사만 추출
if (len(w) != 1) and (w not in stopwords):
temp.append(w)
words.append(temp) #2차원 리스트 생성
if i % 1000 == 0:
print(i, "완료")
# 빈 리스트 삭제
words = [l for l in words if l]
return words
words = BoW(df, words=list(), stopwords=stopwords) #적용하기
위 코드를 실행하시면 words라는 2차원 단어 리스트가 생성됩니다. 이제 연관규칙 분석을 실시하기 위해서는 apriori 라이브러리를 사용해야합니다.
# 연관규칙 분석
def association(words = list(), min_support = 0.01):
result = list(apriori(words, min_support = min_support))
data = pd.DataFrame(result)
data['length'] = data['items'].apply(lambda x : len(x))
# 2개 단어의 지지도가 0.01 이상인 경우만 저장
data = data[(data['length'] == 2) & (data['support'] >= 0.01)].sort_values(by = 'support', ascending=False)
return data
total = association(words = words, min_support=0.01) #적용하기
지지도란, 전체 거래 건수 중에서 X와 Y를 모두 포함하는 거래 건수의 비율입니다. 이를 텍스트에 적용한다면, 두 단어가 동시에 등장하는 텍스트의 비율이 될 것 같네요.
위 코드를 실행하시면 items 컬럼에는 튜플 단어쌍이 담기며 support에는 지지도가 담깁니다. 아래 네트워크 분석에서는 단어쌍의 각 단어를 노드로 하고 단어의 페이지 랭크를 기준으로 단어의 중요성을 추출할 예정입니다.
네트워크 분석
그래프에는 방향성이 있는 그래프와 방향성이 없는 그래프가 있습니다. 저는 무방향 그래프를 사용했습니다.
G = nx.Graph() # undirected graph
ar = (total['items']) # 튜플형 단어쌍 생성
G.add_edges_from(ar) # 단어쌍 edge 추가
# 페이지 랭크
pr = nx.pagerank(G) # 페이지 랭크 알고리즘으로 단어의 중요성 추출
nsize = np.array([v for v in pr.values()])
nsize = 2000 * (nsize - min(nsize)) / (max(nsize) - min(nsize))
방향성이 있는 그래프를 그리기 위해서는 nx.Graph를 nx.DiGraph로 변경해주시면 됩니다. 그래프를 시각화하기 위해서는 다양한 layout을 사용할 수 있는데요, 저는 kamada_kawai_layout을 사용했습니다. 이외의 layout 종류는 아래 링크를 참고해주세요
kamada_kawai_layout — NetworkX 3.1 documentation
A two-level dictionary of optimal distances between nodes, indexed by source and destination node. If None, the distance is computed using shortest_path_length().
networkx.org
시각화 코드는 다음과 같습니다.
position = nx.kamada_kawai_layout(G)
# 한글 폰트 설정
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.figure(figsize = (10,10))
plt.axis('off')
nx.draw_networkx(G, font_size = 13, font_family = 'Malgun Gothic', pos = position, \
node_color = list(pr.values()), node_size = nsize, \
alpha = 0.7, edge_color = '.5', cmap = plt.cm.YlGn)
plt.show()
노드의 크기나 색상은 단어의 중요성을 나타내며, 노드 간의 거리가 가까울수록 강하게 연결되어 있다고 해석할 수 있습니다.
동시 출현 빈도
동시 출현 빈도 분석을 위해서는 1차원 단어 리스트가 필요합니다. 데이터프레임 내 문장 컬럼을 1차원 단어 리스트로 바꾸기 위해서 아래와 같이 전처리를 실행합니다.
# 단어 1차원 리스트
def BoW(df, words = list(), stopwords = list()):
okt = Okt()
for i,j in enumerate(df['review']):
temp = []
for w in okt.nouns(j):
# 1글자 이상의 명사만 & 불용어에 포함되어 있지 않은 경우만
if (len(w) != 1) and (w not in stopwords):
temp.append(w)
text = " ".join(temp)
words.append(text) #1차원 리스트 생성
if i % 1000 == 0:
print(i, "완료")
# 빈 리스트 삭제
words = [l for l in words if l]
return words
words = BoW(df, words=list(), stopwords=stopwords) #적용하기
단어 간의 동시 출현 빈도를 계산하기 위해서는 Countvectorizer를 사용합니다.
def countvec(txt, num):
vectorizer = CountVectorizer(max_features = num) #가장 많이 언급된 단어 num개 이용
count = vectorizer.fit_transform(txt) #문서 - 어휘 행렬
matrix = count.T * count # 어휘 행렬 생성
matrix.setdiag(0) #대각행렬은 0
matrix = pd.DataFrame(matrix.toarray(), columns = vectorizer.get_feature_names_out())
return matrix
total_matrix = countvec(words, 150) #적용하기
위 코드를 실행하시면 가장 많이 등장한 150개 단어 간의 동시 출현 빈도를 나타낸 매트릭스가 생성됩니다. 대각행렬은 자기 자신이므로 0이 되며, 이외의 숫자는 동시에 출현한 빈도를 나타냅니다.
저는 단순히 동시 출현 빈도뿐만 아니라, 단어 간의 유사도를 기반으로 네트워크 분석을 하고 싶었습니다. 코사인 유사도를 두 단어를 연결하는 엣지의 기준으로 삼기 위해서 아래 코드를 실행했습니다.
def word_sim(matrix): #cosine 유사도 사용
sim_matrix = pdist(matrix, metric = 'cosine') #pdist : 유클리디안 거리
sim_matrix = squareform(sim_matrix) # 벡터 > matrix
vocab = list(matrix.columns) #어휘 리스트
sims = []
for i, j in combinations(range(len(vocab)), 2):
if sim_matrix[i, j] == 0:
continue
sims.append((vocab[i], vocab[j], sim_matrix[i, j]))
sim_list = sorted(sims, key=itemgetter(2), reverse=True) #어휘 유사도 행렬 정렬
return sim_list
total_list = word_sim(total_matrix) #적용하기
위 코드를 실행하시면 (단어1, 단어2, 코사인 유사도)가 하나의 요소인 1차원 리스트가 생성됩니다.
네트워크 분석
# 네트워크 생성
def build_word_sim_undirected(sim_list):
G = nx.Graph() #undirected graph
NUM_MAX_WORDS = 150 # 표현 개수 지정
for word1, word2, sim in sim_list[:NUM_MAX_WORDS]:
G.add_edge(word1, word2, weight=sim)
return G
G = build_word_sim_undirected(total_list)
pr = nx.pagerank(G) # 페이지 랭크 알고리즘으로 단어의 중요성 추출
nsize = np.array([v for v in pr.values()])
nsize = 2000 * (nsize - min(nsize)) / (max(nsize) - min(nsize))
저희가 현재 생성한 total_list는 아까 보신 연관규칙과 다른 output이기 때문에 이를 처리하기 위해서는 사용자 정의 함수를 따로 생성해서 적용해야 합니다. 시각화 코드는 위의 코드와 동일합니다.
네트워크 분석 평가 지표
네트워크 분석은 기본적으로 비지도학습입니다. 명확한 기준이 없기 때문에 해석하는 데에 분석가의 주관이 들어가지만, 네트워크의 밀집도가 어떤지, 또한 각 단어의 중요성이 어느 정도인지 판단할 수 있는 지표가 있습니다.
전반적인 성능 확인
def overall(G = None):
average_degree = nx.average_degree_connectivity(G)
clustering_coefficient = nx.average_clustering(G)
try: #연결 요소가 1개인 경우
average_path_length = nx.average_shortest_path_length(G)
except: #연결 요소가 여러 개인 경우
try:
connected_components = list(nx.connected_components(G))
for component in connected_components:
component_graph = G.subgraph(component)
average_path_length = nx.average_shortest_path_length(component_graph)
except:
connected_components = list(nx.strongly_connected_components(G))
for component in connected_components:
component_graph = G.subgraph(component)
average_path_length = nx.average_shortest_path_length(component_graph)
network_density = nx.density(G)
network_modularity = nx.algorithms.community.modularity(G, [G.nodes])
print("Average Degree:", average_degree)
print("Clustering Coefficient:", clustering_coefficient)
print("Average Path Length:", average_path_length)
print("Network Density:", network_density)
print("Network Modularity:", network_modularity)
overall(G) #적용하기
- Average Degree : 그래프의 평균적인 연결성을 나타냅니다.
- Clustering coefficient : 평균적인 군집화 계수를 나타냅니다.
- average path length : 가장 짧은 path의 평균을 나타냅니다. 연결 요소가 여러 개인 경우, 강하게 연결되어 있는 요소를 기준으로 subgraph를 생성한 뒤, 생성된 subgraph에서 가장 짧은 path의 평균을 구합니다.
- Network density : 그래프의 밀집도를 나타냅니다.
- Netword Modularity : 그래프의 모듈성을 나타냅니다. 모듈성이 높은 경우 모듈 내의 노드 간 연결성이 높고, 모듈 외의 노드 간 연결성이 낮은 것을 의미합니다.
각 단어의 중심성
# 단어별 중심성 확인 함수
def centrality(G = None):
degree_centrality = nx.degree_centrality(G)
betweenness_centrality = nx.betweenness_centrality(G)
closeness_centrality = nx.closeness_centrality(G)
pagerank = nx.pagerank(G)
degree = pd.DataFrame(degree_centrality, index=[0]).T.reset_index(drop=False)
between = pd.DataFrame(betweenness_centrality, index=[0]).T.reset_index(drop=False)
close = pd.DataFrame(closeness_centrality, index=[0]).T.reset_index(drop=False)
page = pd.DataFrame(pagerank, index = [0]).T.reset_index(drop=False)
df_list = [degree, between, close, page]
cent = reduce(lambda x,y : pd.merge(x,y, on = 'index'), df_list)
cent.columns = ['단어', '연결 중심성', '매개 중심성', '근접 중심성', 'pagerank']
return cent
# 적용하기
total_cent = centrality(G)
# 연결 중심성 top 10
degree_list = list(total_cent.sort_values(by = '연결 중심성', ascending=False)['단어'][:10])
total_cent.sort_values(by = '연결 중심성', ascending=False).iloc[:10,:2]
- 연결 중심성 : 다른 노드와 얼마나 많이 연결되어 있는지. 연결 중심성이 높은 노드는 네트워크에서 중요한 역할을 한다고 볼 수 있습니다.
- 매개 중심성 : 다른 노드 간의 최단 경로를 얼마나 많이 지나는지. 매개 중심성이 높은 노드는 다른 노드 간의 정보 전달에서 중요한 역할을 한다고 볼 수 있습니다.
- 근접 중심성 : 다른 노드와 얼마나 가깝게 연결되어 있는지. 근접 중심성이 높은 노드는 네트워크에서 정보 전달의 빠른 속도를 나타낸다고 볼 수 있습니다.
- pagerank : 중심성 알고리즘(고유벡터 중심성, Katz 중심성...) 중 가장 개선된 알고리즘입니다. 노드의 중요성을 측정하는 지표입니다.
Github
github에는 최종적으로 보고서에 포함된 동시출현빈도 기준 네트워크 분석만 있습니다.
GitHub - Sennysideup/Project
Contribute to Sennysideup/Project development by creating an account on GitHub.
github.com
References
'Data > Python' 카테고리의 다른 글
GPU 없이 생성형 AI 사용하기 - Pandasai 사용 방법 & 후기 (1) | 2025.02.12 |
---|---|
좌표별 가장 가까운 지하철역 계산하기 (cKDtree, haversine) (0) | 2023.07.28 |
셀레니움 없이 네이버 블로그 검색 결과 수집하기 (API & beautifulsoup) (0) | 2023.07.08 |