1. 项目概述:为什么我们需要一份干净的A股风险警示标签数据?
如果你在A股市场做量化研究、因子挖掘或者公司财务分析,有一个问题几乎无法回避:如何处理那些被贴上“ST”、“*ST”甚至更早的“PT”标签的股票?这些标签是交易所对存在特定风险(比如连续亏损、财务造假、经营异常)的上市公司施加的特殊处理标识。简单来说,它们就像是股票代码上的“黄牌”或“红牌”。在构建投资组合、计算市场指数或者进行学术研究时,如果不把这些“问题股”剔除,你的回测结果可能会被严重扭曲。想象一下,你设计了一个基于盈利能力的选股策略,结果里面混入了一堆因为连续亏损而被ST的公司,策略的有效性从源头上就打了折扣。
然而,获取一份准确、完整且易于使用的A股风险警示历史数据,并不是一件简单的事。交易所的公告是分散的,数据格式不一,手动整理费时费力且容易出错。更麻烦的是,ST状态是动态变化的:一家公司今年被ST,明年可能因为情况好转而“摘帽”,也可能情况恶化变成*ST,甚至最终退市。这就需要一份按时间序列记录每家上市公司风险警示状态的数据面板。这正是本项目要解决的问题:整理一份从A股市场诞生至今,所有上市公司是否被ST、*ST或PT的标识数据,并附上清晰、可复现的Stata整理代码,数据更新至2023年。有了这套“工具包”,研究者可以一键筛选出任意时间点上的“正常股”,确保分析基础的洁净。
2. 核心需求解析与数据来源设计
2.1 明确“ST/*ST/PT”的定义与影响
首先,我们必须厘清这几个标签的具体含义和演变,这是数据准确性的基石。
- PT (Particular Transfer,特别转让):这是历史产物,主要在2000年代初用于暂停上市的公司。这类股票只能在每周五进行集合竞价转让,流动性极差。随着退市制度的完善,PT制度已基本退出舞台,但在整理早期历史数据时必须包含。
- ST (Special Treatment,特别处理):这是当前最常见的风险警示。公司被ST通常因为财务状况异常,例如最近两个会计年度净利润为负,或最近一年净资产为负。被ST后,股票日涨跌幅限制从10%缩小为5%,意在提示风险。
- *ST (退市风险警示):在ST基础上,风险进一步升级。通常因为情况比ST更严重,例如连续三年亏损,或存在重大违法强制退市情形。*ST是退市前的最后一道警示,涨跌幅同样为5%。
对于研究而言,这些股票通常需要被排除,原因有三:1)财务异常:其财务数据本身已失真或处于非正常状态,不具可比性;2)交易规则异化:涨跌幅限制不同,影响收益率计算的公平性;3)样本污染:它们往往伴随极端的价格波动(连续跌停或涨停),会严重干扰因子有效性检验和市场收益率计算。
2.2 数据来源的选取与挑战
整理这类数据,可靠的数据源是关键。通常有三个主要渠道:
- 交易所官方公告:最权威的来源。上海证券交易所和深圳证券交易所会发布关于对上市公司实施ST、*ST及撤销处理的公告。但问题在于,公告是文本格式,且历史公告的获取和解析(爬虫)工作量巨大,需要处理PDF、HTML等多种格式,并准确提取公司代码、全称、实施日期等关键信息。
- 专业金融数据终端:如Wind、Choice、iFinD等。它们有结构化的风险警示历史数据,但属于付费商业数据,且直接导出可能无法满足个性化的时间频率(如日度)或字段需求。
- 开源数据社区与学术数据库:如CNRDS、CSMAR等学术数据库,或GitHub上的一些开源项目。这些数据质量参差不齐,需要仔细核对,但通常是研究者,尤其是学生和独立研究者,性价比最高的起点。
本项目的设计思路是,以开源/学术数据为基础,通过交叉验证和逻辑规则清洗,构建高质量面板数据。具体而言,我们可以从CSMAR或CNRDS获取上市公司基本资料表中的“是否ST/PT”字段作为基础,但这通常是季度或年度数据,且可能存在缺失或错误。然后,我们需要结合交易所公告日期(可从网络爬取或利用现有整理好的公告日期列表)进行精细化处理,将状态变更精确到“日”级别,并编写逻辑规则(如“摘帽”后状态应归为正常)来修正数据矛盾。
注意:数据源的任何微小错误都可能导致最终结果出现系统性偏差。例如,如果漏掉了一家公司在某段时间的ST状态,那么在回测中就会错误地将其纳入样本。因此,在代码中必须设置多重校验和人工抽查环节。
3. 数据整理的核心步骤与Stata实现
3.1 数据准备与初步清洗
假设我们已经从CSMAR数据库下载了“中国上市公司基本信息表”(表名通常如Company_Background),其中包含字段:Stkcd(股票代码)、Listdt(上市日期)、Enddt(退市日期)、State(上市状态),以及我们最关心的RiskWarning(风险警示类型,可能用代码表示,如‘ST’, ‘*ST’, ‘S’等,不同数据库编码方式不同)。
第一步是在Stata中导入并理解数据结构。
* 假设数据已保存为`company_info.dta` use "path/to/company_info.dta", clear * 查看变量标签和内容 describe codebook RiskWarning, compact tab RiskWarning这一步的目的是确认RiskWarning字段的编码规则。例如,可能发现“1”代表正常,“2”代表ST,“3”代表*ST,“4”代表PT,或者直接用字符串‘ST’、‘*ST’表示。我们需要将其统一转换为便于理解的分类变量。
* 示例:根据编码创建新的分类变量 gen risk_type = "" replace risk_type = "正常" if RiskWarning == "1" replace risk_type = "ST" if RiskWarning == "2" | RiskWarning == "ST" replace risk_type = "*ST" if RiskWarning == "3" | RiskWarning == "*ST" replace risk_type = "PT" if RiskWarning == "4" | RiskWarning == "PT" replace risk_type = "其他" if missing(risk_type) & !missing(RiskWarning) label var risk_type "风险警示类型" * 检查转换结果 tab risk_type但这里有一个核心问题:数据库中的这个字段,通常是报告期(如年报、季报)的截面状态。我们需要的是日度面板数据,即对于每一天,每只股票都有一个风险警示状态。这就需要进行时间序列的扩展和插值。
3.2 构建日度风险警示状态面板
构建日度面板的核心逻辑是“状态持续假设”:即一家公司的风险警示状态从生效日开始,持续到下一次状态变更日(或退市日)的前一天。
首先,我们需要一份A股全市场所有股票、所有交易日的日期面板作为骨架。
* 生成一个包含所有交易日期的文件`trading_dates.dta`(可从行情数据中提取唯一日期获得) use "path/to/all_stock_daily_prices.dta", clear keep date duplicates drop date, force sort date save "trading_dates.dta", replace然后,我们需要一份记录了每家股票风险警示状态变更事件的数据。理想情况下,这份数据应包含Stkcd(股票代码)、change_date(状态变更生效日)、new_risk_type(变更后的新状态)。这份数据可以通过解析交易所公告获得,是整理工作的核心难点和价值所在。假设我们已经通过爬虫和人工核对,得到了一份相对干净的变更事件表risk_change_events.dta。
* 步骤1:为每只股票创建其生命周期的日期序列 use "company_info.dta", clear keep Stkcd Listdt Enddt expand = (Enddt - Listdt + 1) // 假设Enddt存在,若未退市则为最新日期 bysort Stkcd: gen date = Listdt + _n - 1 format date %td keep if dow(date)!=0 & dow(date)!=6 // 粗略剔除周末,更精确应用交易日历 save "stock_daily_skeleton.dta", replace * 步骤2:与状态变更事件表合并 merge m:1 Stkcd date using "risk_change_events.dta", keepusing(new_risk_type) rename new_risk_type risk_type_event接下来是关键的一步:使用Stata的carryforward命令(或fillin+ 排序逻辑)来填充状态。我们假设在第一次状态变更前,股票状态为“正常”。
* 步骤3:填充状态 bysort Stkcd (date): gen risk_type_daily = risk_type_event * 对于每个股票,将非缺失值向前填充(locf, last observation carried forward) bysort Stkcd (date): replace risk_type_daily = risk_type_daily[_n-1] if missing(risk_type_daily) & _n>1 * 处理初始状态(上市首日到第一次事件日之间) bysort Stkcd (date): replace risk_type_daily = "正常" if missing(risk_type_daily) & _n==1然而,现实情况往往更复杂。变更事件表可能有缺失,或者数据库中的季度状态数据与事件日期不完全匹配。这时就需要引入第二数据源进行交叉验证与冲突解决。
3.3 多源数据交叉验证与冲突解决
我们可以引入另一个数据源,比如从Wind导出的月度ST状态列表。将其处理成与我们的日度面板相同格式(Stkcd,date,risk_type_wind),然后进行合并比对。
merge 1:1 Stkcd date using "wind_risk_monthly.dta", keepusing(risk_type_wind)合并后,risk_type_daily(来自事件填充)和risk_type_wind(来自Wind月度)之间可能出现不一致。我们需要制定一套冲突解决规则:
- 两者一致:直接采纳。
- *Wind显示为ST/ST,而事件填充为正常:这可能是我们的事件表漏记了。需要以Wind数据为线索,回溯查找该时间段内的交易所公告进行核实。在代码中,我们可以先将其标记为“待核实”,并输出到日志文件。
- *事件填充为ST/ST,而Wind显示为正常:同样需要核实。有时Wind的数据更新存在滞后,事件数据可能更及时。
- 两者均为缺失:通常意味着该日股票状态为正常。
在Stata中,我们可以这样实现一个简单的冲突标记逻辑:
gen conflict_flag = 0 gen risk_type_final = risk_type_daily * 规则:当Wind数据非缺失且与当前填充数据不同时,标记冲突,并优先采用更严格的警示状态(即*ST > ST > 正常) replace conflict_flag = 1 if !missing(risk_type_wind) & risk_type_wind != risk_type_daily * 定义一个状态严重程度的数值 gen severity = 0 replace severity = 1 if risk_type_daily == "ST" replace severity = 2 if risk_type_daily == "*ST" replace severity = 3 if risk_type_daily == "PT" gen severity_wind = 0 replace severity_wind = 1 if risk_type_wind == "ST" replace severity_wind = 2 if risk_type_wind == "*ST" replace severity_wind = 3 if risk_type_wind == "PT" * 冲突时,取严重程度更高的状态 replace risk_type_final = risk_type_wind if conflict_flag == 1 & severity_wind > severity处理完冲突后,我们得到的就是一份初步的、经过校验的日度风险警示状态面板risk_panel_preliminary.dta。
4. 数据质量检查与常见问题处理
4.1 逻辑一致性检查
数据整理出来后,必须进行严格的逻辑检查,以下是一些必做的检查项及其Stata实现:
状态连续性检查:同一只股票,其风险警示状态的变化应符合“正常<->ST<->ST->退市”或“正常<->PT”的基本路径,不应出现跳跃(如直接从正常变为ST,虽然罕见但需核查)或反复横跳。
bysort Stkcd (date): gen prev_type = risk_type_final[_n-1] gen illogical_change = 0 * 例如,检查是否出现“正常”直接变“*ST”(跳过ST),通常需要结合具体规则,这里仅示例 replace illogical_change = 1 if risk_type_final=="*ST" & prev_type=="正常" list Stkcd date prev_type risk_type_final if illogical_change == 1与退市状态的兼容性检查:已经退市的股票,在退市日期之后不应再有任何状态记录。同时,在退市前,其状态很可能就是*ST。
merge m:1 Stkcd using “company_info.dta”, keepusing(Enddt) gen after_delist = date > Enddt & !missing(Enddt) count if after_delist == 1 & !missing(risk_type_final) * 如果计数>0,说明退市后还有数据,需要清理关键日期的验证:随机抽取若干次知名的ST/*ST事件(例如,某些财务造假大案的公司),核对我们的数据中状态变更的日期是否与公开报道的生效日一致。
4.2 缺失值与边界情况处理
- 上市初期:新股上市初期不可能被ST。我们的数据在上市日期到首次财报披露日之间,应强制设为“正常”。
- 长时间停牌期间:股票停牌时,其风险警示状态是冻结的。我们的日度面板在停牌日依然保留其状态是没问题的,因为我们需要的是日历日的状态标识。但需注意,在计算收益率等需要交易数据的指标时,这些日期自然会被剔除。
- B股、科创板、创业板等特殊板块:ST规则基本一致,但科创板、创业板试点注册制后,退市流程有所变化。数据源需要覆盖全市场,代码处理逻辑上通常无需特别区分,但需知晓规则背景。
4.3 生成最终易用的指标
对于大多数研究而言,我们最终需要的往往不是一个字符串分类变量,而是一个或多个简洁的二进制(0/1)指标,便于在回归或筛选时直接使用。
* 生成最常用的“是否被风险警示”指标 gen is_risk_warning = (inlist(risk_type_final, "ST", "*ST", "PT")) label var is_risk_warning "是否被ST/*ST/PT (1=是)" * 有时需要区分ST和*ST gen is_ST = (risk_type_final == "ST") gen is_STstar = (risk_type_final == "*ST") label var is_ST "是否为ST (1=是)" label var is_STstar "是否为*ST (1=是)" * 保存最终数据集 keep Stkcd date is_risk_warning is_ST is_STstar risk_type_final order Stkcd date sort Stkcd date save "A_share_ST_data_2023.dta", replace最终的数据集应包含股票代码、日期、以及几个核心的指示变量。文件不宜过大,可以通过compress命令优化存储。
5. Stata高级技巧:效率优化与批量处理
当处理全市场、长达三十多年的日度数据时,数据量可能达到千万行级别。原始的循环操作会极其缓慢。必须运用Stata的向量化操作和高效命令。
- 避免循环:如上文所示,使用
bysort配合_n,_N,以及replace ... if ...的向量化操作,效率远高于forvalues循环。 - 使用
joinby替代多层嵌套循环:在创建股票-日期骨架时,如果股票数量多,expand日期差的方法可能内存消耗大。另一种更高效的方法是使用joinby。* 方法二:使用joinby (更高效处理大量股票) use “company_info.dta”, clear keep Stkcd Listdt Enddt cross using “trading_dates.dta” keep if date >= Listdt & (date <= Enddt | missing(Enddt)) - 合理使用
preserve/restore和临时文件:在复杂的多步骤清洗中,将中间结果保存为临时文件(tempfile),可以释放内存,并使流程更清晰。tempfile skeleton events merged preliminary final save `skeleton` ... merge 1:1 Stkcd date using `events` save `merged` - 利用
collapse快速汇总统计:如果你想快速了解每年被ST的公司数量,collapse命令是神器。
这正是热词中“stata的collapse怎么画图”的一个典型应用场景:先use “A_share_ST_data_2023.dta”, clear gen year = year(date) collapse (sum) count_ST = is_ST count_STstar = is_STstar, by(year) line count_ST count_STstar year, title(“A股历年ST与*ST公司数量”)collapse汇总,再使用twoway line等绘图命令进行可视化。
6. 常见问题排查与实战心得
在实际操作中,你一定会遇到各种报错和数据异常。以下是一些典型问题的排查思路:
问题:
merge命令后观测值数量异常增多(远多于预期)。- 原因:最可能是合并键(
Stkcd和date)不唯一。使用duplicates report Stkcd date分别检查主表和using表。 - 解决:在合并前,务必确保两个数据集中
Stkcd和date的组合是唯一的。如果有重复,需要根据业务逻辑决定是去重(duplicates drop)还是汇总。
- 原因:最可能是合并键(
问题:使用
bysort和_n-1向前填充时,状态没有正确延续。- 原因:数据没有严格按照
Stkcd和date排序。bysort默认升序排列,如果日期顺序混乱,填充就会出错。 - 解决:在执行
bysort Stkcd (date): ...之前,先用sort Stkcd date或isid Stkcd date确保排序正确。isid命令可以同时检查是否唯一且已排序。
- 原因:数据没有严格按照
问题:从数据库导出的
RiskWarning字段是字符串,但包含空格、换行符等不可见字符,导致判断失效。- 原因:原始数据清洗不彻底。
- 解决:使用
replace RiskWarning = strtrim(stritrim(RiskWarning))去除首尾和中间多余空格。对于更复杂的字符,可以用subinstr()函数替换。
问题:最终数据集中,某些股票在已知的被ST期间,
is_risk_warning指标仍为0。- 排查:
- 检查原始事件表中,该股票的ST生效日期是否正确录入。
- 检查该股票在ST生效日是否处于上市状态(是否已退市或尚未上市)。
- 检查冲突解决规则是否错误地覆盖了正确的事件数据。
- 心得:建立一个“已知案例测试集”。手动整理10-20家历史上典型ST公司的关键日期和状态,在每一步数据处理后,都筛选出这些公司进行比对,这是验证数据流水线是否可靠的最直接方法。
- 排查:
关于Stata版本与命令:热词中提到了Stata 18。新版本通常有性能提升和新功能。例如,
frame命令可以更方便地管理多个数据集。但核心的数据处理逻辑(merge,bysort,replace)是通用的。对于“亚组分析”、“莫兰指数”、“核密度估计”等,那是数据准备好之后的分析阶段,与本项目的数据整理阶段是上下游关系。本项目产出的洁净数据,正是为这些高级分析奠定可靠的基础。
最后,数据整理工作没有绝对的“完成”,只有不断的“迭代”。交易所的规则在微调,数据源也可能变化。因此,将整个清洗流程封装成清晰的、模块化的Do文件至关重要。当需要更新到2024年数据时,你只需要替换最新的原始数据文件,重新运行整个Do文件,就能高效地获得更新后的数据集。这套方法的价值,不仅在于一份现成的数据,更在于一套可复现、可审计、可持续更新的数据生产流程。