# 由 Copilot 生成 import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import numpy as np import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots import json from typing import Dict, List # 設定中文字體 plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False class RentalDataVisualizer: """租屋資料視覺化器""" def __init__(self, df: pd.DataFrame = None, analysis_results: Dict = None): """ 初始化視覺化器 Args: df: 資料DataFrame analysis_results: 分析結果字典 """ self.df = df self.analysis_results = analysis_results self.colors = px.colors.qualitative.Set3 def load_data(self, data_path: str): """載入資料""" try: if data_path.endswith('.csv'): self.df = pd.read_csv(data_path, encoding='utf-8-sig') else: raise ValueError("請提供CSV格式的資料檔案") print(f"成功載入 {len(self.df)} 筆資料用於視覺化") except Exception as e: print(f"載入資料時發生錯誤: {e}") def load_analysis_results(self, results_path: str): """載入分析結果""" try: with open(results_path, 'r', encoding='utf-8') as f: self.analysis_results = json.load(f) print("分析結果載入成功") except Exception as e: print(f"載入分析結果時發生錯誤: {e}") def plot_price_distribution(self, save_path: str = "output/price_distribution.png"): """繪製租金分布圖""" if self.df is None or 'price' not in self.df.columns: print("無法繪製租金分布圖:缺少資料") return fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) # 直方圖 ax1.hist(self.df['price'], bins=20, alpha=0.7, color='skyblue', edgecolor='black') ax1.set_xlabel('租金 (元)') ax1.set_ylabel('物件數量') ax1.set_title('租金分布直方圖') ax1.grid(True, alpha=0.3) # 箱形圖 ax2.boxplot(self.df['price'], vert=True, patch_artist=True, boxprops=dict(facecolor='lightgreen', alpha=0.7)) ax2.set_ylabel('租金 (元)') ax2.set_title('租金分布箱形圖') ax2.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() print(f"租金分布圖已儲存: {save_path}") def plot_price_ranges(self, save_path: str = "output/price_ranges.png"): """繪製租金區間分布圖""" if not self.analysis_results or 'price_distribution' not in self.analysis_results: print("無法繪製租金區間圖:缺少分析結果") return dist_data = self.analysis_results['price_distribution'] fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) # 長條圖 bars = ax1.bar(dist_data['ranges'], dist_data['counts'], color=self.colors[:len(dist_data['ranges'])], alpha=0.8) ax1.set_xlabel('租金區間') ax1.set_ylabel('物件數量') ax1.set_title('各租金區間物件數量') ax1.tick_params(axis='x', rotation=45) # 在長條上顯示數值 for bar, count in zip(bars, dist_data['counts']): height = bar.get_height() ax1.text(bar.get_x() + bar.get_width()/2., height + 0.5, f'{count}', ha='center', va='bottom') # 圓餅圖 ax2.pie(dist_data['percentages'], labels=dist_data['ranges'], autopct='%1.1f%%', colors=self.colors[:len(dist_data['ranges'])], startangle=90) ax2.set_title('租金區間比例分布') plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() print(f"租金區間圖已儲存: {save_path}") def plot_area_analysis(self, save_path: str = "output/area_analysis.png"): """繪製坪數分析圖""" if self.df is None or 'area' not in self.df.columns: print("無法繪製坪數分析圖:缺少資料") return # 移除空值 area_data = self.df['area'].dropna() if len(area_data) == 0: print("無法繪製坪數分析圖:沒有有效的坪數資料") return fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) # 散點圖 - 坪數與租金關係 if 'price' in self.df.columns: valid_data = self.df.dropna(subset=['area', 'price']) if len(valid_data) > 0: ax1.scatter(valid_data['area'], valid_data['price'], alpha=0.6, color='coral', s=50) ax1.set_xlabel('坪數') ax1.set_ylabel('租金 (元)') ax1.set_title('坪數與租金關係') ax1.grid(True, alpha=0.3) # 添加趨勢線 z = np.polyfit(valid_data['area'], valid_data['price'], 1) p = np.poly1d(z) ax1.plot(valid_data['area'], p(valid_data['area']), "r--", alpha=0.8) # 坪數分布直方圖 ax2.hist(area_data, bins=15, alpha=0.7, color='lightgreen', edgecolor='black') ax2.set_xlabel('坪數') ax2.set_ylabel('物件數量') ax2.set_title('坪數分布') ax2.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() print(f"坪數分析圖已儲存: {save_path}") def plot_price_per_ping(self, save_path: str = "output/price_per_ping.png"): """繪製每坪租金分析圖""" if self.df is None or 'price_per_ping' not in self.df.columns: print("無法繪製每坪租金圖:缺少資料") return price_per_ping_data = self.df['price_per_ping'].dropna() if len(price_per_ping_data) == 0: print("無法繪製每坪租金圖:沒有有效的每坪租金資料") return fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) # 每坪租金分布 ax1.hist(price_per_ping_data, bins=20, alpha=0.7, color='gold', edgecolor='black') ax1.set_xlabel('每坪租金 (元/坪)') ax1.set_ylabel('物件數量') ax1.set_title('每坪租金分布') ax1.grid(True, alpha=0.3) # 箱形圖 ax2.boxplot(price_per_ping_data, vert=True, patch_artist=True, boxprops=dict(facecolor='orange', alpha=0.7)) ax2.set_ylabel('每坪租金 (元/坪)') ax2.set_title('每坪租金箱形圖') ax2.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() print(f"每坪租金圖已儲存: {save_path}") def plot_keywords_analysis(self, save_path: str = "output/keywords_analysis.png"): """繪製關鍵字分析圖""" if not self.analysis_results or 'description_analysis' not in self.analysis_results: print("無法繪製關鍵字分析圖:缺少分析結果") return desc_analysis = self.analysis_results['description_analysis'] if 'keywords_frequency' not in desc_analysis: print("無法繪製關鍵字分析圖:缺少關鍵字資料") return keywords_data = desc_analysis['keywords_frequency'] # 過濾出有數據的關鍵字 filtered_keywords = {k: v for k, v in keywords_data.items() if v > 0} if not filtered_keywords: print("沒有找到任何關鍵字資料") return keywords = list(filtered_keywords.keys()) frequencies = list(filtered_keywords.values()) plt.figure(figsize=(12, 8)) bars = plt.barh(keywords, frequencies, color=self.colors[:len(keywords)]) plt.xlabel('出現次數') plt.ylabel('關鍵字') plt.title('物件描述關鍵字頻率分析') plt.grid(True, alpha=0.3, axis='x') # 在長條上顯示數值 for bar, freq in zip(bars, frequencies): width = bar.get_width() plt.text(width + 0.1, bar.get_y() + bar.get_height()/2., f'{freq}', ha='left', va='center') plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() print(f"關鍵字分析圖已儲存: {save_path}") def create_interactive_dashboard(self, save_path: str = "output/dashboard.html"): """創建互動式儀表板""" if self.df is None: print("無法創建儀表板:缺少資料") return # 創建子圖 fig = make_subplots( rows=2, cols=2, subplot_titles=('租金分布', '坪數vs租金', '租金區間分布', '每坪租金分布'), specs=[[{"secondary_y": False}, {"secondary_y": False}], [{"type": "bar"}, {"secondary_y": False}]] ) # 1. 租金分布直方圖 fig.add_trace( go.Histogram(x=self.df['price'], name='租金分布', nbinsx=20, marker_color='skyblue', opacity=0.7), row=1, col=1 ) # 2. 坪數vs租金散點圖 if 'area' in self.df.columns: valid_data = self.df.dropna(subset=['area', 'price']) if len(valid_data) > 0: fig.add_trace( go.Scatter(x=valid_data['area'], y=valid_data['price'], mode='markers', name='坪數vs租金', marker=dict(color='coral', size=8, opacity=0.6)), row=1, col=2 ) # 3. 租金區間分布 if self.analysis_results and 'price_distribution' in self.analysis_results: dist_data = self.analysis_results['price_distribution'] fig.add_trace( go.Bar(x=dist_data['ranges'], y=dist_data['counts'], name='租金區間', marker_color='lightgreen'), row=2, col=1 ) # 4. 每坪租金分布 if 'price_per_ping' in self.df.columns: price_per_ping_data = self.df['price_per_ping'].dropna() if len(price_per_ping_data) > 0: fig.add_trace( go.Histogram(x=price_per_ping_data, name='每坪租金', nbinsx=15, marker_color='gold', opacity=0.7), row=2, col=2 ) # 更新布局 fig.update_layout( title_text="高雄市鼓山區租屋市場分析儀表板", title_x=0.5, height=800, showlegend=False ) # 更新軸標籤 fig.update_xaxes(title_text="租金 (元)", row=1, col=1) fig.update_yaxes(title_text="物件數量", row=1, col=1) fig.update_xaxes(title_text="坪數", row=1, col=2) fig.update_yaxes(title_text="租金 (元)", row=1, col=2) fig.update_xaxes(title_text="租金區間", row=2, col=1) fig.update_yaxes(title_text="物件數量", row=2, col=1) fig.update_xaxes(title_text="每坪租金 (元/坪)", row=2, col=2) fig.update_yaxes(title_text="物件數量", row=2, col=2) # 儲存互動式圖表 fig.write_html(save_path) print(f"互動式儀表板已儲存: {save_path}") def generate_all_visualizations(self): """生成所有視覺化圖表""" print("開始生成視覺化圖表...") # 靜態圖表 self.plot_price_distribution() self.plot_price_ranges() self.plot_area_analysis() self.plot_price_per_ping() self.plot_keywords_analysis() # 互動式儀表板 self.create_interactive_dashboard() print("所有視覺化圖表生成完成!") def create_summary_report(self, save_path: str = "output/summary_report.png"): """創建摘要報告圖""" if not self.analysis_results or 'basic_stats' not in self.analysis_results: print("無法創建摘要報告:缺少分析結果") return fig, ax = plt.subplots(figsize=(12, 8)) ax.axis('off') # 標題 fig.suptitle('高雄市鼓山區租屋市場分析摘要報告', fontsize=20, fontweight='bold', y=0.95) # 基本統計資訊 stats = self.analysis_results['basic_stats'] # 創建文字內容 report_text = f""" ? 市場概況 ? 總物件數: {stats['total_properties']} 筆 ? 資料範圍: 2房、整層、電梯大樓 ? 租金統計 ? 平均租金: {stats['price_stats']['mean']:,} 元 ? 中位數租金: {stats['price_stats']['median']:,} 元 ? 最低租金: {stats['price_stats']['min']:,} 元 ? 最高租金: {stats['price_stats']['max']:,} 元 ? 標準差: {stats['price_stats']['std']:,} 元 ? 分布特徵 ? 第一四分位數: {stats['price_stats']['q25']:,} 元 ? 第三四分位數: {stats['price_stats']['q75']:,} 元 """ # 添加面積統計(如果有的話) if 'area_stats' in stats and stats['area_stats']: area_stats = stats['area_stats'] report_text += f""" ? 坪數統計 ? 平均坪數: {area_stats['mean']} 坪 ? 中位數坪數: {area_stats['median']} 坪 ? 最小坪數: {area_stats['min']} 坪 ? 最大坪數: {area_stats['max']} 坪 """ # 添加每坪租金統計(如果有的話) if 'price_per_ping_stats' in stats and stats['price_per_ping_stats']: pp_stats = stats['price_per_ping_stats'] report_text += f""" ? 每坪租金統計 ? 平均每坪租金: {pp_stats['mean']:,} 元/坪 ? 中位數每坪租金: {pp_stats['median']:,} 元/坪 ? 最低每坪租金: {pp_stats['min']:,} 元/坪 ? 最高每坪租金: {pp_stats['max']:,} 元/坪 """ # 添加洞察(如果有的話) if 'insights' in self.analysis_results: report_text += "\n\n? 重要洞察\n" for i, insight in enumerate(self.analysis_results['insights'], 1): report_text += f"? {insight}\n" # 顯示文字 ax.text(0.05, 0.95, report_text, transform=ax.transAxes, fontsize=12, verticalalignment='top', fontfamily='monospace', bbox=dict(boxstyle="round,pad=0.5", facecolor="lightblue", alpha=0.8)) plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() print(f"摘要報告已儲存: {save_path}") if __name__ == "__main__": # 測試視覺化器 visualizer = RentalDataVisualizer() # 載入資料 visualizer.load_data("output/rental_data.csv") visualizer.load_analysis_results("output/analysis_results.json") # 生成所有視覺化圖表 visualizer.generate_all_visualizations() # 創建摘要報告 visualizer.create_summary_report()