在使用 power query 比较两个 excel csv 并显示两个工作表之间的差异 (additions/deletions) 时遇到问题

Having trouble using power query to compare two excel csv's and showing differences (additions/deletions) between two sheets

我有一个日常任务,我需要比较两个 .csv 文件,这些文件包含用户数据以及分配给他们的键集。人们每天都会通过电子邮件收到新的 .csv 文件,并检查添加或删除了哪些用户,以及哪些用户的密钥集发生了更改。每个文件都有大约 1000 个用户。我将其设置为两个文件位于电源查询运行所在的文件夹中。这是我的第一个项目,所以我想看看它是否可以用于此目的。

我玩过 power query,能够显示列表用户之间的差异。我导入并转换了数据,删除了不必要的列,将用户列分组以计算该列中每个名称的数量,如果找到两个则取消选择。这向我展示了差异,但缺乏我试图达到的比较。

我打算尝试其他方法,但我对 power query 可以做的所有事情都没有经验。我制作了一个测试数据集,看看是否有人知道如何创建此报告

Day 1 Keys
Dave 1 Key 1/ Key 2/ Key 3
Dave 2 Key 4/ Key 5
Dave 3 Key 1
Dave 4 Key 3/ Key 5
Day 2 Keys
Dave 2 Key 1/ Key 5
Dave 3 Key 1
Dave 4 Key 3/ Key 5
Dave 5 Key 1

结果应该显示删除了 Dave 1,添加了 Dave 5,Dave 2 有一个密钥更改并显示密钥更改。

如果有人对如何创建此文件有任何想法,请告诉我或指出在哪里可以找到结果。我只能在我的工作计算机上访问 excel,所以我试图找到一种方法来使用可用的软件,而不是说服老板购买任何新软件。

教程化答案

此方法从名为 Key Files 的文件夹中收集数据,该文件夹包含一组每日 csv 文件。它将文件夹中的每个文件汇总为规范化的 table 从一天到下一天的更改事件:

  • 已删除 - 名称已被删除
  • 新 - 名称已添加
  • 已添加密钥 - 名称已添加密钥
  • 密钥已删除 - 名称已删除密钥 输出 table 将包含四个字段:
  • 日期 - 取自 CSV
  • 名称 - 与事件相关
  • 事件 - 按上述定义计算
  • Key - 如果添加或删除了一个键,否则为 null

第 1 步 - 从文件夹中获取文件 因为您还询问了如何将数据导入 Power Query,所以这里有一个示例。一旦你看到它是如何完成的,你就可以研究更多这样的技术,然后从那里开始。我将 csv 文件放入 Documents 文件夹中名为 Key Files 的文件夹中以供说明。我做了三个文件,这样例子就清楚了。

供参考,文件包含以下数据:

keyfile0.csv

15/10/2021 Keys
Dave 0 Key 2/ Key 3
Dave 1 Key 1/ Key 2/ Key 3
Dave 2 Key 4/ Key 5
Dave 3 Key 5
Dave 4 Key 3/ Key 5

keyfile1.csv

16/10/2021 Keys
Dave 1 Key 1/ Key 2/ Key 3
Dave 2 Key 4/ Key 5
Dave 3 Key 1
Dave 4 Key 3/ Key 5
Dave 6 Key 2/ Key 3

keyfile2.csv

17/10/2021 Keys
Dave 2 Key 1/ Key 5
Dave 3 Key 1
Dave 4 Key 3/ Key 5
Dave 5 Key 1
Dave 6 Key 3/ Key 5

要获取这些文件,您需要从 Data 选项卡中 Get Data >> “From Folder**”,如下所示:

我这个例子的测试文件夹路径是:C:\Users\Admin\Documents\Key Files

您还可以从 Text/CSV 获取文件 ,但是如果您的两个 CSV 文件的名称不断变化,您将需要每次都修改您的 Power Query 脚本你运行吧。根据您评论中的描述,我认为将所有 csv 放入一个文件夹并让脚本适应会更容易。

您将得到一个 window,看起来像这样:

您需要选择合并和转换数据。之后,它会根据它看到的第一个文件弹出一个table,你可以点击OK。现在这需要一个解释——PQ 创建了一个脚本和一个函数来读取该文件夹中的所有文件并将它们附加到一个 table 中。这种方法可以让您一次吃掉所有文件,而不必担心它们的名字。您为这种便利付出的代价是您必须将它们拆分回逻辑日值,正如您从该屏幕截图中看到的 Key Files table:

不知何故,第 8 行必须与第 3 行匹配以查看 Dave 1 是否更改了密钥,然后您需要能够检测到 Dave 1 在 10 月 17 日被删除。同时,第 14 行中的 Dave 2 需要与第 9 行中的 Dave 2 进行比较,而不是与第 4 行中的 Dave 2 进行比较。因此您需要某种方式来了解日期的顺序。或者:

  1. 文件名必须按某种顺序序列化或
  2. 您的 header 第 1 天、第 2 天等必须具有与实际日期类似的序列化值。

我选择了 2,因为我无法猜测您的文件名的结构,而且 2 更难实现,因此最好是教程式的答案。我将在下一步中执行此操作,所以让我们到此为止,并显示上面创建 Key Files table 的脚本:

let
    Source = Folder.Files("C:\Users\Admin\Documents\Key Files"),
    #"Filtered Hidden Files1" = Table.SelectRows(Source, each [Attributes]?[Hidden]? <> true),
    #"Invoke Custom Function1" = Table.AddColumn(#"Filtered Hidden Files1", "Transform File", each #"Transform File"([Content])),
    #"Renamed Columns1" = Table.RenameColumns(#"Invoke Custom Function1", {"Name", "Source.Name"}),
    #"Removed Other Columns1" = Table.SelectColumns(#"Renamed Columns1", {"Source.Name", "Transform File"}),
    #"Expanded Table Column1" = Table.ExpandTableColumn(#"Removed Other Columns1", "Transform File", Table.ColumnNames(#"Transform File"(#"Sample File"))),
    #"Changed Type" = Table.TransformColumnTypes(#"Expanded Table Column1",{{"Source.Name", type text}, {"Column1", type text}, {"Column2", type text}})
in
    #"Changed Type"

步骤 2 - 转换密钥文件 Table 如上所示,这一步是必需的,因为上面选择从文件夹中读取。如果我要在实践中这样做,而不是作为教程,我会简化它,但相反,我会分几个步骤来做。为了说明,我将从 Key Files table:

创建两个 table
  • filedates 包含文件名和关联的日期
  • nTable 是标准化的 table,将在最后一步中使用以提供计算结果。

要创建这些文件,right-click 密钥文件 table 和 select 参考 这样做两次。它将创建两个 table,分别称为 Key Files (2)Key Files (3) 将它们重命名为 filedatesnTable。每一个都必须转变。我不会深入细节,因为这会使它变得更长 post,但这是每个的 M 脚本:

对于 filedates,您只想制作一个 table 文件名及其日期。有很多方法可以做到这一点,但我只是筛选了作品“Keys”,因为它计算速度很快。

let
    Source = #"Key Files",
    #"Filtered Rows" = Table.SelectRows(Source, each ([Column2] = "Keys"))
in
    #"Filtered Rows"

对于 nTable,您要删除“键”header,然后将结果与 filedates 这样你就可以有一个序列化的引用。如上所述,我选择使用日期作为序列参考。然后我按行拆分键和renamed/removed 列。

let
    Source = #"Key Files",
    #"Filtered Rows" = Table.SelectRows(Source, each ([Column2] <> "Keys")),
    #"Merged Queries" = Table.NestedJoin(#"Filtered Rows", {"Source.Name"}, filedates, {"Source.Name"}, "filedates", JoinKind.LeftOuter),
    #"Expanded filedates" = Table.ExpandTableColumn(#"Merged Queries", "filedates", {"Column1"}, {"filedates.Column1"}),
    #"Renamed Columns" = Table.RenameColumns(#"Expanded filedates",{{"filedates.Column1", "Date"}, {"Column1", "Name"}, {"Column2", "Keys"}}),
    #"Changed Type" = Table.TransformColumnTypes(#"Renamed Columns",{{"Date", type date}}),
    #"Removed Columns" = Table.RemoveColumns(#"Changed Type",{"Source.Name"}),
    #"Split Column by Delimiter" = Table.ExpandListColumn(Table.TransformColumns(#"Removed Columns", {{"Keys", Splitter.SplitTextByDelimiter("/ ", QuoteStyle.Csv), let itemType = (type nullable text) meta [Serialized.Text = true] in type {itemType}}}), "Keys"),
    #"Changed Type1" = Table.TransformColumnTypes(#"Split Column by Delimiter",{{"Keys", type text}}),
    #"Trimmed Text" = Table.TransformColumns(#"Changed Type1",{{"Keys", Text.Trim, type text}})
in
    #"Trimmed Text"

nTable 的结果如下所示:

步骤 3 - 计算结果 所以这就是你问题的答案。同样,我将把它作为一个单独的步骤来进行说明和模块化。

NB: this is where Ron Rosenfeld said you could simply push this out and then do all the processing in Excel. The remaining steps are complex. I did not create PQ Functions because it would be harder to show and understand. This is more of a tutorial to see how you could do things. With more Power Query knowledge, you can modify this to suit your needs.

下面是接受nTable并产生一个table的M脚本,我命名为output.您可以进入高级编辑器并将其粘贴为新源。之后您可以关闭并加载到您的 Excel sheet 以查看 table 结果。

let
    Source = nTable,
    SortedNTable = Table.Sort(Source,{{"Name", Order.Ascending}, {"Keys", Order.Ascending}, {"Date", Order.Ascending}}),
    UniqueNameDates = Table.Distinct(SortedNTable, {"Name", "Date"}),
    CalculatedLatest = List.Max(SortedNTable[Date]), //
    CalculatedEarliest = List.Min(SortedNTable[Date]),
    NamesFirstSeen = Table.Group(SortedNTable, {"Name"}, {{"Date", each List.Min([Date]), type nullable date}}),
    NamesAdded = Table.AddColumn(NamesFirstSeen, "Event", each "Added"),
    NamesLastSeen = Table.Group(SortedNTable, {"Name"}, {{"LSDate", each List.Max([Date]), type nullable date}}),
    NamesDeleted = Table.AddColumn(NamesLastSeen, "Event", each "Deleted"),
    AdjNamesDeleted = Table.AddColumn(NamesDeleted, "Date", each Date.AddDays([LSDate],1)), //names are  deleted on the day after last seen
    NameKeysFirstSeen = Table.Group(SortedNTable, {"Name", "Keys"}, {{"Date", each List.Min([Date]), type nullable date}}),
    KeysAdded = Table.AddColumn(NameKeysFirstSeen, "Event", each "Key Added"),
    NameKeysLastSeen = Table.Group(SortedNTable, {"Name", "Keys"}, {{"LSDate", each List.Max([Date]), type nullable date}}),
    KeysDeleted = Table.AddColumn(NameKeysLastSeen, "Event", each "Key Deleted"),
    AdjKeysDeleted = Table.AddColumn(KeysDeleted, "Date", each Date.AddDays([LSDate],1)), //keys are  deleted on the day after last seen
   // bring it all together
    #"Appended Query" = Table.Combine({NamesAdded, AdjNamesDeleted, KeysAdded, AdjKeysDeleted}),
    #"Removed Columns" = Table.RemoveColumns(#"Appended Query",{"LSDate"}),
   //filter out first day adds and last day deletes
    #"Filtered Rows" = Table.SelectRows(#"Removed Columns", each [Date] <> CalculatedEarliest or not Text.Contains([Event], "Added")),
    #"Filtered Rows1" = Table.SelectRows(#"Filtered Rows", each [Date] <> Date.AddDays(CalculatedLatest,1) or not Text.Contains([Event], "Deleted")),
    #"Changed Type" = Table.TransformColumnTypes(#"Filtered Rows1",{{"Name", type text}, {"Date", type date}, {"Event", type text}, {"Keys", type text}}),
#"Sorted Rows" = Table.Sort(#"Changed Type",{{"Name", Order.Ascending}, {"Date", Order.Ascending}})
in
    #"Sorted Rows"

上面的脚本使用了逻辑变量名,目的是为了让步骤更清晰,而且里面有一些限制// comments。将其粘贴到高级编辑器中(在您完成第 1 步和第 2 步之后)会让您更清楚地看到它并检查每一步的 输出 table。

总结

这是我根据上面制作的假数据得出的结果:

Name Date Event Keys
Dave 0 16/10/2021 Deleted
Dave 0 16/10/2021 Key Deleted Key 3
Dave 0 16/10/2021 Key Deleted Key 2
Dave 1 17/10/2021 Deleted
Dave 1 17/10/2021 Key Deleted Key 3
Dave 1 17/10/2021 Key Deleted Key 2
Dave 1 17/10/2021 Key Deleted Key 1
Dave 2 17/10/2021 Key Added Key 1
Dave 2 17/10/2021 Key Deleted Key 4
Dave 3 16/10/2021 Key Added Key 1
Dave 3 16/10/2021 Key Deleted Key 5
Dave 5 17/10/2021 Added
Dave 5 17/10/2021 Key Added Key 1
Dave 6 16/10/2021 Added
Dave 6 16/10/2021 Key Added Key 2
Dave 6 16/10/2021 Key Added Key 3
Dave 6 17/10/2021 Key Deleted Key 2
Dave 6 17/10/2021 Key Added Key 5

因此,正如 Ron 在评论中指出的那样,最后一步非常复杂。它以符合您的标准的方式提供事件,但它可能仍然不是您正在寻找的。无论如何,这种 3 步方法允许您将所有要处理的 CSV 文件转储到一个文件夹中,然后处理所有这些文件,无论它们有多大或有多少。

Here is the previous answer which is quite simplistic, but shows the basic idea.

基本答案

为了在 Power Query 中生成模拟您的情况的 M 代码,我使用 Table1 作为 Day1 table 和 Table2 作为 Day2 table .假设您已将这些提取到 Power Query 中,脚本可能是:

let
     Source = Table.NestedJoin(Table2, {"Day 2"}, Table1, {"Day 1"}, "Table1", JoinKind.FullOuter),
     #"Expanded Table1" = Table.ExpandTableColumn(Source, "Table1", {"Day 1", "Keys"}, {"Table1.Day 1", "Table1.Keys"}),
     #"Added Conditional Column" = Table.AddColumn(#"Expanded Table1", "Status", each if [Day 2] = null then "Deleted" else if [Table1.Day 1] = null then "New" else if [Keys] <> [Table1.Keys] then "Changed Keys" else null),
     #"Filtered Rows" = Table.SelectRows(#"Added Conditional Column", each ([Status] <> null)),
     #"Added Conditional Column1" = Table.AddColumn(#"Filtered Rows", "Name", each if [Status] = "New" then [Day 2] else null),
     #"Merged Columns" = Table.CombineColumns(#"Added Conditional Column1",{"Name", "Table1.Day 1"},Combiner.CombineTextByDelimiter("", QuoteStyle.None),"Name"),
     #"Removed Other Columns" = Table.SelectColumns(#"Merged Columns",{"Name", "Status"})
in
    #"Removed Other Columns"

这对两个 table 进行了完全外部联接,然后它使用条件列来确定每一行是新建、删除、更改的键还是没有更改(null )。然后过滤掉无变化并应用另一个条件列来识别新实例并从 Day2 复制名称。它将条件列与 Table1.Day1 名称合并以生成一个统一的名称列表。它删除了不必要的列,您留下了规范化的 table 名称和状态。我不知道你想要它如何呈现,但是有了这样一个规范化的 table,你可以在 Power Query 或 Excel.

中进一步塑造它