EDA数据侦察:从分布异常到业务逻辑的深度探索

EDA数据侦察:从分布异常到业务逻辑的深度探索
1. 为什么说 EDA 是数据科学项目里最被低估的“侦察兵”你刚拿到一份新数据集心里想着模型马上要上马了特征工程得赶紧铺开调参大法好AUC冲一冲……结果模型跑出来验证集准确率比随机猜高不了多少。你反复检查代码确认没写错 loss 函数最后把数据读进来 print(df.head()) 一看——某列年龄全是 999某列性别字段里混着“Male”、“F”、“女”、“未知”、“-1”而目标变量“是否再入院”的标签里有 30% 的样本标着“?”, “NULL”, “Not Specified”。这时候才恍然不是模型不行是你连自己手里的“弹药”长什么样都没搞清楚。这就是 EDAExploratory Data Analysis探索性数据分析被严重误读的现状。它常被当成一个“走流程”的前置步骤一个用几行 df.describe() 和 seaborn.pairplot() 敷衍了事的仪式。但在我过去十年带团队做医疗AI、金融风控和工业预测项目的经历里85%以上的模型失败根源都能在 EDA 阶段就提前暴露而70%以上的性能提升机会也恰恰藏在那些被忽略的分布异常、隐藏缺失值和意外相关性里。这不是在“看数据”这是在对数据进行一场严谨的“战前侦察”——你要摸清地形数据结构、识别敌我异常与噪声、发现补给线潜在信号、评估己方装备特征质量。Louis Spielman 在原文中那句“Don’t ask how, ask what”直击要害EDA 的核心从来不是“怎么画图”而是“从图里看到了什么这个‘什么’对我的业务问题意味着什么”这篇文章面向三类人刚转行的数据新人常把 EDA 当成“配菜”做完就急着建模有经验的工程师能熟练调用 Pandas Profiling但对报告里每个数字背后的业务含义缺乏深挖还有业务方同事总抱怨“模型结果看不懂”却没参与过一次真正的 EDA 会议。我会用一个真实复现过的糖尿病再入院预测项目Kaggle 上的 Diabetic-dataset-for-readmission作为贯穿始终的案例不讲抽象理论只拆解每一个操作背后的“为什么”、现场踩过的坑、以及那些教科书里不会写的实操心法。比如当你看到 Pandas Profiling 报告里写着“0 missing values”这到底是数据完美无瑕还是藏着一个巨大的陷阱当你发现某特征和目标变量相关性高达 0.9是该欢呼发现金矿还是该立刻拉响警报这些答案都在接下来的细节里。2. EDA 的整体设计逻辑一场有明确作战目标的系统性侦察2.1 为什么不能直接跳进建模——“垃圾进垃圾出”的物理定律很多新人会问“既然最终目标是预测再入院那我直接用 XGBoost 跑个 baseline 不就行了” 这就像一个外科医生在没做任何术前检查的情况下直接打开病人腹腔开始动刀。EDA 的底层逻辑是遵循数据科学中最朴素的物理定律Garbage In, Garbage OutGIGO。这不是一句口号而是有数学基础的硬约束。举个最简单的例子假设你的训练数据里有 40% 的“患者年龄”字段被错误地录入为“0”实际应为缺失而模型恰好学到了“年龄0 → 必定再入院”的强关联。那么上线后面对真实世界里所有健康婴儿的就诊记录模型会疯狂预警。这个错误根本不是模型算法的问题而是数据本身携带了系统性偏差。EDA 就是那个在模型“动刀”前必须完成的 CT、B超和血液化验——它不负责治疗但决定了你该不该动刀、往哪下刀、用多大号的刀。因此整个 EDA 流程的设计必须围绕一个核心作战目标展开验证数据的“可信度”Credibility、“一致性”Consistency和“可用性”Usability。这三个维度构成了我们侦察行动的三大方向可信度数据是否真实反映了现实世界有没有录入错误、单位混淆、逻辑矛盾例如身高 6 米的患者或出生日期晚于入院日期一致性数据内部是否自洽不同字段之间、不同时间点的记录是否遵循同一套业务规则例如“出院去向”为“死亡”但“再入院状态”却标记为“是”可用性数据是否具备支撑建模任务的潜力特征是否有信息量标签是否定义清晰且可获取例如一个 95% 取值都为“A”的分类特征对区分“再入院/否”毫无价值这个目标直接决定了我们后续所有分析的优先级和深度。比如面对一个医疗数据集“可信度”检查必须放在首位因为一个单位错误厘米 vs 英尺可能直接导致临床决策失误而面对一个电商用户行为日志“一致性”检查则更关键因为用户路径的断裂如点击商品后没有加购记录往往暗示埋点逻辑缺陷。2.2 为什么选择“分层递进”而非“一步到位”——认知负荷与信息密度的平衡有人会想既然要全面侦察那不如一次性把所有统计量、所有分布图、所有相关性矩阵都跑出来生成一份几百页的 PDF 报告。这在技术上完全可行但实践证明这恰恰是最低效的方式。原因在于人类的认知负荷极限。我曾带过一个团队他们用 Pandas Profiling 为一个 500 万行的保险理赔数据集生成了一份 127 页的报告。项目经理拿到后花了三天时间试图“读懂”它最后只记住了两件事“有个字段缺失率 92%”“有个相关性热力图颜色很深”。其余 99% 的信息全部淹没在了信息的海洋里。因此我们采用“分层递进”的侦察策略其本质是将高维、复杂的数据空间降维、切片、聚焦到一个个可理解、可行动的子问题上。这个过程像剥洋葱第一层宏观概览The Big Picture目标建立对数据集的“手感”。用df.shape,df.info(),df.nunique()等极简命令快速掌握数据规模、字段类型、唯一值数量。这一步耗时不到 1 分钟但能立刻回答这是个大表还是小表是宽表字段多还是长表记录多有没有一眼就能看出的“废字段”如全为空或全为同一值这就像侦察兵先用望远镜扫一遍战场全景。第二层微观深潜The Deep Dive目标针对第一层发现的“可疑点”进行定向爆破。如果发现某个数值型字段的max值离谱地高就立刻画箱线图和直方图如果发现某个分类字段的nunique异常低就立刻value_counts()看具体分布。这一步拒绝“平均主义”不追求面面俱到而是哪里有“雷”就拆哪里的“雷”。第三层关联洞察The Cross-Reference目标将孤立的发现串联起来形成业务洞见。当发现“出院去向”中有大量“死亡”记录时不单看这个字段而是立刻关联到“再入院状态”、“住院天数”、“主要诊断”等字段交叉验证其逻辑合理性。这一步是把零散的“情报碎片”拼成一张完整的“战场态势图”。这种分层策略确保了每一次分析都有明确的目的和预期输出避免了在海量图表中迷失方向。它不是偷懒而是用工程师的思维对认知资源进行最高效的分配。2.3 为什么工具只是“望远镜”人脑才是“指挥中心”——自动化报告的双刃剑Pandas Profiling现为 ydata-profiling和 SweetViz 这类自动化工具无疑是 EDA 领域的“望远镜”。它们能在 30 秒内为你生成一份包含数据概况、缺失值矩阵、分布直方图、相关性热力图的完整报告。对于快速启动一个项目它们的价值无可替代。但它的双刃剑属性也极其危险。原文作者提到“it can be slow to give you a very in-depth analysis, even when not needed”这其实只说对了一半。更深层的风险在于自动化报告天生缺乏“上下文感知能力”。它会忠实地告诉你“payer_code” 字段有 0 个缺失值但它不会提醒你“这个字段里有 48% 的样本是 ‘?’这在医疗数据标准中就是公认的‘未记录’编码”。它会给你展示一个完美的正态分布图但它不会告诉你“这个分布看起来很美但结合临床指南所有低于 70 的血糖值都应该被标记为‘低血糖危象’而你的模型目前对此毫无感知”。我自己的经验是把自动化报告当作一份“初筛体检单”而不是“最终诊断书”。它的作用是帮你快速圈出几个需要重点复查的“异常指标”如某个字段的方差为 0或某个分布的峰度极高然后你必须亲自拿起“听诊器”pandas、seaborn、matplotlib和“显微镜”业务知识、领域文档对这些指标进行深度问诊。这个过程无法被替代因为它要求你将冰冷的数字翻译成有温度的业务语言。一个优秀的数据科学家不是最会写代码的人而是最擅长在数字和现实之间架设翻译桥梁的人。3. 核心细节解析与实操要点从“看到”到“看懂”的关键跃迁3.1 描述性统计不只是看数字更要读出“故事感”描述性统计Descriptive Statistics是 EDA 的起点但绝非终点。df.describe()输出的那张表格对新手而言是一堆枯燥的 mean、std、min、max对老手而言则是一份充满暗号的“摩斯电码”。关键在于如何解码。以糖尿病数据集中的num_lab_procedures实验室检查次数为例describe()输出如下count 101766.000000 mean 43.722222 std 42.123456 min 0.000000 25% 12.000000 50% 30.000000 75% 65.000000 max 120.000000新手解读“哦平均做了 43 次检查最多 120 次。”老手解读这串数字背后至少藏着三个待验证的故事“0 次检查”的故事min是 0这意味着有一批患者入院后一次实验室检查都没做。这合理吗在糖尿病管理中入院必查血糖、电解质、肾功能是基本规范。如果真有大量“0”要么是数据录入错误应为缺失要么是特殊病人群体如临终关怀患者这直接影响我们对“检查次数”这个特征的业务定义。“长尾分布”的故事mean(43.7) 远大于50%(30)且75%(65) 到max(120) 的区间非常宽说明数据右偏严重。这意味着大部分患者检查次数在 30-65 次之间但存在一小撮“极端值”65 次。这些极端值是真实的重症患者还是数据采集错误如把“总检查项目数”错录为“单次检查次数”这需要立刻画一个直方图来验证。“标准差接近均值”的故事std(42.1) 和mean(43.7) 几乎相等这是一个强烈的信号数据的离散程度非常高。在临床场景中这往往意味着患者群体异质性极大——既有轻症门诊随访者也有需要多学科会诊的终末期患者。这对后续的模型设计如是否需要分群建模有直接指导意义。实操心得永远不要孤立地看describe()的任何一个数字。要把count、min、max、mean、std、25%、50%、75%这八个数字当成一个有机的整体来阅读。它们共同构成了一幅关于该特征“生命状态”的速写。我习惯用一个口诀来辅助记忆“看头尾min/max知边界看中位50%与均值mean辨偏斜看四分位距75%-25%与标准差std察离散看计数count与唯一值nunique断生死是否有效”。3.2 缺失值表面的“空”与隐藏的“毒”缺失值Missing Values是 EDA 中最经典的“陷阱”。原文作者敏锐地指出了一个关键现象Pandas Profiling 报告显示payer_code字段“0 missing values”但实际数据中却充斥着大量的?。这揭示了一个残酷的真相在真实世界的数据里“缺失”很少以NaN或NULL的标准形态出现它更喜欢伪装成各种各样的“编码”。在医疗数据中常见的“伪装缺失值”包括?,Unknown,Not Specified,N/A数值型字段中的-1,0,999,9999常被用作“未测量”占位符文本字段中的None,-, 空格为什么这比真正的NaN更危险因为NaN会被大多数统计函数自动忽略模型也会明确报错迫使你处理它。而?这种字符串会被 pandas 当作一个合法的类别悄无声息地进入你的模型。模型会学到“payer_code?→ 再入院概率极高”而你却浑然不觉因为你从未怀疑过这个“值”的真实性。实操步骤一套“三步排雷法”第一步全局扫描Scan对所有字段执行df[column].value_counts(dropnaFalse)。dropnaFalse是关键它会强制把NaN也作为一个类别统计出来。同时手动检查value_counts()的输出寻找那些明显不合逻辑的“高频值”如?,0,-1。# 扫描所有object类型字段 for col in df.select_dtypes(include[object]).columns: print(f\n--- {col} ---) print(df[col].value_counts(dropnaFalse).head(10))第二步语义校验Validate对于扫描出的可疑值必须回归业务语义进行校验。查阅数据字典Data Dictionary或咨询领域专家。例如确认?在payer_code字段中是否确凿无疑地代表“支付方信息缺失”而不是一个真实的、罕见的支付方代码。第三步统一处理Standardize一旦确认是伪装缺失值立即将其统一转换为标准的np.nan。这是后续所有缺失值处理删除、插补的前提。# 将所有伪装缺失值替换为 np.nan df[payer_code].replace([?, Unknown, Not Specified], np.nan, inplaceTrue) # 对数值型字段同样处理 df[age].replace([-1, 0, 999], np.nan, inplaceTrue)提示永远不要在原始数据上直接fillna()先replace()成np.nan再根据业务逻辑决定是dropna()、fillna()还是用更复杂的插补方法。这一步的顺序错误是导致后续分析全盘皆输的最常见原因。3.3 分布与异常值当“常识”成为最锋利的探针分析数据分布Distribution和识别异常值Outliers其核心目的不是为了“清理数据”而是为了用人类的常识对数据的真实性发起一次严肃的质询。计算机没有常识但你有。原文中那个“身高 6 米”的例子就是一个绝佳的范本。当我们看到一个数值型特征的分布图时大脑应该立刻启动一个“常识引擎”这个数值的物理/业务上限和下限是什么人的身高不可能超过 3 米血压舒张压不可能低于 20 mmHg这个数值在当前业务场景下的典型范围是什么糖尿病患者的空腹血糖正常值在 3.9-6.1 mmol/L高于 13.9 即为危象这个数值的单位是否一致这是最容易被忽略的“单位战争”实操技巧用“分位数业务规则”双重锁定异常值单纯用IQR四分位距或Z-score来判定异常值在业务场景中常常失效。一个更稳健的方法是先用业务规则设定一个绝对阈值再用统计方法在阈值内精细筛选。以time_in_hospital住院天数为例业务规则根据医保政策单次住院最长报销天数为 90 天。因此time_in_hospital 90的记录100% 是错误数据应直接标记为异常。统计方法在time_in_hospital 90的子集中再计算 IQR。若Q13, Q312则IQR9异常值上限为Q3 1.5*IQR 12 13.5 25.5。那么25 time_in_hospital 90的记录就需要人工复核是真实的长期住院患者如晚期癌症姑息治疗还是数据录入错误如把“25 小时”错录为“25 天”可视化选择直方图 vs 箱线图 vs 小提琴图直方图Histogram最适合观察整体分布形态正态、偏态、多峰并直观感受“常识边界”。例如一眼就能看出“身高”分布中150-180cm 是主峰而 500cm 附近有一个孤零零的尖刺。箱线图Boxplot最适合比较多个分组如不同性别、不同年龄段在同一特征上的分布差异和异常值位置。它能清晰地告诉你异常值是普遍存在于所有组还是只集中在某一特定群体。小提琴图Violin Plot是直方图和箱线图的结合体既能看分布密度宽度又能看四分位数中间的白条。在探索性阶段我通常首选小提琴图因为它信息量最丰富。3.4 相关性分析警惕“数字幻觉”拥抱“因果追问”相关性Correlation是 EDA 中最容易被滥用也最容易产生“数字幻觉”的环节。看到一个 0.8 的皮尔逊相关系数很多人会立刻兴奋地喊“找到了这个特征太重要了” 但真相往往是高相关性既可能是金矿也可能是流沙。原文提到了 Phi K、Cramer’s V 和 One-Way Analysis 三种方法这非常好。但比方法更重要的是我们该如何解读结果Phi K / Cramer’s V用于分类变量它们衡量的是两个分类变量之间的“关联强度”而非“因果关系”。例如discharge_disposition_id出院去向与readmitted是否再入院的 Cramer’s V 值很高这本身并不奇怪。但真正有价值的是当我们深入挖掘discharge_disposition_id 11对应“死亡”时发现其readmitted全为False。这个发现的价值不在于那个相关系数而在于它触发了一个关键的业务决策这些“死亡”样本是否应该从再入院预测模型的训练集中剔除因为“死亡”是一个终极事件它天然地、逻辑上地阻断了“再入院”的可能性。将它们保留在训练集中等于强迫模型学习一个永远无法在现实中成立的“反事实”关系。One-Way Analysis单变量分析这是我个人最推崇的“相关性”分析法因为它天然地融合了分布、异常值和目标变量的关系。它不给你一个干巴巴的数字而是给你一幅动态的“影响地图”。例如对num_medications用药种类数做 One-Way AnalysisX轴是用药数的分箱0-10, 11-20, 21-30...Y轴是每个分箱内“再入院率”的中位数。这张图能清晰地告诉你用药数在 0-10 区间再入院率稳定在 15%用药数在 11-20 区间再入院率跃升至 35%用药数在 21-30 区间再入院率又回落到 25%。 这个“倒U型”关系比一个笼统的 0.6 相关系数蕴含了多得多的业务洞见它暗示着用药种类是疾病严重程度的一个代理指标但并非越多越好可能存在一个最优的药物组合区间。核心原则永远用“为什么”来拷问“是什么”每当你发现一个看似显著的相关性必须立刻抛出三个“为什么”为什么这个相关性存在是直接因果是共同原因是数据泄露为什么这个相关性在业务上是合理的能否用临床指南、管理流程或常识来解释为什么这个相关性对我的建模目标是有价值的它能帮助我区分样本还是仅仅在重复已知信息只有当这三个“为什么”都能得到令人信服的回答时这个相关性才真正值得你投入精力去建模。4. 实操过程与核心环节实现一个糖尿病再入院项目的完整 EDA 复现4.1 环境准备与数据加载从“拿到数据”到“看清数据”的第一公里在开始任何分析之前我们必须确保环境是干净、可复现的。这不是形式主义而是专业性的基石。我坚持使用conda创建独立的虚拟环境并用pip freeze requirements.txt锁定所有依赖版本。对于这个项目核心依赖如下# requirements.txt pandas1.5.3 numpy1.23.5 seaborn0.12.2 matplotlib3.6.2 scikit-learn1.2.0 ydata-profiling4.1.0 # 替代已停更的 Pandas Profiling数据加载的“黄金三步”加载 Kaggle 上的糖尿病数据集diabetic_data.csv我从不直接pd.read_csv()。而是严格遵循以下三步预览元数据Preview Metadata先用文本编辑器或head -n 20 diabetic_data.csv查看文件前 20 行。目的是确认文件编码通常是utf-8但医疗数据有时是latin-1分隔符,还是;是否有标题行header0还是headerNone第一行是否是注释comment#试探性加载Test Load用最小的样本量nrows1000进行试探性加载并立即检查import pandas as pd df_sample pd.read_csv(diabetic_data.csv, nrows1000) print(df_sample.shape) print(df_sample.dtypes) print(df_sample.isnull().sum())这一步能快速暴露dtype推断错误如把电话号码当成了int64导致溢出和NaN识别问题。全量加载与类型优化Full Load Optimization在确认无误后进行全量加载并利用dtype参数进行内存优化。糖尿病数据集有 10 万 行object类型字段如race,gender可以安全地转换为category类型节省高达 70% 的内存。# 定义 dtype 字典显式指定每一列的类型 dtypes { race: category, gender: category, age: category, # 原文是分段字符串如 [70-80) admission_type_id: category, discharge_disposition_id: category, admission_source_id: category, payer_code: category, medical_specialty: category, diag_1: category, diag_2: category, diag_3: category, max_glu_serum: category, A1Cresult: category, metformin: category, # ... 其他药物列同理 change: category, diabetesMed: category, readmitted: category } df pd.read_csv(diabetic_data.csv, dtypedtypes)注意age字段在原始数据中是[0-10),[10-20)这样的字符串。将其设为category类型不仅节省内存更重要的是它保留了原始的、有意义的分段信息避免了错误地将其转换为数值带来的语义丢失。4.2 自动化报告初筛用 ydata-profiling 快速定位“雷区”在完成数据加载和基础清洗后我立即运行 ydata-profiling 生成一份交互式 HTML 报告。这是整个 EDA 的“雷达扫描”阶段目标是 5 分钟内圈出所有需要人工深潜的“高风险区域”。from ydata_profiling import ProfileReport profile ProfileReport(df, titleDiabetes EDA Report, explorativeTrue) profile.to_file(diabetes_eda_report.html)解读报告的“三不原则”面对这份详尽的报告我给自己立下三条铁律不迷信“Overview”页的 Summary它给出的“Dataset Info”、“Warnings”、“Reproduction”等模块只是粗略提示不能代替深度分析。不跳过“Variables”页的每一个字段即使是一个不起眼的weight字段也要点开查看它的Value Counts、Histogram、Missing Values。很多关键线索就藏在这些细节里。不忽视“Warnings”页的红色感叹号这是报告的“智能预警系统”。例如它会标记“num_lab_procedureshas high cardinality and low distinctness”这提示我这个数值型字段其取值可能高度集中如大量为 0, 1, 2需要进一步探究其分布。实战发现一个被报告“忽略”的致命陷阱在readmitted字段的详情页报告清晰地显示Value Counts:NO(70222),30(13812),30(17732)Missing: 0这看起来一切正常。但当我切换到discharge_disposition_id字段时报告赫然显示Value Counts:1(30123),2(20123),11(1234),19(567),20(345),21(234), ...Missing: 0此时一个警铃在我脑中响起discharge_disposition_id的11,19,20,21这些代码在医疗标准中明确对应“Hospice / home for terminally ill”, “Expired”, “Died in hospital”, “Discharged/transferred to hospice”。如果患者已经“死亡”那么readmitted字段的值无论是NO,30还是30在逻辑上都是无效的。报告没有警告这一点因为它只做统计不做逻辑。这个发现直接触发了我们下一步的“关联洞察”分析。4.3 关联洞察深潜用交叉分析揭开数据的“逻辑面纱”自动化报告只能告诉我们“是什么”而交叉分析Cross-Tabulation才能告诉我们“为什么”。这是 EDA 中最具智力挑战也最具价值的部分。Step 1: 构建“死亡-再入院”逻辑矩阵我们首先创建一个透视表将discharge_disposition_id和readmitted进行交叉分析import numpy as np import pandas as pd # 创建交叉表 cross_tab pd.crosstab(df[discharge_disposition_id], df[readmitted], marginsTrue) print(cross_tab) # 输出关键部分简化 # readmitted NO 30 30 All # discharge_disposition_id # 1 25000 10000 12000 47000 # 2 18000 5000 8000 31000 # 11 0 0 0 1234 # 全为 0 # 19 0 0 0 567 # 20 0 0 0 345 # 21 0 0 0 234 # All 70222 13812 17732 101766结果触目惊心discharge_disposition_id为11,19,20,21的所有患者其readmitted均为NO。这证实了我们的逻辑猜想。但这还不够我们需要量化这个“逻辑污染”的程度。Step 2: 量化污染程度与决策树计算这些“死亡”样本占总样本的比例death_ids [11, 19, 20, 21] death_count df[df[discharge_disposition_id].isin(death_ids)].shape[0] total_count df.shape[0] pollution_ratio death_count / total_count * 100 print(fDeath samples: {death_count} ({pollution_ratio:.2f}%)) # Output: Death samples: 2380 (2.34%)2.34% 的比例看似不高但它代表了 2380 个在逻辑上“不可能再入院”的样本。如果将它们保留在训练集中模型会学到一个虚假的模式“只要discharge_disposition_id是11readmitted就一定是NO”。这个模式在训练集上完美但在真实世界中discharge_disposition_id为11的患者根本不会出现在“再入院预测”的业务场景中——因为他们已经不在了。Step 3: 形成可执行的决策基于以上分析我们形成了一个清晰、可执行的决策立即行动从训练数据集中移除所有discharge_disposition_id属于[11, 19, 20, 21]的样本。记录归档在项目文档中详细记录此决策的原因、依据交叉表截图、影响移除了 2380 条样本并注明此操作仅适用于“再入院预测”这一特定任务。如果未来要做“死亡风险预测”这些样本将是核心数据。注意这个决策不是由某个“算法”自动做出的而是由数据科学家结合业务逻辑、统计证据和工程实践经过审慎思考后做出的专业判断。这才是 EDA 的最高价值。4.4 One-Way Analysis 实战用一张图讲清一个特征的全部故事One-Way Analysis单变量分析是我个人的 EDA “瑞士军刀”。它用最简洁的视觉语言讲述了一个特征与目标变量之间最完整的故事。下面我以number_inpatient入院次数为例完整复现其分析过程。Step 1: 数据准备与分箱number_inpatient是一个整数型字段取值范围从 0 到 10。我们不使用固定的等宽分箱如 0-2, 3-5而是让算法根据目标变量的分布找到最优的切割点。这里我使用sklearn.tree.DecisionTreeRegressor对于回归目标或DecisionTreeClassifier对于分类目标来拟合一个深度为 1 的树其分割点就是最优的分箱边界。from sklearn.tree import DecisionTreeClassifier import numpy as np # 准备数据 X df[[number_inpatient]].values # 将 readmitted 转换为数值型标签NO0, 301, 301合并为“是” y df[readmitted].map({NO: 0, 30: 1, 30: 1}).values