一、数据集介绍
这份昆明职位数据集源自 Boss 直聘,数据量颇为丰富,包含 17731 行、17 列数据。数据集里各个字段都承载着重要信息,具体介绍如下表所示:
字段名 | 含义 |
---|---|
province | 岗位所在省份 |
city | 岗位所在城市 |
category_1 | 岗位的一级分类 |
category_2 | 岗位的二级分类 |
position | 具体职位 |
job_name | 职位名称 |
job_area | 工作区域 |
salary | 薪资待遇 |
experience | 工作经验要求 |
education | 教育程度要求 |
company_name | 招聘公司名称 |
company_industry | 招聘公司所属行业 |
financing_status | 招聘公司的融资状态 |
company_size | 招聘公司的规模 |
skill | 岗位所需技能 |
benefits | 公司提供的福利待遇 |
job_url | 职位详情链接 |
数据集部分数据如下图所示:
二、缺失值处理
在开展缺失值处理工作之前,首要步骤是对数据集中各字段的缺失情况进行全面检测,以明确哪些字段存在缺失值及缺失的具体数量。缺失值检测结果如下图所示,通过该结果可直观掌握各字段的缺失状况,为后续有针对性地制定处理策略提供依据。
缺失值检测结果显示,数据集中多个字段存在缺失值,这可能影响后续分析的准确性。针对不同字段的特点和业务逻辑,采用了多种策略进行缺失值处理。
education(教育程度)字段的缺失值仅 89 条,占数据集总量的 0.5%,删除这些记录对整体数据分布影响甚微,因此采用直接删除策略以保证数据质量。
financing_status(融资状态)字段存在 5762 条缺失值(占比 32.5%),考虑到融资状态与公司行业强相关(如互联网行业多处于 B 轮后融资阶段),可按company_industry分组,计算每组众数作为填充值。例如,科技行业的众数为 “C 轮”,则该行业的缺失值统一填充为 “C 轮”。对于无有效众数的行业(如新兴行业数据稀疏),标记为 “未知”,确保填充逻辑符合行业特性。
company_size(公司规模)字段 存在102 条缺失值(占比 0.6%),占比较少,可采用全局众数填充。经统计,数据集内 “100-499 人” 规模的公司占比最高,因此将缺失值统一填充为该众数。若后续分析发现规模与行业相关性增强,可调整为分组填充策略。
skill(技能要求)字段存在 1267 条缺失值(占比 7.1%)使用业务语义填充。招聘实践中,技能要求缺失通常意味着岗位对技术栈无强制要求,因此统一标注为 “无明确技能要求”。此填充方式保留了职位特性,避免因技术术语差异导致的分析偏差。
benefits(福利待遇)字段存在4684条缺失值(占比26.4%),采用“常规福利”进行填充。
通过上述策略,所有字段的缺失值均被有效处理,确保了数据集的完整性,为后续分析奠定了基础。
缺失值处理代码如下所示:
# 缺失值检测及处理
def handle_missing_values(data):
print("缺失值处理前各列缺失值数量:")
print(data.isnull().sum())
# 处理education字段的缺失值
data.dropna(subset=['education'], inplace=True)
# financing_status(融资状态)字段
# 公司的融资状态可能和公司所处行业有一定联系,不同行业的公司融资情况可能不同。
# 因此,我将依据 company_industry(公司行业)分组,用每组内 financing_status 的众数进行填充,以更贴合实际情况。
# 遍历数据中公司行业列的唯一值
for industry in data['company_industry'].unique():
# 从数据中筛选出当前行业的数据
industry_df = data[data['company_industry'] == industry]
# 计算当前行业融资状态列的众数
# 如果众数存在,则取第一个众数;如果众数为空,则使用 '未知' 作为填充值
mode_value = industry_df['financing_status'].mode().iloc[0] if not industry_df[
'financing_status'].mode().empty else '未知'
# 使用计算得到的众数填充当前行业融资状态列的缺失值
data.loc[data['company_industry'] == industry, 'financing_status'] = data.loc[
data['company_industry'] == industry, 'financing_status'].fillna(mode_value)
# 计算 company_size 列的众数,如果众数存在则取第一个众数作为填充值,若不存在则使用 '未知' 作为填充值
mode_company_size = data['company_size'].mode().iloc[0] if not data[
'company_size'].mode().empty else '未知'
# 使用计算得到的众数填充 company_size 列中的缺失值
data['company_size'] = data['company_size'].fillna(mode_company_size)
# 使用 '无明确技能要求' 填充 skill 列中的缺失值,inplace=True 表示直接在原数据上修改
data['skill'].fillna('无明确技能要求', inplace=True)
# 使用 '常规福利' 填充 benefits 列中的缺失值,inplace=True 表示直接在原数据上修改
data['benefits'].fillna('常规福利', inplace=True)
print("缺失值处理后各列缺失值数量:")
print(data.isnull().sum())
return data
缺失值处理完成后,再次核查缺失值数据,结果如下图所示。可以看到,各字段的缺失值数量均已降为 0,数据集中已不存在缺失值,处理达到预期效果。
三、重复值处理
在数据清洗阶段,需要识别并处理完全重复的记录,即两行数据的所有字段值均相同的情况。这类重复数据可能源于数据采集过程中的冗余或系统误差,会导致后续分析结果出现偏差。因此,采用以下策略进行处理:
处理逻辑:
- 检测数据集中完全重复的行数
- 若存在重复行,则删除重复记录,仅保留其中一行
- 验证处理结果,确保数据集中无重复记录
重复值处理代码如下所示:
# 重复数据检测及处理
def handle_duplicate_data(data):
duplicate_rows = len(data[data.duplicated()])
print("重复行数量:", duplicate_rows)
if duplicate_rows > 0:
data.drop_duplicates(inplace=True)
duplicate_rows = len(data[data.duplicated()])
print("重复行数量:", duplicate_rows)
return data
执行上述重复值处理代码后,结果如下图所示。可以看到,处理前通过data.duplicated()检测,重复行数量为 0 ;处理后再次检测,重复行数量依旧为 0 ,表明数据集中原本就不存在完全重复的记录,无需进行重复值删除操作。
四、薪资数据格式处理
薪资数据在原始数据集中存在多种格式,如"元/月"、“元/天”、“元/时”、“元/周"等不同时间单位的表示,以及带”·“或”*“的特殊格式,甚至包含"面议"等非数值形式。这种格式不统一会严重影响后续薪资分析的准确性和可比性。因此,需要对薪资数据进行标准化处理,将其统一转换为"K”(千)为单位的格式。
处理逻辑:
- 检测非标准格式:通过正则表达式识别不符合"数字-数字K"或"面议"格式的薪资数据
- 单位转换:将不同时间单位的薪资数据统一转换为月薪(K)表示
- 元/月:直接除以1000转换为K
- 元/天:乘以30天再除以1000转换为K
- 元/时:乘以8小时/天 × 22天/月再除以1000转换为K
- 元/周:乘以4周/月再除以1000转换为K
- 特殊格式处理:
- 带"*"的格式:提取数字部分并转换为K
- 带"·“的格式:提取”·"前的部分
- 处理"面议"情况:用非面议薪资的众数替换"面议"值,以保留数据统计特性
薪资数据格式处理代码如下所示:
# 统一薪资数据格式
def convert_salary(s):
if '元/月' in s:
parts = s[:-3].split('-')
low = int(int(parts[0]) / 1000)
high = int(int(parts[1]) / 1000)
return f"{low}-{high}K"
elif '元/天' in s:
parts = s[:-3].split('-')
low = int((int(parts[0]) * 30) / 1000)
high = int((int(parts[1]) * 30) / 1000)
return f"{low}-{high}K"
elif '元/时' in s:
parts = s[:-3].split('-')
low = int((int(parts[0]) * 8 * 22) / 1000)
high = int((int(parts[1]) * 8 * 22) / 1000)
return f"{low}-{high}K"
elif '元/周' in s:
parts = s[:-3].split('-')
low = int((int(parts[0]) * 4) / 1000)
high = int((int(parts[1]) * 4) / 1000)
return f"{low}-{high}K"
elif '*' in s:
parts = s.split('*')[0]
low, high = parts[:-1].split('-')
low = int(int(low[:-1]))
high = int(int(high[:-1]))
return f"{low}-{high}K"
elif '·' in s:
parts = s.split('·')[0]
return parts
return s
# 薪资格式标准化处理
def uniform_salary_format(data):
# 检测格式不统一的数据的单位
# 定义一个正则表达式模式,用于匹配格式为 "数字-数字K" 的字符串
# 其中 ^ 表示字符串的开始,\d+ 表示匹配一个或多个数字,- 表示匹配连字符,$ 表示字符串的结束
pattern = r'^\d+-\d+K$|^面议$'
# 使用 str.match 方法检查 'salary' 列中的每个值是否匹配定义的模式
# ~ 是取反操作符,用于选择不匹配模式的行
# 最终将不匹配模式的行筛选出来,存储在 filtered_df 中
filtered_df = data[~data['salary'].str.match(pattern)]
print(filtered_df['salary'].to_csv(na_rep='nan', index=False))
# 处理薪资格式不统一的数据
data['salary'] = data['salary'].apply(convert_salary)
# 计算薪资列的众数
salary_mode = data[data['salary'] != '面议']['salary'].mode()
if not salary_mode.empty:
mode_value = salary_mode[0]
# 将面议替换为众数
data.loc[data['salary'] == '面议', 'salary'] = mode_value
return data
处理前后的部分薪资数据对比如下。可以看到,处理前薪资格式多样,包含 “元 / 天”“・13 薪” 等不同表述;经标准化转换后,统一为 “数字 - 数字 K” 的简洁格式,让薪资数据更规整,便于后续分析。
五、技能格式处理
技能数据在原始数据集中存在格式不统一的问题,主要表现为技能标签之间存在连续的逗号(如","),以及字符串首尾可能存在多余的逗号。这种不规范的格式会影响后续对技能数据的分词、统计和分析。因此,需要对技能数据进行格式标准化处理。
处理逻辑:
- 连续逗号处理:使用正则表达式将连续的多个逗号替换为单个逗号
- 首尾逗号处理:去除字符串首尾的逗号,确保技能标签的独立性
技能格式处理代码如下所示:
# 技能统一格式处理
def uniform_skill_format(data):
# 使用正则表达式将 'skill' 列中连续的逗号替换为单个逗号
# 然后去除字符串首尾的逗号
data['skill'] = data['skill'].str.replace(',+', ',', regex=True).str.strip(',')
return data
六、拆分薪资列并处理异常值
IQR准则,即四分位距(Interquartile Range, IQR)准则,是一种用于识别数据集中异常值的统计方法。它基于数据集的四分位数来确定哪些观测值可以被视为异常值。这种方法特别适用于偏态分布或小样本的数据集,因为它不依赖于正态分布假设。
四分位距(IQR)简介
- 四分位数:将一组数据按数值大小排序后分成四个等份,处于三个分割点位置的数值称为四分位数。
- 第一四分位数(Q1):也叫下四分位数,表示有25%的数据小于等于这个值。
- 第二四分位数(Q2):即中位数,表示中间值,50%的数据小于等于这个值。
- 第三四分位数(Q3):也叫上四分位数,表示有75%的数据小于等于这个值。
- 四分位距(IQR):是第三四分位数与第一四分位数之间的差值,即
IQR = Q3 - Q1
。IQR反映了中间50%数据的范围。IQR准则的应用
根据IQR准则,任何低于Q1 - 1.5 * IQR
或高于Q3 + 1.5 * IQR
的值都被认为是潜在的异常值(温和异常值)。更极端的情况,如果某个值低于Q1 - 3 * IQR
或高于Q3 + 3 * IQR
,则该值被认为是极端异常值。计算步骤:
- 计算Q1、Q2、Q3:首先对数据进行排序,并找到Q1、Q2和Q3。
- 计算IQR:使用公式
IQR = Q3 - Q1
。- 确定界限:计算下限
Lower Bound = Q1 - 1.5 * IQR
和上限Upper Bound = Q3 + 1.5 * IQR
。- 识别异常值:任何小于下限或大于上限的数据点都视为异常值。
在数据分析过程中,原始的薪资数据通常以范围形式存在(如"8-15K"),这种格式不利于进行数值计算和统计分析。因此,需要将薪资列拆分为最低薪资和最高薪资两列,并对其中的异常值进行处理。
处理逻辑:
- 数据拆分:将标准化后的薪资字符串(如"8-15K")拆分为最低薪资和最高薪资两列
- 类型转换:将拆分后的薪资数据转换为整数类型,便于后续计算
- 异常值检测:使用箱线图方法(IQR准则)检测薪资数据中的异常值
- 计算下四分位数(Q1)、上四分位数(Q3)和四分位距(IQR)
- 定义正常范围为 [Q1-1.5IQR, Q3+1.5IQR]
- 异常值处理:对超出正常范围的异常值进行边界修正
- 小于下界的值调整为下界值
- 大于上界的值调整为上界值
- 数据清理:删除原始薪资列和辅助标记列,保留处理后的结果
拆分薪资列并处理异常值的代码如下:
# 拆分薪资列
def split_salary_column(data):
# 移除 'salary' 列中字符串里的 'K' 字符,不使用正则表达式匹配
salary_series = data['salary'].str.replace('K', '', regex=False)
# 将移除 'K' 后的字符串按 '-' 进行分割,并将分割结果展开为两列
# 分别存储到新的 'salary_lower' 和 'salary_upper' 列中
data[['salary_lower', 'salary_upper']] = salary_series.str.split('-', expand=True)
# 将'salary_lower' 和'salary_upper' 列的数据类型转换为整数
data['salary_lower'] = data['salary_lower'].astype(int)
data['salary_upper'] = data['salary_upper'].astype(int)
# 1.5 这个数值已经成为了一种通用的标准和行业惯例,在很多数据分析、统计学教材以及实际的数据处理应用中被广泛使用。
# 使用箱线图的方法检测异常值
# 计算 salary_lower 列的下四分位数(第25百分位数)
Q1_min = data['salary_lower'].quantile(0.25)
# 计算 salary_lower 列的上四分位数(第75百分位数)
Q3_min = data['salary_lower'].quantile(0.75)
# 计算 salary_lower 列的四分位距,即上四分位数与下四分位数的差值
IQR_min = Q3_min - Q1_min
# 计算 salary_lower 列的下限,小于此值的数据可能为异常值
lower_bound_min = Q1_min - 1.5 * IQR_min
# 计算 salary_lower 列的上限,大于此值的数据可能为异常值
upper_bound_min = Q3_min + 1.5 * IQR_min
# 计算 salary_upper 列的下四分位数(第25百分位数)
Q1_max = data['salary_upper'].quantile(0.25)
# 计算 salary_upper 列的上四分位数(第75百分位数)
Q3_max = data['salary_upper'].quantile(0.75)
# 计算 salary_upper 列的四分位距,即上四分位数与下四分位数的差值
IQR_max = Q3_max - Q1_max
# 计算 salary_upper 列的下限,小于此值的数据可能为异常值
lower_bound_max = Q1_max - 1.5 * IQR_max
# 计算 salary_upper 列的上限,大于此值的数据可能为异常值
upper_bound_max = Q3_max + 1.5 * IQR_max
# 标记 salary_lower 列中的异常值,若值小于下限或大于上限,则标记为 True,否则为 False
data['min_salary_outlier'] = (data['salary_lower'] < lower_bound_min) | (data['salary_lower'] > upper_bound_min)
# 标记 salary_upper 列中的异常值,若值小于下限或大于上限,则标记为 True,否则为 False
data['max_salary_outlier'] = (data['salary_upper'] < lower_bound_max) | (data['salary_upper'] > upper_bound_max)
# 调整 salary_lower 列的异常值
data['salary_lower'] = data['salary_lower'].apply(
lambda x: lower_bound_min if x < lower_bound_min else (upper_bound_min if x > upper_bound_min else x))
# 调整 salary_upper 列的异常值
data['salary_upper'] = data['salary_upper'].apply(
lambda x: lower_bound_max if x < lower_bound_max else (upper_bound_max if x > upper_bound_max else x))
# 这里简单选择删除异常值所在的行,只保留 min_salary_outlier 和 max_salary_outlier 均为 False 的行
# data = data[(~data['min_salary_outlier']) & (~data['max_salary_outlier'])]
# 删除薪资列和辅助列
data.drop(['salary', 'min_salary_outlier', 'max_salary_outlier'], axis=1, inplace=True)
return data
拆分并处理异常值后,截取部分数据展示如下。可见 salary_lower(薪资下限)和 salary_upper(薪资上限)两列已规整呈现,数值经清洗后更具分析价值,后续可基于这些标准化数据开展薪资分布、行业对比等分析 。
七、拆分工作区域列
在招聘数据中,工作区域信息常以复合格式存储(如"城市·行政区"),为便于后续分析岗位的区域分布特征,需对工作区域列进行拆分处理。同时,针对拆分后行政区字段的缺失值,结合行业与区域的关联性进行填充。
处理逻辑:
- 区域拆分:按固定分隔符(·)将工作区域(
job_area
)中的行政区(district
)拆分出来 - 行业关联填充:
- 按公司行业(
company_industry
)分组 - 计算每组内行政区的众数,用众数填充对应行业的缺失值
- 若某行业无有效众数,填充"未知"
- 按公司行业(
- 数据清理:删除原始工作区域列,保留拆分后的行政区字段
拆分工作区域列代码如下:
# 拆分工作区域列
def split_job_area_column(data):
data['district'] = data['job_area'].str.split('·').str[1]
for industry in data['company_industry'].unique():
# 从数据中筛选出当前行业的数据
industry_df = data[data['company_industry'] == industry]
# 计算当前行业所在地区列的众数
# 如果众数存在,则取第一个众数;如果众数为空,则使用 '未知' 作为填充值
mode_value = industry_df['district'].mode().iloc[0] if not industry_df[
'district'].mode().empty else '未知'
# 使用计算得到的众数填充当前行业所在地区列的缺失值
data.loc[data['company_industry'] == industry, 'district'] = data.loc[
data['company_industry'] == industry, 'district'].fillna(mode_value)
# 删除工作区域列
# data.drop('job_area', axis=1, inplace=True)
return data
拆分工作区域列后,得到的部分数据展示如下。原 job_area 列以 “城市・行政区・具体地点” 等复合格式呈现,经处理后,district 列精准提取出行政区信息(如五华区、官渡区等 ),数据格式更清晰,便于后续开展区域维度的招聘数据分析。
八、清洗后的数据集
经过缺失值处理(涵盖各字段针对性填充或删除)、重复值检测删除、薪资数据格式标准化(统一转换为 K 单位并拆分)、技能格式规整(清理冗余逗号)、薪资列拆分及异常值修正、工作区域列拆分提取行政区等一系列数据清洗操作后,得到规范可用的数据集。部分数据截图如下,可见各字段格式统一、内容完整,可支撑后续数据分析。
九、完整代码
from pathlib import Path
import pandas as pd
from sqlalchemy import create_engine
def load_data(csv_file_path):
try:
data = pd.read_csv(csv_file_path)
return data
except FileNotFoundError:
print("未找到指定的 CSV 文件,请检查文件路径和文件名。")
except Exception as e:
print(f"加载数据时出现错误: {e}")
# 保存清洗后的数据为csv文件
def save_to_csv(data, csv_file_path):
# 使用 pathlib 处理文件路径
path = Path(csv_file_path)
# 检查文件所在目录是否存在,如果不存在则创建
path.parent.mkdir(parents=True, exist_ok=True)
data.to_csv(csv_file_path, index=False, encoding='utf-8-sig', mode='w', header=True)
print(f'清洗后的数据已保存到 {csv_file_path} 文件')
# 读取MySQL中的数据
def load_from_mysql(table_name):
# 创建数据库引擎实例
engine = create_engine(f'mysql+mysqlconnector://root:zxcvbq@127.0.0.1:3306/position')
data = pd.read_sql_table(table_name, engine)
return data
# 保存清洗后的数据到MySQL数据库
def save_to_mysql(data, table_name):
# 创建一个 SQLAlchemy 引擎,用于连接 MySQL 数据库
# 使用 mysqlconnector 作为 MySQL 的驱动程序
# 数据库连接信息包括用户名 root、密码 zxcvbq、主机地址 127.0.0.1、端口 3306 以及数据库名 position
engine = create_engine(f'mysql+mysqlconnector://root:zxcvbq@127.0.0.1:3306/position')
# 将 DataFrame 中的数据写入到 MySQL 数据库中
# table_name 是要写入的表名
# con 参数指定了数据库连接引擎
# index=False 表示不将 DataFrame 的索引写入数据库
# if_exists='replace' 表示如果表已经存在,则先删除原表,再创建新表并写入数据
data.to_sql(table_name, con=engine, index=False, if_exists='replace')
print(f'清洗后的数据已保存到 {table_name} 表')
def check_data(data):
print('数据基本信息:')
data.info()
# 查看数据前几行信息
print('数据前几行内容信息:')
print(data.head().to_csv(na_rep='nan'))
# 查看所有列的唯一值
print('所有列的唯一值:')
for column in data.columns:
print(f'{column} 列的唯一值:')
print(data[column].unique())
# 缺失值检测及处理
def handle_missing_values(data):
print("缺失值处理前各列缺失值数量:")
print(data.isnull().sum())
# 处理education字段的缺失值
data.dropna(subset=['education'], inplace=True)
# financing_status(融资状态)字段
# 公司的融资状态可能和公司所处行业有一定联系,不同行业的公司融资情况可能不同。
# 因此,我将依据 company_industry(公司行业)分组,用每组内 financing_status 的众数进行填充,以更贴合实际情况。
# 遍历数据中公司行业列的唯一值
for industry in data['company_industry'].unique():
# 从数据中筛选出当前行业的数据
industry_df = data[data['company_industry'] == industry]
# 计算当前行业融资状态列的众数
# 如果众数存在,则取第一个众数;如果众数为空,则使用 '未知' 作为填充值
mode_value = industry_df['financing_status'].mode().iloc[0] if not industry_df[
'financing_status'].mode().empty else '未知'
# 使用计算得到的众数填充当前行业融资状态列的缺失值
data.loc[data['company_industry'] == industry, 'financing_status'] = data.loc[
data['company_industry'] == industry, 'financing_status'].fillna(mode_value)
# 计算 company_size 列的众数,如果众数存在则取第一个众数作为填充值,若不存在则使用 '未知' 作为填充值
mode_company_size = data['company_size'].mode().iloc[0] if not data[
'company_size'].mode().empty else '未知'
# 使用计算得到的众数填充 company_size 列中的缺失值
data['company_size'] = data['company_size'].fillna(mode_company_size)
# 使用 '无明确技能要求' 填充 skill 列中的缺失值,inplace=True 表示直接在原数据上修改
data['skill'].fillna('无明确技能要求', inplace=True)
# 使用 '常规福利' 填充 benefits 列中的缺失值,inplace=True 表示直接在原数据上修改
data['benefits'].fillna('常规福利', inplace=True)
print("缺失值处理后各列缺失值数量:")
print(data.isnull().sum())
return data
# 重复数据检测及处理
def handle_duplicate_data(data):
duplicate_rows = len(data[data.duplicated()])
print("处理前重复行数量:", duplicate_rows)
if duplicate_rows > 0:
data.drop_duplicates(inplace=True)
duplicate_rows = len(data[data.duplicated()])
print("处理后重复行数量:", duplicate_rows)
return data
# 统一薪资数据格式
def convert_salary(s):
if '元/月' in s:
parts = s[:-3].split('-')
low = int(int(parts[0]) / 1000)
high = int(int(parts[1]) / 1000)
return f"{low}-{high}K"
elif '元/天' in s:
parts = s[:-3].split('-')
low = int((int(parts[0]) * 30) / 1000)
high = int((int(parts[1]) * 30) / 1000)
return f"{low}-{high}K"
elif '元/时' in s:
parts = s[:-3].split('-')
low = int((int(parts[0]) * 8 * 22) / 1000)
high = int((int(parts[1]) * 8 * 22) / 1000)
return f"{low}-{high}K"
elif '元/周' in s:
parts = s[:-3].split('-')
low = int((int(parts[0]) * 4) / 1000)
high = int((int(parts[1]) * 4) / 1000)
return f"{low}-{high}K"
elif '*' in s:
parts = s.split('*')[0]
low, high = parts[:-1].split('-')
low = int(int(low[:-1]))
high = int(int(high[:-1]))
return f"{low}-{high}K"
elif '·' in s:
parts = s.split('·')[0]
return parts
return s
# 薪资格式标准化处理
def uniform_salary_format(data):
# 检测格式不统一的数据的单位
# 定义一个正则表达式模式,用于匹配格式为 "数字-数字K" 的字符串
# 其中 ^ 表示字符串的开始,\d+ 表示匹配一个或多个数字,- 表示匹配连字符,$ 表示字符串的结束
pattern = r'^\d+-\d+K$|^面议$'
# 使用 str.match 方法检查 'salary' 列中的每个值是否匹配定义的模式
# ~ 是取反操作符,用于选择不匹配模式的行
# 最终将不匹配模式的行筛选出来,存储在 filtered_df 中
filtered_df = data[~data['salary'].str.match(pattern)]
print(filtered_df['salary'].to_csv(na_rep='nan', index=False))
# 处理薪资格式不统一的数据
data['salary'] = data['salary'].apply(convert_salary)
# 计算薪资列的众数
salary_mode = data[data['salary'] != '面议']['salary'].mode()
if not salary_mode.empty:
mode_value = salary_mode[0]
# 将面议替换为众数
data.loc[data['salary'] == '面议', 'salary'] = mode_value
return data
# 技能统一格式处理
def uniform_skill_format(data):
# 使用正则表达式将 'skill' 列中连续的逗号替换为单个逗号
# 然后去除字符串首尾的逗号
data['skill'] = data['skill'].str.replace(',+', ',', regex=True).str.strip(',')
return data
# 拆分薪资列
def split_salary_column(data):
# 移除 'salary' 列中字符串里的 'K' 字符,不使用正则表达式匹配
salary_series = data['salary'].str.replace('K', '', regex=False)
# 将移除 'K' 后的字符串按 '-' 进行分割,并将分割结果展开为两列
# 分别存储到新的 'salary_lower' 和 'salary_upper' 列中
data[['salary_lower', 'salary_upper']] = salary_series.str.split('-', expand=True)
# 将'salary_lower' 和'salary_upper' 列的数据类型转换为整数
data['salary_lower'] = data['salary_lower'].astype(int)
data['salary_upper'] = data['salary_upper'].astype(int)
# 1.5 这个数值已经成为了一种通用的标准和行业惯例,在很多数据分析、统计学教材以及实际的数据处理应用中被广泛使用。
# 使用箱线图的方法检测异常值
# 计算 salary_lower 列的下四分位数(第25百分位数)
Q1_min = data['salary_lower'].quantile(0.25)
# 计算 salary_lower 列的上四分位数(第75百分位数)
Q3_min = data['salary_lower'].quantile(0.75)
# 计算 salary_lower 列的四分位距,即上四分位数与下四分位数的差值
IQR_min = Q3_min - Q1_min
# 计算 salary_lower 列的下限,小于此值的数据可能为异常值
lower_bound_min = Q1_min - 1.5 * IQR_min
# 计算 salary_lower 列的上限,大于此值的数据可能为异常值
upper_bound_min = Q3_min + 1.5 * IQR_min
# 计算 salary_upper 列的下四分位数(第25百分位数)
Q1_max = data['salary_upper'].quantile(0.25)
# 计算 salary_upper 列的上四分位数(第75百分位数)
Q3_max = data['salary_upper'].quantile(0.75)
# 计算 salary_upper 列的四分位距,即上四分位数与下四分位数的差值
IQR_max = Q3_max - Q1_max
# 计算 salary_upper 列的下限,小于此值的数据可能为异常值
lower_bound_max = Q1_max - 1.5 * IQR_max
# 计算 salary_upper 列的上限,大于此值的数据可能为异常值
upper_bound_max = Q3_max + 1.5 * IQR_max
# 标记 salary_lower 列中的异常值,若值小于下限或大于上限,则标记为 True,否则为 False
data['min_salary_outlier'] = (data['salary_lower'] < lower_bound_min) | (data['salary_lower'] > upper_bound_min)
# 标记 salary_upper 列中的异常值,若值小于下限或大于上限,则标记为 True,否则为 False
data['max_salary_outlier'] = (data['salary_upper'] < lower_bound_max) | (data['salary_upper'] > upper_bound_max)
# 调整 salary_lower 列的异常值
data['salary_lower'] = data['salary_lower'].apply(
lambda x: lower_bound_min if x < lower_bound_min else (upper_bound_min if x > upper_bound_min else x))
# 调整 salary_upper 列的异常值
data['salary_upper'] = data['salary_upper'].apply(
lambda x: lower_bound_max if x < lower_bound_max else (upper_bound_max if x > upper_bound_max else x))
# 这里简单选择删除异常值所在的行,只保留 min_salary_outlier 和 max_salary_outlier 均为 False 的行
# data = data[(~data['min_salary_outlier']) & (~data['max_salary_outlier'])]
# 删除薪资列和辅助列
data.drop(['salary', 'min_salary_outlier', 'max_salary_outlier'], axis=1, inplace=True)
return data
# 拆分公司规模列(company_size)
def split_company_size_column(data):
# 查看公司规模列的唯一值
company_size_unique = data['company_size'].unique()
print(company_size_unique.tolist())
# 移除公司规模列中字符串里的 '人以上'或'人' 字符,不使用正则表达式匹配
company_size_series = data['company_size'].str.replace('人', '', regex=False)
company_size_series = company_size_series.str.replace('以上', '', regex=False)
data[['company_size_lower', 'company_size_upper']] = company_size_series.str.split('-', expand=True)
return data
# 拆分工作区域列
def split_job_area_column(data):
data['district'] = data['job_area'].str.split('·').str[1]
for industry in data['company_industry'].unique():
# 从数据中筛选出当前行业的数据
industry_df = data[data['company_industry'] == industry]
# 计算当前行业所在地区列的众数
# 如果众数存在,则取第一个众数;如果众数为空,则使用 '未知' 作为填充值
mode_value = industry_df['district'].mode().iloc[0] if not industry_df[
'district'].mode().empty else '未知'
# 使用计算得到的众数填充当前行业所在地区列的缺失值
data.loc[data['company_industry'] == industry, 'district'] = data.loc[
data['company_industry'] == industry, 'district'].fillna(mode_value)
# 删除工作区域列
# data.drop('job_area', axis=1, inplace=True)
return data
if __name__ == '__main__':
df = load_data('../data/original_data/position_dataset.csv')
check_data(df)
df = handle_missing_values(df)
df = handle_duplicate_data(df)
df = uniform_salary_format(df)
df = uniform_skill_format(df)
df = split_salary_column(df)
df = split_job_area_column(df)
# df = split_company_size_column(df)
check_data(df)
save_to_csv(df, '../data/data_cleaning_result/cleaned_position_dataset.csv')
# save_to_mysql(df, 'cleaned_position_dataset')