理想点距离与TOPSIS:78年时间序列综合评价的Python实现与避坑指南
简介面向国际关系与政治学研究的理想点距离数据集1946—2023年可用于分析联合国投票立场演变、国家偏好动态等议题。压缩包内含7个文件涵盖2个CSV表格、Stata格式DTA、R语言Rdata数据文件方便不同统计工具直接读取另附PDF方法论文档、DOCX代码手册及HTML数据来源说明便于理解构建原理与变量含义。整体包体约46.95MB文件结构简洁适合从事定量国际政治研究的高年级本科生、研究生及科研人员使用。目前已有59人学习下载。通过这份资料使用者能直接获得经过整理的理想点估计值、国家间协议得分等核心数据并借助配套文档快速掌握数据生成过程与引用方式省去从原始投票记录中自行估算的繁重步骤。1. 理想点距离数据包78年时间序列直接拿来能做什么如果只看文件名你可能会以为这是一个普通的地理坐标数据——实际上它是一份跨越1946到2023年、共78个年度的理想点距离计算结果。这类数据在多属性评价里特别常用把每一个年份当成一个方案用若干指标算出它离“最好年份”和“最差年份”的距离距离越小说明综合表现越接近理想状态。我刚拿到手时也犯过迷糊以为理想点距离越大越好跑完排序才发现方向搞反了。这个zip适合做时间序列综合评价、科研数据复现或者给论文补一张趋势图的人。但要想用对得先弄清背后的归一化、逆指标和权重处理否则很容易把序数当分数用错。2. 理想点距离的计算逻辑公式、归一化与权重怎么设这一章要把计算逻辑讲清楚因为zip里给出的数字只是结果如果你不知道结果怎么来的就不知道哪些年份的差异是数据带来的哪些是方法带来的。理想点距离这个概念来自TOPSIS英文全称是Technique for Order Preference by Similarity to Ideal Solution中文常译为“逼近理想解的排序法”。它的核心思想很直接最好的方案应该离正理想点最近、离负理想点最远。下面从公式、距离度量、归一化和权重四个角度拆开讲。2.1 从“正负理想点”到D和D-一个两指标手算示例TOPSIS把每个年份看成高维空间中的一个点。正理想点由所有指标的最优值组成负理想点由所有指标的最差值组成。为了不空谈公式我拿一个两指标的手算例子演示。假设只有1946、1947、1948三个年份指标1是“人均产出”越大越好指标2是“单位产出能耗”越小越好原始数据如下年份指标1指标219460.20.819470.50.519480.90.3指标2是成本型指标必须逆向。常用做法是先做极差归一化再用1减去归一化值。指标2的min0.3max0.8所以1946年归一化后是(0.8-0.3)/(0.8-0.3)1逆向后是01947年是0.4逆向后是0.61948年是0逆向后是1。指标1的归一化分别是0、0.43、1。于是得到处理后的矩阵年份R指标1R指标2逆向后19460019470.430.6194811正理想点A(1, 1)负理想点A-(0, 0)。用欧氏距离权重都取1计算每个年份到正负理想点的距离D(1946)sqrt((0-1)^2(0-1)^2)1.414D-(1946)sqrt(00)0所以C0/(1.4140)0。D(1947)sqrt((0.43-1)^2(0.6-1)^2)0.696D-(1947)sqrt(0.43^20.6^2)0.738C0.738/(0.6960.738)0.515。D(1948)0D-(1948)1.414C1。这个手算过程说明了三件事第一D越小说明越接近正理想点D-越大说明越远离负理想点第二C把两个距离合成一个0到1之间的数值C越大排名越靠前第三成本型指标不逆向会让整个排序反向。资源里有78年数据但计算逻辑和这个两指标示例完全一致。2.2 距离度量为什么欧氏距离是默认选型TOPSIS标准实现里距离度量通常用欧氏距离也就是平方和再开方。有人会问能不能用曼哈顿距离答案是可以但很少见。欧氏距离和曼哈顿距离的差别在于曼哈顿距离对每个维度的差异做线性累积欧氏距离对差异先平方再求和因此欧氏距离对某一维度上的极端差异更敏感。在综合评价里这种“敏感”是有价值的——它强调方案需要全面接近理想点而不允许用某一个维度的优异去掩盖另一个维度的严重短板。如果指标之间相关性很强欧氏距离会重复计算重叠信息这时建议先做主成分降维或者用相关系数高的一组指标中选一个代表。常见做法是跑一个相关矩阵看到绝对值超过0.8的指标对就删除其中一个。对于这个zip里的78年时间序列我一般先看指标数如果超过15个就考虑降维但这也意味着计算结果会偏离原始资源里的数值所以做对比时要用同一套流程。2.3 归一化方法极差法还是向量法结果差多少归一化是整个计算里最影响最终数值的一步。常用的极差法和向量法各有侧重用一张表对比方法公式特点适用极差法(x - min) / (max - min)结果严格在0-1之间保留相对位置指标量纲差异大关注历史极值向量法x / sqrt(sum(x^2))结果受量纲影响数值大的列占主导指标同单位且本身可比资源里如果没有说明用哪种归一化我建议默认用极差法。原因是时间序列分析更关心“今年离历史最好水平差多少”极差法的解释很自然。但极差法有一个明显毛病如果某一年出现极端离群值max会被拉得很大其余年份被压缩在一小块区域里。遇到这种情况先对原始指标做对数变换再归一化通常能恢复区分度。向量法在截面数据横向对比时更常用因为每个指标的向量长度本身包含量纲信息。但在跨年时间序列里一个指标如果在几十年间从几十增长到几千向量法会让这个指标完全支配距离计算其他指标几乎不起作用。我见过有人用向量法跑出来C值常年稳定在0.5附近原因就是量纲大的指标主导了结果。2.4 权重怎么给默认等权 vs 熵权与敏感性检查权重是理想点距离的“黑匣子”。资源如果没有说明权重先跑一次等权版本作为基准。等权版本最容易向论文评阅人和协作同事解释也是后续调权重的锚点。如果你想突出某个指标可以用熵权法从数据里算客观权重。熵权法的逻辑是某个指标在各年份上的离散程度越大携带的信息越多权重越高离散程度越小权重越低。对于78年数据长期稳定不变的指标会被熵权法自动降权这是好事。无论用何种方式确定权重都必须满足非负且和为1。权重向量与指标列名的顺序要一一对应否则计算结果会被某个指标意外放大。我在跑数据时还发现一个实用习惯做完等权结果后把其中一个权重上下浮动20%观察排序变化。如果某个权重稍微改变就让C排名大幅变动说明这个指标与其他指标高度相关不能同时赋予过高权重。3. 把 zip 里的数据跑成结果Python 重算与参数核对拿到zip后不要直接相信里面的结果。我的习惯是先解压用Python把原始指标重算一遍核对文件里的数值是否与我自己的脚本输出一致。这样既能验证数据包有没有损坏也能确认计算方法。这一章给出完整的重算流程。3.1 读取前先检查数据列名、缺失与连续年份解压后第一件事不是算距离而是正确读取数据。我通常先把文件放进同一工作目录然后用pandas读进来打印形状、前几行和缺失统计import pandas as pd df pd.read_csv(period_data.csv, encodingutf-8) print(df.shape) print(df.head()) print(df.isnull().sum()) print(df[year].min(), df[year].max())如果文件是gbk编码把encoding改成gbk。如果发现某些列缺失值超过5%需要决定删除还是插补。对于年度数据我倾向用线性插值因为相邻年份关联性强用前值填充会让某几个年份出现平台期在计算距离时产生虚假的“稳定”。如果年份列不连续比如1946到1948之间缺了1947要先补上这个索引或者明确说明样本期间存在断档否则折线图会画出跨越缺失年份的直线让人误以为那几年的C值真有变化。3.2 计算 D、D- 和接近度 C 的完整脚本下面是我常用的完整计算函数使用极差归一化成本型指标会自动逆向。代码里每一步都有注释可以直接复制改列名import numpy as np import pandas as pd def ideal_distance(df, year_col, cols, direction, weights): 计算理想点距离和接近度 df: 包含年份和指标列的DataFrame year_col: 年份列名 cols: 参与计算的指标列名列表 direction: 与cols对应max表示越大越好min表示越小越好 weights: 与cols对应非负且和为1 norm pd.DataFrame(indexdf.index) for col, d, w in zip(cols, direction, weights): minv df[col].min() maxv df[col].max() if maxv minv: # 全常数列无法提供区分度直接给0 norm[col] 0 continue normalized (df[col] - minv) / (maxv - minv) if d min: normalized 1 - normalized norm[col] normalized * w ideal_best norm[cols].max() ideal_worst norm[cols].min() d_plus np.sqrt(((norm[cols] - ideal_best) ** 2).sum(axis1)) d_minus np.sqrt(((norm[cols] - ideal_worst) ** 2).sum(axis1)) c d_minus / (d_plus d_minus) result pd.DataFrame({ year_col: df[year_col], D: d_plus, D-: d_minus, C: c }) return result调用示例result ideal_distance( df, year_colyear, cols[gdp_per_capita, energy_intensity, education_index, unemployment_rate], direction[max, min, max, min], weights[0.25, 0.25, 0.25, 0.25] ) print(result.head())逻辑说明代码先把每个指标按极差法归一化成本型指标用1 - normalized处理再把权重乘到归一化值上。这样后面算距离时不需要再乘权重因为权重已经嵌入矩阵。理想点取的是加权后每一列的最大最小值因此C值受权重影响非常直接。这就是为什么权重顺序一旦错结果立刻失真。参数说明direction和weights必须与cols顺序一致。权重列表中每个值代表对应指标在综合距离中的相对重要性它们必须非负且和为1。如果四个指标都取0.25总权重是1C值落在0到1之间如果某个指标取0.5其他取值就要相应缩小否则总权重超过1C值范围会被压缩甚至出现负值。全常数列在这里被赋0因为一个毫无变化的指标对排名没有任何贡献保留它只会让距离计算多出一个无意义维度。3.3 指标方向处理成本型指标逆向后才参与计算这个坑我几乎每次都提醒别人。很多用户拿到数据直接跑脚本发现结果里“能耗最少”的年份C值反而低原因是他们没有把能耗这类成本型指标逆向。在我们的函数里direction参数传入“min”后代码会自动执行1 - normalized把“越小越好”转成“越大越好”。如果你用的不是这个脚本一定要确认你手里那份实现有没有处理方向问题。方向处理的影响有多大拿刚才手算例子看如果指标2不逆向正理想点会变成(1, 0.8)负理想点变成(0, 0.3)计算出的C会把指标1和指标2的语义完全搞反。在78年数据里如果一个指标是“单位GDP能耗”或“失业率”不逆向的后果就是排名与业务直觉彻底相反。最简单的验证方法单独把某个年份的指标值调到一个明显“好”的方向看C是否上升。如果不变或者下降方向处理一定有问题。4. 时间序列的正确打开方式固定基准、趋势图与排序验证算出了C值并不代表可以直接画折线图下结论。因为时间是连续变量理想点距离的计算基准不同结果含义就完全不同。这一章讲清楚固定基准和逐年重算的区别以及如何用图表读趋势、验证排序稳定性。4.1 固定基准年的理想点跨年比较的稳定参照固定基准年是指在计算归一化时只使用基准期内比如1946-1965年的min和max然后用这个基准去归一化后续所有年份。这样做的好处是基准极值一旦定下来之后每一年的C值都代表“相对于基准期最好综合水平的接近程度”可以跨年直接比较。如果你要回答“2023年相比1950年进步了多少”就必须用固定基准。代码很简单只需把min和max的计算限定在基准期内base df[df[year].between(1946, 1965)] minv base[cols].min() maxv base[cols].max() norm_all (df[cols] - minv) / (maxv - minv)后续用这个norm_all替换函数里的归一化步骤。注意如果后续年份的指标值比基准期最小值还小归一化后会变成负数比最大值还大则超过1。这是正常现象不需要截断。有人看到1948年某个指标出现负值以为是数据错误其实那意味着该指标已经跌破了基准期的最差水平。基准期的选择会影响结论。1946年战后数据波动大如果拿它做基准可能把正常年份都压缩到高分段。我一般会避开明显异常年份比如用1950-1965年作为基准窗口。但这只是个人习惯如果你要对比不同基准期的影响可以随机构造几个窗口分别跑一遍看C的整体走势是否一致。4.2 逐年重算理想点能看相对变化但会“飘”逐年重算是每年单独用当年的min和max归一化再计算C。这样每年C值代表的都是“在当年内部谁最接近这一年的完美状态”适合观察当年各指标的结构差距但跨年C值没有可比性。原因很简单1946年整体水平低但它当年的相对最好年份C可以接近12023年整体水平高但如果优秀年份多每个年份的C反而会分散。这就导致逐年重算的C序列看起来没有固定基准那么有趋势而且会出现“飘”的感觉也就是某个年份没有政策变化但C值突然跳变。我的习惯是两种都跑一遍。固定基准结果画主趋势图逐年重算结果做辅助排序图。如果两种方法得出的排名差异很大说明指标体系中有某个指标在不同时间段的极值变化过大检查是否需要对数变换或改用稳健归一化。数据包里的年份跨度长达78年经济、技术、环境指标的量级变化可能很剧烈不处理量纲差异逐年重算的结果会很不稳定。4.3 折线图与热力图综合趋势和单一指标归因C值本身只是一条折线要解释趋势拐点需要回到子指标层面。折线图用matplotlibimport matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(result[year], result[C], markero, linewidth1) plt.xlabel(year) plt.ylabel(C) plt.title(Ideal point closeness from 1946 to 2023) plt.grid(True) plt.show()这条折线能看出整体上升、下降或波动的区间。但C是一个综合数某个年份的下跌可能只是某一个指标的异常。这时候画热力图更直观。先把每个年份、每个指标的归一化值整理成透视表再用seaborn画pivot df.pivot_table(indexyear, columnsindicator, valuesnormalized_value) sns.heatmap(pivot, cmapRdYlGn, yticklabels10) plt.show()热力图上的颜色可以快速定位某一年特别暗或者特别亮往往就是拉低该年C值的元凶。我在复现资源数据时就用这种方式定位出某两个年份的异常进而发现是原始数据录入错误。4.4 接近度不是“得分”C 只适合排序和趋势这一点几乎每个做评价的人都会问C值越大是不是分数越高严格来说不是。C是一个相对排序值它的绝对大小取决于参与比较的年份集合和指标集合。如果你单独删掉两个年份剩下年份的C值就会全部变化。所以C更适合做排序和趋势而不是做绝对水平的评分。比如资源里的1946-2023年C值你不能说“1970年综合评分80分”只能说“1970年在同一套指标和基准下比1965年更接近理想点”。如果论文需要给一个“分数”建议另外做一个线性加权综合得分然后用C做稳健性验证。一个常用的验证方法是计算综合得分排序和C排序的Spearman相关系数如果相关系数低于0.8说明两种方法结果不一致需要重新检查指标权重或者归一化方式。C值只告诉你“离理想点远近”不告诉你“值不值这么远”。5. 避坑指南理想点距离计算与使用的五个常见问题这一章写五个我踩过的坑按处理阶段分两组。每条都是“现象-原因-解决”的结构希望你不用再走一遍弯路。5.1 数据预处理阶段的坑缺失、全常数列与编码坑1跑出来的结果全是NaN。现象D和C列出现NaN或者部分年份没有结果。原因原始数据有缺失值某指标列全为常数导致归一化分母为0或者列名不匹配导致索引错位。解决打印df.info()和df.isnull().sum()先处理缺失删除全常数列或者在代码中判断maxvminv时直接赋0核对cols里的列名是否都在df.columns里有拼写差异用df.rename统一。坑2读取CSV时报UnicodeDecodeError。现象pandas读文件时抛编码错误一直读不进去。原因zip里文件可能是gbk编码尤其从Excel另存的CSV很常见。解决用encodinggbk读取或者先用小工具自动检测编码。这个坑不是理想点距离特有的但卡在这一步会浪费很多时间。5.2 计算与比较阶段的坑方向、权重和基准坑3成本型指标没逆向。现象C排序和你直觉相反比如“单位能耗最低”的年份反而排名靠后。原因方向参数错用为max正理想点其实是“最耗能”年份。解决在direction列表里标记min并在归一化后执行1 - norm。跑完第一版后随机挑一个年份把一个成本型指标改成明显更优的值看C是否上升如果不升方向一定错了。坑4权重加和不等于1。现象C值范围不在0-1之间甚至出现负数。原因weights列表加和不是1距离被放大或压缩。解决每次计算前加assert abs(sum(weights) - 1) 1e-9强制校验。等权时直接把权重列表写成[1/n]*n而不是在调用时临时手写减少漏改概率。坑5跨年比较用了逐年理想点。现象C序列年年跳变没有趋势或者在某个时间点突然断崖。原因用的是逐年重算的C归一化基准每年都变跨年比较没有意义。解决改用固定基准年。如果对基准期的选择不放心就把基准期分别设为1946-1965、1950-1965、1955-1970跑出三条C序列看走势是否一致。如果三条线差异巨大说明数据中有极端离群值主导了基准需要先做平滑或对数变换。6. 从数据包到结论一个三年滚动平均的稳健性验证技巧最后讲一个我每次跑完C序列都会执行的验证技巧。78年的数据即使用了固定基准也可能因为个别年份的数据异常出现一次性的尖峰或低谷。在做趋势判断之前我会先对C序列做三年滚动平均用平滑后的曲线看大趋势。代码很简单c_series result.set_index(year)[C] c_roll c_series.rolling(window3, min_periods2, centerTrue).mean() result[C_roll] c_rollrolling是pandas里的滚动窗口函数window3表示用前后各一年加上当年共三年的平均值min_periods2表示窗口内至少有两个有效值就算均值这样1946年开头不会因为窗口不足而出现NaN。centerTrue会让滚动均值对齐到窗口中心避免整条曲线向后平移三年。我会把原始C和C_roll画在同一张图上如果两条线的排序几乎一致说明趋势是稳健的如果某几个关键年份在平滑后排名发生变化就要回头检查这几个年份的原始指标有没有录入错误。一次我在复现某组数据时发现1973年前后出现一个异常尖峰查下来是当年某个指标缺了一条数据被我用前值填充导致极差法失真。改成线性插补后尖峰消失趋势恢复平滑。从那以后我每次跑完理想点距离序列都会强制执行一遍滚动平均验证再下结论。这个习惯帮我拦截了好几次错误的“发现趋势”希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →