Prepping for seasonality talk
Last active
March 9, 2016 20:36
-
-
Save zanarmstrong/91f52e5c48e84ddb9e4e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// everything after here can take daily data and plot it, so would work for "real" data too | |
function aggregateData() { | |
var dateFormat = d3.time.format("%Y-%m-%d") | |
var granularity = ''; | |
var data = []; | |
// incoming data object | |
var dailyData = {}; | |
var days = []; | |
var getQuarterFloor = function(date) { | |
var quarterConvert = [0, 0, 0, 3, 3, 3, 6, 6, 6, 9, 9, 9] | |
return new Date(date.getFullYear(), quarterConvert[date.getMonth()], 1) | |
} | |
var rollupFunction = { | |
'daily': function(d) { | |
return d | |
}, | |
'weekly': function(d) { | |
return d3.time.week(d); | |
}, | |
'monthly': function(d) { | |
return d3.time.month(d); | |
}, | |
'quarterly': function(d) { | |
return getQuarterFloor(d) | |
} | |
} | |
// rollup functions | |
// TODO: generalize this better | |
var rollupSums = function(leaves) { | |
if ((granularity == 'weekly' && leaves.length < 7) || (granularity == 'quarterly' && leaves.length < 90)) { | |
// if a partial week, return empty object (it will complain) | |
// same for partial quarter - this isn't a perfect check, as could still be missing a day or two | |
return {} | |
} | |
return { | |
trendDow: d3.sum(leaves, function(a) { | |
return dailyData[dateFormat(a)].trendDow | |
}), | |
trendDowSeasonal: d3.sum(leaves, function(a) { | |
return dailyData[dateFormat(a)].trendDowSeasonal | |
}), | |
trendDowSeasonalHolidays: d3.sum(leaves, function(a) { | |
return dailyData[dateFormat(a)].trendDowSeasonalHolidays | |
}), | |
trendDowSeasonalHolidaysAdjust: d3.sum(leaves, function(a) { | |
return dailyData[dateFormat(a)].trendDowSeasonalHolidaysAdjust | |
}), | |
} | |
} | |
var update = function() { | |
data = d3.nest().key(rollupFunction[granularity]) | |
.rollup(rollupSums) | |
.entries(days); | |
} | |
var getDateExtent = function() { | |
return [d3.min(dailyData, function(period) { | |
return d3.min(period.key) | |
}), d3.max([0, d3.min(dailyData, function(period) { | |
return d3.max(period.key) | |
})])] | |
} | |
function aggregate(newDailyData, newDays) { | |
dailyData = newDailyData; | |
days = newDays; | |
update() | |
return this | |
} | |
aggregate.getValueExtent = function() { | |
return [d3.min([0, d3.min(data, function(period) { | |
return d3.min([period.values.trendDow, period.values.trendDowSeasonal, period.values.trendDowSeasonalHolidays]) | |
})]), d3.max(data, function(period) { | |
return d3.max([period.values.trendDow, period.values.trendDowSeasonal, period.values.trendDowSeasonalHolidays]) | |
})] | |
} | |
aggregate.granularity = function(newGranularity) { | |
if (!arguments.length) return granularity; | |
granularity = newGranularity; | |
update() | |
return this; | |
} | |
aggregate.getData = function() { | |
update() | |
return data | |
} | |
return aggregate | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// fakeData helps us create a fake dataset | |
function fakeData() { | |
var dateFormat = d3.time.format("%Y-%m-%d") | |
var today = new Date(); | |
var firstWeekValue = 1, | |
weeklyGrowthRate = 1, | |
// first position is Sunday, data given as % of week value | |
dayOfWeekFactors = Array(7).fill(1 / 7), | |
//annual seasonality | |
weekOfYearFactors = Array(53).fill(1), | |
dateRange = [new Date() - 24 * 60 * 60 * 1000 * 728, new Date()], | |
holidays = [], | |
adjustment = []; | |
var dailyData = {} | |
function getPeriodArray(days) { | |
return d3.time.day.utc.range(dateRange[0], dateRange[1], days); | |
} | |
function updateData() { | |
// take in information & update it | |
var trend = []; | |
getPeriodArray(7).forEach(function(d, i) { | |
trend[i] = { | |
date: d, | |
value: firstWeekValue * Math.pow((weeklyGrowthRate), i) | |
} | |
}) | |
// set daily data, based on trend + weekly data | |
var dailyDataArray = [] | |
getPeriodArray(1).forEach(function(d, i) { | |
var daily = trend[Math.floor(i / 7)].value * dayOfWeekFactors[d.getDay()] | |
dailyData[dateFormat(d)] = { | |
trendDow: daily, | |
trendDowSeasonal: daily * weekOfYearFactors[d3.time.weekOfYear(d)], | |
trendDowSeasonalHolidays: daily * weekOfYearFactors[d3.time.weekOfYear(d)], | |
trendDowSeasonalHolidaysAdjust: daily * weekOfYearFactors[d3.time.weekOfYear(d)], | |
} | |
}) | |
// use holiday list to adjust | |
holidays.forEach(function(holiday) { | |
holiday.dates.forEach(function(date) { | |
if (dailyData[date]) { | |
dailyData[date].trendDowSeasonalHolidays = dailyData[date].trendDowSeasonalHolidays * holiday.impact | |
dailyData[date].trendDowSeasonalHolidaysAdjust = dailyData[date].trendDowSeasonalHolidaysAdjust * holiday.impact | |
} | |
}) | |
}) | |
adjustment.forEach(function(impact) { | |
if (impact.dateRange[1] == '') { | |
impact.dateRange[1] = dateRange[1]; | |
} | |
if (impact.dateRange[0] == '') { | |
impact.dateRange[0] = dateRange[0] | |
} | |
var impactDays = d3.time.day.utc.range(impact.dateRange[0], impact.dateRange[1], 1); | |
impactDays.forEach(function(date) { | |
var adjustDate = dateFormat(date) | |
if (dailyData[adjustDate]) { | |
dailyData[adjustDate].trendDowSeasonalHolidaysAdjust = dailyData[adjustDate].trendDowSeasonalHolidaysAdjust * impact.factor; | |
} | |
}) | |
}) | |
} | |
function setUpData() { | |
updateData(); | |
return dailyData; | |
} | |
setUpData.firstWeek = function(newFirstWeekValue) { | |
if (!arguments.length) return firstWeekValue; | |
firstWeekValue = newFirstWeekValue; | |
// update data | |
return this; | |
} | |
// array of | |
setUpData.dayOfWeekFactors = function(newDayOfWeekFactors) { | |
if (!arguments.length) return dayOfWeekFactors; | |
dayOfWeekFactors = newDayOfWeekFactors; | |
return this; | |
} | |
// set holidays | |
setUpData.holidays = function(newHolidays) { | |
if (!arguments.length) return holidays; | |
holidays = newHolidays; | |
return this; | |
} | |
// start/end dates of date range, expects an array of length two with dates | |
setUpData.dateRange = function(newDateRange) { | |
if (!arguments.length) return dateRange; | |
dateRange = newDateRange; | |
return this; | |
} | |
// start/end dates of date range, expects an array of length two with dates | |
setUpData.weeklyGrowth = function(newWeeklyGrowth) { | |
if (!arguments.length) return weeklyGrowthRate; | |
weeklyGrowthRate = newWeeklyGrowth; | |
return this; | |
} | |
// manual adjustment | |
setUpData.adjustment = function(newAdjustment) { | |
if (!arguments.length) return adjustment; | |
adjustment = newAdjustment; | |
return this; | |
} | |
// start/end dates of date range, expects an array of length two with dates | |
setUpData.weekOfYear = function(newWeekOfYearFactors) { | |
if (!arguments.length) return weekOfYearFactors; | |
weekOfYearFactors = newWeekOfYearFactors; | |
return this; | |
} | |
// array of days from start to end of range | |
setUpData.getDays = function() { | |
return getPeriodArray(1) | |
} | |
return setUpData; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<link rel="stylesheet" href="stl.css"> | |
<link href='http://fonts.googleapis.com/css?family=Raleway:400,700' rel='stylesheet' type='text/css'> | |
</head> | |
<body> | |
<div>Seasonal Decomposition</div> | |
<div> | |
<div class="monthly"></div> | |
<div class="monthlyGrowth"></div> | |
<div class="quarterly"></div> | |
<div class="quarterlyGrowth"></div> | |
<div class="weekly"></div> | |
<div class="weeklyGrowth"></div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
<script src="aggregateData.js"></script> | |
<script src="fakeData.js"></script> | |
<script src="timeSeriesLayout.js"></script> | |
<script src="stl.js"></script> | |
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.hidden { | |
display: none; | |
} | |
.lineGraph { | |
fill: none; | |
stroke: #4682b4; | |
stroke-width: 1.5px; | |
} | |
.axis path { | |
fill: none; | |
stroke: #808080; | |
shape-rendering: crispEdges; | |
stroke-width: 1px; | |
} | |
.axis .x { | |
font-size: 11px; | |
} | |
.axis .x .tick { | |
text-anchor: start; | |
} | |
.axis .x .tick text { | |
fill: #808080; | |
transform: rotate(-90deg); | |
} | |
.axis .x.weekly { | |
font-size: 8px; | |
} | |
.bars { | |
fill: #d0d0d0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
// import from other file | |
var fakedData = fakeData() | |
// parameters for fake data | |
var firstWeekValue = 29000, | |
weeklyGrowthRate = 1.0003, | |
// first position is Sunday, data given as % of week value | |
dayOfWeekFactors = [.2, 0, .12, .08, .1, .2, .3], | |
//annual seasonality | |
weekOfYearFactors = [ | |
.9, .9, .9, .9, | |
.9, .9, .9, .9, | |
.9, .9, .9, .9, | |
.9, .9, 1.1, 1.1, | |
1.1, 1.2, 1.2, 1.2, | |
1.2, 1.2, 1.2, 1.2, | |
1.2, 1.2, 1.2, 1.2, | |
1.2, 1.2, 1.2, 1.2, | |
1.2, 1.1, 1.1, 1.1, | |
.9, .9, .9, .9, | |
.9, .9, .9, .9, | |
.9, .9, .9, .9, | |
.9, .9, .9, .9, .9 | |
], | |
holidays = [], | |
/*holidays = [ | |
{ | |
name: 'Easter', | |
dates: ['2010-04-04', '2011-04-24', '2012-04-08', '2013-03-31', '2014-04-20', '2015-04-05', '2016-03-27'], | |
impact: 1.5 | |
}, | |
{ | |
name: 'Christmas', | |
dates: ['2010-12-25', '2011-12-25', '2012-12-25', '2013-12-25', '2014-12-25', '2015-12-25', '2016-12-25'], | |
impact: 0 | |
}, { | |
name: 'Mothers Day', | |
dates: ['2010-05-09', '2011-05-08', '2012-05-13', '2013-05-12', '2014-05-11', '2015-05-10', '2016-05-08'], | |
impact: 1.5 | |
}, { | |
name: 'Thanksgiving', | |
dates: ['2010-11-25', '2011-11-24', '2012-11-22', '2013-11-28', '2014-11-27', '2015-11-26', '2016-11-24'], | |
impact: 1.5 | |
} | |
], | |
*/ | |
adjustment = [{ | |
// march | |
dateRange: [new Date(2016, 2, 1), new Date(2016, 3, 1)], | |
factor: 1 | |
}, { | |
//rest of quarter | |
dateRange: [new Date(2016, 3, 1), new Date(2016, 6, 1)], | |
factor: 1 | |
}], | |
dateRange = [new Date(2012, 0, 1), new Date(2016, 6, 1)]; | |
// apply parameters to fake data | |
fakedData.firstWeek(firstWeekValue) | |
.dayOfWeekFactors(dayOfWeekFactors) | |
.dateRange(dateRange) | |
.holidays(holidays) | |
.adjustment(adjustment) | |
.weeklyGrowth(weeklyGrowthRate) | |
.weekOfYear(weekOfYearFactors); | |
// get data from fakeData functions | |
var dailyData = fakedData() | |
console.log(dailyData, fakedData.getDays()) | |
var dateFormat = d3.time.format("%Y-%m-%d") | |
/* method for printing out data | |
var csvContent = "data:text/csv;charset=utf-8,"; | |
fakedData.getDays().forEach(function(date, index) { | |
var daysData = dailyData[dateFormat(date)]; | |
var dataString = dateFormat(date) + "," + Object.keys(daysData).map(function(k) { | |
return daysData[k] | |
}).join(","); | |
csvContent += index < fakedData.getDays().length ? dataString + "\n" : dataString; | |
}); | |
console.log(csvContent) | |
var encodedUri = encodeURI(csvContent); | |
var link = document.createElement("a"); | |
link.setAttribute("href", encodedUri); | |
link.setAttribute("download", "my_data.csv"); | |
link.click() | |
*/ | |
var plotType = 'trendDowSeasonalHolidaysAdjust' | |
function lineChart(data, dateRange) { | |
function setChart(elem, granularity, plotValue, type) { | |
var chart = d3.select("." + elem).append("svg").attr("width", 1700).attr("height", 300).append("g").attr("transform", "translate(100,30)"); | |
var timeSeries = timeSeriesLayout().granularity(granularity).type(type).dataString(plotValue); | |
var line = d3.svg.line().x(function(d) { | |
return d.x | |
}).y(function(d) { | |
return d.y | |
}).interpolate('linear') | |
chart.append("path") | |
.datum(timeSeries(data, dateRange)) | |
.attr("d", line) | |
.attr("class", "lineGraph " + granularity + " " + type) | |
var axis = chart.append("g").attr("class", "axis") | |
var xAxis = d3.svg.axis().scale(timeSeries.getXScale()).orient("bottom").ticks(getTickInterval(granularity), getTickFrequency(granularity)).tickFormat(getTimeFormat(granularity)); | |
axis.append("g").attr("class", "x " + granularity).call(xAxis).attr("transform", "translate(0," + timeSeries.getYScale()(0) + ")").selectAll("text").attr("dx", "-10px").style("text-anchor", 'end').attr("dy", "-.5em") | |
var yAxis = d3.svg.axis().scale(timeSeries.getYScale()).orient("left").tickFormat(getYAxisFormat(type)) | |
axis.append("g").attr("class", "y").call(yAxis).attr("transform", "translate(0,0)") | |
} | |
return setChart; | |
} | |
function getYAxisFormat(type) { | |
if (type == 'growth') { | |
return d3.format("%") | |
} else { | |
return d3.format("s") | |
} | |
} | |
function getTimeFormat(granularity) { | |
var granularityToFormat = { | |
'weekly': d3.time.format("%d %b"), | |
'quarterly': d3.time.format.multi([ | |
["%Y-Q1", function(d) { | |
return d.getMonth() < 3; | |
}], | |
["Q2", function(d) { | |
return d.getMonth() < 6; | |
}], | |
["Q3", function(d) { | |
return d.getMonth() < 9; | |
}], | |
["Q4", function(d) { | |
return true; | |
}], | |
]), | |
'monthly': d3.time.format.multi([ | |
["%b", function(d) { | |
return d.getMonth(); | |
}], | |
["%Y", function() { | |
return true; | |
}] | |
]), | |
'daily': d3.time.format("%d %b"), | |
} | |
return granularityToFormat[granularity] | |
} | |
function getTickFrequency(granularity) { | |
var granularityToTickFreq = { | |
'weekly': 3, | |
'quarterly': 3, | |
'monthly': 1, | |
'daily': 14, | |
} | |
return granularityToTickFreq[granularity] | |
} | |
function getTickInterval(granularity) { | |
var granularityToTickFreq = { | |
'weekly': d3.time.week, | |
'quarterly': d3.time.month, | |
'monthly': d3.time.month, | |
'daily': d3.time.day, | |
} | |
return granularityToTickFreq[granularity] | |
} | |
function barChart(data, dateRange) { | |
function setBarChart(elem, granularity, plotValue, type) { | |
var chart = d3.select("." + elem).append("svg").attr("width", 1700).attr("height", 300).append("g").attr("transform", "translate(100,30)"); | |
var timeSeries = timeSeriesLayout().granularity(granularity).type(type).dataString(plotValue); | |
var yScale = timeSeries.getYScale(); | |
chart.selectAll(".bars") | |
.data(timeSeries(data, dateRange)) | |
.enter() | |
.append("rect") | |
.attr({ | |
'x': function(d) { | |
return d.x | |
}, | |
'y': function(d) { | |
return d.y < yScale(0) ? d.y : yScale(0) | |
}, | |
'height': function(d) { | |
return d.height | |
}, | |
'width': function(d) { | |
return d.width | |
}, | |
'class': 'bars' | |
}) | |
var axis = chart.append("g").attr("class", "axis") | |
var xAxis = d3.svg.axis().scale(timeSeries.getXScale()).orient("bottom").ticks(getTickInterval(granularity), getTickFrequency(granularity)).tickFormat(getTimeFormat(granularity)) | |
axis.append("g").attr("class", "x " + granularity).call(xAxis).attr("transform", "translate(0," + yScale(0) + ")").selectAll("text").attr("dx", "-70px").style("text-anchor", 'end').attr("dy", "-.2em") | |
var yAxis = d3.svg.axis().scale(timeSeries.getYScale()).orient("left").tickFormat(getYAxisFormat(type)) | |
axis.append("g").attr("class", "y").call(yAxis).attr("transform", "translate(0,0)") | |
} | |
return setBarChart; | |
} | |
lineChart(dailyData, fakedData.dateRange())('monthly', 'monthly', plotType, 'value') | |
barChart(dailyData, fakedData.dateRange())('monthlyGrowth', 'monthly', plotType, 'growth') | |
lineChart(dailyData, fakedData.dateRange())('weekly', 'weekly', plotType, 'value') | |
barChart(dailyData, fakedData.dateRange())('weeklyGrowth', 'weekly', plotType, 'growth') | |
lineChart(dailyData, fakedData.dateRange())('quarterly', 'quarterly', plotType, 'value') | |
barChart(dailyData, fakedData.dateRange())('quarterlyGrowth', 'quarterly', plotType, 'growth') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.hidden | |
display: none | |
.lineGraph | |
fill: none | |
stroke: steelblue | |
stroke-width: 1.5px | |
.axis path | |
fill: none | |
stroke: gray | |
shape-rendering: crispEdges | |
stroke-width: 1px | |
.axis | |
.x | |
font-size: 11px | |
.tick | |
text-anchor: start | |
text | |
fill: gray | |
transform: rotate(-90deg) | |
.axis | |
.x | |
&.weekly | |
font-size: 8px | |
.bars | |
fill: #d0d0d0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function timeSeriesLayout() { | |
// 'growth' or 'value' | |
var type = 'value' | |
// daily, weekly, monthly, quarterly, annual | |
var granularity = 'daily' | |
var width = 1000; | |
var height = 200; | |
var dataString = 'value'; | |
// | |
var periodLookup = { | |
'monthly': 12, | |
'quarterly': 4, | |
'weekly': 52, | |
'daily': 364 | |
}; | |
// set up scales | |
var xScale = d3.time.scale().range([0, width]); | |
var yScale = d3.scale.linear().range([height, 0]); | |
var heightScale = d3.scale.linear() | |
function processTimeSeries(data, dateExtent) { | |
var layoutData = []; | |
// read in data object | |
var aggregate = aggregateData(); | |
aggregate.granularity(granularity) | |
var days = d3.time.day.utc.range(dateExtent[0], dateExtent[1], 1); | |
aggregate(data, days) | |
var extent = aggregate.getValueExtent() | |
var aggData = aggregate.getData(); | |
var barWidth = Math.floor((width) / aggData.length) - 2; | |
// set scales | |
xScale.domain(dateExtent) | |
if (type == 'growth') { | |
heightScale.domain([0, .1]).range([0, height / 2]); | |
yScale.domain([-.1, .1]); | |
} else { | |
heightScale.domain([0, extent[1]]).range([0, height]); | |
yScale.domain([0, extent[1]]); | |
} | |
if (type == 'value') { | |
aggData.forEach(function(day, i) { | |
layoutData[i] = {}; | |
layoutData[i].x = xScale(Date.parse(day.key)); | |
layoutData[i].y = yScale(day.values[dataString]); | |
layoutData[i].height = heightScale(day.values[dataString]); | |
layoutData[i].width = barWidth; | |
}) | |
} else { | |
aggData.forEach(function(day, i) { | |
var period = periodLookup[granularity]; | |
var growth = (i - period) >= 0 ? aggData[i].values[dataString] / aggData[i - period].values[dataString] - 1 : 0; | |
layoutData[i] = {}; | |
layoutData[i].x = xScale(Date.parse(day.key)); | |
layoutData[i].y = yScale(growth); | |
layoutData[i].height = heightScale(Math.abs(growth)); | |
layoutData[i].width = barWidth; | |
}) | |
} | |
return layoutData; | |
} | |
processTimeSeries.type = function(newType) { | |
if (!arguments.length) return type; | |
type = newType; | |
return this; | |
} | |
processTimeSeries.dataString = function(newString) { | |
if (!arguments.length) return dataString; | |
dataString = newString; | |
return this; | |
} | |
processTimeSeries.granularity = function(newGranularity) { | |
if (!arguments.length) return newGranularity; | |
granularity = newGranularity; | |
return this; | |
} | |
processTimeSeries.height = function(newHeight) { | |
if (!arguments.length) return newHeight; | |
height = newHeight; | |
return this; | |
} | |
processTimeSeries.width = function(newWidth) { | |
if (!arguments.length) return newWidth; | |
width = newWidth; | |
return this; | |
} | |
processTimeSeries.getXScale = function() { | |
return xScale; | |
} | |
processTimeSeries.getYScale = function() { | |
return yScale; | |
} | |
return processTimeSeries; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment