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.
@@ -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