An example of how seasonality impacts the trend.
Seasonality means that the time serie has a periodic component, repeating the same pattern on each period. For example, sales of a store may have a week-based seasonality: sales increase on saturday, while there is no sale at all on sunday.
In this example, the season has a length of 4: the first value is high, and the last one is low. Hence, each season has an internal negative trend, which lowers the global trend. A positive internal trend would increase the global trend.
The impact of the season's internal trend on the global trend is higher when :
In this example, we mitigate seasonality with a windowed approach: for each season, we retain the mean of the season (big dots). This produces a deseasonalized time serie. The deseasonalized global trend (light blue line) is then computed based on this deseasonalized time serie.
Seasonality can be mitigated by various approaches, such as simple linear regression, windowed mean (used in this example), moving mean, smoothing, ...
xxxxxxxxxx
<meta charset="utf-8">
<style>
#controls{
position: absolute;
right: 0px;
}
.grid>line, .grid>.intersect {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
.legend {
font-size: 12px;
}
.dot {
fill: lightsteelblue;
stroke: white;
stroke-width: 3px;
}
.dot.deseasonalized {
fill: lightsteelblue;
stroke: white;
stroke-width: 3px;
opacity: 0.2;
}
.dot.draggable:hover, .dot.dragging {
fill: pink;
cursor: ns-resize;
}
.timeline {
fill: none;
stroke: steelblue;
stroke-width: 2px;
opacity: 0.2;
}
.timeline.draggable:hover, .timeline.dragging {
stroke: pink;
opacity: 1;
cursor: ns-resize;
}
.trend {
stroke: steelblue;
}
.trend.deseasonalized {
stroke-opacity: 0.5;
}
</style>
<body>
<div id="controls">
<button onclick="invertTrend();">invert trend</button>
<button onclick="invertSeasonality();">invert seasonality</button>
<button onclick="permuteSeasonality();">permute seasonality</button>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var timeSerie = [];
var deseasonalizedTimeSerie = [];
var seasonLength = 4; //for a sake of simplicity, this parameter is static, but technics allow to find seasonality from raw time serie (eg. [fast] Fourier transformation)
var seasonCount = 5; //timeSerie.length/seasonLength
var WITH_TRANSITION = true;
var WITHOUT_TRANSITION = false
var duration = 500;
var legendHeight = 20;
var xAxisLabelHeight = 20;
var yAxisLabelWidth = 20;
var margin = {top: 20, right: 20, bottom: 20, left: 20},
width = 960 - margin.left - margin.right - yAxisLabelWidth,
height = 500 - margin.top - margin.bottom - xAxisLabelHeight - legendHeight;
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragStarted)
.on("drag", dragged)
.on("dragend", dragEnded);
var x = d3.scale.linear()
.domain([0, 20])
.range([0, width])
var y = d3.scale.linear()
.domain([0, 50])
.range([0, -height])
var xAxisDef = d3.svg.axis()
.scale(x)
.ticks(21);
var yAxisDef = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right + yAxisLabelWidth)
.attr("height", height + margin.top + margin.bottom + xAxisLabelHeight + legendHeight)
.append("g")
.attr("transform", "translate(" + (margin.left) + "," + (height+margin.top) + ")")
var container = svg.append("g");
var grid = container.append("g")
.attr("class", "grid");
var intersects = [];
d3.range(1, x.invert(width)).forEach(function(a) { d3.range(5, y.invert(-height),5).forEach(function(b) { intersects.push([a,b])})});
grid.selectAll(".intersect")
.data(intersects)
.enter().append("path")
.classed("intersect", true)
.attr("d", function(d) { return "M"+[x(d[0])-1,y(d[1])]+"h3M"+[x(d[0]),y(d[1])-1]+"v3"});
grid.selectAll(".x-line")
.data(d3.range(0.5, x.invert(width), 4))
.enter().append("line")
.classed("x-line", true)
.attr("x1", function(d) { return x(d); })
.attr("y1", y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", y(49));
container.selectAll(".season-id")
.data(d3.range(0, 4))
.enter().append("text")
.attr("x", function(d) { return x(2.5+d*seasonLength); })
.attr("y", -6)
.style("text-anchor", "middle")
.text(function(d) { return (d!=3)? "season "+ (d+1) : "..."; });
container.append("g")
.attr("class", "axis x")
.call(xAxisDef);
container.append("text")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Time");
container.append("g")
.attr("class", "axis y")
.call(yAxisDef);
container.append("text")
.attr("x", 6)
.attr("y", -height+10)
.style("text-anchor", "start")
.text("Amount");
var legend = container.append("g")
.classed("legend", true)
.attr("transform", "translate(" + 100 + "," + (xAxisLabelHeight+legendHeight) + ")");
var currentLegend = legend.append("g")
.attr("transform", "translate(" + 0 + ",0)");
currentLegend.append("circle")
.classed("dot", true)
.attr("r", 4)
.attr("cx", -5)
.attr("cy", -5);
currentLegend.append("text")
.attr("dx", 5)
.text(": raw value");
currentLegend = legend.append("g")
.attr("transform", "translate(" + 130 + ",0)");
currentLegend.append("circle")
.classed("dot deseasonalized", true)
.attr("r", 8)
.attr("cx", -5)
.attr("cy", -5);
currentLegend.append("text")
.attr("dx", 5)
.text(": deseasonalized value (season's mean)");
currentLegend = legend.append("g")
.attr("transform", "translate(" + 400 + ",0)");
currentLegend.append("line")
.classed("trend", true)
.attr("x1", -20)
.attr("y1", -5)
.attr("x2", -5)
.attr("y2", -5);
currentLegend.append("text")
.attr("dx", 5)
.text(": trend of raw time serie");
currentLegend = legend.append("g")
.attr("transform", "translate(" + 600 + ",0)");
currentLegend.append("line")
.classed("trend deseasonalized", true)
.attr("x1", -20)
.attr("y1", -5)
.attr("x2", -5)
.attr("y2", -5);
currentLegend.append("text")
.attr("dx", 5)
.text(": trend of deseasonalized time serie");
var timeline = container.append("path")
.classed("timeline", true)
.attr("d", line);
var dotContainer = container.append("g")
.classed("dots", true);
var deseasonalizedDotContainer = container.append("g")
.classed("dots deseasonalized", true);
var trendLine = container.append("line")
.attr("class", "trend")
.attr("x1", x(0))
.attr("y1", y(0))
.attr("x2", x(20))
.attr("y2", y(0));
var deseasonalizedTrendLine = container.append("line")
.attr("class", "trend deseasonalized")
.attr("x1", x(0))
.attr("y1", y(0))
.attr("x2", x(20))
.attr("y2", y(0));
d3.csv("timeserie.csv", dottype, function(error, dots) {
updateTimeline(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTrend(WITHOUT_TRANSITION);
updateDeseasonalizedTimeSerie();
updateDeseasonalizedDots(WITHOUT_TRANSITION);
updateDeseasonalizedTrend(WITHOUT_TRANSITION);
});
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
timeSerie.push(d);
return d;
}
var line = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
function updateDots(withTransition) {
dots = dotContainer.selectAll(".dot")
.data(timeSerie);
dots.enter()
.append("circle")
.classed("dot draggable", true)
.attr("r", 5)
.call(drag);
dots.transition()
.duration(withTransition? duration : 0)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
}
function updateTimeline(withTransition) {
timeline.data(timeSerie).transition()
.duration(withTransition? duration : 0)
.attr("d", line(timeSerie));
}
function updateDeseasonalizedDots(withTransition){
var deseasonalizeDots = deseasonalizedDotContainer.selectAll(".dot.deseasonalized")
.data(deseasonalizedTimeSerie);
deseasonalizeDots.enter()
.append("circle")
.classed("dot deseasonalized", true)
.attr("r", 8)
.attr("cx", function(d) { return x(d.x); });
deseasonalizeDots.transition()
.duration(withTransition? duration : 0)
.attr("cy", function(d) { return y(d.y); });
}
function invertTrend() {
var serieLength = timeSerie.length;
var countSum = 0;
var mean = 0;
timeSerie.forEach(function (d) {
countSum += d.y
});
mean = countSum/serieLength;
timeSerie.forEach(function (d) {
d.y = (mean-d.y)+mean;
});
updateDeseasonalizedTimeSerie();
updateTimeline(WITH_TRANSITION);
updateDots(WITH_TRANSITION);
updateDeseasonalizedDots(WITH_TRANSITION);
updateTrend(WITH_TRANSITION);
updateDeseasonalizedTrend(WITH_TRANSITION);
}
function invertSeasonality() {
//objective: for each season, make the inverse with regards to the season's mean
var i = 0, j = 0;
var seasonMean = 0;
while (i<seasonCount) {
seasonMean = deseasonalizedTimeSerie[i].y;
j = 0;
while (j<seasonLength) {
timeSerie[i*seasonLength+j].y = (seasonMean - timeSerie[i*seasonLength+j].y) + seasonMean;
j++;
}
i++;
}
updateTimeline(WITH_TRANSITION);
updateDots(WITH_TRANSITION);
updateDeseasonalizedDots(WITH_TRANSITION);
updateTrend(WITH_TRANSITION);
updateDeseasonalizedTrend(WITH_TRANSITION);
}
function permuteSeasonality() {
//objective: in each season, n-th value becomes the (n-1)-th value, and the first one becomes the last one
d3.transition()
.duration(duration/2)
.each("start", function() {
line.defined(function(d, i) { return (i%4)!=0; });
updateTimeline(WITHOUT_TRANSITION);
})
.transition()
.duration(duration)
.each("start", function() {
var i = 0;
while (i<timeSerie.length) {
if (timeSerie[i].x%seasonLength === 1) {
timeSerie[i].x += seasonLength-1;
} else {
timeSerie[i].x -= 1
}
i++;
}
updateTimeline(WITH_TRANSITION);
updateDots(WITH_TRANSITION);
updateDeseasonalizedDots(WITH_TRANSITION);
updateTrend(WITH_TRANSITION);
updateDeseasonalizedTrend(WITH_TRANSITION);
})
.transition()
.duration(duration/2)
.each("end", function() {
timeSerie.sort(function(d0, d1){ return d0.x-d1.x; });
line.defined(function(d) { return true; });
updateTimeline(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
});
}
function updateDeseasonalizedTimeSerie() {
//for each season, the mean is computed
deseasonalizedTimeSerie = [];
var seasonLength = 4; //for a sake of simplicity, this parameter is static, but technics allow to find seasonality from raw time serie (eg. [fast] Fourier transformation)
var seasonCount = timeSerie.length/seasonLength;
var i = 0, j = 0;
var seasonCountSum = 0;
while (i<seasonCount) {
seasonCountSum = 0;
j = 0;
while (j<seasonLength) {
seasonCountSum += timeSerie[i*seasonLength+j].y;
j++;
}
deseasonalizedTimeSerie.push({x: (i+0.5)*seasonLength+0.5, y:seasonCountSum/seasonLength});
i++;
}
}
function updateTrend(withTransition) {
// The objective is to draw a line that is the closest line from each point
// (cf. https://en.wikipedia.org/wiki/Linear_regression)
// A simple regression line is of the form y=ax+b, where a is the trend of the time serie
// below code computes 'a' and 'b'
var serieLength = timeSerie.length;
var timeInterval = 1
var countSum = 0;
var orderCountSum = 0;
timeSerie.forEach(function(d){
countSum += d.y;
orderCountSum += (d.x)*(d.y);
});
var a = (12*orderCountSum - 6*(serieLength+1)*countSum)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1));
var b = (2*(2*serieLength+1)*countSum - 6*orderCountSum)/(serieLength*(serieLength-1));
trendLine
.transition()
.duration(withTransition? duration : 0)
.attr("y1", y(b))
.attr("y2", y(a*serieLength+b));
}
function updateDeseasonalizedTrend(withTransition) {
var serieLength = timeSerie.length;
var timeInterval = 1
var countSum = 0;
var orderCountSum = 0;
deseasonalizedTimeSerie.forEach(function(d){
countSum += d.y;
orderCountSum += (d.x)*(d.y);
});
var a = (12*orderCountSum - 6*(serieLength+1)*countSum)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1))*seasonLength;
var b = (2*(2*serieLength+1)*countSum - 6*orderCountSum)/(serieLength*(serieLength-1))*seasonLength;
deseasonalizedTrendLine
.transition()
.duration(withTransition? duration : 0)
.attr("y1", y(b))
.attr("y2", y(a*serieLength+b));
}
function dragStarted(d) {
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d.y += y.invert(d3.event.dy)
updateTimeline(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTrend(WITHOUT_TRANSITION);
updateDeseasonalizedTimeSerie();
updateDeseasonalizedDots(WITHOUT_TRANSITION);
updateDeseasonalizedTrend(WITHOUT_TRANSITION);
}
function dragEnded(d) {
d3.select(this).classed("dragging", false);
}
</script>
https://d3js.org/d3.v3.min.js