基于Pandas和FineBI的昆明职位数据分析与可视化实现(二)- 职位数据清洗与预处理

发布于:2025-06-30 ⋅ 阅读:(20) ⋅ 点赞:(0)


一、数据集介绍

这份昆明职位数据集源自 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,数据集中已不存在缺失值,处理达到预期效果。

在这里插入图片描述


三、重复值处理

在数据清洗阶段,需要识别并处理完全重复的记录,即两行数据的所有字段值均相同的情况。这类重复数据可能源于数据采集过程中的冗余或系统误差,会导致后续分析结果出现偏差。因此,采用以下策略进行处理:

处理逻辑

  1. 检测数据集中完全重复的行数
  2. 若存在重复行,则删除重复记录,仅保留其中一行
  3. 验证处理结果,确保数据集中无重复记录

重复值处理代码如下所示:

# 重复数据检测及处理
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”(千)为单位的格式。

处理逻辑

  1. 检测非标准格式:通过正则表达式识别不符合"数字-数字K"或"面议"格式的薪资数据
  2. 单位转换:将不同时间单位的薪资数据统一转换为月薪(K)表示
    • 元/月:直接除以1000转换为K
    • 元/天:乘以30天再除以1000转换为K
    • 元/时:乘以8小时/天 × 22天/月再除以1000转换为K
    • 元/周:乘以4周/月再除以1000转换为K
  3. 特殊格式处理
    • 带"*"的格式:提取数字部分并转换为K
    • 带"·“的格式:提取”·"前的部分
  4. 处理"面议"情况:用非面议薪资的众数替换"面议"值,以保留数据统计特性

薪资数据格式处理代码如下所示:

# 统一薪资数据格式
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” 的简洁格式,让薪资数据更规整,便于后续分析。

在这里插入图片描述


五、技能格式处理

技能数据在原始数据集中存在格式不统一的问题,主要表现为技能标签之间存在连续的逗号(如","),以及字符串首尾可能存在多余的逗号。这种不规范的格式会影响后续对技能数据的分词、统计和分析。因此,需要对技能数据进行格式标准化处理。

处理逻辑

  1. 连续逗号处理:使用正则表达式将连续的多个逗号替换为单个逗号
  2. 首尾逗号处理:去除字符串首尾的逗号,确保技能标签的独立性

技能格式处理代码如下所示:

# 技能统一格式处理
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,则该值被认为是极端异常值。

计算步骤:

  1. 计算Q1、Q2、Q3:首先对数据进行排序,并找到Q1、Q2和Q3。
  2. 计算IQR:使用公式 IQR = Q3 - Q1
  3. 确定界限:计算下限 Lower Bound = Q1 - 1.5 * IQR 和上限 Upper Bound = Q3 + 1.5 * IQR
  4. 识别异常值:任何小于下限或大于上限的数据点都视为异常值。

在数据分析过程中,原始的薪资数据通常以范围形式存在(如"8-15K"),这种格式不利于进行数值计算和统计分析。因此,需要将薪资列拆分为最低薪资和最高薪资两列,并对其中的异常值进行处理。

处理逻辑

  1. 数据拆分:将标准化后的薪资字符串(如"8-15K")拆分为最低薪资和最高薪资两列
  2. 类型转换:将拆分后的薪资数据转换为整数类型,便于后续计算
  3. 异常值检测:使用箱线图方法(IQR准则)检测薪资数据中的异常值
    • 计算下四分位数(Q1)、上四分位数(Q3)和四分位距(IQR)
    • 定义正常范围为 [Q1-1.5IQR, Q3+1.5IQR]
  4. 异常值处理:对超出正常范围的异常值进行边界修正
    • 小于下界的值调整为下界值
    • 大于上界的值调整为上界值
  5. 数据清理:删除原始薪资列和辅助标记列,保留处理后的结果

拆分薪资列并处理异常值的代码如下:

# 拆分薪资列
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(薪资上限)两列已规整呈现,数值经清洗后更具分析价值,后续可基于这些标准化数据开展薪资分布、行业对比等分析 。

在这里插入图片描述


七、拆分工作区域列

在招聘数据中,工作区域信息常以复合格式存储(如"城市·行政区"),为便于后续分析岗位的区域分布特征,需对工作区域列进行拆分处理。同时,针对拆分后行政区字段的缺失值,结合行业与区域的关联性进行填充。

处理逻辑

  1. 区域拆分:按固定分隔符(·)将工作区域(job_area)中的行政区(district)拆分出来
  2. 行业关联填充
    • 按公司行业(company_industry)分组
    • 计算每组内行政区的众数,用众数填充对应行业的缺失值
    • 若某行业无有效众数,填充"未知"
  3. 数据清理:删除原始工作区域列,保留拆分后的行政区字段

拆分工作区域列代码如下:

# 拆分工作区域列
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')