crosstab 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|