机器学习模型的数据准备过程与模型选择本身同样重要。数据集中的缺陷,例如信息缺失或异常数据点,会显著降低模型性能或导致误导性结论。分析两种常见的数据质量问题,即缺失值和异常值,并展示如何主要使用DataFrames.jl来运用Julia强大的数据操作能力解决它们。理解和处理缺失值缺失数据在数据集中经常出现。值可能由于数据收集错误、某个特征不适用于某些记录或各种其他原因而缺失。在Julia中,缺失值通常由特殊的missing对象表示。DataFrames.jl旨在良好地处理missing值。识别缺失值在处理缺失值之前,需要找出它们。DataFrames.jl提供了几种方法来检查数据中的missing条目。考虑一个样本DataFrame:using DataFrames, Statistics df = DataFrame( ID = 1:5, Age = [25, 30, missing, 22, 28], Salary = [50000, missing, 65000, 48000, missing], Department = ["HR", "Engineering", "HR", missing, "Marketing"] )julia> df 5×4 DataFrame Row │ ID Age Salary Department │ Int64 Any Any Any ─────┼────────────────────────────────────────── 1 │ 1 25 50000 HR 2 │ 2 30 missing Engineering 3 │ 3 missing 65000 HR 4 │ 4 22 48000 missing 5 │ 5 28 missing Marketing您可以检查特定列或整个DataFrame中的缺失值,使用ismissing函数,该函数按元素工作:julia> ismissing.(df.Age) 5-element BitVector: 0 0 1 0 0 julia> ismissing.(df) 5×4 BitMatrix: 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0 1 1 0要获取每列缺失值的摘要,describe函数非常有用:julia> describe(df, :nmissing) 4×2 DataFrame Row │ variable nmissing │ Symbol Int64 ─────┼────────────────────────────────────── 1 │ ID 0 2 │ Age 1 3 │ Salary 2 4 │ Department 1此输出清楚地显示,Age列有一个缺失值,Salary有两个,Department有一个。处理缺失值的策略一旦识别出来,您有几种处理缺失值的策略。选择取决于数据的性质、缺失的程度以及机器学习模型的要求。1. 删除最简单的方法之一是删除包含缺失值的行或列。列表式删除(删除行):如果一行包含一个或多个缺失值,则整行将被舍弃。DataFrames.jl中的dropmissing函数可简化此操作。df_no_missing_rows = dropmissing(df)julia> df_no_missing_rows 1×4 DataFrame Row │ ID Age Salary Department │ Int64 Int64 Int64 String ─────┼────────────────────────────────── 1 │ 1 25 50000 HR您还可以指定要考虑删除的列:df_no_missing_age_salary = dropmissing(df, [:Age, :Salary])注意:尽管直接,但如果缺失值普遍存在,列表式删除可能导致大量数据丢失,并且如果缺失不是随机的,可能会引入偏差。列删除:如果一列的缺失值比例非常高,它可能提供的信息很少,可以完全删除。# Suppose 'Salary' column is deemed to have too many missings df_no_salary_column = select(df, Not(:Salary))这应该谨慎进行,因为您可能会丢弃潜在有用的信息。2. 填充填充涉及用估计或计算的替代值来填补缺失值。这可以保留您的样本大小,但如果操作不慎,可能会引入其他偏差。均值、中位数或众数填充:对于数值特征,缺失值可以用该列中非缺失值的均值或中位数替换。如果数据存在异常值,通常更倾向于使用中位数,因为它对极端值不那么敏感。对于类别特征,缺失值可以用众数(最常见的类别)替换。让我们来填充df中的缺失值:对于Age列(数值),我们使用均值:mean_age = mean(skipmissing(df.Age)) # skipmissing creates an iterator over non-missing values df_imputed_age = deepcopy(df) # Work on a copy df_imputed_age.Age = coalesce.(df_imputed_age.Age, round(Int, mean_age)) # coalesce replaces missing with a valuejulia> mean_age 26.25 julia> df_imputed_age 5×4 DataFrame Row │ ID Age Salary Department │ Int64 Int64 Any Any ─────┼──────────────────────────────────── 1 │ 1 25 50000 HR 2 │ 2 30 missing Engineering 3 │ 3 26 65000 HR <- Imputed Age 4 │ 4 22 48000 missing 5 │ 5 28 missing Marketingcoalesce.(vector, value)函数是一种方便的方法,可以将vector中所有missing条目替换为value。对于Salary列(数值),我们使用中位数:median_salary = median(skipmissing(df.Salary)) df_imputed_salary = deepcopy(df_imputed_age) # Continue from previous imputation df_imputed_salary.Salary = coalesce.(df_imputed_salary.Salary, median_salary)julia> median_salary 57500.0 julia> df_imputed_salary 5×4 DataFrame Row │ ID Age Salary Department │ Int64 Int64 Float64 Any ─────┼─────────────────────────────────────── 1 │ 1 25 50000.0 HR 2 │ 2 30 57500.0 Engineering <- Imputed Salary 3 │ 3 26 65000.0 HR 4 │ 4 22 48000.0 missing 5 │ 5 28 57500.0 Marketing <- Imputed Salary对于Department列(类别),我们使用众数:# Calculate mode for Department department_counts = Dict{String, Int}() for dept in skipmissing(df.Department) department_counts[dept] = get(department_counts, dept, 0) + 1 mode_department = findmax(department_counts)[2] # findmax returns (value, key) for dicts df_imputed_all = deepcopy(df_imputed_salary) df_imputed_all.Department = coalesce.(df_imputed_all.Department, mode_department)julia> mode_department "HR" julia> df_imputed_all 5×4 DataFrame Row │ ID Age Salary Department │ Int64 Int64 Float64 String ─────┼──────────────────────────────────── 1 │ 1 25 50000.0 HR 2 │ 2 30 57500.0 Engineering 3 │ 3 26 65000.0 HR 4 │ 4 22 48000.0 HR <- Imputed Department 5 │ 5 28 57500.0 Marketing使用transform!或combine从DataFrames.jl也可以提供更简洁的方式来执行这些操作,尤其是在分组时。我们稍后会了解到的MLJ.jl包也提供了专用的FillImputer工具,以便在机器学习流程中更流畅地进行填充。更高级的填充:存在像K近邻(KNN)填充(根据相似实例填充缺失值)或回归填充(使用其他特征预测缺失值)等技术。这些技术更复杂,且功能强大,但需要仔细实现。填充的考量: 填充值会改变您的数据集。虽然它允许您保留数据,但它会降低方差,并可能扭曲变量之间的关系。通常,最好创建一个额外的二进制指示列,标记某个值是否最初缺失,因为缺失本身有时对模型来说可能具有信息量。识别和处理异常值异常值是与其余观测值显著不同的数据点。它们可能来自测量误差、实验问题或真实存在的极端自然变异。异常值可能对统计分析和机器学习模型产生过度影响,特别是那些对方差敏感的模型,如线性回归或使用平方误差损失函数的模型。识别异常值检测异常值通常涉及可视化和统计方法的结合。1. 可视化可视化工具非常适合初步感知潜在的异常值。箱线图:箱线图在可视化数值数据分布和突出异常值方面特别有效。 我们来看一个特征Measurement的样本数据集: measurements = [22, 24, 25, 26, 28, 29, 30, 32, 33, 35, 55, 23, 27, 31, 60] 值55和60看起来比大部分数据要高。{ "data": [ { "type": "box", "y": [22, 24, 25, 26, 28, 29, 30, 32, 33, 35, 55, 23, 27, 31, 60], "name": "测量值", "boxpoints": "outliers", "marker": {"color": "#339af0"}, "line": {"color": "#1c7ed6"} } ], "layout": { "title": "测量数据箱线图", "yaxis": { "title": "值" }, "height": 400, "width": 500 } }箱线图显示了Measurement数据中潜在的异常值。在“须”外部的点通常被视为异常值。直方图和散点图:直方图可以在极端位置显示孤立的条形,散点图可以显示远离数据主要聚集区域的点。2. 统计方法Z-分数:Z-分数衡量一个数据点距离均值有多少个标准差。识别异常值的常见阈值是Z-分数大于3或小于-3。 公式为:$Z = (x - \mu) / \sigma$ 其中 $x$ 是数据点,$\mu$ 是均值,$\sigma$ 是标准差。data_b = [10.0, 11.0, 12.0, 13.0, 14.0, 100.0] mean_b = mean(data_b) std_b = std(data_b) z_scores_b = (data_b .- mean_b) ./ std_bjulia> z_scores_b 6-element Vector{Float64}: -0.5401490234706847 -0.5085117491910266 -0.4768744749113684 -0.44523720063171023 -0.4135999263520521 2.384372374556842在这个小型、偏斜的数据集中,100.0的Z-分数约为2.38。如果有更多数据,并且如果100.0相对于更正态的分布确实异常,其Z-分数会更高。有时也使用|Z| > 2或2.5的阈值。四分位距(IQR)方法:此方法根据数据中间50%的分布来定义异常值。计算第一四分位数(Q1,第25百分位)和第三四分位数(Q3,第75百分位)。计算IQR:$IQR = Q3 - Q1$。将低于 $Q1 - 1.5 \times IQR$ 或高于 $Q3 + 1.5 \times IQR$ 的点定义为异常值。q1 = quantile(data_b, 0.25) q3 = quantile(data_b, 0.75) iqr_val = q3 - q1 lower_bound = q1 - 1.5 * iqr_val upper_bound = q3 + 1.5 * iqr_val outliers_b_iqr = [x for x in data_b if x < lower_bound || x > upper_bound]julia> q1 10.75 julia> q3 35.0 # Note: quantile behavior depends on interpolation; with small N, it can be tricky. # For [10,11,12,13,14,100], Q1 (25th) is (10+11)/2 = 10.5 if using certain methods, # Q3 (75th) is (14+100)/2 = 57 if using same simple method. # Statistics.quantile uses a more standard algorithm. # Let's re-run with values that make quartiles intuitive: # data_b_sorted = sort(data_b) -> [10.0, 11.0, 12.0, 13.0, 14.0, 100.0] # Q1 for this using default interpolation in Statistics.jl: # idx = (length(data_b_sorted)-1)*0.25 + 1 = 5*0.25 + 1 = 1.25 + 1 = 2.25 # So it interpolates between 2nd (11.0) and 3rd (12.0) element. # (1-0.25)*11.0 + 0.25*12.0 = 0.75*11 + 0.25*12 = 8.25 + 3 = 11.25 julia> q1_recalc = quantile(sort(data_b), 0.25) # Should be 11.25 11.25 julia> q3_recalc = quantile(sort(data_b), 0.75) # (idx = 5*0.75+1 = 4.75) interpolates 4th (13) and 5th (14) # (1-0.75)*13 + 0.75*14 = 0.25*13 + 0.75*14 = 3.25 + 10.5 = 13.75 13.75 julia> iqr_val_recalc = q3_recalc - q1_recalc 2.5 julia> lower_bound_recalc = q1_recalc - 1.5 * iqr_val_recalc 7.5 julia> upper_bound_recalc = q3_recalc + 1.5 * iqr_val_recalc 17.5 julia> outliers_b_iqr_recalc = [x for x in data_b if x < lower_bound_recalc || x > upper_bound_recalc] 1-element Vector{Float64}: 100.0值100.0被IQR方法正确识别为异常值。处理异常值的策略处理异常值的方法很大程度上取决于其原因和分析目标。删除:如果您确信异常值是由于数据录入错误或测量故障造成的,那么删除它可能是合适的。然而,这应谨慎进行,因为删除真实的极端值可能导致重要信息的丢失。# Assuming df.Salary has the outlier 100.0 from data_b df_cleaned = filter(row -> row.Salary <= upper_bound_recalc && row.Salary >= lower_bound_recalc, df_with_salary_outlier)变换:应用数学变换,如对数、平方根或倒数,可以压缩数据的范围,减少异常值引起的偏度。例如,如果数据是右偏的,对数变换(log.(data))可以使分布更对称。这个主题在数据变换中更广泛地涵盖,但在这里也相关。封顶/温莎化:这涉及用最接近的“可接受”值替换异常值。例如,高于上限($Q3 + 1.5 \times IQR$)的值可以被封顶到这个上限,而低于下限($Q1 - 1.5 \times IQR$)的值可以被封顶到下限。df_capped = deepcopy(df) # Assume df.B is like data_b # df.B = [10.0, 11.0, 12.0, 100.0, 13.0] # upper_bound_recalc was 17.5 df_capped.B = map(x -> x > upper_bound_recalc ? upper_bound_recalc : x, df.B) df_capped.B = map(x -> x < lower_bound_recalc ? lower_bound_recalc : x, df_capped.B)julia> # Assuming original df.B was [10.0, 11.0, 12.0, 100.0, 13.0] julia> # After capping with upper_bound_recalc = 17.5 and lower_bound_recalc = 7.5 julia> # df_capped.B would become [10.0, 11.0, 12.0, 17.5, 13.0]分箱(离散化):将数值数据分组到离散的箱中,有时可以减轻异常值的影响,因为异常值会落入一个极端箱中,但其确切的高值不会不成比例地影响模型。视为缺失:有时,异常值可以被视为缺失值,然后应用适当的填充技术。保留它们:如果异常值代表真实但罕见的现象,且对您的问题很重要(例如,欺诈检测中的欺诈交易),则应保留它们并可能单独分析。使用具有异常值抵抗能力的模型也可能是一种策略。数据清洗是一个迭代过程。它需要仔细检查数据,理解领域知识,并认真考虑每个清洗步骤对后续分析和模型构建的影响。没有放之四海而皆准的解决方案,最好的方法通常涉及结合针对您的特定数据集和目标量身定制的技术。