crosstab 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +23 -0
- data/README.txt +133 -0
- data/Rakefile +17 -0
- data/lib/crosstab.rb +25 -0
- data/lib/crosstab/banner.rb +90 -0
- data/lib/crosstab/cell.rb +162 -0
- data/lib/crosstab/column.rb +64 -0
- data/lib/crosstab/crosstab.rb +243 -0
- data/lib/crosstab/extensions.rb +26 -0
- data/lib/crosstab/generic.rb +83 -0
- data/lib/crosstab/group.rb +28 -0
- data/lib/crosstab/row.rb +65 -0
- data/lib/crosstab/table.rb +85 -0
- data/test/test_banner.rb +81 -0
- data/test/test_cell.rb +95 -0
- data/test/test_column.rb +60 -0
- data/test/test_crosstab.rb +214 -0
- data/test/test_extensions.rb +12 -0
- data/test/test_group.rb +67 -0
- data/test/test_missing.rb +75 -0
- data/test/test_row.rb +60 -0
- data/test/test_table.rb +67 -0
- metadata +87 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
lib/crosstab.rb
|
6
|
+
lib/crosstab/banner.rb
|
7
|
+
lib/crosstab/cell.rb
|
8
|
+
lib/crosstab/column.rb
|
9
|
+
lib/crosstab/crosstab.rb
|
10
|
+
lib/crosstab/extensions.rb
|
11
|
+
lib/crosstab/generic.rb
|
12
|
+
lib/crosstab/group.rb
|
13
|
+
lib/crosstab/row.rb
|
14
|
+
lib/crosstab/table.rb
|
15
|
+
test/test_banner.rb
|
16
|
+
test/test_cell.rb
|
17
|
+
test/test_column.rb
|
18
|
+
test/test_crosstab.rb
|
19
|
+
test/test_extensions.rb
|
20
|
+
test/test_group.rb
|
21
|
+
test/test_missing.rb
|
22
|
+
test/test_row.rb
|
23
|
+
test/test_table.rb
|
data/README.txt
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
crosstab
|
2
|
+
by Michael Judge <mjudge@surveycomplete.com>
|
3
|
+
http://crosstab.rubyforge.org/
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Crosstab is a library for generating formatted pivot tables.
|
8
|
+
|
9
|
+
== FEATURES:
|
10
|
+
|
11
|
+
* Input your data as an array of hashes
|
12
|
+
* Input a report layout, built using a Ruby DSL
|
13
|
+
* Outputs ASCII pivot tables suitable for fast reports
|
14
|
+
* Pretty fast: takes less than a second to process 1,000 records of data by a report with 100 rows and 10 columns.
|
15
|
+
|
16
|
+
== SYNOPSIS:
|
17
|
+
|
18
|
+
require 'crosstab'
|
19
|
+
|
20
|
+
data = [{:gender => "M", :age => 1},
|
21
|
+
{:gender => "F", :age => 2},
|
22
|
+
{:gender => "M", :age => 3}]
|
23
|
+
|
24
|
+
my_crosstab = crosstab data do
|
25
|
+
table do
|
26
|
+
title "Q.A Gender:"
|
27
|
+
row "Male", :gender => "M"
|
28
|
+
row "Female", :gender => "F"
|
29
|
+
end
|
30
|
+
|
31
|
+
table do
|
32
|
+
title "Q.B Age:"
|
33
|
+
group "18 - 54" do
|
34
|
+
row "18 - 34", :age => 1
|
35
|
+
row "35 - 54", :age => 2
|
36
|
+
end
|
37
|
+
row "55 or older", :age => 3
|
38
|
+
end
|
39
|
+
|
40
|
+
banner do
|
41
|
+
column "Total"
|
42
|
+
group "Gender" do
|
43
|
+
column "Male", :gender => "M"
|
44
|
+
column "Female", :gender => "F"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
puts my_crosstab.to_s
|
50
|
+
|
51
|
+
== REPORT:
|
52
|
+
|
53
|
+
# puts my_crosstab.to_s
|
54
|
+
Table 1
|
55
|
+
Q.A Gender:
|
56
|
+
Gender
|
57
|
+
----------------
|
58
|
+
Total Male Female
|
59
|
+
(A) (B) (C)
|
60
|
+
------- ------- -------
|
61
|
+
(BASE) 3 2 1
|
62
|
+
|
63
|
+
Male 2 2 --
|
64
|
+
67% 100%
|
65
|
+
|
66
|
+
Female 1 -- 1
|
67
|
+
33% 100%
|
68
|
+
|
69
|
+
------------------------------------------------------------------------
|
70
|
+
Table 2
|
71
|
+
Q.B Age:
|
72
|
+
Gender
|
73
|
+
----------------
|
74
|
+
Total Male Female
|
75
|
+
(A) (B) (C)
|
76
|
+
------- ------- -------
|
77
|
+
(BASE) 3 2 1
|
78
|
+
|
79
|
+
18 - 54 2 1 1
|
80
|
+
----------------------------- 67% 50% 100%
|
81
|
+
|
82
|
+
18 - 34 1 1 --
|
83
|
+
33% 50%
|
84
|
+
|
85
|
+
35 - 54 1 -- 1
|
86
|
+
33% 100%
|
87
|
+
|
88
|
+
55 or older 1 1 --
|
89
|
+
33% 50%
|
90
|
+
|
91
|
+
|
92
|
+
------------------------------------------------------------------------
|
93
|
+
== JUST THE BEGINNING:
|
94
|
+
|
95
|
+
* I hope to add in later releases:
|
96
|
+
* New export formats: html, pdf, csv, excel.
|
97
|
+
* More stats than just frequency and percentage: mean, median, std. deviation, std. error, and significance testing
|
98
|
+
* Optional row and table suppression for low frequencies
|
99
|
+
* Optional table rows populating from the data
|
100
|
+
* Optional table ranking -- automatically reorder rows based in descending order based on frequencies observed
|
101
|
+
|
102
|
+
== REQUIREMENTS:
|
103
|
+
|
104
|
+
* None
|
105
|
+
|
106
|
+
== INSTALL:
|
107
|
+
|
108
|
+
* sudo gem install crosstab
|
109
|
+
|
110
|
+
== LICENSE:
|
111
|
+
|
112
|
+
(The MIT License)
|
113
|
+
|
114
|
+
Copyright (c) 2007 Michael Judge
|
115
|
+
|
116
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
117
|
+
a copy of this software and associated documentation files (the
|
118
|
+
'Software'), to deal in the Software without restriction, including
|
119
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
120
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
121
|
+
permit persons to whom the Software is furnished to do so, subject to
|
122
|
+
the following conditions:
|
123
|
+
|
124
|
+
The above copyright notice and this permission notice shall be
|
125
|
+
included in all copies or substantial portions of the Software.
|
126
|
+
|
127
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
128
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
129
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
130
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
131
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
132
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
133
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/crosstab.rb'
|
6
|
+
|
7
|
+
Hoe.new('crosstab', Crosstab::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'crosstab'
|
9
|
+
p.summary = 'Crosstab is a library for generating formatted pivot tables.'
|
10
|
+
p.email = "mjudge@surveycomplete.com"
|
11
|
+
p.description = p.paragraphs_of('README.txt', 2..13).join("\n\n")
|
12
|
+
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
13
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
14
|
+
p.remote_rdoc_dir = '' # Release to root
|
15
|
+
end
|
16
|
+
|
17
|
+
# vim: syntax=Ruby
|
data/lib/crosstab.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
3
|
+
|
4
|
+
module Crosstab
|
5
|
+
VERSION = '0.1.0'
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'crosstab/generic'
|
9
|
+
require 'crosstab/crosstab'
|
10
|
+
require 'crosstab/banner'
|
11
|
+
require 'crosstab/table'
|
12
|
+
require 'crosstab/column'
|
13
|
+
require 'crosstab/row'
|
14
|
+
require 'crosstab/cell'
|
15
|
+
require 'crosstab/group'
|
16
|
+
require 'crosstab/extensions'
|
17
|
+
|
18
|
+
require 'rubygems'
|
19
|
+
require 'text/reform'
|
20
|
+
|
21
|
+
def crosstab(data, &block)
|
22
|
+
my_crosstab = Crosstab::Crosstab.new(&block)
|
23
|
+
my_crosstab.data_source(data)
|
24
|
+
my_crosstab
|
25
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class Crosstab::Banner < Crosstab::Generic
|
2
|
+
|
3
|
+
# Pass in a block and we'll execute it within the context of this class. If no block is passed in,
|
4
|
+
# a "Total" column will be added automatically.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# my_crosstab = Crosstab.new do
|
9
|
+
# banner do
|
10
|
+
# column "Male", :a => 1
|
11
|
+
# column "Female", :a => 2
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
def initialize(&block)
|
16
|
+
if block
|
17
|
+
instance_eval(&block)
|
18
|
+
else
|
19
|
+
column "Total"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# attr_reader for the columns attribute which should contain an empty array, or a list of columns
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
#
|
27
|
+
# columns
|
28
|
+
# #=> []
|
29
|
+
#
|
30
|
+
# column "Male", :a => 1
|
31
|
+
#
|
32
|
+
# columns
|
33
|
+
# #=> [Crosstab::Column...]
|
34
|
+
#
|
35
|
+
def columns
|
36
|
+
@columns ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
# Creates a new Crosstab::Column and appends it to the columns array.
|
40
|
+
#
|
41
|
+
# Example:
|
42
|
+
#
|
43
|
+
# columns
|
44
|
+
# #=> []
|
45
|
+
#
|
46
|
+
# column "Male", :a => 1
|
47
|
+
#
|
48
|
+
# columns
|
49
|
+
# #=> [Crosstab::Column...]
|
50
|
+
#
|
51
|
+
def column(name=nil,qualification=nil)
|
52
|
+
columns << Crosstab::Column.new(name, qualification)
|
53
|
+
end
|
54
|
+
|
55
|
+
# DSL child setter, creates a new Group, and columns created within its block will be assigned to that group.
|
56
|
+
#
|
57
|
+
# Example:
|
58
|
+
#
|
59
|
+
# columns
|
60
|
+
# #=> []
|
61
|
+
#
|
62
|
+
# group "Gender" do
|
63
|
+
# column "Male", :a => 1
|
64
|
+
# column "Female", :a => 2
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# columns
|
68
|
+
# #=> [Crosstab::Column..., Crosstab::Column...]
|
69
|
+
#
|
70
|
+
# columns[0].group.title
|
71
|
+
# # => "Gender"
|
72
|
+
#
|
73
|
+
# columns[1].group.title
|
74
|
+
# # => "Gender"
|
75
|
+
|
76
|
+
def group(name=nil, &block)
|
77
|
+
if block
|
78
|
+
old_columns = columns.dup # Save current state
|
79
|
+
|
80
|
+
instance_eval(&block) # Execute block within current scope
|
81
|
+
|
82
|
+
g = Crosstab::Group.new(name) # Create new group
|
83
|
+
|
84
|
+
# Set group for all of the new columns
|
85
|
+
(columns - old_columns).each do |col|
|
86
|
+
col.group g
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
class Crosstab::Cell
|
2
|
+
# Optionally, can take options :base and :frequency for a fast initialization.
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
#
|
6
|
+
# cell = Crosstab::Cell.new
|
7
|
+
# cell.frequency 2
|
8
|
+
# cell.base 4
|
9
|
+
# cell.percentage
|
10
|
+
# # => 0.5
|
11
|
+
#
|
12
|
+
# cell = Crosstab::Cell.new :base => 4, :frequency => 2
|
13
|
+
# cell.percentage
|
14
|
+
# # => 0.5
|
15
|
+
#
|
16
|
+
def initialize(options={})
|
17
|
+
base options[:base] if options[:base]
|
18
|
+
frequency options[:frequency] if options[:frequency]
|
19
|
+
end
|
20
|
+
|
21
|
+
# DSL accessor for the base attribute. The base is the number of records used in calculations for this cell.
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
#
|
25
|
+
# cell = Crosstab::Cell.new
|
26
|
+
#
|
27
|
+
# cell.frequency 2
|
28
|
+
# cell.base 4
|
29
|
+
#
|
30
|
+
# cell.percentage
|
31
|
+
# # => 0.5
|
32
|
+
#
|
33
|
+
def base(value=nil)
|
34
|
+
if value
|
35
|
+
@base = value
|
36
|
+
else
|
37
|
+
@base ||= 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns and sets the frequency for this cell, that is, the number of records that meet both the row and column qualifications.
|
42
|
+
#
|
43
|
+
# Example:
|
44
|
+
#
|
45
|
+
# cell = Crosstab::Cell.new
|
46
|
+
#
|
47
|
+
# cell.frequency 2
|
48
|
+
# cell.base 4
|
49
|
+
#
|
50
|
+
# cell.percentage
|
51
|
+
# # => 0.5
|
52
|
+
#
|
53
|
+
def frequency(value=nil)
|
54
|
+
if value
|
55
|
+
@frequency = value
|
56
|
+
else
|
57
|
+
@frequency ||= 0
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the frequency divided by the base. Always a float.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
#
|
65
|
+
# cell = Crosstab::Cell.new
|
66
|
+
#
|
67
|
+
# cell.frequency 2
|
68
|
+
# cell.base 4
|
69
|
+
#
|
70
|
+
# cell.percentage
|
71
|
+
# # => 0.5
|
72
|
+
#
|
73
|
+
def percentage
|
74
|
+
if base > 0
|
75
|
+
frequency.to_f / base.to_f
|
76
|
+
else
|
77
|
+
0.0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array of default stats (frequency, percentage, then sigtesting). It's only really useful for printing within reports.
|
82
|
+
# If the frequency of the cell is 0, it returns [ "--" ] because what's the point of displaying 0, 0%?
|
83
|
+
#
|
84
|
+
#
|
85
|
+
# Example:
|
86
|
+
#
|
87
|
+
# cell = Crosstab::Cell.new
|
88
|
+
#
|
89
|
+
# cell.frequency 2
|
90
|
+
# cell.base 4
|
91
|
+
#
|
92
|
+
# cell.result
|
93
|
+
# # => [2, 0.5]
|
94
|
+
#
|
95
|
+
def result
|
96
|
+
if frequency > 0
|
97
|
+
[frequency, "#{(percentage * 100).round}%" ]
|
98
|
+
else
|
99
|
+
["--"]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Tests this cell against another and returns true if it is statistically significant. The default testing level is 95%,
|
104
|
+
# represented as a float (i.e., 0.95.) Other levels supported are 0.85, 0.90, and 0.98.
|
105
|
+
#
|
106
|
+
# Example:
|
107
|
+
#
|
108
|
+
# cell1 = Crosstab::Cell.new :base => 100, :frequency => 65
|
109
|
+
# cell2 = Crosstab::Cell.new :base => 100, :frequency => 50
|
110
|
+
#
|
111
|
+
# cell1.significant_against? cell2
|
112
|
+
# # => true
|
113
|
+
#
|
114
|
+
def significant_against?(test_cell, level=0.95)
|
115
|
+
zfactor = { 0.98 => 2.327,
|
116
|
+
0.95 => 1.960,
|
117
|
+
0.90 => 1.645,
|
118
|
+
0.85 => 1.440 }
|
119
|
+
|
120
|
+
# From what I understand, a z-test is meaningless if either of the two cells are in the bottom or top 10%.
|
121
|
+
# So we return false. If you're any good at statistics, I would love to hear your opinion on this.
|
122
|
+
# I've done this shit for ten years and I'm still copying formulas out of books. Frankly, it sounds odd,
|
123
|
+
# and I can't seem to find any good documentation on why it is that we do it this way. 10% sounds like a fucking guess.
|
124
|
+
|
125
|
+
return false unless [self, test_cell].all? { |x| (0.10..0.90).include? x.percentage }
|
126
|
+
|
127
|
+
# The preperation
|
128
|
+
a = self.percentage * self.base + test_cell.percentage * test_cell.base
|
129
|
+
b = self.base + test_cell.base
|
130
|
+
c = 1.0 / self.base + 1.0 / test_cell.base
|
131
|
+
|
132
|
+
# The main calculation
|
133
|
+
z = (self.percentage - test_cell.percentage) / Math.sqrt(a / b * (1 - a / b) * c)
|
134
|
+
|
135
|
+
# The test
|
136
|
+
z >= zfactor[level]
|
137
|
+
end
|
138
|
+
|
139
|
+
# DSL accessor for the column that this cell belongs to.
|
140
|
+
#
|
141
|
+
# Example:
|
142
|
+
#
|
143
|
+
# cell = Crosstab::Cell.new
|
144
|
+
# cell.column
|
145
|
+
# #=> nil
|
146
|
+
#
|
147
|
+
# cell.column Crosstab::Column.new("Male", :a => 1)
|
148
|
+
#
|
149
|
+
# column
|
150
|
+
# #=> Crosstab::Column
|
151
|
+
#
|
152
|
+
# column.title
|
153
|
+
# #=> "Male"
|
154
|
+
|
155
|
+
def column(value=nil)
|
156
|
+
if value
|
157
|
+
@column = value
|
158
|
+
else
|
159
|
+
@column ||= nil
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|