6月4日

数值分析

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import jieba
from wordcloud import WordCloud
import matplotlib
import os
import sys
import logging
from pathlib import Path
import matplotlib.font_manager as fm
from collections import Counter
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import traceback
import re

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)

# 创建输出目录
OUTPUT_DIR = Path('output')
OUTPUT_DIR.mkdir(exist_ok=True)

# 设置matplotlib中文字体
def setup_chinese_font():
    """设置中文字体"""
    try:
        # 检查系统字体
        font_list = [f.name for f in fm.fontManager.ttflist]
        logger.info(f"系统已安装的字体: {font_list}")
        
        # 尝试设置中文字体
        chinese_fonts = [
            'WenQuanYi Micro Hei',
            'WenQuanYi Zen Hei',
            'Noto Sans CJK SC',
            'Noto Sans CJK TC',
            'Noto Sans CJK JP',
            'Microsoft YaHei',
            'SimHei',
            'SimSun',
            'NSimSun',
            'FangSong',
            'KaiTi'
        ]
        
        # 查找可用的中文字体
        available_fonts = [f for f in chinese_fonts if f in font_list]
        
        if available_fonts:
            plt.rcParams['font.family'] = 'sans-serif'
            plt.rcParams['font.sans-serif'] = available_fonts
            logger.info(f"使用字体: {available_fonts[0]}")
        else:
            # 如果没有找到中文字体,尝试使用系统默认字体
            plt.rcParams['font.family'] = 'sans-serif'
            plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
            logger.warning("未找到中文字体,将使用默认字体")
        
        plt.rcParams['axes.unicode_minus'] = False
    except Exception as e:
        logger.warning(f"设置中文字体时出错: {e}")
        logger.warning(traceback.format_exc())
        plt.rcParams['font.sans-serif'] = ['DejaVu Sans']

# 积极词和消极词词典及其权重
POSITIVE_WORDS = {
    '': 10.0, '': 12.0, '': 10.0, '优秀': 15.0, '完美': 20.0, '精彩': 15.0, '出色': 15.0, 
    '惊艳': 20.0, '震撼': 20.0, '惊喜': 18.0, '喜欢': 10.0, '满意': 12.0, '推荐': 15.0, 
    '值得': 12.0, '期待': 8.0, '支持': 10.0, '鼓励': 10.0, '进步': 10.0, '创新': 12.0,
    '独特': 12.0, '精美': 15.0, '流畅': 12.0, '稳定': 10.0, '优化': 10.0, '改进': 10.0,
    '提升': 10.0, '突破': 12.0, '超越': 15.0, '享受': 15.0, '沉浸': 12.0, '体验': 10.0,
    '乐趣': 12.0, '快乐': 15.0, '开心': 15.0, '兴奋': 15.0, '激动': 15.0, '感动': 18.0,
    '良心': 15.0, '诚意': 12.0, '用心': 12.0, '认真': 10.0, '负责': 10.0, '专业': 12.0,
    '成熟': 10.0, '完善': 10.0, '平衡': 10.0, '神作': 20.0, '经典': 18.0, '神级': 20.0,
    '无敌': 18.0, '绝了': 15.0, '太棒': 18.0, '太赞': 18.0, '太强': 15.0, '太爽': 15.0,
    '太好玩': 18.0, '太精彩': 18.0, '太震撼': 20.0, '太惊艳': 20.0, '太感动': 20.0
}

NEGATIVE_WORDS = {
    '': -10.0, '': -15.0, '': -12.0, '失望': -15.0, '垃圾': -20.0, '失败': -15.0,
    '糟糕': -15.0, '问题': -10.0, '缺陷': -12.0, '不足': -10.0, '卡顿': -12.0, '延迟': -10.0,
    '掉帧': -12.0, '崩溃': -15.0, '闪退': -15.0, '错误': -10.0, 'bug': -10.0, '故障': -12.0,
    '异常': -10.0, '': -10.0, '不值': -15.0, '浪费': -15.0, '': -18.0, '': -20.0,
    '忽悠': -18.0, '敷衍': -15.0, '应付': -12.0, '粗糙': -12.0, '无聊': -15.0, '单调': -12.0,
    '重复': -10.0, '枯燥': -15.0, '乏味': -15.0, '无趣': -15.0, '失望': -15.0, '后悔': -18.0,
    '遗憾': -12.0, '': -10.0, '复杂': -10.0, '混乱': -12.0, '繁琐': -10.0, '麻烦': -10.0,
    '困难': -10.0, '痛苦': -15.0, '折磨': -18.0, '煎熬': -18.0, '太差': -18.0, '太烂': -20.0,
    '太糟': -18.0, '太失望': -20.0, '太垃圾': -20.0, '太失败': -20.0, '太糟糕': -20.0,
    '太贵': -15.0, '太不值': -20.0, '太坑': -20.0, '太骗': -20.0, '太无聊': -20.0,
    '太单调': -18.0, '太枯燥': -20.0, '太乏味': -20.0, '太无趣': -20.0, '太后悔': -20.0
}

# 情感强度修饰词
INTENSIFIERS = {
    '非常': 1.5, '特别': 1.5, '极其': 2.0, '十分': 1.5, '': 1.5, '': 1.2,
    '有点': 0.8, '稍微': 0.8, '略微': 0.8, '几乎': 0.8, '基本': 0.8
}

# 否定词
NEGATORS = {'', '', '没有', '', '不要', '不能', '不会', '不应该', '不应该', '不应该'}

def clean_text(text):
    """清理文本数据"""
    if pd.isna(text):
        return ""
    # 转换为字符串
    text = str(text)
    # 移除换行符和制表符
    text = text.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
    # 移除多余的空格
    text = ' '.join(text.split())
    # 只保留中文、英文、数字和基本标点
    text = re.sub(r'[^\u4e00-\u9fff\w\s.,!?,。!?]', '', text)
    return text.strip()

def load_data(file_path):
    """加载数据"""
    try:
        logger.info(f"开始加载数据文件: {file_path}")
        if not os.path.exists(file_path):
            logger.error(f"数据文件不存在: {file_path}")
            sys.exit(1)
        df = pd.read_csv(file_path)
        logger.info(f"成功加载数据,共 {len(df)} 行")
        return df
    except Exception as e:
        logger.error(f"加载数据失败: {e}")
        logger.error(traceback.format_exc())
        sys.exit(1)

def calculate_sentiment_scores(text):
    """计算评论的积极和消极得分"""
    if pd.isna(text) or not text.strip():
        return 0, 0
    
    # 分词
    words = list(jieba.cut(text))
    positive_score = 0
    negative_score = 0
    negator_count = 0
    
    for i, word in enumerate(words):
        # 检查是否是否定词
        if word in NEGATORS:
            negator_count += 1
            continue
            
        # 检查是否是情感强度修饰词
        intensifier = 1.0
        if i > 0 and words[i-1] in INTENSIFIERS:
            intensifier = INTENSIFIERS[words[i-1]]
            
        # 计算积极得分
        if word in POSITIVE_WORDS:
            score = POSITIVE_WORDS[word] * intensifier * (-1 if negator_count % 2 == 1 else 1)
            if score > 0:
                positive_score += score
            else:
                negative_score += abs(score)
        # 计算消极得分
        elif word in NEGATIVE_WORDS:
            score = NEGATIVE_WORDS[word] * intensifier * (-1 if negator_count % 2 == 1 else 1)
            if score < 0:
                negative_score += abs(score)
            else:
                positive_score += score
            
        # 重置否定词计数
        if word in POSITIVE_WORDS or word in NEGATIVE_WORDS:
            negator_count = 0
    
    # 将分数限制在0到100之间
    positive_score = min(positive_score, 100)
    negative_score = min(negative_score, 100)
    
    return positive_score, negative_score

def analyze_comments(df):
    """分析每条评论的情感得分"""
    try:
        logger.info("开始分析评论情感得分...")
        
        # 计算每条评论的积极和消极得分
        df['积极得分'] = 0.0
        df['消极得分'] = 0.0
        
        for idx, row in df.iterrows():
            positive_score, negative_score = calculate_sentiment_scores(row['评论'])
            df.at[idx, '积极得分'] = positive_score
            df.at[idx, '消极得分'] = negative_score
            
            # 输出评论分析结果
            logger.info(f"\n评论 {idx + 1}:")
            logger.info(f"内容: {row['评论'][:100]}...")
            logger.info(f"积极得分: {positive_score:.1f}")
            logger.info(f"消极得分: {negative_score:.1f}")
            logger.info(f"是否推荐: {row['是否推荐']}")
            logger.info("-" * 50)
        
        # 保存分析结果到CSV文件
        output_file = OUTPUT_DIR / 'comment_analysis.csv'
        df[['评论', '积极得分', '消极得分', '是否推荐']].to_csv(output_file, index=False, encoding='utf-8')
        logger.info(f"\n分析结果已保存到: {output_file}")
        
        # 输出统计信息
        logger.info("\n情感分析统计:")
        logger.info(f"平均积极得分: {df['积极得分'].mean():.2f}")
        logger.info(f"平均消极得分: {df['消极得分'].mean():.2f}")
        logger.info(f"最高积极得分: {df['积极得分'].max():.2f}")
        logger.info(f"最高消极得分: {df['消极得分'].max():.2f}")
        
    except Exception as e:
        logger.error(f"分析评论情感得分失败: {e}")
        logger.error(traceback.format_exc())

def plot_daily_reviews(df):
    """绘制每日评价人数曲线"""
    try:
        logger.info("开始生成每日评价人数曲线...")
        
        # 转换日期格式
        def convert_date(date_str):
            try:
                # 处理"8月20日"这样的格式
                month = int(date_str.split('')[0])
                day = int(date_str.split('')[1].split('')[0])
                return pd.Timestamp(2024, month, day)
            except:
                return pd.NaT
        
        # 转换日期列
        df['发布时间(2024年)'] = df['发布时间(2024年)'].apply(convert_date)
        
        # 按日期统计评论数
        daily_counts = df.groupby('发布时间(2024年)').size().reset_index(name='评论数')
        
        # 创建图形
        plt.figure(figsize=(15, 6))
        
        # 绘制折线图
        plt.plot(daily_counts['发布时间(2024年)'], daily_counts['评论数'], 
                marker='o', linestyle='-', linewidth=2, markersize=4)
        
        # 设置标题和标签
        plt.title('每日评价人数趋势')
        plt.xlabel('日期')
        plt.ylabel('评论数量')
        
        # 设置x轴日期格式
        plt.gcf().autofmt_xdate()
        
        # 添加网格线
        plt.grid(True, linestyle='--', alpha=0.7)
        
        # 保存图片
        plt.savefig(OUTPUT_DIR / 'daily_reviews.png', dpi=300, bbox_inches='tight')
        plt.close()
        
        # 输出统计信息
        logger.info(f"评价时间范围: {daily_counts['发布时间(2024年)'].min()} 至 {daily_counts['发布时间(2024年)'].max()}")
        logger.info(f"平均每日评论数: {daily_counts['评论数'].mean():.2f}")
        logger.info(f"最高日评论数: {daily_counts['评论数'].max()}")
        logger.info(f"最低日评论数: {daily_counts['评论数'].min()}")
        
    except Exception as e:
        logger.error(f"生成每日评价人数曲线失败: {e}")
        logger.error(traceback.format_exc())

def plot_recommendation_pie(df):
    """绘制推荐/不推荐饼图"""
    try:
        logger.info("开始生成推荐/不推荐饼图...")
        
        # 统计推荐和不推荐的数量
        recommendation_counts = df['是否推荐'].value_counts()
        
        # 创建图形
        plt.figure(figsize=(10, 8))
        
        # 绘制饼图
        plt.pie(recommendation_counts, 
                labels=['推荐', '不推荐'],
                autopct='%1.1f%%',
                colors=['#2ecc71', '#e74c3c'],
                explode=(0.05, 0),
                shadow=True)
        
        # 设置标题
        plt.title('推荐/不推荐比例')
        
        # 保存图片
        plt.savefig(OUTPUT_DIR / 'recommendation_pie.png', dpi=300, bbox_inches='tight')
        plt.close()
        
        # 输出统计信息
        total = len(df)
        logger.info(f"推荐比例: {recommendation_counts['推荐']/total*100:.1f}%")
        logger.info(f"不推荐比例: {recommendation_counts['不推荐']/total*100:.1f}%")
        
    except Exception as e:
        logger.error(f"生成推荐/不推荐饼图失败: {e}")
        logger.error(traceback.format_exc())

def analyze_playtime_recommendation(df):
    """分析游戏时长与推荐关系"""
    try:
        logger.info("开始生成游戏时长与推荐关系图...")
        
        # 创建图形
        plt.figure(figsize=(12, 6))
        
        # 绘制箱线图
        sns.boxplot(x='是否推荐', y='大圣游戏时长', data=df)
        
        # 设置标题和标签
        plt.title('游戏时长与推荐关系')
        plt.xlabel('是否推荐')
        plt.ylabel('游戏时长(小时)')
        
        # 保存图片
        plt.savefig(OUTPUT_DIR / 'playtime_recommendation.png', dpi=300, bbox_inches='tight')
        plt.close()
        
        # 计算统计信息
        recommended_stats = df[df['是否推荐'] == '推荐']['大圣游戏时长'].describe()
        not_recommended_stats = df[df['是否推荐'] == '不推荐']['大圣游戏时长'].describe()
        
        logger.info("\n游戏时长统计:")
        logger.info("推荐评论:")
        logger.info(f"平均时长: {recommended_stats['mean']:.2f}小时")
        logger.info(f"中位数时长: {recommended_stats['50%']:.2f}小时")
        logger.info(f"最长时长: {recommended_stats['max']:.2f}小时")
        logger.info("\n不推荐评论:")
        logger.info(f"平均时长: {not_recommended_stats['mean']:.2f}小时")
        logger.info(f"中位数时长: {not_recommended_stats['50%']:.2f}小时")
        logger.info(f"最长时长: {not_recommended_stats['max']:.2f}小时")
        
    except Exception as e:
        logger.error(f"生成游戏时长与推荐关系图失败: {e}")
        logger.error(traceback.format_exc())

def plot_sentiment_distribution(df):
    """绘制情感得分分布图"""
    try:
        logger.info("开始生成情感得分分布图...")
        
        # 创建图形
        plt.figure(figsize=(12, 6))
        
        # 绘制积极得分分布
        plt.subplot(1, 2, 1)
        sns.histplot(data=df, x='积极得分', bins=50, hue='是否推荐', 
                    multiple="stack", palette=['red', 'green'])
        plt.title('积极得分分布')
        plt.xlabel('积极得分')
        plt.ylabel('评论数量')
        
        # 绘制消极得分分布
        plt.subplot(1, 2, 2)
        sns.histplot(data=df, x='消极得分', bins=50, hue='是否推荐', 
                    multiple="stack", palette=['red', 'green'])
        plt.title('消极得分分布')
        plt.xlabel('消极得分')
        plt.ylabel('评论数量')
        
        # 调整布局
        plt.tight_layout()
        
        # 保存图片
        plt.savefig(OUTPUT_DIR / 'sentiment_distribution.png', dpi=300, bbox_inches='tight')
        plt.close()
        
    except Exception as e:
        logger.error(f"生成情感得分分布图失败: {e}")
        logger.error(traceback.format_exc())

def generate_wordcloud(df, is_recommended):
    """生成词云图"""
    try:
        logger.info(f"开始生成{'推荐' if is_recommended else '不推荐'}评论词云图...")
        # 提取并清理评论
        comments = df[df['是否推荐'] == is_recommended]['评论'].apply(clean_text)
        logger.info(f"{'推荐' if is_recommended else '不推荐'}评论数量: {len(comments)}")
        comments = comments[comments.str.len() > 0]
        
        if len(comments) == 0:
            logger.warning(f"没有足够的评论来生成{'推荐' if is_recommended else '不推荐'}评论的词云图")
            return
            
        # 合并所有评论
        text = ' '.join(comments)
        
        # 分词
        words = jieba.cut(text)
        # 过滤短词并统计词频
        word_freq = Counter(word for word in words if len(word.strip()) > 1)
        
        if not word_freq:
            logger.warning(f"分词后没有足够的词来生成{'推荐' if is_recommended else '不推荐'}评论的词云图")
            return
            
        logger.info(f"分词后词数: {len(word_freq)}")
        
        # 分离积极词和消极词
        positive_words = {word: freq for word, freq in word_freq.items() if word in POSITIVE_WORDS}
        negative_words = {word: freq for word, freq in word_freq.items() if word in NEGATIVE_WORDS}
        
        # 创建一个新的图像
        img = Image.new('RGBA', (800, 400), color='white')
        draw = ImageDraw.Draw(img)
        
        # 尝试使用已安装的中文字体
        font_paths = [
            '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
            '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
            '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
            '/usr/share/fonts/truetype/arphic/uming.ttc',
            '/usr/share/fonts/truetype/arphic/ukai.ttc'
        ]
        
        # 找到第一个可用的字体
        font_path = None
        for path in font_paths:
            if os.path.exists(path):
                font_path = path
                logger.info(f"词云使用字体: {path}")
                break
        
        if not font_path:
            logger.warning("未找到可用的中文字体,将使用默认字体")
            font_path = None
        
        # 绘制标题
        try:
            title_font = ImageFont.truetype(font_path, 24) if font_path else ImageFont.load_default()
            draw.text((20, 10), f"{'推荐' if is_recommended else '不推荐'}评论词云", font=title_font, fill='black')
        except Exception as e:
            logger.warning(f"绘制标题失败: {e}")
        
        # 绘制积极词
        x, y = 20, 50
        max_width = 760
        max_height = 160
        line_height = 0
        
        # 按词频排序
        sorted_positive = sorted(positive_words.items(), key=lambda x: x[1], reverse=True)
        max_freq = max(positive_words.values()) if positive_words else 1
        
        # 绘制积极词标题
        try:
            subtitle_font = ImageFont.truetype(font_path, 16) if font_path else ImageFont.load_default()
            draw.text((x, y), "积极词:", font=subtitle_font, fill='green')
            y += 25
        except Exception as e:
            logger.warning(f"绘制积极词标题失败: {e}")
        
        # 绘制积极词
        for word, freq in sorted_positive[:25]:  # 只取前25个词
            font_size = int(8 + (freq / max_freq) * 32)
            try:
                font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
                bbox = draw.textsize(word, font=font)
                
                if x + bbox[0] > max_width:
                    x = 20
                    y += line_height + 5
                    line_height = 0
                
                if y + bbox[1] > max_height:
                    break
                
                line_height = max(line_height, bbox[1])
                draw.text((x, y), word, font=font, fill='green')
                x += bbox[0] + 10
            except Exception as e:
                logger.warning(f"绘制积极词失败: {e}")
                continue
        
        # 绘制消极词
        x, y = 20, 220
        line_height = 0
        
        # 按词频排序
        sorted_negative = sorted(negative_words.items(), key=lambda x: x[1], reverse=True)
        max_freq = max(negative_words.values()) if negative_words else 1
        
        # 绘制消极词标题
        try:
            draw.text((x, y), "消极词:", font=subtitle_font, fill='red')
            y += 25
        except Exception as e:
            logger.warning(f"绘制消极词标题失败: {e}")
        
        # 绘制消极词
        for word, freq in sorted_negative[:25]:  # 只取前25个词
            font_size = int(8 + (freq / max_freq) * 32)
            try:
                font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
                bbox = draw.textsize(word, font=font)
                
                if x + bbox[0] > max_width:
                    x = 20
                    y += line_height + 5
                    line_height = 0
                
                if y + bbox[1] > max_height + 160:  # 调整高度限制
                    break
                
                line_height = max(line_height, bbox[1])
                draw.text((x, y), word, font=font, fill='red')
                x += bbox[0] + 10
            except Exception as e:
                logger.warning(f"绘制消极词失败: {e}")
                continue
        
        # 保存图像
        output_path = OUTPUT_DIR / f'wordcloud_{"recommended" if is_recommended else "not_recommended"}.png'
        img.save(output_path)
        logger.info(f"成功生成{'推荐' if is_recommended else '不推荐'}评论词云图: {output_path}")
        
    except Exception as e:
        logger.error(f"生成词云图失败: {e}")
        logger.error(traceback.format_exc())

def main():
    try:
        # 设置中文字体
        setup_chinese_font()
        
        logger.info("开始执行分析...")
        # 从CSV文件加载数据
        df = load_data('csv/wukong.csv')
        # 清理评论数据
        df['评论'] = df['评论'].apply(clean_text)
        # 打印数据统计信息
        logger.info(f"总评论数: {len(df)}")
        logger.info(f"推荐评论数: {len(df[df['是否推荐'] == '推荐'])}")
        logger.info(f"不推荐评论数: {len(df[df['是否推荐'] == '不推荐'])}")
        logger.info(f"非空评论数: {len(df[df['评论'].str.len() > 0])}")
        
        # 分析每条评论的情感得分
        analyze_comments(df)
        
        # 生成可视化
        plot_daily_reviews(df)
        plot_recommendation_pie(df)
        analyze_playtime_recommendation(df)
        plot_sentiment_distribution(df)
        
        # 生成词云
        logger.info('开始生成词云图...')
        generate_wordcloud(df, '推荐')
        generate_wordcloud(df, '不推荐')
        logger.info('所有分析完成')
    except Exception as e:
        logger.error(f'程序执行失败: {e}')
        logger.error(traceback.format_exc())
        sys.exit(1)

if __name__ == '__main__':
    main() 

 

posted @ 2025-06-09 14:56  KuanDong24  阅读(11)  评论(0)    收藏  举报