
做数据可视化项目我一直有个观点数据量大小不是关键能不能把数字讲成人话才是核心。这次拿Python把两个看似不搭边的数据源——班级学生信息和微信好友列表——放在一起做了一次全景分析前者是典型的校园结构化数据后者是带文本信息的社交数据。两个方向走一遍基本上就把Python数据可视化这条链路里的数据清洗、指标设计、图表选型、交互呈现都摸了一遍。这篇文章就是这次实战的完整记录。适合刚学完Python语法、想找个真实项目练手的人也适合已经写过一些爬虫和图表、但对如何把分析做完整还缺一套方法论的同学。我会把项目思路、表结构设计、每个可视化图表的选型理由、以及实操中踩过的坑都拆开讲尽量让你照着走也能复现。1. 项目整体设计与思路拆解1.1 为什么选这两个数据源先说为什么把班级数据和微信好友数据放在一起。这两个数据源差异很大正好覆盖了数据分析最常见的两类场景。班级学生信息是典型的结构化数据一行一条记录字段固定类型清晰学号、姓名、性别、各科分数。这种数据最大的特点是脏得很有规律无非是缺值、重复、类型不对、越界值清洗路径比较标准非常适合用来练基本功。而且学生成绩分析有明确的目标感结论可以直接被班主任拿来用属于做完就有效果的项目新手不会迷失方向。微信好友数据则是半结构化非结构化的混合体。好友列表本身是结构化字段昵称、性别、地区、签名但签名和标签属于文本需要做文本挖掘。这类数据最大的价值在于有故事感——你可以从性别比例看出账号的使用场景从地域分布看出社交圈子的半径从签名关键词拆出好友的兴趣偏好。它比单纯读CSV文件有意思得多也是社交画像挖掘这个说法的来源。选中这两个数据源还有一个现实原因它们都不涉及商业机密和平台数据壁垒。班级数据可以自己模拟一份微信好友数据只分析自己账号的字段信息合规边界清楚。整个项目做下来你能同时掌握数据清洗、统计分析、文本挖掘和可视化四条技能线。1.2 技术栈选型与理由这次实战用到的Python库我按职责分成了四类选型的时候有明确的取舍逻辑职责库选型理由数据处理pandas处理表格数据的绝对主力分组聚合、透视表、缺失值处理一条龙基础可视化matplotlib所有图表的地基定制能力强但API偏底层统计可视化seaborn基于matplotlib封装一行代码出热力图适合做相关性分析交互可视化pyecharts生成HTML图表支持缩放、悬浮提示适合做汇报展示文本分析jieba wordcloud中文分词和词云生成签名挖掘的关键工具为什么不用单一库搞定所有图表我解释一下这背后的考虑。matplotlib和seaborn适合做静态分析图尤其是学术细节很严格的场景比如分数分布直方图、相关性热力图这两类图在matplotlib里能精确控制每个元素。但面对班级期末总结这种需要给非技术人看的场景静态图就不够用了pyecharts生成的可交互HTML图表鼠标悬停能看到具体分数比一张PNG图片有说服力得多。两类数据源正好走两条可视化路线学生分析偏静态和统计性好友画像偏交互和传播性。两条路线都覆盖一遍你才算真正玩转Python可视化。1.3 整体流程设计这个项目不是一个脚本跑完就结束而是按数据分析的标准流程拆成五个阶段数据获取模拟表或接口读取、数据清洗去重、补缺、纠错、指标设计核心KPI定义、可视化呈现图表选择与配色、结论输出从图表里读出可执行的信息。每个阶段单独写一个函数或模块方便复用。后面所有分析都是在这条流程上叠加所以前期设计一定要控制好粒度别把所有逻辑塞进一个脚本里。我的习惯是建一个项目目录按功能拆文件data/放数据clean.py放清洗逻辑analysis.py放指标计算visual/放图表生成脚本最后用一个总入口串联。2. 班级学生信息全景分析从数据清洗到指标设计2.1 数据准备字段怎么定先准备一份模拟数据。这里不建议马上用爬虫或者接真实数据库手造一份30到50行的数据就够了关键是覆盖各种脏情况缺失的分数、重复的学号、性别字段里混入的异常值、总分和分科成绩对不上的记录。这样在清洗阶段才能遇到问题。推荐表结构如下字段名类型说明student_id字符串学号唯一标识name字符串姓名gender字符串性别男/女class_name字符串班级编号chinese浮点语文成绩math浮点数学成绩english浮点英语成绩total_score浮点总分可计算我生成数据时会故意埋几个雷某个学生数学成绩写成字符串89.5某个学生成绩超出150分可能是录入错误还有同一个学号出现两行。这些雷在你真正上手时都会遇到提前埋好才能练到清洗。import pandas as pd import numpy as np np.random.seed(42) students { student_id: [fS{str(i).zfill(3)} for i in range(1, 41)], name: [f学生{i} for i in range(1, 41)], gender: np.random.choice([男, 女], 40), class_name: np.random.choice([高一(1)班, 高一(2)班], 40), chinese: np.random.randint(60, 150, 40), math: np.random.randint(60, 150, 40), english: np.random.randint(60, 150, 40), } df pd.DataFrame(students) df[total_score] df[[chinese, math, english]].sum(axis1) # 故意制造脏数据 df.loc[3, math] 89.5 # 类型异常 df.loc[7, chinese] 165 # 越界值 df.loc[10, gender] 未知 # 枚举值异常2.2 数据清洗三条硬规则拿到数据第一步不是画图是先看数据长什么样用df.info()、df.describe()、df.isnull().sum()三个命令快速体检。班级数据清洗我总结出三条硬规则任何一次处理都要照着做。第一类型是底线。数学成绩出现字符串直接用pd.to_numeric()转换并强制errorscoerce转不过去的变空值再处理。这一步如果不做后面所有聚合计算都会报错甚至静默给出错误结果。第二越界值必须标记而不是删除。像语文165分的记录先判断是否超出合理范围比如0到150再决定是修正还是剔除。我通常的做法是单独存一份异常清单保留原始记录的同时在分析中排除这样能追溯不会误删有效数据。第三重复记录要看业务含义。同一个学号出现两行这时候不能盲目drop_duplicates()要先确认哪条是最新录入的记录我一般按录入时间或总分有效性来保留正确的那行。# 类型统一 df[math] pd.to_numeric(df[math], errorscoerce) # 异常值标记分数超范围设为NaN score_cols [chinese, math, english] for col in score_cols: df.loc[(df[col] 0) | (df[col] 150), col] np.nan # 去重 df df.drop_duplicates(subsetstudent_id, keeplast) # 重新计算总分 df[total_score] df[score_cols].sum(axis1)这里有一个容易被忽略的点总分列一旦存在就会出现总分不等于分科之和的脏数据。与其清洗总分不如直接删掉原始总分列每次分析前实时计算。我踩过这个坑数据源里如果同时包含总分和分科别人维护时很容易只改分科忘改总分。所以凡是能算出来的字段一律不信任原始值。2.3 指标设计班主任真正关心什么数据洗干净之后最忌讳的就是直接疯狂画图。画图之前先想清楚我要回答什么问题。班主任关心的核心问题通常集中在五个方向班级整体水平如何均分、多少人及格多少人优秀及格率与优秀率、成绩分布是否两极分化标准差与分布形态、男生女生有没有差异分组对比、哪两科关联性强相关性分析。我提炼成五组指标每组对应一张图表整体水平各科平均分、总分平均分用柱状图呈现分布形态总分直方图核密度曲线看是正态还是偏态及格与优秀各科及格率、优秀率≥135分用比例条形图性别差异按性别分组算各科均分用分组柱状图学科相关性三科成绩的皮尔逊相关系数矩阵用热力图计算指标时直接利用pandas的分组聚合。比如男女各科均分对比一行groupby(gender)[score_cols].mean()就能拿到。但要注意分组后mean()默认会跳过空值如果你的异常值用的是NaN填充这里的分组结果就天然是合理的不用额外处理。# 男女各科均分对比 gender_mean df.groupby(gender)[score_cols].mean().T print(gender_mean) # 及格率分数 90 为及格 pass_rate (df[score_cols] 90).mean() * 100 print(pass_rate) # 相关性矩阵 corr df[score_cols].corr() print(corr)及格率这一行代码里有个小技巧(df[score_cols] 90)会得到一堆布尔值布尔值mean()直接就是比例不需要再绕一圈算count再相除。这个写法在计算各种比率时非常实用代码也干净。2.4 可视化呈现五张图讲清一个班指标算出来后我分别用seaborn和matplotlib画五张静态图。分科均分柱状图直接调df[score_cols].mean().plot(kindbar)在柱顶标注具体数值。这里有个细节matplotlib默认不支持中文负号一定要在画图前设置全局字体否则标题和坐标轴全是方块。import matplotlib.pyplot as plt # 设置中文字体解决乱码 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False # 总分分布直方图 核密度 import seaborn as sns sns.histplot(df[total_score], bins15, kdeTrue) plt.title(班级总分分布) plt.xlabel(总分) plt.ylabel(人数) plt.show()总分分布图是判断班级水平的直观工具。如果分布呈右偏说明高分学生较少大量学生挤在低分段教学重点应该放在补基础如果呈左偏说明头部学生多可以开展培优。KDE曲线叠加到直方图上以后分布形态一眼就能看出来这个组合非常推荐。相关性热力图我放在最后画因为它是结论性的图数学和物理高相关说明理科思维有共性语文和英语高相关说明语言能力有迁移。虽然学科成绩关联的原因很复杂不能直接下因果结论但热力图能提供一个讨论的起点让老师去关注数学好的学生是不是普遍英语也好这类规律。3. 微信好友社交画像挖掘数据获取与处理实战3.1 微信好友数据的获取方案与边界这部分是我在项目里花时间最多的。要注意一个现实微信官方对个人账号的接口控制很严格早年很多Python库可以通过扫码登录网页微信来获取好友信息但现在基本不可用了。我这次做的是技术演示性质的项目用经典的itchat库做示例但请在运行前明白它很可能无法直接在新版本微信上工作你需要找老版本客户端、模拟环境或者直接用一份符合格式的模拟数据来跑通后续的可视化流程。我的建议是把核心精力放在拿到数据之后怎么处理。因为不管数据是扫码获取还是mock生成后续的清洗、分词、统计、可视化逻辑是通用的。这才是值得学习的地方。# 这是典型的获取登录态后的信息读取方式仅作为技术示意 import itchat # 登录时会弹出二维码扫码后即可获取好友列表 itchat.auto_login(hotReloadTrue) friends itchat.get_friends(updateTrue) import json with open(friends.json, w, encodingutf-8) as f: json.dump(friends, f, ensure_asciiFalse, indent2)这里需要强调合规边界你只能分析自己账号下的好友字段数据不能去获取好友的好友也不能批量给好友打标签发消息。这类分析一旦转向利用数据做骚扰性营销性质就完全变了。我这次项目全部停留在描述性统计的层面用自己账号的数据画图、分词、做词云不涉及任何对外操作。3.2 字段解析能从好友列表挖出什么itchat.get_friends()返回的数据其实是微信在登录态下返回的好友元信息核心字段大概这些字段含义挖掘价值NickName好友昵称可做昵称文本分析、字数统计Sex性别代码1男2女0未知性别比例分布Province / City省份 / 城市好友地域分布画地图Signature个性签名分词后做词云挖掘兴趣与状态UID / UserName唯一标识去重、关联分析RemarkName备注名体现真实关系链可选这里的Sex字段有个细节微信的好友数据里Sex为0的通常代表未填写性别不一定是未知性别也可能是公众号或文件传输助手这类非真实好友。清洗时最好把Sex为0的记录单独拎出来看而不是直接删掉。3.3 数据清洗与文本预处理拿到friends列表之后先转成DataFrame然后执行四步清洗去掉自己列表第一条通常是登录账号本人、去掉公众号头像是非好友标识、UserName带开头的特殊标记、去掉重复昵称、处理缺失字段。import pandas as pd friends_df pd.DataFrame(friends) # 第一条是登录账号自己去掉 friends_df friends_df.iloc[1:].copy() # 去除可能的重复头像标记、公众号等特殊条目 friends_df friends_df[~friends_df[UserName].str.startswith(, naFalse)] # 重点字段缺失过滤 friends_df friends_df.dropna(subset[NickName])签名文本是最难处理的。个性签名里混着大量emoji、URL、换行符和空签名直接分词会得到一堆噪声。我的处理流程是先删掉非文字内容再做分词和去停用词。这里有个re正则的处理技巧可以一次性把URL、符号、emoji通过匹配常见emoji的Unicode区间或借助emoji库清理掉。import re import jieba def clean_signature(text): if not isinstance(text, str): return # 去掉URL text re.sub(rhttp\S|www\.\S, , text) # 去掉和多余空白 text re.sub(r\S, , text) text re.sub(r\s, , text).strip() # 去掉emoji try: import emoji text emoji.replace_emoji(text, ) except ImportError: pass return text friends_df[clean_signature] friends_df[Signature].apply(clean_signature)clean_signature这段代码是把非结构化数据转成可分析文本的关键。清洗完之后再用jieba对每条签名分词并过滤掉停用词表比如的了在这类词最后汇总成一个词频DataFrame供词云使用。4. 可视化呈现从画图到讲一个好故事4.1 学生分析的交互相式实现前面的静态图表适合放在报告里但要是班主任想自己点点看看交互式图表更合适。pyecharts生成了HTML文件可以直接在浏览器打开。我通常做三个交互图班级总分分布柱状图、男女各科均分对比图、学科相关性热力图。from pyecharts.charts import Bar from pyecharts import options as opts # 班级总分分布交互柱状图 hist_data df[total_score].value_counts().sort_index() bar Bar() bar.add_xaxis(hist_data.index.astype(str).tolist()) bar.add_yaxis(人数, hist_data.values.tolist()) bar.set_global_opts( title_optsopts.TitleOpts(title班级总分分布), xaxis_optsopts.AxisOpts(name总分), yaxis_optsopts.AxisOpts(name人数), ) bar.render(total_score_dist.html)pyecharts的好处是鼠标悬停能看到精确值、可以缩放、颜色可以配成品牌色系这些是静态图做不到的。但我的经验是交互图不要滥用一页PPT里放一两个就好太多反而让人不知道看哪里。静态图适合做深度分析交互图适合做展示汇报两者分工明确。4.2 好友画像性别、地域、签名三大图好友画像的可视化我做了三张图性别分布饼图、地域Top10条形图、签名关键词词云。性别分布用饼图最直观但要注意把Sex0的未知性别单独标出来不要并进未知一类就完事。在pyecharts里面饼图的标签格式化可以同时显示人数和百分比这一点比matplotlib默认效果好很多。地域分布要先做字段拆分。Province和City是分开的字段省份可以直接计数城市则需要拼接后计数。我有一个心得地域数据里常常混着未知和空值一定要在画图前过滤掉否则Top榜会被未知霸占图就废了。签名关键词词云是全项目效果最好也最出片的一张图。用清洗后的签名文本做jieba分词统计词频再喂给wordcloud生成词云。from wordcloud import WordCloud import matplotlib.pyplot as plt # 统计词频 word_freq {} for text in friends_df[clean_signature]: words jieba.lcut(text) for word in words: if len(word.strip()) 2: continue word_freq[word] word_freq.get(word, 0) 1 # 生成词云 wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, # 中文字体按需替换 background_colorwhite, width800, height600, max_words200, ).generate_from_frequencies(word_freq) plt.figure(figsize(10, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.show()这里有个极其容易踩的坑WordCloud默认字体不支持中文生成的全是方框。务必要指定font_pathWindows系统可以直接用simhei.ttfmacOS用/System/Library/Fonts/PingFang.ttc找不到字体就去网上下一个开源中文字体。4.3 可视化设计心得让图表不廉价做了这么多张图之后我对怎么把图做得不廉价有几点体会这是网上的教程很少教的。第一不要默认matplotlib的配色。seaborn的darkgrid风格和husl配色能让图表瞬间专业一个档次用set_theme一行搞定。pyecharts默认主题偏蓝绿色也可以换成white主题加自定义色系。第二标签信息密度要高但别拥挤。柱状图顶上的数值标签、图表标题里带结论性文字都能帮读者快速抓重点。但一张图里如果堆了超过六个系列信息量就过载了不如拆成多张子图。第三中文排版是一门学问。中文标签在坐标轴上很容易重叠尤其是bar图的横轴。解决方法是plt.xticks(rotation45)旋转标签并配合plt.tight_layout()自动调整布局否则中文字符串会让图表边缘被截断。第四保存图片时用dpi300否则嵌入PPT会糊。这个细节虽然小但决定了你交付的图表能不能直接上PPT。5. 常见问题与排查技巧实录5.1 问题速查表这次实战运行下来我把遇到过的典型问题整理成了一张速查表后面做类似项目时可以少走弯路现象可能原因解决办法图表中文全部变成方块未设置中文字体在代码开头设置rcParams[font.sans-serif]并指定字体路径坐标轴负号显示为方块缺少unicode负号配置设置axes.unicode_minusFalse数据透视/分组结果出现NaN原始数据有缺失值未处理先dropna或fillna再聚合mean()会跳过NaN但总数要确认friendship列无法访问或报错好友列表接口失效或返回值结构变化切换成本地JSON/CSV模拟数据后续处理逻辑不变词云全是空白停用词过滤太狠或分词结果为空调整停用词表保留部分有意义的单字词pandas读取Excel报错缺少openpyxl引擎pip install openpyxlpyecharts渲染不出图表浏览器环境或资源加载被拦截生成HTML后用浏览器直接打开文件路径5.2 我踩过的三个大坑第一个坑是在清洗前就画了第一张图。我当时拿到数据就急着看分布结果画出来一团乱才发现里面有大量异常值。之后我给自己定了一条铁律任何可视化之前必须先通过info()和describe()做数据体检。数据质量不过关再漂亮的图也是骗自己。第二个坑是性别比例被0值污染。微信好友里Sex字段为0的记录非常多如果你不单独清洗饼图上未知会占掉一大部分让男女比例完全失真。正确处理是先看Sex0的数量是否合理再把未知性别单列出来展示或者干脆在分析中排除并在结论里注明。第三个坑是jieba分词太粗糙。签名文本分词后经常出现人生现在这种大词词频很高但信息量低。我试过加一个自定义停用词表把所有超过五个字的大词都过滤掉词云才变得有辨识度。词汇清洗是个需要反复调参的过程不要指望一次到位。5.3 数据分析的隐形工作量做完这两个项目我最大的感受是数据可视化项目的隐形工作量至少有六成花在清洗和指标设计上真正画图只占四成。很多初学者觉得可视化就是调API画图实际上pandas的清洗逻辑、异常值的判断标准、指标的粒度定义才是决定一个项目质量的关键。比如学生总分分布坐标轴的bin宽度不同呈现出来的分布形态可能完全不同。bin太细显得杂乱bin太宽会掩盖尖峰。这里没有绝对正确的答案只能根据数据量反复调整。我会在histplot里传入bins15但看到分布形态不佳时改成12或20再对比直到故事最清楚为止。微信好友的地域分析也类似。省份字段有大把的未知、空值和重复值比如北京既可能出现在省份也可能出现在城市字段如果清洗逻辑不够细致Top榜就会失真。我的办法是统一先拼接省份和城市在拼接前去掉非内地省份的脏值再单独统计一次省份Top10和城市Top10两个维度分开讲。6. 一些真实感触与后续扩展建议这次项目做完我自己印象最深的一点是同样的数据换一个可视化角度讲出来的故事完全不同。学生成绩表用柱状图看的是整体排名用直方图看的是分布形态用相关性热力图看的是学科迁移规律。微信好友列表用饼图讲性别结构用地图讲地域网络用词云讲群体情绪。数据可视化不是把数字变成图的机械劳动而是不断追问这张图到底想让读者看到什么的思考过程。如果你也想照着做一遍我有一条很实际的建议先从模拟数据开始跑通整条流程再换真实数据。用模拟数据的好处是你知道正确答案在哪里清洗时能验证自己的判断对不对。真实数据一旦出了问题因为你不掌握真实分布排查起来会有很多盲区。后续想往深了扩展的话可以走这几个方向给好友数据加上备注名和标签信息做社群关系网络分析用NetworkX画关系图给学生成绩加上多次月考的时间维度做成绩趋势线把两个数据源结合比如分析好友中同班同学的比例或者好友昵称长度与活跃度的关系。这些都是从这次实战延伸出去的升级版玩法核心方法论没有变变的是你提问的角度。最后分享一个小技巧所有分析脚本都应该加一个开关控制是否输出中间清洗结果。比如清洗完每个步骤后打印前5行方便随时检查数据变化。这个习惯帮我省下了大量排错时间你以后做更大的项目时也会体会到它的价值。