如果您和我一样 - 您至少处理过一次数据集中丢失的数据。或者两次。或者一次太多次了…… 有时,处理那些讨厌的 NA 所需要做的就是删除它们,即删除包含丢失数据的行。然而,这可能并不 最佳的,特别是对于时间序列数据,对于金融数据更是如此。当然,这个问题已经被充分研究过,因此存在许多替代丢弃的方法。 总是 我将研究其中的一些(如下所列)并讨论其优点和缺点: 掉落 LOCF(最后观察结转) 平均(或类似)插补 插值法 剧透警告:没有一刀切的方法!我认为 LOCF 通常是金融领域的一个不错的选择,但也有其缺点。考虑到这一点,让我描述一下我将用来展示它们的方法和数据。 注:如果想学究气一点,方法 2-4 都是 插补的例子。 一些 型号数据 让我们从一些例子开始,说明为什么人们一开始会关心放弃。为了说明这一点,我生成了一些过于简化的每日股票价格数据,假设它遵循没有漂移的随机游走(即平均长期价格应保持不变)——尽管不是最准确的,但却是一个良性的假设。 np.random.seed(10) # needed for reproducibility price_moves = 3*pd.Series(np.random.randn(100)) # generating random "steps" with 0 mean price_vec = 100 + price_moves.cumsum() # generating brownian motion price_vec.plot() 嗯,剧情看起来还是很用心的。 假设我们现在想要找出每日价格差异的经验平均值 - price_vec.diff().mean() #sample mean >0.20030544816842052 显然非零,与生成序列不同 - 但这只是样本噪声。到目前为止,一切都很好。 丢弃数据 现在让我们通过删除一些数据点来稍微变形该数据: price_vec_na_simple = price_vec.copy() price_vec_na_simple.iloc[90:95] = np.array([np.NaN, np.NaN, np.NaN, np.NaN, np.NaN]) # price_vec_na_simple.diff().mean() >0.1433356258183252 我们立即注意到几件事 - 尽管 向量显然包含 NA,但平均值在某种程度上是非 NA diff 平均值与我们之前获得的平均值不同 现在,#1 非常简单 - 默认情况下自动删除 NA。 pd.mean 但是#2呢?让我们重新思考一下我们正在计算的内容。 很容易证明,至少在没有 NA 的情况下,平均价格差异应该只是 - 事实上,当我们对价格差异求和时,所有“中间”部分都会抵消,就像这样 ! (price_vec[99]-price_vec[0])/99 (price_vec[1] - price_vec[0]) + (price_vec[2] - price_vec[1]) + .. 现在,插入丢失的数据后,如果我们 取差异 删除 ,则这种取消被破坏 - 一些简单的数学表明您现在正在计算 。 首先 然后 NA (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 为了说明这一点,请注意现在省略了以下两项 和 ,因为 计算为 NA 然后被删除。 price_vec[95] - price_vec[94] price_vec[90] - price_vec[89] (NA - any number) 我们来验证一下: (price_vec[99] - price_vec[0])/99 >0.20030544816842052 (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 >0.1433356258183252 现在,我们如何解决问题变得更加清楚了 - 我们需要 删除 NA,然后 - 首先 diff price_vec_na_simple.dropna().diff().mean() >0.21095999328376203 均值 回到了应有的位置 - 出现了一个小差异,因为现在均值的项数减少了 - 94 而不是 99。 几乎 好吧,看起来如果我们只关心平均值,我们只使用 就可以了(只要我们做对了)?毕竟, 和 之间的差异显然在我们的噪声容限范围内。嗯,不完全是——让我们看看为什么。 dropna 0.2 0.21 洛卡夫 什么是LOCF? LOCF 代表最后观察结转。它背后的想法非常简单 - 如果我在某些时间间隔记录数据,这可能是规律的,也可能不是规律的,如果缺少某些特定间隔的观察,我们只需假设我们的变量没有任何变化,并将其替换为最后一个非变量缺失值(例如 - [3, 5, NA, 8] → [3, 5, 5, 8])。有人可能会问 - 为什么首先要关心缺少观察的区间,即不只是像“删除”方法中那样将其删除?那么,答案就在于我上面没有提到的“掉落”的固有缺陷。 假设您一次记录多个量,尤其是那些通常不会太快变化的量,例如每小时记录的温度和湿度。假设您有 10:00、11:00 和 12:00 的两个值, 只有 13:00 的湿度。您是否只是删除该“行”——即假装您没有 13:00 的读数?好吧,如果您只有两个变量,那也没关系 - 即使您刚刚删除了一些可能有价值的信息(13:00 的湿度)。 如果您同时出现许多此类事件或许多变量,则删除可能会导致您实际上根本没有数据! 但 但是, 一个非常有吸引力的替代方案是假设 12:00 到 13:00 之间温度 变化。毕竟,如果有人在 12:30 来找我们并问我们 - “当前温度是多少”,我们会正确地回答 12:00 的读数(当然,如果我们无法立即获得新的读数) )。为什么不对 13:00 值使用相同的逻辑? 没有任何 使用 LOCF(金融领域)? 为什么 首先,让我们用之前的数据测试一下我们新发现的方法: price_vec_na_simple.ffill().diff().mean() # ffill performs LOCF by default >0.20030544816842052 看来我们准确地恢复了原来的价值!此外,如果您想对价格差异数据进行进一步研究 - 现在看起来更加“有序”,因为它每天都有一个条目,即使其中五个条目现在为 0(为什么?尝试运行 自己看看)。 price_vec_na_simple.ffill().diff().iloc[90:95] 除此之外,在金融领域,缺失数据和 数据经常聚集在一起。让我举例说明: 异常 #inflate two observations, delete three next ones price_moves_na[90] += 20 price_moves_na[91] += 30 price_moves_na[92] -= 50 # to "deflate" the price shock back price_vec_na = (100 + price_moves_na.cumsum()) price_vec_na[92:95] = [np.NaN, np.NaN, np.NaN] price_vec_na.tail(20).plot() price_vec_na.diff().dropna().mean() >0.7093365245831178 我们可以看到,价格大幅上涨后,连续3天都没有数据。这并不是一个听起来那么“人为”的例子!想象一下,交易在飙升后停止了,至少在这个特定的交易所上。然后事情稍微稳定下来,所以价格回到了正常状态。也许是渐进的事情幕后发生的事情实际上“连接”了尖峰和尖峰后平静下来之间的点。但 不知道这一点,也没有任何数据! 你 如果我们没有任何新数据,最自然的假设是什么?好吧,回想一下,我们的数据生成模型从根本上来说是基于价格 。那么,如果没有新数据,也许价格根本就没有变化?这正是 LOCF(最后观察结转)所假设的。 变化的 一些上下文数学 好奇的读者的旁注 - 也许关于 LOCF 特别适合股票价格数据的更基本观点是,它通常被建模为 。粗略地说,鞅是我们对明天的最佳猜测就是我们今天所看到的,或者 为什么 鞅 E[x_{t+1} | x_t] = x_t 好吧,回到实际数据!让我们从视觉上和数字上检查 LOCF 的后果: price_vec_na.ffill().tail(20).plot() price_vec_na.ffill().diff().mean() >0.20030544816842052 我们立刻就看到了 LOCF 的优点和缺点(字面意思)!其一,平均值回到了我们“期望”的位置——即未改变的经验值。然而,我们引入了一个相当难看的时期,其中价格与“典型”不一致,并且在第 94 天和第 95 天之间人为地降低了价格。 那么插补呢? 让我们将 LOCF 得到的结果与(平均)插补进行对比。这是 NA 处理的一个非常常见的选择,尤其是对于非时间序列数据。然而,如果天真地这样做,它在用于财务数据时会存在许多缺点。 如果您仅使用所有样本均值,则会引入明显的前瞻偏差 - 即您使用未来数据来估算过去的值。 使用某种回顾或滚动平均值当然更好 - 然而,它有时可能会与我们之前描述的鞅“基线”视图产生冲突。 让我们更详细地研究一下这一点。我将对我们的旧数据使用回顾插补 - price_vec_na_impute = price_vec_na.copy() price_vec_na_impute[price_vec_na_impute.isna()] = price_vec_na.iloc[:90].mean() price_vec_na_impute.diff().mean() >0.20030544816842052 我们恢复“正确”的价格变化均值,与 LOCF 相同。但我们在第 91 天和第 92 天之间引入了人为的价格下降,这在某些方面甚至比我们之前的下降还要糟糕。毕竟,那一件事发生在事情可能平静下来时或之后,而这件事只是假设一切立即恢复正常。除此之外,在实践中,平衡回顾窗口可能会有些挑战性,以便我们a)捕获最近的趋势,同时b)捕获长期趋势(通常的偏差-方差权衡)。 添加第二个变量 假设我们现在想要执行一项更复杂的任务 - 当一个或两个价格序列缺少数据时,从经验数据中提取两种资产价格变动之间的相关性。当然,我们仍然可以使用丢弃,但是: 即使我们可以使用它,它是最佳的吗? 如果我们有很多变量怎么办 - 那么删除至少有一个 NA 的所有行可能会让我们根本没有数据! 人们想要计算相关性的原因有很多——它是几乎所有多变量模型中 EDA 的第一步,它在任何类型的 中都得到广泛使用,等等。因此,尽可能准确地测量这个数字是非常有必要的! 投资组合构建 为了说明这一点,让我们生成第二个变量,其与第一个变量的“内置”相关性为 0.4。为此,我们将使用一种 。人们可以想到的情况是,两只相关的股票都有一个重要的风险因素,但第二只股票也面临着第一只股票所没有的主要风险因素。以谷歌和 Facebook 为例,第一个因素可能是对科技行业的普遍情绪,第二个因素可能是与竞争对手社交网络的竞争。 高斯混合模型 np.random.seed(2) # needed to ensure a fixed second series price_moves_2 = pd.Series(np.random.randn(100)) price_vec_2 = 50+(0.4*price_moves/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum() # all this math to ensure we get a 0.4 "theoretical" correlation with the first one 让我们检查“基线”经验相关性 - 即没有 NA 和跳跃。 pd.concat([price_vec, price_vec_2], axis = 1).diff().corr().iloc[0,1] >0.4866403018044526 现在,这相当接近“理论”相关性 - 众所周知,相关性的经验测量容易产生相当大的噪声。 无异常值的 NA 下一步,我们将检查具有 NA 的案例,但没有异常值。我们还将比较如果在 之前和之后 会发生什么 diff dropna pd.concat([price_vec_na_simple, price_vec_2], axis = 1).diff().corr().iloc[0,1] # implicit dropna after diff >0.5022675176281746 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.5287405341268966 这两个结果都非常接近,与我们之前得到的“经验”值相差不远。让我们验证 LOCF 和插补也执行正常: pd.concat([price_vec_na_simple, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.5049380499525835 price_vec_na_simple_impute = price_vec_na_simple.copy() price_vec_na_simple_impute[price_vec_na_simple_impute.isna()] = price_vec_na_simple_impute.iloc[:90].mean() pd.concat([price_vec_na_simple_impute, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.4866728183859715 通过比较以上 4 个结果,我们看到所有方法都表现得很好。也许我们应该对异常情况抱有同样的期望? 具有异常值的 NA 请记住,为了保持一致,我们需要让第二个价格系列经历与第一个价格系列经历的 价格冲击,但 以下 NA。回到上面的例子 - 想象一些重大事件导致第一个风险因素激增,最终停止第一个资产的交易。当然,第二个资产也会经历这些,但程度可能较小,因此不会发生停止,因此不会出现 NA。 相同 没有 price_vec_na_2 = 50+(0.4*price_moves_na/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum() 让我们再次比较我们所有方法的性能 - pd.concat([price_vec_na, price_vec_na_2], axis = 1).diff().corr().iloc[0,1] >0.6527112906179914 pd.concat([price_vec_na, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7122391279139506 这与理论值和经验值都有很大差异! LOCF 和插补怎么样? pd.concat([price_vec_na, price_vec_na_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.33178239830519984 pd.concat([price_vec_na_impute, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7280990594963112 现在我们终于看到 LOCF 的价值了!它显然优于所有其他方法! 潜在的陷阱+插值 当然,这并不是 100% 稳健。其一,通过执行 LOCF,我们会在缺失数据结束时引入大幅价格下跌。如果它与第二个价格向量上的一些异常值一致,结果可能会发生相当大的变化。 (*给读者的一个练习 - 翻转 价格变动的符号并检查它如何影响结果)。目前尚不清楚仅引入此价格下降是否“干净”,而不是在价格峰值 和之后的“正常”值 之间进行插值。然而,尤其是对于“实时”使用,插值实际上是不可能的!毕竟,如果今天是第 93 天,我们如何使用第 95 天结束时记录的未来值进行插值?对于历史研究 - 当然,这仍然是一种选择,但目前仍不清楚如何解释并将其用于实际预测!总之,跨时间维度的插值是可能的,但有些更值得怀疑。 price_vec_na_2[95] price_vec_na[91] price_vec_na[95] 结论 我试图提供一个小型案例研究来介绍和倡导为什么 LOCF 通常是处理金融时间序列中缺失数据的最具吸引力和最简单的选项。 回顾一下,优点是: 从鞅/“信息流”的角度来看很有吸引力 超级容易实施 无需优化(与插补相反) 至少妥善处理异常值 一些缺点: 可能会在缺失期结束时导致大幅跳跃 当用于多个变量时可能会错过一些微妙的关节动态 作为一家自营交易店的量化分析师,我在几乎所有的研究中都使用它作为有效的基准。当然,有些情况需要采取更细致的措施,但这些措施很少且相差甚远,并且通常不能通过提到的其他 3 种方法中的任何一种真正 100%“解决”。