That means the best place to start is figuring out how to get words from the user and what kinds of words those are.
In our game we have to create a Lexicon of words:
When we get to nouns, we have a slight problem since each room could have a different set of Nouns, but let's just pick this small set to work with for now and improve it later.
Once we have our lexicon of words we need a way to break up sentences so that we can figure out what they are. In our case, we've defined a sentence as "words separated by spaces", so we really just need to do this:
stuff
=
gets
.
chomp
()
words
=
stuff
.
split
()
That's really all we'll worry about for now, but this will work really well for quite a while.
Once we know how to break up a sentence into words, we just have to go through the list of words and figure out what "type" they are. To do that we're going to use a handy little Ruby structure called a "struct". A struct is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class. It's created like this:
Pair
=
Struct
.
new
(
:token
,
:word
)
first_word
=
Pair
.
new
(
"direction"
,
"north"
)
second_word
=
Pair
.
new
(
"verb"
,
"go"
)
sentence
=
[
first_word
,
second_word
]
This creates a pair of (TOKEN, WORD) that lets you look at the word and do things with it.
This is just an example, but that's basically the end result. You want to take input from the user, carve it into words with split, then analyze those words to identify their type, and finally make a sentence out of them.
Now you are ready to write your scanner. This scanner will take a string of input from a user and return a sentence that's composed of a list of structs with the (TOKEN, WORD) pairings. If a word isn't part of the lexicon then it should still return the WORD, but set the TOKEN to an error token. These error tokens will tell the user they messed up.
Here's where it gets fun. I'm not going to tell you how to do this. Instead I'm going to write a unit test und you are going to write the scanner so that the unit test works.
There is one tiny thing I will help you with first, and that's converting numbers. In order to do this though, we're going to cheat and use exceptions. An exception is an error that you get from some function you may have run. What happens is your function "raises" an exception when it encounters an error, then you have to handle that exception. For example, if you type this into IRB:
ruby-1.9.2-p180 :001 > Integer("hell")
ArgumentError: invalid value for Integer(): "hell"
from (irb):1:in `Integer'
from (irb):1
from /home/rob/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `'
That ArgumentError is an exception that the Integer() function threw because what you handed Integer() is not a number. The Integer() function could have returned a value to tell you it had an error, but since it only returns numbers, it'd have a hard time doing that. It can't return -1 since that's a number. Instead of trying to figure out what to return when there's an error, the Integer() function raises the TypeError exception and you deal with it.
You deal with an exception by using the begin and rescue keywords:
def
convert_number
(
s
)
begin
Integer
(
s
)
rescue
ArgumentError
nil
end
end
You put the code you want to "begin" inside the begin block, and then you put the code to run for the error inside the rescue. In this case, we want to call Integer() on something that might be a number. If that has an error, then we "rescue" it and return nil instead.
In your scanner that you write, you should use this function to test if something is a number. You should also do it as the last thing you check for before declaring that word an error word.
Here are the files test/test_lexicon.rb that you should use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
require
'test/unit'
require_relative
"../lib/lexicon"
class
LexiconTests
<
Test
::
Unit
::
TestCase
Pair
=
Lexicon
::
Pair
@@lexicon
=
Lexicon
.
new
()
def
test_directions
()
assert_equal
(
[
Pair
.
new
(
:direction
,
'north'
)
]
,
@@lexicon
.
scan
(
"north"
))
result
=
@@lexicon
.
scan
(
"north south east"
)
assert_equal
(
result
,
[
Pair
.
new
(
:direction
,
'north'
),
Pair
.
new
(
:direction
,
'south'
),
Pair
.
new
(
:direction
,
'east'
)
]
)
end
def
test_verbs
()
assert_equal
(
@@lexicon
.
scan
(
"go"
),
[
Pair
.
new
(
:verb
,
'go'
)
]
)
result
=
@@lexicon
.
scan
(
"go kill eat"
)
assert_equal
(
result
,
[
Pair
.
new
(
:verb
,
'go'
),
Pair
.
new
(
:verb
,
'kill'
),
Pair
.
new
(
:verb
,
'eat'
)
]
)
end
def
test_stops
()
assert_equal
(
@@lexicon
.
scan
(
"the"
),
[
Pair
.
new
(
:stop
,
'the'
)
]
)
result
=
@@lexicon
.
scan
(
"the in of"
)
assert_equal
(
result
,
[
Pair
.
new
(
:stop
,
'the'
),
Pair
.
new
(
:stop
,
'in'
),
Pair
.
new
(
:stop
,
'of'
)
]
)
end
def
test_nouns
()
assert_equal
(
@@lexicon
.
scan
(
"bear"
),
[
Pair
.
new
(
:noun
,
'bear'
)
]
)
result
=
@@lexicon
.
scan
(
"bear princess"
)
assert_equal
(
result
,
[
Pair
.
new
(
:noun
,
'bear'
),
Pair
.
new
(
:noun
,
'princess'
)
]
)
end
def
test_numbers
()
assert_equal
(
@@lexicon
.
scan
(
"1234"
),
[
Pair
.
new
(
:number
,
1234
)
]
)
result
=
@@lexicon
.
scan
(
"3 91234"
)
assert_equal
(
result
,
[
Pair
.
new
(
:number
,
3
),
Pair
.
new
(
:number
,
91234
)
]
)
end
def
test_errors
()
assert_equal
(
@@lexicon
.
scan
(
"ASDFADFASDF"
),
[
Pair
.
new
(
:error
,
'ASDFADFASDF'
)
]
)
result
=
@@lexicon
.
scan
(
"bear IAS princess"
)
assert_equal
(
result
,
[
Pair
.
new
(
:noun
,
'bear'
),
Pair
.
new
(
:error
,
'IAS'
),
Pair
.
new
(
:noun
,
'princess'
)
]
)
end
end
|
Remember that you will want to make a new project with your skeleton, type in this test case (do not copy-paste!) and write your scanner so that the test runs. Focus on the details and make sure everything works right.
Focus on getting one test working at a time. Keep this simple and just put all the words in your lexicon in lists that are in your lexicon.rb file. Do not modify the input list of words, but instead make your own new list with your lexicon pairs in it. Also, use the include? method with these lexicon arrays to check if a word is in the lexicon.
What we should be able to get from our little game lexicon scanner is a list that looks like this (yours will be formatted differently):
ruby-1.9.2-p180 :003 > print Lexicon.scan("go north")
[#<struct Lexicon::Pair token=:verb, word="go">,
#<struct Lexicon::Pair token=:direction, word="north">] => nil
ruby-1.9.2-p180 :004 > print Lexicon.scan("kill the princess")
[#<struct Lexicon::Pair token=:verb, word="kill">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="princess">] => nil
ruby-1.9.2-p180 :005 > print Lexicon.scan("eat the bear")
[#<struct Lexicon::Pair token=:verb, word="eat">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="bear">] => nil
ruby-1.9.2-p180 :006 > print Lexicon.scan("open the door and smack the bear in the nose")
[#<struct Lexicon::Pair token=:error, word="open">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="door">,
#<struct Lexicon::Pair token=:error, word="and">,
#<struct Lexicon::Pair token=:error, word="smack">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:noun, word="bear">,
#<struct Lexicon::Pair token=:stop, word="in">,
#<struct Lexicon::Pair token=:stop, word="the">,
#<struct Lexicon::Pair token=:error, word="nose">] => nil
ruby-1.9.2-p180 :007 >
Now let us turn this into something the game can work with, which would be some kind of Sentence class.
If you remember grade school, a sentence can be a simple structure like:
Subject Verb Object
Obviously it gets more complex than that, and you probably did many days of annoying sentence graphs for English class. What we want is to turn the above lists of structs into a nice Sentence object that has subject, verb, and object.
To do this we need four tools:
def
peek
(
word_list
)
begin
word_list
.
first
.
token
rescue
nil
end
end
Very easy. Now for the match function:
def
match
(
word_list
,
expecting
)
begin
word
=
word_list
.
shift
if
word
.
token
==
expecting
word
else
nil
end
rescue
nil
end
end
Again, very easy, and finally our skip function:
def
skip
(
word_list
,
word_type
)
while
peek
(
word_list
)
==
word_type
match
(
word_list
,
word_type
)
end
end
By now you should be able to figure out what these do. Make sure you understand them.
With our tools we can now begin to build Sentence objects from our array of structs. What we do is a process of:
The best way to demonstrate this is to give you the code to read, but here's where this exercise is different from the previous one: You will write the test for the parser code I give you. Rather than giving you the test so you can write the code, I will give you the code, and you have to write the test.
Here's the code that I wrote for parsing simple sentences using the ex48 Lexicon class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
class
ParserError
<
Exception
end
class
Sentence
def
initialize
(
subject
,
verb
,
object
)
# remember we take Pair.new(:noun, "princess") structs and convert them
@subject
=
subject
.
word
@verb
=
verb
.
word
@object
=
object
.
word
end
end
def
peek
(
word_list
)
begin
word_list
.
first
.
token
rescue
nil
end
end
def
match
(
word_list
,
expecting
)
begin
word
=
word_list
.
shift
if
word
.
token
==
expecting
word
else
nil
end
rescue
nil
end
end
def
skip
(
word_list
,
token
)
while
peek
(
word_list
)
==
token
match
(
word_list
,
token
)
end
end
def
parse_verb
(
word_list
)
skip
(
word_list
,
:stop
)
if
peek
(
word_list
)
==
:verb
return
match
(
word_list
,
:verb
)
else
raise
ParserError
.
new
(
"Expected a verb next."
)
end
end
def
parse_object
(
word_list
)
skip
(
word_list
,
:stop
)
next_word
=
peek
(
word_list
)
if
next_word
==
:noun
return
match
(
word_list
,
:noun
)
end
if
next_word
==
:direction
return
match
(
word_list
,
:direction
)
else
raise
ParserError
.
new
(
"Expected a noun or direction next."
)
end
end
def
parse_subject
(
word_list
,
subj
)
verb
=
parse_verb
(
word_list
)
obj
=
parse_object
(
word_list
)
return
Sentence
.
new
(
subj
,
verb
,
obj
)
end
def
parse_sentence
(
word_list
)
skip
(
word_list
,
:stop
)
start
=
peek
(
word_list
)
if
start
==
:noun
subj
=
match
(
word_list
,
:noun
)
return
parse_subject
(
word_list
,
subj
)
elsif
start
==
:verb
# assume the subject is the player then
return
parse_subject
(
word_list
,
Pair
.
new
(
:noun
,
"player"
))
else
raise
ParserError
.
new
(
"Must start with subject, object, or verb not:
#{
start
}
"
)
end
end
|
You briefly learned about exceptions, but not how to raise them. This code demonstrates how to do that with the ParserError at the top. Notice that it uses classes to give it the type of Exception. Also notice the use of raise keyword to raise the exception.
In your tests, you will want to work with these exceptions, which I'll show you how to do.
For Exercise 49 is write a complete test that confirms everything in this code is working. That includes making exceptions happen by giving it bad sentences.
Check for an exception by using the function assert_raise from the Test::Unit documentation. Learn how to use this so you can write a test that is expected to fail, which is very important in testing. Learn about this function (and others) by reading the Test::Unit documentation.
When you are done, you should know how this bit of code works, and how to write a test for other people's code even if they do not want you to. Trust me, it's a very handy skill to have.
These final three exercises will be very hard and you should take your time with them. In this first one you'll build a simple web version of one of your games. Before you attempt this exercise you must have completed Exercise 46 successfully and have a working RubyGems installed such that you can install packages and know how to make a skeleton project directory. If you don't remember how to do this, go back to Exercise 46 and do it all over again.
Before creating your first web application, you'll first need to install the "web framework" called Sinatra. The term "framework" generally means "some package that makes it easier for me to do something". In the world of web applications, people create "web frameworks" to compensate for the difficult problems they've encountered when making their own sites. They share these common solutions in the form of a package you can download to bootstrap your own projects.
In our case, we'll be using the Sinatra framework, but there are many, many, many others you can choose from. For now, learn Sinatra then branch out to another one when you're ready (or just keep using Sinatra since it's good enough).
Using gem install Sinatra:
$ gem install sinatra
Fetching: tilt-1.3.2.gem (100%)
Fetching: sinatra-1.2.6.gem (100%)
Successfully installed tilt-1.3.2
Successfully installed sinatra-1.2.6
2 gems installed
Installing ri documentation for tilt-1.3.2...
Installing ri documentation for sinatra-1.2.6...
Installing RDoc documentation for tilt-1.3.2...
Installing RDoc documentation for sinatra-1.2.6...
Now you're going to make an initial very simple "Hello World" web application and project directory using Sinatra. First, make your project directory:
$
cd projects
$ bundle gem gothonweb
You'll be taking the game from Exercise 42 and making it into a web application, so that's why you're calling it gothonweb. Before you do that, we need to create the most basic Sinatra application possible. Put the following code into lib/gothonweb.rb:
1 2 3 4 5 6 7 8 9 |
require_relative
"gothonweb/version"
require
"sinatra"
module
Gothonweb
get
'/'
do
greeting
=
"Hello, World!"
return
greeting
end
end
|
Then run the application like this:
$ ruby lib/gothonweb.rb
== Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick
[2011-07-18 11:27:07] INFO WEBrick 1.3.1
[2011-07-18 11:27:07] INFO ruby 1.9.2 (2011-02-18) [x86_64-linux]
[2011-07-18 11:27:07] INFO WEBrick::HTTPServer#start: pid=6599 port=4567
Finally, use your web browser and go to the URL http://localhost:4567/ and you should see two things. First, in your browser you'll see Hello, World!. Second, you'll see your terminal with new output like this:
<IP_ADDRESS> - - [18/Jul/2011 11:29:10] "GET / HTTP/1.1" 200 12 0.0015
localhost - - [18/Jul/2011:11:29:10 EDT] "GET / HTTP/1.1" 200 12
- -> /
<IP_ADDRESS> - - [18/Jul/2011 11:29:10] "GET /favicon.ico HTTP/1.1" 404 447 0.0008
localhost - - [18/Jul/2011:11:29:10 EDT] "GET /favicon.ico HTTP/1.1" 404 447
- -> /favicon.ico
Those are log messages that Sinatra prints out so you can see that the server is working, and what the browser is doing behind the scenes. The log messages help you debug and figure out when you have problems. For example, it's saying that your browser tried to get /favicon.ico but that file didn't exist so it returned 404 Not Found status code.
I haven't explained the way any of this web stuff works yet, because I want to get you setup and ready to roll so that I can explain it better in the next two exercises. To accomplish this, I'll have you break your Sinatra application in various ways and then restructure it so that you know how it's setup.
Here's what's happening when your browser hits your application:
Make sure you really understand this. Draw up a diagram of how this information flows from your browser, to Sinatra, then to the / block and back to your browser.
First, delete line 6 where you assign the greeting variable, then hit refresh in your browser. You should see an error page now that gives you lots of information on how your application just exploded. You know that the variable greeting is now missing, but Sinatra gives you this nice error page to track down exactly where. Do each of the following with this page:
You can break your Sinatra application, but did you notice that "Hello World" isn't a very good HTML page? This is a web application, and as such it needs a proper HTML response. To do that you will create a simple template that says "Hello World" in a big green font.
The first step is to create a lib/views/index.erb file that looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |

<title>Gothons Of Planet Percal #25</title>

<%
if
greeting
%>

I just wanted to say
<%=
greeting
%>
.
<%
else
%>
Hello, world!
<%
end
%>

|
What is a .erb file? ERB stands for Embedded Ruby. .erb files are HTML with bits of Ruby code embedded within. If you know what HTML is then this should look fairly familiar. If not, research HTML and try writing a few web pages by hand so you know how it works. Since this is an erb template, Sinatra will fill in "holes" in the text depending on variables you pass in to the template. Every place you see <%= greeting %> will be a variable you'll pass to the template that alters its contents.
To make your lib/gothonweb.rb script do this, you need to add some code to tell Sinatra where to load the template and to render it. Take that file and change it like this:
1 2 3 4 5 6 7 8 9 10 |
require_relative
"gothonweb/version"
require
"sinatra"
require
"erb"
module
Gothonweb
get
'/'
do
greeting
=
"Hello, World!"
erb
:index
,
:locals
=>
{
:greeting
=>
greeting
}
end
end
|
Pay close attention to how I changed the last line of the / block so it calls erb passing in your greeting variable.
Once you have that in place, reload the web page in your browser and you should see a different message in green. You should also be able to do a View Source on the page in your browser to see that it is valid HTML.
This may have flown by you very fast, so let me explain how a template works:
To get deeper into this, change the greeting variable and the HTML to see what effect it has. Also create another template named lib/views/foo.lib and render that using erb :foo instead of erb :index like before. This will show you how the first parameter you pass to erb is just matched to a .erb file in lib/views/.
While it's exciting to see the browser display "Hello World", it's even more exciting to let the user submit text to your application from a form. In this exercise we'll improve our starter web application using forms and figure out how to do automated testing for a web application.
Time for some boring stuff. You need to understand a bit more about how the web works before you can make a form. This description isn't complete, but it's accurate and will help you figure out what might be going wrong with your application. Also, creating forms will be easier if you know what they do.
I'll start with a simple diagram that shows you the different parts of a web request and how the information flows:
I've labeled the lines with letters so I can walk you through a regular request process:
In this description there are a few terms you should know so that you have a common vocabulary to work with when talking about your web application:
The software that you're probably using every day. Most people don't know what it really does, they just call it "the internet". Its job is to take addresses (like http://learnpythonthehardway.org) you type into the URL bar, then use that information to make requests to the server at that address.
This is normally a URL (Uniform Resource Locator) like http://learnpythonthehardway.org/ and indicates where a browser should go. The first part http indicates the protocol you want to use, in this case "Hyper-Text Transport Protocol". You can also try ftp://ibiblio.org/ to see how "File Transport Protocol" works. The learnpythonthehardway.org part is the "hostname", or a human readable address you can remember and which maps to a number called an IP address, similar to a telephone number for a computer on the Internet. Finally, URLs can have a trailing path like the /book/ part of http://learnpythonthehardway.org/book/ which indicates a file or some resource on the server to retrieve with a request. There are many other parts, but those are the main ones.
Once a browser knows what protocol you want to use (http), what server you want to talk to (learnpythonthehardway.org), and what resource on that server to get, it must make a connection. The browser simply asks your Operating System (OS) to open a "port" to the computer, usually port 80. When it works the OS hands back to your program something that works like a file, but is actually sending and receiving bytes over the network wires between your computer and the other computer at "learnpythonthehardway.org". This is also the same thing that happens with http://localhost:8080/ but in this case you're telling the browser to connect to your own computer (localhost) and use port 4567 rather than the default of 80. You could also do http://learnpythonthehardway.org:80/ and get the same result, except you're explicitly saying to use port 80 instead of letting it be that by default.
Your browser is connected using the address you gave. Now it needs to ask for the resource it wants (or you want) on the remote server. If you gave /book/ at the end of the URL, then you want the file (resource) at /book/, and most servers will use the real file /book/index.html but pretend it doesn't exist. What the browser does to get this resource is send a request to the server. I won't get into exactly how it does this, but just understand that it has to send something to query the server for the request. The interesting thing is that these "resources" don't have to be files. For instance, when the browser in your application asks for something, the server is returning something your code generated.
The server is the computer at the end of a browser's connection that knows how to answer your browser's requests for files/resources. Most web servers just send files, and that's actually the majority of traffic. But you're actually building a server in Ruby that knows how to take requests for resources, and then return strings that you craft using Ruby. When you do this crafting, you are pretending to be a file to the browser, but really it's just code. As you can see from Ex. 50, it also doesn't take much code to create a response.
This is the HTML (css, javascript, or images) your server wants to send back to the browser as the answer to the browser's request. In the case of files, it just reads them off the disk and sends them to the browser, but it wraps the contents of the disk in a special "header" so the browser knows what it's getting. In the case of your application, you're still sending the same thing, including the header, but you generate that data on the fly with your Ruby code.
That is the fastest crash course in how a web browser accesses information on servers on the internet. It should work well enough for you to understand this exercise, but if not, read about it as much as you can until you get it. A really good way to do that is to take the diagram, and break different parts of the web application you did in Exercise 50. If you can break your web application in predictable ways using the diagram, you'll start to understand how it works.
The best way to play with forms is to write some code that accepts form data, and then see what you can do. Take your lib/gothonweb.rb file and make it look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
require_relative
"gothonweb/version"
require
"sinatra"
require
"erb"
module
Gothonweb
get
'/'
do
greeting
=
"Hello, World!"
erb
:index
,
:locals
=>
{
:greeting
=>
greeting
}
end
get
'/hello'
do
name
=
params
[
:name
]
||
"Nobody"
greeting
=
"Hello,
#{
name
}
"
erb
:index
,
:locals
=>
{
:greeting
=>
greeting
}
end
end
|
Restart Sinatra (hit CTRL-C and then run it again) to make sure it loads again, then with your browser go to http://localhost:4567/hello which should display, "I just wanted to say Hello, Nobody." Next, change the URL in your browser to http://localhost:4567/hello?name=Frank and you'll see it say "Hello, Frank." Finally, change the name=Frank part to be your name. Now it's saying hello to you.
Let's break down the changes I made to your script.
You're also not restricted to just one parameter on the URL. Change this example to give two variables like this: http://localhost:4567/hello?name=Frank&greet=Hola. Then change the code to get params[:name] and params[:greet] like this:
greeting
=
"
#{
greet
}
,
#{
name
}
"
Passing the parameters on the URL works, but it's kind of ugly and not easy to use for regular people. What you really want is a "POST form", which is a special HTML file that has a <form> tag in it. This form will collect information from the user, then send it to your web application just like you did above.
Let's make a quick one so you can see how it works. Here's the new HTML file you need to create, in lib/views/hello_form.erb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |

<title>Sample Web Form</title>

Fill Out This Form

<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">

Your Name: <input type="text" name="name">

<input type="submit">
</form>

|
You should then change lib/gothonweb.rb to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
require_relative
"gothonweb/version"
require
"sinatra"
require
"erb"
module
Gothonweb
get
'/'
do
greeting
=
"Hello, World!"
erb
:index
,
:locals
=>
{
:greeting
=>
greeting
}
end
get
'/hello'
do
erb
:hello_form
end
post
'/hello'
do
greeting
=
"
#{
params
[
:greet
]
||
"Hello"
}
,
#{
params
[
:name
]
||
"Nobody"
}
"
erb
:index
,
:locals
=>
{
:greeting
=>
greeting
}
end
end
|
Once you've got those written up, simply restart the web application again and hit it with your browser like before.
This time you'll get a form asking you for "A Greeting" and "Your Name". When you hit the Submit button on the form, it will give you the same greeting you normally get, but this time look at the URL in your browser. See how it's http://localhost:4567/hello even though you sent in parameters.
The part of the hello_form.erb file that makes this work is the line with <form action="/hello" method="POST">. This tells your browser to:
How this new application works is:
As an exercise, go into the lib/views/index.erb file and add a link back to just /hello so that you can keep filling out the form and seeing the results. Make sure you can explain how this link works and how it's letting you cycle between lib/views/index.erb and lib/views/hello_form.erb and what's being run inside this latest Ruby code.
When you work on your game in the next Exercise, you'll need to make a bunch of little HTML pages. Writing a full web page each time will quickly become tedious. Luckily you can create a "layout" template, or a kind of shell that will wrap all your other pages with common headers and footers. Good programmers try to reduce repetition, so layouts are essential for being a good programmer.
Change lib/views/index.erb to be like this:
1 2 3 4 5 |
<%
if
greeting
%>

I just wanted to say
<%=
greeting
%>
.
<%
else
%>
Hello, world!
<%
end
%>
>
|
Change lib/views/hello_form.erb to be like this:
1 2 3 4 5 6 7 8 9 |

Fill Out This Form

<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">

Your Name: <input type="text" name="name">

<input type="submit">
</form>
|
All we're doing is stripping out the "boilerplate" at the top and the bottom which is always on every page. We'll put that back into a single lib/views/layout.erb file that handles it for us from now on.
Once you have those changes, create a lib/views/layout.erb file with this in it:
1 2 3 4 5 6 7 8 |

<title>Gothons From Planet Percal #25</title>

<%=
yield
%>

|
Sinatra automatically looks for a layout template called layout by default to use as the base template for all other templates. You can customize which template is used as the base for any given page, too. Restart your application and then try to change the layout in interesting ways, but without changing the other templates.
It's easy to test a web application with your browser by just hitting refresh, but come on, we're programmers here. Why do some repetitive task when we can write some code to test our application? What you're going to do next is write a little test for your web application form based on what you learned in Exercise 47. If you don't remember Exercise 47, read it again.
I've created a simple little function for that lets you assert things about your web application's response, aptly named assert_response. Create the file test/test_gothonweb.rb with these contents:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
require
'test/unit'
def
assert_response
(
resp
,
contains
=
nil
,
matches
=
nil
,
headers
=
nil
,
status
=
200
)
assert_equal
(
resp
.
status
,
status
,
"Expected response
#{
status
}
not in
#{
resp
}
"
)
if
status
==
200
assert
(
resp
.
body
,
"Response data is empty."
)
end
if
contains
assert
((
resp
.
body
.
include?
contains
),
"Response does not contain
#{
contains
}
"
)
end
if
matches
reg
=
Regexp
.
new
(
matches
)
assert
reg
.
match
(
contains
),
"Response does not match
#{
matches
}
"
end
if
headers
assert_equal
(
resp
.
headers
,
headers
)
end
end
|
Finally, run test/test_gothonweb.rb to test your web application:
$ ruby
test/test_gothonweb.rb
Loaded suite test/test_gothonweb
Started
.
Finished in 0.023839 seconds.
1 tests, 9 assertions, 0 failures, 0 errors, 0 skips
Test run options: --seed 57414
What I'm doing here is I'm actually importing the whole application from the lib/gothonweb.rb library, then running it manually.
The rack/test library we have included has a very simple API for processing requests. Its get, put, post, delete, and head methods simulate the respective type of request on the application.
All mock request methods have the same argument signature:
get
'/path'
,
params
=
{},
rack
\
_env
=
{}
This works without running an actual web server so you can do tests with automated tests and also use your browser to test a running server.
To validate responses from this function, use the assert_response function from test/test_gothonweb.rb which has:
assert_response
(
resp
,
contains
=
nil
,
matches
=
nil
,
headers
=
nil
,
status
=
200
)
Pass in the response you get from calling get or post then add things you want checked. Use the contains parameter to make sure that the response contains certain values. Use the status parameter to check for certain responses. There's actually quite a lot of information in this little function so it would be good for you to study it.
In the test/test_gothonweb.rb automated test I'm first making sure the /foo URL returns a "404 Not Found" response, since it actually doesn't exist. Then I'm checking that /hello works with both a GET and POST form. Following the test should be fairly simple, even if you might not totally know what's going on.
Take some time studying this latest application, especially how the automated testing works.
We're coming to the end of the book, and in this exercise I'm going to really challenge you. When you're done, you'll be a reasonably competent Ruby beginner. You'll still need to go through a few more books and write a couple more projects, but you'll have the skills to complete them. The only thing in your way will be time, motivation, and resources.
In this exercise, we won't make a complete game, but instead we'll make an "engine" that can run the game from Exercise 42 in the browser. This will involve refactoring Exercise 42, mixing in the structure from Exercise 47, adding automated tests, and finally creating a web engine that can run the games.
This exercise will be huge, and I predict you could spend anywhere from a week to months on it before moving on. It's best to attack it in little chunks and do a bit a night, taking your time to make everything work before moving on.
You've been altering the gothonweb project for two exercises and you'll do it one more time in this exercise. The skill you're learning is called "refactoring", or as I like to call it, "fixing stuff". Refactoring is a term programmers use to describe the process of taking old code, and changing it to have new features or just to clean it up. You've been doing this without even knowing it, as it's second nature to building software.
What you'll do in this part is take the ideas from Exercise 47 of a testable "map" of Rooms, and the game from Exercise 42, and combine them together to create a new game structure. It will have the same content, just "refactored" to have a better structure.
First step is to grab the code from ex47.rb and copy it to gothonweb/lib/map.rb and copy ex47_tests.rb file to gothonweb/test/test_map.rb and run the test suite again to make sure it keeps working.
Note
From now on I won't show you the output of a test run, just assume that you should be doing it and it'll look like the above unless you have an error.
Once you have the code from Exercise 47 copied over, it's time to refactor it to have the Exercise 42 map in it. I'm going to start off by laying down the basic structure, and then you'll have an assignment to make the map.rb file and the map_tests.rb file complete.
First thing to do is lay out the basic structure of the map using the Room class as it is now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
class
Room
attr_accessor
:name
,
:description
,
:paths
def
initialize
(
name
,
description
)
@name
=
name
@description
=
description
@paths
=
{}
end
def
go
(
direction
)
@paths
[
direction
]
end
def
add_paths
(
paths
)
@paths
.
update
(
paths
)
end
end
central_corridor
=
Room
.
new
(
"Central Corridor"
,
%q{
The Gothons of Planet Percal #25 have invaded your ship and destroyed
your entire crew. You are the last surviving member and your last
mission is to get the neutron destruct bomb from the Weapons Armory,
put it in the bridge, and blow the ship up after getting into an
escape pod.
 He's blocking the door to the
Armory and about to pull a weapon to blast you.
}
)
laser_weapon_armory
=
Room
.
new
(
"Laser Weapon Armory"
,
%q{
Lucky for you they made you learn Gothon insults in the academy.

The Gothon stops, tries not to laugh, then busts out laughing and can't move.

You do a dive roll into the Weapon Armory, crouch and scan the room
for more Gothons that might be hiding. It's dead quiet, too quiet.
You stand up and run to the far side of the room and find the
neutron bomb in its container. There's a keypad lock on the box
and you need the code to get the bomb out. If you get the code
wrong 10 times then the lock closes forever and you can't
get the bomb. The code is 3 digits.
}
)
the_bridge
=
Room
.
new
(
"The Bridge"
,
%q{
The container clicks open and the seal breaks, letting gas out.
You grab the neutron bomb and run as fast as you can to the
bridge where you must place it in the right spot.
You burst onto the Bridge with the netron destruct bomb
under your arm and surprise 5 Gothons who are trying to
take control of the ship. Each of them has an even uglier
clown costume than the last. They haven't pulled their
weapons out yet, as they see the active bomb under your
arm and don't want to set it off.
}
)
escape_pod
=
Room
.
new
(
"Escape Pod"
,
%q{
You point your blaster at the bomb under your arm
and the Gothons put their hands up and start to sweat.
You inch backward to the door, open it, and then carefully
place the bomb on the floor, pointing your blaster at it.
You then jump back through the door, punch the close button
and blast the lock so the Gothons can't get out.
Now that the bomb is placed you run to the escape pod to
get off this tin can.
You rush through the ship desperately trying to make it to
the escape pod before the whole ship explodes. It seems like
hardly any Gothons are on the ship, so your run is clear of
interference. You get to the chamber with the escape pods, and
now need to pick one to take. Some of them could be damaged
but you don't have time to look. There's 5 pods, which one
do you take?
}
)
the_end_winner
=
Room
.
new
(
"The End"
,
%q{
You jump into pod 2 and hit the eject button.
The pod easily slides out into space heading to
the planet below. As it flies to the planet, you look
back and see your ship implode then explode like a
bright star, taking out the Gothon ship at the same
time. You won!
}
)
the_end_loser
=
Room
.
new
(
"The End"
,
%q{
You jump into a random pod and hit the eject button.
The pod escapes out into the void of space, then
implodes as the hull ruptures, crushing your body
into jam jelly.
}
)
escape_pod
.
add_paths
({
'2'
=>
the_end_winner
,
'*'
=>
the_end_loser
})
generic_death
=
Room
.
new
(
"death"
,
"You died."
)
the_bridge
.
add_paths
({
'throw the bomb'
=>
generic_death
,
'slowly place the bomb'
=>
escape_pod
})
laser_weapon_armory
.
add_paths
({
'0132'
=>
the_bridge
,
'*'
=>
generic_death
})
central_corridor
.
add_paths
({
'shoot!'
=>
generic_death
,
'dodge!'
=>
generic_death
,
'tell a joke'
=>
laser_weapon_armory
})
START
=
central_corridor
|
You'll notice that there are a couple of problems with our Room class and this map:
Once you've got that basically written out, here's the new automated test test/test_map.rb that you should have to get yourself started:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
require
'test/unit'
require_relative
'../lib/map'
class
MapTests
<
Test
::
Unit
::
TestCase
def
test_room
()
gold
=
Room
.
new
(
"GoldRoom"
,
%q{This room has gold in it you can grab. There's a
door to the north.}
)
assert_equal
(
gold
.
name
,
"GoldRoom"
)
assert_equal
(
gold
.
paths
,
{})
end
def
test_room_paths
()
center
=
Room
.
new
(
"Center"
,
"Test room in the center."
)
north
=
Room
.
new
(
"North"
,
"Test room in the north."
)
south
=
Room
.
new
(
"South"
,
"Test room in the south."
)
center
.
add_paths
({
'north'
=>
north
,
'south'
=>
south
})
assert_equal
(
center
.
go
(
'north'
),
north
)
assert_equal
(
center
.
go
(
'south'
),
south
)
end
def
test_map
()
start
=
Room
.
new
(
"Start"
,
"You can go west and down a hole."
)
west
=
Room
.
new
(
"Trees"
,
"There are trees here, you can go east."
)
down
=
Room
.
new
(
"Dungeon"
,
"It's dark down here, you can go up."
)
start
.
add_paths
({
'west'
=>
west
,
'down'
=>
down
})
west
.
add_paths
({
'east'
=>
start
})
down
.
add_paths
({
'up'
=>
start
})
assert_equal
(
start
.
go
(
'west'
),
west
)
assert_equal
(
start
.
go
(
'west'
)
.
go
(
'east'
),
start
)
assert_equal
(
start
.
go
(
'down'
)
.
go
(
'up'
),
start
)
end
def
test_gothon_game_map
()
assert_equal
(
START
.
go
(
'shoot!'
),
generic_death
)
assert_equal
(
START
.
go
(
'dodge!'
),
generic_death
)
room
=
START
.
go
(
'tell a joke'
)
assert_equal
(
room
,
laser_weapon_armory
)
end
end
|
Your task in this part of the exercise is to complete the map, and make the automated test completely validate the whole map. This includes fixing all the generic_death objects to be real endings. Make sure this works really well and that your test is as complete as possible because we'll be changing this map later and you'll use the tests to make sure it keeps working.
At a certain point in your web application you'll need to keep track of some information and associate it with the user's browser. The web (because of HTTP) is what we like to call "stateless", which means each request you make is independent of any other requests being made. If you request page A, put in some data, and click a link to page B, all the data you sent to page A just disappears.
The solution to this is to create a little data store (usually in a database, on disk, or in cookies) that uses a number unique to each browser to keep track of what that browser was doing. In Sinatra it's fairly easy, and here's an example showing how it's done using Rack middleware:
require
'rubygems'
require
'sinatra'
use
Rack
::
Session
::
Pool
get
'/count'
do
session
[
:count
]
||=
0
session
[
:count
]
+=
1
"Count:
#{
session
[
:count
]
}
"
end
get
'/reset'
do
session
.
clear
"Count reset to 0."
end
You should have your game map working and a good unit test for it. I now want to make a simple little game engine that will run the rooms, collect input from the player, and keep track of where a play is in the game.  The catch is I'm going to make a very simple one with basic HTML files, and it'll be up to you to complete it. Here's the base engine:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
require_relative
"gothonweb/version"
require_relative
"map"
require
"sinatra"
require
"erb"
module
Gothonweb
use
Rack
::
Session
::
Pool
get
'/'
do
# this is used to "setup" the session with starting values
p
START
session
[
:room
]
=
START
redirect
(
"/game"
)
end
get
'/game'
do
if
session
[
:room
]
erb
:show_room
,
:locals
=>
{
:room
=>
session
[
:room
]
}
else
# why is this here? do you need it?
erb
:you_died
end
end
post
'/game'
do
action
=
"
#{
params
[
:action
]
||
nil
}
"
# there is a bug here, can you fix it?
if
session
[
:room
]
session
[
:room
]
=
session
[
:room
].
go
(
params
[
:action
]
)
end
redirect
(
"/game"
)
end
end
|
You should next delete lib/views/hello_form.erb and lib/views/index.erb and create the two templates mentioned in the above code. Here's a very simple lib/views/show_room.erb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |

<%=
room
.
name
%>

<%=
room
.
description
%>

<%
if
room
.
name
==
"death"
%>

Play Again?

<%
else
%>

<form action="/game" method="POST">
- <input type="text" name="action"> <input type="SUBMIT">
</form>

<%
end
%>
|
That is the template to show a room as you travel through the game. Next you need one to tell someone they died in the case that they got to the end of the map on accident, which is lib/views/you_died.erb:
1 2 3 4 |

You Died!

Looks like you bit the dust.

Play Again

|
With those in place, you should now be able to do the following:
Do you feel like this was a huge amount of information thrown at you all at once? Good, I want you to have something to tinker with while you build your skills.  You'll notice that what you've written so far isn't very well built, it is just a first version of the code. Your task now is to make the game more complete by doing these things:
Good luck!
You've finished this book and have decided to continue with programming. Maybe it will be a career for you, or maybe it will be a hobby. You'll need some advice to make sure you continue on the right path, and get the most enjoyment out of your newly chosen activity.
I've been programming for a very long time. So long that it's incredibly boring to me. At the time that I wrote this book, I knew about 20 programming languages and could learn new ones in about a day to a week depending on how weird they were. Eventually though this just became boring and couldn't hold my interest anymore. This doesn't mean I think programming is boring, or that you will think it's boring, only that I find it uninteresting at this point in my journey.
What I discovered after this journey of learning is that it's not the languages that matter but what you do with them. Actually, I always knew that, but I'd get distracted by the languages and forget it periodically. Now I never forget it, and neither should you.
Which programming language you learn and use doesn't matter. Do not get sucked into the religion surrounding programming languages as that will only blind you to their true purpose of being your tool for doing interesting things.
Programming as an intellectual activity is the only art form that allows you to create interactive art. You can create projects that other people can play with, and you can talk to them indirectly. No other art form is quite this interactive. Movies flow to the audience in one direction. Paintings do not move. Code goes both ways.
Programming as a profession is only moderately interesting. It can be a good job, but you could make about the same money and be happier running a fast food joint. You're much better off using code as your secret weapon in another profession.
 People who can code in biology, medicine, government, sociology, physics, history, and mathematics are respected and can do amazing things to advance those disciplines.
Of course, all of this advice is pointless. If you liked learning to write software with this book, you should try to use it to improve your life any way you can. Go out and explore this weird wonderful new intellectual pursuit that barely anyone in the last 50 years has been able to explore. Might as well enjoy it while you can.
Finally, I'll say that learning to create software changes you and makes you different. Not better or worse, just different. You may find that people treat you harshly because you can create software, maybe using words like "nerd". Maybe you'll find that because you can dissect their logic that they hate arguing with you. You may even find that simply knowing how a computer works makes you annoying and weird to them.
 The world needs more weird people who know how things work and who love to figure it all out. When they treat you like this, just remember that this is your journey, not theirs. Being different is not a crime, and people who tell you it is are just jealous that you've picked up a skill they never in their wildest dreams could acquire.
You can code. They cannot. That is pretty damn cool.
This simple book is meant to get you started in programming. The title says it's the hard way to learn to write code; but it's actually not. It's only the "hard" way because it's the way people used to teach things. With the help of this book, you will do the incredibly simple things that all programmers need to do to learn a language:
That's it. This will be very difficult at first, but stick with it. If you go through this book, and do each exercise for one or two hours a night, you will have a good foundation for moving onto another book. You might not really learn "programming" from this book, but you will learn the foundation skills you need to start learning the language.
This book's job is to teach you the three most essential skills that a beginning programmer needs to know: Reading and Writing, Attention to Detail, Spotting Differences.
It seems stupidly obvious, but, if you have a problem typing, you will have a problem learning to code. Especially if you have a problem typing the fairly odd characters in source code. Without this simple skill you will be unable to learn even the most basic things about how software works.
Typing the code samples and getting them to run will help you learn the names of the symbols, get familiar with typing them, and get you reading the language.
The one skill that separates bad programmers from good programmers is attention to detail. In fact, it's what separates the good from the bad in any profession. Without paying attention to the tiniest details of your work, you will miss key elements of what you create. In programming, this is how you end up with bugs and difficult-to-use systems.
By going through this book, and copying each example exactly , you will be training your brain to focus on the details of what you are doing, as you are doing it.
A very important skill -- that most programmers develop over time -- is the ability to visually notice differences between things. An experienced programmer can take two pieces of code that are slightly different and immediately start pointing out the differences. Programmers have invented tools to make this even easier, but we won't be using any of these. You first have to train your brain the hard way, then you can use the tools.
While you do these exercises, typing each one in, you will be making mistakes. It's inevitable; even seasoned programmers would make a few. Your job is to compare what you have written to what's required, and fix all the differences. By doing so, you will train yourself to notice mistakes, bugs, and other problems.
You must type each of these exercises in, manually. If you copy and paste, you might as well just not even do them. The point of these exercises is to train your hands, your brain, and your mind in how to read, write, and see code. If you copy-paste, you are cheating yourself out of the effectiveness of the lessons.
While you are studying programming, I'm studying how to play guitar. I practice it every day for at least 2 hours a day. I play scales, chords, and arpeggios for an hour at least and then learn music theory, ear training, songs and anything else I can. Some days I study guitar and music for 8 hours because I feel like it and it's fun. To me repetitive practice is natural and just how to learn something.  Keep trying and eventually it'll be easier and fun.
As you study this book, and continue with programming, remember that anything worth doing is difficult at first. Maybe you are the kind of person who is afraid of failure so you give up at the first sign of difficulty. Maybe you never learned self-discipline so you can't do anything that's "boring".  Maybe you are competitive and unfairly compare yourself to someone like me who's been programming for 20+ years.
Whatever your reason for wanting to quit, keep at it. Force yourself. If you run into an Extra Credit you can't do, or a lesson you just do not understand, then skip it and come back to it later. Just keep going because with programming there's this very odd thing that happens.
At first, you will not understand anything. It'll be weird, just like with learning any human language. You will struggle with words, and not know what symbols are what, and it'll all be very confusing.  If you keep doing the exercises and keep trying to understand them, you will get it. You might not be a master coder, but you will at least understand how programming works.
If you give up, you won't ever reach this point. You will hit the first confusing thing (which is everything at first) and then stop. If you keep trying, keep typing it in, trying to understand it and reading about it, you will eventually get it.
But, if you go through this whole book, and you still do not understand how to code, at least you gave it a shot. You can say you tried your best and a little more and it didn't work out, but at least you tried. You can be proud of that.
This book is Copyright (C) 2011 by Zed A. Shaw. You are free to distribute this book to anyone you want, so long as you do not charge anything for it, and it is not altered. You must give away the book in its entirety, or not at all. This means it's alright for you to teach a class using the book, so long as you aren't charging students for the book and you give them the whole book unmodified.
I'd like to thank a few people who helped with this edition of the book. First is my editor at Pretty Girl Editing Services who helped me edit the book and is just lovely all by herself. Then there's Greg Newman, who did the cover jacket and artwork, plus reviewed copies of the book. His artwork made the book look like a real book, and didn't mind that I totally forgot to give him credit in the first edition. I'd also like to thank Brian Shumate for doing the website landing page and other site design help, which I need a lot of help on.
Finally, I'd like to thank the hundreds of thousands of people who read the first edition and especially the ones who submitted bug reports and comments to improve the book. It really made this edition solid and I couldn't have done it without all of you. Thank you.
You're not a programmer quite yet. I like to think of this book as giving you your "programming brown belt". You know enough to start another book on programming and handle it just fine. This book should have given you the mental tools and attitude you need to go through most Ruby books and actually learn something. It might even make it easy.
Rob says: For fun, I recommend you check out Why's (Poignant) Guide to Ruby: http://mislav.uniqpath.com/poignant-guide Most of the actual programming content will be review by now, but Why is a brilliant mind and his book is a work of art. Check out some of his open source projects, which are still floating around. You can learn a lot by reading his code.
You could probably start hacking away at some programs right now, and if you have that itch, go ahead. Just understand anything you write will probably suck.
Finally, remember that this is something you have to do at least a couple hours a night for a while before you can get good. If it helps, while you're struggling to learn Ruby every night, I'm hard at work learning to play guitar. I work at it about 2 or 4 hours a day and still practice scales.
Everyone is a beginner at something.


--- date: "2019-08-21T11:00:29+02:00" title: Time analyses weight: 13 ---
The next section of the template produces analyses and outputs related to time, such as epidemic curves.
The first half of this code chunk (epicurve
) is about setting-up the data for the epidemic curves.
First, the function incidence()
counts the number of cases for each week, by date_of_onset
. It stores this information in the object inc_week_7
. Read more about incidence()
in the Help pane.
Second, the theme elements are set that ggplot()
will use when plotting the epidemic curves (you can read more about ggplot()
and theme elements in the Advanced R Basics page). This includes the angle and placement of x-axis label text, having no title in the legend, and setting colors and type for x and y gridlines.
Third, the text labels for the x and y axes, title, and subtitle (subtitle automatically using the reporting_week
defined at the top of the template). These are stored in the object epicurve_labels
.
{{% notice tip %}}
It is necessary to use the “full name” of variables for this to work correctly - this means date_of_onset should be listed in incidence() as linelist_cleaned$date_of_onset
.
{{% /notice %}}
# This code creates case counts for each week of your outbreak, overall
# As with aweek, you can change the start of your week to e.g. "Sunday week"
inc_week_7 <- incidence(linelist_cleaned$date_of_onset, interval = "Monday week")
# this sets the theme in ggplot for epicurves
epicurve_theme <- theme(
axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1),
legend.title = element_blank(),
panel.grid.major.x = element_line(color = "grey60", linetype = 3),
panel.grid.major.y = element_line(color = "grey60", linetype = 3)
)
# This sets the labels in ggplot for the epicurves
epicurve_labels <- labs(x = "Calendar week",
y = "Cases (n)",
title = "Cases by week of onset",
subtitle = str_glue("Source: MSF data from {reporting_week}")
)
In this next part of the chunk, the plot of the epidemic curve is finalized and displayed.
{{% notice tip %}}
The plot()
function used here is from the package incidence, not the base R plot()
function. This one uses ggplot()
and so has different syntax and structure. As with ggplot()
, layers of the graphic are added sequentially using the +
symbol at the end of each sub-command.
{{% /notice %}}
In the example below, the weekly count data from inc_week_7
are plotted first, then miscellaneous settings are specified: the y-axis origin is set, the labels, and finally the theme parameters. All of these elements are stored in the object basic_curve
, which is finally run to display the plot.
{{% notice tip %}}
For some functions, it is necessary to use the “full name” of variables - for example when using incidence()
, date_of_onset
should be listed as linelist_cleaned$date_of_onset
. Otherwise you may get an error such as “object ‘date_of_onset’ not found”.
{{% /notice %}}
# plot your epicurve as a ggplot (incidence::plot is different to base::plot)
basic_curve <- plot(inc_week_7, show_cases = TRUE, border = "black", n_breaks = nrow(inc_week_7)) +
scale_y_continuous(expand = c(0, 0)) + # set origin for axes
# add labels to axes and below chart
epicurve_labels +
# change visuals of dates and remove legend title
epicurve_theme
# show your plot (stored for later use)
basic_curve
This x-axis is difficult to read with every week displayed. To increase readability we can uncomment and use the helper code at the bottom of that code chunk. This modifies the already-defined basic_curve
object (with a +
) to specify that the x-axis shows only a specified number of breaks/dates (in this case 6). Note: A warning that a scale for x is being replaced is okay.
{{% notice tip %}} The data are still shown by week - it is only the labels on the scale that have changed. {{% /notice %}}
# Modifies basic_curve to show only 6 breaks in the x-axis
basic_curve + scale_x_incidence(inc_week_7, n_breaks = 6)
To change the intervals used for the data aggregation themselves, use the interval =
argument in the original incidence()
command. There are the options to do this biweekly, by month, quarter or year (search the Help pane for the incidence()
function in the incidence package). Below is a biweekly example (the axes labels have been returned to default - showing each two-week increment).
{{% notice tip %}}
In the code below, the plot is not saved to an object and simply prints when the plot()
command and it’s various added (+
) elements such as scale, labels, and theme are run.
{{% /notice %}}
# interval is set to "2 Monday weeks" for biweekly case aggregation
inc_week_14 <- incidence(linelist_cleaned$date_of_onset, interval = "2 Monday weeks")
# plot the epicurve
plot(inc_week_14, show_cases = TRUE, border = "black", n_breaks = nrow(inc_week_14)) +
scale_y_continuous(expand = c(0,0)) + # set origin for axes
# add labels to axes and below chart
epicurve_labels +
# change visuals of dates and remove legend title
epicurve_theme
To stratify the epidemic curve by a categorical variable (such as gender), the groups =
argument is added to the incidence()
function. Additionally, the show_cases =
argument of plot()
can be set to FALSE
to improve aesthetics.
{{% notice tip %}}
Remember that if listing a variable, the incidence()
function expects its “full name”, such as linelist_cleaned$sex
**. Otherwise you may get an error such as “object ‘sex’ not found”.
{{% /notice %}}
# get counts by gender
inc_week_7_gender <- incidence(linelist_cleaned$date_of_onset,
interval = "2 Monday weeks",
groups = linelist_cleaned$sex)
# plot your epicurve
# here we remove the boxes around each case as it makes gender colours hard to see! (show_cases = FALSE)
plot(inc_week_7_gender, show_cases = FALSE, border = "black", n_breaks = nrow(inc_week_7_gender)) +
scale_y_continuous(expand = c(0,0)) + # set origin for axes
# add labels to axes and below chart
epicurve_labels +
# change visuals of dates, remove legend title and move legend to bottom
epicurve_theme
Lastly, we can limit the entire plot to a subset of observations by first filtering the dataset and passing the result to the incidence()
function.
In the example below, incidence()
operates upon a filtered version of linelist_cleaned
that only includes inpatients. The resulting object is used, as before, in plot()
in the step below.
# filter the dataset and pass it to the incidence() function
inc_week_7_sex_fac <- linelist_cleaned %>%
filter(patient_facility_type == "Inpatient") %>%
with(incidence(date_of_onset, interval = "2 Monday weeks", groups = sex))
# plot as before
plot(inc_week_7_sex_fac, show_cases = TRUE, border = "black", n_breaks = nrow(inc_week_7_sex_fac)) +
scale_y_continuous(expand = c(0, 0)) + # set origin for axes
# add labels to axes and below chart
epicurve_labels +
# change visuals of dates, remove legend title and move legend to bottom
epicurve_theme
The code below produces a table of the attack rates by epiweek.
First, the object cases
is defined as linelist observations counted and arranged/ordered by epiweek, accompanied by a cumulative sum column.
Second, the object ar
is defined using the attack_rate()
function, which looks with cases
at counts for each week (column “n”) and the denominator population
, which was defined earlier as the sum of the regional populations (62336). The rate multiplier is set to 10000 but is changeable. The epiweeks are also attached to ar
as a column.
Third, aesthetic operations are done to ar
to improve column names. Finally, the table is produced with kable()
.
# counts and cumulative counts by week
cases <- linelist_cleaned %>%
arrange(date_of_onset) %>% # arrange by date of onset
count(epiweek, .drop = FALSE) %>% # count all epiweeks and include zero counts
mutate(cumulative = cumsum(n)) # add a cumulative sum
# attack rate for each week
ar <- attack_rate(cases$n, population, multiplier = 10000) %>%
bind_cols(select(cases, epiweek), .) # add the epiweek column to table
# Display the table
ar %>%
merge_ci_df(e = 4) %>% # merge the lower and upper CI into one column
rename("Epiweek" = epiweek,
"Cases (n)" = cases,
"Population" = population,
"AR (per 10,000)" = ar,
"95%CI" = ci) %>%
knitr::kable(digits = 1, align = "r")
| Epiweek | Cases (n) | Population | AR (per 10,000) | 95%CI |
|---|---|---|---|---|
| 2016-W23 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W24 | 0 | 62336 | 0.0 | (0.00–0.62) |
| 2016-W25 | 0 | 62336 | 0.0 | (0.00–0.62) |
| 2016-W26 | 0 | 62336 | 0.0 | (0.00–0.62) |
| 2016-W27 | 0 | 62336 | 0.0 | (0.00–0.62) |
| 2016-W28 | 0 | 62336 | 0.0 | (0.00–0.62) |
| 2016-W29 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W30 | 2 | 62336 | 0.3 | (0.09–1.17) |
| 2016-W31 | 0 | 62336 | 0.0 | (0.00–0.62) |
| 2016-W32 | 0 | 62336 | 0.0 | (0.00–0.62) |
| 2016-W33 | 3 | 62336 | 0.5 | (0.16–1.42) |
| 2016-W34 | 7 | 62336 | 1.1 | (0.54–2.32) |
| 2016-W35 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W36 | 2 | 62336 | 0.3 | (0.09–1.17) |
| 2016-W37 | 17 | 62336 | 2.7 | (1.70–4.37) |
| 2016-W38 | 12 | 62336 | 1.9 | (1.10–3.36) |
| 2016-W39 | 9 | 62336 | 1.4 | (0.76–2.74) |
| 2016-W40 | 10 | 62336 | 1.6 | (0.87–2.95) |
| 2016-W41 | 3 | 62336 | 0.5 | (0.16–1.42) |
| 2016-W42 | 5 | 62336 | 0.8 | (0.34–1.88) |
| 2016-W43 | 5 | 62336 | 0.8 | (0.34–1.88) |
| 2016-W44 | 3 | 62336 | 0.5 | (0.16–1.42) |
| 2016-W45 | 5 | 62336 | 0.8 | (0.34–1.88) |
| 2016-W46 | 12 | 62336 | 1.9 | (1.10–3.36) |
| 2016-W47 | 13 | 62336 | 2.1 | (1.22–3.57) |
| 2016-W48 | 23 | 62336 | 3.7 | (2.46–5.54) |
| 2016-W49 | 19 | 62336 | 3.0 | (1.95–4.76) |
| 2016-W50 | 35 | 62336 | 5.6 | (4.04–7.81) |
| 2016-W51 | 22 | 62336 | 3.5 | (2.33–5.34) |
| 2016-W52 | 31 | 62336 | 5.0 | (3.50–7.06) |
| 2017-W01 | 16 | 62336 | 2.6 | (1.58–4.17) |
| 2017-W02 | 22 | 62336 | 3.5 | (2.33–5.34) |
| 2017-W03 | 14 | 62336 | 2.2 | (1.34–3.77) |
| 2017-W04 | 20 | 62336 | 3.2 | (2.08–4.96) |
| 2017-W05 | 16 | 62336 | 2.6 | (1.58–4.17) |
| 2017-W06 | 118 | 62336 | 18.9 | (15.81–22.66) |
| 2017-W07 | 20 | 62336 | 3.2 | (2.08–4.96) |
| 2017-W08 | 41 | 62336 | 6.6 | (4.85–8.92) |
| 2017-W09 | 15 | 62336 | 2.4 | (1.46–3.97) |
| 2017-W10 | 39 | 62336 | 6.3 | (4.58–8.55) |
| 2017-W11 | 8 | 62336 | 1.3 | (0.65–2.53) |
| 2017-W12 | 14 | 62336 | 2.2 | (1.34–3.77) |
| 2017-W13 | 8 | 62336 | 1.3 | (0.65–2.53) |
| 2017-W14 | 11 | 62336 | 1.8 | (0.99–3.16) |
| 2017-W15 | 3 | 62336 | 0.5 | (0.16–1.42) |
| 2017-W16 | 5 | 62336 | 0.8 | (0.34–1.88) |
| - | 825 | 62336 | 132.3 | (123.67–141.62) |
This code produces the cumulative attack rate, by week. In the attack_rate()
command it references the cases$cumulative
column, instead of cases$n column
.
# cumulative attack rate by week
attack_rate(cases$cumulative, population, multiplier = 10000) %>%
bind_cols(select(cases, epiweek), .) %>% # add the epiweek column to table
merge_ci_df(e = 4) %>% # merge the lower and upper CI into one column
rename("Epiweek" = epiweek,
"Cases (n)" = cases,
"Population" = population,
"AR (per 10,000)" = ar,
"95%CI" = ci) %>%
knitr::kable(digits = 1, align = "r")
| Epiweek | Cases (n) | Population | AR (per 10,000) | 95%CI |
|---|---|---|---|---|
| 2016-W23 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W24 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W25 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W26 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W27 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W28 | 1 | 62336 | 0.2 | (0.03–0.91) |
| 2016-W29 | 2 | 62336 | 0.3 | (0.09–1.17) |
| 2016-W30 | 4 | 62336 | 0.6 | (0.25–1.65) |
| 2016-W31 | 4 | 62336 | 0.6 | (0.25–1.65) |
| 2016-W32 | 4 | 62336 | 0.6 | (0.25–1.65) |
| 2016-W33 | 7 | 62336 | 1.1 | (0.54–2.32) |
| 2016-W34 | 14 | 62336 | 2.2 | (1.34–3.77) |
| 2016-W35 | 15 | 62336 | 2.4 | (1.46–3.97) |
| 2016-W36 | 17 | 62336 | 2.7 | (1.70–4.37) |
| 2016-W37 | 34 | 62336 | 5.5 | (3.90–7.62) |
| 2016-W38 | 46 | 62336 | 7.4 | (5.53–9.84) |
| 2016-W39 | 55 | 62336 | 8.8 | (6.78–11.48) |
| 2016-W40 | 65 | 62336 | 10.4 | (8.18–13.29) |
| 2016-W41 | 68 | 62336 | 10.9 | (8.61–13.83) |
| 2016-W42 | 73 | 62336 | 11.7 | (9.32–14.72) |
| 2016-W43 | 78 | 62336 | 12.5 | (10.03–15.61) |
| 2016-W44 | 81 | 62336 | 13.0 | (10.46–16.15) |
| 2016-W45 | 86 | 62336 | 13.8 | (11.17–17.03) |
| 2016-W46 | 98 | 62336 | 15.7 | (12.90–19.15) |
| 2016-W47 | 111 | 62336 | 17.8 | (14.79–21.44) |
| 2016-W48 | 134 | 62336 | 21.5 | (18.15–25.45) |
| 2016-W49 | 153 | 62336 | 24.5 | (20.95–28.75) |
| 2016-W50 | 188 | 62336 | 30.2 | (26.15–34.78) |
| 2016-W51 | 210 | 62336 | 33.7 | (29.44–38.55) |
| 2016-W52 | 241 | 62336 | 38.7 | (34.09–43.85) |
| 2017-W01 | 257 | 62336 | 41.2 | (36.49–46.57) |
| 2017-W02 | 279 | 62336 | 44.8 | (39.81–50.31) |
| 2017-W03 | 293 | 62336 | 47.0 | (41.93–52.69) |
| 2017-W04 | 313 | 62336 | 50.2 | (44.96–56.07) |
| 2017-W05 | 329 | 62336 | 52.8 | (47.39–58.78) |
| 2017-W06 | 447 | 62336 | 71.7 | (65.38–78.64) |
| 2017-W07 | 467 | 62336 | 74.9 | (68.44–82.00) |
| 2017-W08 | 508 | 62336 | 81.5 | (74.73–88.86) |
| 2017-W09 | 523 | 62336 | 83.9 | (77.04–91.37) |
| 2017-W10 | 562 | 62336 | 90.2 | (83.03–97.89) |
| 2017-W11 | 570 | 62336 | 91.4 | (84.26–99.22) |
| 2017-W12 | 584 | 62336 | 93.7 | (86.42–101.56) |
| 2017-W13 | 592 | 62336 | 95.0 | (87.65–102.89) |
| 2017-W14 | 603 | 62336 | 96.7 | (89.35–104.73) |
| 2017-W15 | 606 | 62336 | 97.2 | (89.81–105.23) |
| 2017-W16 | 611 | 62336 | 98.0 | (90.58–106.06) |
| - | 1436 | 62336 | 230.4 | (218.88–242.44) |
This next code chunk applies a filter for only inpatient observations. It also uses a different function, case_fatality_rate_df()
, to produce a table showing deaths, cases, CFR, and CFR confidence intervals.
# group by known outcome and case definition
cfr <- linelist_cleaned %>%
filter(patient_facility_type == "Inpatient") %>%
case_fatality_rate_df(grepl("Dead", exit_status), group = epiweek)
cfr %>%
merge_ci_df(e = 4) %>% # merge the lower and upper CI into one column
rename("Epiweek" = epiweek,
"Deaths" = deaths,
"Cases" = population,
"CFR (%)" = cfr,
"95%CI" = ci) %>%
knitr::kable(digits = 1, align = "r")
| Epiweek | Deaths | Cases | CFR (%) | 95%CI |
|---|---|---|---|---|
| 2016-W23 | 0 | 0 | - | (NaN–NaN) |
| 2016-W24 | 0 | 0 | - | (NaN–NaN) |
| 2016-W25 | 0 | 0 | - | (NaN–NaN) |
| 2016-W26 | 0 | 0 | - | (NaN–NaN) |
| 2016-W27 | 0 | 0 | - | (NaN–NaN) |
| 2016-W28 | 0 | 0 | - | (NaN–NaN) |
| 2016-W29 | 0 | 0 | - | (NaN–NaN) |
| 2016-W30 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W31 | 0 | 0 | - | (NaN–NaN) |
| 2016-W32 | 0 | 0 | - | (NaN–NaN) |
| 2016-W33 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W34 | 0 | 0 | - | (NaN–NaN) |
| 2016-W35 | 0 | 0 | - | (NaN–NaN) |
| 2016-W36 | 0 | 0 | - | (NaN–NaN) |
| 2016-W37 | 0 | 4 | 0 | (0.00–48.99) |
| 2016-W38 | 0 | 3 | 0 | (0.00–56.15) |
| 2016-W39 | 0 | 2 | 0 | (0.00–65.76) |
| 2016-W40 | 0 | 5 | 0 | (0.00–43.45) |
| 2016-W41 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W42 | 0 | 5 | 0 | (0.00–43.45) |
| 2016-W43 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W44 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W45 | 0 | 3 | 0 | (0.00–56.15) |
| 2016-W46 | 0 | 4 | 0 | (0.00–48.99) |
| 2016-W47 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W48 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W49 | 0 | 0 | - | (NaN–NaN) |
| 2016-W50 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W51 | 0 | 1 | 0 | (0.00–79.35) |
| 2016-W52 | 0 | 2 | 0 | (0.00–65.76) |
| 2017-W01 | 0 | 2 | 0 | (0.00–65.76) |
| 2017-W02 | 0 | 1 | 0 | (0.00–79.35) |
| 2017-W03 | 0 | 2 | 0 | (0.00–65.76) |
| 2017-W04 | 0 | 4 | 0 | (0.00–48.99) |
| 2017-W05 | 0 | 1 | 0 | (0.00–79.35) |
| 2017-W06 | 0 | 5 | 0 | (0.00–43.45) |
| 2017-W07 | 0 | 1 | 0 | (0.00–79.35) |
| 2017-W08 | 0 | 1 | 0 | (0.00–79.35) |
| 2017-W09 | 0 | 1 | 0 | (0.00–79.35) |
| 2017-W10 | 0 | 1 | 0 | (0.00–79.35) |
| 2017-W11 | 0 | 0 | - | (NaN–NaN) |
| 2017-W12 | 0 | 0 | - | (NaN–NaN) |
| 2017-W13 | 0 | 2 | 0 | (0.00–65.76) |
| 2017-W14 | 0 | 1 | 0 | (0.00–79.35) |
| 2017-W15 | 0 | 0 | - | (NaN–NaN) |
| 2017-W16 | 0 | 0 | - | (NaN–NaN) |
| (Missing) | 0 | 27 | 0 | (0.00–12.46) |
These code chunks produce plots of the AR (in the population), CFR (among inpatients only), and the AF and CFR as line graphs together with the Epidemic Curve.
The first chunk, ar_line_graph
, prepares a plot of attack rate by epiweek, but does not yet print it. You can print it alone by running a command of its name ar_plot
.
{{% notice tip %}}
This ar_plot
depends on you having already run the attack_rate_per_week
chunk. If you have not, you may see “Error: epiweek not found”
{{% /notice %}}
ar_plot <- ggplot(ar, aes(x = week2date(epiweek) + (7 * 0.5), group = 1)) +
geom_ribbon(aes(ymin = lower, ymax = upper),
color = "blue", fill = "blue", linetype = 2, alpha = 0.2, show.legend = FALSE) +
geom_line(aes(y = ar), color = "blue", show.legend = FALSE) +
scale_y_continuous(expand = c(0, 0)) + # set origin for axes
# scale the x axis the same as the incidence curve. Expand forces it to align.
incidence::scale_x_incidence(inc_week_7, n_breaks = nrow(inc_week_7), expand = c(0, 7 * 1.5)) +
# add labels to axes and below chart
labs(x = "Calendar week", y = "AR [95% CI]", subtitle = "Attack Rate (per 10,000)") +
# change visuals of dates and remove legend title
epicurve_theme
The second code chunk, cfr_line_graph
prepares a plot of CFR by epiweek, but also does not yet print it. You can print it alone by running a command with its name cfr_plot
.
{{% notice tip %}} This cfr_plot depends on you having already run the cfr_per_week chunk. If you have not, you may see “Error: epiweek not found”. {{% /notice %}}
cfr_plot <- ggplot(cfr, aes(x = week2date(epiweek) + (7 * 0.5), group = 1)) +
geom_ribbon(aes(ymin = lower, ymax = upper),
color = "red", fill = "red", linetype = 2, alpha = 0.2, show.legend = FALSE) +
geom_line(aes(y = cfr), color = "red", show.legend = FALSE) +
scale_y_continuous(expand = c(0, 0)) + # set origin for axes
# scale the x axis the same as the incidence curve. Expand forces it to align.
incidence::scale_x_incidence(inc_week_7, n_breaks = nrow(inc_week_7), expand = c(0, 7 * 1.5)) +
# add labels to axes and below chart
labs(x = "Calendar week", y = "CFR [95% CI]",
subtitle = "Case Fatality Ratio [95% CI] Among Inpatients") +
# change visuals of dates and remove legend title
epicurve_theme
Adding the AR and CFR plots with an Epidemic Curve (chunk epicurve_ar_cfr
) and plotting them all together is now possible with the function cowplot()
, which nicely combines different plots. You can read more about cowplot()
in the Advanced and Miscellaneous R Basics page.
# Define no specified axis text or title over the combined plot
nofx <- theme(axis.text.x = element_blank(),
axis.title.x = element_blank())
# Combine the three plots together
cowplot::plot_grid(
basic_curve + nofx,
ar_plot + nofx,
cfr_plot,
align = "v", # align plots vertically
axis = "lr", # only by their left and right margins
ncol = 1 # allow only one column
)
## Error in stop_if_not_aweek_string(x): aweek strings must match the pattern 'YYYY-Www-d'. The first incorrect string was: '(Missing)'
This next code chunk describes each epiweek’s admission by their case definition status. It uses tab_linelist()
again to look at linelist_cleaned
, with rows as epiweek
, and columns as case_def
, with row and column totals.
# get counts and props of admissions by epiweek and case definition
# include column and row totals
tab_linelist(linelist_cleaned, epiweek, strata = case_def,
col_total = TRUE,
row_total = TRUE) %>%
select(-variable) %>%
rename_redundant("%" = proportion) %>%
augment_redundant(" (n)" = " n$") %>%
kable(digits = 1)
| value | Confirmed (n) | % | Probable (n) | % | Suspected (n) | % | Missing (n) | % | Total |
|---|---|---|---|---|---|---|---|---|---|
| 2016-W23 | 1 | 1.4 | 0 | 0 | 0 | 0.0 | 0 | 0.0 | 1 |
| 2016-W29 | 0 | 0.0 | 0 | 0 | 1 | 0.2 | 0 | 0.0 | 1 |
| 2016-W30 | 1 | 1.4 | 0 | 0 | 0 | 0.0 | 1 | 3.6 | 2 |
| 2016-W33 | 1 | 1.4 | 1 | 25 | 1 | 0.2 | 0 | 0.0 | 3 |
| 2016-W34 | 0 | 0.0 | 0 | 0 | 2 | 0.4 | 5 | 17.9 | 7 |
| 2016-W35 | 0 | 0.0 | 0 | 0 | 1 | 0.2 | 0 | 0.0 | 1 |
| 2016-W36 | 1 | 1.4 | 0 | 0 | 1 | 0.2 | 0 | 0.0 | 2 |
| 2016-W37 | 9 | 12.9 | 0 | 0 | 8 | 1.6 | 0 | 0.0 | 17 |
| 2016-W38 | 5 | 7.1 | 0 | 0 | 5 | 1.0 | 2 | 7.1 | 12 |
| 2016-W39 | 2 | 2.9 | 0 | 0 | 3 | 0.6 | 4 | 14.3 | 9 |
| 2016-W40 | 4 | 5.7 | 0 | 0 | 3 | 0.6 | 3 | 10.7 | 10 |
| 2016-W41 | 1 | 1.4 | 0 | 0 | 0 | 0.0 | 2 | 7.1 | 3 |
| 2016-W42 | 2 | 2.9 | 0 | 0 | 3 | 0.6 | 0 | 0.0 | 5 |
| 2016-W43 | 1 | 1.4 | 0 | 0 | 3 | 0.6 | 1 | 3.6 | 5 |
| 2016-W44 | 0 | 0.0 | 0 | 0 | 3 | 0.6 | 0 | 0.0 | 3 |
| 2016-W45 | 2 | 2.9 | 0 | 0 | 3 | 0.6 | 0 | 0.0 | 5 |
| 2016-W46 | 2 | 2.9 | 0 | 0 | 8 | 1.6 | 2 | 7.1 | 12 |
| 2016-W47 | 0 | 0.0 | 0 | 0 | 10 | 2.0 | 3 | 10.7 | 13 |
| 2016-W48 | 2 | 2.9 | 0 | 0 | 20 | 3.9 | 1 | 3.6 | 23 |
| 2016-W49 | 0 | 0.0 | 1 | 25 | 18 | 3.5 | 0 | 0.0 | 19 |
| 2016-W50 | 2 | 2.9 | 0 | 0 | 30 | 5.9 | 3 | 10.7 | 35 |
| 2016-W51 | 3 | 4.3 | 0 | 0 | 19 | 3.7 | 0 | 0.0 | 22 |
| 2016-W52 | 4 | 5.7 | 0 | 0 | 26 | 5.1 | 1 | 3.6 | 31 |
| 2017-W01 | 3 | 4.3 | 0 | 0 | 13 | 2.6 | 0 | 0.0 | 16 |
| 2017-W02 | 1 | 1.4 | 0 | 0 | 21 | 4.1 | 0 | 0.0 | 22 |
| 2017-W03 | 1 | 1.4 | 1 | 25 | 12 | 2.4 | 0 | 0.0 | 14 |
| 2017-W04 | 3 | 4.3 | 1 | 25 | 16 | 3.1 | 0 | 0.0 | 20 |
| 2017-W05 | 3 | 4.3 | 0 | 0 | 13 | 2.6 | 0 | 0.0 | 16 |
| 2017-W06 | 9 | 12.9 | 0 | 0 | 109 | 21.4 | 0 | 0.0 | 118 |
| 2017-W07 | 1 | 1.4 | 0 | 0 | 19 | 3.7 | 0 | 0.0 | 20 |
| 2017-W08 | 3 | 4.3 | 0 | 0 | 38 | 7.5 | 0 | 0.0 | 41 |
| 2017-W09 | 0 | 0.0 | 0 | 0 | 15 | 2.9 | 0 | 0.0 | 15 |
| 2017-W10 | 1 | 1.4 | 0 | 0 | 38 | 7.5 | 0 | 0.0 | 39 |
| 2017-W11 | 0 | 0.0 | 0 | 0 | 8 | 1.6 | 0 | 0.0 | 8 |
| 2017-W12 | 1 | 1.4 | 0 | 0 | 13 | 2.6 | 0 | 0.0 | 14 |
| 2017-W13 | 1 | 1.4 | 0 | 0 | 7 | 1.4 | 0 | 0.0 | 8 |
| 2017-W14 | 0 | 0.0 | 0 | 0 | 11 | 2.2 | 0 | 0.0 | 11 |
| 2017-W15 | 0 | 0.0 | 0 | 0 | 3 | 0.6 | 0 | 0.0 | 3 |
| 2017-W16 | 0 | 0.0 | 0 | 0 | 5 | 1.0 | 0 | 0.0 | 5 |
| Total | 70 | 100.0 | 4 | 100 | 509 | 100.0 | 28 | 100.0 | 611 |
Finally, this produces the same table as above but uses exit_status2
as the strata/column variable to display exits per epiweek. Note that this is also filtered to only include inpatient observations.
# get counts and props of admissions by epiweek among inpatients
# include column and row totals
linelist_cleaned %>%
filter(patient_facility_type == "Inpatient") %>%
tab_linelist(epiweek, strata = exit_status2,
col_total = TRUE, row_total = TRUE) %>%
select(-variable) %>%
rename("Week" = value) %>%
rename_redundant("%" = proportion) %>%
augment_redundant(" (n)" = " n$") %>%
kable(digits = 1)
| Week | Dead (n) | % | Discharged (n) | % | Left (n) | % | Missing (n) | % | Total |
|---|---|---|---|---|---|---|---|---|---|
| 2016-W30 | 0 | 0.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 1 |
| 2016-W33 | 0 | 0.0 | 1 | 2.9 | 0 | 0 | 0 | 0.0 | 1 |
| 2016-W37 | 0 | 0.0 | 2 | 5.7 | 0 | 0 | 2 | 14.3 | 4 |
| 2016-W38 | 1 | 12.5 | 2 | 5.7 | 0 | 0 | 0 | 0.0 | 3 |
| 2016-W39 | 0 | 0.0 | 2 | 5.7 | 0 | 0 | 0 | 0.0 | 2 |
| 2016-W40 | 1 | 12.5 | 4 | 11.4 | 0 | 0 | 0 | 0.0 | 5 |
| 2016-W41 | 0 | 0.0 | 0 | 0.0 | 1 | 50 | 0 | 0.0 | 1 |
| 2016-W42 | 2 | 25.0 | 1 | 2.9 | 0 | 0 | 2 | 14.3 | 5 |
| 2016-W43 | 0 | 0.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 1 |
| 2016-W44 | 0 | 0.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 1 |
| 2016-W45 | 2 | 25.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 3 |
| 2016-W46 | 0 | 0.0 | 3 | 8.6 | 0 | 0 | 1 | 7.1 | 4 |
| 2016-W47 | 0 | 0.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 1 |
| 2016-W48 | 0 | 0.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 1 |
| 2016-W50 | 0 | 0.0 | 1 | 2.9 | 0 | 0 | 0 | 0.0 | 1 |
| 2016-W51 | 0 | 0.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 1 |
| 2016-W52 | 1 | 12.5 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 2 |
| 2017-W01 | 0 | 0.0 | 1 | 2.9 | 1 | 50 | 0 | 0.0 | 2 |
| 2017-W02 | 0 | 0.0 | 1 | 2.9 | 0 | 0 | 0 | 0.0 | 1 |
| 2017-W03 | 0 | 0.0 | 2 | 5.7 | 0 | 0 | 0 | 0.0 | 2 |
| 2017-W04 | 0 | 0.0 | 4 | 11.4 | 0 | 0 | 0 | 0.0 | 4 |
| 2017-W05 | 0 | 0.0 | 0 | 0.0 | 0 | 0 | 1 | 7.1 | 1 |
| 2017-W06 | 0 | 0.0 | 5 | 14.3 | 0 | 0 | 0 | 0.0 | 5 |
| 2017-W07 | 1 | 12.5 | 0 | 0.0 | 0 | 0 | 0 | 0.0 | 1 |
| 2017-W08 | 0 | 0.0 | 1 | 2.9 | 0 | 0 | 0 | 0.0 | 1 |
| 2017-W09 | 0 | 0.0 | 1 | 2.9 | 0 | 0 | 0 | 0.0 | 1 |
| 2017-W10 | 0 | 0.0 | 1 | 2.9 | 0 | 0 | 0 | 0.0 | 1 |
| 2017-W13 | 0 | 0.0 | 2 | 5.7 | 0 | 0 | 0 | 0.0 | 2 |
| 2017-W14 | 0 | 0.0 | 1 | 2.9 | 0 | 0 | 0 | 0.0 | 1 |
| Total | 8 | 100.0 | 35 | 100.0 | 2 | 100 | 14 | 100.0 | 59 |


Direct to garment printing is a high quality digital print method that is essentially a giant computer printer, but instead printing on paper, it prints on garments. This process is best for complex designs with lots of different color tones or fine details, such as a photograph.
The process for DTG starts with treating the garment with a solution that helps the ink to adhere. This will sometimes leave a residue that will wash out after the first washing of the item.
DTG uses a water based ink that absorbs into the cotton of an item. This gives the feel of the print being in the garment rather than on the garment. For this reason DTG is best suited for items with a high cotton content. 100% cotton will yield the best results. We can produce high quality prints on anything that is a minimum of 50% cotton.
There are 4 pallet sizes that we use. All pallets have a ¼ inch no print zone around the perimeter making our maximum print sizes for each pallet ½ inch smaller than the pallet size.
The pallets:
- 7x8- this most often used for infant/toddler sized garments
- 10x12- this yields the highest quality hoodie front prints, and is used for youth sized shirts
- 14x16- This is our standard pallet for garments
- 16x18- This is our largest pallet and it is used for oversized prints
This method is not meant for sleeves, hoods, pants or hats but for areas that can fit seamlessly on the pallets. Printing across areas, such as front-to-sleeve can not be achieved with this method.
If a client is not using our graphic design team for art assistance, we ask that all designs be sent to us as print ready files.
What do we mean by print ready? This means that we should have no editing or adjustments to do on our end; every detail in the provided design is to be printed. For DTG printing, the best format for a print ready file is a high resolution (at least 300 DPI) png on a transparent background, that is cropped to the edges of the design.
There are no minimums for DTG Printing.
For more examples please check out our
instagram!


Money Play. How should Red play 11?
pips Red=136 White=141
(CorrectAnswer= 0/0, ErrorScore= 0)
Over the board I was seduced by the prospect of picking up the second White blot and so played bar/24, 3/2(2)*, 6/5.
This is not an unreasonable plan but 3/2(2)* means that Red holds two point six pips apart and that significantly damages his priming chances. Given that he ahead in the race the right plan turns out to be combining racing and priming chances and the correct play is bar/22, 6/5.
It goes to show just how difficult double ones can be to play. Even bar/23, 7/5 is an error as it gives White more attacking chances. In case you wondered bar/24, 7/4 is a double blunder.
Congratulations if you git this one correct for the right reasons.
This is not an unreasonable plan but 3/2(2)* means that Red holds two point six pips apart and that significantly damages his priming chances. Given that he ahead in the race the right plan turns out to be combining racing and priming chances and the correct play is bar/22, 6/5.
It goes to show just how difficult double ones can be to play. Even bar/23, 7/5 is an error as it gives White more attacking chances. In case you wondered bar/24, 7/4 is a double blunder.
Congratulations if you git this one correct for the right reasons.
1. Cubeful 2-ply bar/22 6/5 Eq.: +0.598 0.631 0.216 0.005 - 0.369 0.062 0.002 2. Cubeful 2-ply bar/23 7/5 Eq.: +0.569 (-0.029) 0.620 0.234 0.006 - 0.380 0.067 0.003 3. Cubeful 2-ply bar/24 6/5 3/2*(2) Eq.: +0.568 (-0.030) 0.615 0.271 0.006 - 0.385 0.064 0.002 4. Cubeful 2-ply bar/24 7/6 3/2*(2) Eq.: +0.553 (-0.046) 0.612 0.269 0.006 - 0.388 0.063 0.002 5. Cubeful 2-ply bar/22 7/6 Eq.: +0.549 (-0.049) 0.619 0.220 0.005 - 0.381 0.064 0.002


Namespaces in TactJS work about the way you'd expect them to. They allow you to create sections of related scripts and help organize your code-base. Thanks to the bootloader, fully qualified objects declared as behaviors can even be loaded asychronously when they're needed as long as the naming convention requirements in the file-system or script repository are met.
Creating and using a new namespace is as simple as declaring it as in the following example:
// Define a namespace $.types.ns("Docs.Demos");
Namespaces can be a single word, or multiple words separated by a '.' and they can be reused as often as you like so you can have many different classes under the same namespace.
If you want to be able to load your classes or behaviors asynchronously via the bootloader, your project's script folder must follow a certain pattern. It's pretty simple: the namespace should correspond with a location in your scripts folder such that the "Docs.Demos.Namespace.ClassName" object is located in the file at "_scriptsRoot/docs/demos/namespace/classname.js"
The bootloader defaults "_scriptsRoot" to "scripts/" but since it's a global variable, you can change it in the head of your document like so:
_scriptsRoot = "../scripts/";
For a working example of namespaces and the bootloader, download and explore our Quickstart Guide.


At appropriate doses, UV-C radiation is an effective technique for decontaminating surfaces from viruses and bacteria.
While we do not reccomend re-using single-use personal protective equipment (PPE) in general, the current COVID-19 pandemic has caused
serious shortages. Decontaminating and re-using PPE is most likely substantially safer than either not wearing PPE or re-using
PPE without decontamination. To this end, hospitals have begun using UV-C as an approach to decontaminating personal protective equipment.
However, every hospital has different UV lights available and differently-sized rooms in which to use them.
This website allows you to model placing UV-C lights in different positions within a room.
Cautions:
We have not yet validated this calculator via comparison to a real set up. Use at your own risk.
We are currently modeling UV lights as point sources. This assumption should hold within a certian slice of vertical space within a room, at certain distances from the lights. (we're working on quantifying these ranges)
The accuracy of this calculator depends on the accuracy of the information you put in about your UV lamps.
UV light bulbs lose intensity as they age. Ideally, the UV output of each bulb should be measured regularly. If this is not possible, care should be taken to use new bulbs.
This calculator should only be used with UV-C lights, as opposed to UV-A or UV-B lights. Only UV-C radiation has been shown to be effective at killing pathogens.
UV-C radiation is harmful to humans. People should not be in a UV decontamination room while the UV lights are on.
UV-C radiation can eventually break down materials used in PPE. The number of decontamination cycles each piece of PPE has been through should be tracked, and any PPE showing excessive degradation should be discarded. In particular, N95 masks should be regularly fit-tested.
How to use:
The heatmap shows the number of minutes an item would need to be left at each x,y position in the room in order to recieve the specified target dose of UV-C radiation. Regions colored black would take more than the specified maximum allowable time to recieve the specified dose.
To add a UV light, either click on a spot on the heatmap or use the "Create New Lamp" form on the right (fill in the lamp data and click "Add lamp with these settings.").
To change the settings of an existing light, right click on the circle representing that lamp on the heat map, enter the desired new settings, and click "Save changes".
The x and y coordinates of a light can also be changed by clicking and dragging it.
Lights can be removed by clicking on them. All lights can be simultaneously removed by clicking the "Remove all lamps" button.
Features coming soon:
More (i.e. any) options for units of measurement.
If you have questions or want to know more, [contact info goes here]


Symptoms
Find Clinic
Track Recovery
Profile
John Doe
23
Moderate
1
Sprained ankles can be treated to some extent at home however for a more effective and quicker recovery we recommend you see a professional therapist.
Home treatment starts by applying the PRICE principles (proteciton, rest, ice, compression and elevation). Apply a cold therapy and compression wrap to reduce pain, inflammation and swelling. Ice or cold therapy can be applied for 10 mins every hour initially reducing frequency as symptoms improve. Wear an ankle support to protect the injured ligaments and provice compression.
Taping is another treatment that can be used to both protect the injured ankle but also improve confidence following such an injury. Here at sportsinjuryclinic.net we show you simple steps on how to tape your ankle for both the recovery period and for when you return to sport.
This is then followed by a program of mobilising, strengthening and finally functional exercises. For a comprehensive rehabilitation program, we have compiled a step-by-step ankle sprain rehabilitation program that takes you from the initial injury through to full fitness.
One of the most important parts of ankle injury rehabilitation and also in preventing re-injuries is proprioception. It is essential that proprioception or balance / coordination exercises are performed to “fire” up the muscles around the ankle joint complex to prevent re-injuries from occurring.
Finally, if you choose to see a professional therapist to treat your ankle injury, he / she may choose to use electrotherapy such as ultrasound or sports massage to help stimulate healing and blood flow into the area.


{% load model_helpers %}
Hello,
A new personnel action form (PAF) form has been submitted and is awaiting your review. This form has been submitted by:
{{data.created_by.first_name}} {{data.created_by.last_name}}
The requested position title and department are listed below.
The details of this request can be viewed and approved here.
Please take a moment to review this submission and approve or deny it.
Thank you.
{% if data.to_approver %} to_approver = {{data.to_approver}}


{% extends "base.html" %} {% block content %} {# NOTE: ordinary users should be able to edit only these fields: password, name, email, and only for their own account. They shouldn't be able to add new users. #} {% if user and user.id %}


Tin Whiskers needs 4 beertenders to efficently and effectively serve beer to our guests.
Job functions will include pouring beer, cleaning everything, and interacting with customers, all while ensuring they have the best visit possible.
SHALL Requirements: Excitement about craft beer, a willingness to learn, and a great personality.
RECOMMENDED Requirements: In-depth knowledge of craft beer, the brewing process, and serving beer.
For the full job description please see: Beertender Job Description
If you are interested, shoot an email to us at<EMAIL_ADDRESS>with your resume and any reasons you think you are the right person for the job!


|
Bullet Collision Detection & Physics Library
|
#include <btSoftBodyHelpers.h>
Public Types | |
| enum | _ { Nodes = 0x0001, Links = 0x0002, Faces = 0x0004, Tetras = 0x0008, Normals = 0x0010, Contacts = 0x0020, Anchors = 0x0040, Notes = 0x0080, Clusters = 0x0100, NodeTree = 0x0200, FaceTree = 0x0400, ClusterTree = 0x0800, Joints = 0x1000, Std = Links + Faces + Tetras + Anchors + Notes + Joints, StdTetra = Std - Faces + Tetras } |
Definition at line 26 of file btSoftBodyHelpers.h.
| enum fDrawFlags::_ |
| Enumerator | |
|---|---|
| Nodes | |
| Links | |
| Faces | |
| Tetras | |
| Normals | |
| Contacts | |
| Anchors | |
| Notes | |
| Clusters | |
| NodeTree | |
| FaceTree | |
| ClusterTree | |
| Joints | |
| Std | |
| StdTetra |
Definition at line 28 of file btSoftBodyHelpers.h.


src.encoding package¶
Subpackages¶
Submodules¶
src.encoding.apps module¶
src.encoding.boolean_frequency module¶
-
src.encoding.boolean_frequency.
boolean
(log, event_names, label, encoding)¶ Return type: DataFrame
-
src.encoding.boolean_frequency.
frequency
(log, event_names, label, encoding)¶ Return type: DataFrame
src.encoding.common module¶
-
src.encoding.common.
data_encoder_decoder
(job, training_log, test_log)¶ Return type: None
-
src.encoding.common.
encode_label_logs
(training_log, test_log, job, additional_columns=None, encode=True)¶
src.encoding.complex_last_payload module¶
-
src.encoding.complex_last_payload.
complex
(log, labelling, encoding, additional_columns)¶ Return type: DataFrame
-
src.encoding.complex_last_payload.
last_payload
(log, labelling, encoding, additional_columns)¶ Return type: DataFrame
src.encoding.encoder module¶
src.encoding.encoding_container module¶
-
class
src.encoding.encoding_container.
EncodingContainer
¶ Bases:
src.encoding.encoding_container.EncodingContainer
Inner object describing encoding configuration.
-
static
encode
(df)¶ Return type: None
-
static
init_label_encoder
(df)¶ Return type: None
-
is_all_in_one
()¶ Return type: bool
-
is_boolean
()¶ Return type: bool
-
is_complex
()¶ Return type: bool
-
is_zero_padding
()¶ Return type: bool
-
static
src.encoding.encoding_parser module¶
-
class
src.encoding.encoding_parser.
DataEncoder
(task, is_targets_dataset=False)¶ Bases:
object
support class for EncodingParser, tasked with actual parsing/one-hot encoding
-
class
DataTypes
¶ Bases:
enum.Enum
possible data types for each column
-
CATEGORICAL
= 'categorical'¶
-
NUMERIC
= 'numeric'¶
-
-
build_encoders
(data)¶ builds an encoder for each column
first the base headers are extracted (prefix_1 -> prefix, org:resources:Amount_1 -> org_resources:Amount) and then a dictionary of LabelEncoders is built. Numerical data stores min and max instead of a LabelEncoder.
Parameters: data ( DataFrame
) – input dataframeReturn type: None
-
encode_data
(data, train=True)¶ encodes the input data
actual data encoding, using the built encoders. For each column type the right encoding is done (to class/normalization)
Parameters: - data (
DataFrame
) – input dataframe - train (
bool
) – flag indicating whether the input is a train dataframe or a test one
Return type: None
- data (
-
get_n_classes_x
()¶ returns the number of training/test classes
returns the highest number of classes for the encoded dataframe, adding 1 if there are numerical values. The structure is [one-hot encoding, normalized_value] for each variable, such that a categorical variable becomes [0 0 0 1 0.0] where a numerical value becomes [0 0 0 0 0 0.263]
Returns: number of training/test classes + 1 (for numerical values)
-
get_numerical_limits
(header='label')¶ returns the numerical limits for the input header
returns the min and max value from the stored LabelEncoders, using header as index
Parameters: header – label associated with the data we want to extract min and max from Returns: min and max values associated with the column _header_
-
to_one_hot
(data)¶ one hot encoding
transforms the encoded data into the one-hot representation
Parameters: data ( DataFrame
) – input dataframeReturn type: ndarray
Returns: one-hot encoded array
-
class
-
class
src.encoding.encoding_parser.
EncodingParser
(encoding, binary_target, task)¶ Bases:
object
parses the encoded datasets into a suitable format for the keras models (0-1 float range, one-hot encodable classes etc.), plus minor utils
-
denormalize_predictions
(predictions)¶ denormalizes the predictive_model predictions
denormalizes the predictions using the stored y min and max
Parameters: predictions ( ndarray
) – predictive_model predictionsReturn type: ndarray
Returns: denormalized predictions
-
get_n_classes_x
()¶
-
parse_targets
(targets)¶ parses the target dataset
encodes the target dataset based on the encoding given in the init method. Stores min and max value/classes number based on the encoding :type targets:
DataFrame
:param targets: input dataset :rtype:ndarray
:return: parsed input dataset
-
parse_testing_dataset
(test_data)¶ parses the test dataset
encodes the test dataset based on the encoding given in the init method :type test_data:
DataFrame
:param test_data: input dataset :rtype:ndarray
:return: parsed input dataset
-
parse_training_dataset
(train_data)¶ parses the training dataset
encodes the training dataset based on the encoding given in the init method :type train_data:
DataFrame
:param train_data: input dataset :rtype:ndarray
:return: parsed input dataset
-
src.encoding.models module¶
-
class
src.encoding.models.
DataEncodings
¶ Bases:
enum.Enum
An enumeration.
-
LABEL_ENCODER
= 'label_encoder'¶
-
ONE_HOT_ENCODER
= 'one_hot'¶
-
-
class
src.encoding.models.
Encoding
(id, data_encoding, value_encoding, add_elapsed_time, add_remaining_time, add_executed_events, add_resources_used, add_new_traces, features, prefix_length, padding, task_generation_type)¶ Bases:
src.common.models.CommonModel
-
exception
DoesNotExist
¶ Bases:
django.core.exceptions.ObjectDoesNotExist
-
exception
MultipleObjectsReturned
¶ Bases:
django.core.exceptions.MultipleObjectsReturned
-
add_elapsed_time
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
add_executed_events
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
add_new_traces
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
add_remaining_time
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
add_resources_used
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
data_encoding
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
features
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
get_data_encoding_display
(*, field=<django.db.models.fields.CharField: data_encoding>)¶
-
get_task_generation_type_display
(*, field=<django.db.models.fields.CharField: task_generation_type>)¶
-
get_value_encoding_display
(*, field=<django.db.models.fields.CharField: value_encoding>)¶
-
id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
job_set
¶ Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Parent.children
is aReverseManyToOneDescriptor
instance.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
labelledlog_set
¶ Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Parent.children
is aReverseManyToOneDescriptor
instance.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
objects
= <django.db.models.manager.Manager object>¶
-
padding
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
prefix_length
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
task_generation_type
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
to_dict
()¶ Return type: dict
-
value_encoding
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
exception
src.encoding.simple_index module¶
-
src.encoding.simple_index.
add_trace_row
(trace, encoding, labelling, event_index, column_len, attribute_classifier=None, executed_events=None, resources_used=None, new_traces=None)¶ Row in data frame
-
src.encoding.simple_index.
simple_index
(log, labelling, encoding)¶ Return type: DataFrame


Git branch to checkout. Example: develop
If not specified, HEAD will be used, which points to the default branch configured in your Git repo.


This bus stop was designed for a team of entrepreneurs working to improve mobility in Detroit, MI. The team was working on a challenge to improve mobility within the city. During their research they found that only 10% of city bus stops had benches and shelters. Furthermore, they learned that 40% of bus routes are proposed for changes. And, the reason that bus stops are lacking shelters? The city doesn't have the human capital keep up with the maintenance of current bus stops.
They proposed an innovative solution to this problem. To create a bus stop designed with molecularity in mind. A bus stop that is adaptable to changes and with parts that are interchangeable - making it easy to maintain. But, to bring their idea to life they needed a prototype to test and pitch their concept.
I designed and created a 3D design of this team's concept - helping them bring their idea to life. On this project I coordinated with the team to help them build an initial design in SketchUp. I then worked with them on several revisions of the design to bring it to its final iteration. I then uploaded the model online to a cloud-based platform, making it available to their full use. Feel free to explore the model below!


{% extends "errors/base.html" %} {% block error_code %}404{% endblock %} {% block title %}Oops! You're lost.{% endblock %} {% block content %}The page you are looking for was not found.{% endblock %}