用电量查询填补时间空白

Posted

技术标签:

【中文标题】用电量查询填补时间空白【英文标题】:fill time gaps with power query 【发布时间】:2019-01-22 17:20:28 【问题描述】:

我有以下数据

   start        stop       status
+-----------+-----------+-----------+
| 09:01:10  | 09:01:40  |  active   |
| 09:02:30  | 09:04:50  |  active   |
| 09:10:01  | 09:11:50  |  active   |
+-----------+-----------+-----------+

我想用“被动”来填补空白

   start        stop       status
+-----------+-----------+-----------+
| 09:01:10  | 09:01:40  |  active   |
| 09:01:40  | 09:02:30  |  passive  |
| 09:02:30  | 09:04:50  |  active   |
| 09:04:50  | 09:10:01  |  passive  |
| 09:10:01  | 09:11:50  |  active   |
+-----------+-----------+-----------+

如何在 M Query 语言中执行此操作?

【问题讨论】:

【参考方案1】:

您可以尝试以下方法(我的前两个步骤 someTablechangedTypes 只是为了重新创建您的示例数据):

let
    someTable = Table.FromColumns("09:01:10", "09:02:30", "09:10:01", "09:01:40", "09:04:50", "09:11:50", "active", "active", "active", "start","stop","status"),
    changedTypes = Table.TransformColumnTypes(someTable, "start", type duration, "stop", type duration, "status", type text),
    listOfRecords = Table.ToRecords(changedTypes),
    transformList = List.Accumulate(List.Skip(List.Positions(listOfRecords)), listOfRecords0, (listState, currentIndex) =>
        let
            previousRecord = listOfRecordscurrentIndex-1,
            currentRecord = listOfRecordscurrentIndex,
            thereIsAGap = currentRecord[start] <> previousRecord[stop],
            recordsToAdd = if thereIsAGap then [start=previousRecord[stop], stop=currentRecord[start], status="passive"], currentRecord else currentRecord,
            append = listState & recordsToAdd
        in
            append
    ),
    backToTable = Table.FromRecords(transformList, type table [start=duration, stop=duration, status=text])
in
    backToTable

这是我开始的(在changedTypes 步骤):

这就是我最终的结果:

要与您现有的 M 代码集成,您可能需要:

从我的代码中删除 someTablechangedTypes(并替换为您现有的查询) 将listOfRecords 步骤中的changedTypes 更改为您调用的最后一步(否则,如果您的代码中没有changedTypes 表达式,则会收到错误消息)。

编辑:

对于我的回答,我的建议是:

尝试在上面的代码中更改这一行:

listOfRecords = Table.ToRecords(changedTypes),

listOfRecords = List.Buffer(Table.ToRecords(changedTypes)),

我发现将列表存储在内存中显着减少了我的刷新时间(如果量化,可能约为 90%)。我想存在限制和缺点(例如,如果列表不适合),但可能适合您的用例。

您是否遇到过类似的行为?此外,不幸的是,我的基本图表显示了代码整体的非线性复杂性。

最后说明:我发现在刷新查询时生成和处理 100k 行会导致堆栈溢出(这可能是由于输入行的生成,可能不是新行的插入,不知道)。很明显,这种方法有局限性。

【讨论】:

请注意,如果您的数据未正确排序,则在移动索引值时需要小心。 是的,显示的数据(有问题)似乎已排序,所以我做了这个假设。如果不是,则需要在填补空白之前对其进行排序。 在对数万条记录执行此操作时,是否详细说明了此方法的性能? @intrixius 理论上,这种方法只查看(表的)每一行一次,因为它一次完成所有事情。它应该是 O(N) 并且尽可能高效(就复杂性而言)。但是,在实践中,我不知道是否还有其他更有效的方法,因为 tablerecordstable 的转换可能无法很好地扩展(取决于 Power Query 的内部实现)。您可以使用自己的数据进行尝试——或使用List.Repeat 有效地将行数乘以 10k 以模拟该比例。我会这样做,但我是通过手机发帖的。 我已经用5000条记录测试了这种方法,但是在它还在运行的时候2分钟后取消了它......当我只保留500条记录时,大约需要7秒; 2000 行大约是 30 秒...【参考方案2】:

我想我可能有更好的解决方案。

从您的源表(假设它已排序)中,添加一个从 0 开始的索引列和一个从 1 开始的索引列,然后将表与自身合并,对索引列进行左外连接并展开start 专栏。

删除除stopstatusstart.1 之外的列并过滤掉空值。

将列重命名为startstatusstop,并将"active" 替换为"passive"

最后,将此表附加到您的原始表中。

let
    Source = Table.RenameColumns(#"Removed Columns","Column1.2", "start", "Column1.3", "stop", "Column1.4", "status"),
    Add1Index = Table.AddIndexColumn(Source, "Index", 1, 1),
    Add0Index = Table.AddIndexColumn(Add1Index, "Index.1", 0, 1),
    SelfMerge = Table.NestedJoin(Add0Index,"Index",Add0Index,"Index.1","Added Index1",JoinKind.LeftOuter),
    ExpandStart1 = Table.ExpandTableColumn(SelfMerge, "Added Index1", "start", "start.1"),
    RemoveCols = Table.RemoveColumns(ExpandStart1,"start", "Index", "Index.1"),
    FilterNulls = Table.SelectRows(RemoveCols, each ([start.1] <> null)),
    RenameCols = Table.RenameColumns(FilterNulls,"stop", "start", "start.1", "stop"),
    ActiveToPassive = Table.ReplaceValue(RenameCols,"active","passive",Replacer.ReplaceText,"status"),
    AppendQuery = Table.Combine(Source, ActiveToPassive),
    #"Sorted Rows" = Table.Sort(AppendQuery,"start", Order.Ascending)
in
    #"Sorted Rows"

这应该是 O(n) 复杂度与@chillin 类似的逻辑,但我认为应该比使用自定义函数更快,因为它将使用内置-in 可能高度优化的合并。

【讨论】:

不错的方法,我昨天想到了这个,但我认为它只会帮助确定在哪里插入行的初始部分。但我认为您关于合并高度优化的观点似乎是可能的,看看 OP 是否可以证实这一点会很有趣。好主意。 我刚刚用一组超过一百万个不同的开始时间对此进行了测试,并且查询的加载时间不到 30 秒。它几乎可以完全通过 GUI 完成! 非常酷,您的解决方案似乎更具可扩展性和性能。 OP 应该改变接受的解决方案。 让我也试试这个,我会带着我的结果回来并选择最佳答案:-) 这太完美了!它运行得非常顺利同时,我多次使用这个“添加索引(0)和添加索引(1)”原理来解决类似的问题,我必须找到以前的出现......【参考方案3】:

我会这样处理:

    复制第一个表。 将“主动”替换为“被动”。 删除start 列。 将stop 重命名为start。 通过在当前stop 时间之后的原始表中查找最早的start 时间来创建一个新的stop 列。 过滤掉这个新列中的空值。 将此表附加到原始表。

M 代码如下所示:

let
    Source = <...your starting table...>
    PassiveStatus = Table.ReplaceValue(Source,"active","passive",Replacer.ReplaceText,"status"),
    RemoveStart = Table.RemoveColumns(PassiveStatus,"start"),
    RenameStart = Table.RenameColumns(RemoveStart,"stop", "start"),
    AddStop = Table.AddColumn(RenameStart, "stop", (C) => List.Min(List.Select(Source[start], each _ > C[start])), type time),
    RemoveNulls = Table.SelectRows(AddStop, each ([stop] <> null)),
    CombineTables = Table.Combine(Source, RemoveNulls),
    #"Sorted Rows" = Table.Sort(CombineTables,"start", Order.Ascending)
in
    #"Sorted Rows"

上面唯一棘手的一点是自定义列部分,我在其中定义了这样的新列:

(C) => List.Min(List.Select(Source[start], each _ > C[start]))

这将获取列/列表Source[start] 中的每个项目并将其与当前行中的时间进行比较。它只选择当前行中时间之后出现的那些,然后在该列表中取最小值以找到最早的那个。

【讨论】:

这也可以按预期工作,谢谢!由于Chilling的速度有点快,所以我接受了他的回答……您能否详细说明两者哪个性能更好?我需要对数万条记录执行此操作... 我猜它们都可以处理数万条记录。他的计算复杂度似乎较低,但很难预测性能。尝试他们两个,看看他们如何处理。如果它们都合理,那么如果您必须在一年内回来并对其进行修改,请选择您更容易理解和维护的那个。 我用更多数据测试了这种方法。我有一个 20.000 记录表;我忘了添加一个“保留顶部行”过滤器,所以它运行所有行......不幸的是,它使 Excel Power Query 无响应大约一分钟;当它再次响应时,它继续加载......之后我只保留了 500 行,它运行了 5 秒; 2000 行运行了 30 秒,但同样,excel 在一段时间内变得无响应......

以上是关于用电量查询填补时间空白的主要内容,如果未能解决你的问题,请参考以下文章

SQL 查询以填补跨时间缺失的空白并获取最后一个非空值

填补轮廓空白

用基于优先级的集合来填补空白

填补日期空白和插值

复制记录以填补日期之间的空白

填补表中值之间的空白 - MySQL