📚 引言

在上一篇Matplotlib教程中,我们学习了基础图表的绘制。今天,我们将深入探索Seaborn统计可视化库和Plotly交互式图表库,让你的数据可视化作品更加专业、美观和具有交互性。本文涵盖Seaborn的高级统计图表、Plotly的动态交互功能以及实际项目案例。


1. Seaborn:统计可视化利器

1.1 Seaborn风格设置

import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# 设置Seaborn风格
sns.set_style('whitegrid')  # 可选: darkgrid, whitegrid, dark, white, ticks
sns.set_context('notebook')  # 可选: paper, notebook, talk, poster

# 设置调色板
sns.set_palette('husl')  # 可选: deep, muted, bright, pastel, dark, colorblind

# 更精细的样式控制
sns.set_theme(
    style='whitegrid',
    palette='viridis',
    font='SimHei',
    font_scale=1.2,
    rc={'figure.figsize': (12, 6)}
)

# 加载示例数据
tips = sns.load_dataset('tips')
iris = sns.load_dataset('iris')
titanic = sns.load_dataset('titanic')

print("数据预览:")
print(tips.head())

1.2 分布图:直方图、KDE、箱线图

# 创建子图
fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# 1. 直方图 + KDE
sns.histplot(data=tips, x='total_bill', kde=True, color='#2980b9', ax=axes[0, 0])
axes[0, 0].set_title('总消费分布(直方图+KDE)', fontsize=12)

# 2. 核密度估计图
sns.kdeplot(data=tips, x='total_bill', hue='time', fill=True, alpha=0.5, ax=axes[0, 1])
axes[0, 1].set_title('KDE密度曲线(按用餐时间)', fontsize=12)

# 3. 箱线图
sns.boxplot(data=tips, x='day', y='total_bill', palette='Set2', ax=axes[0, 2])
axes[0, 2].set_title('箱线图(按星期分组)', fontsize=12)

# 4. 小提琴图
sns.violinplot(data=tips, x='day', y='total_bill', hue='sex', split=True, ax=axes[1, 0])
axes[1, 0].set_title('小提琴图(含性别对比)', fontsize=12)

# 5. 蜂群图
sns.swarmplot(data=tips, x='day', y='total_bill', hue='time', size=4, ax=axes[1, 1])
axes[1, 1].set_title('蜂群图', fontsize=12)

# 6. 累积分布图
sns.ecdfplot(data=tips, x='total_bill', hue='sex', ax=axes[1, 2])
axes[1, 2].set_title('累积分布图', fontsize=12)

plt.tight_layout()
plt.show()

1.3 关系图:散点图、联合分布、热力图

# 创建图形
fig = plt.figure(figsize=(15, 10))

# 1. 散点图 + 回归线
ax1 = fig.add_subplot(2, 2, 1)
sns.regplot(data=tips, x='total_bill', y='tip', 
            scatter_kws={'alpha': 0.6, 's': 50},
            line_kws={'color': 'red', 'linewidth': 2},
            ax=ax1)
ax1.set_title('消费金额 vs 小费金额(含回归线)', fontsize=12)

# 2. 联合分布图
ax2 = fig.add_subplot(2, 2, 2)
sns.jointplot(data=tips, x='total_bill', y='tip', kind='hex', height=5)
# 注意: jointplot 创建的是独立图形,这里仅作示例
plt.close()

# 单独绘制联合分布
g = sns.JointGrid(data=tips, x='total_bill', y='tip', height=5)
g.plot_joint(sns.scatterplot, alpha=0.5)
g.plot_marginals(sns.histplot, kde=True)
g.fig.suptitle('联合分布图', y=1.02)
plt.show()

# 3. 相关性热力图
ax3 = fig.add_subplot(2, 2, 3)
correlation = tips.select_dtypes(include=[np.number]).corr()
sns.heatmap(correlation, annot=True, cmap='coolwarm', center=0,
            square=True, linewidths=1, cbar_kws={'shrink': 0.8}, ax=ax3)
ax3.set_title('特征相关性热力图', fontsize=12)

# 4. 成对关系图
ax4 = fig.add_subplot(2, 2, 4)
sns.pairplot(iris, hue='species', diag_kind='kde', height=2)
plt.suptitle('鸢尾花数据集成对关系图', y=1.02)
plt.show()

plt.tight_layout()
plt.show()

1.4 分类图:条形图、计数图、点图

# 创建图形
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 1. 条形图(带置信区间)
sns.barplot(data=titanic.dropna(), x='class', y='fare', hue='sex', 
            ci=95, palette='Set1', ax=axes[0, 0])
axes[0, 0].set_title('不同舱位票价对比(含置信区间)', fontsize=12)

# 2. 计数图
sns.countplot(data=titanic, x='class', hue='survived', 
              palette='Set2', ax=axes[0, 1])
axes[0, 1].set_title('不同舱位幸存人数统计', fontsize=12)

# 3. 点图
sns.pointplot(data=titanic.dropna(), x='class', y='age', hue='sex',
              markers=['o', 's'], linestyles=['-', '--'], ax=axes[1, 0])
axes[1, 0].set_title('不同舱位年龄分布(按性别)', fontsize=12)

# 4. 分类散点图(Strip Plot)
sns.stripplot(data=titanic.dropna(), x='class', y='age', hue='survived',
              dodge=True, jitter=True, alpha=0.7, ax=axes[1, 1])
axes[1, 1].set_title('年龄分布与幸存情况', fontsize=12)

plt.tight_layout()
plt.show()

2. 高级统计图表

2.1 矩阵图与聚类图

# 聚类热力图(Clustermap)
from scipy.cluster import hierarchy

# 准备数据
correlation_matrix = iris.iloc[:, :-1].corr()

# 创建聚类热力图
g = sns.clustermap(correlation_matrix, annot=True, cmap='coolwarm',
                   center=0, square=True, linewidths=1,
                   figsize=(10, 8), dendrogram_ratio=0.2,
                   cbar_pos=(0.02, 0.8, 0.05, 0.18))
g.fig.suptitle('特征聚类热力图', y=1.02, fontsize=14)
plt.show()

# 分层聚类树状图
from scipy.cluster.hierarchy import dendrogram, linkage

# 计算链接矩阵
linked = linkage(iris.iloc[:, :-1], method='ward')

plt.figure(figsize=(12, 6))
dendrogram(linked, orientation='top', labels=iris['species'].values,
           distance_sort='descending', show_leaf_counts=True)
plt.title('鸢尾花数据分层聚类树状图', fontsize=14)
plt.xlabel('样本')
plt.ylabel('距离')
plt.show()

2.2 线性回归诊断图

from sklearn.linear_model import LinearRegression
import statsmodels.api as sm

# 线性回归模型
X = tips[['total_bill']]
y = tips['tip']

model = LinearRegression()
model.fit(X, y)
y_pred = model.predict(X)
residuals = y - y_pred

# 创建回归诊断图
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 1. 残差 vs 拟合值
axes[0, 0].scatter(y_pred, residuals, alpha=0.5)
axes[0, 0].axhline(y=0, color='red', linestyle='--')
axes[0, 0].set_xlabel('拟合值')
axes[0, 0].set_ylabel('残差')
axes[0, 0].set_title('残差图', fontsize=12)

# 2. Q-Q图(正态性检验)
import scipy.stats as stats
stats.probplot(residuals, dist="norm", plot=axes[0, 1])
axes[0, 1].set_title('Q-Q图', fontsize=12)

# 3. 残差直方图 + KDE
sns.histplot(residuals, kde=True, ax=axes[1, 0])
axes[1, 0].set_title('残差分布', fontsize=12)

# 4. 残差 vs 顺序
axes[1, 1].plot(residuals, 'o-', alpha=0.5)
axes[1, 1].axhline(y=0, color='red', linestyle='--')
axes[1, 1].set_xlabel('观测顺序')
axes[1, 1].set_ylabel('残差')
axes[1, 1].set_title('残差顺序图', fontsize=12)

plt.tight_layout()
plt.show()

# 输出诊断统计
print(f"残差均值: {residuals.mean():.4f}")
print(f"残差标准差: {residuals.std():.4f}")
print(f"残差偏度: {stats.skew(residuals):.4f}")
print(f"残差峰度: {stats.kurtosis(residuals):.4f}")

3. Plotly:交互式可视化

3.1 基础交互图表

import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# 3.1 交互式散点图
fig_scatter = px.scatter(tips, x='total_bill', y='tip', color='time',
                         size='size', hover_data=['day', 'sex'],
                         title='消费金额与小费关系(交互式)',
                         labels={'total_bill': '消费金额($)', 'tip': '小费($)'})
fig_scatter.show()

# 3.2 交互式折线图
# 准备时间序列数据
date_rng = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')
ts_data = pd.DataFrame(date_rng, columns=['date'])
ts_data['value'] = np.cumsum(np.random.randn(len(date_rng))) + 100

fig_line = px.line(ts_data, x='date', y='value', 
                   title='2024年趋势图',
                   labels={'date': '日期', 'value': '数值'})
fig_line.update_traces(line=dict(color='#2980b9', width=2))
fig_line.show()

# 3.3 交互式柱状图
fig_bar = px.bar(titanic.groupby('class').size().reset_index(name='count'),
                 x='class', y='count', color='class',
                 title='泰坦尼克号乘客舱位分布',
                 labels={'class': '舱位等级', 'count': '人数'})
fig_bar.show()

# 3.4 交互式饼图
fig_pie = px.pie(tips, names='day', values='total_bill',
                 title='各天消费总额占比',
                 hole=0.3)  # 环形图
fig_pie.show()

3.2 高级交互图表

# 3.5 3D散点图
fig_3d = px.scatter_3d(iris, x='sepal_length', y='sepal_width', z='petal_length',
                       color='species', size='petal_width',
                       title='鸢尾花3D可视化',
                       labels={'sepal_length': '花萼长度', 
                               'sepal_width': '花萼宽度',
                               'petal_length': '花瓣长度'})
fig_3d.show()

# 3.6 气泡图(带动画)
# 准备动画数据
np.random.seed(42)
animation_data = pd.DataFrame({
    'year': np.repeat(range(2010, 2024), 50),
    'gdp': np.random.randn(14*50) * 10 + 50,
    'life_exp': np.random.randn(14*50) * 5 + 70,
    'population': np.random.rand(14*50) * 100,
    'country': np.random.choice(['中国', '美国', '日本', '德国', '英国'], 14*50)
})

fig_bubble = px.scatter(animation_data, x='gdp', y='life_exp', 
                        size='population', color='country',
                        animation_frame='year', hover_name='country',
                        title='全球发展动态气泡图',
                        labels={'gdp': 'GDP', 'life_exp': '预期寿命'})
fig_bubble.show()

# 3.7 旭日图(层级数据)
# 准备层级数据
hierarchy_data = pd.DataFrame({
    'category': ['电子产品', '电子产品', '服装', '服装', '食品', '食品'],
    'subcategory': ['手机', '电脑', '男装', '女装', '零食', '饮料'],
    'sales': [100, 80, 60, 70, 90, 50]
})

fig_sunburst = px.sunburst(hierarchy_data, path=['category', 'subcategory'], 
                           values='sales', title='销售额层级分布')
fig_sunburst.show()

# 3.8 等高线图
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig_contour = go.Figure(data=
    go.Contour(z=Z, x=x, y=y, colorscale='Viridis',
               contours=dict(coloring='heatmap'))
)
fig_contour.update_layout(title='等高线图', width=600, height=500)
fig_contour.show()

3.3 多子图布局

# 创建多子图
fig_subplots = make_subplots(
    rows=2, cols=2,
    subplot_titles=('散点图', '柱状图', '箱线图', '折线图'),
    specs=[[{'type': 'scatter'}, {'type': 'bar'}],
           [{'type': 'box'}, {'type': 'scatter'}]]
)

# 添加散点图
fig_subplots.add_trace(
    go.Scatter(x=tips['total_bill'], y=tips['tip'], mode='markers',
               marker=dict(color='#2980b9', size=8)),
    row=1, col=1
)

# 添加柱状图
day_counts = tips['day'].value_counts()
fig_subplots.add_trace(
    go.Bar(x=day_counts.index, y=day_counts.values, marker_color='#e74c3c'),
    row=1, col=2
)

# 添加箱线图
fig_subplots.add_trace(
    go.Box(y=tips['total_bill'], name='总消费', marker_color='#27ae60'),
    row=2, col=1
)

# 添加折线图
ts_data_monthly = ts_data.resample('M', on='date').mean()
fig_subplots.add_trace(
    go.Scatter(x=ts_data_monthly.index, y=ts_data_monthly['value'],
               mode='lines+markers', line=dict(color='#9b59b6', width=2)),
    row=2, col=2
)

fig_subplots.update_layout(height=800, title_text='多图表综合分析看板', showlegend=False)
fig_subplots.show()

4. 地理数据可视化

# 4.1 地图散点图
# 准备地理数据(示例)
geo_data = pd.DataFrame({
    'city': ['北京', '上海', '广州', '深圳', '成都', '武汉', '西安', '杭州'],
    'lat': [39.9042, 31.2304, 23.1291, 22.5431, 30.5728, 30.5928, 34.3416, 30.2741],
    'lon': [116.4074, 121.4737, 113.2644, 114.0579, 104.0668, 114.3055, 108.9402, 120.1551],
    'value': [100, 95, 85, 90, 70, 75, 65, 80]
})

fig_map = px.scatter_geo(geo_data, lat='lat', lon='lon', text='city',
                         size='value', color='value',
                         title='中国主要城市数据分布',
                         projection='natural earth')
fig_map.show()

# 4.2 中国地图(需要安装 china-map 插件)
# pip install china-map
try:
    import china_map
    fig_china = px.choropleth(geo_data, locations='city', locationmode='country names',
                              color='value', scope='asia',
                              title='中国城市数据热力图')
    fig_china.show()
except:
    print("需要安装 china-map 包以显示中国地图")

5. 实战案例:电商销售数据分析看板

# 生成模拟电商数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', '2024-12-31', freq='D')
sales_data = pd.DataFrame({
    'date': np.tile(dates, 5),
    'category': np.repeat(['电子产品', '服装', '食品', '家居', '美妆'], len(dates)),
    'sales': np.random.randint(1000, 10000, len(dates)*5),
    'quantity': np.random.randint(10, 500, len(dates)*5),
    'customer_score': np.random.uniform(3, 5, len(dates)*5)
})

# 按日期和类别聚合
daily_sales = sales_data.groupby(['date', 'category'])['sales'].sum().reset_index()
category_sales = sales_data.groupby('category')['sales'].sum().reset_index()

# 创建综合看板
fig_dashboard = make_subplots(
    rows=3, cols=2,
    subplot_titles=('销售趋势(按类别)', '类别销售额占比', 
                    '销量分布箱线图', '客户评分分布',
                    '日销售额趋势', '销售额与销量关系'),
    specs=[[{'type': 'scatter'}, {'type': 'pie'}],
           [{'type': 'box'}, {'type': 'histogram'}],
           [{'type': 'scatter'}, {'type': 'scatter'}]]
)

# 1. 销售趋势(按类别)
for category in daily_sales['category'].unique():
    cat_data = daily_sales[daily_sales['category'] == category]
    fig_dashboard.add_trace(
        go.Scatter(x=cat_data['date'], y=cat_data['sales'], 
                   name=category, mode='lines'),
        row=1, col=1
    )

# 2. 类别销售额占比
fig_dashboard.add_trace(
    go.Pie(labels=category_sales['category'], values=category_sales['sales']),
    row=1, col=2
)

# 3. 销量分布箱线图
fig_dashboard.add_trace(
    go.Box(x=sales_data['category'], y=sales_data['quantity'], 
           name='销量分布', boxmean='sd'),
    row=2, col=1
)

# 4. 客户评分分布
fig_dashboard.add_trace(
    go.Histogram(x=sales_data['customer_score'], nbinsx=20,
                 marker_color='#27ae60'),
    row=2, col=2
)

# 5. 日销售额趋势(总计)
daily_total = sales_data.groupby('date')['sales'].sum().reset_index()
fig_dashboard.add_trace(
    go.Scatter(x=daily_total['date'], y=daily_total['sales'],
               mode='lines', name='总销售额', line=dict(color='#e74c3c', width=2)),
    row=3, col=1
)

# 6. 销售额与销量关系
fig_dashboard.add_trace(
    go.Scatter(x=sales_data['quantity'], y=sales_data['sales'],
               mode='markers', marker=dict(color=sales_data['customer_score'],
                                           colorscale='Viridis', showscale=True),
               text=sales_data['category'],
               hovertemplate='销量: %{x}
销售额: %{y}
类别: %{text}'), row=3, col=2 ) # 更新布局 fig_dashboard.update_layout( height=1000, title_text='电商销售数据分析看板', showlegend=True, hovermode='closest' ) # 更新轴标签 fig_dashboard.update_xaxes(title_text='日期', row=1, col=1) fig_dashboard.update_yaxes(title_text='销售额', row=1, col=1) fig_dashboard.update_xaxes(title_text='类别', row=2, col=1) fig_dashboard.update_yaxes(title_text='销量', row=2, col=1) fig_dashboard.update_xaxes(title_text='客户评分', row=2, col=2) fig_dashboard.update_yaxes(title_text='频次', row=2, col=2) fig_dashboard.update_xaxes(title_text='日期', row=3, col=1) fig_dashboard.update_yaxes(title_text='总销售额', row=3, col=1) fig_dashboard.update_xaxes(title_text='销量', row=3, col=2) fig_dashboard.update_yaxes(title_text='销售额', row=3, col=2) fig_dashboard.show() # 输出关键指标 print("=" * 50) print("电商销售数据分析报告") print("=" * 50) print(f"总销售额: {sales_data['sales'].sum():,.0f}") print(f"总销量: {sales_data['quantity'].sum():,}") print(f"平均客单价: {sales_data['sales'].sum() / sales_data['quantity'].sum():.2f}") print(f"平均客户评分: {sales_data['customer_score'].mean():.2f}") print("\n各类别销售占比:") for _, row in category_sales.sort_values('sales', ascending=False).iterrows(): pct = row['sales'] / category_sales['sales'].sum() * 100 print(f" {row['category']}: {pct:.1f}%")

6. 图表导出与部署

# 6.1 Seaborn图表保存
fig, ax = plt.subplots(figsize=(12, 6))
sns.lineplot(data=daily_sales, x='date', y='sales', hue='category')
plt.title('销售趋势图')
plt.tight_layout()

# 保存为不同格式
plt.savefig('sales_trend.png', dpi=300, bbox_inches='tight')  # PNG
plt.savefig('sales_trend.pdf', bbox_inches='tight')  # PDF
plt.savefig('sales_trend.svg', bbox_inches='tight')  # SVG矢量图
plt.close()

# 6.2 Plotly图表保存
fig_scatter.write_html('scatter_plot.html')  # 保存为HTML
fig_scatter.write_image('scatter_plot.png')  # 保存为PNG(需要安装kaleido)
fig_scatter.write_image('scatter_plot.pdf')  # 保存为PDF

# 6.3 批量导出报告
import os
from datetime import datetime

def export_dashboard(fig, name):
    """导出仪表盘为多种格式"""
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')

    # 创建导出目录
    os.makedirs('exports', exist_ok=True)

    # 导出HTML
    fig.write_html(f'exports/{name}_{timestamp}.html')

    # 导出图片(如果安装了kaleido)
    try:
        fig.write_image(f'exports/{name}_{timestamp}.png', width=1200, height=800)
        fig.write_image(f'exports/{name}_{timestamp}.pdf')
        print(f"已导出: {name}_{timestamp}")
    except:
        print("需要安装 kaleido 包以导出图片: pip install -U kaleido")

# 导出看板
# export_dashboard(fig_dashboard, 'sales_dashboard')

可视化工具对比

工具 优势 劣势 适用场景
Matplotlib 功能全面、高度可定制 代码繁琐、默认样式一般 基础图表、论文配图
Seaborn 统计图表丰富、美观、简洁 灵活性略低 统计分析、数据探索
Plotly 交互式、3D支持、Web友好 学习曲线较陡 Web应用、仪表盘
Bokeh 交互式、大规模数据 文档较少 实时数据可视化
Altair 声明式语法、语法简洁 大数据集性能差 快速原型开发

总结

本文深入讲解了数据可视化的高级技巧:

  • Seaborn统计图表:分布图、关系图、分类图、矩阵图
  • Plotly交互图表:散点图、3D图、动画图、旭日图
  • 多子图布局:综合看板创建
  • 地理可视化:地图散点图、热力图
  • 实战案例:完整的电商数据分析看板
  • 图表导出:多种格式保存与部署

📌 进阶建议:根据场景选择合适的工具 - 探索性分析用Seaborn,Web展示用Plotly,学术论文用Matplotlib。


🔗 相关文章推荐


如果这篇文章对你有帮助,欢迎点赞、收藏、转发!