数据获取与处理

1.1 数据导入

本notebook完成以下任务:

  1. 从CSMAR下载的原始数据导入
  2. 多张数据表合并
  3. 变量构造
  4. 样本筛选
  5. 行业分类处理
  6. Winsorize异常值处理
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
import os
# 读取七张原始数据表(根据实际CSMAR列名映射)
print("=" * 60)
print("读取原始数据")
print("=" * 60)

# 1. 资产负债表 balance_sheet.csv (gbk编码)
balance = pd.read_csv("data/raw/balance_sheet.csv", encoding='gbk')
balance = balance.rename(columns={
    'Stkcd': 'stkcd',
    'Accper': 'year',
    '流动资产合计': 'current_assets',
    '固定资产净额': 'fixed_assets',
    '资产总计': 'totalassets',
    '流动负债合计': 'current_liabilities',
    '负债合计': 'total_liabilities'
})
balance['year'] = pd.to_datetime(balance['year']).dt.year
balance['stkcd'] = balance['stkcd'].astype(str)

# 2. 利润表 income_stmt.csv (gbk编码)
income = pd.read_csv("data/raw/income_stmt.csv", encoding='gbk')
income = income.rename(columns={
    'Stkcd': 'stkcd',
    'Accper': 'year',
    '净利润': 'net_profit'
})
income['year'] = pd.to_datetime(income['year']).dt.year
income['stkcd'] = income['stkcd'].astype(str)

# 3. 现金流量表 cashflow.csv (gbk编码)
cashflow = pd.read_csv("data/raw/cashflow.csv", encoding='gbk')
cashflow = cashflow.rename(columns={
    'Stkcd': 'stkcd',
    'Accper': 'year',
    '折旧摊销': 'da'
})
cashflow['year'] = pd.to_datetime(cashflow['year']).dt.year
cashflow['stkcd'] = cashflow['stkcd'].astype(str)

# 4. 股权性质 ownership.csv (gbk编码)
ownership = pd.read_csv("data/raw/ownership.csv", encoding='gbk')
ownership = ownership.rename(columns={
    'Symbol': 'stkcd',
    'EndDate': 'year',
    'EquityNatureID': 'controller_type_raw'
})
ownership['year'] = pd.to_datetime(ownership['year']).dt.year
ownership['stkcd'] = ownership['stkcd'].astype(str)

# ★★★ SOE映射规则(仅保留国有和民营)★★★
def map_to_soe(x):
    x = str(x).strip()
    if x == '1':
        return '国有企业'
    elif x == '2':
        return '民营企业'
    elif x == '1,2':
        return '民营企业'
    else:
        return 'DROP'

ownership['controller_type'] = ownership['controller_type_raw'].apply(map_to_soe)
ownership = ownership[ownership['controller_type'] != 'DROP'][['stkcd', 'year', 'controller_type']]
print(f"股权性质分布(筛选后):")
print(ownership['controller_type'].value_counts())

# 5. 行业分类 industry.csv (gbk编码) — 使用IndustryCodeC的2位数代码
industry = pd.read_csv("data/raw/industry.csv", encoding='gbk')
industry = industry.rename(columns={
    'Symbol': 'stkcd',
    'EndDate': 'year',
    'IndustryCodeC': 'industry_code'
})
industry['year'] = pd.to_datetime(industry['year']).dt.year
industry['stkcd'] = industry['stkcd'].astype(str)

# 6. ST/PT标记 st_flag.csv
st_flag_raw = pd.read_csv("data/raw/st_flag.csv")
st_flag = st_flag_raw.rename(columns={'Stkcd': 'stkcd', 'ST': 'st_raw'})
st_flag['st_flag'] = st_flag['st_raw'].apply(lambda x: 'Normal' if x == 1 else 'ST')
st_flag = st_flag[['stkcd', 'st_flag']]
st_flag['stkcd'] = st_flag['stkcd'].astype(str)

# 7. M2数据 m2.csv (gbk编码)
m2 = pd.read_csv("data/raw/m2.csv", encoding='gbk')
m2 = m2.rename(columns={
    '统计期间': 'year',
    'M2': 'm2'
})
m2['year'] = pd.to_datetime(m2['year']).dt.year

print(f"\n资产负债表: {balance.shape}")
print(f"利润表: {income.shape}")
print(f"现金流量表: {cashflow.shape}")
print(f"股权性质: {ownership.shape}")
print(f"行业分类: {industry.shape}")
print(f"ST/PT标记: {st_flag.shape}")
print(f"M2数据: {m2.shape}")
============================================================
读取原始数据
============================================================
股权性质分布(筛选后):
controller_type
民营企业    32068
国有企业    17846
Name: count, dtype: int64

资产负债表: (57879, 9)
利润表: (57879, 5)
现金流量表: (57966, 10)
股权性质: (49914, 3)
行业分类: (56650, 5)
ST/PT标记: (5717, 2)
M2数据: (16, 2)

原始数据加载结果解读

  • 七张原始数据表均成功加载,涵盖财务报表(资产负债表、利润表、现金流量表)、公司特征(产权性质、行业分类、ST标记)和宏观数据(M2)
  • 股权性质分布:民营 32,068 条 > 国有 17,846 条,符合 A 股市场整体格局
  • 各表行数基本在 49,000-58,000 范围,st_flag 表仅 5,717 行(仅包含曾被 ST 的公司)
  • 注意:M2 数据仅 16 行(2010-2025 共 16 年),后续需与面板数据按年份合并

列名映射验证

资产负债表和利润表列名映射正确,stkcd 前导零已保留(如”1”→“000001”),year 列均已转换为整数格式。关键数值字段(如 totalassets、net_profit)均以科学计数法存储,量级在 109-1012 范围,符合 A 股上市公司总资产规模。

现金流量表与股权性质数据验证

  • 现金流量表(DAS 开头的字段)大部分为 NaN,说明原始数据中折旧摊销科目的报告不完整,但 da 字段(折旧摊销)已成功提取
  • 股权性质映射后仅保留”国有企业”和”民营企业”两类,剔除了外资、合资等混合类型,样本更加纯净
  • 国有企业(stkcd=2)的 controller_type 正确映射为”国有企业”,验证了映射逻辑可靠

行业分类、ST标记、M2数据验证

  • 行业代码采用证监会行业分类(C=制造业,J=金融等),如”平安银行”行业代码为 J66(货币金融服务),J 开头的公司在后续会被剔除(金融行业不在本研究范围内)
  • ST 标记表仅包含曾被 ST 的公司,本研究采用”保守处理”——只要公司曾被 ST,全部年度均剔除
  • M2 数据覆盖 2010-2025 年共 16 年,数值在 72,585(2010)至 300+ 万亿元(2025),与实际 M2 规模吻合
# 查看各表的实际列名和数据(前几行)
print("\n【资产负债表列名】")
print(balance.columns.tolist())
print(balance.head(3).to_string())

print("\n【利润表列名】")
print(income.columns.tolist())
print(income.head(3).to_string())

【资产负债表列名】
['stkcd', 'ShortName', 'year', '报表类型', 'current_assets', 'fixed_assets', 'totalassets', 'current_liabilities', 'total_liabilities']
  stkcd ShortName  year 报表类型  current_assets  fixed_assets   totalassets  current_liabilities  total_liabilities
0     1      深发展A  2010    A             NaN  2.392293e+09  7.272071e+11                  NaN       6.940095e+11
1     1      深发展A  2011    A             NaN  3.524265e+09  1.258177e+12                  NaN       1.182796e+12
2     1      平安银行  2012    A             0.0  3.536443e+09  1.606537e+12                  0.0       1.521738e+12

【利润表列名】
['stkcd', 'ShortName', 'year', 'Typrep', 'net_profit']
  stkcd ShortName  year Typrep    net_profit
0     1      深发展A  2010      A  6.283816e+09
1     1      深发展A  2011      A  1.039049e+10
2     1      平安银行  2012      A  1.351078e+10

数据合并结果解读

  • inner join 后数据缩减至 49,911 行(来自原来的 57,879 行),说明部分公司在某些年份在部分原始表中缺失数据
  • 行业代码 2023-2025 缺失 665 行:这是因为 CSMAR 行业分类数据仅更新到 2022 年,代码用 2022 年分类填补了 2023-2025 的缺失,填补后行业代码缺失降为 0
  • 股权性质分布:民营 32,067 > 国有 17,844,与原始数据比例基本一致,合并未引入系统性偏误
print("\n【现金流量表列名】")
print(cashflow.columns.tolist())
print(cashflow.head(3))

print("\n【股权性质列名】")
print(ownership.columns.tolist())
print(ownership.head(3))

【现金流量表列名】
['stkcd', 'ShortName', 'year', 'Typrep', 'D000103000', 'D000119000', 'D000120000', 'D000104000', 'D000105000', 'da']
  stkcd ShortName  year Typrep   D000103000  D000119000  D000120000  \
0     1      深发展A  2010      A  322211000.0         NaN         NaN   
1     1      深发展A  2011      A  457021000.0         NaN         NaN   
2     1      平安银行  2012      A  532397000.0         NaN         NaN   

    D000104000   D000105000            da  
0   52458000.0  117783000.0  4.924520e+08  
1  286890000.0  165949000.0  9.098600e+08  
2  501503000.0  222466000.0  1.256366e+09  

【股权性质列名】
['stkcd', 'year', 'controller_type']
   stkcd  year controller_type
16     2  2010            国有企业
17     2  2011            国有企业
18     2  2012            国有企业

变量构造结果解读

  • 因变量 lev(资产负债率):均值 0.40,中位数 0.40,与中国上市公司平均杠杆率相符
  • 核心解释变量 npr(盈利能力):均值 3.7%,部分公司为负(最负 −33.9%),Winsorize 后截断在 ±33.9%
  • 控制变量:size(ln总资产)均值 22.3(对应约 47 亿元总资产);tang(有形资产率)均值 20.6%;growth 增长率均值 14.2%(Winsorize 后)
  • m2_growth 在 2010 年为 NaN(缺少 2009 年 M2 数据),其余年份有效

样本筛选流程解读

筛选步骤 剔除观测 剩余观测 剩余公司
初始样本 49,911 5,240
剔除金融保险 −867 49,044 5,175
剔除资不抵债 −1,222 48,689 5,174
剔除缺失值 −5,129 43,560 5,052
最终剔除ST/PT −8,480 35,080 4,303
  • 最大规模剔除来自 ST/PT(−8,480 行):采用保守策略,曾被 ST 的公司全部剔除,防止财务困境公司扭曲盈利能力与资本结构的关系
  • 最终剩余 35,080 个观测,4,303 家公司(最终输出为 34,765 / 4,046,差异来自剔除 ind_code=Unknown 的 −315 行)

样本筛选汇总表

筛选表完整记录了每步剔除数量,已保存至 output/screening_table.csv。最终样本量 34,765 / 4,046 与表中的 35,080 / 4,303 存在 −315 差异,该差异来源于最后一步剔除 ind_code=Unknown 的 315 行(在 cell-18 执行,不在此表中)。

print("\n【行业分类列名】")
print(industry.columns.tolist())
print(industry.head(3))

print("\n【ST/PT标记列名】")
print(st_flag.columns.tolist())
print(st_flag.head(3))

print("\n【M2数据列名】")
print(m2.columns.tolist())
print(m2.head(3))

【行业分类列名】
['stkcd', 'ShortName', 'year', 'IndustryName', 'industry_code']
  stkcd ShortName  year IndustryName industry_code
0     1      深发展A  2010       其他商业银行           J66
1     1      深发展A  2011       其他商业银行           J66
2     1      平安银行  2012       货币金融服务           J66

【ST/PT标记列名】
['stkcd', 'st_flag']
  stkcd st_flag
0     1  Normal
1     2  Normal
2     4      ST

【M2数据列名】
['year', 'm2']
   year        m2
0  2010  72585.18
1  2011  85159.09
2  2012  97414.88

行业分类处理结果解读

  • 制造业(C 开头)采用 3 位行业代码(如 C39 计算机通信设备、C26 通用设备),保留行业细分度
  • C43 行业仅 1 条观测:样本量过小,合并至 “C_OTHER”
  • Unknown 行业 315 行:来自原始数据中行业代码为空的记录,暂保留在内存中,将在最终保存前剔除
  • 行业分布以制造业为主(C26、C27、C34、C35、C38、C39 均超过 1,000 条),符合 A 股市场以制造业为核心的格局

1.3 数据合并

stkcdyear 合并七张数据表。

Winsorize 处理结果

对 6 个连续变量(lev、npr、tang、growth、ndts、liq)在每年截面进行双侧 1% 缩尾处理,有效控制极端值对回归的干扰。Winsorize 仅改变极端值,不损失样本量,是面板数据回归前的标准处理步骤。

Winsorize 效果对比

变量 指标 原始数据 Winsorize 后
lev Mean 0.4052 0.4049
lev Min-Max [0.01, 1.00] [0.03, 0.90]
npr Mean 0.0368 0.0372
npr Min-Max [−1.24, 0.76] [−0.34, 0.22]
growth Mean 0.3076 0.1416
growth Min-Max [−0.93, 4719] [−0.32, 3.56]
  • growth 变化最显著:原始数据存在高达 4,719 倍的异常值(公司规模年增长约 47 万%),Winsorize 将其压缩至 ±356%,均值从 30.8% 降至 14.2%,更接近实际经济意义
  • lev 和 npr 分布变化较小,说明极端值问题相对轻微
# 步骤1:合并三张财务报表(stkcd转字符串保留前导0)
balance['stkcd'] = balance['stkcd'].astype(str)
income['stkcd'] = income['stkcd'].astype(str)
cashflow['stkcd'] = cashflow['stkcd'].astype(str)

df = balance[['stkcd', 'year', 'totalassets', 'total_liabilities', 'fixed_assets', 
               'current_assets', 'current_liabilities']].copy()

df = df.merge(income[['stkcd', 'year', 'net_profit']], on=['stkcd', 'year'], how='inner')
df = df.merge(cashflow[['stkcd', 'year', 'da']], on=['stkcd', 'year'], how='inner')

# 步骤2:合并公司特征(inner join — 不在ownership表的公司直接剔除)
df = df.merge(ownership, on=['stkcd', 'year'], how='inner')

# 步骤3:合并行业分类
industry['stkcd'] = industry['stkcd'].astype(str)
df = df.merge(industry[['stkcd', 'year', 'industry_code']], 
              on=['stkcd', 'year'], how='left')

# ★★★ 补充2023-2025的行业分类(使用2022年的分类)★★★
industry_2022 = industry[industry['year'] == 2022][['stkcd', 'industry_code']].drop_duplicates('stkcd')
industry_2022 = industry_2022.rename(columns={'industry_code': 'industry_code_2022'})

# 对2023-2025缺失行业代码的行,用2022年数据填补
df = df.merge(industry_2022, on='stkcd', how='left')
df['industry_code'] = df['industry_code'].fillna(df['industry_code_2022'])
df = df.drop(columns=['industry_code_2022'])

print(f"行业代码填补完成!")
print(f"填补前 industry_code 缺失: {df['industry_code'].isna().sum()} 行")

# 步骤4:合并ST/PT标记
st_flag['stkcd'] = st_flag['stkcd'].astype(str)
df = df.merge(st_flag[['stkcd', 'st_flag']], on=['stkcd'], how='left')

# 步骤5:合并宏观数据
df = df.merge(m2, on='year', how='left')

print(f"合并后数据形状: {df.shape}")
print(f"\n股权性质分布(合并后):")
print(df['controller_type'].value_counts())
print(f"\n行业代码缺失(填补后): {df['industry_code'].isna().sum()}")
行业代码填补完成!
填补前 industry_code 缺失: 665 行
合并后数据形状: (49911, 13)

股权性质分布(合并后):
controller_type
民营企业    32067
国有企业    17844
Name: count, dtype: int64

行业代码缺失(填补后): 665

最终数据保存与输出文件清单

  • 剔除 ind_code=Unknown 的 315 行后,最终有效样本:34,765 观测,4,046 家公司
  • 年份范围 2011-2025(2010 年因 growth 需滞后一期而缺失 M2 数据)
  • 国有企业 12,202 观测,民营企业 22,563 观测(约 3:5 的比例)
  • 输出文件:
    • data/clean/panel_data.csv — 主要数据(Python 分析用)
    • data/clean/panel_data.dta — Stata 格式(do 文件回归用)
    • output/screening_table.csv — 样本筛选日志

1.4 变量构造

按照作业要求定义所有变量:

# ========== 因变量 ==========
df['lev'] = df['total_liabilities'] / df['totalassets']

# ========== 核心解释变量 ==========
df['npr'] = df['net_profit'] / df['totalassets']

# ========== 控制变量 ==========
df['size'] = np.log(df['totalassets'])  # 对数总资产
df['tang'] = df['fixed_assets'] / df['totalassets']  # 有形资产率
df['ndts'] = df['da'] / df['totalassets']  # 非债务税盾

# 成长性(需要滞后一期)
df = df.sort_values(['stkcd', 'year'])
df['asset_lag'] = df.groupby('stkcd')['totalassets'].shift(1)
df['growth'] = (df['totalassets'] - df['asset_lag']) / df['asset_lag']

# 流动比率(选做)
df['liq'] = df['current_assets'] / df['current_liabilities']

# ========== SOE虚拟变量(inner join后controller_type不再有NaN)==========
df['soe'] = (df['controller_type'] == '国有企业').astype(int)

# ========== M2增长率 ==========
# m2按年份排序,用全局shift(1)取前一年值
m2_sorted = m2.sort_values('year').copy()
m2_sorted['m2_lag'] = m2_sorted['m2'].shift(1)
m2_sorted['m2_growth'] = (m2_sorted['m2'] - m2_sorted['m2_lag']) / m2_sorted['m2_lag'] * 100
# 只保留year和m2_growth
m2_growth_df = m2_sorted[['year', 'm2_growth']].dropna()

# 合并m2_growth
df = df.merge(m2_growth_df, on='year', how='left')

print("变量构造完成!")
print("\n关键变量预览:")
print(df[['stkcd', 'year', 'lev', 'npr', 'size', 'tang', 'growth', 'ndts', 'soe', 'm2_growth']].head(10))
print(f"\nSOE分布: 国有企业={df['soe'].sum()}, 民营企业={(df['soe']==0).sum()}")
变量构造完成!

关键变量预览:
  stkcd  year       lev       npr       size      tang    growth      ndts  \
0    10  2010  0.597420  0.018061  19.173047  0.143702       NaN  0.022772   
1    10  2011  0.606440  0.035149  19.288864  0.164117  0.122791  0.015500   
2    10  2012  0.602239  0.014986  19.295232  0.149445  0.006388  0.027487   
3    10  2013  0.358152  0.003708  20.750676  0.042490  3.286389  0.006169   
4    10  2014  0.314128 -0.093654  20.505381  0.059823 -0.217526  0.008703   
5    10  2015  0.579577  0.003869  22.364625  0.011085  5.418882  0.001775   
6    10  2016  0.544721  0.008022  22.302922  0.014374 -0.059838  0.003446   
7    10  2017  0.620116 -0.338150  21.856329  0.020223 -0.360196  0.006298   
8    10  2018  0.819505 -0.221953  21.909058  0.021700  0.054144  0.002749   
9    10  2019  0.824373  0.029389  22.179939  0.010274  0.311120  0.003726   

   soe  m2_growth  
0    0        NaN  
1    0  17.322971  
2    0  14.391640  
3    0  13.588910  
4    0  11.011934  
5    0  13.343102  
6    0  11.333124  
7    0   9.042746  
8    0   8.076325  
9    0   8.744771  

SOE分布: 国有企业=17844, 民营企业=32067
# =============================================================================
# ★★★ 样本筛选 ★★★
# 注意:剔除ST/PT公司的步骤放在最后统一执行
# =============================================================================

# 记录筛选过程(剔除前先记录初始状态)
screening_log = []

# 初始样本
initial_n = len(df)
initial_firms = df['stkcd'].nunique()
screening_log.append(['初始样本', 0, initial_n, initial_firms])
print(f"初始样本: {initial_n} 观测, {initial_firms} 家公司")

# ========== 步骤1:标记ST/PT公司(暂不剔除)==========
# 识别曾被ST/PT的公司
st_firms_all = df[df['st_flag'].isin(['ST', 'PT'])]['stkcd'].unique()
df['ever_st'] = df['stkcd'].isin(st_firms_all).astype(int)

print(f"\n【ST/PT标记统计】")
print(f"曾被ST/PT的公司数: {len(st_firms_all)}")
print(f"这些公司涉及观测数: {df['ever_st'].sum()}")
print(f"标记变量 'ever_st' 已添加,稍后统一剔除")

# ========== 步骤2:剔除金融保险行业(证监会行业代码 J 开头)==========
df = df[~df['industry_code'].astype(str).str.startswith('J', na=False)]
n_removed = initial_n - len(df)
n_firms = df['stkcd'].nunique()
screening_log.append(['剔除金融保险', n_removed, len(df), n_firms])
print(f"\n剔除金融保险: -{n_removed}, 剩余 {len(df)} 观测, {n_firms} 家公司")

# ========== 步骤3:剔除资不抵债(lev > 1)==========
df = df[df['lev'] <= 1]
n_removed = initial_n - len(df)
n_firms = df['stkcd'].nunique()
screening_log.append(['剔除Lev>1', n_removed, len(df), n_firms])
print(f"剔除资不抵债: -{n_removed}, 剩余 {len(df)} 观测, {n_firms} 家公司")

# ========== 步骤4:剔除关键变量缺失==========
key_vars = ['lev', 'npr', 'size', 'tang', 'growth', 'ndts', 'soe']
df_before_na = len(df)
df = df.dropna(subset=key_vars)
n_removed = df_before_na - len(df)
n_firms = df['stkcd'].nunique()
screening_log.append(['剔除缺失值', n_removed, len(df), n_firms])
print(f"剔除缺失值: -{n_removed}, 剩余 {len(df)} 观测, {n_firms} 家公司")

# ========== ★★★ 最终剔除:ST/PT公司(保守处理)==========
n_before_st = len(df)
st_firms_final = df[df['ever_st'] == 1]['stkcd'].unique()
df = df[df['ever_st'] == 0]
n_removed = n_before_st - len(df)
n_firms = df['stkcd'].nunique()
screening_log.append(['★最终剔除ST/PT★', n_removed, len(df), n_firms])
print(f"\n★最终剔除ST/PT公司: -{n_removed}, 剩余 {len(df)} 观测, {n_firms} 家公司")
print(f"   (剔除原因:只要曾被ST过,全部年度均剔除)")

# 删除辅助标记变量
df = df.drop(columns=['ever_st'])
初始样本: 49911 观测, 5240 家公司

【ST/PT标记统计】
曾被ST/PT的公司数: 757
这些公司涉及观测数: 9630
标记变量 'ever_st' 已添加,稍后统一剔除

剔除金融保险: -867, 剩余 49044 观测, 5175 家公司
剔除资不抵债: -1222, 剩余 48689 观测, 5174 家公司
剔除缺失值: -5129, 剩余 43560 观测, 5052 家公司

★最终剔除ST/PT公司: -8480, 剩余 35080 观测, 4303 家公司
   (剔除原因:只要曾被ST过,全部年度均剔除)
# 呈现样本筛选流程表
screening_df = pd.DataFrame(screening_log, 
                            columns=['筛选步骤', '剔除观测数', '剩余观测数', '剩余公司数'])
print("\n" + "=" * 70)
print("样本筛选流程表")
print("=" * 70)
print(screening_df.to_string(index=False))

# 保存筛选表
screening_df.to_csv('output/screening_table.csv', index=False, encoding='utf-8-sig')

======================================================================
样本筛选流程表
======================================================================
       筛选步骤  剔除观测数  剩余观测数  剩余公司数
       初始样本      0  49911   5240
     剔除金融保险    867  49044   5175
    剔除Lev>1   1222  48689   5174
      剔除缺失值   5129  43560   5052
★最终剔除ST/PT★   8480  35080   4303

1.6 行业分类处理

制造业(C开头)用2位行业代码,其他行业用1位代码;样本量<30的制造业子行业合并为”其他制造业”。

# 行业分类处理函数
# 制造业(C开头):使用3位数行业代码(C39、C13、C26等)
# 其他行业:使用1位数行业代码(K70→K,I65→I等)
# 制造业2位数子行业样本量<30 → 合并为"其他制造业"
def classify_industry(code):
    if pd.isna(code) or str(code).strip() == '':
        return 'Unknown'
    code_str = str(code).strip()
    if code_str.startswith('C'):
        return code_str[:3]  # 制造业取前3位,如C39、C13、C26
    else:
        return code_str[0]   # 其他行业取前1位,如K70→K,I65→I

df['ind_code'] = df['industry_code'].apply(classify_industry)

# 统计各行业样本量
ind_counts = df['ind_code'].value_counts()
print("各行业样本量分布:")
print(ind_counts.sort_index())

# 样本量<30的制造业子行业合并为"其他制造业"
small_mfg = ind_counts[(ind_counts < 30) & (ind_counts.index.str.startswith('C'))].index.tolist()
if len(small_mfg) > 0:
    df.loc[df['ind_code'].isin(small_mfg), 'ind_code'] = 'C_OTHER'
    print(f"\n已合并以下制造业子行业: {small_mfg}")

print("\n处理后行业分布:")
print(df['ind_code'].value_counts().sort_index())
各行业样本量分布:
ind_code
A           391
B           676
C13         455
C14         440
C15         394
C17         415
C18         347
C19          69
C20          62
C21         191
C22         307
C23          78
C24         145
C25         119
C26        2280
C27        2309
C28         218
C29         859
C30         909
C31         286
C32         632
C33         716
C34        1391
C35        2262
C36        1238
C37         523
C38        2282
C39        3470
C40         527
C41         147
C42          51
C43           1
D          1068
E           802
F          1634
G          1124
H           101
I          2619
K           974
L           436
M           519
N           540
O            15
P            33
Q            67
R           482
S           161
Unknown     315
Name: count, dtype: int64

已合并以下制造业子行业: ['C43']

处理后行业分布:
ind_code
A           391
B           676
C13         455
C14         440
C15         394
C17         415
C18         347
C19          69
C20          62
C21         191
C22         307
C23          78
C24         145
C25         119
C26        2280
C27        2309
C28         218
C29         859
C30         909
C31         286
C32         632
C33         716
C34        1391
C35        2262
C36        1238
C37         523
C38        2282
C39        3470
C40         527
C41         147
C42          51
C_OTHER       1
D          1068
E           802
F          1634
G          1124
H           101
I          2619
K           974
L           436
M           519
N           540
O            15
P            33
Q            67
R           482
S           161
Unknown     315
Name: count, dtype: int64

1.7 Winsorize异常值处理

对连续变量在截面层面(每年)进行双侧1% Winsorize。

from scipy.stats.mstats import winsorize

# 需要Winsorize的变量
winsor_vars = ['lev', 'npr', 'tang', 'growth', 'ndts', 'liq']

# 保存原始数据用于对比
df_raw = df.copy()

# 按年分别进行Winsorize
df = df.copy()
for var in winsor_vars:
    if var in df.columns:
        df[var] = df.groupby('year')[var].transform(
            lambda x: winsorize(x, limits=[0.01, 0.01])
        )
        print(f"{var} Winsorize完成!")

print("\nWinsorize处理完成!")
lev Winsorize完成!
npr Winsorize完成!
tang Winsorize完成!
growth Winsorize完成!
ndts Winsorize完成!
liq Winsorize完成!

Winsorize处理完成!
# Winsorize前后对比统计
print("=" * 70)
print("Winsorize前后对比")
print("=" * 70)

compare_vars = ['lev', 'npr', 'growth']

for var in compare_vars:
    print(f"\n{var}】")
    print(f"  原始数据 - Mean: {df_raw[var].mean():.4f}, Std: {df_raw[var].std():.4f}, Min: {df_raw[var].min():.4f}, Max: {df_raw[var].max():.4f}")
    print(f"  Winsorize - Mean: {df[var].mean():.4f}, Std: {df[var].std():.4f}, Min: {df[var].min():.4f}, Max: {df[var].max():.4f}")
======================================================================
Winsorize前后对比
======================================================================

【lev】
  原始数据 - Mean: 0.4052, Std: 0.1963, Min: 0.0075, Max: 0.9974
  Winsorize - Mean: 0.4049, Std: 0.1950, Min: 0.0320, Max: 0.8958

【npr】
  原始数据 - Mean: 0.0368, Std: 0.0660, Min: -1.2401, Max: 0.7586
  Winsorize - Mean: 0.0372, Std: 0.0576, Min: -0.3385, Max: 0.2227

【growth】
  原始数据 - Mean: 0.3076, Std: 25.2225, Min: -0.9312, Max: 4719.6119
  Winsorize - Mean: 0.1416, Std: 0.2784, Min: -0.3184, Max: 3.5559

1.8 保存清洗后数据

# ★★★ 剔除行业代码为Unknown的行(原始数据中不存在的公司)★★★
n_before_unknown = len(df)
df = df[df['ind_code'] != 'Unknown']
n_removed = n_before_unknown - len(df)
print(f"剔除 ind_code=Unknown: -{n_removed} 行, 剩余 {len(df)} 行")

# 保存清洗后的面板数据
df.to_csv("data/clean/panel_data.csv", index=False)

# 同时保存为Stata格式(.dta)供Stata回归使用
# 删除controller_type列(避免Stata导出错误)+ st_flag列
df_export = df.drop(columns=['controller_type', 'st_flag'], errors='ignore')
df_export.to_stata("data/clean/panel_data.dta", write_index=False)

print("=" * 60)
print("数据处理完成!")
print("=" * 60)
print(f"最终样本: {len(df):,} 观测, {df['stkcd'].nunique():,} 家公司")
print(f"年份范围: {df['year'].min()} - {df['year'].max()}")
print(f"SOE: 国有企业={df['soe'].sum():,}, 民营企业={(df['soe']==0).sum():,}")
print("\n保存文件:")
print("  - data/clean/panel_data.csv")
print("  - data/clean/panel_data.dta")
print("  - output/screening_table.csv")
剔除 ind_code=Unknown: -315 行, 剩余 34765 行
============================================================
数据处理完成!
============================================================
最终样本: 34,765 观测, 4,046 家公司
年份范围: 2011 - 2025
SOE: 国有企业=12,202, 民营企业=22,563

保存文件:
  - data/clean/panel_data.csv
  - data/clean/panel_data.dta
  - output/screening_table.csv
# 最终数据预览
print("最终数据变量列表:")
print(df.columns.tolist())
print("\n数据前10行:")
print(df.head(10).to_string())
最终数据变量列表:
['stkcd', 'year', 'totalassets', 'total_liabilities', 'fixed_assets', 'current_assets', 'current_liabilities', 'net_profit', 'da', 'controller_type', 'industry_code', 'st_flag', 'm2', 'lev', 'npr', 'size', 'tang', 'ndts', 'asset_lag', 'growth', 'liq', 'soe', 'm2_growth', 'ind_code']

数据前10行:
   stkcd  year   totalassets  total_liabilities  fixed_assets  current_assets  current_liabilities   net_profit           da controller_type industry_code st_flag         m2       lev       npr       size      tang      ndts     asset_lag    growth       liq  soe  m2_growth ind_code
16    11  2011  3.499608e+09       2.368502e+09  6.501154e+07    2.827201e+09         2.239468e+09  257461077.5  35200034.90            国有企业           K70  Normal   85159.09  0.676791  0.073569  21.975917  0.018577  0.010058  2.913281e+09  0.201260  1.262443    1  17.322971        K
17    11  2012  3.950706e+09       2.446687e+09  7.882117e+07    3.190070e+09         2.278634e+09  374822152.4  40838063.78            国有企业           K70  Normal   97414.88  0.619304  0.094875  22.097160  0.019951  0.010337  3.499608e+09  0.128899  1.399992    1  14.391640        K
18    11  2013  3.873253e+09       2.069609e+09  7.530102e+07    3.144604e+09         1.814228e+09  300840563.8  41241930.84            国有企业           K70  Normal  110652.50  0.534334  0.077671  22.077360  0.019441  0.010648  3.950706e+09 -0.019605  1.733302    1  13.588910        K
19    11  2014  3.883288e+09       1.808183e+09  6.406923e+07    3.191649e+09         1.493840e+09  417498679.9  41381958.55            国有企业           K70  Normal  122837.48  0.465632  0.107512  22.079948  0.016499  0.010656  3.873253e+09  0.002591  2.136540    1  11.011934        K
20    11  2015  4.379763e+09       2.278995e+09  8.592952e+07    3.665272e+09         1.999450e+09  156819966.7  39474798.69            国有企业           K70  Normal  139227.81  0.520347  0.035806  22.200261  0.019620  0.009013  3.883288e+09  0.127849  1.833140    1  13.343102        K
21    11  2016  6.654356e+09       4.243059e+09  7.393201e+07    5.628030e+09         4.105670e+09  354857241.7  41916711.86            国有企业           K70  Normal  155006.67  0.637636  0.053327  22.618538  0.011110  0.006299  4.379763e+09  0.519341  1.370795    1  11.333124        K
22    11  2017  5.393332e+09       2.470776e+09  2.934690e+07    4.505840e+09         2.416224e+09  622962734.4  45653783.38            国有企业           K70  Normal  169023.53  0.458117  0.115506  22.408429  0.005441  0.008465  6.654356e+09 -0.174089  1.864827    1   9.042746        K
23    11  2018  5.820202e+09       2.479003e+09  3.261259e+07    4.712264e+09         2.425060e+09  592661752.0  28356746.32            国有企业           K70  Normal  182674.42  0.425931  0.101828  22.484601  0.005603  0.004872  5.393332e+09  0.079148  1.943153    1   8.076325        K
24    11  2019  1.077249e+10       7.505924e+09  9.355778e+07    9.458354e+09         5.200678e+09  742130050.5  51555943.36            国有企业           K70  Normal  198648.88  0.696768  0.068891  23.100262  0.008685  0.004786  5.820202e+09  0.850879  1.818677    1   8.744771        K
25    11  2020  1.220736e+10       8.426235e+09  1.162339e+08    1.059504e+10         4.727260e+09  731337869.7  49348180.23            国有企业           K70  Normal  218679.59  0.690259  0.059910  23.225305  0.009522  0.004042  1.077249e+10  0.133197  2.241264    1  10.083475        K