View Javadoc
1   /*
2    * The baseCode project
3    *
4    * Copyright (c) 2011 University of British Columbia
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
12   * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
13   * specific language governing permissions and limitations under the License.
14   */
15  package ubic.basecode.math.linearmodels;
16  
17  import java.util.ArrayList;
18  import java.util.Arrays;
19  import java.util.Collection;
20  import java.util.Collections;
21  import java.util.HashSet;
22  import java.util.LinkedHashMap;
23  import java.util.LinkedHashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import org.apache.commons.lang3.StringUtils;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import ubic.basecode.dataStructure.matrix.DenseDoubleMatrix;
33  import ubic.basecode.dataStructure.matrix.DoubleMatrix;
34  import ubic.basecode.dataStructure.matrix.ObjectMatrix;
35  import ubic.basecode.dataStructure.matrix.StringMatrix;
36  import cern.colt.matrix.DoubleMatrix2D;
37  import cern.colt.matrix.impl.DenseDoubleMatrix2D;
38  
39  /**
40   * Represents the A matrix in regression problems posed as Ax=b. The starting point is a matrix of sample information,
41   * where the rows are sample names and the columns are the names of factors or covariates. Intercept and interaction
42   * terms can be added.
43   * <p>
44   * Baseline levels are initially determined by the order in which factor levels appear. You can re-level using the
45   * setBaseline method.
46   * <p>
47   *
48   * @author paul
49   * 
50   */
51  public class DesignMatrix {
52  
53      private static Logger log = LoggerFactory.getLogger( DesignMatrix.class );
54      /**
55       *
56       */
57      private List<Integer> assign = new ArrayList<>();
58  
59      /**
60       * Names of factors for which at least some coefficients were dropped.
61       */
62      private final Set<String> droppedFactors = new HashSet<>();
63  
64      private boolean hasIntercept = false;
65  
66      private final Set<String[]> interactions = new LinkedHashSet<>();
67  
68      /**
69       * 
70       * @return a collection of String arrays. If empty, there are no interactions. Each array represents an interaction
71       *         in the model. The elements of the array are the terms included in the interaction, as Strings provided when calling addInteraction.
72       */
73      public Collection<String[]> getInteractionTerms() {
74          return interactions;
75      }
76  
77      /**
78       * Only applied for categorical factors.
79       */
80      private final Map<String, List<String>> levelsForFactors = new LinkedHashMap<>();
81  
82      private DoubleMatrix<String, String> matrix;
83  
84      /**
85       * Store which terms show up in which columns of the design
86       */
87      private Map<String, List<Integer>> terms = new LinkedHashMap<>();
88  
89      /**
90       * Saved version of the original factors provided.
91       */
92      private final Map<String, List<Object>> valuesForFactors = new LinkedHashMap<>();
93  
94      /**
95       * Whether to try to remove unusable factors from the design matrix (currently applies to interactions). If false,
96       * the design matrix might need modification by the caller.
97       */
98      private boolean strict = true;
99  
100     /**
101      * @param factor in form of Doubles or Strings. Any other types will yield errors.
102      * @param start
103      */
104     public DesignMatrix( Object[] factor, int start, String factorName ) {
105         matrix = this.buildDesign( 1, Arrays.asList( factor ), null, start, factorName );
106     }
107 
108     /**
109      * @param sampleInfo in form of Doubles or Strings. Any other types will yield errors.
110      */
111     public DesignMatrix( ObjectMatrix<String, String, ? extends Object> sampleInfo ) {
112         this( sampleInfo, true );
113     }
114 
115     /**
116      * @param sampleInfo in form of Doubles or Strings. Any other types will yield errors.
117      * @param intercept
118      */
119     public DesignMatrix( ObjectMatrix<String, String, ?> sampleInfo, boolean intercept ) {
120         matrix = this.designMatrix( sampleInfo, intercept );
121         this.hasIntercept = intercept;
122         if ( sampleInfo.getRowNames().size() == matrix.rows() ) matrix.setRowNames( sampleInfo.getRowNames() );
123 
124     }
125 
126     /**
127      * In limma, the contrasts are built by referring to the coefficients by name, which isn't what we want.
128      * 
129      * 
130      * @return
131      */
132     public DoubleMatrix2D makeContrasts() {
133         /*
134          * Limma: This function expresses contrasts between a set of parameters as a numeric matrix. The parameters are
135          * usually
136          * the coefficients from a linear model fit, so the matrix specifies which comparisons between the coefficients
137          * are to be extracted from the fit. The output from this function is usually used as input to contrasts.fit.
138          * The contrasts can be specified either as expressions using ... or as a character vector through contrasts.
139          * (Trying to specify contrasts both ways will cause an error.)
140          * 
141          * 
142          */
143         throw new RuntimeException();
144     }
145 
146     public DesignMatrix( StringMatrix<String, String> sampleInfo ) {
147         this( sampleInfo, true );
148     }
149 
150     /**
151      * @param design
152      * @param intercept whether to include an intercept in the model
153      * @param strict    whether to remove columns from the design matrix that are unlikely to be useable. By default
154      *                  this is "true" for other constructors.
155      */
156     public DesignMatrix( ObjectMatrix<String, String, Object> design, boolean intercept, boolean strict ) {
157         this( design, intercept );
158         this.strict = strict;
159     }
160 
161     /**
162      * Append additional factors/covariates to this
163      *
164      * @param sampleInfo
165      */
166     public void add( ObjectMatrix<String, String, Object> sampleInfo ) {
167         this.matrix = this.designMatrix( sampleInfo, this.matrix );
168     }
169 
170     /**
171      * Add interaction term; only works if there exactly two factors so this can figure out which interaction to add.
172      * For more control use the other method.
173      */
174     public void addInteraction() {
175         if ( this.terms.size() != 2 && !hasIntercept ) {
176             throw new IllegalArgumentException( "You must specify which two terms" );
177         }
178 
179         if ( this.terms.size() == 2 && hasIntercept || this.terms.size() < 2 ) {
180             throw new IllegalArgumentException( "You need at least two terms" );
181         }
182 
183         if ( this.terms.size() > 3 ) {
184             throw new IllegalArgumentException( "You must specify which two terms, there are " + this.terms.size()
185                     + " terms: " + StringUtils.join( this.terms.keySet(), "," ) );
186         }
187 
188         List<String> iterms = new ArrayList<>();
189         for ( String t : terms.keySet() ) {
190             if ( t.equals( LinearModelSummary.INTERCEPT_COEFFICIENT_NAME ) ) {
191                 continue;
192             }
193             iterms.add( t );
194         }
195 
196         this.addInteraction( iterms.toArray( new String[] {} ) );
197     }
198 
199     /**
200      * This will not add the interaction unless all of the terms are already part of the design, and interactions that
201      * obviously can't be estimated will be dropped if possible, but otherwise this is fairly brain dead.
202      *
203      * @param interactionTerms
204      */
205     public void addInteraction( String... interactionTerms ) {
206 
207         /*
208          * If any of these terms were already dropped, bail.
209          */
210         for ( String t1 : interactionTerms ) {
211             if ( !this.getLevelsForFactors().containsKey( t1 ) ) {
212                 log.warn( "Can't add interaction involving a non-existent or unused terms: " + t1 );
213                 return;
214             }
215         }
216 
217         /*
218          * Figure out which columns of the data we need to look at.
219          *
220          * If the factor is in >1 columns - we have to add two or more columns
221          */
222         Collection<String> doneTerms = new HashSet<>();
223 
224         // the column where the interaction "goes"
225         int interactionIndex = terms.size();
226 
227         String termName = StringUtils.join( interactionTerms, ":" );
228         Set<String> usedInteractionTerms = new HashSet<>();
229         Arrays.sort( interactionTerms );
230         for ( String t1 : interactionTerms ) {
231 
232             if ( doneTerms.contains( t1 ) ) continue;
233             List<Integer> cols1 = terms.get( t1 );
234 
235             for ( int i = 0; i < cols1.size(); i++ ) {
236                 double[] col1i = this.matrix.getColumn( cols1.get( i ) );
237 
238                 assert col1i.length > 0;
239 
240                 for ( String t2 : interactionTerms ) {
241                     if ( t1.equals( t2 ) ) continue;
242                     doneTerms.add( t2 );
243                     List<Integer> cols2 = terms.get( t2 );
244 
245                     assert cols2 != null;
246 
247                     for ( int j = 0; j < cols2.size(); j++ ) {
248                         double[] col2i = this.matrix.getColumn( cols2.get( j ) );
249 
250                         Double[] prod = new Double[col1i.length];
251 
252                         this.matrix = this.copyWithSpace( this.matrix, this.matrix.columns() + 1 );
253                         String columnName = null;
254                         int numValid = 0;
255                         for ( int k = 0; k < col1i.length; k++ ) {
256                             prod[k] = col1i[k] * col2i[k]; // compute the value for the interaction, should be either 0 or 1
257                             if ( prod[k] != 0 ) numValid++;
258                             // We don't really need the columnName to be distinct from the term name, it just
259                             // makes it clearer which factor values are considered non-zero combination.
260                             if ( prod[k] != 0 && StringUtils.isBlank( columnName ) ) {
261                                 String if1 = this.valuesForFactors.get( t1 ).get( k ).toString();
262                                 String if2 = this.valuesForFactors.get( t2 ).get( k ).toString();
263                                 columnName = t1 + if1 + ":" + t2 + if2;
264                             }
265 
266                             matrix.set( k, this.matrix.columns() - 1, prod[k] );
267                         }
268 
269                         if ( numValid < 2 && strict ) {
270                             /*
271                              * remove the column, we won't be able to estimate it
272                              */
273                             log.info( "Interaction term " + termName + " won't be estimable, dropping" );
274                             matrix = matrix.getColRange( 0, this.matrix.columns() - 2 );
275                             continue;
276                         }
277 
278                         boolean redundant = checkForRedundancy( this.matrix, this.matrix.columns() - 2 );
279                         if ( redundant && strict ) {
280                             /*
281                              * remove the column.
282                              */
283                             log.info( "Interaction term " + termName + " is redundant with another column, dropping" );
284                             matrix = matrix.getColRange( 0, this.matrix.columns() - 2 );
285                             continue;
286                         }
287 
288                         usedInteractionTerms.add( t1 );
289                         usedInteractionTerms.add( t2 );
290 
291                         matrix.addColumnName( columnName );
292                         if ( !this.terms.containsKey( termName ) ) {
293                             this.terms.put( termName, new ArrayList<Integer>() );
294                         }
295                         terms.get( termName ).add( this.matrix.columns() - 1 );
296                         assign.add( interactionIndex );
297                     }
298                 }
299             }
300         }
301 
302         if ( !usedInteractionTerms.isEmpty() ) {
303             this.interactions.add( usedInteractionTerms.toArray( new String[] {} ) );
304         }
305 
306     }
307 
308     /**
309      * @return
310      */
311     public List<Integer> getAssign() {
312         return assign;
313     }
314 
315     public String getBaseline( String factorName ) {
316         return this.levelsForFactors.get( factorName ).toString();
317     }
318 
319     public DoubleMatrix2D getDoubleMatrix() {
320         return new DenseDoubleMatrix2D( matrix.asDoubles() );
321     }
322 
323     public Map<String, List<String>> getLevelsForFactors() {
324         return levelsForFactors;
325     }
326 
327     public DoubleMatrix<String, String> getMatrix() {
328         return matrix;
329     }
330 
331     public List<String> getTerms() {
332         List<String> result = new ArrayList<>();
333         result.addAll( terms.keySet() );
334         return result;
335     }
336 
337     public Map<String, List<Object>> getValuesForFactors() {
338         return valuesForFactors;
339     }
340 
341     public boolean hasIntercept() {
342         return this.hasIntercept;
343     }
344 
345     public boolean isHasIntercept() {
346         return hasIntercept;
347     }
348 
349     /**
350      * @param factorName
351      * @param baselineFactorValue
352      */
353     public void setBaseline( String factorName, String baselineFactorValue ) {
354         if ( !this.levelsForFactors.containsKey( factorName ) ) {
355             throw new IllegalArgumentException( "No factor known by name " + factorName + ", choices are: "
356                     + StringUtils.join( this.levelsForFactors.keySet(), "," ) );
357         }
358 
359         if ( this.droppedFactors.contains( factorName ) ) {
360             log.warn( "Can't set baseline for a dropped factor, skipping" );
361             return;
362         }
363 
364         List<String> oldValues = this.levelsForFactors.get( factorName );
365         int index = oldValues.indexOf( baselineFactorValue );
366         if ( index < 0 ) {
367             throw new IllegalArgumentException( baselineFactorValue + " is not a level of the factor " + factorName );
368         }
369 
370         if ( index == 0 ) return;
371 
372         /*
373          * Put the given level in the desired location; move the others along.
374          */
375         List<String> releveled = new ArrayList<>();
376         releveled.add( oldValues.get( index ) );
377         for ( int i = 0; i < oldValues.size(); i++ ) {
378             if ( i == index ) continue;
379             releveled.add( oldValues.get( i ) );
380         }
381         this.levelsForFactors.put( factorName, releveled );
382 
383         /*
384          * Recompute the design.
385          */
386         this.rebuild();
387     }
388 
389     @Override
390     public String toString() {
391         return this.matrix.toString();
392     }
393 
394     /**
395      * Refresh the design matrix, for example after releveling.
396      */
397     protected void rebuild() {
398         this.matrix = null;
399         this.assign.clear();
400         this.terms.clear();
401 
402         if ( this.hasIntercept ) {
403             int nrows = valuesForFactors.values().iterator().next().size();
404             matrix = addIntercept( nrows );
405         }
406 
407         int i = 0;
408         for ( String factorName : valuesForFactors.keySet() ) {
409             List<Object> factorValues = valuesForFactors.get( factorName );
410             this.valuesForFactors.put( factorName, factorValues );
411 
412             if ( factorValues.get( 0 ) instanceof String && !this.levelsForFactors.containsKey( factorName ) ) {
413                 this.levels( factorName, factorValues.toArray( new String[] {} ) );
414             }
415 
416             matrix = buildDesign( i + 1, factorValues, matrix, 2, factorName );
417 
418             i++;
419         }
420 
421         if ( !this.interactions.isEmpty() ) {
422             List<String[]> redoInteractionTerms = new ArrayList<>();
423             for ( String[] interactionTerms : interactions ) {
424                 redoInteractionTerms.add( interactionTerms );
425             }
426             this.interactions.clear();
427             for ( String[] t : redoInteractionTerms ) {
428                 this.addInteraction( t );
429             }
430         }
431     }
432 
433     /**
434      * @param  vec
435      * @param  inputDesign
436      * @return
437      */
438     private DoubleMatrix<String, String> addContinuousCovariate( List<?> vec,
439             DoubleMatrix<String, String> inputDesign ) {
440         DoubleMatrix<String, String> tmp;
441         /*
442          * CONTINUOUS COVARIATE
443          */
444         log.debug( "Treating factor as continuous covariate" );
445         if ( inputDesign != null ) {
446             /*
447              * copy it into a new one.
448              */
449             assert vec.size() == inputDesign.rows();
450             int numberofColumns = inputDesign.columns() + 1;
451             tmp = copyWithSpace( inputDesign, numberofColumns );
452         } else {
453             tmp = new DenseDoubleMatrix<>( vec.size(), 1 );
454             tmp.assign( 0.0 );
455         }
456         int startcol = 0;
457         if ( inputDesign != null ) {
458             startcol = inputDesign.columns();
459         }
460         for ( int i = startcol; i < tmp.columns(); i++ ) {
461             for ( int j = 0; j < tmp.rows(); j++ ) {
462                 tmp.set( j, i, ( Double ) vec.get( j ) );
463             }
464         }
465         return tmp;
466     }
467 
468     /**
469      * @param  rows
470      * @return
471      */
472     private DoubleMatrix<String, String> addIntercept( int rows ) {
473         DoubleMatrix<String, String> tmp;
474         tmp = new DenseDoubleMatrix<>( rows, 1 );
475         tmp.addColumnName( LinearModelSummary.INTERCEPT_COEFFICIENT_NAME );
476         tmp.assign( 1.0 );
477         this.assign.add( 0 );
478         this.terms.put( LinearModelSummary.INTERCEPT_COEFFICIENT_NAME, new ArrayList<Integer>() );
479         this.terms.get( LinearModelSummary.INTERCEPT_COEFFICIENT_NAME ).add( 0 );
480         return tmp;
481     }
482 
483     /**
484      * The primary method for actually setting up the design matrix. Redundant or constant columns are dropped (except
485      * the intercept, if included)
486      *
487      * @param  which        column of the input matrix are we working on.
488      * @param  factorValues of doubles or strings.
489      * @param  inputDesign
490      * @param  start        1 or 2. Set to 1 to get a column for each level (must not have an intercept in the model);
491      *                      Set to 2
492      *                      to get a column for all but the last (Redundant) level (or another redundant column)
493      * @param  factorName   String to associate with the factor
494      * @return
495      */
496     @SuppressWarnings("unchecked")
497     private DoubleMatrix<String, String> buildDesign( int columnNum, List<?> factorValues,
498             DoubleMatrix<String, String> inputDesign, final int start, String factorName ) {
499 
500         int startUsed = start;
501 
502         if ( !terms.containsKey( factorName ) ) {
503             terms.put( factorName, new ArrayList<Integer>() );
504         }
505         DoubleMatrix<String, String> tmp = null;
506         if ( factorValues.get( 0 ) instanceof Double ) {
507             tmp = addContinuousCovariate( factorValues, inputDesign );
508             this.assign.add( columnNum );
509             terms.get( factorName ).add( columnNum );
510             tmp.addColumnName( factorName );
511         } else {
512             /*
513              * CATEGORICAL COVARIATE
514              */
515             List<String> levels;
516             if ( this.levelsForFactors.containsKey( factorName ) ) {
517                 levels = this.levelsForFactors.get( factorName );
518             } else {
519                 levels = levels( factorName, ( List<String> ) factorValues );
520             }
521 
522             // if ( levels.size() == 1 ) {
523             // /*
524             // * This is okay if it is the intercept (as in a one-sample t-test). So this isn't the place to
525             // * do this. We assume the user knows what they are doing.
526             // */
527             // log.warn( "Factor " + factorName + " was constant; not adding to the design" );
528             // this.droppedFactors.add( factorName );
529             // return inputDesign;
530             // }
531 
532             tmp = inputDesign;
533 
534             List<String> levelList = new ArrayList<>();
535             levelList.addAll( levels );
536 
537             int startcol = 0;
538             if ( tmp != null ) {
539                 startcol = inputDesign.columns();
540             }
541 
542             int currentColumn = startcol;
543             int maxColumn = levels.size() + startcol - ( startUsed - 1 );
544             Collection<String> usedLevels = new HashSet<>();
545             for ( int i = startcol; i < maxColumn; i++ ) {
546 
547                 // log.info( tmp );
548 
549                 int currentLevelIndex = i - startcol + ( startUsed - 1 );
550 
551                 if ( currentLevelIndex >= levelList.size() ) {
552                     if ( startUsed > 1 ) {
553                         // go back and use it; we must have removed a redundant level.
554                         currentLevelIndex = 0;
555                     }
556                 }
557 
558                 String level = levelList.get( currentLevelIndex );
559                 log.debug( "Adding column for Level=" + level + " at index " + currentLevelIndex );
560 
561                 // make space for the new values
562                 if ( tmp != null ) {
563                     tmp = copyWithSpace( tmp, tmp.columns() + 1 );
564                 } else {
565                     tmp = new DenseDoubleMatrix<>( factorValues.size(), 1 );
566                     tmp.assign( 0.0 );
567                 }
568 
569                 String contrastingValue = "";
570                 assert tmp != null;
571                 for ( int j = 0; j < tmp.rows(); j++ ) {
572                     boolean isBaseline = !factorValues.get( j ).equals( level );
573                     if ( !isBaseline ) {
574                         contrastingValue = ( String ) factorValues.get( j );
575                     }
576                     tmp.set( j, currentColumn, isBaseline ? 0.0 : 1.0 );
577                 }
578 
579                 // boolean redundant = checkForRedundancy( tmp, currentColumn );
580                 // if ( redundant ) {
581                 // log.warn( "Column for factor " + factorName + " level=" + level + " is redundant, dropping" );
582                 // droppedFactors.add( factorName );
583                 //
584                 // tmp = tmp.getColRange( 0, currentColumn - 1 );
585                 // maxColumn++; // add one more column
586                 // continue;
587                 // }
588 
589                 currentColumn++;
590 
591                 if ( StringUtils.isBlank( contrastingValue ) ) {
592                     contrastingValue = "_" + i;
593                 }
594                 tmp.setColumnName( factorName + contrastingValue, currentColumn );
595                 this.assign.add( columnNum ); // all of these columns use this factor.
596                 terms.get( factorName ).add( i );
597                 usedLevels.add( level );
598             }
599             /*
600              * if ( usedLevels.size() < levels.size() ) { log.info( "Used levels: " + StringUtils.join( usedLevels, " "
601              * ) ); log.info( "All levels: " + StringUtils.join( levelList, " " ) ); }
602              */
603 
604         }
605 
606         return tmp;
607     }
608 
609     /**
610      * Check if the column is redundant with a previous column
611      *
612      * @param  tmp
613      * @param  column index of column to check.
614      * @return        true if a column with index < column is the same as the column at the given index.
615      */
616     private boolean checkForRedundancy( DoubleMatrix<String, String> tmp, int column ) {
617         for ( int p = 0; p < column; p++ ) {
618 
619             boolean foundRedundant = true;
620             for ( int v = 0; v < tmp.rows(); v++ ) {
621                 if ( tmp.get( v, column ) != tmp.get( v, p ) ) {
622                     foundRedundant = false;
623                     break;
624                 }
625             }
626 
627             if ( foundRedundant ) {
628                 return true;
629             }
630         }
631         return false;
632     }
633 
634     /**
635      * Add extra empty columns to a matrix, implemented by copying.
636      *
637      * @param  inputDesign
638      * @param  numberofColumns how many to add.
639      * @return
640      */
641     private DoubleMatrix<String, String> copyWithSpace( DoubleMatrix<String, String> inputDesign,
642             int numberofColumns ) {
643         DoubleMatrix<String, String> tmp;
644         tmp = new DenseDoubleMatrix<>( inputDesign.rows(), numberofColumns );
645         tmp.assign( 0.0 );
646 
647         for ( int i = 0; i < inputDesign.rows(); i++ ) {
648             for ( int j = 0; j < inputDesign.columns(); j++ ) {
649                 String colName = inputDesign.getColName( j );
650                 if ( i == 0 && colName != null ) {
651                     tmp.setColumnName( colName, j );
652                 }
653                 tmp.set( i, j, inputDesign.get( i, j ) );
654             }
655         }
656 
657         if ( !inputDesign.getRowNames().isEmpty() ) tmp.setRowNames( inputDesign.getRowNames() );
658         return tmp;
659     }
660 
661     /**
662      * Build a "standard" design matrix from a matrix of sample information.
663      *
664      * @param  sampleInfo
665      * @param  intercept  if true, an intercept term is included.
666      * @return
667      */
668     private DoubleMatrix<String, String> designMatrix( ObjectMatrix<String, String, ?> sampleInfo, boolean intercept ) {
669         DoubleMatrix<String, String> tmp = null;
670         if ( intercept ) {
671             int rows = sampleInfo.rows();
672             tmp = addIntercept( rows );
673         }
674         return designMatrix( sampleInfo, tmp );
675     }
676 
677     /**
678      * @param  sampleInfo
679      * @param  design
680      * @return
681      */
682     private DoubleMatrix<String, String> designMatrix( ObjectMatrix<String, String, ?> sampleInfo,
683             DoubleMatrix<String, String> design ) {
684         for ( int i = 0; i < sampleInfo.columns(); i++ ) {
685             Object[] factorValuesAr = sampleInfo.getColumn( i );
686             List<Object> factorValues = Arrays.asList( factorValuesAr );
687             design = buildDesign( i + 1, factorValues, design, 2, sampleInfo.getColName( i ) );
688             this.valuesForFactors.put( sampleInfo.getColName( i ), factorValues );
689         }
690         return design;
691     }
692 
693     /**
694      * @param  vec
695      * @return
696      */
697     private List<String> levels( String factorName, List<String> vec ) {
698         return this.levels( factorName, vec.toArray( new String[] {} ) );
699     }
700 
701     /**
702      * @param  factorName
703      * @param  vec
704      * @return
705      */
706     private List<String> levels( String factorName, String[] vec ) {
707         Set<String> flevs = new LinkedHashSet<>();
708         for ( String v : vec ) {
709             flevs.add( v );
710         }
711         List<String> result = new ArrayList<>();
712         for ( String fl : flevs ) {
713             result.add( fl );
714         }
715         this.levelsForFactors.put( factorName, result );
716         return result;
717     }
718 
719 }