# Vehicle Counters (#146)

The three rules of Ruby Q.:

1. Please do not post any solutions or spoiler discussion for this quiz
until
48 hours have passed from the time on this message.

2. Support Ruby Q. by submitting ideas as often as you can:

http://www.rubyquiz.com/

1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Gavin K.

You have been hired to work for a small city government. The city
recently
bought a vehicle counter, one of those kinds that uses pneumatic rubber
hoses
stretched across the road. The company that sells the machine also sells
software to interpret the raw data. However, the city has asked you to
see if
you can interpret it instead, saving them some money.

The data from the machine looks like this:

A268981
A269123
A604957
B604960
A605128
B605132
A1089807
B1089810
A1089948
B1089951

The numbers are the number of milliseconds since midnight when the mark
occurred. Thus, the first line above represents a pair of tires driving
by at
12:04:28am. The second line represents another pair of tires going by
142ms
later (almost certainly the 2nd axle of the car).

The vehicle counter has two tubes - one stretches across both lanes of
traffic,
and one goes just across traffic in one direction. Each hose
independently
records when tires drive over it. As such, cars going in one direction
(say,
northbound) only record on one sensor (preceded with an ‘A’), while cars
going
in the direction (say, southbound) are recorded on both sensors. Lines
3-6 above
represent a second car going in the other direction. The first set of
tires hit
the ‘A’ sensor at 12:10:04am, and then hit the ‘B’ sensor 3ms later. The
second
set of tires then hit the ‘A’ sensor 171ms later, and then the ‘B’
sensor 4ms
later.

The machine was left to run for 5 days in a row (starting on a Monday).
This is
obvious because the times in the data make several sudden drops:

A86328771
B86328774
A86328899
B86328902
A582668
B582671
A582787
B582789

The city has asked you to see how many analysis features you can provide
from
what the manufacturer’s software offers:

• Total vehicle counts in each direction: morning versus evening,
per hour, per half hour, per 20 minutes, and per 15 minutes.
• The above counts can be displayed for each day of the session,
or you can see averages across all the days.
• Peak volume times.
• The (rough) speed distribution of traffic.
• Rough distance between cars during various periods.

Luckily for you, you know that:

• The speed limit on the road where this is recorded is 40mph
(that doesn’t mean that everyone drives this speed, or that
no one exceeds it, but it’s a good starting point).
• The average wheelbase of cars in the city is around 100 inches
between axles.
• Only 2-axle vehicles were allowed on this road during the
recording sessions.

The full data can be found at:

http://rubyquiz.com/vehicle_counter.data.gz

Hello,

My solution is available on Pastie:
http://pastie.caboo.se/117189

I was hoping for a bit of a cleaner solution to this problem but it does
what it needs to do. Two classes are used to process the data. The first
class reads raw data from file and builds a list of records for each
direction. It assumes 2 consecutive A records (A / A) represent one
direction, and 4 A / B / A / B records indicate the other direction.
This
parser needs to be beefed-up to handle cases where 2 cars pass at the
same
time in opposite directions, but that case does not really come up in
the
sample dataset.

The next class, DirectionDataReport, analyzes the processed direction
records and generates a report. Counts are reported, as well as peak
times.
To make things a bit easier, average speed is reported rather than a
speed
distribution. Average distance between cars is also reported.

Speed is calculated by the following method:

# Assumes 100 inch wheelbase contraint from quiz statement

def calc_speed(start_time, end_time)
# Inches MSec Miles Miles
# ------ * ---- * ------ = -----
# MSec Hr Inches Hour
return (100.0 / (end_time - start_time)) * MSecPerHour /
(InchesPerMile

• 1.0)
end

Likewise, the following method calculates a rough estimate of the
distance
between two cars. If there is only one car on the road during a time
interval then the distance is simply set to 0. This is a rough
calculation
that might be improved by taking the speed of both cars into account:

# miles/hr * time_delta (in msecs) * msec/hr = miles between cars
follower_speed = calc_speed(follower_time[0], follower_time[1])

``````dist = follower_speed * ((follower_time[0] - leader_time[0]) /
``````

(MSecPerHour * 1.0))

``````return dist
``````

end

Most of the data is output as a series of columns. So counts for time
interval 1:00 - 2:00 would be shown under the 1:00 column, and so on.
Here
is the output generated from the sample data file:

From the data it looks like most people traveling on this road are
commuting
south into the city, although both directions are busy:

Northbound
Data 7:00 8:00 9:00 15:00 16:00 17:00 18:00
Avg Count 128 199 118 196 300 398 100
Avg Speed 40.08 39.96 40.15 39.93 40.15 40.28 40.25
Avg Dist 0.51 0.24 0.21 0.24 0.14 0.18 0.27

Southbound
Data 7:00 8:00 9:00 15:00 16:00 17:00 18:00
Avg Count 261 448 78 176 169 173 35
Avg Speed 40.03 40.08 40.37 40.04 39.97 40.1 39.9
Avg Dist 0.1 0.09 0.46 0.15 0.18 0.2 0.69

Thanks,

Justin

I wanted to graph the data, so I ended up using the scruffy library to
handle the graphing. Since there were potentially so many graphs, I
ended up creating a graphing domain specific language, so I could
easily create the graph variations. I separated the graphing DSL
(grapher.rb) from all the traffic-related code (traffic_analyzer.rb,
traffic_grapher.rb, common.rb), in case that might be useful to
someone.

Basically, the entry point is main.rb, and when you run it, it will
generate a series of PNG files graphing various useful bits of data.
The file names of the PNG files should indicate the information they
contain. The next step would be to put a front-end on this, so the
user could easily select graphs for display or perhaps generate them
as requested.

I wish I had time to clean up the code and comment things better.

And I realize it’s a pretty late submission and therefore may not be
considered in the ultimate the summary.

The code can be found at:

``````http://learnruby.com/examples/ruby-quiz-146.shtml
``````

Eric

====

On-site, hands-on Ruby training is available from http://LearnRuby.com
!

Attached is my solution (in progress). Before I get onto the analysis
reports, I’m still trying to simplify the data post parsing. But, at
least it’s getting there.

As you can see from the results, I’m only properly pulling data from
the first day. This is why I enjoy quizzes; I’m sure I tried to fix
this same problem at some point.

#!/usr/bin/env ruby
WHEELBASE_AVG = 100 # inches
INCHES_PER_MILE = 63_360
SECONDS_PER_HOUR = 3600.0
INCHES_PER_SECOND_TO_MPH = 17.6

USAGE = <<ENDUSAGE
Usage:
vehicle_counter [-t time_segment] [-a] data_file
-t,–time the number of minutes per segment (defaults to 60)
-a,–average average time samples across days
(defaults to showing each day indpendently)
ENDUSAGE

ARGS = {
:time_segment => 60,
#:data_file => ‘vehicle_counter.data’
}
UNFLAGGED_ARGS = [ :data_file ]
next_arg = UNFLAGGED_ARGS.first
ARGV.each{ |arg|
case arg
when ‘-t’,’–time’
next_arg = :time_segment
when ‘-a’,’–average’
ARGS[:average] = true
else
if next_arg
if next_arg==:time_segment
arg = arg.to_i
end
ARGS[next_arg] = arg
UNFLAGGED_ARGS.delete( next_arg )
end
next_arg = UNFLAGGED_ARGS.first
end
}

if !ARGS[:data_file] || !ARGS[:time_segment]
puts USAGE
exit
end

class Record
SECONDS_PER_DAY = 3600 * 24
attr_accessor :speed
def initialize( str, day_offset )
_, @direction, @ms = /([AB])(\d+)/.match( str ).to_a
@ms = @ms.to_i
@time = Time.gm( 2007 ) + ( @ms.to_i / 1000.0 ) + ( day_offset *
SECONDS_PER_DAY )
end
end

# Prepare data

day_changes = 0
records = raw_data.inject([]){ |records,line|
record = Record.new( line, ARGS[:average] ? 0 : day_changes )
if (last_record = records.last) && (record.ms < last_record.ms)
day_changes += 1
end
records << record
}

# Convert axle pairs to speed

last_record = {}
records.each{ |record|
if last_axle = last_record[ record.direction ]
last_axle.speed = WHEELBASE_AVG / ( record.time -
last_axle.time )
last_axle.speed /= INCHES_PER_SECOND_TO_MPH
last_record[ record.direction ] = nil
else
last_record[ record.direction ] = record
end
}
records.delete_if{ |r| r.speed.nil? }

# Figure out which direction gets double hits

possible_directions = records.map{ |r| r.direction }.uniq
double_hit_direction = possible_directions.map{ |dir|
[ records.select{ |r| r.direction == dir }.length , dir ]
}.sort.last.last

# Remove extraneous records

require ‘enumerator’
records.each_cons(2){ |r1,r2|
if (r1.direction == double_hit_direction) &&
(r2.direction != double_hit_direction) &&
# 0.02 seconds @ 50mph is ~18 inches
# If the times are this close, it must be a double hit
(r1.time - r2.time).abs < 0.02
r1.speed = nil
end
}
records.delete_if{ |r| r.speed.nil? }

t1 = Time.gm(0)
ms_trigger = 0
slot_count = nil
ms_per_slot = ARGS[ :time_segment ] * 60 * 1000
records << Record.new( ‘B99999999’, 0 )
records.each{ |r|
if r.ms >= ms_trigger
if slot_count
t2 = t1 + ms_per_slot / 1000.0
print "#{t1.strftime(’%H:%M’)}…#{t2.strftime(’%H:%M’)} : "
puts “%4i %s, %4i %s” % possible_directions.map{ |dir|
[slot_count[dir],dir==“A” ? “left” : “right”]
}.flatten
t1 = t2
end
slot_count = Hash[ *possible_directions.map{ |d| [d,0] }.flatten ]
ms_trigger += ms_per_slot
end
slot_count[ r.direction ] += 1
}

Slim2:Code phrogz\$ ruby vehicle_counter.rb vehicle_counter.data -t 15
00:00…00:15 : 2 left, 1 right
00:15…00:30 : 1 left, 4 right
00:30…00:45 : 3 left, 2 right
00:45…01:00 : 1 left, 1 right
01:00…01:15 : 0 left, 2 right
01:15…01:30 : 0 left, 1 right
01:30…01:45 : 0 left, 3 right
01:45…02:00 : 1 left, 1 right
02:00…02:15 : 0 left, 1 right
02:15…02:30 : 1 left, 2 right
02:30…02:45 : 0 left, 2 right
02:45…03:00 : 1 left, 1 right
03:00…03:15 : 0 left, 3 right
03:15…03:30 : 0 left, 2 right
03:30…03:45 : 1 left, 1 right
03:45…04:00 : 1 left, 1 right
04:00…04:15 : 1 left, 3 right
04:15…04:30 : 0 left, 1 right
04:30…04:45 : 0 left, 1 right
04:45…05:00 : 1 left, 0 right
05:00…05:15 : 0 left, 5 right
05:15…05:30 : 0 left, 15 right
05:30…05:45 : 1 left, 4 right
05:45…06:00 : 2 left, 8 right
06:00…06:15 : 13 left, 8 right
06:15…06:30 : 18 left, 9 right
06:30…06:45 : 13 left, 9 right
06:45…07:00 : 8 left, 5 right
07:00…07:15 : 18 left, 64 right
07:15…07:30 : 16 left, 62 right
07:30…07:45 : 44 left, 66 right
07:45…08:00 : 44 left, 64 right
08:00…08:15 : 49 left, 59 right
08:15…08:30 : 45 left, 75 right
08:30…08:45 : 46 left, 151 right
08:45…09:00 : 44 left, 169 right
09:00…09:15 : 35 left, 25 right
09:15…09:30 : 32 left, 14 right
09:30…09:45 : 23 left, 16 right
09:45…10:00 : 26 left, 14 right
10:00…10:15 : 29 left, 16 right
10:15…10:30 : 33 left, 15 right
10:30…10:45 : 25 left, 14 right
10:45…11:00 : 23 left, 12 right
11:00…11:15 : 26 left, 32 right
11:15…11:30 : 16 left, 38 right
11:30…11:45 : 35 left, 26 right
11:45…12:00 : 26 left, 26 right
12:00…12:15 : 33 left, 29 right
12:15…12:30 : 25 left, 42 right
12:30…12:45 : 30 left, 27 right
12:45…13:00 : 31 left, 27 right
13:00…13:15 : 27 left, 41 right
13:15…13:30 : 29 left, 32 right
13:30…13:45 : 24 left, 38 right
13:45…14:00 : 31 left, 25 right
14:00…14:15 : 26 left, 41 right
14:15…14:30 : 23 left, 44 right
14:30…14:45 : 28 left, 49 right
14:45…15:00 : 21 left, 46 right
15:00…15:15 : 42 left, 48 right
15:15…15:30 : 55 left, 46 right
15:30…15:45 : 44 left, 55 right
15:45…16:00 : 42 left, 48 right
16:00…16:15 : 41 left, 51 right
16:15…16:30 : 58 left, 41 right
16:30…16:45 : 93 left, 40 right
16:45…17:00 : 102 left, 35 right
17:00…17:15 : 80 left, 41 right
17:15…17:30 : 111 left, 53 right
17:30…17:45 : 114 left, 39 right
17:45…18:00 : 106 left, 39 right
18:00…18:15 : 19 left, 6 right
18:15…18:30 : 21 left, 11 right
18:30…18:45 : 27 left, 7 right
18:45…19:00 : 25 left, 6 right
19:00…19:15 : 9 left, 9 right
19:15…19:30 : 12 left, 7 right
19:30…19:45 : 11 left, 18 right
19:45…20:00 : 3 left, 10 right
20:00…20:15 : 7 left, 11 right
20:15…20:30 : 9 left, 12 right
20:30…20:45 : 9 left, 6 right
20:45…21:00 : 11 left, 8 right
21:00…21:15 : 8 left, 10 right
21:15…21:30 : 7 left, 4 right
21:30…21:45 : 12 left, 11 right
21:45…22:00 : 6 left, 16 right
22:00…22:15 : 10 left, 5 right
22:15…22:30 : 17 left, 5 right
22:30…22:45 : 7 left, 8 right
22:45…23:00 : 16 left, 2 right
23:00…23:15 : 9 left, 2 right
23:15…23:30 : 4 left, 4 right
23:30…23:45 : 4 left, 6 right
23:45…00:00 : 8916 left, 9061 right