# Vehicle Counters (#146)

**URL:** <https://www.ruby-forum.com/t/vehicle-counters-146/119715>\
**Category:** Ruby\
**Created:** [November 9, 2007, 2:07pm UTC](https://www.ruby-forum.com/t/vehicle-counters-146/119715 "2007-11-09T14:07:47Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [November 9, 2007, 2:07pm UTC](https://www.ruby-forum.com/t/vehicle-counters-146/119715/1 "2007-11-09T14:07:47Z")

</div>

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/](http://www.rubyquiz.com/)

1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps  
everyone  
on Ruby T. follow the discussion. Please reply to the original quiz  
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](http://rubyquiz.com/vehicle_counter.data.gz)

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [November 13, 2007, 6:44pm UTC](https://www.ruby-forum.com/t/vehicle-counters-146/119715/2 "2007-11-13T18:44:37Z")

</div>

Hello,

My solution is available on Pastie:  
[http://pastie.caboo.se/117189](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:

# Calculate speed based upon start/end times recorded by sensor

# 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:

def calc\_distance(leader\_time, follower\_time)  
# 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:  
[http://justin.ethier.googlepages.com/output.txt](http://justin.ethier.googlepages.com/output.txt)  
[http://justin.ethier.googlepages.com/output-avg.txt](http://justin.ethier.googlepages.com/output-avg.txt)

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

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [November 15, 2007, 8:40am UTC](https://www.ruby-forum.com/t/vehicle-counters-146/119715/3 "2007-11-15T08:40:37Z")

</div>

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.

But this already got more complicated than I originally had planned.  
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](http://LearnRuby.com)  
!

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [November 14, 2007, 2:50am UTC](https://www.ruby-forum.com/t/vehicle-counters-146/119715/4 "2007-11-14T02:50:35Z")

</div>

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\_reader :time, :direction, :ms  
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

raw\_data = IO.readlines( ARGS[:data\_file] )  
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
