ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+pandas+ECharts:京东手机数据清洗与可视化实战

Python+pandas+ECharts:京东手机数据清洗与可视化实战 简介一份基于Python与ECharts构建京东手机销售数据分析与可视化系统的完整方案文档适合电子商务从业者、数据分析人员以及计算机专业学生阅读参考。文档以电商平台真实数据为对象完整覆盖爬虫采集、Pandas清洗、MySQL存储、Flask后端搭建与ECharts可视化展示五个核心环节先介绍京东商品详情参数和用户评价的抓取方法再说明反爬机制应对策略、重复值与缺失值处理、数据格式统一等预处理思路最后展示可视化大屏与图表组合方式帮助读者建立从数据获取到业务呈现的全链路认知。资源为单个PDF文件内容来自正式期刊论文大小2.01MB含系统框架图、数据表设计和流程图结构紧凑、技术要点清晰既可作为高校大数据课程设计参考资料也可用于电商数据分析项目起步阶段的方法借鉴。当前已有264人学习下载适合希望快速了解电商数据挖掘与Web可视化结合应用的读者。1. 为什么用Python和ECharts做京东手机数据分析与可视化而不是直接截Excel月份复盘电商大促活动时运营同事丢过来一张京东手机品类的销量明细说昨天数据涨得有点怪希望下班前给到原因。如果只是把商品维度表放进Excel最多排出Top 50却回答不了「哪个价格段在贡献销量、哪个品牌在拖后腿、过去28天的趋势拐点在哪里」这类问题。原因在于手机SKU粒度太细同款机型的颜色、容量能拆出十几个编号不经过清洗和口径归一直接透视图表只能看到噪音Python负责把数据洗成业务口径ECharts把口径变成可读的图表这套组合适合每周都要出品类分析报表、需要把结论交给非技术同事的场景也是做数据分析与可视化项目最常用的一条落地路径。2. 数据从哪来requestspandas抓取京东手机列表并清洗成可分析的数据集2.1 页面结构分析与请求参数构造做京东手机分析的第一步不是画图而是拿到一张「能对上口径」的明细表。我一般会先确认数据来源业务数据库导出的订单明细、搜索页抓包得到的商品快照或者第三方采集好的CSV文件三者字段差异很大。如果要从搜索页直接取数最常见的做法是构造search.jd.com的参数用requests带上一份能被识别的User-Agent去取列表页然后用BeautifulSoup抽取商品项。需要注意京东页面一部分内容由异步接口在首屏后填充对于促销价、评价数这类字段脚本可能只拿到骨架。所以稳妥的思路是把它当作补充数据源主力仍然用一份全量销售明细。# fetch_jd.py 仅示意页面采集流程动态字段建议配合离线快照使用 import requests from bs4 import BeautifulSoup def fetch_list(keyword手机, page1): resp requests.get( https://search.jd.com/Search, params{ keyword: keyword, enc: utf-8, page: page, sort: sort_totalsales15_desc, }, headers{ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 }, timeout10, ) soup BeautifulSoup(resp.text, html.parser) return soup这段代码的逻辑是把关键词、编码方式和排序字段拼到params里请求发出后由BeautifulSoup解析HTML。关键参数是sortsort_totalsales15_desc它决定列表按近15日销量倒序排列page决定翻页。需要留意User-Agent不能省略否则可能被重定向到验证页。拿到soup之后从.gl-item选择器取每个商品块sku_id从># parse_items.py def parse_items(soup): rows [] for item in soup.select(.gl-item): sku_id item.get(data-sku) title item.select_one(.p-name em).get_text(stripTrue) if item.select_one(.p-name em) else price_text item.select_one(.p-price i) price price_text.get_text(stripTrue) if price_text else comment item.get(data-comment) or 0 rows.append({sku_id: sku_id, title: title, price: price, comment: comment}) return rowssku_id在这里充当去重主键title用来解析品牌和型号price字段还带着货币符号comment如果没有值就默认0避免后续转int报错。把rows交给pandas之后需要做类型转换import pandas as pd df pd.DataFrame(rows) df[sku_id] df[sku_id].astype(str) df[title] df[title].astype(str) df[price] ( df[price] .str.replace(¥, , regexFalse) .str.replace(,, , regexFalse) ) df[price] pd.to_numeric(df[price], errorscoerce) df[comment] pd.to_numeric(df[comment], errorscoerce).fillna(0).astype(int)to_numeric(errorscoerce)很关键遇到「暂无报价」「无」这类文本时会转成NaN而不是抛异常后续再统一处理缺失值比一遍遍try/except干净。price字段的逗号千分位分隔符必须先去掉否则「5,499」会变成字符串而不是5499。做完这一步才能进入清洗环节否则后面的groupby结果全是脏数据。2.3 清洗策略去重、缺省值与异常价格清洗不是删数据而是把与业务口径冲突的观测值标记出来。手机类目最常见的情况有三个同一商品在搜索结果里因促销分会重复出现部分SKU的价格字段为空还有个别价格低到离谱比如1元、0.1元的赠品或补差价链接这些都会把均价和销售份额拉偏。我采用的顺序是先去重再补缺值最后做区间合理性校验。字段问题表现清洗规则sku_id同一SKU多次出现drop_duplicates(keepfirst)price空值或带货币符号去除符号后to_numeric空值交给均值/过滤处理sales字符串或缺失to_numeric后fillna(0)title品牌名混在颜色规格里正则词表匹配未命中过滤# clean.py df df.drop_duplicates(subset[sku_id], keepfirst) df df[df[sku_id].str.len() 6] df df.dropna(subset[price]) df df[df[price] 0] # 过滤明显异常价格低于50元或高于50000元 df df[(df[price] 50) (df[price] 50000)] # 品牌字段示例从标题提取真实项目可用品牌词表匹配 df[brand] df[title].str.extract( r(Apple|华为|小米|荣耀|OPPO|vivo|三星|魅族|一加|realme|iQOO), expandFalse, ) df df[df[brand].notna()]参数说明drop_duplicates(keepfirst)保留第一次出现的SKU通常对应销量最高的那个结果品牌用正则词表提取比全标题拆词更可控。如果某些商品标题包含「官方旗舰店」而不含品牌名词表匹配会漏可以追加一个别名映射比如「荣耀Honor」「iQOOvivo子品牌」这类业务规则。清洗完成后再检查df.info()看每列非空数量是否合理。保存成parquet或CSV供下一章做指标计算避免每次重新解析页面。提示如果原始数据本身就是数据库导出的明细表可以跳过抓取部分只保留2.2的类型转换和2.3的清洗逻辑注意补充订单日期和商品类目字段。3. 销量口径与聚合计算用pandas算出品牌份额、价格区间和走势3.1 指标口径销量是SKU级还是SPU级先统一口径再计算。京东手机商品结构至少分两层SPU是消费者看到的「商品详情页」同一详情页下不同颜色、容量就是不同SKU。比如某款手机有12个SKU在数据表里就是12行。如果直接按SKU做饼图饼会碎成几十片观感差、结论也难读。所以在聚合前把title里的产品系列提取成一个model字段例如「Apple iPhone 15 Pro」「华为 Mate 60 Pro」从业务角度分析到型号粒度已经足够。df[model] df[title].str.replace(r.*?|\(.*?\), , regexTrue) df[model] df[model].str.extract( r(iPhone [\dA-Za-z ]|Mate \d|小米\d|Redmi [\dA-Za-z]), expandFalse, )这段正则的核心是先把「括号里的内容当作SKU差异」丢弃再从剩余文本中匹配型号特征。真实项目中会遇到国际化命名如「iPhone 15 Pro Max」「Redmi Note 13 Pro」正则词表需要按列表页曝光最多的型号持续维护。另一个常被忽略的点是同一型号在不同店铺会挂不同SPU所以销量聚合的粒度应该是「品牌型号」而不是sku_id或商品链接。只有把这个口径讲清楚后面算出的份额才是用户能看懂的品牌份额。3.2 用groupby算品牌份额与价格分布品牌份额的本质是分组求和而不是平均值。我习惯先做一张分组汇总表把核心指标一次算全再从中取给图表用的子集df[gmv] df[price] * df[sales] brand_stats ( df.groupby(brand) .agg( sku_count(sku_id, nunique), total_sales(sales, sum), total_gmv(gmv, sum), ) .sort_values(total_sales, ascendingFalse) .reset_index() ) brand_stats[share] brand_stats[total_sales] / brand_stats[total_sales].sum() brand_stats[avg_price] brand_stats[total_gmv] / brand_stats[total_sales].clip(lower1)nunique统计品牌下不同SKU数量sum分别汇总销量和销售额。之所以先算gmv再除销量而不是直接对price求平均是因为一个品牌下高价机型少、低价机型多时算术平均价会被少数SKU带偏用销售额除以销量得到的是真实的成交均价这也是在做价格分析时最容易出错的地方。clip(lower1)防止销量为0时除零保留NaN会让后续图表显示出错。价格分布用pd.cut分段常见的段位是千元机、次旗舰和高端机边界可以在参数里调bins [0, 1000, 2000, 3000, 5000, 10000, 50000] labels [0-1k, 1k-2k, 2k-3k, 3k-5k, 5k-10k, 10k] df[price_band] pd.cut(df[price], binsbins, labelslabels, rightFalse) band_sales ( df.groupby(price_band, observedFalse)[sales] .sum() .sort_index() )rightFalse表示区间左闭右开1000元整落在0-1k不会漏掉边界值observedFalse让分组保留声明过的全部类别即使某些区间没有数据也会在图表上显示为0避免柱状图少一根柱子。对于饼图share直接取band_sales / band_sales.sum()然后转成百分比。如果发现某个区间占比极小可以在labels里合并减少饼图扇区数量。3.3 时间序列重采样把订单级数据变成日销量分析趋势时原始数据往往是订单明细或每日快照需要先转成以天为单位的序列df[order_date] pd.to_datetime(df[order_date], errorscoerce) daily ( df.dropna(subset[order_date]) .set_index(order_date)[sales] .resample(D) .sum() ) daily daily.asfreq(D, fill_value0) daily_sma7 daily.rolling(7, min_periods1).mean()resample(D)按自然日聚合先把order_date转成datetime再设为索引这是与ECharts折线图对接的关键ECharts的x轴需要等间距时间点缺失日期直接跳过会导致刻度间距失真。asfreq(D, fill_value0)补上没有任何销售的日期补零比移除更符合销售事实。rolling(7).mean()得到7日均线用于观察促销日前后是真实增长还是脉冲式波动如果某一天的日销量曲线明显高于均线说明有大促活动在推量。提示重采样前先确认order_date的时区。如果数据来自带时区的时间戳直接to_datetime后再resample会把东八区零点前的订单归到前一天统一用tz_localize(None)去掉时区更稳妥。4. 把聚合结果交给ECharts折线、饼图与柱状图的渲染细节4.1 从DataFrame到option的转换函数数据计算完成之后最常见的传输方式不是直接在JavaScript里写死数值而是由pandas生成JSON再由页面读取。我一般会把聚合结果封装成字典再json.dumps注入到HTML模板里import json def build_line_option(series): return { tooltip: {trigger: axis}, legend: {data: [销量]}, grid: {left: 50, right: 20, bottom: 40, top: 40}, xAxis: { type: category, data: series.index.strftime(%m-%d).tolist(), }, yAxis: {type: value, name: 销量(台)}, series: [ { name: 销量, type: line, smooth: True, data: [int(v) for v in series.tolist()], } ], } option build_line_option(daily)date index经过strftime转成MM-DD数值统一int是为了避免Timestamp对象无法被json序列化。构建完成后页面里用JSON.parse(document.getElementById(data).textContent)读取或者直接模板替换option ...。ECharts初始化时机要放在DOM渲染之后否则容器宽度为0图表画出来一片空白。常见做法是把init写进window.onload或使用setTimeout延后尤其当图表容器位于折叠的tab页时需要等tab显示后再init。4.2 折线图x轴刻度和tooltip自动换行拿到日销量序列后折线图的常见问题集中在两个地方x轴标签挤成一团、tooltip一行显示不完多系列信息。第一个问题的解法是给axisLabel加旋转和自动隐藏xAxis: { axisLabel: { interval: 0, rotate: 30, hideOverlap: true, color: #666 } }interval: 0表示每个时间点都显示rotate: 30逆时针旋转标签30度可读性最好hideOverlap: true在标签仍然重叠时自动跳过部分标签比手动算step更省事。如果横跨两个月showMaxLabel与showMinLabel能保证首尾日期始终可见。对手机品类来说大促日通常集中在月中和月末标签隐藏过头容易漏掉关键拐点配合dataZoom前后缩放进来看更完整。场景关键参数作用x轴标签密集axisLabel.rotate/hideOverlap旋转并自动隐藏重叠项tooltip多系列挤在一行formatter拼接br/每个系列单独一行饼图引导线黏连labelLine.length2拉长水平引线柱状图用自定义图片itemStyle.color.image用Image对象填充柱体tooltip自动换行发生在multi-series场景比如同时显示销量与7日均线。用formatter函数拼接字符串即可tooltip: { trigger: axis, formatter: function (params) { let header params[0]?.axisValue || ; return ( header params .map( (p) br/${p.marker}${p.seriesName}: ${Number(p.value).toLocaleString()} ) .join() ); } }p.marker是系列高亮小圆点p.axisValue是当前x轴刻度。关键点是返回的字符串里直接拼br/框架会把它当HTML渲染所以不要在业务数据里混入未清洗的用户输入防止XSS。如果嫌字符串拼接太原始也可以用textStyle.lineHeight统一行距设置confine: true能让tooltip在大屏边缘不超出画布。4.3 饼图labelLine偏移与自定义柱状图图片饼图在品牌份额展示中出场率最高也最容易出现引导线错位。ECharts里labelLine控制数据标签到扇区之间的引线小圆点偏移通常不是圆点本身而是引导线长度或label对齐方式配置不对series: [ { type: pie, radius: [35%, 65%], label: { formatter: {b}\n{d}%, alignTo: edge, edgeDistance: 20, lineHeight: 18 }, labelLine: { length: 10, length2: 20, smooth: true } } ]radius内圆外圆设成环形饼图增大中心区域利用率alignTo: edge让所有标签右对齐视觉上比默认更整齐length2控制标签与引导线转折点之间的水平距离经常遇到的末尾小圆点偏移多数是length2太小导致小圆点和文本黏连调到20后基本可解。smooth: true让引线带曲线大屏上看更柔和。柱状图自定义图片是另一个高频需求运营希望用品牌Logo代替普通柱体颜色。ECharts的itemStyle.color支持image对象和repeatconst img new Image(); img.src /logo/apple.png; itemStyle: { color: { image: img, repeat: repeat } }注意image字段接收的是Image对象的引用而不是路径字符串一定要在图片onload完成调用chart.setOption否则整张图渲染成透明。如果想要每个柱子显示不同Logo给data里每一项配itemStyle即可series: [ { type: bar, barWidth: 26, data: brandStats.map((item, index) ({ value: item.total_sales, itemStyle: { color: { image: logoList[index], repeat: repeat } } })) } ]4.4 多图表联动点击柱状图更新饼图进阶一点的做法是让柱状图与饼图联动。比如点击「Apple」柱子右侧饼图变成Apple内部不同价格区间的销售占比这个功能不需要引第三方库只用ECharts的myChart.on(click, ...)和setOption配合。要点在于给柱状图数据项预先带上brand字段点击时把它传给饼图的series.datamyChart.on(click, function (params) { if (!params.data || !params.data.brand) return; const bandData getBandDataForBrand(params.data.brand); pieChart.setOption( { series: [{ data: bandData }] }, true ); pieChart.setOption({ title: { text: params.data.brand 价格段分布 } }); });setOption第二个参数传true是notMerge模式会清空旧series并替换成新数据避免残留上一次的图形。这里有个容易被忽略的细节点击事件触发在init之后如果图表被resize过params.data仍能取到原始对象但因为柱状图data被map包了一层需要在构建data时保留brand属性否则拿到的只是数字。多图表联动能显著提升仪表盘的使用价值也是做可视化大屏时最值得花时间的部分。5. 让可视化回答业务问题tooltip格式化、大屏布局与性能调优5.1 单图里的业务结论优先级markLine与强调色图表不是把数据画出来就完了。对京东手机这种量级的数据通常会在折线图里叠加三条辅助线全周期均值线、销量最高点标注、大促日markLine。ECharts的markLine可以直接读取计算后的常量不用再手动查数据series: [ { type: line, markLine: { silent: true, symbol: none, label: { formatter: 均值 {c} }, data: [{ yAxis: dailyMean, name: 日均销量 }] }, markPoint: { data: [{ type: max, name: 峰值 }] } } ]type: max会在数据最大值位置自动打点比手动指定索引更稳尤其当时间范围动态变化时。这样折线图一眼就能定位到异常高点再配合markArea把大促日期块标灰业务同学不需要KPI背景也能直接回答「为什么那天涨了」。5.2 大屏布局多图表resize与容器自适应可视化大屏的常见坑是图表在1920宽下正常在1366的显示器上挤压变形。ECharts本身不负责容器自适应需要监听window的resize事件同时兼顾tab切换window.addEventListener(resize, function () { lineChart.resize(); pieChart.resize(); }); const observer new ResizeObserver(() { lineChart.resize(); pieChart.resize(); }); observer.observe(document.getElementById(dashboard));ResizeObserver比resize事件更可靠因为容器大小变化不一定来自窗口变化比如侧边栏收起、tab切换。如果在Vue或React里使用组件卸载时记得observer.disconnect()否则会重复触发。布局上多用百分比宽度和calc()不要给图表容器写死像素宽度grid配置留出right空间给饼图标签避免edge对齐后标签被裁剪。5.3 大数据量下的性能sampling与dataZoom按月粒度展示日销量只有30多个点完全不需要优化但如果是分钟级订单数据series里的data可能上万图表会明显掉帧。此时可以在line系列里开sampling: lttbECharts会通过最大三角形面积算法保留趋势特征点series: [ { type: line, sampling: lttb, data: largeArray } ]lttb比min和max采样更能保住尖峰和波谷适合手机销量这种突刺型曲线。配合dataZoom的zoomLock和start/end让首屏只渲染一个时间窗dataZoom: [ { type: inside, start: 0, end: 30 }, { type: slider, start: 0, end: 30, zoomLock: true } ]zoomLock: true锁住缩放比例用户只能平移不能无限制放大避免拖到只有两三个点。如果数据量再上一个量级最后的手段是把数据在Python端按小时downsample后再交给ECharts而不是让前端承受全部原始记录。这里正好是验证图表正确性的位置拿清洗后数据集的daily.sum()与页面tooltip显示的最后一个值比对数值一致再继续做性能优化否则再好看的图也只是一张会动的错误报表。本文还有配套的精品资源点击获取
返回列表