数据探索:像侦探一样分析数据
🕵️ 前言:数据侦探的日常
想象一下,你是一个穿着风衣的侦探,面前摆着一堆乱七八糟的证据。有些证据看起来很重要,有些像是红鲱鱼,还有些藏在角落里不起眼却可能是破案关键。这就是数据探索的真实写照——在一堆数据中寻找真相!
今天我们要学习的数据探索(Exploratory Data Analysis,简称EDA),就像是侦探工作的第一步:仔细观察现场,寻找线索,建立假设。不过别担心,我们不需要放大镜和烟斗,只需要Python和一颗好奇心!
📚 目录
🎯 什么是数据探索
数据探索就像是第一次约会——你需要了解对方的基本情况,看看有没有共同话题,观察一下有没有什么奇怪的地方。只不过,你的约会对象是一堆数字和字符串。
数据探索的核心目标:
- 了解数据的基本情况:有多少行多少列,都是什么类型的数据
- 发现数据的分布特征:数据是怎么分布的,有没有倾斜
- 识别异常值:找出那些"不合群"的数据点
- 探索变量间的关系:看看哪些变量是好朋友,哪些是冤家
- 为后续分析做准备:为建模和预测打下基础
🔧 侦探的工具箱
首先,让我们准备好侦探工具:
# 导入必要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
# 设置中文字体和图形样式
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
sns.set_palette("husl")
# 设置pandas显示选项
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 100)
pd.set_option('display.width', 1000)
📊 数据的第一印象
就像见到新朋友要先握手问好,我们也要先和数据打个招呼:
# 创建一个示例数据集(电商销售数据)
np.random.seed(42)
n_samples = 1000
# 生成模拟数据
data = {
'customer_id': range(1, n_samples + 1),
'age': np.random.normal(35, 12, n_samples).astype(int),
'gender': np.random.choice(['男', '女'], n_samples),
'city': np.random.choice(['北京', '上海', '广州', '深圳', '杭州'], n_samples),
'purchase_amount': np.random.lognormal(6, 0.8, n_samples),
'purchase_frequency': np.random.poisson(3, n_samples),
'satisfaction_score': np.random.uniform(1, 5, n_samples),
'is_vip': np.random.choice([0, 1], n_samples, p=[0.7, 0.3])
}
# 添加一些异常值和缺失值
data['age'][np.random.choice(n_samples, 20)] = np.random.choice([150, -10, 200], 20)
data['purchase_amount'][np.random.choice(n_samples, 30)] = np.nan
data['satisfaction_score'][np.random.choice(n_samples, 50)] = np.nan
df = pd.DataFrame(data)
# 数据探索的第一步:基本信息
print("📋 数据基本信息")
print("=" * 50)
print(f"数据形状: {df.shape}")
print(f"总行数: {len(df)}")
print(f"总列数: {len(df.columns)}")
print("\n列名:")
print(df.columns.tolist())
数据类型侦查
def explore_data_types(df):
"""探索数据类型"""
print("🔍 数据类型侦查报告")
print("=" * 50)
type_info = df.dtypes.value_counts()
print("数据类型分布:")
for dtype, count in type_info.items():
print(f" {dtype}: {count} 列")
print("\n详细信息:")
for col in df.columns:
dtype = df[col].dtype
null_count = df[col].isnull().sum()
null_pct = (null_count / len(df)) * 100
unique_count = df[col].nunique()
print(f" {col:20s} | {str(dtype):10s} | 缺失: {null_count:3d} ({null_pct:5.1f}%) | 唯一值: {unique_count}")
explore_data_types(df)
数据预览
def data_preview(df):
"""数据预览"""
print("👀 数据预览")
print("=" * 50)
print("前5行:")
print(df.head())
print("\n后5行:")
print(df.tail())
print("\n随机5行:")
print(df.sample(5))
data_preview(df)
📈 数值变量的秘密
数值变量就像是会说话的数字,我们需要倾听它们的故事:
def analyze_numerical_variables(df):
"""分析数值变量"""
print("📊 数值变量分析报告")
print("=" * 50)
# 获取数值列
numerical_cols = df.select_dtypes(include=[np.number]).columns
print(f"数值列: {list(numerical_cols)}")
# 描述性统计
desc = df[numerical_cols].describe()
print("\n描述性统计:")
print(desc)
# 检查分布
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
axes = axes.ravel()
for i, col in enumerate(numerical_cols[:4]):
# 直方图
axes[i].hist(df[col].dropna(), bins=30, alpha=0.7, color='skyblue', edgecolor='black')
axes[i].set_title(f'{col} 分布')
axes[i].set_xlabel(col)
axes[i].set_ylabel('频次')
# 添加统计信息
mean_val = df[col].mean()
median_val = df[col].median()
axes[i].axvline(mean_val, color='red', linestyle='--', label=f'均值: {mean_val:.2f}')
axes[i].axvline(median_val, color='green', linestyle='--', label=f'中位数: {median_val:.2f}')
axes[i].legend()
plt.tight_layout()
plt.show()
analyze_numerical_variables(df)
分布形状诊断
def distribution_diagnosis(df, column):
"""分布形状诊断"""
print(f"🔬 {column} 分布诊断")
print("=" * 50)
data = df[column].dropna()
# 基本统计量
print(f"均值: {data.mean():.2f}")
print(f"中位数: {data.median():.2f}")
print(f"标准差: {data.std():.2f}")
print(f"偏度: {data.skew():.2f}")
print(f"峰度: {data.kurtosis():.2f}")
# 分布形状判断
if abs(data.skew()) < 0.5:
skew_desc = "近似对称"
elif data.skew() > 0:
skew_desc = "右偏(长尾在右)"
else:
skew_desc = "左偏(长尾在左)"
print(f"分布形状: {skew_desc}")
# 绘制分布图
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 直方图
axes[0].hist(data, bins=30, alpha=0.7, color='lightblue', edgecolor='black')
axes[0].set_title(f'{column} 直方图')
axes[0].set_xlabel(column)
axes[0].set_ylabel('频次')
# 箱线图
axes[1].boxplot(data, vert=True)
axes[1].set_title(f'{column} 箱线图')
axes[1].set_ylabel(column)
# Q-Q图
stats.probplot(data, dist="norm", plot=axes[2])
axes[2].set_title(f'{column} Q-Q图')
plt.tight_layout()
plt.show()
# 诊断购买金额分布
distribution_diagnosis(df, 'purchase_amount')
📋 分类变量的故事
分类变量就像是给数据贴标签,我们需要看看每个标签有多少人:
def analyze_categorical_variables(df):
"""分析分类变量"""
print("📋 分类变量分析报告")
print("=" * 50)
# 获取分类列
categorical_cols = df.select_dtypes(include=['object']).columns
print(f"分类列: {list(categorical_cols)}")
fig, axes = plt.subplots(1, len(categorical_cols), figsize=(15, 5))
if len(categorical_cols) == 1:
axes = [axes]
for i, col in enumerate(categorical_cols):
# 计算频次
value_counts = df[col].value_counts()
print(f"\n{col} 分布:")
for value, count in value_counts.items():
percentage = (count / len(df)) * 100
print(f" {value}: {count} ({percentage:.1f}%)")
# 绘制条形图
value_counts.plot(kind='bar', ax=axes[i], color='lightcoral')
axes[i].set_title(f'{col} 分布')
axes[i].set_xlabel(col)
axes[i].set_ylabel('频次')
axes[i].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()
analyze_categorical_variables(df)
交叉表分析
def cross_table_analysis(df, var1, var2):
"""交叉表分析"""
print(f"🔄 {var1} vs {var2} 交叉分析")
print("=" * 50)
# 创建交叉表
cross_tab = pd.crosstab(df[var1], df[var2], margins=True)
print("频次交叉表:")
print(cross_tab)
# 百分比交叉表
cross_tab_pct = pd.crosstab(df[var1], df[var2], normalize='index') * 100
print("\n百分比交叉表:")
print(cross_tab_pct.round(1))
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
# 热力图
sns.heatmap(cross_tab.iloc[:-1, :-1], annot=True, fmt='d', ax=axes[0], cmap='Blues')
axes[0].set_title(f'{var1} vs {var2} 热力图')
# 堆叠条形图
cross_tab.iloc[:-1, :-1].plot(kind='bar', stacked=True, ax=axes[1])
axes[1].set_title(f'{var1} vs {var2} 堆叠条形图')
axes[1].set_xlabel(var1)
axes[1].set_ylabel('频次')
axes[1].legend(title=var2)
plt.tight_layout()
plt.show()
# 分析性别和城市的关系
cross_table_analysis(df, 'gender', 'city')
🔍 寻找数据中的异常
异常值就像是数据中的"奇葩",可能是错误,也可能是重要发现:
def detect_outliers(df, column, method='iqr'):
"""检测异常值"""
print(f"🚨 {column} 异常值检测")
print("=" * 50)
data = df[column].dropna()
if method == 'iqr':
# IQR方法
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data < lower_bound) | (data > upper_bound)]
print(f"IQR方法:")
print(f" Q1: {Q1:.2f}")
print(f" Q3: {Q3:.2f}")
print(f" IQR: {IQR:.2f}")
print(f" 下界: {lower_bound:.2f}")
print(f" 上界: {upper_bound:.2f}")
elif method == 'zscore':
# Z-score方法
z_scores = np.abs(stats.zscore(data))
outliers = data[z_scores > 3]
print(f"Z-score方法:")
print(f" 阈值: 3")
print(f" 异常值数量: {len(outliers)}")
print(f" 异常值比例: {len(outliers)/len(data)*100:.1f}%")
if len(outliers) > 0:
print(f" 异常值范围: {outliers.min():.2f} - {outliers.max():.2f}")
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
# 箱线图
axes[0].boxplot(data, vert=True)
axes[0].set_title(f'{column} 箱线图')
axes[0].set_ylabel(column)
# 散点图
axes[1].scatter(range(len(data)), data, alpha=0.6)
if method == 'iqr':
axes[1].axhline(y=lower_bound, color='red', linestyle='--', label='下界')
axes[1].axhline(y=upper_bound, color='red', linestyle='--', label='上界')
axes[1].set_title(f'{column} 散点图')
axes[1].set_xlabel('索引')
axes[1].set_ylabel(column)
axes[1].legend()
plt.tight_layout()
plt.show()
return outliers
# 检测年龄异常值
age_outliers = detect_outliers(df, 'age', method='iqr')
print(f"年龄异常值: {age_outliers.values}")
多变量异常检测
def multivariate_outlier_detection(df, columns):
"""多变量异常检测"""
print("🎯 多变量异常检测")
print("=" * 50)
# 选择数值列
data = df[columns].dropna()
# 计算马氏距离
from scipy.spatial.distance import mahalanobis
# 计算协方差矩阵的逆
cov_matrix = np.cov(data.T)
inv_cov_matrix = np.linalg.inv(cov_matrix)
# 计算每个点的马氏距离
mean = data.mean()
mahal_dist = []
for i in range(len(data)):
point = data.iloc[i]
dist = mahalanobis(point, mean, inv_cov_matrix)
mahal_dist.append(dist)
# 找出异常值
threshold = np.percentile(mahal_dist, 95) # 95%分位数作为阈值
outlier_indices = np.where(np.array(mahal_dist) > threshold)[0]
print(f"异常值阈值: {threshold:.2f}")
print(f"异常值数量: {len(outlier_indices)}")
print(f"异常值比例: {len(outlier_indices)/len(data)*100:.1f}%")
# 可视化
plt.figure(figsize=(10, 6))
plt.scatter(range(len(mahal_dist)), mahal_dist, alpha=0.6)
plt.axhline(y=threshold, color='red', linestyle='--', label=f'阈值: {threshold:.2f}')
plt.title('马氏距离分布')
plt.xlabel('数据点索引')
plt.ylabel('马氏距离')
plt.legend()
plt.show()
return outlier_indices
# 检测年龄和购买金额的多变量异常
outlier_indices = multivariate_outlier_detection(df, ['age', 'purchase_amount'])
💝 变量间的关系
变量间的关系就像是人际关系,有些是好朋友,有些是敌人:
def correlation_analysis(df):
"""相关性分析"""
print("💕 变量相关性分析")
print("=" * 50)
# 计算相关性矩阵
numerical_cols = df.select_dtypes(include=[np.number]).columns
corr_matrix = df[numerical_cols].corr()
print("相关性矩阵:")
print(corr_matrix.round(3))
# 寻找强相关关系
print("\n强相关关系 (|r| > 0.5):")
strong_corr = []
for i in range(len(corr_matrix.columns)):
for j in range(i+1, len(corr_matrix.columns)):
corr_val = corr_matrix.iloc[i, j]
if abs(corr_val) > 0.5:
col1, col2 = corr_matrix.columns[i], corr_matrix.columns[j]
strong_corr.append((col1, col2, corr_val))
print(f" {col1} vs {col2}: {corr_val:.3f}")
# 可视化相关性矩阵
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
# 热力图
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0, ax=axes[0])
axes[0].set_title('相关性热力图')
# 散点图矩阵 (选择部分变量)
selected_cols = numerical_cols[:4] # 选择前4个数值列
pd.plotting.scatter_matrix(df[selected_cols], alpha=0.6, figsize=(10, 10), ax=axes[1])
plt.tight_layout()
plt.show()
return strong_corr
strong_correlations = correlation_analysis(df)
分组分析
def group_analysis(df, group_by, analyze_col):
"""分组分析"""
print(f"👥 按{group_by}分组分析{analyze_col}")
print("=" * 50)
# 分组统计
grouped = df.groupby(group_by)[analyze_col].agg([
'count', 'mean', 'median', 'std', 'min', 'max'
]).round(2)
print("分组统计:")
print(grouped)
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
# 箱线图
df.boxplot(column=analyze_col, by=group_by, ax=axes[0])
axes[0].set_title(f'{analyze_col} 按 {group_by} 分组箱线图')
axes[0].set_xlabel(group_by)
axes[0].set_ylabel(analyze_col)
# 条形图
grouped['mean'].plot(kind='bar', ax=axes[1], color='lightgreen')
axes[1].set_title(f'{analyze_col} 按 {group_by} 分组均值')
axes[1].set_xlabel(group_by)
axes[1].set_ylabel(f'{analyze_col} 均值')
axes[1].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()
return grouped
# 按性别分析购买金额
gender_analysis = group_analysis(df, 'gender', 'purchase_amount')
🚀 高级侦探技巧
主成分分析(PCA)
def pca_analysis(df, n_components=2):
"""主成分分析"""
print("🔍 主成分分析")
print("=" * 50)
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 选择数值列
numerical_cols = df.select_dtypes(include=[np.number]).columns
data = df[numerical_cols].dropna()
# 标准化
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
# PCA
pca = PCA(n_components=n_components)
pca_result = pca.fit_transform(data_scaled)
# 解释方差比例
explained_variance_ratio = pca.explained_variance_ratio_
print(f"前{n_components}个主成分解释的方差比例:")
for i, ratio in enumerate(explained_variance_ratio):
print(f" PC{i+1}: {ratio:.3f} ({ratio*100:.1f}%)")
print(f"累计解释方差比例: {sum(explained_variance_ratio):.3f} ({sum(explained_variance_ratio)*100:.1f}%)")
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
# 散点图
axes[0].scatter(pca_result[:, 0], pca_result[:, 1], alpha=0.6)
axes[0].set_xlabel(f'PC1 ({explained_variance_ratio[0]*100:.1f}%)')
axes[0].set_ylabel(f'PC2 ({explained_variance_ratio[1]*100:.1f}%)')
axes[0].set_title('PCA 散点图')
# 载荷图
loadings = pca.components_.T
for i, (col, loading) in enumerate(zip(numerical_cols, loadings)):
axes[1].arrow(0, 0, loading[0], loading[1],
head_width=0.05, head_length=0.05, fc='red', ec='red')
axes[1].text(loading[0]*1.1, loading[1]*1.1, col, fontsize=10)
axes[1].set_xlabel('PC1')
axes[1].set_ylabel('PC2')
axes[1].set_title('PCA 载荷图')
axes[1].grid(True)
plt.tight_layout()
plt.show()
return pca_result, pca
pca_result, pca_model = pca_analysis(df)
聚类分析
def clustering_analysis(df, n_clusters=3):
"""聚类分析"""
print("🎯 聚类分析")
print("=" * 50)
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# 选择数值列
numerical_cols = df.select_dtypes(include=[np.number]).columns
data = df[numerical_cols].dropna()
# 标准化
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
# K-means聚类
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
clusters = kmeans.fit_predict(data_scaled)
# 添加聚类结果到数据框
data_with_clusters = data.copy()
data_with_clusters['cluster'] = clusters
# 分析每个聚类
print("聚类分析结果:")
for i in range(n_clusters):
cluster_data = data_with_clusters[data_with_clusters['cluster'] == i]
print(f"\n聚类 {i} ({len(cluster_data)} 个样本):")
print(cluster_data.describe().round(2))
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
# 使用前两个主成分进行可视化
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
pca_result = pca.fit_transform(data_scaled)
# 聚类散点图
colors = ['red', 'blue', 'green', 'orange', 'purple']
for i in range(n_clusters):
mask = clusters == i
axes[0].scatter(pca_result[mask, 0], pca_result[mask, 1],
c=colors[i], label=f'聚类 {i}', alpha=0.6)
axes[0].set_xlabel('PC1')
axes[0].set_ylabel('PC2')
axes[0].set_title('聚类结果 (PCA空间)')
axes[0].legend()
# 聚类中心
centers_pca = pca.transform(kmeans.cluster_centers_)
axes[0].scatter(centers_pca[:, 0], centers_pca[:, 1],
c='black', marker='x', s=200, linewidths=3, label='中心')
# 每个特征的聚类对比
cluster_means = data_with_clusters.groupby('cluster').mean()
cluster_means.plot(kind='bar', ax=axes[1])
axes[1].set_title('各聚类特征均值对比')
axes[1].set_xlabel('聚类')
axes[1].set_ylabel('特征值')
axes[1].legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
return clusters, kmeans
clusters, kmeans_model = clustering_analysis(df)
🎯 实战案例:房价侦探
让我们用一个完整的案例来演示数据探索的全过程:
# 创建房价数据集
def create_housing_dataset():
"""创建房价数据集"""
np.random.seed(42)
n_samples = 1000
# 生成特征
area = np.random.normal(100, 30, n_samples) # 面积
rooms = np.random.poisson(3, n_samples) + 1 # 房间数
age = np.random.exponential(10, n_samples) # 房龄
distance = np.random.gamma(2, 2, n_samples) # 距离市中心距离
# 生成价格(有一定的逻辑关系)
price = (area * 0.5 + rooms * 10 - age * 0.3 - distance * 2 +
np.random.normal(0, 10, n_samples) + 50)
# 确保价格为正
price = np.maximum(price, 20)
# 添加一些异常值
outlier_indices = np.random.choice(n_samples, 20, replace=False)
price[outlier_indices] = price[outlier_indices] * 3
housing_data = pd.DataFrame({
'area': area,
'rooms': rooms,
'age': age,
'distance_to_center': distance,
'price': price,
'district': np.random.choice(['朝阳', '海淀', '西城', '东城'], n_samples),
'has_elevator': np.random.choice([0, 1], n_samples)
})
return housing_data
def comprehensive_eda(df):
"""综合数据探索分析"""
print("🏠 房价数据综合探索分析")
print("=" * 70)
# 1. 基本信息
print("1. 基本信息")
print("-" * 30)
print(f"数据形状: {df.shape}")
print(f"数据类型:\n{df.dtypes}")
print(f"缺失值:\n{df.isnull().sum()}")
# 2. 描述性统计
print("\n2. 描述性统计")
print("-" * 30)
print(df.describe())
# 3. 相关性分析
print("\n3. 相关性分析")
print("-" * 30)
numerical_cols = df.select_dtypes(include=[np.number]).columns
corr_matrix = df[numerical_cols].corr()
# 找出与价格最相关的特征
price_corr = corr_matrix['price'].sort_values(ascending=False)
print("与价格的相关性:")
for col, corr in price_corr.items():
if col != 'price':
print(f" {col}: {corr:.3f}")
# 4. 可视化
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
# 价格分布
axes[0, 0].hist(df['price'], bins=30, alpha=0.7, color='skyblue')
axes[0, 0].set_title('价格分布')
axes[0, 0].set_xlabel('价格')
axes[0, 0].set_ylabel('频次')
# 面积vs价格
axes[0, 1].scatter(df['area'], df['price'], alpha=0.6)
axes[0, 1].set_title('面积 vs 价格')
axes[0, 1].set_xlabel('面积')
axes[0, 1].set_ylabel('价格')
# 房间数vs价格
room_price = df.groupby('rooms')['price'].mean()
axes[0, 2].bar(room_price.index, room_price.values, color='lightgreen')
axes[0, 2].set_title('房间数 vs 平均价格')
axes[0, 2].set_xlabel('房间数')
axes[0, 2].set_ylabel('平均价格')
# 房龄vs价格
axes[1, 0].scatter(df['age'], df['price'], alpha=0.6, color='coral')
axes[1, 0].set_title('房龄 vs 价格')
axes[1, 0].set_xlabel('房龄')
axes[1, 0].set_ylabel('价格')
# 距离vs价格
axes[1, 1].scatter(df['distance_to_center'], df['price'], alpha=0.6, color='gold')
axes[1, 1].set_title('距离市中心 vs 价格')
axes[1, 1].set_xlabel('距离市中心')
axes[1, 1].set_ylabel('价格')
# 相关性热力图
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0, ax=axes[1, 2])
axes[1, 2].set_title('相关性热力图')
plt.tight_layout()
plt.show()
# 5. 区域分析
print("\n5. 区域分析")
print("-" * 30)
district_stats = df.groupby('district')['price'].agg(['count', 'mean', 'median', 'std'])
print(district_stats.round(2))
# 区域价格分布
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
df.boxplot(column='price', by='district', ax=axes[0])
axes[0].set_title('各区域价格分布')
axes[0].set_xlabel('区域')
axes[0].set_ylabel('价格')
district_stats['mean'].plot(kind='bar', ax=axes[1], color='lightcoral')
axes[1].set_title('各区域平均价格')
axes[1].set_xlabel('区域')
axes[1].set_ylabel('平均价格')
axes[1].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()
# 6. 异常值检测
print("\n6. 异常值检测")
print("-" * 30)
# 使用IQR方法检测价格异常值
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]
print(f"价格异常值数量: {len(outliers)}")
print(f"异常值比例: {len(outliers)/len(df)*100:.1f}%")
if len(outliers) > 0:
print(f"异常值价格范围: {outliers['price'].min():.2f} - {outliers['price'].max():.2f}")
return {
'correlation_matrix': corr_matrix,
'district_stats': district_stats,
'outliers': outliers
}
# 创建并分析房价数据
housing_df = create_housing_dataset()
analysis_results = comprehensive_eda(housing_df)
🔧 常见问题与解决方案
Q1: 数据量太大,探索很慢怎么办?
A1: 数据抽样策略
def sample_data_for_eda(df, sample_size=10000):
"""为EDA抽样数据"""
if len(df) > sample_size:
# 分层抽样
if 'category_column' in df.columns:
sample_df = df.groupby('category_column').apply(
lambda x: x.sample(min(len(x), sample_size // df['category_column'].nunique()))
).reset_index(drop=True)
else:
sample_df = df.sample(n=sample_size, random_state=42)
print(f"数据从 {len(df)} 行抽样到 {len(sample_df)} 行")
return sample_df
else:
return df
Q2: 缺失值太多,怎么处理?
A2: 缺失值分析
def analyze_missing_values(df):
"""分析缺失值"""
missing_stats = df.isnull().sum()
missing_pct = (missing_stats / len(df)) * 100
missing_df = pd.DataFrame({
'missing_count': missing_stats,
'missing_percentage': missing_pct
})
# 按缺失比例排序
missing_df = missing_df.sort_values('missing_percentage', ascending=False)
print("缺失值分析:")
print(missing_df[missing_df['missing_count'] > 0])
# 可视化缺失值模式
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=True, yticklabels=False, cmap='viridis')
plt.title('缺失值模式')
plt.show()
return missing_df
Q3: 如何自动化EDA流程?
A3: 自动化EDA函数
def automated_eda(df, target_col=None):
"""自动化EDA"""
report = {
'basic_info': {},
'numerical_analysis': {},
'categorical_analysis': {},
'correlation_analysis': {},
'outlier_analysis': {}
}
# 基本信息
report['basic_info'] = {
'shape': df.shape,
'dtypes': df.dtypes.to_dict(),
'missing_values': df.isnull().sum().to_dict()
}
# 数值变量分析
numerical_cols = df.select_dtypes(include=[np.number]).columns
for col in numerical_cols:
report['numerical_analysis'][col] = {
'mean': df[col].mean(),
'median': df[col].median(),
'std': df[col].std(),
'skewness': df[col].skew(),
'kurtosis': df[col].kurtosis()
}
# 分类变量分析
categorical_cols = df.select_dtypes(include=['object']).columns
for col in categorical_cols:
report['categorical_analysis'][col] = df[col].value_counts().to_dict()
# 相关性分析
if len(numerical_cols) > 1:
corr_matrix = df[numerical_cols].corr()
report['correlation_analysis'] = corr_matrix.to_dict()
# 异常值检测
for col in numerical_cols:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df[col] < lower_bound) | (df[col] > upper_bound)]
report['outlier_analysis'][col] = {
'outlier_count': len(outliers),
'outlier_percentage': len(outliers) / len(df) * 100
}
return report
# 使用自动化EDA
eda_report = automated_eda(housing_df, target_col='price')
print("自动化EDA报告生成完成!")
📖 扩展阅读
推荐工具和库
- pandas-profiling: 自动生成EDA报告
- sweetviz: 美观的EDA可视化
- autoviz: 自动化数据可视化
- plotly: 交互式图表
- bokeh: 大数据可视化
学习资源
- 《Python数据分析实战》
- 《利用Python进行数据分析》
- Kaggle Learn 的数据可视化课程
- 各种数据科学竞赛的EDA kernel
🎬 下集预告
下一期我们将学习《统计分析:从数据中挖掘洞察》,将深入探讨:
- 假设检验的艺术
- 置信区间的秘密
- 回归分析的威力
- 贝叶斯统计的魅力
📝 总结与思考题
本期总结
- 数据探索是数据科学的基础:就像侦探工作一样,需要仔细观察和分析
- 掌握多种探索方法:从基本统计到高级分析技术
- 可视化是强大工具:图表比数字更直观
- 异常值需要特别关注:可能是错误,也可能是重要发现
- 变量关系分析很重要:为后续建模奠定基础
思考题
- 如果你是一个电商数据分析师,你会如何探索用户购买行为数据?
- 在什么情况下异常值应该被保留?什么情况下应该被删除?
- 如何设计一个通用的EDA流程,适用于不同类型的数据集?
- 相关性高的变量一定有因果关系吗?如何区分相关性和因果性?
实践作业
- 找一个真实的数据集,进行完整的EDA分析
- 比较不同异常值检测方法的效果
- 尝试使用pandas-profiling等自动化工具
- 为你的EDA结果写一份数据洞察报告
记住,数据探索就像和数据谈恋爱——需要耐心、细心,还要有一点点好奇心。只有真正了解你的数据,才能从中发现有价值的洞察!🕵️♂️✨