/* * Copyright 2007 Flaptor (flaptor.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.flaptor.hist4j; import java.util.ArrayList; /** * The HistogramDataNode stores the histogram data for a range of values. * It knows the minimum and maximum values for which it counts the number of instances. * When the count exceeds the allowed limit it splits itself in two, increasing the * histogram resolution for this range. * * @author Jorge Handl */ public class HistogramDataNode extends HistogramNode { private static final long serialVersionUID = -1L; // Attributes of a data node. private Cell cell = new Cell(); /** * Creates an empty data node. */ public HistogramDataNode() { reset(); } /** * Creates a data node for the given range with the given instance count. * * @param count * the number of data instances in the given range. * @param minValue * the start of the range of counted values. * @param maxValue * the end of the range of counted values. */ public HistogramDataNode(long count, float minValue, float maxValue) { reset(); cell.count = count; cell.minValue = minValue; cell.maxValue = maxValue; } /** * Clears the data node. */ public void reset() { cell.count = 0; cell.minValue = Float.MAX_VALUE; cell.maxValue = -Float.MAX_VALUE; } /** * Adds a value to the data node. * <p> * If the value falls inside of the nodes' range and the count does not exceed the imposed limit, it simply increments the count.<br> * If the value falls outside of the nodes' range, it expands the range.<br> * If the count exceeds the limit, it splits in two assuming uniform distribution inside the node.<br> * If the value falls outside of the nodes' range AND the count exceeds the limit, it creates a new node for that value. * * @param root * a reference to the adaptive histogram instance that uses this structure. * @param value * the value for which the count is to be incremented. * @return A reference to itself if no structural change happened, or a reference to the new fork node if this node was split. */ public HistogramNode addValue(AdaptiveHistogram root, float value) { // "self" is what is returned to the caller. If this node needs to be replaced by a fork node, // this variable will hold the new fork node and it will be returned to the caller. // Otherwise, the node returned will be this, in which case nothing changes. HistogramNode self = this; if (value >= cell.minValue && value <= cell.maxValue) { // the value falls within this nodes' range if (cell.count < root.getCountPerNodeLimit() // there is enough room in this node for the new value || cell.minValue == cell.maxValue) { // or the node defines a zero-width range so it can't be split cell.count++; } else { // not enough room, distribute the value count among the new nodes, assuming uniform distribution float splitValue = (cell.minValue + cell.maxValue) / 2; long rightCount = cell.count / 2; long leftCount = rightCount; boolean countWasOdd = (leftCount + rightCount < cell.count); // assign the new value to the corresponding side. If the count is odd, add the extra item to the other side to keep balance if (value > splitValue) { rightCount++; leftCount += (countWasOdd ? 1 : 0); } else { leftCount++; rightCount += (countWasOdd ? 1 : 0); } // create a new subtree that will replace this node HistogramNode leftNode = new HistogramDataNode(leftCount, cell.minValue, splitValue); HistogramNode rightNode = new HistogramDataNode(rightCount, splitValue, cell.maxValue); self = new HistogramForkNode(splitValue, leftNode, rightNode); } } else { // the value falls outside of this nodes' range if (cell.count < root.getCountPerNodeLimit()) { // there is enough room in this node for the new value cell.count++; // extend the range of this node, assuming that the tree structure above correctly directed // the given value to this node and therefore it lies at one of the borders of the tree. if (value < cell.minValue) cell.minValue = value; if (value > cell.maxValue) cell.maxValue = value; } else { // not enough room, create a new sibling node for the new value and put both under a new fork node if (value < cell.minValue) { cell.minValue = Math.min(cell.minValue, (value + cell.maxValue) / 2); self = new HistogramForkNode(cell.minValue, new HistogramDataNode(1, value, cell.minValue), this); } else { cell.maxValue = Math.max(cell.maxValue, (cell.minValue + value) / 2); self = new HistogramForkNode(cell.maxValue, this, new HistogramDataNode(1, cell.maxValue, value)); } } } return self; } /** * Returns the number of data points stored in the same bucket as a given value. * * @param value * the reference data point. * @return the number of data points stored in the same bucket as the reference point. */ public long getCount(float value) { long res = 0; if (value >= cell.minValue && value <= cell.maxValue) { res = cell.count; } return res; } /** * Returns the cumulative density function for a given data point. * * @param value * the reference data point. * @return the cumulative density function for the reference point. */ public long getAccumCount(float value) { long res = 0; if (value >= cell.minValue) { res = cell.count; } return res; } // Linear interpolation for float values. private float interpolate(float x0, float y0, float x1, float y1, float x) { return y0 + ((x - x0) * (y1 - y0)) / (x1 - x0); } /** * Returns the data point where the running cumulative count reaches the target cumulative count. * It uses linear interpolation over the range of the node to get a better estimate of the true value. * * @param accumCount * an array containing:<br> * - accumCount[0] the running cumulative count. <br> * - accumCount[1] the target cumulative count. * @return the data point where the running cumulative count reaches the target cumulative count. */ public Float getValueForAccumCount(long[] accumCount) { Float res = null; long runningAccumCount = accumCount[0]; long targetAccumCount = accumCount[1]; if (runningAccumCount <= targetAccumCount && runningAccumCount + cell.count >= targetAccumCount) { float val = interpolate((float) runningAccumCount, cell.minValue, (float) (runningAccumCount + cell.count), cell.maxValue, (float) targetAccumCount); res = new Float(val); } accumCount[0] += cell.count; return res; } /** * Applies a convertion function to the values stored in the histogram. * * @param valueConversion * a class that defines a function to convert the value. */ public void apply(AdaptiveHistogram.ValueConversion valueConversion) { cell.minValue = valueConversion.convertValue(cell.minValue); cell.maxValue = valueConversion.convertValue(cell.maxValue); } /** * Prints this nodes' data with a margin depending on the level of the node in the tree. * * @param level * the level of this node in the tree. */ public void show(int level) { margin(level); System.out.println("Data: " + cell.count + " (" + cell.minValue + "," + cell.maxValue + ")"); } /** * Build the table representing the histogram data adding this node's cell to it. */ public void toTable(ArrayList<Cell> table) { table.add(cell); } }