跳到主要内容 Python 复现 FactSet Revere 供应链断裂与重构指标测度 | 极客日志
Python AI 算法
Python 复现 FactSet Revere 供应链断裂与重构指标测度 基于丁浩员等发表于《经济研究》的跨国供应链断裂与重构研究,使用 Python 和 FactSet Revere 数据库进行复现。文章解读了断裂、恢复及转移指标的测度逻辑,提供了从原始数据筛选、沪深 A 股企业标记、时间顺序调整到月度数据生成的完整代码实现。重点解决了供应链关系的时间连贯性处理及变量计算细节,为相关实证研究提供技术参考。
片刻 发布于 2026/3/30 更新于 2026/4/17 9 浏览1 对 FactSet 全球供应链数据库的简单解读
数据库获取渠道:正规来讲是去 WRDS 官网购买,也可以寻找其他数据源。
可以获取到的数据包括 company.dta、relations.dta 两个数据文件,前者为公司在各个时间段的信息,后者为各个时间段的企业关系信息(含供应关系),还有一系列变量含义的说明文档。
结合数据库说明文档,在此对一些比较重要的变量进行解读:
首先是 company.dta,有七个变量比较重要:
第一处,start_和 end_,公司信息的时间段数据。另外,end_可能会出现 01jan4000 的情况,一般默认成至今即可。
第二处,id,企业在该套数据库中的唯一标识符,用于与 relations.dta 数据库进行匹配。
第三处,ticker,如果该企业是上市公司,这里就有上市企业代码,中国沪深 A 股上市企业就是六位的代码,注意这里没法显示深交所代码前边的 0,需要自行补齐后,与年份结合就可以与国泰安数据库进行匹配。
第四处,cusip 和 isin,两种国际通用的唯一标识符,北美地区为 cusip,其他为 isin,用于与其他外国数据库里的控制变量进行匹配。
第五处,country,公司所处地区(注意不要在文章里解读为国家,因为数据库里边还有 TW、HK、MO)。
再就是 relations.dta,有九个变量比较重要:
第一处,供应链关系信息的时间段数据,表示公司在 start_至 end_期间的某些特征属性。
第二处,供应链关系 rel_type,有供应商(supplier)、客户(customer)、竞争者、合作者等,前两类是对供应链研究有用的数据。该变量表示 target 企业是 source 企业的 rel_type。
第三处,source 企业和 target 企业各自的 id,本数据库中的唯一标识符,可以与 company.dta 数据进行匹配。
第四处,source 企业和 target 企业各自的 isin、cusip,同上。
2 对丁浩员等测度方式的解读
2.1 断裂和恢复指标的解读
首先要明确,丁浩员等的数据是月度频次的,即以'供应链关系 - 月份'为数据单元,注意与常见的'企业 - 年份'数据单元作出区分。举例来说,如果一个供应链关系对在 2019 年 1 月至 2019 年 6 月存在,那么这个供应链关系在生成的数据中就应该有六条数据。
关于断裂的测量,文章只有短短的两行话,并没有说得很详细。
以我的理解,如果同一个供应链关系对在 2019 年 1 月至 6 月存在,7 月至 9 月不存在,10 月至 12 月存在,那么 2019 年 1 月至 5 月、10 月至 11 月,Break 变量为 0,代表未发生断裂行为。2019 年 6 月和 2019 年 12 月,Break 为 1,代表发生断裂行为。而 2019 年 7 月至 9 月,该供应链对无数据(这是我无法确定的,这段时间标 0 和标 1 感觉都不合适,我就当做该期间数据不存在)。
恢复指标的测量比较好理解,还是上边的例子,同一个供应链关系,在 2019 年 6 月断裂后又在 2019 年 10 月恢复,从论文注释中可以看出,他们是在 2019 年 6 月的 Recover 标'1'。
而我其实是希望更好的捕捉到供应链恢复这个行为,想将 2019 年 10 月的 Recover 标记为'1'。因此,我把两种标注的代码都在下文展示,注意看代码中的注释提示。
2.2 转移指标的解读
关于重构中转移的变量感觉可能争议比较大,在此我只表明我对文章的理解和猜测,同样并不代表作者本来的做法。
如果一家中国供应商,在整个数据库中有多个不同的外国客户,以与客户彻底断裂时间,注意是彻底的断裂,即不再出现恢复情况的最早时间 T 为基准,若与其他客户构建供应链关系的时间晚于 T,则将其构建新供应链的时间的 Transfer 标记为 1。例如:
某中国供应商在数据库中有且只有 5 个外国客户,分别为 A、B、C、D、E
与外国客户 A 在 2019 年 1 月至 2 月和 2019 年 10 月至 12 月存在供应链关系
与外国客户 B 在 2019 年 3 月至 5 月存在供应链关系
与外国客户 C 在 2019 年 4 月至 7 月存在供应链关系
与外国客户 D 在 2019 年 6 月至 8 月存在供应链关系
与外国客户 E 在 2019 年 9 月至 10 月存在供应链关系
那么最早彻底断链时间为 2019 年 5 月,则在 D 的 6 月份 Transfer 标 1,E 的 9 月份 Transfer 标 1,其他为 0。当然这种做法也存在 bug,会把后续构建的新供应链,都认为是第一个彻底断裂的客户转移过来的,但是事实上情况可能更加复杂。
当然也有一种可能,就是跟断裂和恢复一样,标在原供应链的结束时间(即 B 的 5 月份,C 的 7 月份,D 的 8 月份都标 1),同样也有 bug,比如 E 供应链的构建到底是 BCD 谁转移过来的?事实上会有三条链断只为了构建一个新的 E 吗?所以这个转移变量的测量有点儿弄不清。
再次声明,以上是我的理解,并不一定是这篇文章中真正的标注方式,我也不知道该怎么去处理这块数据,大家自行根据需求把控。所以如果在论文中用到这个变量,建议最好明确把自己的思路说一遍,防止出问题。
最后把 Recover 和 Transfer 的 1 合并,即为 Refill。
2.3 遗留的问题
可能是作者和审稿人的不注意,留下了个难以解释的问题,在数据说明部分提到了'本文从该数据库中筛选出所有中国上市公司的跨国供应链关系数据。其中'跨国'说明研究数据应该不包括客户为中国企业的供应链关系对,但是转移指标的说明时,又说可以转移到中国,有点儿相互矛盾。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
3 供应链断裂与重构指标的测度 依据丁浩员等的做法,该部分的目标是进行以中国沪深 A 股上市企业作为供应商,外国企业作为客户的供应链断裂与重构指标的测度。本节不考虑中国企业作为客户的情况。
3.1 原始数据库处理 原始数据确实有点大,建议根据自己所需要的研究区间进行精简操作。例如我的研究区间是 18-24,那就只需要保留 start_是 2018 年及之后的数据,对 company.dta、relations.dta 都进行如下操作,并另存为新数据文件:
gen year=year(start_)
drop if year<2018
3.2 中国沪深 A 股上市企业作为供应商的供应链关系对筛选、标记与调整
3.2.1 原始数据筛选 在 company.dta 里筛选出中国沪深 A 股上市企业,先删除没有股票代码的数据,再删除不是 CN 的公司
drop if missing(ticker)
drop if country!="CN"
后续导出至 Excel 进行粗略筛选,仅保留在沪深 A 股上市的企业,可以用数字大小来判别,即股票代码在 000001-099999(深证),300000-399999(创业板),600000-699999(上证),当然如果想保留北证,就加上 400000-499999、800000-899999。其他的诸如新三板(9 开头)、美股(英文)、港股上市(五位数字代码)的都给删除。
最后根据企业 id、股票代码进行 excel 的删除重复值操作,保证一个 id 就只对应一个股票代码。将这两列保存为 CNcompany.xlsx,主要用于后续的数据匹配工作。
在 relations.dta 中仅保留关系为 customer 和 supplier 的数据,去除竞争者合作者之类的干扰数据。
keep if rel_type=="CUSTOMER" | rel_type=="SUPPLIER"
3.2.2 供应链数据标记 运行下方 Python 代码(注意看我的注释),读取 relations.dta,将结果输出至 result.xlsx,即为与中国沪深 A 股上市公司相关的所有供应链关系对。
import pandas as pd
unique=pd.read_excel('CNcompany.xlsx' ,header=[0 ])
IDlist=unique['id' ].tolist()
df=pd.read_stata('relations.dta' )
result=pd.DataFrame(columns=['start_' ,'end_' ,'id' ,'rel_type' ,'source_company_id' ,'source_ticker' ,'source_cusip' ,'target_company_id' ,'target_cusip' ,'flag' ],index=range (1 ,200000 ))
index=1
for i in range (len (df)):
print (i)
if int (df['source_company_id' ][i]) in IDlist and int (df['target_company_id' ][i]) in IDlist:
result['start_' ][index]=df['start_' ][i]
result['end_' ][index]=df['end_' ][i]
result['id' ][index]=df['id' ][i]
result['rel_type' ][index]=df['rel_type' ][i]
result['source_company_id' ][index]=df['source_company_id' ][i]
result['source_ticker' ][index]=df['source_ticker' ][i]
result['source_cusip' ][index]=df['source_cusip' ][i]
result['target_company_id' ][index]=df['target_company_id' ][i]
result['target_cusip' ][index]=df['target_cusip' ][i]
result['flag' ][index]='S&T'
index+=1
elif int (df['source_company_id' ][i]) in IDlist:
result['start_' ][index]=df['start_' ][i]
result['end_' ][index]=df['end_' ][i]
result['id' ][index]=df['id' ][i]
result['rel_type' ][index]=df['rel_type' ][i]
result['source_company_id' ][index]=df['source_company_id' ][i]
result['source_ticker' ][index]=df['source_ticker' ][i]
result['source_cusip' ][index]=df['source_cusip' ][i]
result['target_company_id' ][index]=df['target_company_id' ][i]
result['target_cusip' ][index]=df['target_cusip' ][i]
result['flag' ][index]='S'
index+=1
elif int (df['target_company_id' ][i]) in IDlist:
result['start_' ][index]=df['start_' ][i]
result['end_' ][index]=df['end_' ][i]
result['id' ][index]=df['id' ][i]
result['rel_type' ][index]=df['rel_type' ][i]
result['source_company_id' ][index]=df['source_company_id' ][i]
result['source_ticker' ][index]=df['source_ticker' ][i]
result['source_cusip' ][index]=df['source_cusip' ][i]
result['target_company_id' ][index]=df['target_company_id' ][i]
result['target_cusip' ][index]=df['target_cusip' ][i]
result['flag' ][index]='T'
index+=1
else :
continue
result.to_excel('result.xlsx' )
3.2.3 根据 rel_type 关系和 flag 标记进行调整 至此时,reslut.xlsx 上有着供应链关系仅有 customer 和 supplier,至少有一端是中国沪深 A 股上市公司,以及标记中国企业具体在哪一端的 flag 数据。
创建 CN_supplier 和 CN_customer 两个工作表,分别存储中国企业在供应商位置和客户位置的数据
对于 rel_type 为 customer,表示 target 中的企业是 source 中的企业的客户,flag 标记为 S 的不动放到 CN_supplier 表,flag 标记为 T 的调换位置后放在 CN_customer 表(当然不调整也可以,但我习惯把中国企业放左边,下同)。
对于 rel_type 为 supplier,表示 target 中的企业是 source 中的企业的供应商,flag 标记为 S 的调换位置放到 CN_supplier 表,flag 标记为 T 的不动放在 CN_customer 表。
将列标题中的 source 改为 supplier,target 改为 customer,完成单一端为中国沪深 A 股上市公司的数据调整。
注意:以上操作不一定要按我的方法来,只要准确理解了 rel_type 和 flag 标记的含义,自行处理即可。
对于 flag 标记为 S&T,即两端均为中国沪深 A 股上市公司,如果做到是跨国供应链,直接删除即可(我是这么做的)。如果做的是全球供应链,建议与自己的导师讨论,这里不再讨论。
3.3 供应链关系的时间顺序调整与断裂、恢复变量测度
3.3.1 供应链关系的时间顺序调整 在数据中,大部分(95% 左右)供应链关系对是按照时间顺序下来的,即上一条时间线的 end_是早于或等于下一条的 start_
但是有少部分数据像下图这样,第 3 行和第 5 行中间出现了接不上的情况,这时候就得删除第四行,或者把第四行换到第三行的位置,才能让供应链关系的时间顺序被后边的代码识别。
需要做这部分工作也的确是迫于无奈,因为我一开始没有想到合适的办法去解决这一现象,耗费了大量时间人工来做。
后来我求助了外援,他是用 stata 进行该部分的处理,似乎也求助过 deepseek、chatGPT 等 AI 工具,但是具体做法没向我透露,需要大家自行想办法。
3.3.2 月度数据的生成 运行下方 Python 之前,在 excel 里插入'supplier_customer'空列,并把 source_company_id 和 target_company_id 做一个字符串拼接,中间用'-'分割,作为每段供应链关系的唯一标识符。
注意,由于代码运行逻辑中需要靠着从供应链关系开始的第一行往下读,如果 excel 里最后一行是这段供应链关系唯一的一行数据,往下读的时候就会报错。所以请事先作出调整,把非单行供应链关系调到最后边。
运行后会输出该批次有问题需要调整的拼接值,如果上一步弄好了那就不会输出,如果有问题的话需要手工调整。
建议分批运行代码(调整代码中的 start 和 end 值),完成后进行 excel 拼接。
最终输出最终包含 break 和 recover 的数据文件。
import pandas as pd
dfindex=1
monthList=['2018-01' ,'2018-02' ,'2018-03' ,'2018-04' ,'2018-05' ,'2018-06' ,'2018-07' ,'2018-08' ,'2018-09' ,'2018-10' ,'2018-11' ,'2018-12' ,'2019-01' ,'2019-02' ,'2019-03' ,'2019-04' ,'2019-05' ,'2019-06' ,'2019-07' ,'2019-08' ,'2019-09' ,'2019-10' ,'2019-11' ,'2019-12' ,'2020-01' ,'2020-02' ,'2020-03' ,'2020-04' ,'2020-05' ,'2020-06' ,'2020-07' ,'2020-08' ,'2020-09' ,'2020-10' ,'2020-11' ,'2020-12' ,'2021-01' ,'2021-02' ,'2021-03' ,'2021-04' ,'2021-05' ,'2021-06' ,'2021-07' ,'2021-08' ,'2021-09' ,'2021-10' ,'2021-11' ,'2021-12' ,'2022-01' ,'2022-02' ,'2022-03' ,'2022-04' ,'2022-05' ,'2022-06' ,'2022-07' ,'2022-08' ,'2022-09' ,'2022-10' ,'2022-11' ,'2022-12' ,'2023-01' ,'2023-02' ,'2023-03' ,'2023-04' ,'2023-05' ,'2023-06' ,'2023-07' ,'2023-08' ,'2023-09' ,'2023-10' ,'2023-11' ,'2023-12' ,'2024-01' ,'2024-02' ,'2024-03' ,'2024-04' ,'2024-05' ,'2024-06' ,'2024-07' ,'2024-08' ,'2024-09' ,'2024-10' ,'2024-11' ,'2024-12' ,'4000-01' ]
DATA=pd.read_excel('SUPPLIER_DATA.xlsx' ,sheet_name='Sheet1' ,header=[0 ])
SCchain=set (DATA['supplier_customer' ])
SCchains=list (SCchain)
df=pd.DataFrame(columns=['supplier_customer' ,'supplier_id' ,'supplier_ticker' ,'customer_id' ,'TIME' ,'BREAK' ,'REFILL' ,'RECOVER' ,'TRANSFER' ],index=range (1 ,100000 ))
adjustSC=[]
start=0
end=1000
for SC in SCchains[start:end]:
l=SC.split('-' )
supplier_id=l[0 ]
customer_id=l[1 ]
index=DATA.loc[DATA['supplier_customer' ]==SC].index[0 ]
count=1
while DATA['supplier_customer' ][index+count]==SC:
count+=1
if count==1 :
startmonth=DATA['start' ][index]
endmonth=DATA['end' ][index]
for j in range (monthList.index(endmonth)-monthList.index(startmonth)+1 ):
df['supplier_id' ][dfindex]=supplier_id
df['customer_id' ][dfindex]=customer_id
df['supplier_customer' ][dfindex]=SC
df['TIME' ][dfindex]=monthList[monthList.index(startmonth)+j]
df['BREAK' ][dfindex]=0
dfindex+=1
df['BREAK' ][dfindex-1 ]=1
elif count>=2 :
flag=0
breaki=0
if monthList.index(DATA['end' ][index+i-1 ])==monthList.index(DATA['start' ][index+i])and i!=count:
continue
if monthList.index(DATA['end' ][index+i-1 ])<monthList.index(DATA['start' ][index+i])or i==count:
if flag==0 :
startmonth=DATA['start' ][index]
else :
startmonth=DATA['start' ][index+breaki]
df['RECOVER' ][index+i-1 ]=1
endmonth=DATA['end' ][index+i-1 ]
for month in monthList[ monthList.index(startmonth): monthList.index(endmonth)+1 ]:
df['supplier_id' ][dfindex]=supplier_id
df['customer_id' ][dfindex]=customer_id
df['supplier_customer' ][dfindex]=SC
df['TIME' ][dfindex]=month
df['BREAK' ][dfindex]=0
dfindex+=1
df['BREAK' ][dfindex-1 ]=1
flag=1
breaki=i
elif monthList.index(DATA['end' ][index+i-1 ])>monthList.index(DATA['start' ][index+i]):
print (SC+" need to adjust" )
adjustSC.append(SC)
adjustSC=set (adjustSC)
adjustSC=list (adjustSC)
print (adjustSC)
print (len (adjustSC))
chain1=set (df['supplier_customer' ])
chains1=list (chain1)
print (len (chains1))
print (len (SCchains))
df.to_excel(f'result_{start} to {end} .xlsx' )
3.4 供应链转移的测量 读取 3.2.1 中调整好的以中国沪深 A 股上市公司为供应商 excel 表(即代码中的 SUPPLIER_DATA.xlsx),以及上一步测算好供应链断裂、恢复变量的 excel 表(即代码中的 result.xlsx)。
由于转移变量的测量逻辑比较复杂,所以需要单独新建一个 py 文件测量,代码如下:
import pandas as pd
monthList=['2018-01' ,'2018-02' ,'2018-03' ,'2018-04' ,'2018-05' ,'2018-06' ,'2018-07' ,'2018-08' ,'2018-09' ,'2018-10' ,'2018-11' ,'2018-12' ,'2019-01' ,'2019-02' ,'2019-03' ,'2019-04' ,'2019-05' ,'2019-06' ,'2019-07' ,'2019-08' ,'2019-09' ,'2019-10' ,'2019-11' ,'2019-12' ,'2020-01' ,'2020-02' ,'2020-03' ,'2020-04' ,'2020-05' ,'2020-06' ,'2020-07' ,'2020-08' ,'2020-09' ,'2020-10' ,'2020-11' ,'2020-12' ,'2021-01' ,'2021-02' ,'2021-03' ,'2021-04' ,'2021-05' ,'2021-06' ,'2021-07' ,'2021-08' ,'2021-09' ,'2021-10' ,'2021-11' ,'2021-12' ,'2022-01' ,'2022-02' ,'2022-03' ,'2022-04' ,'2022-05' ,'2022-06' ,'2022-07' ,'2022-08' ,'2022-09' ,'2022-10' ,'2022-11' ,'2022-12' ,'2023-01' ,'2023-02' ,'2023-03' ,'2023-04' ,'2023-05' ,'2023-06' ,'2023-07' ,'2023-08' ,'2023-09' ,'2023-10' ,'2023-11' ,'2023-12' ,'2024-01' ,'2024-02' ,'2024-03' ,'2024-04' ,'2024-05' ,'2024-06' ,'2024-07' ,'2024-08' ,'2024-09' ,'2024-10' ,'2024-11' ,'2024-12' ,'4000-01' ]
DATA=pd.read_excel('SUPPLIER_DATA.xlsx' ,sheet_name='Sheet1' ,header=[0 ])
df=pd.read_excel('result.xlsx' ,sheet_name='Sheet1' ,header=[0 ])
SCchain=set (DATA['supplier_customer' ])
SCchains=list (SCchain)
SCchains.remove('8727858-99946865-688223' )
df['supplier_id' ]=df['supplier_id' ].astype(str )
company=set (df['supplier_id' ])
companies=list (company)
print (len (companies))
for company in companies:
company_chain=[]
for SC in SCchains:
if SC.startswith(company):
company_chain.append(SC)
if len (company_chain)==1 :
continue
elif len (company_chain)>1 :
startDate={}
endDate={}
for chain in company_chain:
index=DATA.loc[DATA['supplier_customer' ]==chain].index[0 ]
count=1
while DATA['supplier_customer' ][index+count]==chain:
count+=1
start=DATA['start' ][index]
end=DATA['end' ][index+count-1 ]
startDate[chain]=start
endDate[chain]=end
mindate=monthList.index('4000-01' )
for end in list (endDate.values()):
if monthList.index(end)<mindate:
mindate=monthList.index(end)
for chain,start in startDate.items():
if monthList.index(start)>mindate:
print (chain)
df.loc[index,'TRANSFER' ]=1
df.to_excel('A.result.xlsx' )