Kanban Simulator
Helpers for running simulations of Kanban systems.
Currently no GUI, but works well in a Jupyter/iPython Notebook, like
(requires installation of ipython[notebook]
, pandas
, numpy
,
matplotlib
and openpyxel
)::
import random
import kanban_simulator.board as kb
# For rendering HTML output in an iPython notebook:
from IPython.display import display, HTML
%matplotlib inline
# For data analysis and plan view:
import pandas as pd
import numpy as np
def to_plan(board, start_date, finished_day, freq='W-MON'):
"""Use Pandas to print a week-by-week plan-like view showing
what state each card was in each week.
"""
grid = pd.DataFrame(
index=[c.name for c in board.donelog.cards],
columns=pd.date_range(start_date, freq='D', periods=finished_day)
)
for card in board.donelog.cards:
for col, data in card.history.items():
for day in data['dates']:
grid.ix[card.name, day-1] = col.name
return grid.resample(freq, label='left', axis=1).first().fillna("")
# Build a backlog with some epics.
# Stipulate that when the epic enters the "Build" sublane-column, it will
# split into a number of stories.
backlog = kb.Backlog(cards=[
kb.Epic("Epic one", splits={'Build': random.randint(5, 10)}),
kb.Epic("Epic two", splits={'Build': random.randint(10, 20)}),
kb.Epic("Epic three", splits={'Build': 30}),
kb.Epic("Epic four", splits={'Build': 50}),
kb.Epic("Epic five", splits={'Build': 50}),
kb.Epic("Epic six", splits={'Build': 50}),
kb.Epic("Epic seven", splits={'Build': 50}),
])
# Create a lane and clone it so that we have two lanes with the same columns
# It has a lane-wide WIP limit (optional), and a series of columns
# operating on epics. The "Build" column has a sub-lane (or rather,
# might have one or more depending on the number of epics in this column,
# subject to WIP limits), which operates on stories. The epic itself splits
# into stories and becomes a backlog for these stories, as per the number of
# stories above.
lane_template = kb.Lane(
name="<lane name>",
wip_limit=3,
columns=[
kb.Column(
name="Discovery",
touch=lambda: random.randint(5, 10),
wip_limit=1,
card_type=kb.Epic
),
kb.QueueColumn(
name="Ready for Build",
wip_limit=1,
card_type=kb.Epic
),
kb.SublaneColumn(
name="Build",
lane_template=kb.Lane(
name="Build",
columns=[
kb.Column(
name="Analysis",
touch=lambda: random.randint(1, 3),
wip_limit=3,
card_type=kb.Story
),
kb.Column(
name="Development",
touch=lambda: random.randint(1, 4),
wip_limit=3,
card_type=kb.Story
),
kb.Column(
name="Test",
touch=lambda: random.randint(1, 2),
wip_limit=3,
card_type=kb.Story
),
],
),
wip_limit=1,
card_type=kb.Epic
),
kb.Column(
name="Final testing",
touch=lambda: random.randint(1, 5),
wip_limit=1,
card_type=kb.Epic
),
]
)
lanes = [
lane_template.clone(name="Team 1"),
lane_template.clone(name="Team 2"),
]
# Create the board
board = kb.Board(
name="Test simulation",
lanes=lanes,
backlog=backlog
)
# Show the Kanban board day by day. The board is a state machine,
# so when we iterate through it, the state changes. We use `clone()` to
# get a new copy so we can use the same `board` later.
for day, board_state in board.clone():
print "Day", day
board_html = board_state.to_html()
# iPython notebook specific magic to print HTML
display(HTML(board_html))
# If we only want the end state, we can just do:
days, board_state = board.clone().run_simulation()
print "It took", days, "days"
# The cards are in the `board_state.donelog.cards` list. They have
# attributes like `age` (total number of days), `dates` (dates the card
# was active), `touch` (number of days actually working on a card, as
# opposed to waiting), and `history` (a breakdown of `age`, `dates` and
# `touch`) by column name.
# We can also run a Monte Carlo simulation:
mc_results = board.run_monte_carlo_simulation(trials=100)
# We can do some data analysis on the finish dates of each
finishes = pd.Series([r[0] for r in mc_results])
print "Monte Carlo, after", len(mc_results), "loops. Quantiles:"
print finishes.quantile([0.5, 0.85, 0.95])
# Histogram of finishes
finishes.plot.hist()
# Board at the 85th percentile, output as a grid plan
day85, board85 = mc_results[int(len(mc_results) * 0.85)]
plan = to_plan(board85, '2016-06-01', day85)
display(HTML(plan.to_html()))
# Save to Excel (requires openpyxl)
plan.to_excel("simulation.xlsx", "Simulation")
Changelog
0.3 - 03 June 2016
* BREAKING: If touch
or a splits
value is a function, it will be called with the
card as an argument.
* Card splits can now be either a callable or a number (analogous to
touch
)
* New column type, SharedWIPColumn(), which can group multiple columns so
that they have a shared overall WIP limit.
* Fixed problem whereby lane WIP limits could be ignored in the first
column
0.2 - 24 May 2016
* Card history
is now an OrderedDict
* A backlog can now have a chained "parent" backlog via card_source
0.1 - 24 May 2016
* Initial release