Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

356
Vistas
Calcule max en una ventana deslizante para TimeSeries

Aporte:

 public class MyObject { public double Value { get; set; } public DateTime Date { get; set; } }

Método para generar objetos de prueba:

 public static MyObject[] GetTestObjects() { var rnd = new Random(); var date = new DateTime(2021, 1, 1, 0, 0, 0); var result = new List<MyObject>(); for (int i = 0; i < 50000; i++) { //this is to simulate real data having gaps if (rnd.Next(100) < 25) { continue; } var myObject = new MyObject() { Value = rnd.NextDouble(), Date = date.AddMinutes(15 * i) }; result.Add(myObject); } return result.ToArray(); }

Dado esto, necesito calcular el valor máximo de los 12 meses anteriores para cada myObject. Podría pensar en hacer esto InParallel, pero tal vez haya una solución optimizada.

Perdón por no estar claro, esto es lo que uso ahora mismo para obtener lo que quiero:

 public MyObject[] BruteForceBackward(MyObject[] testData) { return testData.AsParallel().Select(point => { var max = testData.Where(x => x.Date <= point.Date && x.Date >= point.Date.AddYears(-1)).Max(x => x.Value); return new MyObject() { Date = point.Date, Value = point.Value / max }; }).OrderBy(r => r.Date).ToArray(); }

Esto funciona pero es lento y consume recursos del procesador (imagínate, tienes 100k objetos), creo que debe haber algo mejor

over 4 years ago · Santiago Trujillo
4 Respuestas
Responde la pregunta

0

Suponiendo que quiere decir que necesita el Value máximo para cada uno de los últimos 12 meses desde result , entonces puede usar LINQ:

 var beginDateTime = DateTime.Now.AddMonths(-12); var ans = result.Where(r => r.Date >= beginDateTime).GroupBy(r => r.Date.Month).Select(mg => mg.MaxBy(r => r.Value)).ToList();

Ejecutando un tiempo, entiendo que poner AsParallel después del result cambia el tiempo de ejecución de alrededor de 16 ms (primera ejecución) a alrededor de 32 ms, por lo que en realidad es más lento. Es casi lo mismo después de Where y unos 23 ms después de GroupBy (procesando los 12 grupos en paralelo). Al menos en mi PC, no hay suficientes datos ni operaciones complejas para el paralelismo, pero GroupBy no es el más eficiente.

Usando una matriz y probando cada elemento, obtengo los resultados en aproximadamente 1,2 ms:

 var maxMOs = new MyObject[12]; foreach (var r in result.Where(r => r.Date >= beginDateTime)) { var monthIndex = r.Date.Month-1; if (maxMOs[monthIndex] == null || r.Value > maxMOs[monthIndex].Value) maxMOs[monthIndex] = r; }

Tenga en cuenta que los resultados no son cronológicos; puede compensar monthIndex con el mes de hoy para ordenar los resultados si lo desea.

 var maxMOs = new MyObject[12]; var offset = DateTime.Now.Month-11; foreach (var r in result.Where(r => r.Date >= beginDateTime)) { var monthIndex = r.Date.Month-offset; if (maxMOs[monthIndex] == null || r.Value > maxMOs[monthIndex].Value) maxMOs[monthIndex] = r; }

Una microoptimización (principalmente útil en ejecuciones repetidas) es invertir la prueba y usar el operador de propagación nula:

 if (!(r.Value <= maxMOs[monthIndex]?.Value))

Esto ahorra alrededor de 0,2 ms en la primera ejecución, pero hasta 0,5 ms en las ejecuciones posteriores.

over 4 years ago · Santiago Trujillo Denunciar

0

Tenía un proyecto similar en el que tenía que calcular esas cosas en toneladas de datos de sensores.

En general, desea reducir la cantidad de bucles que recorren todos sus datos. En el mejor de los casos, desea tocar cada elemento solo una vez.

Process Array (equiv. de BruteForceBackwards )

 public static MyObject[] FlowThroughForward(ref MyObject[] testData) { // generate return array MyObject[] returnData = new MyObject[testData.Length]; // keep track to minimize processing double currentMaximum = 0; List<MyObject> maximumValues = new List<MyObject>(); // go through the elements for (int i = 0; i < testData.Length; i++) { // calculate the oldest date to keep in tracking list DateTime targetDate = testData[i].Date.AddYears(-1); // maximum logic if (testData[i].Value >= currentMaximum) { // new maximum found, clear tracking list // this is the best case scenario maximumValues.Clear(); currentMaximum = testData[i].Value; } else { // unfortunately, no new maximum was found // go backwards the maximum tracking list and check for smaller values // clear the list of all smaller values. The list should therefore always // be in descending order for (int b = maximumValues.Count - 1; b >= 0; b--) { if (maximumValues[b].Value <= testData[i].Value) { // a lower value has been found. We have a newer, higher value // clear this waste value from the tracking list maximumValues.RemoveAt(b); } else { // there are no more lower values. // stop looking for smaller values to save time break; } } } // append new value to tracking list, no matter if higher or lower // all future values might be lower maximumValues.Add(testData[i]); // check if the oldest value is too old to be kept in the tracking list while (maximumValues[0].Date < targetDate) { // oldest value is to be removed maximumValues.RemoveAt(0); // update maximum currentMaximum = maximumValues[0].Value; } // add object to result list returnData[i] = new MyObject() { Date = testData[i].Date, Value = testData[i].Value / currentMaximum }; ; } return returnData; }

Datos en tiempo real o datos transmitidos

Nota: si tiene listas realmente grandes, es posible que tenga problemas de memoria con su enfoque para pasar una matriz completa. En este caso: pase un valor a la vez, páselo del valor más antiguo al valor más nuevo. Guarde los valores de uno en uno. Esta función también se puede utilizar en datos en tiempo real.
El método de prueba está incluido en el código.

 static void Main(string[] args) { int length = 50000; Stopwatch stopWatch1 = new Stopwatch(); stopWatch1.Start(); var myObject = new MyObject(); var result = new List<MyObject>(); var date = new DateTime(2021, 1, 1, 0, 0, 0); for (int i = 0; i < length; i++) { //this is to simulate real data having gaps if (rnd.Next(100) < 25) { continue; } myObject.Value = rnd.NextDouble(); myObject.Date = date.AddMinutes(15 * i); result.Add(CalculateNextObject(ref myObject)); } stopWatch1.Stop(); Console.WriteLine("test code executed in " + stopWatch1.ElapsedMilliseconds + " ms"); Thread.Sleep(1000000); } private static Random rnd = new Random(); private static double currentMaximum = 0; private static List<MyObject> maximumValues = new List<MyObject>(); public static MyObject CalculateNextObject(ref MyObject input) { // calculate the oldest date to keep in tracking list DateTime targetDate = input.Date.AddYears(-1); // maximum logic if (input.Value >= currentMaximum) { // new maximum found, clear tracking list // this is the best case scenario maximumValues.Clear(); currentMaximum = input.Value; } else { // unfortunately, no new maximum was found // go backwards the maximum tracking list and check for smaller values // clear the list of all smaller values. The list should therefore always // be in descending order for (int b = maximumValues.Count - 1; b >= 0; b--) { if (maximumValues[b].Value <= input.Value) { // a lower value has been found. We have a newer, higher value // clear this waste value from the tracking list maximumValues.RemoveAt(b); } else { // there are no more lower values. // stop looking for smaller values to save time break; } } } // append new value to tracking list, no matter if higher or lower // all future values might be lower maximumValues.Add(input); // check if the oldest value is too old to be kept in the tracking list while (maximumValues[0].Date < targetDate) { // oldest value is to be removed maximumValues.RemoveAt(0); // update maximum currentMaximum = maximumValues[0].Value; } // add object to result list MyObject returnData = new MyObject() { Date = input.Date, Value = input.Value / currentMaximum }; return returnData; }

Método de prueba

 static void Main(string[] args) { MyObject[] testData = GetTestObjects(); Stopwatch stopWatch1 = new Stopwatch(); Stopwatch stopWatch2 = new Stopwatch(); stopWatch1.Start(); MyObject[] testresults1 = BruteForceBackward(testData); stopWatch1.Stop(); Console.WriteLine("BruteForceBackward executed in " + stopWatch1.ElapsedMilliseconds + " ms"); stopWatch2.Start(); MyObject[] testresults2 = FlowThroughForward(ref testData); stopWatch2.Stop(); Console.WriteLine("FlowThroughForward executed in " + stopWatch2.ElapsedMilliseconds + " ms"); Console.WriteLine(); Console.WriteLine("Comparing some random test results: "); var rnd = new Random(); for (int i = 0; i < 10; i++) { int index = rnd.Next(0, testData.Length); Console.WriteLine("Index: " + index + " brute: " + testresults1[index].Value + " flow: " + testresults2[index].Value); } Thread.Sleep(1000000); }

Resultado de la prueba

Las pruebas se realizaron en una máquina con 32 núcleos, por lo que, en teoría, el enfoque de subprocesos múltiples debería ser una ventaja, pero ya verá;)

Función Tiempo de función hora %
Fuerza BrutaHacia Atrás 5334ms 99,9%
Flujo a través de adelante 5ms 0.094%

Factor de mejora del rendimiento: ~tiempo/1000

salida de consola con validación de datos:

 BruteForceBackward executed in 5264 ms FlowThroughForward executed in 5 ms Comparing some random test results: Index: 25291 brute: 0.989688139105413 flow: 0.989688139105413 Index: 11945 brute: 0.59670821976193 flow: 0.59670821976193 Index: 30282 brute: 0.413238225210297 flow: 0.413238225210297 Index: 33898 brute: 0.38258761939139 flow: 0.38258761939139 Index: 8824 brute: 0.833512217105447 flow: 0.833512217105447 Index: 22092 brute: 0.648052464067263 flow: 0.648052464067263 Index: 24633 brute: 0.35859417692481 flow: 0.35859417692481 Index: 24061 brute: 0.540642018793402 flow: 0.540642018793402 Index: 34219 brute: 0.498785766613022 flow: 0.498785766613022 Index: 2396 brute: 0.151471808392111 flow: 0.151471808392111

El uso de la CPU fue mucho mayor en Bruteforce al revés debido a la paralelización. ingrese la descripción de la imagen aquí

El peor de los casos son largos períodos de valores decrecientes. El código aún se puede optimizar enormemente, pero supongo que esto debería ser suficiente. Para una mayor optimización, se podría buscar reducir la mezcla de listas al eliminar/agregar elementos a maximumValues .

over 4 years ago · Santiago Trujillo Denunciar

0

Aquí hay una solución similar a la respuesta de julian bechtold . La diferencia es que el máximo (y todas las variables relacionadas) se mantienen ocultos de la implementación principal, en una clase separada cuyo propósito es únicamente realizar un seguimiento del máximo durante el último año. El algoritmo es el mismo, solo uso algunas expresiones de Linq aquí y allá.

Realizamos un seguimiento del máximo en la siguiente clase:

 public class MaxSlidingWindow { private readonly List<MyObject> _maximumValues; private double _max; public MaxSlidingWindow() { _maximumValues = new List<MyObject>(); _max = double.NegativeInfinity; } public double Max => _max; public void Add(MyObject myObject) { if (myObject.Value >= _max) { _maximumValues.Clear(); _max = myObject.Value; } else { RemoveValuesSmallerThan(myObject.Value); } _maximumValues.Add(myObject); RemoveObservationsBefore(myObject.Date.AddYears(-1)); _max = _maximumValues[0].Value; } private void RemoveObservationsBefore(DateTime targetDate) { var toRemoveFromFront = 0; while (_maximumValues[toRemoveFromFront].Date < targetDate && toRemoveFromFront <= maximumValues3.Count -1) { toRemoveFromFront++; } _maximumValues.RemoveRange(0, toRemoveFromFront); } private void RemoveValuesSmallerThan(double targetValue) { var maxEntry = _maximumValues.Count - 1; var toRemoveFromBack = 0; while (toRemoveFromBack <= maxEntry && _maximumValues[maxEntry - toRemoveFromBack].Value <= targetValue) { toRemoveFromBack++; } _maximumValues.RemoveRange(maxEntry - toRemoveFromBack + 1, toRemoveFromBack); } }

Se puede utilizar de la siguiente manera:

 public static MyObject[] GetTestObjects_MaxSlidingWindow() { var rnd = new Random(); var date = new DateTime(2021, 1, 1, 0, 0, 0); var result = new List<MyObject>(); var maxSlidingWindow = new MaxSlidingWindow(); for (int i = 0; i < 50000; i++) { //this is to simulate real data having gaps if (rnd.Next(100) < 25) { continue; } var myObject = new MyObject() { Value = rnd.NextDouble(), Date = date.AddMinutes(15 * i) }; maxSlidingWindow.Add(myObject); var max = maxSlidingWindow.Max; result.Add(new MyObject { Date = myObject.Date, Value = myObject.Value / max }); } return result.ToArray(); }

Vea los tiempos relativos a continuación: la solución anterior es un poco más rápida (cronometrada en más de 10 millones de ejecuciones), pero apenas se nota:

Tiempos relativos

over 4 years ago · Santiago Trujillo Denunciar

0

Un problema interesante y desafiante. Reuní una solución utilizando un enfoque de programación dinámica (aprendido por primera vez en la clase de algoritmos CS en el '78). Primero, se construye un árbol que contiene valores máximos locales precalculados sobre rangos definidos recursivamente. Una vez construido, el valor máximo para un rango arbitrario se puede calcular de manera eficiente principalmente utilizando los valores precalculados. Solo en los límites del rango, el cálculo desciende hasta el nivel del elemento.

No es tan rápido como el método FlowThroughForward de Julian Bechtold, pero el acceso aleatorio a rangos puede ser una ventaja.

Código para agregar a Main:

 Console.WriteLine(); Stopwatch stopWatch3 = new Stopwatch(); stopWatch3.Start(); MyObject[] testresults3 = RangeTreeCalculation(ref testData, 10); stopWatch3.Stop(); Console.WriteLine($"RangeTreeCalculation executed in {stopWatch3.ElapsedMilliseconds} ms"); ... test comparison Console.WriteLine($"Index: {index} brute: {testresults1[index].Value} flow: {testresults2[index].Value} rangeTree: {testresults3[index].Value}");

Función de prueba:

 public static MyObject[] RangeTreeCalculation(ref MyObject[] testDataArray, int partitionThreshold) { // For this implementation, we need to convert the Array to an ArrayList, because we need a // reference type object that can be shared. List<MyObject> testDataList = testDataArray.ToList(); // Construct a tree containing recursive collections of pre-calculated values var rangeTree = new RangeTree(testDataList, partitionThreshold); MyObject[] result = new MyObject[testDataList.Count]; Parallel.ForEach(testDataList, (item, state, i) => { var max = rangeTree.MaxForDateRange(item.Date.AddYears(-1), item.Date); result[i] = new MyObject() { Date = item.Date, Value = item.Value / max }; }); return result; }

Clase de apoyo:

 // Class used to divide and conquer using dynamic programming. public class RangeTree { public List<MyObject> Data; // This reference is shared by all members of the tree public int Start { get; } // Index of first element covered by this node. public int Count { get; } // Number of elements covered by this node. public DateTime FirstDateTime { get; } public DateTime LastDateTime { get; } public double MaxValue { get; } // Pre-calculated max for all elements covered by this node. List<RangeTree> ChildRanges { get; } // Top level node constructor public RangeTree(List<MyObject> data, int partitionThreshold) : this(data, 0, data.Count, partitionThreshold) { } // Child node constructor, which covers an recursively decreasing range of element. public RangeTree(List<MyObject> data, int start, int count, int partitionThreshold) { Data = data; Start = start; Count = count; FirstDateTime = Data[Start].Date; LastDateTime = Data[Start + Count - 1].Date; if (count <= partitionThreshold) { // If the range is smaller than the threshold, just calculate the local max // directly from the items. No child ranges are defined. MaxValue = Enumerable.Range(Start, Count).Select(i => Data[i].Value).Max(); } else { // We still have a significant range. Decide how to further divide them up into sub-ranges. // (There may be room for improvement here to better balance the tree.) int partitionSize = (count - 1) / partitionThreshold + 1; int partitionCount = (count - 1) / partitionSize + 1; if (count < partitionThreshold * partitionThreshold) { // When one away from leaf nodes, prefer fewer full leaf nodes over more // less populated leaf nodes. partitionCount = (count - 1) / partitionThreshold + 1; partitionSize = (count - 1) / partitionCount + 1; } ChildRanges = Enumerable.Range(0, partitionCount) .Select(partitionNum => new { ChildStart = Start + partitionNum * partitionSize, ChildCount = Math.Min(partitionSize, Count - partitionNum * partitionSize) }) .Where(part => part.ChildCount > 0) // Defensive .Select(part => new RangeTree(Data, part.ChildStart, part.ChildCount, partitionThreshold)) .ToList(); // Now is the dynamic programming part: // Calculate the local max as the max of all child max values. MaxValue = ChildRanges.Max(chile => chile.MaxValue); } } // Get the max value for a given range of dates withing this rangeTree node. // This used the precalculated values as much as possible. // Only at the fringes of the date range to we calculate at the element level. public double MaxForDateRange(DateTime fromDate, DateTime thruDate) { double calculatedMax = Double.MinValue; if (fromDate > this.LastDateTime || thruDate < this.FirstDateTime) { // Entire range is excluded. Nothing of interest here folks. calculatedMax = Double.MinValue; } else if (fromDate <= this.FirstDateTime && thruDate >= this.LastDateTime) { // Entire range is included. Use the already-calculated max. calculatedMax = this.MaxValue; } else if (ChildRanges != null) { // We have child ranges. Recurse and accumulate. // Possible optimization: Calculate max for middle ranges first, and only bother // with extreme partial ranges if their local max values exceed the preliminary result. for (int i = 0; i < ChildRanges.Count; ++i) { double childMax = ChildRanges[i].MaxForDateRange(fromDate, thruDate); if (childMax > calculatedMax) { calculatedMax = childMax; } } } else { // Leaf range. Loop through just this limited range of notes, checking individually for // date in range and accumulating the result. for (int i = 0; i < this.Count; ++i) { var element = Data[this.Start + i]; if (fromDate <= element.Date && element.Date <= thruDate && element.Value > calculatedMax) { calculatedMax = element.Value; } } } return calculatedMax; } }

Hay mucho margen de mejora, como la parametrización de los tipos y la generalización de la funcionalidad para admitir más que solo Max(Value), pero el marco está ahí.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda