Thursday, January 8, 2009

Very draft version of the RubyRx clinical programing language

Thanks to John for posting those notes from the latest session.

Bellow is a rough draft of the RubyRx Domain Specific Language (DSL). RubyRx is a language that will be used to analyze data collected during clinical trial of pharmaceutical drugs and medical devices.

This will certainly look very familiar to the folks who joined us for the session on January 6th. I have made some changes to simplify and clarify the code.

The basic idea is that we create a class that has a single array instance variable, plus a method to push elements onto that array and a method to access the array from outside the object.

Then I instantiate an object from that class, and push a bunch of hashes into the object's array instance variable. The data in the hashes are simple versions of the rows of data that we typically find in demographics data collected during clinical trials. Each hash would be equivalent to a row in a data set (or a database table) and the key-value pairs in the hash are equivalent to the variables in a data set or database table. (By the way, this is just my quick and dirty way of creating some data to work with -- all of this will shortly be replaced by database tables, and ActiveRecord classes and methods.)

The next part is an Output class. In the Output class, the initialize method provides the facility to create an array instance variable of the object created above (and that object's hash with the demographics data). The Output class also has a freq_by_trtgrp method. This method accepts an array of symbols as its parameter and produces a variable for each of of the symbols in the parameter. For example, if the parameter is race and sex, instantiated objects of the Output class will have two instance variables (@race and @sex). These instance variables will be nested hashes 3 levels deep. The levels will be the type of statistic requested (:frequency, in this case), the dose group (say, 'Placebo'), and the values of race ('WHITE', 'ASIAN' and so forth), each with a corresponding integer of how often that particular value of race occurred. For example:

p @race # {:frequency => {'Placebo' => {'WHITE' => 12, 'ASIAN' => 7, 'BLACK' => 17}, '0.2 mg' => {'ASIAN' => 10, 'BLACK' => 3, 'WHITE' => 22}}}

The idea is to use this nested hash (along with similar ones for sex, age, BMI and so forth) to create an output summary table for demographics. I will write more on that in a later post.

Here's the code:

t0 = Time.new

def nested_hash(levels, &def_proc)
inner_hash = def_proc ? Hash.new(&def_proc) : Hash.new(0)
if levels == 1
inner_hash
else
l = [lambda {|h,k| h[k] = inner_hash}]
(levels-2).times do |i|
l[i+1] = lambda {|h,k| h[k] = Hash.new( &l[i] ) }
end
Hash.new(&l[-1])
end
end

class DM
attr_accessor :sex, :race, :brthdtc, :trtgrp

def initialize(obj)
@sex = obj[:sex]
@race = obj[:race]
@brthdtc = obj[:brthdtc]
@trtgrp = obj[:trtgrp]
end
end

class Output
attr_accessor :sex, :race, :brthdtc, :trtgrp

def initialize(sdtm_data)
@sdtm_data = sdtm_data
end

def freq_by_trtgrp(*args)
args.each do |arg|
instance_eval("@#{arg} = {}")

instance_eval("@#{arg}[:frequency] = nested_hash(2)")

@sdtm_data.a.each do |e|
instance_eval("@#{arg}[:frequency][e.trtgrp][e.#{arg}] += 1")
end
end

return self
end
end

class DM_Table
attr_accessor :a

def initialize
@a = []
end

def << (obj)
@a << obj
end
end

dt = DM_Table.new
dt << trtgrp =""> 'Placebo', :sex => 'M', :race => 'BLACK', :brthdtc => '1968-10-01'})
dt << trtgrp =""> 'VELCADE', :sex => 'F', :race => 'ASIAN', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'Placebo', :sex => 'M', :race => 'WHITE', :brthdtc => '1968-10-01'})
dt << trtgrp =""> 'VELCADE', :sex => 'M', :race => 'ASIAN', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'VELCADE', :sex => 'F', :race => 'ASIAN', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'VELCADE', :sex => 'M', :race => 'WHITE', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'Placebo', :sex => 'F', :race => 'WHITE', :brthdtc => '1968-10-01'})
dt << trtgrp =""> 'Placebo', :sex => 'M', :race => 'BLACK', :brthdtc => '1968-10-01'})
dt << trtgrp =""> 'Placebo', :sex => 'M', :race => 'WHITE', :brthdtc => '1968-10-01'})
dt << trtgrp =""> 'VELCADE', :sex => 'F', :race => 'ASIAN', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'Placebo', :sex => 'M', :race => 'WHITE', :brthdtc => '1968-10-01'})
dt << trtgrp =""> 'VELCADE', :sex => 'M', :race => 'ASIAN', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'VELCADE', :sex => 'F', :race => 'ASIAN', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'VELCADE', :sex => 'M', :race => 'WHITE', :brthdtc => '1970-07-04'})
dt << trtgrp =""> 'Placebo', :sex => 'F', :race => 'WHITE', :brthdtc => '1968-10-01'})
dt << trtgrp =""> 'Placebo', :sex => 'F', :race => 'BLACK', :brthdtc => '1968-10-01'})

begin
o = Output.new(dt)
o.freq_by_trtgrp(:sex, :race)
p o.sex
p o.race
rescue => e
puts e
ensure
puts "Elapsed time: #{ Time.new - t0 }"
end


The output of executing the above code is:

{:frequency=>{"VELCADE"=>{"M"=>9, "F"=>7}, "Placebo"=>{"M"=>9, "F"=>7}}}
{:frequency=>{"VELCADE"=>{"ASIAN"=>6, "WHITE"=>7, "BLACK"=>3}, "Placebo"=>{"ASIAN"=>6, "WHITE"=>7, "BLACK"=>3}}}
Elapsed time: 0.001752

Pleas let me know if there are any questions or comments on this code.

Thanks,

Glenn

No comments: