【经管论文复现】Python+FactSet Revere全球供应链数据库,测度供应链断裂与重构变量——丁浩员等(2024)《经济研究》复现
丁浩员等在《经济研究》2024年第8期发表了一篇题为《贸易政策冲击下的跨国供应链断裂与重构研究》的文章,提出了跨国供应链断裂与重构两个变量的测度方式,但是稍微有点儿说得不够清楚。下文将对其所采用的FastSet Revere全球供应链数据库和其测度方式进行简单解读,并基于Python,结合Excel操作实现。
目录
丁浩员等在《经济研究》2024年第8期发表了一篇题为《贸易政策冲击下的跨国供应链断裂与重构研究》的文章,提出了跨国供应链断裂与重构两个变量的测度方式,但是稍稍有点儿说得不够清楚(狗头保命)。
下文将对其所采用的FastSet Revere全球供应链数据库和其测度方式进行个人观点解读,并不保证一定与其观点完全一致,并基于Python,结合Excel操作实现。
1 对Factset全球供应链数据库的简单解读
数据库获取渠道:正规来讲是去WRDS官网花钱买,也可以去某克数据网、某鱼、某宝。
可以获取到的数据包括company.dta、ralations.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月和20 19年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 遗留的问题
可能是作者和审稿人的不注意,留下了个难以解释的问题,在数据说明部分提到了“本文从该数据库中筛选出所有中国上市公司的跨国供应链关系数据。其中“跨国”说明研究数据应该不包括客户为中国企业的供应链关系对,但是转移指标的说明时,又说可以转移到中国,有点儿相互矛盾。
3 供应链断裂与重构指标的测度
依据丁浩员等的做法,该部分的目标是进行以中国沪深A股上市企业作为供应商,外国企业作为客户的供应链断裂与重构指标的测度。本节不考虑中国企业作为客户的情况。
3.1 原始数据库处理
原始数据确实有点大,建议根据自己所需要的研究区间进行精简操作。例如我的研究区间是18-24,那就只需要保留start_是2018年及之后的数据,对company.dta、ralations.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() #读取唯一id,在这个列表里的id代表的都是中国沪深A股上市公司
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)#仅用来查看运行进度
#如果source和target都是中国沪深A股上市公司,录信息,标记为S&T,表示位于source和target端
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
#如果source是中国沪深A股上市公司,录信息,标记为S,表示位于source端
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
#如果target是中国沪深A股上市公司,录信息,标记为T,表示位于target端
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]:#一次性运行完可能有点慢,可以调整上边start和end的数据,分批运行
l=SC.split('-')
supplier_id=l[0]#供应商id
customer_id=l[1]#客户id
index=DATA.loc[DATA['supplier_customer']==SC].index[0]#获取供应链对的行索引
count=1#定义当前供应链对的重复次数
while DATA['supplier_customer'][index+count]==SC:
count+=1#通过while循环,计算出当前供应链对的重复次数
#对在月度层面连贯的供应链进行合并,并计算break和recover
#如果只有一列数据,则根据该列直接生成
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 #记录断裂时的i
for i in range(1,count+1):
#如果下一行的start与本行的end相同,则说明两条连贯,不进行操作
if monthList.index(DATA['end'][index+i-1])==monthList.index(DATA['start'][index+i]) and i!=count:
continue
#如果下一行的start比本行的end晚,则说明两条不连贯,进行操作
if monthList.index(DATA['end'][index+i-1])<monthList.index(DATA['start'][index+i]) or i==count:
#当flag为0,则说明之前未进行过此操作,第一行的start即为开始的时间点
if flag==0:
startmonth=DATA['start'][index]
#当flag不为0,则说明之前进行过此操作,索引为index+breaki的行的start即为开始的时间点
else:
startmonth=DATA['start'][index+breaki]
#丁浩员等的方法,将恢复标记在断裂行为的那一行
df['RECOVER'][index+i-1]=1
#我的方法,将恢复标记在恢复行为的那一行
#df['RECOVER'][dfindex]=1
endmonth=DATA['end'][index+i-1] #设置end为结束的时间点
#对df里的数据进行赋值,从monthList中根据下表提取月份
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
#如果下一行的start与本行的end早,则说明时间上倒置,进行人工调整,以避免处置该情况使得代码过于复杂,工作量约为唯一供应链对数的4%左右
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])#完成break和recover计算的月度数据表
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:
#如果SC的开头是该企业代码,就添加到列表
if SC.startswith(company):
company_chain.append(SC)
#如果只搜索到一个同名,则不存在转移,直接跳过
if len(company_chain)==1:
continue
#如果搜索到多个,可能存在转移,执行后续操作
elif len(company_chain)>1:
#print(company_chain)
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#通过while循环,计算出当前供应链对的重复次数
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)
#丁浩员等的方法,将转移标记在断裂行为的那一行
index=df.loc[(df['Relation']==list_of_key[k]) & (df['Time']==monthList[mindate])].index[0]
#我的方法,将转移标记在转移行为的那一行
#index=df.loc[df['supplier_customer']==chain].index[0]#获取供应链对开始的行索引
df.loc[index,'TRANSFER']=1
df.to_excel('A.result.xlsx')
更多推荐
所有评论(0)