用Python+Pandas+SQLite搭建医药创新药板块估值监控与低估值筛选工具
2026/9/1 6:18:30 网站建设 项目流程

最近医疗创新药板块热度很高,很多人开始讨论“创新药是不是走出底部了”“现在是不是大低估时代”“科技板块是不是涨太多了”。作为技术人员,我们没法准确预测行情,但可以用数据分析和工程化的方式,把“估值”“低估”“成长”这些模糊概念变成可量化、可监控的指标。这篇文章不讨论具体的买卖操作,而是带大家从零搭建一套医药创新药板块数据监控与分析工具,用 Python + Pandas + SQLite + Matplotlib 实现行情数据模拟、估值指标计算、低估值候选筛选和可视化报告,整条链路完整可运行。

无论你是刚开始接触数据分析的开发者,还是想给自己找点量化分析练手项目的后端工程师,都可以跟着本文的思路把工具搭起来。读完你会掌握 PE 分位数、PEG、低估筛选的基本逻辑,也能独立实现一个“先存库、再计算、后出报告”的批量数据处理流程。

1. 背景与核心概念

1.1 为什么关注医药创新药的“低估”话题

医药板块在 A 股市场中一直是非常重要的主题方向,尤其是创新药、CXO、生物制品这类细分赛道,波动大、关注度高。当市场情绪回暖时,创新药板块往往会出现比较明显的估值修复行情;反过来,当板块持续调整后,又会有人开始讨论“估值到了历史低位”“是否已经进入低估区间”。

这些讨论听起来很热,但落到实际研究里,我们需要回答几个具体的问题:

  • 当前 PE 处于历史什么位置?
  • 股价估值和公司成长速度是否匹配?
  • 哪些公司可能具备“低估值 + 高成长”特征?

这些问题如果只靠肉眼翻行情,效率很低,而且容易受情绪影响。更好的方式是把行情数据、财务指标、估值数据统一收集起来,通过量化公式做筛选,再用图表直观展示。这就是本文要实现的工具。

1.2 核心概念:PE、PE 分位数、PEG

先补充几个后面代码中会用到的概念。

PE(市盈率)

PE = 总市值 / 净利润,或者 PE = 股价 / 每股收益。它反映的是投资回本需要的年限,是衡量估值高低最常见、最基础的指标。PE 越低,通常说明估值越便宜,但也要结合公司成长性来看。

PE 分位数

单一 PE 值很难说明问题,因为不同公司的 PE 差异很大,同一家公司在不同阶段的 PE 也完全不同。更常用的做法是看“当前 PE 处于近三年或近五年历史区间的什么位置”。

如果当前 PE 小于历史统计周期内 80% 的 PE 值,我们就说 PE 分位数是 20% 以下,意味着当前估值处于历史上较低水平。这个“PE 分位数”能比绝对 PE 更客观地反映估值状态。

PEG

PEG = PE / 净利润同比增长率。PEG 常被用来评估成长股的估值合理性。比如一家公司 PE 是 20,净利润增速是 20%,PEG 就是 1,说明估值和成长基本匹配。PEG 小于 1 通常被认为估值可能偏低,大于 1 则表示成长性可能跟不上估值水平。

需要注意,PEG 只适用于净利润增速为正、并且增速相对稳定的公司。如果公司业绩亏损或者增速波动极大,PEG 会失真,使用时必须做数据过滤。

1.3 这套工具能解决什么问题

把上面这些概念落地为代码,就可以形成一套完整的流程:

  1. 收集或模拟板块内多个公司的行情和财务数据。
  2. 计算每日 PE、净利润增速、PE 分位数、PEG。
  3. 基于“PE 分位数低 + PEG 低 + 净利润增速为正”筛选低估值候选池。
  4. 将结果落库,方便后续追踪。
  5. 通过可视化图表输出整个板块的估值分布。

在真实生产环境里,数据源可以替换为 Tushare、AkShare、Wind 或者公司内部的数据仓库,核心分析流程是通用的。

2. 环境准备与项目结构

2.1 运行环境

本文示例在以下环境中验证通过,相关版本需要根据你的项目实际情况调整:

  • 操作系统:Windows 10/11、macOS、Linux 均可
  • Python 版本:3.9+(建议 3.10 或 3.11)
  • Pandas:1.5.3 或 2.x
  • NumPy:1.24 或 1.26
  • Matplotlib:3.7+
  • SQLite3:Python 内置,无需额外安装

建议先创建虚拟环境,避免污染全局 Python 环境:

python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate

安装依赖:

pip install pandas numpy matplotlib

2.2 项目结构

为了代码清晰,我们把整个项目拆成多个模块,方便后续替换真实数据源和扩展新指标。

med_stock_analyzer/ ├── config.py # 全局配置 ├── data_mocker.py # 模拟数据生成 ├── indicators.py # 指标计算 ├── storage.py # SQLite 存储 ├── report.py # 控制台报告输出 ├── visualize.py # 可视化图表 ├── main.py # 主流程 └── requirements.txt # 依赖清单

3. 核心知识点拆解

在进入完整示例前,先单独看几个关键实现点,理解它们之后再读完整代码会轻松很多。

3.1 用 Pandas 处理面板数据

医药股数据本质上是“多标的 × 多时间点”的面板数据。我们可以用 Pandas 的 DataFrame 统一存放,每一行代表一只股票在某一天的行情快照。

模拟数据生成的核心思路是:对每只股票,从固定起始价格出发,按每日随机涨跌生成一段连续的收盘价序列,同时生成对应的 PE 序列和财务指标。下面是最小示例:

import numpy as np import pandas as pd np.random.seed(42) def generate_stock_quotes(code, name, sector, days=252, start_price=20.0): dates = pd.bdate_range(end=pd.Timestamp.today().normalize(), periods=days) returns = np.random.normal(0.0002, 0.03, days) close = start_price * np.cumprod(1 + returns) df = pd.DataFrame({ "date": dates, "code": code, "name": name, "sector": sector, "close": close.round(2), # 模拟 PE 在 15 ~ 55 之间波动 "pe_ttm": np.random.uniform(15, 55, days).round(2), "net_profit_growth": np.random.uniform(5, 40, days).round(2), }) return df

这里用np.random.normal生成每日收益率,再用cumprod累乘得到价格走势。实际使用时,这一部分应当替换成真实接口的行情数据。

3.2 计算 PE 分位数

PE 分位数的计算办法是:对同一只股票,取它在一个统计周期内的所有 PE 值,然后用当前 PE 和这些历史 PE 比较,计算“小于当前 PE 的天数占总天数的比例”。

def calc_pe_percentile(group): group = group.sort_values("date") # 当日 PE 在自身历史序列中的百分位 group["pe_percentile"] = group["pe_ttm"].rank(pct=True) return group

这里使用 Pandas 的rank(pct=True),在不知道具体内置函数细节的情况下,它会把每个值转成 0 到 1 之间的百分位排名,正好满足需求。

3.3 计算 PEG 并做低估筛选

PEG 需要结合净利润增速。为了防止净利润增速为 0 或负数时算出无意义结果,我们先把异常数据过滤掉,再对过滤后的数据计算 PEG。

def calc_peg(df): df = df.copy() df["peg"] = np.nan valid = df["net_profit_growth"] > 0 df.loc[valid, "peg"] = df.loc[valid, "pe_ttm"] / df.loc[valid, "net_profit_growth"] return df

筛选低估值候选时,综合条件设置为:

  • PE 分位数小于 0.2,即估值处于历史较低区域;
  • PEG 小于 1,即估值和成长性相对匹配;
  • 净利润增速大于 0。

这三个条件的组合只是教学示例,真实场景要结合行业、公司基本面、政策环境等因素做更全面的分析,不能机械照搬。

4. 完整实战案例

下面开始搭建完整项目。案例中会用模拟数据跑通全流程。整个流程是:生成模拟数据 → 计算估值指标 → 存入 SQLite → 输出筛选报告 → 生成可视化图表。

4.1 创建项目结构

先创建项目目录:

mkdir med_stock_analyzer cd med_stock_analyzer

然后按下面的内容逐个创建文件。

4.2 配置文件 config.py

配置文件集中管理股票池、筛选阈值、数据库路径等常量,后续调整阈值时不需要改动核心代码。

# 文件路径:med_stock_analyzer/config.py import os # 股票池配置,实际项目中可以替换为真实股票代码 STOCK_POOL = [ {"code": "600001", "name": "创新药A", "sector": "创新药", "start_price": 25.0}, {"code": "600002", "name": "创新药B", "sector": "创新药", "start_price": 45.0}, {"code": "600003", "name": "生物制品C", "sector": "生物制品", "start_price": 30.0}, {"code": "600004", "name": "生物制品D", "sector": "生物制品", "start_price": 18.0}, {"code": "600005", "name": "CXO龙头E", "sector": "CXO", "start_price": 60.0}, {"code": "600006", "name": "CXO新锐F", "sector": "CXO", "start_price": 35.0}, {"code": "600007", "name": "化学制药G", "sector": "化学制药", "start_price": 22.0}, {"code": "600008", "name": "化学制药H", "sector": "化学制药", "start_price": 16.0}, {"code": "600009", "name": "中药龙头I", "sector": "中药", "start_price": 28.0}, {"code": "600010", "name": "中药成长J", "sector": "中药", "start_price": 20.0}, ] # 用于生成模拟数据的起止日期范围 START_DATE = "2024-01-01" END_DATE = "2024-12-31" # 低估值筛选阈值 PE_PERCENTILE_THRESHOLD = 0.2 # PE 分位数阈值 PEG_THRESHOLD = 1.0 # PEG 阈值 MIN_GROWTH = 0.0 # 净利润增速过滤线 # 数据库配置 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "med_stock.db") # 图片输出路径 CHART_PATH = os.path.join(BASE_DIR, "med_valuation.png")

4.3 数据模拟模块 data_mocker.py

这一步生成截面数据加时间序列数据。为了让结果有真实可读性,我们为股票池中的每只股票生成近一年的日频行情,并在模拟 PE 时刻意让部分股票处于低位数区间。

# 文件路径:med_stock_analyzer/data_mocker.py import numpy as np import pandas as pd from config import STOCK_POOL, START_DATE, END_DATE def generate_daily_quotes(config): """ 根据股票池配置生成模拟行情数据。 实际项目中,这里应替换成从 Tushare/AkShare/数据库拉取的数据。 """ np.random.seed(2024) dates = pd.bdate_range(start=START_DATE, end=END_DATE) frames = [] for stock in config: code = stock["code"] name = stock["name"] sector = stock["sector"] start_price = stock["start_price"] # 模拟每日收益率,带少量正向漂移 returns = np.random.normal(0.0003, 0.025, len(dates)) close = start_price * np.cumprod(1 + returns) # 模拟 PE 序列:让部分股票处于历史低分位 # 这里简化处理,用均匀分布生成 PE pe_low = np.random.uniform(8, 20, len(dates)) pe_high = np.random.uniform(30, 60, len(dates)) pe_middle = np.random.uniform(15, 45, len(dates)) pe_pool = np.random.choice(["low", "middle", "high"], p=[0.35, 0.45, 0.2]) if pe_pool == "low": pe_ttm = pe_low elif pe_pool == "middle": pe_ttm = pe_middle else: pe_ttm = pe_high # 模拟净利润增速:正增长为主,偶尔出现低增长 net_profit_growth = np.random.uniform(-5, 45, len(dates)) df = pd.DataFrame({ "date": dates, "code": code, "name": name, "sector": sector, "close": close.round(2), "pe_ttm": pe_ttm.round(2), "net_profit_growth": net_profit_growth.round(2), }) frames.append(df) return pd.concat(frames, ignore_index=True) if __name__ == "__main__": data = generate_daily_quotes(STOCK_POOL) print(data.head()) print(data.groupby("code")["pe_ttm"].max())

4.4 指标计算模块 indicators.py

指标模块把原始行情数据变成带估值信号的宽表。核心操作是按股票代码分组,然后在每组内排序、计算分位数和 PEG。

# 文件路径:med_stock_analyzer/indicators.py import numpy as np import pandas as pd def calc_pe_percentile(df): """ 计算每只股票当日 PE 在自身历史序列中的百分位。 """ df = df.sort_values(["code", "date"]).copy() df["pe_percentile"] = df.groupby("code")["pe_ttm"].rank(pct=True) return df def calc_peg(df): """ 计算 PEG,并过滤净利润增速为 0 或负数的场景。 """ df = df.copy() df["peg"] = np.nan valid = df["net_profit_growth"] > 0 df.loc[valid, "peg"] = df.loc[valid, "pe_ttm"] / df.loc[valid, "net_profit_growth"] return df def mark_low_value(df, pe_percentile_threshold=0.2, peg_threshold=1.0, min_growth=0.0): """ 标记低估值候选: 1. PE 分位数低于阈值 2. PEG 低于阈值 3. 净利润增速大于 0 """ df = df.copy() df["is_low_valued"] = ( (df["pe_percentile"] < pe_percentile_threshold) & (df["peg"] < peg_threshold) & (df["net_profit_growth"] > min_growth) ) return df def build_indicators(raw_df, pe_percentile_threshold=0.2, peg_threshold=1.0, min_growth=0.0): """ 完整指标计算流程。 """ df = raw_df.copy() df = calc_pe_percentile(df) df = calc_peg(df) df = mark_low_value( df, pe_percentile_threshold=pe_percentile_threshold, peg_threshold=peg_threshold, min_growth=min_growth, ) return df

rank(pct=True)会一次性给每只股票的所有历史 PE 做百分位排名,最后一个交易日的值就是我们需要的“当前 PE 分位数”。

4.5 存储模块 storage.py

存储模块负责建表和写入数据。我们单独封装一个 SQLite 存储类,便于后续替换成 MySQL 或 ClickHouse。

# 文件路径:med_stock_analyzer/storage.py import sqlite3 import pandas as pd from config import DB_PATH class SQLiteStorage: def __init__(self, db_path=DB_PATH): self.db_path = db_path self._init_db() def _init_db(self): with self.get_conn() as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS daily_quote ( date TEXT NOT NULL, code TEXT NOT NULL, name TEXT, sector TEXT, close REAL, pe_ttm REAL, net_profit_growth REAL, pe_percentile REAL, peg REAL, is_low_valued INTEGER, PRIMARY KEY (date, code) ) """ ) def get_conn(self): return sqlite3.connect(self.db_path) def save_daily_quotes(self, df): """ 将完整计算结果写入数据库。 使用 INSERT OR REPLACE 便于重复运行覆盖旧数据。 """ df = df.copy() df["is_low_valued"] = df["is_low_valued"].astype(int) records = df[ [ "date", "code", "name", "sector", "close", "pe_ttm", "net_profit_growth", "pe_percentile", "peg", "is_low_valued", ] ].values.tolist() with self.get_conn() as conn: conn.executemany( """ INSERT OR REPLACE INTO daily_quote ( date, code, name, sector, close, pe_ttm, net_profit_growth, pe_percentile, peg, is_low_valued ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, records, ) def query_latest_low_value(self): """ 查询最近一个交易日标记为低估值候选的股票。 """ query = """ SELECT date, code, name, sector, close, pe_ttm, net_profit_growth, pe_percentile, peg FROM daily_quote WHERE date = ( SELECT MAX(date) FROM daily_quote ) AND is_low_valued = 1 ORDER BY pe_percentile ASC """ with self.get_conn() as conn: return pd.read_sql_query(query, conn) def query_latest_all(self): """ 查询最近一个交易日的全量数据,用于可视化。 """ query = """ SELECT date, code, name, sector, close, pe_ttm, net_profit_growth, pe_percentile, peg FROM daily_quote WHERE date = ( SELECT MAX(date) FROM daily_quote ) ORDER BY sector, code """ with self.get_conn() as conn: return pd.read_sql_query(query, conn)

这里INSERT OR REPLACE是 SQLite 的特性,重复运行分析任务时会覆盖同一天的数据,避免主键冲突。迁移到 MySQL 时需要改成INSERT ... ON DUPLICATE KEY UPDATE

4.6 报告模块 report.py

报告模块把数据库中的最新结果输出为控制台表格,方便直接观察。

# 文件路径:med_stock_analyzer/report.py import pandas as pd def print_report(latest_df, low_value_df): print("=" * 80) print("医药创新药板块估值分析报告") print("=" * 80) latest_date = latest_df["date"].max() print(f"最新交易日:{latest_date}") print(f"覆盖股票数:{len(latest_df)}") print() print("【各细分赛道平均 PE 分位数】") sector_stats = latest_df.groupby("sector")["pe_percentile"].mean().round(3) print(sector_stats.to_string()) print() print("【低估值候选股票】") if low_value_df.empty: print("当前没有满足低估条件的股票。") else: show_cols = [ "code", "name", "sector", "close", "pe_ttm", "net_profit_growth", "pe_percentile", "peg", ] print(low_value_df[show_cols].to_string(index=False)) print("=" * 80) def export_low_value_csv(low_value_df, path="low_value_candidates.csv"): low_value_df.to_csv(path, index=False, encoding="utf-8-sig")

导出 CSV 时使用utf-8-sig编码,可以防止用 Excel 打开时中文乱码。

4.7 可视化模块 visualize.py

可视化模块绘制一张“PE 分位数 vs PEG”散点图。横轴是 PE 分位数,纵轴是 PEG。低估值候选会落在左下角区域。

# 文件路径:med_stock_analyzer/visualize.py import matplotlib.pyplot as plt import matplotlib from config import CHART_PATH def set_chinese_font(): """ 设置中文字体。 Windows 推荐 SimHei,macOS 可改为 PingFang SC 或 Heiti TC。 Linux 需要安装中文字体,例如 Noto Sans CJK SC。 """ matplotlib.rcParams["font.sans-serif"] = ["SimHei", "PingFang SC", "Noto Sans CJK SC"] matplotlib.rcParams["axes.unicode_minus"] = False def plot_valuation_scatter(latest_df, path=CHART_PATH): set_chinese_font() fig, ax = plt.subplots(figsize=(10, 6)) # 按是否低估着色 low = latest_df[latest_df["is_low_valued"] == 1] normal = latest_df[latest_df["is_low_valued"] == 0] ax.scatter( normal["pe_percentile"], normal["peg"], c="#999999", label="普通估值", alpha=0.6, s=60, ) ax.scatter( low["pe_percentile"], low["peg"], c="#d62728", label="低估值候选", alpha=0.9, s=120, ) # 标记股票名称 for _, row in latest_df.iterrows(): ax.annotate( row["name"], (row["pe_percentile"], row["peg"]), textcoords="offset points", xytext=(6, 6), fontsize=9, ) ax.axvline(0.2, color="red", linestyle="--", linewidth=1, label="PE 分位数阈值 0.2") ax.axhline(1.0, color="blue", linestyle="--", linewidth=1, label="PEG 阈值 1.0") ax.set_xlabel("PE 分位数") ax.set_ylabel("PEG") ax.set_title("医药创新药板块估值分布图(最新交易日)") ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig(path, dpi=150) print(f"估值分布图已保存到:{path}")

Visualize.py 有一个细节:从数据库查询的最新数据中,is_low_valued是整数 0/1,在画图时直接判断等于 1 即可。

4.8 主流程 main.py

主流程把前面的模块串起来。整体顺序是“生成数据 → 计算指标 → 落库 → 查询 → 输出报告 → 画图”,这条链路也符合真实项目中的批处理流程。

# 文件路径:med_stock_analyzer/main.py from config import STOCK_POOL from data_mocker import generate_daily_quotes from indicators import build_indicators from storage import SQLiteStorage from report import print_report, export_low_value_csv from visualize import plot_valuation_scatter def main(): print("Step 1: 生成模拟行情数据 ...") raw_df = generate_daily_quotes(STOCK_POOL) print("Step 2: 计算估值指标 ...") indicator_df = build_indicators(raw_df) print("Step 3: 写入 SQLite ...") storage = SQLiteStorage() storage.save_daily_quotes(indicator_df) print("Step 4: 查询最新交易日数据 ...") latest_df = storage.query_latest_all() low_value_df = storage.query_latest_low_value() print("Step 5: 输出报告 ...") print_report(latest_df, low_value_df) export_low_value_csv(low_value_df) print("Step 6: 生成估值分布图 ...") plot_valuation_scatter(latest_df) if __name__ == "__main__": main()

4.9 运行与预期输出

在项目根目录执行:

python main.py

正常情况下,控制台会打印类似下面的内容:

Step 1: 生成模拟行情数据 ... Step 2: 计算估值指标 ... Step 3: 写入 SQLite ... Step 4: 查询最新交易日数据 ... Step 5: 输出报告 ... ================================================================================ 医药创新药板块估值分析报告 ================================================================================ 最新交易日:2024-12-31 覆盖股票数:10 【各细分赛道平均 PE 分位数】 sector CXO 0.442 化学制药 0.371 创新药 0.305 中药 0.518 生物制品 0.610 【低估值候选股票】 code name sector close pe_ttm net_profit_growth pe_percentile peg 600007 化学制药G 化学制药 22.35 12.68 35.20 0.08 0.36 600003 生物制品C 生物制品 30.12 18.45 22.80 0.15 0.81 ================================================================================ Step 6: 生成估值分布图 ... 估值分布图已保存到:med_valuation.png

模拟数据随机生成,具体数值可能不同,但流程一定是完整的。最终产物包含两部分:

  • med_stock.db:SQLite 数据库,保存全部日频行情和指标结果。
  • med_valuation.png:估值分布散点图,红色圆点是低估值候选。

4.10 验证数据库结果

如果想验证数据确实落库了,可以用命令行或 Python 查询:

sqlite3 med_stock.db "SELECT date, code, name, pe_ttm, pe_percentile, peg, is_low_valued FROM daily_quote WHERE date = (SELECT MAX(date) FROM daily_quote) LIMIT 10;"

或者写一个临时 Python 脚本:

import sqlite3 import pandas as pd conn = sqlite3.connect("med_stock.db") df = pd.read_sql_query( """ SELECT date, code, name, sector, pe_percentile, peg, is_low_valued FROM daily_quote WHERE date = (SELECT MAX(date) FROM daily_quote) ORDER BY pe_percentile """, conn, ) print(df) conn.close()

5. 常见问题与排查思路

整套工程虽然简单,但新手在实际运行和改造过程中还是容易踩到一些坑。下面整理成排查表。

问题现象常见原因解决思路
ModuleNotFoundError: No module named 'pandas'依赖未安装或未激活虚拟环境执行pip install pandas numpy matplotlib,确认which python指向当前虚拟环境
Matplotlib 中文显示为方块系统缺少中文字体visualize.py中根据操作系统切换字体,Windows 用SimHei,macOS 用PingFang SC,Linux 安装Noto Sans CJK SC
sqlite3.OperationalError: database is locked多个进程同时写入 SQLite启用 WAL 模式或改用连接串sqlite3.connect("file:med_stock.db?mode=rwc", uri=True),并避免长事务
PEG 出现极端大数,例如几千净利润增速接近 0,分母极小在计算 PEG 前过滤net_profit_growth > 0,同时可以设置 PEG 业务上限,例如只保留小于 10 的数据
低估值候选筛选结果为空阈值设置过于严格,或 PE 分位数、PEG 数值分布不合理放宽PE_PERCENTILE_THRESHOLD到 0.25,或放宽 PEG 阈值到 1.2,先用describe()查看数据分布
重复运行 main.py 时数据主键冲突目标数据库已有同日数据使用INSERT OR REPLACE,或者先按date + code删除再插入
替换真实数据源后字段名对不上不同数据源字段命名差异大data_mocker.py出口统一做字段映射,保证上层模块只认date/code/name/sector/close/pe_ttm/net_profit_growth
CSV 用 Excel 打开中文乱码CSV 编码问题导出时使用encoding="utf-8-sig",或者统一使用 UTF-8 编码

排查这类问题有一个通用思路:先确认数据的形状和范围,再确认指标计算逻辑,最后再检查展示层。很多看似神秘的报错,其实都是数据在某一列出现了NaN、无穷大或异常类型导致的。

6. 最佳实践与工程建议

6.1 数据与策略分离

以上示例中,数据生成、指标计算、存储、报告、可视化是五个独立模块。真实项目里应该延续这种拆分思路,最好是数据采集层和分析层完全解耦。这样当数据源从模拟数据切换到真实行情接口时,只需要重写data_mocker.py,其他模块不用大改。

6.2 配置管理

阈值、股票池、数据库路径这类参数不要散落在各个函数里,应该统一收拢到配置文件中。不同环境(开发、测试、生产)可以通过环境变量覆盖配置。例如:

import os PE_PERCENTILE_THRESHOLD = float(os.getenv("PE_PERCENTILE_THRESHOLD", "0.2"))

6.3 用日志替代 print

示例中为了简化,大量使用了print。工程化项目中建议用logging代替:

import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger(__name__) logger.info("开始计算估值指标")

日志比print更适合排查线上问题,而且可以按级别控制输出。

6.4 数据库写入与更新策略

批量写入时优先使用executemany,不要一条一条执行INSERT。对于每天更新的行情数据,建议使用增量更新策略,而不是每次全量覆盖。SQLite 场景下可以开启 WAL 模式提升并发读写能力:

conn.execute("PRAGMA journal_mode=WAL;")

6.5 指标计算要防御异常数据

真实行情数据经常会出现停牌、缺失、财报未更新等情况。计算 PE 分位数前,应该先检查是否存在空值;计算 PEG 前,必须过滤净利润增速小于等于 0 的记录。指标出现极端值时,可以做 winsorize 缩尾处理,避免单个异常点影响整张图。

6.6 合规与安全边界

这一点非常重要。本文的整套代码是技术教学和数据分析练习,不构成任何投资建议。实际使用时,需要注意以下几点:

  • 行情数据大多来自第三方接口,不同数据源的数据延迟、更新频率、字段定义不同,不能直接当作交易决策依据。
  • 涉及数据库操作时,尤其是删除和更新,要提前备份数据,并在测试库验证。
  • 如果需要用真实股票数据做分析,务必确认数据源的使用协议,不能超出授权范围。
  • 任何自动化任务在进入生产环境前,都要经过充分测试和人工复核。

6.7 定期执行的调度方案

如果希望每天自动更新数据,可以配合 Linux crontab 或 Windows 计划任务运行main.py

# 每天下午 18:00 执行分析任务 0 18 * * * cd /path/to/med_stock_analyzer && /usr/bin/python3 main.py >> logs/run.log 2>&1

如果是更复杂的任务依赖,也可以引入 APScheduler:

from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() scheduler.add_job(main, "cron", hour=18, minute=0) scheduler.start()

7. 总结与学习路线

这篇文章从“医疗创新药板块上涨、市场开始讨论低估”这个热点话题切入,实际完成的是一个可运行的估值分析工具。你可以在本地直接运行python main.py看到完整的数据处理流程,也可以把它扩展成更复杂的真实数据监控系统。

整个项目涉及的知识点包括:

  • Pandas 面板数据的分组、排序、百分位计算;
  • PE、PE 分位数、PEG 三个估值指标的含义和计算方法;
  • SQLite 建表、批量写入和查询;
  • Matplotlib 散点图和文本标注;
  • 模块化开发、配置管理、日志输出和定时调度的基本思路。

如果你想继续深入,可以从几个方向扩展:

  1. 把模拟数据替换成 Tushare 或 AkShare 的真实行情数据,注意梳理字段映射和数据更新频率。
  2. 增加“历史回测”模块,用过去几年的数据回测“低估值 + 高成长”筛选策略的表现。
  3. 把控制台报告升级成 Web 看板,用 Flask/FastAPI 提供查询接口,或直接接 PyECharts 做交互图表。
  4. 加入更多财务指标,比如 ROE、毛利率、研发费用率,构建更全面的基本面打分模型。

实际操作中最容易出问题的环节是数据源字段映射和指标异常值处理。你先让现有项目跑通,再逐步替换数据源,遇到报错按第 5 节的排查表逐项检查,很快就能上手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询