데이터분석 #4 리뷰 데이터 분석 #
#2025-08-19
1. 목적 #
리뷰 데이터를 보고
- 감성 점수와 평점의 관계
- 리뷰 길이와 감성 점수의 관계
- 카테고리별 감성 차이
- Review_length가 AI 임베딩 유사도에 영향을 줄 수 있는지
인사이트 생성하기.
#
2. 코드 #
import os
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.pyplot as plt
import matplotlib as mpl
from sentence_transformers import SentenceTransformer, util
# Mac 환경 한글 폰트 설정
plt.rc('font', family='AppleGothic')
mpl.rcParams['axes.unicode_minus'] = False
# Set path
os.chdir("/Users/yshmbid/Documents/home/github/Data-MLOps/0814")
os.getcwd()
'/Users/yshmbid/Documents/home/github/Data-MLOps/0814'
# 1. 데이터 불러오기
df = pd.read_csv('reviews.csv')
df
# 2. 결측치 및 기본 정보 확인
print("결측치 개수:\n", df.isnull().sum())
print("\n데이터 기본 정보:")
print(df.info())
# 결측치 처리: review_text 또는 sentiment_score 중 하나라도 결측인 행 제거
before_rows = df.shape[0]
df = df.dropna(subset=['review_text', 'sentiment_score'], how='any')
after_rows = df.shape[0]
print(f"\n제거된 행 수: {before_rows - after_rows} ({(before_rows - after_rows) / before_rows * 100:.1f}% 데이터 손실)")
print(f"남은 데이터 수: {after_rows}개")
결측치 개수:
review_id 0
product_id 0
category 0
review_text 5
review_length 0
num_words 0
sentiment_score 5
rating 0
dtype: int64
데이터 기본 정보:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 review_id 200 non-null object
1 product_id 200 non-null object
2 category 200 non-null object
3 review_text 195 non-null object
4 review_length 200 non-null int64
5 num_words 200 non-null int64
6 sentiment_score 195 non-null float64
7 rating 200 non-null int64
...
None
제거된 행 수: 10 (5.0% 데이터 손실)
남은 데이터 수: 190개
# 3. 분포 시각화 및 이상치 탐지
# 수치형 컬럼만 선택
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns
# 히스토그램 (분포 확인)
fig, axes = plt.subplots(nrows=1, ncols=len(numeric_cols), figsize=(5*len(numeric_cols), 4))
for ax, col in zip(axes, numeric_cols):
sns.histplot(df[col], kde=True, ax=ax)
ax.set_title(f'{col} Distribution')
plt.tight_layout()
plt.show()
# 박스플롯 (이상치 확인)
fig, axes = plt.subplots(nrows=1, ncols=len(numeric_cols), figsize=(5*len(numeric_cols), 4))
for ax, col in zip(axes, numeric_cols):
sns.boxplot(x=df[col], ax=ax)
ax.set_title(f'{col} Boxplot')
plt.tight_layout()
plt.show()
# 이상치 처리
df_clean = df.copy()
# review_length 상하위 1% 제거
lower_bound = df_clean['review_length'].quantile(0.01)
upper_bound = df_clean['review_length'].quantile(0.99)
df_clean = df_clean[(df_clean['review_length'] >= lower_bound) & (df_clean['review_length'] <= upper_bound)]
# num_words 상하위 1% 제거
lower_bound = df_clean['num_words'].quantile(0.01)
upper_bound = df_clean['num_words'].quantile(0.99)
df_clean = df_clean[(df_clean['num_words'] >= lower_bound) & (df_clean['num_words'] <= upper_bound)]
print("상하위 1% 절삭 후 데이터 크기:", df_clean.shape)
상하위 1% 절삭 후 데이터 크기: (184, 8)
# 4. 범주별 평균 평점
category_mean_rating = df.groupby('category')['rating'].mean().sort_values(ascending=False)
print(category_mean_rating)
# 시각화
plt.figure(figsize=(6,4))
sns.barplot(x=category_mean_rating.index, y=category_mean_rating.values)
plt.title("Category별 평균 평점")
plt.show()
category
electronics 3.764706
home 3.500000
fashion 3.469388
sports 3.285714
Name: rating, dtype: float64
# 5. Sentiment Score vs Rating
plt.figure(figsize=(6,4))
sns.scatterplot(data=df, x='sentiment_score', y='rating', alpha=0.5)
plt.title("감성 점수 vs 평점")
plt.show()
# 상관계수 확인
corr_sentiment_rating = df['sentiment_score'].corr(df['rating'])
print("감성 점수와 평점의 상관계수:", corr_sentiment_rating)
감성 점수와 평점의 상관계수: -0.020926485382556512
# 6. Review Length vs Rating (violinplot)
plt.figure(figsize=(6,4))
sns.violinplot(data=df, x='rating', y='review_length')
plt.title("리뷰 길이 vs 평점")
plt.show()
# 상관계수 확인
corr_length_rating = df['review_length'].corr(df['rating'])
print("리뷰 길이와 평점의 상관계수:", corr_length_rating)
리뷰 길이와 평점의 상관계수: -0.018622392015914393
# 7. Category별 평균 Sentiment Score
category_sentiment = df_clean.groupby('category')['sentiment_score'].mean().sort_values(ascending=False)
print("Category별 평균 Sentiment Score:")
print(category_sentiment)
plt.figure(figsize=(8, 5))
sns.barplot(data=df_clean, x='category', y='sentiment_score', ci=None, order=category_sentiment.index)
plt.title('Category별 평균 Sentiment Score')
plt.xticks(rotation=45)
plt.show()
Category별 평균 Sentiment Score:
category
sports 0.241842
fashion 0.152500
electronics 0.112400
home 0.069375
Name: sentiment_score, dtype: float64
# 9. Review Length vs 평균 Embedding Similarity
# 1) 임베딩 생성
model = SentenceTransformer('snunlp/KR-SBERT-V40K-klueNLI-augSTS')
texts = df_clean['review_text'].fillna("").tolist()
embeddings = model.encode(texts, convert_to_tensor=True)
# 2) 모든 리뷰 쌍 간 코사인 유사도 행렬 계산
similarity_matrix = util.cos_sim(embeddings, embeddings).cpu().numpy()
# 3) 자기 자신과의 유사도(=1.0) 제외한 평균 유사도 계산
mean_similarities = []
for i in range(len(similarity_matrix)):
# i번째 리뷰의 다른 리뷰들과의 평균 유사도
sims = np.delete(similarity_matrix[i], i) # 자기 자신 제외
mean_similarities.append(np.mean(sims))
# 4) 데이터프레임에 평균 유사도 컬럼 추가
df_clean['mean_embedding_similarity'] = mean_similarities
# 5) 상관계수 확인
corr_length_mean_similarity = df_clean['review_length'].corr(df_clean['mean_embedding_similarity'])
print(f"리뷰 길이와 평균 Embedding Similarity 상관계수: {corr_length_mean_similarity:.3f}")
# 6) 시각화
plt.figure(figsize=(6, 5))
sns.scatterplot(data=df_clean, x='review_length', y='mean_embedding_similarity', alpha=0.6)
plt.title('Review Length vs 평균 Embedding Similarity')
plt.show()
리뷰 길이와 평균 Embedding Similarity 상관계수: 0.044
#
3. 생각 #
결측치 처리
- 나는 결측치가 하나라도 있는 샘플은 다 제거했는데 다른 사람들꺼보니깐 review_text 컬럼의 결측값을 ’no review’로 대체하는 경우도 있었다. 이게 낫나?
- 리뷰랑 상관없는 인사이트 (감성점수 vs 평점, 카테고리별 감성차이)에는 데이터가 확보되니깐 좋고.
- 리뷰 길이가 AI 임베딩 유사도에 영향을 줄수있는지 <- 여기서는 오히려 잘못된 데이터 심어주는게 대지않나 싶음.
- 리뷰길이 vs 감성점수의 관계도 마찬가지.
이상치 탐지
- 이상치 탐지는 보통 IQR을 쓰던데 나는 IQR 너무 많지 않나 한두개만 제거하면대는데? 생각해서 챗지피티한테 다른거추천해달라니깐 상하위 1% 추천해주길래 그걸로햇다.
- 다른사람들 IQR 한거보니 리뷰길이는 3개 단어개수는 2개등 몇개 안되길래 결과는 비슷햇을듯. (나는 6개 제거됏엇던듯)
- 근데 rating이 평점같은데 평점은 1점 줄수있지않나? 특이취향을 제거하는셈이 돼버리니깐 이건 제거안하길 잘한거같다.
이상치 box plot
- before box plot그리고 after box plot도 그렷으면 더 이뻤겟다.
상관관계
- 나는 감성 점수 vs 평점, 리뷰 길이 vs 감성 점수, 리뷰 길이 vs AI 임베딩 유사도 비교에서 매번 상관계수를 그냥 구햇는데
- correlation matrix 그린 사람도 있어서 그것도 괜찮은듯하다
- 상관관계 전부다 낮게나왓는데 그건 남들도 마찬가지 같아서 다행이엇다.
감성점수 vs 평점 scatter plot
- 장르별로 색깔 다르게한사람 좀 있던데 그림자체는 안이쁘지만 좋은접근같았다.
category별 평균평점
- 다른사람들도 어쩔수없었겟지만 아쉬운게 y축 max를 모르니깐 플롯이 다 안이뻣다. 멀 말하고자하는지 잘 안보엿다. 아마 max 5였겠지? 근데이건 정보가 없으니깐..
리뷰 길이 vs AI 임베딩 유사도
- 이거야말로 어케하란건지 모르겠어서
- 처음에는 랜덤하게고른(사실 첫번째) 기준 리뷰와의 유사도를 다 계산하고 리뷰길이 vs 임베딩유사도의 corr을 구했는데
- 목적이 ‘모든 리뷰 쌍 간의 임베딩 유사도’ 또는 ‘임베딩 모델의 특성상 길이가 의미 표현에 미치는 영향’을 보는건데
- 내가수행한건 ‘기준 리뷰와의 유사도가 길이에 따라 변하는지’ 본거라 데이터셋 전체의 관계를 본게아니라 한 기준점에 대해서만 수행한셈이 되길래,
- 각 리뷰가 다른 모든 리뷰와 가지는 평균 임베딩 유사도를 계산하는 방식으로 다시 했었다
- 처음에는 랜덤하게고른(사실 첫번째) 기준 리뷰와의 유사도를 다 계산하고 리뷰길이 vs 임베딩유사도의 corr을 구했는데
# 원래 분석
embeddings = model.encode(texts, convert_to_tensor=True)
reference_embedding = embeddings[0]
similarities = util.cos_sim(reference_embedding, embeddings)[0].cpu().numpy()
df_clean['embedding_similarity'] = similarities
# 변경 분석
embeddings = model.encode(texts, convert_to_tensor=True)
similarity_matrix = util.cos_sim(embeddings, embeddings).cpu().numpy()
mean_similarities = []
for i in range(len(similarity_matrix)): # i번째 리뷰의 다른 리뷰들과의 평균 유사도
sims = np.delete(similarity_matrix[i], i) # 자기 자신 제외
mean_similarities.append(np.mean(sims))
df_clean['mean_embedding_similarity'] = mean_similarities
- 남들 어케했는지 궁금했는데
- ‘기준 리뷰와의 유사도가 길이에 따라 변하는지’ 본사람도있고
- 임베딩 어케하는지에따라 다르다 그냥이렇게쓴사람도 있고…
- 얘는 답을 몰겟음.