← 返回第2周学习计划
第2周 · 12行教学数据 · 先检查再修复

第2周跟练:把脏CSV变成有依据的数据

接着第1周做。仍按5个单元、每次约2小时安排;本周只增加pandas。你要能解释每一次删除、填值和保留缺失的理由,并留下可复现的记录。

本周目标:每一步都能解释

原始CSV → 体检报告 → 有依据的修复 → 检查唯一键 → 分组收益

继续使用首月的prices_raw.csv、corrections.csv和transfer_missing.csv。本包另有未在跟练中演示的transfer_exam.csv,留到隔日验收。所有数据均为虚构,日期只是教学标签。

原始文件留存;在副本上处理;没有依据的价格继续待查。不是每个NaN都该被消除,也不是程序成功退出就说明数据可信。

准备:用同一个Python安装与运行pandas

  1. 解压并打开学习文件夹

    下载并“全部解压”,在VS Code打开直接包含week2.py、prices_raw.csv、requirements.txt的目录。本包15个文件,输出与环境稍后生成。不会配置Python时,先回第1周环境教程。

  2. 安装本周唯一新增的库

    打开PowerShell终端。在当前学习目录创建环境;已有本目录.venv则跳过第一行。若python不可用但py可用,第一行用py替换。后续命令使用同一条解释器路径。

    powershell
    python -m venv .venv
    .\.venv\Scripts\python.exe -m pip install -r requirements.txt
    .\.venv\Scripts\python.exe -c "import sys,pandas; print(sys.executable); print(pandas.__version__)"
    .\.venv\Scripts\python.exe -m pip freeze > requirements-lock.txt

    requirements.txt要求pandas 2.2以上、4以下;pip会按Python兼容性选版本。requirements-lock.txt记录本次实际安装版本。无需激活,也不用更改PowerShell策略。

    检查点:路径指向这个项目的.venv,且能输出pandas版本。终端命令不要写进.py文件。

  3. 确认文件与字段

    powershell
    Get-Location
    Get-ChildItem -Name
    .\.venv\Scripts\python.exe .\01_read.py

    date是教学日期,symbol是标的文字代码,close是教学价格;目标唯一键是date+symbol。CSV是逗号分隔的文本,不要求安装Excel。

    macOS/Linux:用python3创建环境,后续解释器换成./.venv/bin/python,文件参数去掉.\前缀。浏览器练习无需本机安装,但不能替代文件输出与多文件测试。

单元1 · 读进来,先看清表

跟着做

先在VS Code中打开prices_raw.csv看原始文字,再运行01_read.py。逐项看文件路径、shape、head、dtypes以及缺失价格对应的行。read_prices定义在同目录week2.py中,下方展示实际函数。

powershell
.\.venv\Scripts\python.exe .\01_read.py

pd.read_csv把CSV读成DataFrame;一行是一条记录,一列是一个字段。date转成可排序的日期,symbol按文字读取,close转数值。errors='raise'让非法文字报错,避免混淆“本来缺失”和“转换失败”。

查看本步骤的注释函数

这是week2.py中的实际片段,由步骤脚本调用;依赖导入和共用常量已在下载包中提供。不要只复制片段运行。

python
def read_prices(source):
    # symbol按文字读,避免000001变成1;utf-8-sig兼容UTF-8的BOM。
    # keep_default_na=False保留原始文字,只把明确的空价格转为缺失。
    frame = pd.read_csv(source, encoding="utf-8-sig", dtype="string", keep_default_na=False)
    # 多出未命名字段时,pandas可能把它当索引;本教程不接受这种隐式解释。
    if not isinstance(frame.index, pd.RangeIndex):
        raise ValueError("发现隐式索引,请检查CSV每行字段数是否与表头一致")
    if set(frame.columns) != set(COLS):
        raise ValueError("价格表必须只有date、symbol、close三列,请检查表头和分隔符")
    if frame.empty or frame[KEYS].apply(lambda col: col.str.strip().eq("")).any().any():
        raise ValueError("价格表不能为空,date和symbol不能缺失")
    frame["date"] = pd.to_datetime(frame["date"], format="%Y-%m-%d", errors="raise")
    if frame["date"].isna().any():
        raise ValueError("日期不能为NaT")
    # errors='raise'让文字脏值报错,不把oops悄悄变成缺失。
    frame["close"] = pd.to_numeric(frame["close"].replace("", pd.NA), errors="raise").astype(float)
    if not frame["close"].dropna().map(math.isfinite).all():
        raise ValueError("价格不能为无穷大")
    return frame[COLS]
查看运行入口:01_read.py
python
from pathlib import Path
import pandas as pd
from week2 import read_prices

ROOT = Path(__file__).resolve().parent  # 脚本所在目录,不是终端碰巧所在目录。
print("pandas版本:", pd.__version__)
print("读取文件:", ROOT / "prices_raw.csv")

# read_prices里的CSV读取和类型转换在教程逐行展示。
frame = read_prices(ROOT / "prices_raw.csv")
print("行数、列数:", frame.shape)  # shape与dtypes是属性,不加()。
print(frame.head())
print(frame.dtypes)
print("缺失价格对应的行:")
print(frame.loc[frame["close"].isna()])
# 本步骤只读取和观察,不输出清洗结果。

应看到什么

12行3列;date是日期类型、symbol是字符串类型、close是数值类型;B在01-06的价格缺失。pandas版本不同,dtype显示名称可能不同,不要求逐字一致。

停下来检查:能指出date+symbol为什么是预期唯一键;能解释为什么000001这类代码不能随便读成整数。只观察,不在Excel里手动覆盖原始文件。

留下的证据:补充data-dictionary.md:字段含义、类型、允许缺失与否、还不知道的信息。

单元2 · 区分重复、冲突和缺失

跟着做

读q-065,再运行02_audit.py。先手工圈出A的01-06两行与01-07两行,对照下方体检表。分别打印duplicated(..., keep=False)和keep='first'的结果,观察全部涉及行与额外行的差别。

powershell
.\.venv\Scripts\python.exe .\02_audit.py

完全重复是date、symbol、close都相同;键冲突是同一个date+symbol对应不一致价格。nunique(dropna=False)在组内数不同状态,缺失也算一种状态。isna统计缺失;close<=0检查非正价格,二者不是同一个条件。

查看本步骤的注释函数

这是week2.py中的实际片段,由步骤脚本调用;依赖导入和共用常量已在下载包中提供。不要只复制片段运行。

python
def audit(frame):
    # True在求和时计为1。默认keep='first'只数额外行。
    # dropna=False把缺失视为一种状态,避免“一个有效值+一个缺失”被漏报。
    states = frame.groupby(KEYS)["close"].nunique(dropna=False)
    return {
        "rows": len(frame),
        "exact_duplicate_extra": int(frame.duplicated(COLS, keep="first").sum()),
        "key_duplicate_extra": int(frame.duplicated(KEYS, keep="first").sum()),
        "conflict_groups": int((states > 1).sum()),
        "missing_close": int(frame["close"].isna().sum()),
        "nonpositive_close": int((frame["close"] <= 0).sum()),
    }
查看运行入口:02_audit.py
python
from pathlib import Path
from week2 import KEYS, audit, read_prices

ROOT = Path(__file__).resolve().parent
frame = read_prices(ROOT / "prices_raw.csv")
for name, count in audit(frame).items():
    print(name, count)

# keep=False显示全部涉及行;keep='first'只标出需要额外计数的行。
print("重复键涉及的全部行:")
print(frame.loc[frame.duplicated(KEYS, keep=False)].sort_values(KEYS))
print("说明:本样本额外重复键为2行,全部涉及行为4行。")

应看到什么

总行12;完全重复额外1;键重复额外2;冲突键组1;缺失价格1;非正价格1。重复键全部涉及4行,但额外只多2行。

停下来检查:解释为什么不能直接把异常数量相加再从12里减掉。只按symbol去重会怎样?它会错误删除同一标的其他日期的正常记录。

留下的证据:把6个统计及其定义写进自己的audit.md;保留被检查的具体行。

单元3 · 有依据才修复,每一步留日志

跟着做

先读corrections.csv的verified_close与reason,再展开修复函数,最后运行03_clean.py。打开outputs/change-log.csv,逐条对照旧值、新值和理由;最后核对cleaned.csv。

powershell
.\.venv\Scripts\python.exe .\03_clean.py

先在副本上删除完全重复的额外行;再按date+symbol定位核验记录,把冲突、缺失和零价改为核验值。只有核验后相同的记录才合并。没有依据的剩余冲突或缺失会报错,阻止输出。

查看本步骤的注释函数

这是week2.py中的实际片段,由步骤脚本调用;依赖导入和共用常量已在下载包中提供。不要只复制片段运行。

python
def clean_prices(raw, fixes):
    if fixes[KEYS].isna().any().any() or fixes.duplicated(KEYS).any():
        raise ValueError("核验表键缺失或重复,必须先查清")
    if fixes["reason"].fillna("").str.strip().eq("").any():
        raise ValueError("核验表必须写明reason")
    verified = fixes["verified_close"]
    if verified.isna().any() or not verified.map(math.isfinite).all() or (verified <= 0).any():
        raise ValueError("核验价格必须是有限正数")
    work = raw.copy(deep=True)  # 修复副本,原始数据留作证据。
    logs = []

    # 只删除“所有业务列完全相同”的额外行,先把删除写入日志。
    duplicate = work.duplicated(COLS, keep="first")
    for row in work.loc[duplicate].itertuples():
        logs.append({
            "action": "drop_exact_duplicate", "date": row.date, "symbol": row.symbol,
            "old_values": json.dumps([None if pd.isna(row.close) else row.close]),
            "new_value": row.close, "rows_before": 1, "rows_after": 0,
            "reason": "该额外行与保留行的date、symbol、close完全一致",
        })
    work = work.loc[~duplicate].copy()

    for fix in fixes.itertuples(index=False):
        # 用两个键定位,不能只用symbol覆盖所有日期。
        mask = (work["date"] == fix.date) & (work["symbol"] == fix.symbol)
        before = work.loc[mask, "close"]
        if before.empty:
            raise ValueError("核验键未出现在原表,请确认拿的是同一份数据")
        if len(before) == 1 and before.iloc[0] == fix.verified_close:
            continue  # 已经正确时不虚构一次改动。
        old = [None if pd.isna(value) else float(value) for value in before]
        logs.append({
            "action": "verified_repair", "date": fix.date, "symbol": fix.symbol,
            "old_values": json.dumps(old), "new_value": fix.verified_close,
            "rows_before": len(before), "rows_after": 1, "reason": fix.reason,
        })
        work.loc[mask, "close"] = fix.verified_close
        # 此时这些行已由同一条核验依据确认相同,才可以合并。
        work = work.drop_duplicates(COLS, keep="first")

    validate_clean(work)  # 无核验依据的剩余问题在这里阻断输出。
    work = work.sort_values(["symbol", "date"]).reset_index(drop=True)
    columns = ["action", "date", "symbol", "old_values", "new_value",
               "rows_before", "rows_after", "reason"]
    return work, pd.DataFrame(logs, columns=columns)
查看运行入口:03_clean.py
python
from week2 import main

# 先阅读教程中的clean_prices;本文件调用同一套完整流程。
# 成功后在outputs生成cleaned.csv、returns.csv、change-log.csv、audit.md。
# 未解决的缺失/冲突会报错,不会静默输出“干净数据”。
main()

应看到什么

原始12行→去完全重复后11行→按核验表处理后10行。生成4条日志:删除1条完全重复额外行;核验冲突组1次;补缺失1次;纠正零价1次。日志4条不代表删除4行。

停下来检查:解释old_values中的null代表修复前缺失;说出4条日志各自的依据。核验表是事后教学资料,不能冒充历史回测时已经知道的信息。

留下的证据:outputs/中的cleaned.csv、returns.csv、change-log.csv、audit.md。原始文件不变;重跑会更新生成结果。

单元4 · 排序、分组,再算收益

跟着做

先读q-312,确认03_clean.py成功,再运行04_returns.py。按symbol、date看每组前几行,核对每组首条缺失。最后观察transfer_missing.csv的保留缺失行与删除缺失行对照。

powershell
.\.venv\Scripts\python.exe .\04_returns.py

sort_values确定时间顺序;groupby('symbol')让每个标的单独计算;pct_change(fill_method=None)不自动填缺失。它算相邻记录收益,并不自动补出缺少的日期或核对交易日历。

查看本步骤的注释函数

这是week2.py中的实际片段,由步骤脚本调用;依赖导入和共用常量已在下载包中提供。不要只复制片段运行。

python
def add_returns(frame):
    if frame.duplicated(KEYS).any():
        raise ValueError("先解决重复键,再计算收益")
    known = frame["close"].dropna()
    if (known <= 0).any() or not known.map(math.isfinite).all():
        raise ValueError("已知价格必须是有限正数")
    result = frame.sort_values(["symbol", "date"]).copy()
    # 每个symbol单独计算;缺失不填充;每组首条收益保持缺失。
    result["return"] = result.groupby("symbol")["close"].pct_change(fill_method=None)
    return result.reset_index(drop=True)
查看运行入口:04_returns.py
python
from pathlib import Path
from week2 import add_returns, read_prices, validate_clean

ROOT = Path(__file__).resolve().parent
source = ROOT / "outputs" / "cleaned.csv"
if not source.exists():
    raise SystemExit("先运行03_clean.py,再运行本文件")
clean = read_prices(source)
validate_clean(clean)
returns = add_returns(clean)
print(returns.to_string(index=False))

for symbol, group in returns.groupby("symbol"):
    # 清洗结果已无价格缺失,只有每组首期收益缺失,此处才可略去首期。
    cumulative = (1 + group["return"].dropna()).prod() - 1
    print(f"{symbol} 累计收益: {cumulative:.2%}")

# 文件中保留缺失行与删除缺失行会产生不同的相邻记录收益。
missing = read_prices(ROOT / "transfer_missing.csv")
print("保留缺失行:")
print(add_returns(missing).to_string(index=False))
print("仅用于对照:删除缺失行后,10%是跨缺口收益")
print(add_returns(missing.dropna(subset=["close"])).to_string(index=False))

应看到什么

A价格100、110、121、110、121;B价格50、52.5、55、60.5、60.5。两组累计均21%,两条组内首行收益缺失。缺口样例10、缺失、11保留时三条收益均缺失;删除中间行后出现跨缺口的10%。

停下来检查:找到两只标的相接处,解释为何直接对整列pct_change会跨组。价格本身缺失时,不得照搬脚本对完整数据dropna后连乘的做法来声称完整期间收益。

留下的证据:returns.csv与learning-log.md,写清时间顺序、分组键、缺口处理和累计收益口径。

手工核对:先看清“数的是什么”

手机可左右滑动表格;代码块也可横向滚动。

口径数量对应数据
原始记录12不包含CSV表头
完全重复额外行1A / 01-06,两条价格110
键重复额外行2A / 01-06与01-07各多一条
冲突键组1A / 01-07价格121与120不一致
缺失价格1B / 01-06,空字段
非正价格1A / 01-08,价格0

完全重复与键重复有交集;“额外行”“全部涉及行”“冲突组”也不是同一个单位。先去掉完全重复后的11行,再处理有依据的冲突,得到10行;不能将所有异常数机械相减。

最终输出怎样检查

  • cleaned.csv:10行、3列,date+symbol唯一,价格有限且大于0;按symbol、date排序。
  • returns.csv:10行,每标的首条收益缺失,共2条;A/B各自累计21%。首条缺失不填0。
  • change-log.csv:4条变更;old_values中的null代表原先缺失。rows_before/after描述该事件涉及的记录数。
  • audit.md:保留原始数据的6项统计;它描述修复前,不应偷偷换成清洗后的统计。

若本次运行失败,outputs可能仍是上一次成功的结果;确认本次成功退出后再使用。第三周跟练时,把outputs/cleaned.csv复制到第3周包的week3.py同目录。

查看完整流程:week2.py
python
"""第2周完整参考。全部价格与核验依据都是教学数据。

读取 -> 体检 -> 按核验表修复 -> 检查 -> 计算收益 -> 输出。
不会覆盖输入CSV;outputs中的练习结果会在重跑时更新。
"""
import json
import math
from pathlib import Path

import pandas as pd

ROOT = Path(__file__).resolve().parent
KEYS = ["date", "symbol"]
COLS = ["date", "symbol", "close"]


# region read
def read_prices(source):
    # symbol按文字读,避免000001变成1;utf-8-sig兼容UTF-8的BOM。
    # keep_default_na=False保留原始文字,只把明确的空价格转为缺失。
    frame = pd.read_csv(source, encoding="utf-8-sig", dtype="string", keep_default_na=False)
    # 多出未命名字段时,pandas可能把它当索引;本教程不接受这种隐式解释。
    if not isinstance(frame.index, pd.RangeIndex):
        raise ValueError("发现隐式索引,请检查CSV每行字段数是否与表头一致")
    if set(frame.columns) != set(COLS):
        raise ValueError("价格表必须只有date、symbol、close三列,请检查表头和分隔符")
    if frame.empty or frame[KEYS].apply(lambda col: col.str.strip().eq("")).any().any():
        raise ValueError("价格表不能为空,date和symbol不能缺失")
    frame["date"] = pd.to_datetime(frame["date"], format="%Y-%m-%d", errors="raise")
    if frame["date"].isna().any():
        raise ValueError("日期不能为NaT")
    # errors='raise'让文字脏值报错,不把oops悄悄变成缺失。
    frame["close"] = pd.to_numeric(frame["close"].replace("", pd.NA), errors="raise").astype(float)
    if not frame["close"].dropna().map(math.isfinite).all():
        raise ValueError("价格不能为无穷大")
    return frame[COLS]
# endregion read


def read_corrections(source):
    fixes = pd.read_csv(source, encoding="utf-8-sig", dtype="string", keep_default_na=False)
    if set(fixes.columns) != {"date", "symbol", "verified_close", "reason"}:
        raise ValueError("核验表的表头不符合约定")
    fixes["date"] = pd.to_datetime(fixes["date"], format="%Y-%m-%d", errors="raise")
    fixes["verified_close"] = pd.to_numeric(fixes["verified_close"], errors="raise").astype(float)
    return fixes


# region audit
def audit(frame):
    # True在求和时计为1。默认keep='first'只数额外行。
    # dropna=False把缺失视为一种状态,避免“一个有效值+一个缺失”被漏报。
    states = frame.groupby(KEYS)["close"].nunique(dropna=False)
    return {
        "rows": len(frame),
        "exact_duplicate_extra": int(frame.duplicated(COLS, keep="first").sum()),
        "key_duplicate_extra": int(frame.duplicated(KEYS, keep="first").sum()),
        "conflict_groups": int((states > 1).sum()),
        "missing_close": int(frame["close"].isna().sum()),
        "nonpositive_close": int((frame["close"] <= 0).sum()),
    }
# endregion audit


def validate_clean(frame):
    if frame.empty or frame[KEYS].isna().any().any():
        raise ValueError("结果不能为空或缺少键")
    if frame.duplicated(KEYS).any():
        raise ValueError("仍有未经核验的冲突键,请补查来源,不能随意保留第一条")
    if frame["close"].isna().any() or not frame["close"].map(math.isfinite).all():
        raise ValueError("仍有缺失或非有限价格,请补查核验依据")
    if (frame["close"] <= 0).any():
        raise ValueError("仍有非正价格,请补查核验依据")


# region clean
def clean_prices(raw, fixes):
    if fixes[KEYS].isna().any().any() or fixes.duplicated(KEYS).any():
        raise ValueError("核验表键缺失或重复,必须先查清")
    if fixes["reason"].fillna("").str.strip().eq("").any():
        raise ValueError("核验表必须写明reason")
    verified = fixes["verified_close"]
    if verified.isna().any() or not verified.map(math.isfinite).all() or (verified <= 0).any():
        raise ValueError("核验价格必须是有限正数")
    work = raw.copy(deep=True)  # 修复副本,原始数据留作证据。
    logs = []

    # 只删除“所有业务列完全相同”的额外行,先把删除写入日志。
    duplicate = work.duplicated(COLS, keep="first")
    for row in work.loc[duplicate].itertuples():
        logs.append({
            "action": "drop_exact_duplicate", "date": row.date, "symbol": row.symbol,
            "old_values": json.dumps([None if pd.isna(row.close) else row.close]),
            "new_value": row.close, "rows_before": 1, "rows_after": 0,
            "reason": "该额外行与保留行的date、symbol、close完全一致",
        })
    work = work.loc[~duplicate].copy()

    for fix in fixes.itertuples(index=False):
        # 用两个键定位,不能只用symbol覆盖所有日期。
        mask = (work["date"] == fix.date) & (work["symbol"] == fix.symbol)
        before = work.loc[mask, "close"]
        if before.empty:
            raise ValueError("核验键未出现在原表,请确认拿的是同一份数据")
        if len(before) == 1 and before.iloc[0] == fix.verified_close:
            continue  # 已经正确时不虚构一次改动。
        old = [None if pd.isna(value) else float(value) for value in before]
        logs.append({
            "action": "verified_repair", "date": fix.date, "symbol": fix.symbol,
            "old_values": json.dumps(old), "new_value": fix.verified_close,
            "rows_before": len(before), "rows_after": 1, "reason": fix.reason,
        })
        work.loc[mask, "close"] = fix.verified_close
        # 此时这些行已由同一条核验依据确认相同,才可以合并。
        work = work.drop_duplicates(COLS, keep="first")

    validate_clean(work)  # 无核验依据的剩余问题在这里阻断输出。
    work = work.sort_values(["symbol", "date"]).reset_index(drop=True)
    columns = ["action", "date", "symbol", "old_values", "new_value",
               "rows_before", "rows_after", "reason"]
    return work, pd.DataFrame(logs, columns=columns)
# endregion clean


# region returns
def add_returns(frame):
    if frame.duplicated(KEYS).any():
        raise ValueError("先解决重复键,再计算收益")
    known = frame["close"].dropna()
    if (known <= 0).any() or not known.map(math.isfinite).all():
        raise ValueError("已知价格必须是有限正数")
    result = frame.sort_values(["symbol", "date"]).copy()
    # 每个symbol单独计算;缺失不填充;每组首条收益保持缺失。
    result["return"] = result.groupby("symbol")["close"].pct_change(fill_method=None)
    return result.reset_index(drop=True)
# endregion returns


def main():
    raw = read_prices(ROOT / "prices_raw.csv")
    fixes = read_corrections(ROOT / "corrections.csv")
    report = audit(raw)
    print("原始数据体检:", report)
    clean, log = clean_prices(raw, fixes)
    returns = add_returns(clean)
    out = ROOT / "outputs"
    out.mkdir(exist_ok=True)
    clean.to_csv(out / "cleaned.csv", index=False, encoding="utf-8-sig")
    returns.to_csv(out / "returns.csv", index=False, encoding="utf-8-sig")
    log.to_csv(out / "change-log.csv", index=False, encoding="utf-8-sig")
    (out / "audit.md").write_text(
        "# 原始数据体检\n\n" + "\n".join(f"- {key}: {value}" for key, value in report.items()),
        encoding="utf-8",
    )
    print(f"清洗后: {len(clean)}行;变更日志: {len(log)}条;首条缺失收益: {returns['return'].isna().sum()}条")
    print("结果保存在outputs;不能把本教学核验表当作历史当时已知信息。")


if __name__ == "__main__":
    main()

检查方法:让错误真的被发现

powershell
.\.venv\Scripts\python.exe -m unittest -v

应看到Ran 11 tests与OK。它们检查异常口径、清洗结果、日志、原始数据不变、缺失核验依据、核验表重复、非法价格、分组收益、缺口、非法文字和重复运行等行为;每个测试可能包含多个小案例。

  1. 拿掉证据,应该停止

    先另存备份corrections.csv,只删除B在01-06的核验行,运行03_clean.py。应因仍有缺失而失败。恢复原文件再跑,确认成功;不要通过ffill让错误“消失”。

  2. 故意漏掉分组,定位边界错误

    在04_returns.py中计算returns之后,临时加print(clean["close"].pct_change(fill_method=None)),与正确输出并排看。A最后价格121后紧接B首价50,会产生约-58.68%的错误跨标的收益;B首条正确结果应缺失。删除临时代码,再重跑。

  3. 证明可以重复运行

    连续两次运行03_clean.py,比较cleaned.csv、日志与体检输出;输入文件保持原样,结果一致。把“预测、实际、根因、修复、复测”写进learning-log.md。

常见报错与误判

ModuleNotFoundError: No module named 'pandas'

先定位:安装包的Python和运行代码的Python可能不同。

怎么修:用同一个.venv路径执行-m pip install -r requirements.txt,再用它输出sys.executable与pandas.__version__。不要只运行不确定来源的pip。

修后检查:版本查询成功,再运行01_read.py。

pip安装失败 / No matching distribution

先定位:可能是网络、Python版本或包版本不兼容,尚未执行清洗代码。

怎么修:看最后几行,确认Python 3.10+和当前requirements.txt;网络失败先恢复连接再重试。不要随意删版本限制或改脚本来掩盖安装错误。可先用本页浏览器小练习。

修后检查:安装命令成功退出,随后import pandas成功。

FileNotFoundError / 找不到CSV

先定位:没有解压、打开了错误目录,或把输入移到了脚本找不到的位置。

怎么修:用VS Code打开直接含week2.py的文件夹。确认prices_raw.csv等CSV与脚本同目录,运行01_read.py查看它打印的完整读取路径。

修后检查:显示12行3列;04_returns.py需先成功运行03_clean.py。

ModuleNotFoundError: No module named 'week2'

先定位:只复制了步骤脚本,没有同目录的共用函数文件。

怎么修:完整解压下载包,保留week2.py。不要只复制网页函数片段运行;浏览器请用独立的StringIO示例。

修后检查:本地运行01_read.py和-m unittest -v成功。

UnicodeDecodeError / 中文乱码

先定位:来源文件编码与读取编码不一致。

怎么修:本包是UTF-8,用utf-8-sig兼容BOM。外部文件先查来源与编码,保留原文件;不要用errors='ignore'把坏字节吞掉。

修后检查:表头、reason中文可读,列数与行数符合预期。

KeyError / 表头不符合约定 / Unnamed: 0

先定位:列名拼错、分隔符不对、额外保存了索引,或者文件已被手动改动。

怎么修:打印frame.columns或直接查看CSV首行。目标是date,symbol,close;输出要to_csv(..., index=False)。不要盲目跳过列或行。

修后检查:价格表只有3列,读回后没有多出索引列。

ValueError: 日期或数值不能解析

先定位:例如日期不存在、价格写成oops或混入单位。

怎么修:回到原始行确认内容,记录问题并查来源。不要直接改成errors='coerce'后把转换失败当成正常缺失;本教程对无法解释的内容报错。

修后检查:合法日期与价格能解析,故意放入oops仍应报错。

仍有未经核验的冲突 / 仍有缺失价格

先定位:核验表不完整,或处理的是另一份数据。

怎么修:检查(date,symbol)对应的核验记录;没有依据就保留待查问题并停止生成结果。不要用ffill、补0或drop_duplicates(keys)强行跑通。

修后检查:恢复原核验表后成功;删除一条核验记录时应再次失败。

SettingWithCopyWarning / ChainedAssignmentError / 改了却没生效

先定位:用连续两次索引修改临时对象,结果难以保证。

怎么修:在副本上用work.loc[mask, 'close'] = value一次定位并赋值。不要靠关闭警告来修复。

修后检查:重新打印该键对应行,并检查输出文件和日志中的新值。

PermissionError: 输出文件无法写入

先定位:Excel占用生成CSV,或目录没有写权限。

怎么修:关闭正在打开的输出CSV,在自己可写的学习目录重跑。保留原始输入,不覆盖数据源。

修后检查:本次命令成功后,重新打开outputs,核对10行及4条日志。

AssertionError / FAILED / Ran 0 tests

先定位:计算或检查与预期不一致;0个测试则可能在错误目录运行。

怎么修:先看失败测试名,再用最小数据手算。确认test_week2.py与week2.py同目录,运行-m unittest -v。不要删断言或修改预期来迁就错误。

修后检查:应为Ran 11 tests和OK;原始文件内容保持不变。

NaN很多 / 收益突然很大 / 输出还是旧的

先定位:可能未分组、未排序、缺失行传播,或本次失败后误用了上次输出。

怎么修:先打印每组前三行和输入价格;缺失不一定是错误。检查本次运行是否成功退出,失败时旧outputs不代表本次结果。

修后检查:教学清洗结果有10行、2个首期缺失收益;缺口练习按题意保留更多缺失。

浏览器小练习:缺失行为什么影响两个收益

StringIO把内嵌文字当作一个小CSV,避免本地文件路径问题。先预测输出;再把CSV中间那条空价格改成10,观察缺失数量和收益如何变化。这是跟练样例,隔日验收另用新数据。

这里只运行独立示例,不读取你的本机CSV,不运行pip命令或本地多文件脚本。首次需联网加载pandas;刷新或离开前主动复制保存代码。

Python 练习区

在你的浏览器内运行。首次需联网下载 Python;支持标准库和 Pyodide 提供的 NumPy、pandas 等包。行情抓取、系统文件访问和部分第三方库请在本地环境运行。输出自检仅比对结果关键词,不代替逻辑审查。

Python · 独立工作线程 · 执行限时10秒

单元5 · 关闭参考后独立验收

45分钟,只拿原始CSV、核验表和题干,从空文件实现读取、体检、核验修复与收益计算。保存自己的代码和输出,不把下载包的week2.py直接当作独立成果。

  • 至少3个检查:结果行数、唯一键、正价格;解释原始6项统计口径。
  • 每次修复写明旧值、新值、依据;缺失依据时能停止并说明待查内容。
  • 收益先排序再分组;首条保持缺失。至少用一只标的与第1周函数手算核对。

沿用首月5项评分:异常口径、修复证据、时序分组、可重复检查、独立解释。每项0–2分,至少8/10;“修复有证据”和“时序与分组正确”必须各2分。现成测试全通过只算检查结果,不等于独立掌握。

隔24–48小时:新数据复测

只打开transfer_exam.csv,不看参考代码。20分钟内处理6条乱序记录:确认键、排序分组、标记缺失、计算能计算的收益;没有核验表,说明哪些问题必须待查,以及删掉缺失行会改变什么。

完成复测后再看参考口径

保留6行。A依次20、缺失、24,3条收益均缺失;B依次50、55、60.5,首条缺失、其后10%和10%,累计21%。总计4个缺失收益。A首末涨20%仅是跨缺口变化;删除中间行后虽会算出20%,也不能因此声称每日收益已补齐。

若统计数不对,回12行原表手工圈键;若收益错位,先只打印两组各3行;若需要AI代写,回单元1–2重做小表。原transfer_missing.csv已经跟练过,不把复述它作为唯一复测证据。

官方资料,只查当前需要的部分

入口核验于2026-10-20。先查read_csv、duplicated、isna和分组pct_change;不要求通读整本文档。真实数据还需确认复权、时区、交易日历与可得时点,本教程不把这些假设自动补齐。