sqa-bi 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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/docs.yml +55 -0
  3. data/.quality/reek_baseline.txt +5 -0
  4. data/.rubocop.yml +224 -0
  5. data/CHANGELOG.md +33 -0
  6. data/CLAUDE.md +128 -0
  7. data/COMMITS.md +196 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +229 -0
  10. data/Rakefile +170 -0
  11. data/decision_support_techniques.md +391 -0
  12. data/docs/EXPLORATION.md +128 -0
  13. data/docs/api/index.md +66 -0
  14. data/docs/api/likelihood.md +78 -0
  15. data/docs/api/llm-elicitors.md +119 -0
  16. data/docs/api/llm-support.md +136 -0
  17. data/docs/api/posterior.md +106 -0
  18. data/docs/api/prior.md +81 -0
  19. data/docs/api/time-series-predictor.md +96 -0
  20. data/docs/assets/css/custom.css +25 -0
  21. data/docs/assets/diagrams/architecture.svg +75 -0
  22. data/docs/assets/diagrams/bayes-pipeline.svg +48 -0
  23. data/docs/assets/diagrams/kde.svg +52 -0
  24. data/docs/assets/diagrams/llm-bayes-loop.svg +58 -0
  25. data/docs/assets/diagrams/provider-resolution.svg +77 -0
  26. data/docs/assets/js/mathjax.js +18 -0
  27. data/docs/development.md +164 -0
  28. data/docs/examples/index.md +198 -0
  29. data/docs/getting-started/core-concepts.md +119 -0
  30. data/docs/getting-started/installation.md +112 -0
  31. data/docs/getting-started/quick-start.md +143 -0
  32. data/docs/guide/likelihood.md +132 -0
  33. data/docs/guide/posterior.md +135 -0
  34. data/docs/guide/predictor.md +191 -0
  35. data/docs/guide/prior.md +141 -0
  36. data/docs/guide/tuning.md +156 -0
  37. data/docs/guide/uncertainty.md +148 -0
  38. data/docs/index.md +110 -0
  39. data/docs/llm/index.md +124 -0
  40. data/docs/llm/likelihood-estimation.md +161 -0
  41. data/docs/llm/prior-elicitation.md +172 -0
  42. data/docs/llm/providers.md +184 -0
  43. data/docs/requirements.txt +8 -0
  44. data/lib/sqa/bi/likelihood.rb +167 -0
  45. data/lib/sqa/bi/llm_likelihood_estimator.rb +110 -0
  46. data/lib/sqa/bi/llm_prior_elicitor.rb +110 -0
  47. data/lib/sqa/bi/llm_support.rb +299 -0
  48. data/lib/sqa/bi/posterior.rb +189 -0
  49. data/lib/sqa/bi/prior.rb +135 -0
  50. data/lib/sqa/bi/time_series_predictor.rb +219 -0
  51. data/lib/sqa/bi/version.rb +7 -0
  52. data/lib/sqa/bi.rb +57 -0
  53. data/mkdocs.yml +174 -0
  54. metadata +272 -0
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SQA
4
+ module BI
5
+ # Main interface for Bayesian time series prediction
6
+ #
7
+ # TimeSeriesPredictor combines Prior, Likelihood, and Posterior to provide
8
+ # a simple API for predicting probability distributions over discrete outcomes
9
+ # given time series features.
10
+ #
11
+ # @example Basic usage
12
+ # predictor = TimeSeriesPredictor.new(
13
+ # outcomes: [-2, -1, 0, 1, 2],
14
+ # bandwidth: 1.0
15
+ # )
16
+ #
17
+ # # Train with historical data
18
+ # predictor.train([1.0, 2.0, 3.0], outcome: 1)
19
+ # predictor.train([1.1, 2.1, 2.9], outcome: 1)
20
+ # predictor.train([-1.0, -2.0, 0.5], outcome: -2)
21
+ #
22
+ # # Predict for new data
23
+ # posterior = predictor.predict([1.05, 2.05, 3.0])
24
+ # puts posterior.summary
25
+ class TimeSeriesPredictor
26
+ attr_reader :outcomes, :prior, :likelihood, :feature_dimension, :bandwidth
27
+
28
+ # Initialize predictor
29
+ #
30
+ # @param outcomes [Array] Discrete outcome values (e.g., [-2, -1, 0, 1, 2])
31
+ # @param bandwidth [Float] Kernel bandwidth for likelihood estimation
32
+ # @param prior_probabilities [Hash, nil] Custom prior probabilities
33
+ # If nil, uses uniform prior
34
+ # @param update_prior [Boolean] Whether to update prior from observations
35
+ #
36
+ # @example
37
+ # predictor = TimeSeriesPredictor.new(
38
+ # outcomes: [-2, -1, 0, 1, 2],
39
+ # bandwidth: 1.0,
40
+ # update_prior: true
41
+ # )
42
+ def initialize(outcomes:, bandwidth: 1.0, prior_probabilities: nil, update_prior: true)
43
+ @outcomes = outcomes.sort
44
+ @bandwidth = bandwidth
45
+ @update_prior = update_prior
46
+ @feature_dimension = nil
47
+
48
+ # Initialize prior
49
+ @prior = Prior.new(@outcomes, prior_probabilities)
50
+
51
+ # Initialize likelihood
52
+ @likelihood = Likelihood.new([], bandwidth: @bandwidth)
53
+ end
54
+
55
+ # Train predictor with new observation
56
+ #
57
+ # @param features [Array<Numeric>] Feature vector (e.g., [x, y, z])
58
+ # @param outcome [Numeric] Observed outcome
59
+ # @return [self] For method chaining
60
+ #
61
+ # @example
62
+ # predictor.train([1.0, 2.0, 3.0], outcome: 1)
63
+ # predictor.train([1.1, 2.1, 2.9], outcome: 1)
64
+ def train(features, outcome:)
65
+ validate_features!(features)
66
+ validate_outcome!(outcome)
67
+
68
+ @likelihood.add_observation(features, outcome)
69
+
70
+ # Update prior based on observed frequencies
71
+ if @update_prior && @likelihood.size >= @outcomes.size
72
+ @prior = @prior.update_from_observations(@likelihood.outcome_counts)
73
+ end
74
+
75
+ self
76
+ end
77
+
78
+ # Batch train with multiple observations
79
+ #
80
+ # @param observations [Array<Hash>] Array of {features: [...], outcome: ...}
81
+ # @return [self] For method chaining
82
+ #
83
+ # @example
84
+ # observations = [
85
+ # {features: [1.0, 2.0, 3.0], outcome: 1},
86
+ # {features: [1.1, 2.1, 2.9], outcome: 1},
87
+ # {features: [-1.0, -2.0, 0.5], outcome: -2}
88
+ # ]
89
+ # predictor.train_batch(observations)
90
+ def train_batch(observations)
91
+ observations.each do |obs|
92
+ train(obs[:features], outcome: obs[:outcome])
93
+ end
94
+ self
95
+ end
96
+
97
+ # Predict posterior distribution for given features
98
+ #
99
+ # @param features [Array<Numeric>] Feature vector to predict
100
+ # @return [Posterior] Posterior probability distribution
101
+ #
102
+ # @example
103
+ # posterior = predictor.predict([1.0, 2.0, 3.0])
104
+ # puts "Most likely outcome: #{posterior.max_outcome}"
105
+ # puts "Confidence: #{posterior.confidence}"
106
+ def predict(features)
107
+ validate_features!(features)
108
+
109
+ # Compute likelihoods for all outcomes
110
+ likelihoods = @likelihood.compute(features, @outcomes)
111
+
112
+ # Compute posterior
113
+ Posterior.new(@prior, likelihoods)
114
+ end
115
+
116
+ # Predict and return most likely outcome
117
+ #
118
+ # @param features [Array<Numeric>] Feature vector
119
+ # @return [Numeric] Most likely outcome (MAP estimate)
120
+ def predict_outcome(features)
121
+ predict(features).max_outcome
122
+ end
123
+
124
+ # Predict probabilities as simple hash
125
+ #
126
+ # @param features [Array<Numeric>] Feature vector
127
+ # @return [Hash] {outcome => probability}
128
+ def predict_proba(features)
129
+ predict(features).to_h
130
+ end
131
+
132
+ # Get current number of training observations
133
+ #
134
+ # @return [Integer]
135
+ def training_size
136
+ @likelihood.size
137
+ end
138
+
139
+ # Check if predictor has been trained
140
+ #
141
+ # @return [Boolean]
142
+ def trained?
143
+ !@likelihood.empty?
144
+ end
145
+
146
+ # Get distribution of outcomes in training data
147
+ #
148
+ # @return [Hash] {outcome => count}
149
+ def training_distribution
150
+ @likelihood.outcome_counts
151
+ end
152
+
153
+ # Reset predictor to initial state
154
+ #
155
+ # @param keep_prior [Boolean] Whether to keep the current prior
156
+ # @return [self]
157
+ def reset!(keep_prior: false)
158
+ @prior = Prior.new(@outcomes) unless keep_prior
159
+ @likelihood = Likelihood.new([], bandwidth: @bandwidth)
160
+ @feature_dimension = nil
161
+ self
162
+ end
163
+
164
+ # Update kernel bandwidth for likelihood estimation
165
+ #
166
+ # @param new_bandwidth [Float] New bandwidth value
167
+ # @return [self]
168
+ def bandwidth=(new_bandwidth)
169
+ @bandwidth = new_bandwidth
170
+ # Recreate likelihood with new bandwidth
171
+ @likelihood = Likelihood.new(@likelihood.observations, bandwidth: @bandwidth)
172
+ self
173
+ end
174
+
175
+ # Get summary statistics
176
+ #
177
+ # @return [String] Formatted summary
178
+ def summary
179
+ <<~SUMMARY
180
+ TimeSeriesPredictor Summary:
181
+ =============================
182
+ Outcomes: #{@outcomes.inspect}
183
+ Feature Dimension: #{@feature_dimension || 'not set'}
184
+ Training Samples: #{training_size}
185
+ Bandwidth: #{@bandwidth}
186
+ Update Prior: #{@update_prior}
187
+
188
+ Prior Distribution:
189
+ #{@prior}
190
+ Prior Entropy: #{format('%.3f', @prior.entropy)} bits
191
+
192
+ Training Distribution:
193
+ #{training_distribution.map { |outcome, count| " #{outcome}: #{count}" }.join("\n")}
194
+ SUMMARY
195
+ end
196
+
197
+ private
198
+
199
+ def validate_features!(features)
200
+ unless features.is_a?(Array) && features.all?(Numeric)
201
+ raise ArgumentError, "Features must be array of numbers"
202
+ end
203
+
204
+ dimension = features.size
205
+
206
+ if @feature_dimension.nil?
207
+ @feature_dimension = dimension
208
+ elsif dimension != @feature_dimension
209
+ raise ArgumentError, "Feature dimension mismatch: expected #{@feature_dimension}, got #{dimension}"
210
+ end
211
+ end
212
+
213
+ def validate_outcome!(outcome)
214
+ return if @outcomes.include?(outcome)
215
+ raise ArgumentError, "Invalid outcome: #{outcome}. Must be one of #{@outcomes}"
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SQA
4
+ module BI
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/sqa/bi.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bi/version"
4
+ require_relative "bi/prior"
5
+ require_relative "bi/likelihood"
6
+ require_relative "bi/posterior"
7
+ require_relative "bi/time_series_predictor"
8
+ require_relative "bi/llm_support"
9
+ require_relative "bi/llm_prior_elicitor"
10
+ require_relative "bi/llm_likelihood_estimator"
11
+
12
+ module SQA
13
+ # Bayesian inference over discrete outcomes from time series data.
14
+ #
15
+ # SQA::BI answers questions of the form "given these features, what is
16
+ # the probability distribution over outcomes?" — where the outcomes are
17
+ # a small ordered set such as [-2, -1, 0, 1, 2] (strong downtrend
18
+ # through strong uptrend). It provides:
19
+ #
20
+ # - Prior distributions with Laplace smoothing
21
+ # - Gaussian kernel density estimation for likelihoods
22
+ # - Bayesian posterior computation, with entropy, confidence,
23
+ # KL divergence from the prior, and sampling
24
+ # - An LLM as an optional prior elicitor or likelihood function over
25
+ # textual evidence, where Ruby still does all the arithmetic
26
+ #
27
+ # The core math is domain-agnostic — it knows nothing about markets.
28
+ # This gem therefore does not depend on the +sqa+ gem; see
29
+ # +examples/03_stock_market_prediction_v2.rb+ for the market-facing
30
+ # side, which bridges SQA::DataFrame into a feature vector.
31
+ #
32
+ # @example Basic usage
33
+ # include SQA::BI
34
+ #
35
+ # predictor = TimeSeriesPredictor.new(
36
+ # outcomes: [-2, -1, 0, 1, 2],
37
+ # bandwidth: 1.0
38
+ # )
39
+ #
40
+ # predictor.train([1.0, 2.0, 3.0], outcome: 1)
41
+ # predictor.train([1.1, 2.1, 2.9], outcome: 1)
42
+ #
43
+ # posterior = predictor.predict([1.05, 2.05, 3.0])
44
+ # puts posterior.summary
45
+ module BI
46
+ class Error < StandardError; end
47
+
48
+ # Convenience constructor for a predictor.
49
+ #
50
+ # @param outcomes [Array] discrete outcomes
51
+ # @param bandwidth [Float] kernel bandwidth
52
+ # @return [TimeSeriesPredictor]
53
+ def self.predictor(outcomes:, bandwidth: 1.0, **)
54
+ TimeSeriesPredictor.new(outcomes: outcomes, bandwidth: bandwidth, **)
55
+ end
56
+ end
57
+ end
data/mkdocs.yml ADDED
@@ -0,0 +1,174 @@
1
+ # MkDocs configuration for SQA::BI documentation
2
+ site_name: SQA::BI — Bayesian Inference
3
+ site_description: Bayesian inference over discrete outcomes from time series data
4
+ site_author: Dewayne VanHoozer
5
+ site_url: https://madbomber.github.io/sqa-bi
6
+ copyright: Copyright &copy; 2026 Dewayne VanHoozer
7
+
8
+ # Repository information
9
+ repo_name: madbomber/sqa-bi
10
+ repo_url: https://github.com/MadBomber/sqa-bi
11
+ edit_uri: edit/main/docs/
12
+
13
+ # Configuration
14
+ theme:
15
+ name: material
16
+
17
+ # Color scheme - default to dark mode
18
+ palette:
19
+ # Palette toggle for dark mode (default)
20
+ - scheme: slate
21
+ primary: indigo
22
+ accent: cyan
23
+ toggle:
24
+ icon: material/brightness-7
25
+ name: Switch to light mode
26
+
27
+ # Palette toggle for light mode
28
+ - scheme: default
29
+ primary: indigo
30
+ accent: cyan
31
+ toggle:
32
+ icon: material/brightness-4
33
+ name: Switch to dark mode
34
+
35
+ # Typography
36
+ font:
37
+ text: Roboto
38
+ code: Roboto Mono
39
+
40
+ # Theme features
41
+ features:
42
+ # Navigation
43
+ - navigation.instant
44
+ - navigation.tracking
45
+ - navigation.tabs
46
+ - navigation.tabs.sticky
47
+ - navigation.sections
48
+ - navigation.path
49
+ - navigation.indexes
50
+ - navigation.top
51
+
52
+ # Table of contents
53
+ - toc.follow
54
+
55
+ # Search
56
+ - search.suggest
57
+ - search.highlight
58
+ - search.share
59
+
60
+ # Header
61
+ - header.autohide
62
+
63
+ # Content
64
+ - content.code.copy
65
+ - content.code.annotate
66
+ - content.tabs.link
67
+ - content.tooltips
68
+ - content.action.edit
69
+ - content.action.view
70
+
71
+ # Link and reference validation. With --strict these become build errors,
72
+ # which is what keeps the cross-references in this site honest.
73
+ validation:
74
+ omitted_files: warn
75
+ absolute_links: warn
76
+ unrecognized_links: warn
77
+ anchors: warn
78
+
79
+ # Plugins
80
+ plugins:
81
+ - search:
82
+ separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
83
+
84
+ # Extensions
85
+ markdown_extensions:
86
+ # Python Markdown
87
+ - abbr
88
+ - admonition
89
+ - attr_list
90
+ - def_list
91
+ - footnotes
92
+ - md_in_html
93
+ - tables
94
+ - toc:
95
+ permalink: true
96
+ title: On this page
97
+
98
+ # Python Markdown Extensions
99
+ - pymdownx.arithmatex:
100
+ generic: true
101
+ - pymdownx.betterem:
102
+ smart_enable: all
103
+ - pymdownx.caret
104
+ - pymdownx.details
105
+ - pymdownx.emoji:
106
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
107
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
108
+ - pymdownx.highlight:
109
+ anchor_linenums: true
110
+ line_spans: __span
111
+ pygments_lang_class: true
112
+ - pymdownx.inlinehilite
113
+ - pymdownx.keys
114
+ - pymdownx.magiclink:
115
+ repo_url_shorthand: true
116
+ user: madbomber
117
+ repo: sqa-bi
118
+ - pymdownx.mark
119
+ - pymdownx.smartsymbols
120
+ - pymdownx.superfences
121
+ - pymdownx.tabbed:
122
+ alternate_style: true
123
+ - pymdownx.tasklist:
124
+ custom_checkbox: true
125
+ - pymdownx.tilde
126
+
127
+ # Extra CSS and JavaScript
128
+ extra_css:
129
+ - assets/css/custom.css
130
+
131
+ extra_javascript:
132
+ - assets/js/mathjax.js
133
+ - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
134
+
135
+ # Social media and extra configuration
136
+ extra:
137
+ social:
138
+ - icon: fontawesome/brands/github
139
+ link: https://github.com/madbomber/sqa-bi
140
+ name: SQA::BI on GitHub
141
+ - icon: fontawesome/solid/gem
142
+ link: https://rubygems.org/gems/sqa-bi
143
+ name: SQA::BI on RubyGems
144
+
145
+ # Navigation
146
+ nav:
147
+ - Home: index.md
148
+ - Getting Started:
149
+ - Installation: getting-started/installation.md
150
+ - Quick Start: getting-started/quick-start.md
151
+ - Core Concepts: getting-started/core-concepts.md
152
+ - Guide:
153
+ - The Prior: guide/prior.md
154
+ - The Likelihood: guide/likelihood.md
155
+ - The Posterior: guide/posterior.md
156
+ - The Predictor: guide/predictor.md
157
+ - Quantifying Uncertainty: guide/uncertainty.md
158
+ - Tuning and Modeling Choices: guide/tuning.md
159
+ - LLM Integration:
160
+ - Overview: llm/index.md
161
+ - Prior Elicitation: llm/prior-elicitation.md
162
+ - Likelihood Estimation: llm/likelihood-estimation.md
163
+ - Providers and Local Models: llm/providers.md
164
+ - API Reference:
165
+ - Overview: api/index.md
166
+ - SQA::BI::Prior: api/prior.md
167
+ - SQA::BI::Likelihood: api/likelihood.md
168
+ - SQA::BI::Posterior: api/posterior.md
169
+ - SQA::BI::TimeSeriesPredictor: api/time-series-predictor.md
170
+ - SQA::BI::LlmSupport: api/llm-support.md
171
+ - LLM Elicitors: api/llm-elicitors.md
172
+ - Examples: examples/index.md
173
+ - Development: development.md
174
+ - Exploration Notes: EXPLORATION.md