crosstab 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ == 0.1.0 / 2007-11-17
2
+
3
+ * First release
4
+
@@ -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
@@ -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.
@@ -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
@@ -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