本文作者:张祎帆 河南大学经济学院
本文编辑:梁莹
技术总编:兰博文
Stata and Python 数据分析
爬虫俱乐部Stata数据处理与实证研究实战、Python基础编程与文本分析进阶课程可在小鹅通平台查看,欢迎大家多多支持订阅!如需了解详情,可以通过课程链接(https://appbqiqpzi66527.h5.xiaoeknow.com/homepage/10)或课程二维码进行访问哦~




大家在做实证研究或者写课程论文时,是不是经常遇到这种头疼的情况,你手头有一堆样本(比如 200 个城市或者 3000 家上市公司),为了做异质性分析,你需要把它们分成几组。最传统的做法是按中位数切一刀,分成“高组”和“低组”;或者按行政等级,分成省会和非省会。但导师可能会问你:为什么这么分?依据充分吗?‘高’和‘低’的界限真的就在中位数那里吗?如果你想让分组更客观,或者想探索数据内部自然的结构,K-Means 聚类算法就是你必须要掌握的利器。
今天我们不谈枯燥的数学推导,不玩那几朵鸢尾花,直接上一个真实的区域经济场景,带大家用 Python 跑一遍聚类分析。



简单来说,K-Means 就是一种“物以类聚,人以群分”的无监督学习算法。想象一下,操场上站了 100 个人,你想把他们分成 3 组。K-Means 的逻辑是这样的:
随机点将:先在操场上随便插 3 面旗子(质心)。
各自站队:每个人看看离哪面旗子最近,就加入那个队伍。
更新中心:队伍站好后,大家算出这个队伍的“中心位置”,把旗子挪到那个中心点去。
循环往复:重复第 2 和第 3 步,直到旗子不再移动为止。
最终,数据自动抱团,你也就得到了分类结果。



为了让大家更有体感,我们模拟一个区域经济研究的场景。假设我们要研究某省份的 15 个城市,手头有两个核心指标:人均 GDP:代表经济发展的“硬实力”。第三产业占比:代表经济结构的“软实力”或现代化程度。我们的目标是:不带预设偏见,看看这些城市自动能聚成几类。


打开你的 Jupyter Notebook,我们先造点数据(实际研究中直接 pd.read_excel 即可)。
import pandas as pdimport numpy as npimport matplotlib.pyplot as pltfrom matplotlib.font_manager import FontPropertiesfrom sklearn.cluster import KMeansfrom sklearn.preprocessing import StandardScaler# 设置随机种子,保证结果可复现np.random.seed(42)chinese_font = FontProperties(fname='C:/Windows/Fonts/msyh.ttc')times_font = FontProperties(fname='C:/Windows/Fonts/times.ttf')# 模拟 15 个城市的数据data = {'城市': [f'城市{i}' for i in range(1, 16)],# 人均GDP:范围 3万-12万'人均GDP': [32000, 35000, 85000, 110000, 42000, 95000, 115000, 31000,60000, 62000, 105000, 29000, 88000, 45000, 58000],# 第三产业占比:范围 30%-65%'第三产业占比': [0.32, 0.35, 0.55, 0.62, 0.38, 0.58, 0.65, 0.30,0.45, 0.48, 0.60, 0.31, 0.53, 0.40, 0.46]}df = pd.DataFrame(data)print(df.head())



请注意:人均 GDP 是“万”级别的数字,而第三产业占比是 0 到 1 之间的小数。如果我们直接算距离,K-Means 会完全被 GDP 主导,忽略掉产业结构。所以,必须把它们拉到同一起跑线上(标准化)。
features = df[['人均GDP', '第三产业占比']]scaler = StandardScaler()features_scaled = scaler.fit_transform(features)# 看看标准化后的前 5 行print(features_scaled[:5])



sse = [] # 存放误差平方和k_range = range(1, 10)for k in k_range:kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)kmeans.fit(features_scaled)sse.append(kmeans.inertia_) # inertia_ 就是 SSE# 绘图plt.figure(figsize=(8, 5))plt.plot(k_range, sse, marker='o')plt.xlabel('聚类数量 K', fontproperties=chinese_font)plt.ylabel('误差平方和 SSE', fontproperties=chinese_font)plt.title('手肘法确定最佳K值', fontproperties=chinese_font)plt.xticks(fontproperties=times_font)plt.yticks(fontproperties=times_font)plt.show()



选定 K=3,我们来看看结果。
# 建立模型kmeans_final = KMeans(n_clusters=3, random_state=42, n_init=10)# 训练并打标签df['聚类标签'] = kmeans_final.fit_predict(features_scaled)# 为了方便展示,我们将结果可视化plt.figure(figsize=(10, 6))# 根据各组人均 GDP 均值自动命名,避免把 Cluster 编号误当成等级顺序cluster_mean_gdp = df.groupby('聚类标签')['人均GDP'].mean().sort_values()cluster_names = {cluster_mean_gdp.index[0]: '欠发达组',cluster_mean_gdp.index[1]: '过渡组',cluster_mean_gdp.index[2]: '发达组'}cluster_colors = {cluster_mean_gdp.index[0]: 'green',cluster_mean_gdp.index[1]: 'blue',cluster_mean_gdp.index[2]: 'red'}for label in cluster_mean_gdp.index[::-1]:temp = df[df['聚类标签'] == label]plt.scatter(temp['人均GDP'], temp['第三产业占比'],s=100, c=cluster_colors[label],label=f'Cluster {label}:{cluster_names[label]}')# 将质心反标准化回原始量纲,方便和散点放在同一张图里理解centers = scaler.inverse_transform(kmeans_final.cluster_centers_)plt.scatter(centers[:, 0], centers[:, 1],s=220, c='black', marker='X', label='质心')plt.title('城市经济聚类结果', fontproperties=chinese_font)plt.xlabel('人均GDP', fontproperties=chinese_font)plt.ylabel('第三产业占比', fontproperties=chinese_font)plt.xticks(fontproperties=times_font)plt.yticks(fontproperties=times_font)plt.legend(prop=chinese_font)plt.grid(True, alpha=0.3)plt.show()# 查看分组的均值,理解每一组的含义print(df.groupby('聚类标签')[['人均GDP', '第三产业占比']].mean())

聚类结果可视化如图所示,可以看到城市被清晰地分成了三堆,黑色 X 表示各组质心。



跑完代码不是结束,解释结果才是经济学研究的核心。根据输出的均值结果,我们可以这样定义:

这里要特别注意:Cluster 0、Cluster 1、Cluster 2 只是算法自动生成的标签,并不天然代表“第一名、第二名、第三名”。真正的经济含义,要结合每组的人均 GDP 和第三产业占比均值来命名。
Cluster 2 (欠发达组):人均 GDP 低(约3.2万),第三产业占比低(约32%)。这可能是处于工业化初期或农业占比大的城市,后续政策重点通常不是盲目上马高端服务业,而是先补基础设施、就业承载能力和公共服务短板。
Cluster 1 (中等发达/过渡组):人均 GDP 中等(约5.3万),第三产业占比接近43%。这部分城市正在经历结构转型,既有一定产业基础,也还没有完全进入服务业主导阶段,适合重点观察制造业升级、生产性服务业培育和人口吸引力变化。
Cluster 0 (发达组):人均 GDP 高(约10万),第三产业占比接近59%。这是典型的服务业驱动型中心城市,往往承担金融、信息、商务服务、科教文卫等高端功能,对周边城市也可能产生辐射带动作用。
你看,不需要人为划定界限,数据自己告诉了我们这三个梯队的存在。



K-Means 能干的事儿远不止给城市分级,在我们的研究中还有很多妙用:
股票风格分类: 用“收益率波动率”和“换手率”做聚类,可以把股票分为“稳健白马股”、“高频投机股”等,构建投资组合时避免风格过于单一。
区域收敛性检验: 在做经济增长收敛性研究时,全样本可能不收敛,但这并不代表地区之间完全没有规律。可以先用 K-Means 按初始收入、产业结构、创新投入等变量识别“俱乐部”,再分别检验组内是否收敛。这样得到的结论会更细:不是所有城市都奔向同一个稳态,而是相似基础条件的城市更可能向同一发展水平靠拢。
异质性分析的分组依据: 当你做回归分析时,如果审稿人质疑你分组的随意性,你可以理直气壮地说:“本文采用 K-Means 聚类算法,根据企业财务指标的内在结构特征进行客观分组。”例如研究数字金融对企业创新的影响时,可以先用企业规模、资产负债率、现金流和研发强度聚类,再比较不同类型企业的政策敏感度。
城市画像与政策分层: 政府做区域规划时,不同城市不应该套同一套政策。可以把人均 GDP、人口净流入、财政收入、产业占比、公共服务水平放在一起聚类,识别“中心带动型”“工业支撑型”“人口流出型”等类型,再分别设计招商、交通、教育和产业扶持策略。
消费群体细分: 企业做市场研究时,也可以把消费者的收入、消费频率、客单价、品类偏好等指标放入 K-Means。聚类后不只是得到“高消费/低消费”这么粗的标签,还能区分“高频低价型”“低频高价型”“价格敏感型”等群体,为定价、促销和会员运营提供依据。
论文稳健性补充: 如果正文已经使用中位数分组,也可以把 K-Means 作为补充检验:先让算法根据多个指标重新分组,再看核心回归结论是否仍然成立。若两种分组方式得到的方向一致,文章的说服力会更强。



最后,给各位同门提两点注意事项:
异常值敏感:K-Means 是根据均值来计算中心的,一个极端的离群值可能会把中心拉偏。如果数据里有特别离谱的极端值,记得先处理掉,或者改用对异常值不敏感的 K-Medoids 算法。
变量量纲:再次强调,一定要标准化!一定要标准化! 不要拿元和百分比直接混在一起算距离。
声明:代码仅供学习使用,请勿用做任何商业行为!
重磅福利!为了更好地服务各位同学的研究,爬虫俱乐部将在小鹅通平台上持续提供金融研究所需要的各类指标,包括上市公司十大股东、股价崩盘、投资效率、融资约束、企业避税、分析师跟踪、净资产收益率、资产回报率、国际四大审计、托宾Q值、第一大股东持股比例、账面市值比、沪深A股上市公司研究常用控制变量等一系列深加工数据,基于各交易所信息披露的数据利用Stata在实现数据实时更新的同时还将不断上线更多的数据指标。我们以最前沿的数据处理技术、最好的服务质量、最大的诚意望能助力大家的研究工作!相关数据链接,请大家访问:(https://appbqiqpzi66527.h5.xiaoeknow.com/homepage/10)或扫描二维码:
对我们的推文累计打赏超过1000元,我们即可给您开具发票,发票类别为“咨询费”。用心做事,不负您的支持!
往期推文推荐 Stata封神命令——cnborder Python实战:使用PuLP库解决线性规划问题 Stata入门:statsby命令详解—分组统计的“效率神器” Python实战:DrissionPage 一键查询商品历史价格 Stata中codebook命令介绍及应用
Stata绘制热力图 自动化报告生成:sum2docx与reg2docx深度应用指南 Python库:【Tableau】气候数据可视化 Stata | 从字节串到Unicode:Stata ustrfrom使用手册
Seminar|CEO言行不一,审计师如何“明察秋毫”?——一项关于诚信、审计与公司治理的深度研究
《赌神》 里的发哥能赢赌场?蒙特卡洛模拟:现实里“赌神”赢不了这5.26%”
Python | 别让Emoji毁了你的模型!Python机器学习中的Emoji “神翻译”指南 Stata爬虫——我的数据去哪里了?
python爬虫 | 获取港股基本信息
Stata入门:tempvar命令与tempfile命令详解
Python绘图:用matplotlib库绘制好看的折线图
Seminar | 注意力独特性与企业绩效:增长行动的中介作用
Stata | 从sum2docx到reg2docx——基于《中国工业经济》期刊文章的结果输出 Seminar | 邻避效应:内在动机与企业污染治理
Stata爬取豆瓣读书,一键获取你的读书清单
Seminar | 自动化对企业报告质量的影响
识别处理重复值duplicates命令
关于我们 微信公众号“Stata and Python数据分析”分享实用的Stata、Python等软件的数据处理知识,欢迎转载、打赏。我们是由李春涛教授领导下的研究生及本科生组成的大数据处理和分析团队。
我们团队一直为广大用户提供数据采集和分析的服务工作,如果您有这方面的需求,请发邮件到statatraining@163.com。
此外,欢迎大家踊跃投稿,介绍一些关于Stata和Python的数据处理和分析技巧。
投稿邮箱:statatraining@163.com投稿要求:1)必须原创,禁止抄袭;2)必须准确,详细,有例子,有截图;注意事项:1)所有投稿都会经过本公众号运营团队成员的审核,审核通过才可录用,一经录用,会在该推文里为作者署名,并有赏金分成。 2)邮件请注明投稿,邮件名称为“投稿+推文名称”。3)应广大读者要求,现开通有偿问答服务,如果大家遇到有关数据处理、分析等问题,可以在公众号中提出,只需支付少量赏金,我们会在后期的推文里给予解答。