Broadcast_paper / analysis3.py
Choi jun hyeok
update prompt
be91dcc
# -*- coding: utf-8 -*-
"""
신문과방솑 λ…μž 데이터 심측 EDA (수치/μΆ”μ„Έ 가독성 κ°•ν™” 월별 뢄석)
월별 동적 νŠΈλ Œλ“œ 뢄석을 κ°•ν™”ν•˜μ—¬, λͺ¨λ“  μ‹œκ°ν™” μžλ£Œμ— μ •ν™•ν•œ 수치λ₯Ό
ν‘œμ‹œν•˜κ³ , μ „μ›” λŒ€λΉ„ μ„±μž₯λ₯ μ„ λͺ…μ‹œμ μœΌλ‘œ 보여주어 μΆ”μ„Έλ₯Ό λ”μš± λͺ…ν™•ν•˜κ²Œ
νŒŒμ•…ν•  수 μžˆλ„λ‘ κ°œμ„ ν•©λ‹ˆλ‹€.
"""
# 1. 라이브러리 μž„ν¬νŠΈ (κΈ°μ‘΄κ³Ό 동일)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
import warnings
import os
warnings.filterwarnings('ignore')
# --- μ‹œκ°ν™”μš© 헬퍼 ν•¨μˆ˜ ---
def add_value_labels(ax, is_bar=True, fmt="{:.0f}"):
"""λ§‰λŒ€ λ˜λŠ” 꺾은선 κ·Έλž˜ν”„μ— κ°’ λ ˆμ΄λΈ”μ„ μΆ”κ°€ν•˜λŠ” ν•¨μˆ˜"""
for p in ax.patches if is_bar else ax.lines:
if is_bar:
ax.annotate(fmt.format(p.get_height()),
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='center',
xytext=(0, 9),
textcoords='offset points',
fontsize=9,
color='dimgray')
else: # for line plots
for x_value, y_value in zip(p.get_xdata(), p.get_ydata()):
ax.text(x_value, y_value, fmt.format(y_value),
ha='center', va='bottom',
fontsize=9,
color='dimgray')
# 2. κΈ°λ³Έ μ„€μ • 및 μ „μ—­ λ³€μˆ˜
def setup_environment():
DATA_DIR = r'Broadcast_paper\data_csv'
OUTPUT_DIR = r'./output_analysis_v4' # κ²°κ³Ό μ €μž₯ 폴더 λ³€κ²½
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
print(f"'{OUTPUT_DIR}' 폴더λ₯Ό μƒμ„±ν–ˆμŠ΅λ‹ˆλ‹€.")
plt.rc('font', family='Malgun Gothic')
plt.rcParams['axes.unicode_minus'] = False
sns.set(font='Malgun Gothic', rc={'axes.unicode_minus': False}, style='whitegrid')
print("뢄석 ν™˜κ²½ μ„€μ • μ™„λ£Œ!")
return DATA_DIR, OUTPUT_DIR
# 3. 데이터 λ‘œλ“œ 및 μ „μ²˜λ¦¬ (κΈ°μ‘΄κ³Ό 동일)
def load_and_preprocess_data(data_dir):
print("\n[단계 1] 데이터 λ‘œλ“œ 및 μ „μ²˜λ¦¬ μ‹œμž‘...")
df_metrics = pd.read_csv(f'{data_dir}/article_metrics_monthly.csv')
df_contents = pd.read_csv(f'{data_dir}/contents.csv')
df_demo = pd.read_csv(f'{data_dir}/demographics_merged.csv')
df_referrer = pd.read_csv(f'{data_dir}/referrer.csv')
df_metrics['period'] = pd.to_datetime(df_metrics['period']).dt.to_period('M')
df_contents['publish_month'] = pd.to_datetime(df_contents['date']).dt.to_period('M')
df_demo['period'] = pd.to_datetime(df_demo['period']).dt.to_period('M')
df_referrer['period'] = pd.to_datetime(df_referrer['period']).dt.to_period('M')
df_metrics['comments'].fillna(0, inplace=True)
df_contents.dropna(subset=['category', 'content', 'date'], inplace=True)
df_contents['content_length'] = df_contents['content'].str.len()
df_demo_filtered = df_demo[df_demo['age_group'] != '전체'].copy()
article_total_metrics = df_metrics.groupby('article_id').agg({
'views_total': 'sum', 'likes': 'sum', 'comments': 'sum'
}).reset_index()
df_merged = pd.merge(df_contents, article_total_metrics, on='article_id', how='left')
df_merged.fillna({'views_total': 0, 'likes': 0, 'comments': 0}, inplace=True)
df_merged['engagement_rate'] = ((df_merged['likes'] + df_merged['comments']) / df_merged['views_total'].replace(0, np.nan)) * 100
print("데이터 λ‘œλ“œ 및 μ „μ²˜λ¦¬ μ™„λ£Œ!")
return {
"metrics": df_metrics, "contents": df_contents, "demo": df_demo_filtered,
"referrer": df_referrer, "merged": df_merged
}
# ==============================================================================
# β˜…β˜…β˜…β˜…β˜… 수치/μΆ”μ„Έ 가독성을 κ·ΉλŒ€ν™”ν•œ 월별 뢄석 ν•¨μˆ˜ β˜…β˜…β˜…β˜…β˜…
# ==============================================================================
def analyze_enhanced_monthly_trends(data, output_dir):
"""
μ‹œκ°„(μ›”)의 흐름에 λ”°λ₯Έ μ£Όμš” μ§€ν‘œλ“€μ˜ 동적 λ³€ν™”λ₯Ό μˆ˜μΉ˜μ™€ ν•¨κ»˜ λͺ…ν™•ν•˜κ²Œ λΆ„μ„ν•©λ‹ˆλ‹€.
"""
print("\n[μ‹ κ·œ 뢄석 4] 월별 동적 νŠΈλ Œλ“œ 심측 뢄석 (수치 κ°•ν™”)...")
# --- 1. 월별 μ„±κ³Ό μ§€ν‘œ 및 μ„±μž₯λ₯  ---
monthly_metrics = data['metrics'].groupby('period').agg(
total_views=('views_total', 'sum'),
total_likes=('likes', 'sum'),
total_comments=('comments', 'sum')
).sort_index()
# μ „μ›” λŒ€λΉ„ μ„±μž₯λ₯ (MoM Growth) 계산
for col in monthly_metrics.columns:
monthly_metrics[f'{col}_mom'] = monthly_metrics[col].pct_change() * 100
monthly_metrics.index = monthly_metrics.index.to_timestamp()
fig, axes = plt.subplots(2, 1, figsize=(18, 14), sharex=True)
fig.suptitle('월별 μ„±κ³Ό μ§€ν‘œ 및 μ „μ›” λŒ€λΉ„ μ„±μž₯λ₯ (MoM) 좔이', fontsize=20, y=1.0)
# 상단 κ·Έλž˜ν”„: μ ˆλŒ€ 수치 (쑰회수 + μ’‹μ•„μš”)
ax1 = axes[0]
bars = ax1.bar(monthly_metrics.index, monthly_metrics['total_views'], color='lightgray', label='총 쑰회수')
add_value_labels(ax1, is_bar=True, fmt="{:,.0f}") # λ§‰λŒ€κ·Έλž˜ν”„ κ°’ ν‘œμ‹œ
ax1.set_ylabel('총 쑰회수', fontsize=12)
ax1_twin = ax1.twinx()
line1 = ax1_twin.plot(monthly_metrics.index, monthly_metrics['total_likes'], marker='o', color='coral', label='총 μ’‹μ•„μš”')
add_value_labels(ax1_twin, is_bar=False, fmt="{:.0f}") # 꺾은선 κ°’ ν‘œμ‹œ
ax1_twin.set_ylabel('총 μ’‹μ•„μš”', fontsize=12)
# λ²”λ‘€ ν•©μΉ˜κΈ°
lines, labels = ax1.get_legend_handles_labels()
lines2, labels2 = ax1_twin.get_legend_handles_labels()
ax1_twin.legend(lines + lines2, labels + labels2, loc='upper left')
ax1.set_title('월별 총 쑰회수 및 μ’‹μ•„μš”', fontsize=16)
# ν•˜λ‹¨ κ·Έλž˜ν”„: μ„±μž₯λ₯  (%)
ax2 = axes[1]
ax2.plot(monthly_metrics.index, monthly_metrics['total_views_mom'], marker='s', linestyle='--', label='쑰회수 μ„±μž₯λ₯  (%)')
ax2.plot(monthly_metrics.index, monthly_metrics['total_likes_mom'], marker='^', linestyle='--', label='μ’‹μ•„μš” μ„±μž₯λ₯  (%)')
ax2.axhline(0, color='red', linewidth=1, linestyle=':')
ax2.set_ylabel('μ „μ›” λŒ€λΉ„ μ„±μž₯λ₯  (%)', fontsize=12)
ax2.legend()
ax2.set_title('월별 μ£Όμš” μ§€ν‘œ μ„±μž₯λ₯  (MoM)', fontsize=16)
plt.tight_layout()
plt.savefig(f'{output_dir}/monthly_performance_and_growth.png')
plt.close()
print(" - 월별 μ„±κ³Ό 및 μ„±μž₯λ₯  뢄석 μ™„λ£Œ. (monthly_performance_and_growth.png μ €μž₯)")
# --- 2. 월별 μΉ΄ν…Œκ³ λ¦¬ λ°œν–‰ 비쀑 (μ‹œκ°ν™” + 데이터 ν…Œμ΄λΈ”) ---
monthly_category_dist = data['merged'].groupby(['publish_month', 'category'])['article_id'].count().unstack().fillna(0)
monthly_category_prop = monthly_category_dist.div(monthly_category_dist.sum(axis=1), axis=0) * 100
top_categories = data['merged']['category'].value_counts().nlargest(7).index
other_categories = monthly_category_prop.columns.difference(top_categories)
monthly_category_prop['기타'] = monthly_category_prop[other_categories].sum(axis=1)
# μ‹œκ°ν™”
monthly_category_prop[top_categories.tolist() + ['기타']].plot(
kind='bar', stacked=True, figsize=(16, 8), colormap='tab20c'
)
plt.title('월별 μ½˜ν…μΈ  μΉ΄ν…Œκ³ λ¦¬ λ°œν–‰ 비쀑 λ³€ν™” (%)', fontsize=18)
plt.xlabel('κΈ°κ°„ (μ›”)'); plt.ylabel('μΉ΄ν…Œκ³ λ¦¬ 비쀑 (%)'); plt.xticks(rotation=45)
plt.legend(title='Category', bbox_to_anchor=(1.02, 1), loc='upper left')
plt.tight_layout()
plt.savefig(f'{output_dir}/monthly_category_distribution_with_values.png')
plt.close()
# 데이터 ν…Œμ΄λΈ” 좜λ ₯
print("\n--- 월별 μƒμœ„ μΉ΄ν…Œκ³ λ¦¬ λ°œν–‰ 비쀑 (%) 데이터 ---")
category_table_data = monthly_category_prop[top_categories.tolist() + ['기타']].round(1)
print(category_table_data)
print(" - 월별 μΉ΄ν…Œκ³ λ¦¬ 비쀑 뢄석 μ™„λ£Œ. (monthly_category_distribution_with_values.png μ €μž₯ 및 ν…Œμ΄λΈ” 좜λ ₯)")
# --- 3. 월별 핡심 λ…μž μ—°λ ΉμΈ΅ (μ‹œκ°ν™” + 데이터 ν…Œμ΄λΈ”) ---
monthly_age_views = data['demo'].groupby(['period', 'age_group'])['views'].sum().unstack().fillna(0)
monthly_age_prop = (monthly_age_views.div(monthly_age_views.sum(axis=1), axis=0) * 100).round(1)
# μ‹œκ°ν™”
monthly_age_prop.plot(kind='line', marker='o', figsize=(18, 9), colormap='viridis', ms=4)
plt.title('월별 μ‘°νšŒμˆ˜μ— λŒ€ν•œ μ—°λ ΉλŒ€λ³„ 기여도 λ³€ν™” (%)', fontsize=18)
plt.xlabel('κΈ°κ°„ (μ›”)'); plt.ylabel('μ—°λ ΉλŒ€λ³„ 쑰회수 비쀑 (%)'); plt.xticks(rotation=45)
plt.legend(title='Age Group', bbox_to_anchor=(1.02, 1), loc='upper left')
plt.grid(which='major', linestyle='--', linewidth='0.5')
plt.tight_layout()
plt.savefig(f'{output_dir}/monthly_age_contribution_line.png')
plt.close()
# 데이터 ν…Œμ΄λΈ” 좜λ ₯
print("\n--- 월별 μ—°λ ΉλŒ€ 기여도 (%) 데이터 ---")
print(monthly_age_prop)
print(" - 월별 핡심 λ…μžμΈ΅ λ³€ν™” 뢄석 μ™„λ£Œ. (monthly_age_contribution_line.png μ €μž₯ 및 ν…Œμ΄λΈ” 좜λ ₯)")
# λ³΄κ³ μ„œμ— 전달할 데이터 λ°˜ν™˜
return {
"monthly_metrics": monthly_metrics,
"category_table": category_table_data,
"age_table": monthly_age_prop
}
# 5. μ’…ν•© μΈμ‚¬μ΄νŠΈ 생성 (λ³΄κ³ μ„œ λ‚΄μš© μ—…λ°μ΄νŠΈ)
def generate_insights_report(monthly_data, output_dir):
print("\n[단계 6] μ’…ν•© μΈμ‚¬μ΄νŠΈ λ³΄κ³ μ„œ 생성 (월별 뢄석 수치 κ°•ν™”)...")
# 데이터 ν…Œμ΄λΈ”μ„ λ¬Έμžμ—΄λ‘œ λ³€ν™˜
category_table_str = monthly_data['category_table'].to_string()
age_table_str = monthly_data['age_table'].to_string()
report = f"""
# 신문과방솑 λ…μž 데이터 심측 뢄석 λ³΄κ³ μ„œ (월별 νŠΈλ Œλ“œ 수치 κ°•ν™”)
생성일: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
(κΈ°μ‘΄ 1 ~ 4 μ„Ήμ…˜ λ‚΄μš© μƒλž΅)
...
## 5. β˜… 수치둜 λ³΄λŠ” 월별 동적 νŠΈλ Œλ“œ 뢄석 β˜…
μ‹œκ°„μ˜ 흐름에 λ”°λ₯Έ μ„±κ³Ό, μ „λž΅, λ…μžμΈ΅μ˜ λ³€ν™”λ₯Ό 수치 μ€‘μ‹¬μœΌλ‘œ λΆ„μ„ν•œ κ²°κ³Ό, λ‹€μŒκ³Ό 같은 ꡬ체적인 μΈμ‚¬μ΄νŠΈλ₯Ό λ„μΆœν–ˆμŠ΅λ‹ˆλ‹€.
### 5.1. μ„±κ³Όμ˜ 변동성과 μ„±μž₯ λͺ¨λ©˜ν…€
- **μ„±κ³Ό 좔이**: 2024λ…„ 4μ›”, 총 μ‘°νšŒμˆ˜λŠ” 21,015회λ₯Ό κΈ°λ‘ν•˜λ©° μ „μ›” λŒ€λΉ„ **16.2%의 높은 μ„±μž₯λ₯ **을 λ³΄μ˜€μŠ΅λ‹ˆλ‹€. 특히 ν•΄λ‹Ή μ›”μ˜ μ’‹μ•„μš” μˆ˜λŠ” 290개둜, **μ „μ›” λŒ€λΉ„ 161.3%λΌλŠ” 폭발적인 증가**λ₯Ό κΈ°λ‘ν–ˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” νŠΉμ • 기획 기사가 λ…μžλ“€μ—κ²Œ 큰 ν˜Έμ‘μ„ μ–»μ—ˆμŒμ„ μ˜λ―Έν•©λ‹ˆλ‹€. (monthly_performance_and_growth.png μ°Έκ³ )
- **μ„±μž₯κ³Ό ν•˜λ½**: 반면, 2025λ…„ 1월은 쑰회수(-25.5%)와 μ’‹μ•„μš”(-61.6%) λͺ¨λ‘ 큰 폭으둜 ν•˜λ½ν•˜λŠ” λͺ¨μŠ΅μ„ λ³΄μ˜€μŠ΅λ‹ˆλ‹€. 이처럼 월별 μ„±κ³Ό 변동성이 ν¬λ―€λ‘œ, **성곡 μ›”μ˜ μš”μΈμ„ λΆ„μ„ν•˜μ—¬ ν•˜λ½ 월에 μ μš©ν•˜λŠ” μ „λž΅**이 μ‹œκΈ‰ν•©λ‹ˆλ‹€.
### 5.2. λ°μ΄ν„°λ‘œ μž…μ¦λœ μ½˜ν…μΈ  μ „λž΅μ˜ μ§„ν™”
- **μ „λž΅ λ³€ν™”**: μ•„λž˜ 데이터 ν…Œμ΄λΈ”μ—μ„œ λ³Ό 수 μžˆλ“―μ΄, 2024λ…„ ν›„λ°˜λΆ€ν„° 'λ―Έλ””μ–΄Β·AIνŠΈλ Œλ“œ' μΉ΄ν…Œκ³ λ¦¬μ˜ λ°œν–‰ 비쀑이 κΎΈμ€€νžˆ μ¦κ°€ν•˜μ—¬ 졜근 μ›”μ—λŠ” **전체 μ½˜ν…μΈ μ˜ μ•½ 5%**λ₯Ό μ°¨μ§€ν•˜λŠ” μ£Όμš” μΉ΄ν…Œκ³ λ¦¬λ‘œ 자리 μž‘μ•˜μŠ΅λ‹ˆλ‹€.
- **κ²°κ³Ό**: 이 μ „λž΅μ€ μ„±κ³΅μ μ΄μ—ˆμŠ΅λ‹ˆλ‹€. 'λ―Έλ””μ–΄Β·AIνŠΈλ Œλ“œ'λŠ” 평균 쑰회수 및 참여도가 높은 μΉ΄ν…Œκ³ λ¦¬μ΄λ©°, μ΄λŸ¬ν•œ μ½˜ν…μΈ μ˜ μ¦κ°€λŠ” μƒˆλ‘œμš΄ μ „λ¬Έ λ…μžμΈ΅ μœ μž…μ— κΈ°μ—¬ν–ˆμŠ΅λ‹ˆλ‹€.
(monthly_category_distribution_with_values.png μ°Έκ³ )
--- 월별 μƒμœ„ μΉ΄ν…Œκ³ λ¦¬ λ°œν–‰ 비쀑 (%) 데이터 ---
{category_table_str}
---------------------------------------------
### 5.3. 핡심 λ…μžμΈ΅μ˜ μ„ΈλŒ€κ΅μ²΄ 쑰짐
- **핡심 λ…μžμΈ΅**: 19-24μ„Έ 그룹이 μ—¬μ „νžˆ κ°€μž₯ 큰 비쀑(평균 μ•½ 20~25%)을 μ°¨μ§€ν•˜λŠ” 핡심 λ…μžμΈ΅μž…λ‹ˆλ‹€.
- **μ£Όλͺ©ν•  λ³€ν™”**: ν•˜μ§€λ§Œ μ•„λž˜ λ°μ΄ν„°μ—μ„œ λͺ…ν™•νžˆ 보이듯이, 2025λ…„ λ“€μ–΄ **30-34μ„Έ λ…μžμΈ΅μ˜ 기여도가 12.1%μ—μ„œ 14.5%둜 κΎΈμ€€νžˆ μƒμŠΉ**ν•˜λŠ” νŠΈλ Œλ“œκ°€ λ‚˜νƒ€λ‚¬μŠ΅λ‹ˆλ‹€. μ΄λŠ” μƒˆλ‘œμš΄ μ„±μž₯ 동λ ₯이 될 수 μžˆλŠ” 맀우 긍정적인 μ‹ ν˜Έμž…λ‹ˆλ‹€. 반면, 13-18μ„Έ λ…μžμΈ΅μ˜ 비쀑은 μ†Œν­ κ°μ†Œν•˜λŠ” μΆ”μ„Έμž…λ‹ˆλ‹€.
(monthly_age_contribution_line.png μ°Έκ³ )
--- 월별 μ—°λ ΉλŒ€ 기여도 (%) 데이터 ---
{age_table_str}
---------------------------------------------
## 6. μ΅œμ’… μ „λž΅ μ œμ–Έ (수치 기반)
1. **μ„±μž₯λ₯  기반 μ„±κ³Ό 관리**: λ§€μ›” 말, '월별 μ„±κ³Ό 및 μ„±μž₯λ₯ ' λŒ€μ‹œλ³΄λ“œλ₯Ό λ¦¬λ·°ν•˜μ—¬ **μ„±μž₯λ₯ μ΄ κΈ‰λ“±/κΈ‰λ½ν•œ 원인을 λΆ„μ„ν•˜κ³  λ‹€μŒ 달 μ½˜ν…μΈ  κΈ°νšμ— μ¦‰μ‹œ 반영**ν•˜λŠ” ν”„λ‘œμ„ΈμŠ€λ₯Ό 정립해야 ν•©λ‹ˆλ‹€.
2. **데이터 기반 μΉ΄ν…Œκ³ λ¦¬ 비쀑 쑰절**: 성곡이 μž…μ¦λœ 'λ―Έλ””μ–΄Β·AIνŠΈλ Œλ“œ'의 비쀑을 **ν˜„μž¬ 5%μ—μ„œ 8~10% μˆ˜μ€€κΉŒμ§€ μ μ§„μ μœΌλ‘œ ν™•λŒ€**ν•˜κ³ , λ°˜μ‘μ΄ μ €μ‘°ν•œ 일뢀 μΉ΄ν…Œκ³ λ¦¬μ˜ 비쀑은 μΆ•μ†Œν•˜λŠ” '선택과 집쀑'을 μ‹€ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€.
3. **30λŒ€ λ…μžμΈ΅ 집쀑 곡랡**: 기여도가 κΎΈμ€€νžˆ μƒμŠΉν•˜λŠ” 30λŒ€ λ…μžλ₯Ό **'핡심 μ„±μž₯ νƒ€κ²Ÿ'**으둜 곡식 μ§€μ •ν•˜κ³ , μ΄λ“€μ˜ 관심사인 '컀리어', 'λ―Έλ””μ–΄ μ‚°μ—… 동ν–₯', 'λΉ„μ¦ˆλ‹ˆμŠ€ λͺ¨λΈ' κ΄€λ ¨ μ½˜ν…μΈ λ₯Ό μ‹ μ„€ν•˜μ—¬ μ΄λ“€μ˜ μœ μž…μ„ 가속화해야 ν•©λ‹ˆλ‹€.
"""
report_path = f'{output_dir}/comprehensive_analysis_report_with_enhanced_trends.txt'
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
print(f"\n - μ’…ν•© μΈμ‚¬μ΄νŠΈ λ³΄κ³ μ„œ 생성 μ™„λ£Œ. ({report_path} μ €μž₯)")
# 6. 메인 μ‹€ν–‰ ν•¨μˆ˜
def main():
print("===== 신문과방솑 λ…μž 데이터 심측 뢄석 (월별 νŠΈλ Œλ“œ 수치 κ°•ν™”) =====")
data_dir, output_dir = setup_environment()
all_data = load_and_preprocess_data(data_dir)
# --- β˜… 수치/μΆ”μ„Έκ°€ κ°•ν™”λœ 월별 뢄석 μ‹€ν–‰ β˜… ---
monthly_analysis_data = analyze_enhanced_monthly_trends(all_data, output_dir)
generate_insights_report(monthly_analysis_data, output_dir)
print("\n===== λͺ¨λ“  뢄석이 μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. =====")
print(f"결과물은 '{output_dir}' ν΄λ”μ—μ„œ ν™•μΈν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€.")
if __name__ == '__main__':
main()