1. 药品销售数据清洗的典型场景与挑战
药品销售数据作为医药行业的核心业务数据,具有几个鲜明的特征:首先,数据来源极其复杂,可能来自医院HIS系统、药店POS机、电商平台、线下批发记录等多种渠道;其次,字段规范不统一,同一种药品在不同系统中可能有不同名称(如"阿司匹林肠溶片"与"拜阿司匹灵");第三,数据质量问题高发,常见有空值、异常值、重复记录等问题。
我在处理某连锁药店3年销售数据时,曾遇到一个典型案例:同一家分店同一天的"连花清瘟胶囊"销售记录,在原始数据中出现了6种不同写法(包含"连花清瘟"、"连花清瘟胶囊"、"连花清瘟胶囊(24粒)"等),还有将"盒"误录为"合"的情况。这种数据如果不经清洗直接分析,结果将完全失真。
药品数据特有的敏感属性也增加了清洗难度。比如:
- 药品规格单位混乱(盒/瓶/支/袋混用)
- 批号与有效期格式不统一
- 特殊字符问题(如温度符号℃变成乱码)
- 医保分类代码缺失
- 销售时间戳格式不一致(有年月日、有Unix时间戳)
关键经验:药品数据清洗前必须与业务部门确认药品主数据标准,建立药品名称、规格、单位的映射规则表,这是后续所有清洗工作的基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Python数据清洗工具链的选型策略
面对药品销售数据清洗任务,Python生态提供了完整的工具链。经过多个项目验证,我的核心工具组合是:
python复制基础工具包:
import pandas as pd # 数据操作核心
import numpy as np # 数值计算
import re # 正则表达式
专业扩展包:
from fuzzywuzzy import fuzz # 模糊匹配
import pyclinrec # 医学术语识别
from datetime import datetime # 时间处理
为什么选择这个组合?Pandas的DataFrame结构特别适合表格型销售数据的处理,其向量化操作比逐行处理效率高10倍以上。而fuzzywuzzy对于药品名称的模糊匹配准确率能达到85%以上(需配合自定义规则)。
对于大型数据集(超过100万行),建议使用:
python复制import dask.dataframe as dd # 替代pandas处理大数据
from pandarallel import pandarallel # 多核并行加速
pandarallel.initialize()
实测案例:在某省医保数据分析项目中,使用普通Pandas清洗800万行数据需要42分钟,改用Dask+并行处理后缩短到9分钟。配置要点是:
- 设置合适的分区大小(建议每个分区100-200MB)
- 避免在循环中反复创建DataFrame
- 对字符串操作优先使用
.str访问器
3. 药品数据清洗的完整实操流程
3.1 原始数据质量诊断
首先用这套诊断代码快速定位问题:
python复制def data_diagnose(df):
# 空值统计
null_report = df.isnull().sum().to_frame('空值数')
null_report['空值占比'] = (null_report['空值数']/len(df)).round(2)
# 唯一值分析
unique_report = df.nunique().to_frame('唯一值数')
# 数据样例
sample_report = pd.concat([df.head(3), df.sample(3)], axis=0)
return {
'空值分析': null_report,
'唯一值分析': unique_report,
'数据样例': sample_report
}
典型药品数据诊断结果示例:
- 商品名字段:12%空值,但唯一值数与总行数比为1:1.3,说明存在大量近似重复
- 销售日期字段:5%异常值(如3000-01-01)
- 销售金额字段:有0.7%的负值(应全部为正值)
3.2 药品名称标准化七步法
这是经过20多个项目验证的标准化流程:
-
基础清洗:去除前后空格、统一全半角
python复制df['药品名称'] = df['药品名称'].str.strip().str.replace(r'[\s\u3000]+', ' ', regex=True) -
规格提取:用正则分离药品名和规格
python复制pattern = r'(.*?)(\d+[mgml片粒支盒袋]+\w*)(.*)' df[['药品名','规格','后缀']] = df['药品名称'].str.extract(pattern) -
模糊匹配:建立标准药品名录
python复制from fuzzywuzzy import process standards = ['阿司匹林肠溶片', '连花清瘟胶囊',...] # 标准名录 def fuzzy_match(name): return process.extractOne(name, standards, scorer=fuzz.token_set_ratio)[0] df['标准名称'] = df['药品名'].parallel_apply(fuzzy_match) -
人工复核:对匹配度<85%的记录输出复核表
-
单位统一:将"g"、"克"统一为"mg"等
-
批号校验:验证批号是否符合药监格式
-
最终校验:与业务系统进行二次核对
3.3 销售数据异常值处理方案
针对药品销售数据的特殊异常场景:
场景1:拆零销售导致的非整数数量
python复制# 识别合理拆零(如0.5片)
valid_decimals = {0.5, 0.25} # 允许的小数值
df['异常数量'] = ~(df['数量'].isin(valid_decimals) | df['数量'].eq(df['数量'].astype(int)))
场景2:促销导致的异常低价
python复制# 动态价格阈值计算
price_stats = df.groupby('标准名称')['单价'].agg(['mean', 'std'])
df = df.merge(price_stats, on='标准名称')
df['价格异常'] = (df['单价'] < (df['mean'] - 3*df['std'])) | (df['单价'] > (df['mean'] + 3*df['std']))
场景3:时间维度异常
python复制# 转换多种日期格式
def parse_date(date_str):
for fmt in ('%Y-%m-%d', '%Y/%m/%d', '%Y%m%d', '%d-%b-%y'):
try:
return datetime.strptime(date_str, fmt)
except:
continue
return pd.NaT
df['销售日期'] = df['销售日期'].apply(parse_date)
df['日期异常'] = df['销售日期'].isna() | (df['销售日期'] > datetime.now())
4. 进阶清洗技巧与性能优化
4.1 内存优化方案
药品销售数据往往时间跨度大,原始CSV可能达到GB级别。这套方法可将内存占用降低60%:
python复制dtypes = {
'药品ID': 'category', # 低基数字段用category
'单价': 'float32', # 价格类用32位浮点
'数量': 'uint16', # 数量不会太大
'门店编号': 'category'
}
df = pd.read_csv('sales.csv', dtype=dtypes)
# 分块处理大文件
chunk_size = 100000
chunks = pd.read_csv('large_sales.csv', chunksize=chunk_size, dtype=dtypes)
result = pd.concat([process_chunk(chunk) for chunk in chunks])
4.2 多进程加速实践
对于千万级数据,这个并行模式可提升3-5倍速度:
python复制from multiprocessing import Pool, cpu_count
def parallel_clean(df):
with Pool(cpu_count()-1) as pool:
# 分块处理
chunks = np.array_split(df, cpu_count()*2)
results = pool.map(clean_chunk, chunks)
return pd.concat(results)
def clean_chunk(chunk):
# 应用各种清洗函数
chunk = standardize_names(chunk)
chunk = validate_dates(chunk)
return chunk
4.3 自动化测试体系
建立数据质量测试用例,确保清洗逻辑的稳定性:
python复制import unittest
class TestDataCleaning(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.clean_df = load_cleaned_data()
def test_no_null_in_critical_fields(self):
critical_fields = ['药品ID', '销售日期', '数量', '金额']
for field in critical_fields:
with self.subTest(field=field):
self.assertFalse(self.clean_df[field].isnull().any())
def test_all_prices_positive(self):
self.assertTrue((self.clean_df['单价'] > 0).all())
if __name__ == '__main__':
unittest.main(argv=[''], exit=False)
5. 业务逻辑验证与数据归档
5.1 业务规则校验
清洗后的数据必须通过业务逻辑测试:
python复制def validate_business_rules(df):
# 规则1:同一批号的有效期必须一致
batch_expiry = df.groupby(['标准名称','批号'])['有效期'].nunique()
assert (batch_expiry == 1).all(), "存在批号对应多个有效期的情况"
# 规则2:特殊药品必须有医师信息
controlled_drugs = ['麻醉类','精神类'] # 示例
mask = df['药品类型'].isin(controlled_drugs)
assert not df[mask]['医师执照号'].isnull().any(), "特殊药品缺少医师信息"
# 规则3:销售日期不晚于有效期
mask = df['销售日期'] > df['有效期']
assert not mask.any(), f"存在{sum(mask)}条过期销售记录"
5.2 数据版本控制
使用这套方案管理清洗过程版本:
python复制import hashlib
from datetime import datetime
def create_data_version(raw_path, clean_df, params):
# 生成版本指纹
raw_hash = hashlib.md5(open(raw_path,'rb').read()).hexdigest()[:8]
time_str = datetime.now().strftime("%Y%m%d_%H%M")
version = f"v1_{time_str}_{raw_hash}"
# 保存元数据
meta = {
'version': version,
'create_time': datetime.now().isoformat(),
'source_file': raw_path,
'row_count': len(clean_df),
'params': params # 清洗参数
}
# 保存数据
save_path = f"./cleaned/{version}.parquet"
clean_df.to_parquet(save_path)
with open(f"./cleaned/{version}_meta.json", 'w') as f:
json.dump(meta, f)
return version
5.3 自动化监控看板
用此代码生成数据质量监控报告:
python复制import matplotlib.pyplot as plt
def generate_quality_report(df):
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 空值分布
nulls = df.isnull().mean().sort_values(ascending=False)
nulls[nulls > 0].plot.bar(ax=axes[0,0], title='空值占比')
# 数值分布
df['单价'].plot.hist(ax=axes[0,1], bins=50, title='单价分布')
# 时间趋势
df['销售日期'].value_counts().sort_index().plot(
ax=axes[1,0], title='每日销售趋势')
# 药品分布
df['标准名称'].value_counts()[:20].plot.barh(
ax=axes[1,1], title='Top20药品销量')
plt.tight_layout()
return fig
