8 min read

Python 数据分析

Python 数据分析
Photo by ZHENYU LUO / Unsplash

序幕:建立数据

1. 表格数据的导入

01-从 Excel 表格导入

df = pd.read_excel('文件路径.xlsx')

df.head(10)

02-从 csv 文件导入

2. 自行建立表格

# 1. 建立数据
data = {
	'Language Code': [
		'af', 'sq', 'am', 'ar'
	],
	'Language Full Name': [
		'Afrikaans', 'Albanian', 'Amharic', 'Arabic'
	],
	'Language Chinese Name': [
		'南非荷兰语', '阿尔巴尼亚语', '阿姆哈拉语', '阿拉伯语'
	]
}

# 2. 创建DataFrame
df = pd.DataFrame(data)

# 3. 显示结果
print(df)

添加一行

添加一列

第一部分:数据预处理

1. 空值问题

df['列名'].isnull().sum())  # 查看 NaN 个数
df['列名'] = df['列名'].fillna(0)  # 将 NaN 替换为 0

2. 属性的数据类型转换

df.dtypes  # 显示所有列的数据类型
df['列名'].dtypes  # 查看某一列的数据类型
df['列名'] = df['列名'].astype(某种数据类型)  # 将某一列转化为某种数据类型
  • 显示某种数据类型的所有属性
int_column_names = df.select_dtypes(include='int64').columns  # 显示 int64 类型的所有属性
print(", ".join(int_column_names))
  • 连续型数据类型分段
# 以 df['年龄'] 举例
bins = [2, 4, 6, 8, 10, 12, 14, 16, 18, float('inf')]  # 添加一个无限大区间
labels = ['2-4', '4-6', '6-8', '8-10', '10-12', '12-14', '14-16', '16-18', '>18']

# 对年龄数据进行分箱
df['年龄区间'] = pd.cut(df['年龄'], bins=bins, labels=labels, right=False)

# 统计每个区间的数量
counts = df['年龄区间'].value_counts(sort=False)

# 绘制直方图
counts.plot(kind='bar', edgecolor='black')

3. 数据分组

当数据存在多个维度的时候——有许多因素可以对对象进行描述,这个时候就需要根据需求对某一些因素进行归类分组,就需要用到 pd.groupby()

例子1:号码资源分析
  • 数据解释:
    • 假设某号码资源有三种状态:空闲、在途、冻结
    • 现在要统计在某个号码省、号码地市空闲号码的占比
big_area 号码省 号码地市 状态 count(1) 
0 s 北京 北京 空闲 1087 
1 n 山东 淄博 冻结 164 
2 n 山东 滨州 在途 164 
3 s 山东 临沂 空闲 27 
4 s 山东 潍坊 空闲 187
...
  • 步骤
  1. 使用 groupby号码省、号码地市、状态进行分组;
status_counts = df.groupby(['号码省', '号码地市', '状态'])['count(1)'].sum().unstack(fill_value=0)

"""
- 知识点:
df.groupby(by)[列].聚合函数()
	- by:分组的依据,可以是某一列、多个列、函数等;
	- [列]:要对哪些列进行操作;
	- 聚合函数:如 .sum()、.mean()、.count()、.max()、.min() 等。

- 代码解读
	- groupby(['号码省', '号码地市', '状态'])
		- 按省、市、状态三级分组。
	- ['count(1)'].sum()
		- 对每个组的 count(1) 求和。 
	- .unstack(fill_value=0)
		- 将“状态”这一列从行索引展开成列索引(类似 Excel 透视表),填充空值为 0。
    
- 结果:
号码省,号码地市,冻结,占用,在途,空闲,预占
上海,上海,1173,15843,146,4542,29
云南,临沧,4,48,3,109,0
云南,丽江,7,138,62,7,0
云南,保山,10,86,2,27,0
云南,大理,13,105,11,24,0
"""
  1. 使用sum计算状态总和
cal_col = ['冻结', '占用', '在途', '空闲', '预占']
status_counts['总记录数'] = status_counts[cal_col].sum(axis=1)

"""
- 知识点:
	添加 cal_col 是比较规范的做法,虽然 pandas 默认只对数值列(numeric columns)求和,非数值列(比如字符串)会被自动忽略


- 结果:
号码省,号码地市,冻结,占用,在途,空闲,预占,总记录数
上海,上海,1173,15843,146,4542,29,21733
云南,临沧,4,48,3,109,0,164
云南,丽江,7,138,62,7,0,214
云南,保山,10,86,2,27,0,125
"""
  1. 计算空闲占比
  2. 重置索引并整理列顺序
后续就与分组无关啦,最终结果如下:
号码省,号码地市,冻结,占用,在途,空闲,预占,总记录数,空闲百分比
上海,上海,1173,15843,146,4542,29,21733,20.9%
云南,临沧,4,48,3,109,0,164,66.46%
云南,丽江,7,138,62,7,0,214,3.27%
云南,保山,10,86,2,27,0,125,21.6%

第二部分:数据分析

0. 变量重命名


  • 查看某属性有哪些分类
df['列名'].unique()  # 查看该列的分类
  • 更改分类名
df['性别'] = df['性别'].replace({0: '女', 1: '男'})  # 用性别举例
  • 如果是分类变量,推荐使用
df['性别'] = df['性别'].cat.rename_categories({0: '女', 1: '男'})

1. 分类变量和分类变量


"""
拿性别和是否续报举例,其中性别
"""
# 计算分类和数据表
crosstab = pd.crosstab(df['性别'], df['是否续报'])
proportions = crosstab.div(crosstab.sum(axis=1), axis=0)  # 按性别分组计算比例
table = tabulate(proportions, headers='keys', tablefmt='grid', showindex=True)
print(table)

# 绘制堆积柱状图
ax = proportions.plot(kind='bar', stacked=True, color=['lightblue', 'orange', 'pink'], edgecolor='black')

# 添加比例标注
for i, (gender, row) in enumerate(proportions.iterrows()):  # 遍历每个性别的比例数据
    bottom = 0  # 初始化底部位置
    for category, value in row.items():  # 遍历每个分类(是否报名)的比例
        if value > 0:  # 只标注非零比例
            plt.text(
                i, 
                bottom + value / 2,  # 底部位置 + 当前堆积柱的中点
                f'{value:.1%}',  # 转换为百分比形式
                ha='center', va='center', fontsize=12, color='black'
            )
            bottom += value  # 更新底部位置

# 设置标题和标签
plt.title('“性别”与“是否续报”的关系')
plt.xlabel('性别')
plt.xticks(rotation=0)
plt.ylabel('比例')
plt.legend(title='是否续报', loc='upper right')
plt.tight_layout()  # 自动调整布局
plt.show()

# 卡方检验
chi2, p, dof, expected = chi2_contingency(crosstab)
print(f"卡方统计量: {chi2}")
print(f"p值: {p}")
  • 两个分类变量
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import chi2_contingency

# 定义年龄段分组
bins = [0, 4, 8, 12, 16, float('inf')]
labels = ['<4', '4-8', '8-12', '12-16', '>16']
df['年龄段'] = pd.cut(df['年龄'], bins=bins, labels=labels)

# 创建多维交叉表
crosstab = pd.crosstab([df['年龄段'], df['性别']], df['是否续报'])
proportions = crosstab.div(crosstab.sum(axis=1), axis=0)  # 按每个年龄段-性别组计算比例
table = tabulate(proportions, headers='keys', tablefmt='grid', showindex=True)
print(table)

# 绘制堆积柱状图
ax = proportions.unstack(level=1).plot(
    kind='bar', 
    stacked=True, 
    color=['lightblue', 'orange', 'pink'], 
    edgecolor='black', 
    figsize=(12, 6)
)

# 设置标题和标签
plt.title('“年龄段”与“性别”对“是否续报”的影响')
plt.xlabel('年龄段-性别组合')
plt.ylabel('比例')
plt.xticks(rotation=45)
plt.legend(title='是否续报', loc='upper right')
plt.tight_layout()
plt.show()

# 卡方检验
chi2, p, dof, expected = chi2_contingency(crosstab)
print("卡方检验结果:")
print(f"卡方统计量: {chi2:.2f}")
print(f"p值: {p:.4f}")
print(f"自由度: {dof}")

2. 数值变量和分类变量


# 更新分组
bins = [0, 4, 8, 12, 16, float('inf')]  # 重新定义年龄段
labels = ['<4', '4-8', '8-12', '12-16', '>16']  # 更新标签

# 年龄分段
df['年龄段'] = pd.cut(df['年龄'], bins=bins, labels=labels)

# 计算分类和数据表
crosstab = pd.crosstab(df['年龄段'], df['是否续报'])
proportions = crosstab.div(crosstab.sum(axis=1), axis=0)  # 按年龄段分组计算比例
table = tabulate(proportions, headers='keys', tablefmt='grid', showindex=True)
print(table)

# 绘制堆积柱状图
ax = proportions.plot(kind='bar', stacked=True, color=['lightblue', 'orange', 'pink'], edgecolor='black')

for i, (age_group, row) in enumerate(proportions.iterrows()):  # 遍历每个年龄段的比例数据
    bottom = 0  # 初始化底部位置
    for category, value in row.items():  # 遍历每个分类(是否续报)的比例
        if value > 0:  # 只标注非零比例
            plt.text(
                i, 
                bottom + value / 2,  # 底部位置 + 当前堆积柱的中点
                f'{value:.1%}',  # 转换为百分比形式
                ha='center', va='center', fontsize=12, color='black'
            )
            bottom += value  # 更新底部位置

# 设置标题和标签
plt.title('“年龄段”与“是否续报”的关系')
plt.xlabel('年龄段')
plt.xticks(rotation=0)
plt.ylabel('比例')

# 调整图例位置
plt.legend(title='是否续报', loc='upper center', bbox_to_anchor=(0.5, -0.1), ncol=3, frameon=False)

# 自动调整布局
plt.tight_layout()
plt.show()

# 卡方检验
chi2, p, dof, expected = chi2_contingency(crosstab)
print(f"卡方统计量: {chi2}")
print(f"p值: {p}")

3. 时序变量和分类变量


df['是否续报'] = df['是否续报'].map({'是': 1, '否': 0}).astype(float)
# 将分类变量转化为浮点数来计算比率

df['赛考确认日期'] = df['赛考确认时间'].dt.date 
# 时间太散
# 对 2024-06-27 15:04:26 这种值,dt.date 会提取出 2024-06-27

trend = df.groupby('赛考确认日期')['是否续报'].mean()
trend.plot(kind='line', marker='o', color='blue')
plt.title('赛考确认时间与续报率的关系')
plt.xlabel('赛考确认时间')
plt.xticks(rotation=60)
plt.ylabel('续报率')
plt.grid(True)
plt.show()
df['是否续报'] = df['是否续报'].map({'是': 1, '否': 0}).astype(float)
df['续报结束日期'] = pd.to_datetime(df['续报结束日期'], errors='coerce')

# 筛选特定日期范围内的数据
start_date = pd.to_datetime('2024-09-15')
end_date = pd.to_datetime('2024-12-08')
df_filtered = df[(df['续报结束日期'] >= start_date) & (df['续报结束日期'] <= end_date)]

trend = df_filtered.groupby('续报结束日期')['是否续报'].mean()
trend.plot(kind='line', marker='o', color='blue')
plt.title('续报结束日期与续报率的关系')
plt.xlabel('续报结束日期')
plt.xticks(rotation=60)
plt.ylabel('续报率')
plt.grid(True)
plt.show()

番外:

1. 解决Mac中 matplotlib 绘图中文乱码问题

  1. 下载 SimHei 中文字体
  2. 保存到 Mac 应用字体册中(字体册 - 文件 - 将字体添加到“当前用户”)
    1. 找到缓存的位置
    1. 删除缓存
  3. 重新载入项目,输入代码

删除 matplotlib 的缓存

import matplotlib as mpl
print(mpl.get_cachedir())
cd 缓存地址
ls
rm *
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
i