Pregunta

Hemos configurado iReport para generar el gráfico siguiente:

Los datos reales son puntos en azul, la línea de tendencia es de color verde. Los problemas incluyen:

  • Demasiados puntos de datos para la línea de tendencia
  • La línea de tendencia no sigue una curva de Bezier (spline)

El origen del problema es con la clase incrementador. El incrementador está provisto de los puntos de datos iterativamente. No parece ser una manera de obtener el conjunto de datos. El código que calcula la línea de tendencia se ve de la siguiente manera:

import java.math.BigDecimal;
import net.sf.jasperreports.engine.fill.*;

/**
 * Used by an iReport variable to increment its average.
 */
public class MovingAverageIncrementer
  implements JRIncrementer {
  private BigDecimal average;

  private int incr = 0;

  /**
   * Instantiated by the MovingAverageIncrementerFactory class.
   */
  public MovingAverageIncrementer() {
  }

  /**
   * Returns the newly incremented value, which is calculated by averaging
   * the previous value from the previous call to this method.
   * 
   * @param jrFillVariable Unused.
   * @param object New data point to average.
   * @param abstractValueProvider Unused.
   * @return The newly incremented value.
   */
  public Object increment( JRFillVariable jrFillVariable, Object object, 
                           AbstractValueProvider abstractValueProvider ) {
    BigDecimal value = new BigDecimal( ( ( Number )object ).doubleValue() );

    // Average every 10 data points
    //
    if( incr % 10 == 0 ) {
      setAverage( ( value.add( getAverage() ).doubleValue() / 2.0 ) );
    }

    incr++;

    return getAverage();
  }


  /**
   * Changes the value that is the moving average.
   * @param average The new moving average value.
   */
  private void setAverage( BigDecimal average ) {
    this.average = average;
  }

  /**
   * Returns the current moving average average.
   * @return Value used for plotting on a report.
   */
  protected BigDecimal getAverage() {
    if( this.average == null ) {
      this.average = new BigDecimal( 0 );
    }

    return this.average;
  }

  /** Helper method. */    
  private void setAverage( double d ) {
    setAverage( new BigDecimal( d ) );
  }
}

¿Cómo crear una representación más suave y más precisa de la línea de tendencia?

¿Fue útil?

Solución

Esto depende del comportamiento del elemento que se está midiendo. Esto es algo que se mueve (o cambios) de una manera que puede ser modelado?

Si no se espera que el elemento que desea cambiar, entonces su tendencia debería ser el valor medio subyacente de todo el conjunto de la muestra, no sólo las dos últimas mediciones. Usted puede conseguir esto usando el teorema de Bayes. El promedio de funcionamiento puede ser calculado de forma incremental utilizando la fórmula simple

NCM1 = (MTN * N + x) / (N + 1)

donde x es la medición en el tiempo t + 1, NCM1 es la media de un tiempo t + 1, MTN es la media en el tiempo t, y N es el número de mediciones tomadas por el tiempo t.

Si el elemento que se está midiendo fluctúa de manera que se puede predecir mediante alguna ecuación subyacente, entonces usted puede utilizar un filtro de Kalman para proporcionar una mejor estimación de la siguiente punto en base a las mediciones anteriores (recientes) y la ecuación que modela el comportamiento predicho.

Como punto de partida, la entrada de Wikipedia sobre estimadores bayesianos y filtros de Kalman será útil .

Otros consejos

imagen resultante

El resultado es aún incompleta, sin embargo, muestra claramente una mejor línea de tendencia que la de la pregunta.

Cálculo

Había dos componentes clave que faltan:

  • ventana deslizante. A List de valores Double que no puede crecer más allá de un tamaño dado.
  • Cálculo. Una variación en la respuesta aceptar (uno menos llamada a getIterations()):

    ((value - previousAverage) / (getIterations() + 1)) + previousAverage

Código fuente

import java.math.BigDecimal;

import java.util.ArrayList;
import java.util.List;

import net.sf.jasperreports.engine.fill.AbstractValueProvider;
import net.sf.jasperreports.engine.fill.JRFillVariable;
import net.sf.jasperreports.engine.fill.JRIncrementer;


/**
 * Used by an iReport variable to increment its average.
 */
public class RunningAverageIncrementer
  implements JRIncrementer {
  /** Default number of tallies. */
  private static final int DEFAULT_TALLIES = 128;

  /** Number of tallies within the sliding window. */
  private static final int DEFAULT_SLIDING_WINDOW_SIZE = 30;

  /** Stores a sliding window of values. */
  private List<Double> values = new ArrayList<Double>( DEFAULT_TALLIES );

  /**
   * Instantiated by the RunningAverageIncrementerFactory class.
   */
  public RunningAverageIncrementer() {
  }

  /**
   * Calculates the average of previously known values.
   * @return The average of the list of values returned by getValues().
   */
  private double calculateAverage() {
    double result = 0.0;
    List<Double> values = getValues();

    for( Double d: getValues() ) {
      result += d.doubleValue();
    }

    return result / values.size();
  }

  /**
   * Called each time a new value to be averaged is received.
   * @param value The new value to include for the average.
   */
  private void recordValue( Double value ) {
    List<Double> values = getValues();

    // Throw out old values that should no longer influence the trend.
    //
    if( values.size() > getSlidingWindowSize() ) {
      values.remove( 0 );
    }

    this.values.add( value );
  }

  private List<Double> getValues() {
    return values;
  }

  private int getIterations() {
    return getValues().size();
  }

  /**
   * Returns the newly incremented value, which is calculated by averaging
   * the previous value from the previous call to this method.
   * 
   * @param jrFillVariable Unused.
   * @param tally New data point to average.
   * @param abstractValueProvider Unused.
   * @return The newly incremented value.
   */
  public Object increment( JRFillVariable jrFillVariable, Object tally, 
                           AbstractValueProvider abstractValueProvider ) {
    double value = ((Number)tally).doubleValue();

    recordValue( value );

    double previousAverage = calculateAverage();
    double newAverage = 
      ((value - previousAverage) / (getIterations() + 1)) + previousAverage;

    return new BigDecimal( newAverage );
  }

  protected int getSlidingWindowSize() {
    return DEFAULT_SLIDING_WINDOW_SIZE;
  }
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top