MongoDB

Lücken füllen mit $densify und $fill

Die Analyse von Zeitreihendaten ist oft erheblich einfacher, wenn man sich darauf verlassen kann, dass keine Lücken in den Daten vorhanden sind. Seit Version 5.2 bietet MongoDB mit den Aggregation Stages $densify und $fill wichtige Werkzeuge, um fehlende Dokumente zu erzeugen und fehlende Properties entweder mit konstanten Werten Daten oder auf Basis einer Interpolation zu füllen.

Ausgangsdaten

Für unser Beispiel nehmen wir an, dass in einer Collection folgende Daten vorhanden sind:

[
  {
    ts: ISODate("2023-09-20T00:00:00Z"),
    value: 100,
  },
  {
    ts: ISODate("2023-09-21T00:00:00Z"),
    value: 150,
  },
  {
    ts: ISODate("2023-09-24T00:00:00Z"),
    value: 50,
  },
  {
    ts: ISODate("2023-09-26T00:00:00Z"),
    value: 10,
  },
]

In diesem Dataset fehlen also Werte für den 22., 23. und 25.09.

Fehlende Dokumente mit $densify erzeugen

Über die $densify Stage sind wir in der Lage, die fehlenden Dokumente zu erzeugen:

[
  {
    $densify: {
      field: "ts",
      range: {
        step: 1,
        unit: "day",
        bounds: [ISODate("2023-09-20T00:00:00Z"), ISODate("2023-09-27T00:00:00Z")]
      }
    }
  }
]

Wichtige Parameter in der Stage sind einerseits das Feld, das die aufzufüllenden Werte enthält (field), die Schrittweite und Einheit, in der neue Daten erzeugt werden sollen. Da es sich in unserem Beispiel um Datumswerte handelt, haben wir eine Schrittweite von einem Tag eingestellt. Auch der Bereich, für den neue Werte erzeugt werden sollen, ist von Bedeutung. Falls der Range über die aktuell vorhandenen Minimal- und Maximalwerte hinausgeht, werden auch an den Rändern neue Dokumente erzeugt. Zu beachten ist, dass die Obergrenze exklusiv ist. In unserem Beispiel werden also Werte bis einschließlich 26.09. erzeugt.

Obige Aggregation führt zunächst zu folgender Ausgabe:

[
  {
    ts: ISODate("2023-09-20T00:00:00Z"),
    value: 100,
  },
  {
    ts: ISODate("2023-09-21T00:00:00Z"),
    value: 150,
  },
  {
    ts: ISODate("2023-09-22T00:00:00Z"),
  },
  {
    ts: ISODate("2023-09-23T00:00:00Z"),
  },
  {
    ts: ISODate("2023-09-24T00:00:00Z"),
    value: 50,
  },
  {
    ts: ISODate("2023-09-25T00:00:00Z"),
  },
  {
    ts: ISODate("2023-09-26T00:00:00Z"),
    value: 10,
  },
]

Die Ausgabe enthält die fehlenden Dokumente für die drei fehlenden Datumswerte. Auch wenn die Werte in der value Property bei den neu erzeugten Dokumenten noch fehlen, ist damit bereits der erste Schritt geschafft.

Statische Werte setzen

Die $fill Stage dient dazu, die noch fehlenden Daten zu setzen. Wollen wir beispielsweise annehmen, dass der Wert 0 in diesem Fall eingesetzt werden soll, so können wir folgende Stage-Definition dafür verwenden:

[
  {
    $densify: {
      // ...
    }
  },
  {
    $fill: {
      output: {
        value: {value: 0},
      }
    }
  }
]

Nach der Stage ist die value Property mit 0 gefüllt, sofern sie noch nicht vorhanden war:

[

  {
    ts: ISODate("2023-09-20T00:00:00Z"),
    value: 100,
  },
  {
    ts: ISODate("2023-09-21T00:00:00Z"),
    value: 150,
  },
  {
    ts: ISODate("2023-09-22T00:00:00Z"),
    value: 0,
  },
  {
    ts: ISODate("2023-09-23T00:00:00Z"),
    value: 0,
  },
  {
    ts: ISODate("2023-09-24T00:00:00Z"),
    value: 50,
  },
  {
    ts: ISODate("2023-09-25T00:00:00Z"),
    value: 0,
  },
  {
    ts: ISODate("2023-09-26T00:00:00Z"),
    value: 10,
  },
]

Werte interpolieren

In manchen Fällen reichen statische Werte jedoch nicht aus; vielmehr bestimmt sich der einzusetzende Wert anhand der vorhandenen benachbarten Werte. Die $fill Stage bietet dafür zwei Möglichkeiten:

Bei der linearen Interpolation werden die Werte entsprechend der Verbindung der vorausgehenden und nachfolgenden Werte weitergeführt:

[
  {
    $densify: {
      // ...
    },
  },
  {
    $fill: {
      sortBy: {
        ts: 1,
      },
      output: {
        value: { method: "linear" },
      },
    },
  },
]

Die Aggregation Pipeline führt zu folgender Ausgabe:

[

  {
    ts: ISODate("2023-09-20T00:00:00Z"),
    value: 100,
  },
  {
    ts: ISODate("2023-09-21T00:00:00Z"),
    value: 150,
  },
  {
    ts: ISODate("2023-09-22T00:00:00Z"),
    value: 116.66666666666667,
  },
  {
    ts: ISODate("2023-09-23T00:00:00Z"),
    value: 83.33333333333334,
  },
  {
    ts: ISODate("2023-09-24T00:00:00Z"),
    value: 50,
  },
  {
    ts: ISODate("2023-09-25T00:00:00Z"),
    value: 30,
  },
  {
    ts: ISODate("2023-09-26T00:00:00Z"),
    value: 10,
  },
]

Eine weitere Möglichkeit bietet die Verwendung der Methode “Last observation carried forward” (locf), bei der der zuletzt vorhandene Wert fortgeführt wird:

[
  {
    $densify: {
      // ...
    },
  },
  {
    $fill: {
      sortBy: {
        ts: 1,
      },
      output: {
        value: { method: "locf" },
      },
    },
  },
]

Die Ausgabe zeigt, dass die beiden Werte 150 und 50 jeweils in die folgenden, durch $densify hinzugefügten Dokumente verwendet werden:

[

  {
    ts: ISODate("2023-09-20T00:00:00Z"),
    value: 100,
  },
  {
    ts: ISODate("2023-09-21T00:00:00Z"),
    value: 150,
  },
  {
    ts: ISODate("2023-09-22T00:00:00Z"),
    value: 150,
  },
  {
    ts: ISODate("2023-09-23T00:00:00Z"),
    value: 150,
  },
  {
    ts: ISODate("2023-09-24T00:00:00Z"),
    value: 50,
  },
  {
    ts: ISODate("2023-09-25T00:00:00Z"),
    value: 50,
  },
  {
    ts: ISODate("2023-09-26T00:00:00Z"),
    value: 10,
  },
]

Falls die für die Interpolation notwendigen Ausgangswerte nicht vorhanden sind, wird ein null-Wert eingesetzt. In diesem Fall bietet die Verwendung von $fill also keine Garantie dafür, dass auch alle Werte anschließend vorhanden sind.

Verwendung von Partitionen

Unsere Beispiele sind zur besseren Übersicht davon ausgegangen, dass keine Partitionen in den Daten vorhanden sind. Es werden also alle Dokumente herangezogen, wenn Werte aufgefüllt werden. Sowohl $densify als auch $fill bieten die Möglichkeit, über den partitionBy Parameter die Identifikation von fehlenden Dokumenten und Werten auf Basis einer Partition vorzunehmen. Enthalten unsere Dokumente beispielsweise eine Eigenschaft sensor mit der Sensor-Id und wollen wir sicherstellen, dass für jede Kombination von Sensor und Datum ein Dokument vorhanden ist und dass die Interpolation der Werte innerhalb der Partition erfolgt, so können wir dies folgende Aggregation Pipeline verwenden:

[
  {
    $densify: {
      field: "ts",
      partitionByFields: ["sensor"],
      range: {
        step: 1,
        unit: "day",
        bounds: [
          ISODate("2023-09-20T00:00:00Z"),
          ISODate("2023-09-27T00:00:00Z"),
        ],
      },
    },
  },
  {
    $fill: {
      partitionByFields: ["sensor"],
      sortBy: {
        ts: 1,
      },
      output: {
        value: { method: "locf" },
      },
    },
  },
]

Obige Pipeline bildet die Partitionen auf Basis einer Sensor-Id im Feld sensor; sie erzeugt für jedes Datum, das in einer Partition fehlt, ein neues Dokument und füllt die Werte innerhalb der Partitionsgrenzen auf.

Fazit

Die beiden Stages $densify und $fill sind ein mächtiges Werkzeug bei der Vorbereitung von Daten zur Analyse. Auch ist es mitunter möglich, den API-Code zu vereinfachen, wenn man sich darauf verlassen kann, dass Daten für alle Datumswerte vorhanden sind.

Das MongoDB-Blog geht im weiterführenden Artikel Preparing Time Series data for Analysis Tools with $densify and $fill genauer auf die beiden Aggregation Stages ein und zeigt auch, wie eine regelmäßige Verteilung der Daten erreicht werden kann.


MongoDB Analyse Time Series Collections NoSql
Markus Wildgruber
Markus Wildgruber

Geschäftsführer

  • CloudArchitect
  • DeveloperCoach
  • DotNet
  • MongoDB
  • Angular