【问题标题】:Conditional Split if Delimiter is missing如果缺少分隔符,则进行条件拆分
【发布时间】:2021-04-10 18:23:37
【问题描述】:

背景: 下表包含材料 A、B、C 等相关文件的示例数据(文件名称为 MSDS、RMQ、COA、技术数据表)。来自这些文件的信息文件包括日期、重金属杂质和残留溶剂杂质及其含量,以 ppm(百万分之几)为单位。

我使用 power query 对这些数据进行了排序,以便生成下图中显示的 2 个表。

这些包含在文档中发现的最高量的重金属(蓝色)和残留溶剂(黄色)以及包含此值的文档的来源。为了复制电子表格,我在底部提供了(相当广泛的)M 代码。虽然对于这个问题非常简单; “Heavy Metals”和“Residual Solvents”是用来分隔数据的短语。

小问题: 尽管对表格的功能感到满意,但我并不认为“拆分列的拆分”(参见 M 代码)是分离数据的完全令人满意的解决方案。随后我意识到,如果一个单元格意外不包含“重金属”作为分隔符,逻辑将导致该单元格的残留溶剂数据丢失(如单元格 4E 的情况(材料 C,技术数据表))。

我可能会坚持要求那些使用此电子表格的人确保这些短语始终存在,但是我想在这里询问是否有人对提供的 M 代码有任何聪明的替代方案,以便尽管没有分隔符的情况下重金属可能会丢失(或者如果拼写不正确),残留溶剂仍会通过。

我很欣赏这对某人来说是一项相当艰巨的工作,幸运的是这是一个相对较小的问题,因此任何建议都只是一个奖励。我也只是通过它非常有趣地展示了如何使用 power query 来拆分单元格内看似复杂的数据。另请注意,表中的数据“杂乱无章”,以测试这是否会导致任何问题。

M 代码: 这是残留溶剂表的代码。 Power 查询将数据拆分为重金属和残留溶剂,然后根据表格删除相应的列。

    let
    Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
    #"Changed Type" = Table.TransformColumnTypes(Source,{{"Material", type text}, {"MSDS", type text}, {"RMQ", type text}, {"COA", type text}, {"Technical Data Sheet", type text}}),
    
    //Clean Up user Input (removed additional Spaces)
    #"Trimmed Text" = Table.TransformColumns(#"Changed Type",{{"Material", Text.Trim, type text}, {"MSDS", Text.Trim, type text}, {"RMQ", Text.Trim, type text}, {"COA", Text.Trim, type text}, {"Technical Data Sheet", Text.Trim, type text}}),
   
   //Split Data into Date, Heavy Metals and Residual Solvents
   //MSDS 
    #"Split MSDS by Heavy Metals" = Table.SplitColumn(#"Trimmed Text", "MSDS", Splitter.SplitTextByDelimiter("Heavy Metals", QuoteStyle.Csv), {"MSDS Date", "MSDS Heavy Metals"}),
    #"Split MSDS by Residual Solvents" = Table.SplitColumn(#"Split MSDS by Heavy Metals", "MSDS Heavy Metals", Splitter.SplitTextByDelimiter("Residual Solvents", QuoteStyle.Csv), {"MSDS Heavy Metals", "MSDS Residual Solvents"}),
    
    // RMQ
    #"Split RMQ by Heavy Metals" = Table.SplitColumn(#"Split MSDS by Residual Solvents", "RMQ", Splitter.SplitTextByDelimiter("Heavy Metals", QuoteStyle.Csv), {"RMQ Date", "RMQ Heavy Metals"}),
    #"Split Column by Residual Solvents" = Table.SplitColumn(#"Split RMQ by Heavy Metals", "RMQ Heavy Metals", Splitter.SplitTextByDelimiter("Residual Solvents", QuoteStyle.Csv), {"RMQ Heavy Metals", "RMQ Residual Solvents"}),
    
    //COA
    #"Split COA by Heavy Metals" = Table.SplitColumn(#"Split Column by Residual Solvents", "COA", Splitter.SplitTextByDelimiter("Heavy Metals", QuoteStyle.Csv), {"COA Date", "COA Heavy Metals"}),
    #"Split COA by Residual Solvents" = Table.SplitColumn(#"Split COA by Heavy Metals", "COA Heavy Metals", Splitter.SplitTextByDelimiter("Residual Solvents", QuoteStyle.Csv), {"COA Heavy Metals", "COA Residual Solvents"}),
    
    //Technical Data Sheet
    #"Split Technical Data Sheet by Heavy Metals" = Table.SplitColumn(#"Split COA by Residual Solvents", "Technical Data Sheet", Splitter.SplitTextByDelimiter("Heavy Metals", QuoteStyle.Csv), {"Technical Data Sheet Date", "Technical Data Sheet Heavy Metals"}),
    #"Split Technical Data Sheet by Residual Solvents" = Table.SplitColumn(#"Split Technical Data Sheet by Heavy Metals", "Technical Data Sheet Heavy Metals", Splitter.SplitTextByDelimiter("Residual Solvents", QuoteStyle.Csv), {"Technical Data Sheet Heavy Metals", "Technical Data Sheet Residual Solvents"}),
    
    //Changes Data to date type
    #"Changed Type1" = Table.TransformColumnTypes(#"Split Technical Data Sheet by Residual Solvents",{{"MSDS Date", type date}, {"RMQ Date", type date}, {"COA Date", type date}, {"Technical Data Sheet Date", type date}}),
   
   //Remove Date (add // to add date back into data)
    #"Removed Date" = Table.RemoveColumns(#"Changed Type1",{"MSDS Date", "RMQ Date", "COA Date", "Technical Data Sheet Date"}),
    
    //Clean up unnecessary line breaks
    #"Trimmed Text1" = Table.TransformColumns(#"Removed Date",{{"Material", Text.Trim, type text}, {"MSDS Heavy Metals", Text.Trim, type text}, {"MSDS Residual Solvents", Text.Trim, type text}, {"RMQ Heavy Metals", Text.Trim, type text}, {"RMQ Residual Solvents", Text.Trim, type text}, {"COA Heavy Metals", Text.Trim, type text}, {"COA Residual Solvents", Text.Trim, type text}, {"Technical Data Sheet Heavy Metals", Text.Trim, type text}, {"Technical Data Sheet Residual Solvents", Text.Trim, type text}}),
    #"Removed Columns" = Table.RemoveColumns(#"Trimmed Text1",{"MSDS Heavy Metals", "RMQ Heavy Metals", "COA Heavy Metals", "Technical Data Sheet Heavy Metals"}),
    
    #"Renamed Columns" = Table.RenameColumns(#"Removed Columns",{{"MSDS Residual Solvents", "MSDS"}, {"RMQ Residual Solvents", "RMQ"}, {"COA Residual Solvents", "COA"}, {"Technical Data Sheet Residual Solvents", "Technical Data Sheet"}}),
    
    //Unpivot data into columns, split and clean up as necessary necessary
    #"Unpivoted Other Columns" = Table.UnpivotOtherColumns(#"Renamed Columns", {"Material"}, "Source", "Amount"),
    
    #"Split Column by Delimiter" = Table.ExpandListColumn(Table.TransformColumns(#"Unpivoted Other Columns", {{"Amount", Splitter.SplitTextByDelimiter("#(lf)", QuoteStyle.None), let itemType = (type nullable text) meta [Serialized.Text = true] in type {itemType}}}), "Amount"),
    #"Changed Type2" = Table.TransformColumnTypes(#"Split Column by Delimiter",{{"Amount", type text}}),
    #"Trimmed Text2" = Table.TransformColumns(#"Changed Type2",{{"Amount", Text.Trim, type text}}),
    #"Split Column by Delimiter1" = Table.SplitColumn(#"Trimmed Text2", "Amount", Splitter.SplitTextByEachDelimiter({" "}, QuoteStyle.Csv, true), {"Heavy Metal", "Amount"}),
    #"Changed Type3" = Table.TransformColumnTypes(#"Split Column by Delimiter1",{{"Heavy Metal", type text}, {"Amount", Int64.Type}}),

 // if date is to be included add this line 
        //{"Date", (t) => t[Date]{List.PositionOf(t[Amount],List.Max(t[Amount]))}, type date}


    //Group rows by Material and Metal
//Extract the highest amount and corresponding source
    #"Grouped Rows" = Table.Group(#"Changed Type3", {"Material", "Heavy Metal"}, {
        {"Amount", each List.Max([Amount]),type number},
        {"Source", (t) => t[Source]{List.PositionOf(t[Amount],List.Max(t[Amount]))}, type text}
       
        })
in
    #"Grouped Rows"

文件链接:

https://1drv.ms/u/s!AsrLaUgt0KCLhXtP-jYDd4Z0ujKQ?e=Ba8Htx

【问题讨论】:

  • 我不明白。您编写“此单元格的残留溶剂数据将丢失”,但 E4 中的溶剂似乎确实列在您的第二个结果表中。
  • 如果您能提供可以复制/粘贴到 Excel 工作表中的数据来说明问题,那将会很有帮助。
  • 啊抱歉,我一定是在拍照前没有刷新数据。我现在会更新这张照片,但你会看到甲苯、DMSO 和甲醇消失了
  • @RonRosenfeld 我不确定如何轻松共享数据,所以我暂时将一个驱动器链接添加到文件本身。希望这会让事情变得更容易。

标签: excel split office365 powerquery delimiter


【解决方案1】:

我会以不同的方式在溶剂和金属之间进行拆分,这样无论缺少一个类别还是另一个类别都无关紧要。

如果Residual SolventsHeavy Metals 可能存在拼写错误,您甚至可以像我在代码中那样做一些模糊匹配而不是相等。

  • 取消透视除 Material 列之外的列以创建三列
  • Value 列按换行分成行
  • Trim Value 列,然后过滤掉空白
  • 根据值列添加自定义列,仅复制日期或字符串Heavy MetalsResidual Solvents
  • 向下填写,使每一行都有一个条目
  • 过滤掉日期(仅选择金属和溶剂条目)。
  • 过滤值和自定义列(参见代码中的注释)
  • 在物质和数量之间拆分值列
  • 这将为您留下一个包含五列的表格
    • 您可以过滤第五列的金属或溶剂
  • 然后按 Material 分组并提取您想要的内容

M 代码用于溶剂表

let
    Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
    #"Changed Type" = Table.TransformColumnTypes(Source,{{"Material", type text}, {"MSDS", type text}, {"RMQ", type text}, {"COA", type text}, {"Technical Data Sheet", type text}}),
    
    //Unpivot to develop a single column of solvent/metals/date data
    #"Unpivoted Other Columns" = Table.UnpivotOtherColumns(#"Changed Type", {"Material"}, "Attribute", "Value"),

    //split into rows by line feed
    #"Split Column by Delimiter" = Table.ExpandListColumn(Table.TransformColumns(#"Unpivoted Other Columns", 
        {{"Value", Splitter.SplitTextByDelimiter("#(lf)", QuoteStyle.Csv), let itemType = (type nullable text) meta [Serialized.Text = true] in type {itemType}}}), "Value"),
    #"Trimmed Text" = Table.TransformColumns(#"Split Column by Delimiter",{{"Value", Text.Trim, type text}}),

    //filter out the blank rows
    #"Filtered Rows" = Table.SelectRows(#"Trimmed Text", each ([Value] <> "")),

    //Add custom column for separating the tables
    #"Added Custom" = Table.AddColumn(#"Filtered Rows", "Custom", each try Date.FromText([Value]) otherwise 
        if [Value] = "Heavy Metals" or [Value] = "Residual Solvents" then [Value] else null),
    #"Changed Type1" = Table.TransformColumnTypes(#"Added Custom",{{"Custom", type text}}),
    #"Filled Down" = Table.FillDown(#"Changed Type1",{"Custom"}),

    //Filter the value and custom columns to remove contaminant type from Value column and remove dates from Custom column
    #"Filtered Rows1" = Table.SelectRows(#"Filled Down", 
        each ([Custom] = "Heavy Metals" or [Custom] = "Residual Solvents") and ([Value] <> "Heavy Metals" and [Value] <> "Residual Solvents")),

    //split substance from amount
    #"Split Column by Delimiter1" = Table.SplitColumn(#"Filtered Rows1", "Value", 
        Splitter.SplitTextByEachDelimiter({" "}, QuoteStyle.Csv, true), {"Substance", "Amount"}),
    #"Changed Type2" = Table.TransformColumnTypes(#"Split Column by Delimiter1",{{"Substance", type text}, {"Amount", Int64.Type}}),

    //Filter for Solvents Table
    #"Filtered Rows2" = Table.SelectRows(#"Changed Type2", each ([Custom] = "Residual Solvents")),

    //Groub by Material and Substance, then extract the Max contaminant and Source
    #"Grouped Rows" = Table.Group(#"Filtered Rows2", {"Material", "Substance"}, {
        {"Amount", each List.Max([Amount]), type number},
        {"Source", (t) => t[Attribute]{List.PositionOf(t[Amount],List.Max(t[Amount]))}, type text}
        })
in
    #"Grouped Rows"

注意

为了获得学习经验,我决定解决从两个或多个来源引用相同的最大数量的问题。

我更改了Table.Group 函数的Source 提取行以将这些作为分号分隔的字符串返回:

    //Group by Material and Substance
    //Extract Max Amount and Source (or multiple Sources if max amount identical)
    #"Grouped Rows" = Table.Group(#"Filtered Rows2", {"Material", "Substance"}, {
        //{"All", each _, type table [Material=nullable text, Attribute=text, Substance=nullable text, Amount=nullable number, Custom=nullable text]},        
        {"Amount", each List.Max([Amount]), type number},
        //{"Source", (t) => t[Attribute]{List.PositionOf(t[Amount],List.Max(t[Amount]))}, type text},
        {"Source", (t)=> Text.Combine(
            List.Generate(() => [Counter=1, IDX = List.PositionOf(t[Amount],List.Max(t[Amount]))],
                 each [Counter] <= List.Count(List.PositionOf(t[Amount],List.Max(t[Amount]),4)),
                 each [Counter = [Counter]+1, IDX = List.PositionOf(t[Amount],List.Max(t[Amount]),4){[Counter]}],
                 each t[Attribute]{[IDX]}),"; ")}
        })

以这种方式使用List.Generate 是根据Chris Webb's BI Blog 中解决的一些不同的问题改编而来的

【讨论】:

  • 这真的很棒。以更清晰的方式拆分信息并使修改更容易。非常感谢。
  • 我刚刚在最后看到了你的注释,有趣的是,如果它们在不同属性中相同,则只取其中一个最大值实际上非常有用。由于某种原因,您发布的带有我之前提出的问题的“分组行”的代码仅返回最大值之一。此处发布的新代码为具有相同最大值的每个属性返回两者。这对我来说真的很奇怪,因为比较 Grouping the Rows 的代码看起来是相同的。知道为什么会这样吗?
  • @Nick 绝对不是相同的代码。对于Source这一行,前面的代码行被注释掉了;返回多个匹配的行正在使用List.Generate 函数。顺便说一句,如果您需要包含日期信息并遇到问题,请告诉我。
  • 啊,请忽略我。我在每列中输入了 X 30 和 X 30(额外的空格),因此它们的标识不同。再次感谢您的帮助。
  • 您好,您在回答中提到,如果重金属拼写错误,您应用了模糊匹配。通过错误拼写重金属来尝试这一点,我得到了两个独立的结果。浏览代码,我看不到任何模糊匹配的参考。这是不是事后才想到的?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-23
  • 2018-04-25
  • 2010-10-29
相关资源
最近更新 更多