Namo
Named dimensional data for Ruby.
Namo is a Ruby library for working with multi-dimensional data using named dimensions. It infers dimensions and coordinates from plain arrays of hashes — the same shape you get from databases, CSV files, JSON, and YAML — so there's no reshaping step.
Installation
gem install namo
Or in your Gemfile:
gem 'namo'
Usage
Create a Namo instance from an array of hashes:
require 'namo'
sales = Namo.new([
{product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
{product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
{product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
{product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
])
Dimensions and coordinates are inferred:
sales.dimensions
# => [:product, :quarter, :price, :quantity]
sales.coordinates[:product]
# => ['Widget', 'Gadget']
sales.coordinates[:quarter]
# => ['Q1', 'Q2']
Selection
Select by named dimension using keyword arguments:
# Single value
sales[product: 'Widget']
# => #<Namo [
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
# ]>
# Multiple dimensions
sales[product: 'Widget', quarter: 'Q1']
# => #<Namo [
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}
# ]>
# Range
sales[price: 10.0..20.0]
# => #<Namo [
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
# {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
# ]>
# Array of values
sales[quarter: ['Q1']]
# => #<Namo [
# {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
# {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
# ]>
Projection
Project to specific dimensions:
sales[:product, :price]
# => #<Namo [
# {product: 'Widget', price: 10.0},
# {product: 'Widget', price: 10.0},
# {product: 'Gadget', price: 25.0},
# {product: 'Gadget', price: 25.0}
# ]>
Selection and projection can be chained:
sales[product: 'Widget'][:quarter, :price]
# => #<Namo [
# {quarter: 'Q1', price: 10.0},
# {quarter: 'Q2', price: 10.0}
# ]>
Or combined in a single call (names before selectors):
sales[:quarter, :price, product: 'Widget']
# => #<Namo [
# {quarter: 'Q1', price: 10.0},
# {quarter: 'Q2', price: 10.0}
# ]>
Selection and projection always return a new Namo instance, so everything chains.
Formulae
Define computed dimensions using []=:
sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
sales[:product, :quarter, :revenue]
# => #<Namo [
# {product: 'Widget', quarter: 'Q1', revenue: 1000.0},
# {product: 'Widget', quarter: 'Q2', revenue: 1500.0},
# {product: 'Gadget', quarter: 'Q1', revenue: 1000.0},
# {product: 'Gadget', quarter: 'Q2', revenue: 1500.0}
# ]>
Formulae compose:
sales[:cost] = proc{|row| row[:quantity] * 4.0}
sales[:profit] = proc{|row| row[:revenue] - row[:cost]}
sales[:product, :quarter, :profit]
# => #<Namo [
# {product: 'Widget', quarter: 'Q1', profit: 600.0},
# {product: 'Widget', quarter: 'Q2', profit: 900.0},
# {product: 'Gadget', quarter: 'Q1', profit: 840.0},
# {product: 'Gadget', quarter: 'Q2', profit: 1260.0}
# ]>
Formulae work with selection and projection:
sales[product: 'Widget'][:revenue, :quarter]
# => #<Namo [
# {revenue: 1000.0, quarter: 'Q1'},
# {revenue: 1500.0, quarter: 'Q2'}
# ]>
Formulae carry through selection — a filtered Namo instance remembers its formulae.
Enumerable
Namo includes Enumerable, so each, reduce, map, select, min_by, and all the rest work out of the box. Rows are yielded as Row objects, so formulae are accessible during enumeration:
sales.reduce(0){|sum, row| sum + row[:quantity]}
# => 350
sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:quantity]}
# => 250
sales[:revenue] = proc{|row| row[:price] * row[:quantity]}
sales.reduce(0){|sum, row| sum + row[:revenue]}
# => 5000.0
sales[product: 'Widget'].reduce(0){|sum, row| sum + row[:revenue]}
# => 2500.0
sales.map{|row| row[:product]}
# => ['Widget', 'Widget', 'Gadget', 'Gadget']
sales.min_by{|row| row[:price]}[:product]
# => 'Widget'
sales.flat_map{|row| [row[:price]]}
# => [10.0, 10.0, 25.0, 25.0]
Extracting data
to_a returns an array of hashes:
sales[:product, :quarter, :revenue].to_a
# => [
# {product: 'Widget', quarter: 'Q1', revenue: 1000.0},
# {product: 'Widget', quarter: 'Q2', revenue: 1500.0},
# {product: 'Gadget', quarter: 'Q1', revenue: 1000.0},
# {product: 'Gadget', quarter: 'Q2', revenue: 1500.0}
# ]
Why?
Every other multi-dimensional array library requires you to pre-shape your data before you can work with it. Namo takes it in the form it likely already comes in.
Name
Namo: nam(ed) (dimensi)o(ns). A companion to Numo (numeric arrays for Ruby). And in Aussie culture 'o' gets added to the end of names.
Contributing
- Fork it (https://github.com/thoran/namo/fork)
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new pull request
License
MIT