File Coverage

blib/lib/DiaColloDB/Profile/Multi.pm
Criterion Covered Total %
statement 12 161 7.4
branch 0 70 0.0
condition 0 72 0.0
subroutine 4 23 17.3
pod 17 18 94.4
total 33 344 9.5


line stmt bran cond sub pod time code
1             ## -*- Mode: CPerl -*-
2             ##
3             ## File: DiaColloDB::Profile::Multi.pm
4             ## Author: Bryan Jurish <moocow@cpan.org>
5             ## Description: collocation db, co-frequency profiles, by date
6              
7              
8             package DiaColloDB::Profile::Multi;
9 1     1   7 use DiaColloDB::Profile;
  1         2  
  1         27  
10 1     1   5 use DiaColloDB::Persistent;
  1         2  
  1         19  
11 1     1   4 use DiaColloDB::Utils qw(:html :list);
  1         1  
  1         39  
12 1     1   205 use strict;
  1         2  
  1         2057  
13              
14             ##==============================================================================
15             ## Globals & Constants
16              
17             our @ISA = qw(DiaColloDB::Persistent);
18              
19             ##==============================================================================
20             ## Constructors etc.
21              
22             ## $mp = CLASS_OR_OBJECT->new(%args)
23             ## + %args, object structure:
24             ## (
25             ## profiles => \@profiles, ##-- ($profile, ...) : sub-profiles, with {label} key
26             ## titles => \@titles, ##-- item group titles (default:undef: unknown)
27             ## qinfo => \%qinfo, ##-- query info (optional)
28             ## )
29             ## + %qinfo structure:
30             ## (
31             ## q12 => $q12, ##-- collocation-pair (w1,w2) count-query string (DDC)
32             ## q1 => $q1, ##-- collocation-item (w1) count-query string (DDC)
33             ## q2 => $q2, ##-- collocation-item (w2) count-query string (DDC)
34             ## qN => $qN, ##-- total frequency count-query string (DDC)
35             ## fcoef => $fcoef, ##-- item count coefficient (DDC)
36             ## qtemplate => $qtemplate, ##-- template query string (replace '__W2.i__' with w2 item property #i (e.g. 0:date, 1:lemma, ...))
37             ## )
38             sub new {
39 0     0 1   my $that = shift;
40 0   0       my $mp = bless({
41             profiles=>[],
42             #titles=>undef,
43             #qinfo=>{},
44             @_
45             }, (ref($that)||$that));
46 0           return $mp;
47             }
48              
49             ## $mp2 = $mp->clone()
50             ## $mp2 = $mp->clone($keep_compiled)
51             ## + clones %$mp
52             ## + if $keep_score is true, compiled data is cloned too
53             sub clone {
54 0     0 1   my $mp = shift;
55 0           my $profiles = $mp->{profiles};
56             return bless({
57 0           profiles=>[map {$_->clone(@_)} @$profiles],
58 0           ($mp->{titles} ? (titles=>[@{$mp->{titles}}]) : qw()),
59 0 0         ($mp->{qinfo} ? (qinfo=>{%{$mp->{qinfo}}}) : qw()),
  0 0          
60             }, ref($mp)
61             );
62             }
63              
64             ## $size = $mp->size()
65             ## + returns total number of collocates in any sub-profile
66             sub size {
67 0     0 0   my $mp = shift;
68 0           my $size = 0;
69 0           $size += $_->size foreach (grep {defined($_)} @{$mp->{profiles}});
  0            
  0            
70 0           return $size;
71             }
72              
73             ##==============================================================================
74             ## I/O
75              
76             ##--------------------------------------------------------------
77             ## I/O: JSON
78             ## + mostly INHERITED from DiaCollocDB::Persistent
79              
80             ## $obj = $CLASS_OR_OBJECT->loadJsonData( $data,%opts)
81             ## + guts for loadJsonString(), loadJsonFile()
82             sub loadJsonData {
83 0     0 1   my $that = shift;
84 0           my $mp = $that->DiaColloDB::Persistent::loadJsonData(@_);
85 0   0       bless($_,'DiaColloDB::Profile') foreach (@{$mp->{profiles}//[]});
  0            
86 0           return $mp;
87             }
88              
89             ##--------------------------------------------------------------
90             ## I/O: Text
91              
92             ## undef = $CLASS_OR_OBJECT->saveTextHeader($fh, hlabel=>$hlabel, titles=>\@titles)
93             sub saveTextHeader {
94 0     0 1   my ($that,$fh,%opts) = @_;
95 0           DiaColloDB::Profile::saveTextHeader($that,$fh,hlabel=>'label',@_);
96             }
97              
98             ## $bool = $obj->saveTextFile($filename_or_handle, %opts)
99             ## + wraps saveTextFh(); INHERITED from DiaCollocDB::Persistent
100              
101             ## $bool = $mp->saveTextFh($fh,%opts)
102             ## + %opts:
103             ## header => $bool, ##-- include header-row? (default=1)
104             ## ... ##-- other options passed to DiaColloDB::Profile::saveTextFh()
105             ## + save text representation to a filehandle (guts)
106             sub saveTextFh {
107 0     0 1   my ($mp,$fh,%opts) = @_;
108 0           my $ps = $mp->{profiles};
109 0 0 0       $mp->saveTextHeader($fh,%opts) if ($opts{header}//1);
110 0           foreach (@$ps) {
111 0 0         $_->saveTextFh($fh,%opts,header=>0)
112             or $mp->logconfess("saveTextFile() saved for sub-profile with label '", $_->label, "': $!");
113             }
114 0           return $mp;
115             }
116              
117             ##--------------------------------------------------------------
118             ## I/O: HTML
119              
120             ## $bool = $mp->saveHtmlFile($filename_or_handle, %opts)
121             ## + %opts:
122             ## (
123             ## table => $bool, ##-- include <table>..</table> ? (default=1)
124             ## body => $bool, ##-- include <html><body>..</html></body> ? (default=1)
125             ## qinfo => $varname, ##-- include <script> for qinfo data? (default='qinfo')
126             ## header => $bool, ##-- include header-row? (default=1)
127             ## format => $fmt, ##-- printf score formatting (default="%.2f")
128             ## )
129             sub saveHtmlFile {
130 0     0 1   my ($mp,$file,%opts) = @_;
131 0 0         my $fh = ref($file) ? $file : IO::File->new(">$file");
132 0 0         $mp->logconfess("saveHtmlFile(): failed to open '$file': $!") if (!ref($fh));
133 0 0 0       $fh->print("<html><body>\n") if ($opts{body}//1);
134             $fh->print("<script type=\"text/javascript\">$opts{qinfo}=", DiaColloDB::Utils::saveJsonString($mp->{qinfo}, pretty=>0), ";</script>\n")
135 0 0 0       if ($mp->{qinfo} && ($opts{qinfo} //= 'qinfo'));
      0        
136 0 0 0       $fh->print("<table><tbody>\n") if ($opts{table}//1);
137             $fh->print("<tr>",(
138 0           map {"<th>".htmlesc($_)."</th>"}
139             qw(N f1 f2 f12 score),
140             qw(label),
141 0   0       @{$mp->{titles}//[qw(item2)]},
142             ),
143             "</tr>\n"
144 0 0 0       ) if ($opts{header}//1);
145 0           my $ps = $mp->{profiles};
146 0           foreach (@$ps) {
147 0 0         $_->saveHtmlFile($file, %opts,table=>0,body=>0,header=>0)
148             or $mp->logconfess("saveHtmlFile() saved for sub-profile with label '", $_->label, "': $!");
149             }
150 0 0 0       $fh->print("</tbody><table>\n") if ($opts{table}//1);
151 0 0 0       $fh->print("</body></html>\n") if ($opts{body}//1);
152 0 0         $fh->close() if (!ref($file));
153 0           return $mp;
154             }
155              
156             ##==============================================================================
157             ## Compilation and Trimming
158              
159             ## $mp_or_undef = $mp->compile($func,%opts)
160             ## + compile all sub-profiles for score-function $func, one of qw(f mi ld); default='f'
161             ## + %opts are passed to sub-profile compile()
162             sub compile {
163 0     0 1   my ($mp,$func,%opts) = @_;
164 0   0       $_->compile($func,%opts) or return undef foreach (@{$mp->{profiles}});
  0            
165 0           return $mp;
166             }
167              
168             ## $mp = $mp->uncompile()
169             ## + un-compiles all scores for $mp
170             sub uncompile {
171 0     0 1   $_->uncompile() foreach (@{$_[0]{profiles}});
  0            
172 0           return $_[0];
173             }
174              
175             ## $class = $CLASS_OR_OBJECT->pclass()
176             ## + class for psum()
177             sub pclass {
178 0     0 1   return 'DiaColloDB::Profile';
179             }
180              
181             ## $prf = $mp->sumover()
182             ## $prf = $CLASS_OR_OBJECT->sumover(\@profiles,%opts)
183             ## + sum of sub-profiles, compiled as for $profiles[0]
184             ## + used for global trimming,
185             ## + local %opts:
186             ## autoN => $bool, ##-- whether to guess whether we're using a pre-v0.12 style shared N (default=true)
187             ## + other %opts are passed to sub-profile compile() method if called
188             sub sumover {
189 0     0 1   my $that = shift;
190 0 0 0       my $prfs = (@_ ? shift : undef) // (ref($that) ? $that->{profiles} : undef) // [];
    0 0        
191 0           my %opts = @_;
192              
193             ##-- guess whether to sum sub-profile N (for compatibility with diacollo <= v0.11)
194             my $sumN = (!defined($opts{autoN}) || $opts{autoN}
195 0 0 0       ? (@$prfs ? (grep {$_->{N} != $prfs->[0]{N}} @$prfs) : 1)
  0 0          
196             : (@$prfs <= 1));
197 0           delete $opts{autoN};
198              
199 0 0         my $psum = $that->pclass->new(N=>($sumN ? 0 : $prfs->[0]{N}))->_sum($prfs,N=>$sumN,f1=>1);
200 0 0 0       $psum->compile($prfs->[0]{score}, %opts) if ($prfs->[0] && $prfs->[0]{score});
201 0           return $psum;
202             }
203              
204             ## $mp_or_undef = $mp->trim(%opts)
205             ## + %opts: as for DiaColloDB::Profile::trim(), also:
206             ## (
207             ## empty => $bool, ##-- remove empty sub-profiles? (default=true)
208             ## global => $bool, ##-- trim sub-profiles globally (default=false)
209             ## extend => \%label2keys, ##-- maps Profile::Multi labels to trim() keys
210             ## )
211             ## + calls $prf->trim(%opts) for each sub-profile $prf
212             sub trim {
213 0     0 1   my ($mp,%opts) = @_;
214              
215             ##-- defaults
216 0   0       $opts{kbest} //= -1;
217 0   0       $opts{cutoff} //= '';
218 0   0       $opts{global} //= 0;
219              
220             ##-- trim empty sub-profiles
221 0 0 0       @{$mp->{profiles}} = grep {!$_->empty} @{$mp->{profiles}} if (!exists($opts{empty}) || $opts{empty});
  0            
  0            
  0            
222              
223 0 0         if ($opts{extend}) {
    0          
224             ##-- "extend" mode: slice-dependent trimming for 2nd-pass distributed queries
225 0           my $extend = $opts{extend};
226             $_->trim(%opts,keep=>($extend->{$_->{label}}//{})) or return undef
227 0   0       foreach (grep {defined($_)} @{$mp->{profiles}});
  0   0        
  0            
228             }
229             elsif (!$opts{global}) {
230             ##-- slice-local trimming (default)
231 0   0       $_->trim(%opts) or return undef foreach (grep {defined($_)} @{$mp->{profiles}});
  0            
  0            
232             } else {
233             ##-- global trimming
234 0           my $psum = $mp->sumover();
235              
236             ##-- DEBUG: dump sum
237             #$psum->logwarn("global trimming basis:");
238             #$psum->saveTextFh(\*STDERR);
239              
240 0           my %pkeys = map {($_=>undef)} @{$psum->which(%opts)};
  0            
  0            
241 0   0       $_->trim(keep=>\%pkeys) or return undef foreach (@{$mp->{profiles}});
  0            
242             }
243 0           return $mp;
244             }
245              
246             ## $mp = $mp->stringify( $obj)
247             ## $mp = $mp->stringify(\@key2str)
248             ## $mp = $mp->stringify(\&key2str)
249             ## $mp = $mp->stringify(\%key2str)
250             ## + stringifies multi-profile (destructive) via $obj->i2s($key2), $key2str->($i2) or $key2str->{$i2}
251             sub stringify {
252 0     0 1   my $mp = shift;
253 0   0       $_->stringify(@_) or return undef foreach (@{$mp->{profiles}});
  0            
254 0           return $mp;
255             }
256              
257             ## @ppairs = $CLASS_OR_OBJECT->align($mp1,$mp2)
258             ## \@ppairs = $CLASS_OR_OBJECT->align($mp1,$mp2)
259             ## + aligns subprofile-pairs from $mp1 and $mp2
260             ## + $mp1, $mp2 are either:
261             ## - HASH-refs with a 'profiles' key (e.g. DiaColloDB::Profile::Multi objects)
262             ## - ARRAY-refs of DiaColloDB::Profile-like objects to align
263             ## + subprofiles are aligned in stored order
264             ## + arguments must be EITHER singletons (1 subprofile) OR of same size
265             ## - this lets you compare e.g. a global profile with a sliced one by
266             ## something like PDL's "implicit threading"
267             ## + formerly defined in DiaColloDB::Profile::MultiDiff
268             sub align {
269 0     0 1   my ($that,$mpa,$mpb) = @_;
270 0 0         my $psa = UNIVERSAL::isa($mpa,'HASH') ? $mpa->{profiles} : $mpa;
271 0 0         my $psb = UNIVERSAL::isa($mpb,'HASH') ? $mpb->{profiles} : $mpb;
272 0 0 0       if (@$psa==1 || @$psb==1 || @$psa==@$psb) {
      0        
273             ##-- align cyclically (allow slices)
274             my @pairs = map {
275 0 0         [
276 0 0 0       (@$psa==1 && $_ != 0 ? $psa->[0]->clone(1) : $psa->[$_]),
    0 0        
277             (@$psb==1 && $_ != 0 ? $psb->[0]->clone(1) : $psb->[$_]),
278             ]
279             } (0..($#$psa > $#$psb ? $#$psa : $#$psb));
280 0 0         return wantarray ? @pairs : \@pairs;
281             }
282 0           $that->logconfess("align(): cannot align non-trivial multi-profiles of unequal size (".scalar(@$psa)." != ".scalar(@$psb).")");
283             }
284              
285             ## \@mps = $CLASS_OR_OBJECT->xfill(\@mps, %opts)
286             ## @mps = $CLASS_OR_OBJECT->xfill(\@mps, %opts)
287             ## + ensure sub-profile labels for all MultiProfiles in \@mps are identical, and can be passed to align()
288             ## + %opts:
289             ## class => $class, ##-- null profile class (for filling)
290             sub xfill {
291 0     0 1   my ($that,$mps,%opts) = @_;
292 0   0       my $class = $opts{class} || 'DiaColloDB::Profile';
293              
294             ##-- collect pseudo-set of all labels
295 0 0         my $labels = sluniq [sort {$a <=> $b || $a cmp $b} map {$_->{label}} map {@{$_->{profiles}}} @$mps];
  0            
  0            
  0            
  0            
296              
297 0           my ($mp,%l2p);
298 0           foreach $mp (@$mps) {
299 0           %l2p = (map {($_->{label}=>$_)} @{$mp->{profiles}});
  0            
  0            
300 0           $l2p{$_} = $class->new(label=>$_, N=>0,f1=>0) foreach (grep {!exists($l2p{$_})} @$labels);
  0            
301 0           @{$mp->{profiles}} = @l2p{@$labels};
  0            
302             }
303              
304 0 0         return wantarray ? @$mps : $mps;
305             }
306              
307             ## \@xkeys = $CLASS_OR_OBJECT->xkeys(\@mps)
308             ## @xkeys = $CLASS_OR_OBJECT->xkeys(\@mps)
309             ## + find missing slice-wise item keys for each profile in \@mps
310             ## + argument multi-profiles in \@mps must have identical sub-profile labels (see xfill() method)
311             ## + returns a list @xkeys=(\%xkeys1,...,\%xkeysN) for argument @mps=($mp1,...,$mpN),
312             ## where $xkeys[$i] is a HASH-ref of the form C<( $label => \@sxkeys, ... )> whose
313             ## keys are sub-profile labels (slices) and whose values are ARRAY-refs C<\@sxkeys>
314             ## of those item-keys C<$sxkey> present in some sub-profile C<$p> of some argument
315             ## C<$mps[$j]> with C<($j != $i)>, C<($p-E<gt>{label} eq $label)>, and missing
316             ## from C<$mps[$i]>, i.e. C<(!exists $mps[$i]{f12}{$sxkey})>.
317             sub xkeys {
318 0     0 1   my ($that,$mps) = @_;
319              
320             ##-- index profiles by slice : %s2prf = ( $slice=>\@sliceProfilesByI )
321 0           my (%s2prf,$prf,$i);
322 0           for ($i=0; $i <= $#$mps; ++$i) {
323 0           foreach $prf (@{$mps->[$i]{profiles}}) {
  0            
324 0           $s2prf{$prf->{label}}[$i] = $prf;
325             }
326             }
327              
328             ##-- find "missing" keys by slice
329 0           my @xkeys = qw();
330 0           my ($slice,$prfs, $skeys,$pf12);
331 0           while (($slice,$prfs)=each %s2prf) {
332 0           $skeys = luniq [map {keys %{$_->{f12}}} grep {defined($_)} @{$prfs}];
  0            
  0            
  0            
  0            
333 0           for ($i=0; $i <= $#$mps; ++$i) {
334 0 0 0       $pf12 = ($prfs->[$i] ? $prfs->[$i]{f12} : undef) // {};
335 0           $xkeys[$i]{$slice} = [grep {!exists $pf12->{$_}} @$skeys];
  0            
336             }
337             }
338              
339 0 0         return wantarray ? @xkeys : \@xkeys;
340             }
341              
342              
343             ##==============================================================================
344             ## Binary operations
345              
346             ## $mp = $mp->_add($mp2,%opts)
347             ## + adds $mp2 frequency data to $mp (destructive)
348             ## + implicitly un-compiles sub-profiles
349             ## + %opts: passed to Profile::_add()
350             sub _add {
351 0     0     my ($amp,$bmp) = (shift,shift);
352 0           my %a2data = map {($_->label=>$_)} @{$amp->{profiles}};
  0            
  0            
353 0           my ($bkey,$bprf,$aprf);
354 0           foreach $bprf (@{$bmp->{profiles}}) {
  0            
355 0           $bkey = $bprf->label;
356 0 0         if (defined($aprf=$a2data{$bkey})) {
357 0           $aprf->_add($bprf,@_);
358             } else {
359 0           $a2data{$bkey} = $bprf->clone();
360             }
361             }
362 0           @{$amp->{profiles}} = sort {$a->label cmp $b->label} values %a2data; ##-- re-sort
  0            
  0            
363 0           return $amp->uncompile();
364             }
365              
366             ## $mp3 = $mp1->add($mp2,%opts)
367             ## + returns sum of $mp1 and $mp2 frequency data (constructive)
368             ## + %opts: passed to Profile::_add()
369             sub add {
370 0     0 1   return $_[0]->clone->_add(@_[1..$#_]);
371             }
372              
373             ## $diff = $mp1->diff($mp2)
374             ## + returns score-diff of multi-profiles $mp1 and $mp2; wraps DiaColloDB::Profile::MultiDiff->new($mp1,$mp2)
375             sub diff {
376 0     0 1   return DiaColloDB::Profile::MultiDiff->new(@_);
377             }
378              
379              
380             ##==============================================================================
381             ## Footer
382             1;
383              
384             __END__