본문 바로가기

텍스트마이닝

BERTopic과 인과분석: 정신 건강 문제에 영향을 미치는 요인 탐색

이 연구에서는 BERTopic과 인과 발견 분석을 결합하여 정신 건강에 영향을 미치는 요인들을 체계적으로 분석하고자 하였습니다. 이는 데이터 기반으로 특정 주제가 정신 건강 문제와 어떻게 연관되는지 이해하려는 시도입니다.

데이터셋 

  • Kaggle에서 수집한 27977개의 텍스트 데이터 사용
  • 다양한 정신 건강 문제를 겪고 있는 개인들의 표현과 감정을 포함함
  • 두 개의 열로 구성되어 있음. 첫 번째 열은 텍스트 데이터, 두 번째 열은 해당 텍스트가 정신 건강 문제와 관련이 있는지를 나타내는 레이블 (레이블이 1이면 관련 있음, 0이면 관련 없음)

연구절차

  • 연구 절차는 다음과 같습니다. 먼저 데이터 전처리(Data Pre-processing)를 통해 데이터를 정리한 후, BERTopic 모델을 사용하여 토픽을 추출합니다. 추출된 토픽과 데이터의 라벨(Label) 열을 기반으로 토픽 가중치(Topic Weights)를 계산하고, 이를 데이터 통합(Data Integration) 단계에서 결합합니다. 이후, 인과 발견(Causal Discovery)을 통해 변인 간의 관계를 분석하고, 로지스틱 회귀(Logistic Regression)를 사용하여 예측 모델을 구축합니다. 마지막으로 분석 결과를 해석합니다.

연구 절차

 

import pandas as pd
import numpy as np
from bertopic import BERTopic

df = pd.read_csv("mental_health.csv")

df.head()

 

데이터셋

df['label'].value_counts()

data = df[['text']]
data.head()

데이터

데이터 전처리

  • 텍스트를 소문자로 변환하고, 알파벳이 아닌 문자, 단일 문자 단어, 숫자, 기호를 제거
  • 중복된 행을 제거
  • 이중 공백을 단일 공백으로 대체
  • Lemmatization 적용
  • 전처리의 마지막 단계로, spaCy의 불용어 리스트를 사용하여 불용어를 제거. 추가적으로, 프로젝트 맥락에서 의미가 없거나 lemmatization 중에 구조가 변경된 특정 단어들도 불용어 리스트에 추가하였음: ['just', 'wa', 'don', 've', 'ha', 'doe', 'm', 'll', 'didn', 'doesn', 'wan', 'an']
  • 결측값과 중복 항목을 확인하였음. 결측값은 없었으나 중복된 12개의 행을 제거
  • "langdetect" 패키지를 사용하여 영어 텍스트만을 분석 대상으로 유지하였음. 그 결과, 2,226개의 비영어 텍스트가 제거되어, 최종적으로 25,739개의 항목이 남았음
import re
import spacy
import stopwords
import nltk

nlp_model = spacy.load("en_core_web_sm") # load model
stop_words_spacy = spacy.lang.en.stop_words.STOP_WORDS
#stop_words_nltk = stopwords.words('english')
custom_stopwords = ['just', 's', 'br', 'wa', 'don', "ve", "ha", "doe", "m", "ll", "didn", "doesn", "wan", "na"]
stop_words_spacy.update(custom_stopwords)


processed_text = []
def spacy_preprocessing(text, stop_words = None, lemmatize = False):
    text = text.lower()  # Lowercase
    text = re.sub(r'[^\w\s]', '', text)  # Subs non-alphabetic characters to spaces
    text = re.sub(r'\d+', '', text)  # Remove numbers
    text = re.sub(r'[\W_]+', ' ', text)  # Remove non-word characters (punctuation and symbols)
    text = re.sub("\s+", ' ', text)  # Subs double spaces and more with a single space
    text = text.strip()  # Trims whitespaces at the ends
    text = re.sub(r'\b\w\b', '', text)  # Remove single characters
   
    if lemmatize == True:
        doc = nlp_model(text)
        text = [token.lemma_.strip() for token in doc if not token.is_stop and token.lemma_]
        text = ' '.join(text)
    else:
        text = ' '.join(text)
        
    if stop_words is not None:
        text = [token for token in text.split() 
                if token not in stop_words]      
        text = ' '.join(text)
    else:
        text = ' '.join(text)
    return text
df['processed_text'] = df['text'].apply(lambda x: spacy_preprocessing(x, stop_words_spacy, lemmatize = True))
df.sample(20)

20 샘플 text vs processed_text

# Dropping null values
df = df.dropna(subset=['processed_text'])

# Dropping duplicates
df = df.drop_duplicates(subset=['processed_text'])
import pandas as pd
from langdetect import detect

# Detecting only english reviews
def is_english(text):
    try:
        return detect(text) == 'en'
    except:
        return False

df['is_english'] = df['processed_text'].apply(is_english)
english_reviews_df = df[df['is_english'] == True]

english_reviews_df.head()

df['is_english'].value_counts()

# Dropping non-English reviews

df = df[df['is_english'] == True].drop(columns=['is_english'])

df.to_csv("mental_cleaned.csv")

Wordcloud of processed_text tokens
Token frequency plot

BERTopic [토픽모델링]

# Reading processed data

df = pd.read_csv("mental_cleaned.csv")
df['processed_text'] = df['processed_text'].astype(str)

data = df[['processed_text']]
data.head()
from sklearn.feature_extraction.text import CountVectorizer

# Removing stopwords by count vectorizer again
vectorizer_model = CountVectorizer(stop_words="english")
# UMAP 
from umap import UMAP
umap = UMAP (n_neighbors=15,
            n_components = 5,
            metric='cosine',
            min_dist=0.0,
            low_memory = False)
            
# HDBSCAN
import hdbscan
hdbscan_model = hdbscan.HDBSCAN(min_cluster_size=15, metric='euclidean', cluster_selection_method='eom', prediction_data=True)
# Creating BERTopic model
model = BERTopic(verbose=True, umap_model=umap, hdbscan_model=hdbscan_model, vectorizer_model=vectorizer_model, 
                 nr_topics=10, calculate_probabilities=True)
docs = data.processed_text.to_list()
topics, probabilities = model.fit_transform(docs)
model.get_topic_freq()

 

  • BERTopic 모델을 생성할 때, nr_topics 값을 10으로 수동으로 설정했습니다. 이때, 토픽 -1은 이상값(outliers)을 나타내므로 이를 고려하지 않았으며, 최종적으로 9개의 토픽과 그 빈도(frequencies)를 도출했습니다.

Topic Frequencies

model.get_topic(0)

Example of topic 0

  • 토픽 확률(topic probabilities)에 이 방식으로 접근할 수 있는데, 이는 모델을 생성할 때 미리 설정한 `calculate_prob=True` 파라미터 덕분입니다.
# Topic probabilities
probabilities

Topic Probabilities

topics_rep = pd.DataFrame(model.get_topic_info())
topics_rep

Topic Representations

 

WordClouds for Each Topic

 

Wordclouds of Topics

# Labeling the texts by dominant topics
document_topics = []
for doc, topic in zip(docs, topics):
    document_topics.append(topic)

r_data = {"Document": docs, "Topic": document_topics}

r_df = pd.DataFrame(r_data)

 

  • 관련 텍스트와 우세한 토픽(dominant topic)을 포함한 토픽 가중치(topic weights)로 구성된 데이터프레임을 생성합니다.
# Combining topic weights, texts and dominant topics together
topic_weights_df = pd.DataFrame(probabilities, columns=[f"Topic_{i}" for i in range(len(probabilities[0]))])

result_df = pd.concat([r_df, topic_weights_df], axis=1)

print(result_df)
  • 그리고 라벨(label) 열도 함께 결합합니다.
# Adding "label" column to the data frame
result_df = pd.concat([result_df, df['label']], axis=1)
result_df

Final DataFrame

 

앞서 언급한 것처럼, 토픽 -1은 이상값(outliers)이기 때문에 제외합니다.

# Excluding Topic -1 values, filtering the data frame. 
filtered_df = result_df[result_df['Topic'] != -1]

# Save excluded df to csv
filtered_df.to_csv('topic_labeled_excluded.csv')

 

토픽 개수가 100 미만인 토픽 6, 7, 8을 제외하기 위해 임계값을 설정하고, 관련 열들을 데이터프레임에서 제거합니다.

# Setting threshold value for topics, excluding <100 count topics. (topic 6,7,8)

threshold_df = filtered_df[~filtered_df['Topic'].isin([6, 7, 8])]
threshold_df = threshold_df.drop(columns=['Topic_6', 'Topic_7', 'Topic_8'])

 

남은 토픽 가중치의 합이 1이 되도록 정규화(normalizing)합니다.

# Normalizing remaining topics weights, (sum of topic weights = 1)
cols_to_normalize = ['Topic_0', 'Topic_1', 'Topic_2', 'Topic_3', 'Topic_4', 'Topic_5']
threshold_df[cols_to_normalize] = threshold_df[cols_to_normalize].div(threshold_df[cols_to_normalize].sum(axis=1), axis=0)
# Saving the final dataframe
threshold_df.to_csv("threshold_df.csv")

 

인과분석 [Causal Discovery Analysis]

  • 이 프로젝트의 목표는 정신 건강을 나타내는 '레이블' 변수에 가장 큰 영향을 미치는 주제를 조사하는 것이었습니다. 따라서 인과 발견 분석을 수행했습니다.
import pandas as pd
import numpy as np
import numpy as np
import pandas as pd
import graphviz
import lingam
from lingam.utils import make_dot
from lingam.utils import make_prior_knowledge
print([np.__version__, pd.__version__, graphviz.__version__, lingam.__version__])
np.set_printoptions(precision=3, suppress=True)
np.random.seed(100)

df = pd.read_csv("threshold_df.csv")
df = df.drop(["Unnamed: 0"], axis=1)
pd.options.display.float_format = '{:.6f}'.format
df = df.drop(['Topic'], axis=1)
df_analysis = df.drop(['Document'], axis=1)
df_analysis.info

def make_graph(adjacency_matrix, labels=None):
    idx = np.abs(adjacency_matrix) > 0.01
    dirs = np.where(idx)
    d = graphviz.Digraph(engine='dot')
    names = labels if labels else [f'x[1]' for i in range(len(adjacency_matrix))]
    for to, from_, coef in zip(dirs[0], dirs[1], adjacency_matrix[idx]):
        d.edge(names[from_], names[to], label=f'{coef:.2f}')
    return d

 

pk = make_prior_knowledge(n_variables=len(df_analysis.columns),
sink_variables=[6]) #label

model = lingam.DirectLiNGAM(prior_knowledge=pk)
model.fit(df_analysis)
labels = [f'[1]. {col}' for i, col in enumerate(df_analysis.columns)]
print(labels)
dot = make_graph(model.adjacency_matrix_, labels)
print(dot)
make_dot(model.adjacency_matrix_, labels)

Causal Analysis Graph

from sklearn.linear_model import LogisticRegression
target = 6 # label
features = [i for i in range(df_analysis.shape[1]) if i != target]
reg = LogisticRegression(solver='liblinear')
reg.fit(df_analysis.iloc[:, features].values, df_analysis.iloc[:, target].values)
ce = lingam.CausalEffect(model)
effects = ce.estimate_effects_on_prediction(df_analysis.values, target, reg)
df_effects = pd.DataFrame()
df_effects['feature'] = df_analysis.columns
df_effects['effect_plus'] = effects[:, 0]
df_effects['effect_minus'] = effects[:, 1]
df_effects

Causal Effects
Project_Report.docx
1.17MB