[prev in list] [next in list] [prev in thread] [next in thread]
List: sas-l
Subject: Re: StackOverflow: Split a tablle into three tables
From: Paul Dorfman <sashole () BELLSOUTH ! NET>
Date: 2018-10-22 18:42:04
Message-ID: 751296400.15022555.1540233724624 () mail ! yahoo ! com
[Download RAW message or body]
Mark,
All of these are astute observations on your part; and I fully agree on needing to \
have the APPEND method. Another way to reduce the memory footprint is to index the \
original file and use BY+hash - provided, of course, that no BY group is too large. \
However, it essentially requires an extra pass through the input file (albeit reading \
the keys only). Best regardsPaul Dorfman
From: "Keintz, Mark" <mkeintz@WHARTON.UPENN.EDU>
To: SAS-L@LISTSERV.UGA.EDU
Sent: Monday, October 22, 2018 11:01 AM
Subject: Re: StackOverflow: Split a tablle into three tables
Yes, I practice the hash-of-hashes approach all the time for dynamic output naming, \
but, in a single data pass regime, it is a memory hog.
As a single pass solution, this takes a lot less resources than hash (consider a \
dataset with hundreds of variables), so (1) it has to be considerably faser, and (2) \
has a near-zero probability of failing due to memory constraints.
And you'll notice in the 2nd half of my email directly addresses the possibility of \
unexpectedly high classifier cardinality. It uses hash-of-hashes for the presumably \
less frequent (therefore less memory consuming) categories of the classifier.
If only the hash object had an append method to complement the output method.
Regards,
Mark
From: Roger DeAngelis [mailto:rogerjdeangelis@gmail.com]
Sent: Monday, October 22, 2018 9:06 AM
To: Keintz, Mark <mkeintz@wharton.upenn.edu>
Cc: SAS-L@LISTSERV.UGA.EDU
Subject: Re: StackOverflow: Split a tablle into three tables
Hi Mark
Interesting, but is'nt it possible to 'output' dynamic datasets with a hash, \
without the the want datasets.
ie
data _null_;
h.output(dataset:cats('team_',team));
On Mon, Oct 22, 2018 at 6:02 AM Keintz, Mark <mkeintz@wharton.upenn.edu> wrote:
If you want dynamically named subsets, but only know there will be no more than, say \
8, how about outputting WANT1 through WANT8, followed by a proc datasets for rename \
purposes? It's one pass through the data and has low memory requirements. Say you \
want one file for each team in dataset have:
data have;
set sashelp.baseball;
where team<'Detroit'; /*8 or fewer teams*/
team=compress(team);
run;
data want1 want2 want3 want4 want5 want6 want7 want8 unexpected;
set have end=end_of_have;
array tm {8} $14 _temporary_ ;
i=whichc(team,of tm{*});
if i=0 and nout<=8 then do;
nout+1;
i=nout;
if i<=8 then tm{i}=team;
end;
select(i);
when (1) output want1;
when (2) output want2;
when (3) output want3;
when (4) output want4;
when (5) output want5;
when (6) output want6;
when (7) output want7;
when (8) output want8;
otherwise output unexpected;
end;
if end_of_have then do;
call execute('proc datasets nolist library=work;');
do i=1 to min(8,nout);
call execute(cats('change want',i,'=team_',tm{i},';'));
end;
if nout<8 then do i=nout+1 to 8;
call execute(cats('delete want',i,';'));
end;
if nout<9 then call execute('delete unexpected;');
call execute('quit;');
end;
drop i nout;
run;
The UNEXPECTED dataset is for obvious purposes, just in case you underestimate the \
cardinality of teams. It could then be submitted to the same program, essentially \
requiring a re-read of a presumably small subset of the original data.
Of course you also could combine the above with the hash-of-hashes approach for any \
teams that otherwise would go to dataset unexpected. It would reduce the likelihood \
of excess memory requirements for hash objects, without extra reading of any data. \
data have;
set sashelp.baseball;
team=compress(team);
run;
data want1 want2 want3 want4 want5 want6 want7 want8;
set have end=end_of_have;
array tm {8} $14 _temporary_ ;
if _n_=1 then do;
declare hash h;
declare hash hoh();
hoh.definekey('team');
hoh.definedata('team','h');
hoh.definedone();
declare hiter ihoh ('hoh');
end;
i=whichc(team,of tm{*});
if i=0 and nout<8 then do;
nout+1; i=nout; tm{i}=team;
end;
if i^=0 then select(i);
when (1) output want1;
when (2) output want2;
when (3) output want3;
when (4) output want4;
when (5) output want5;
when (6) output want6;
when (7) output want7;
when (8) output want8;
end;
else do;
if hoh.find()^=0 then do;
h=_new_ hash(dataset:'sashelp.baseball(obs=0)');
h.definekey('name');
h.definedata(all:'Y');
h.definedone();
hoh.add();
end;
h.add();
end;
if end_of_have then do;
call execute('proc datasets nolist library=work;');
do i=1 to nout;
call execute(cats('change want',i,'=team_',tm{i},';'));
end;
if nout<8 then do i=nout+1 to 8;
call execute(cats('delete want',i,';'));
end;
call execute('quit;');
if hoh.num_items>0 then do while (ihoh.next()=0);
h.output(dataset:cats('team_',team));
end;
end;
drop i nout;
run;
Regards,
Mark
-----Original Message-----
From: SAS(r) Discussion [mailto:SAS-L@LISTSERV.UGA.EDU] On Behalf Of Roger DeAngelis
Sent: Sunday, October 21, 2018 12:51 PM
To: SAS-L@LISTSERV.UGA.EDU
Subject: StackOverflow: Split a tablle into three tables
StackOverflow: Split a tablle into three tables
Stack Overflow
https://stackoverflow.com/questions/52890433/iterate-over-a-custom-set-in-sas
see (14 techniques to split a table)
https://github.com/rogerjdeangelis/utl_thirteen_algorithms_to_split_a_table_based_on_groups_of_data
Below is a solution closest to what you were doing?
INPUT (split into three based on LIST_OF_YM_VALUES \
==================================================
WORK.HAVE total obs=3
LIST_OF_
YM_
DATE I AUX1 AUX2 VALUES
17929 1 17957 20090301 200903
17929 2 17988 20090401 200904
17929 3 18018 20090501 200905
EXAMPLE OUTPUT
--------------
WORK.LOG total obs=3
TABLE RC STATUS
200903 0 Table Created
200904 0 Table Created
200905 0 Table Created
THREE OUTPUT TABLES
WANT_200903 total obs=1 LIST_OF_
YM_
DATE I AUX1 AUX2 VALUES
17929 1 17957 20090301 200903
WANT_200904 total obs=1 LIST_OF_
YM_
DATE I AUX1 AUX2 VALUES
17929 2 17988 20090401 200904
WANT_200905 total obs=1 LIST_OF_
YM_
DATE I AUX1 AUX2 VALUES
17929 3 18018 20090501 200905
PROCESS
=======
%symdel yymm / nowarn; * for development; data log;
if _n_=0 then do;
%let rc=%sysfunc(dosubl('
proc sql;
select quote(list_of_ym_values) into :yymm
separated by "," from have
;quit;
'));
end;
do yymm=&yymm;
call symputx('yrmo',yymm);
rc=dosubl('
proc sql;
create table want_&yrmo as
select * from have
where list_of_ym_values = "&yrmo"
;quit;
%let cc=&syserr;
');
if symgetn('cc') = 0 then status="Table Created ";
else status="Table Creation Failed";
output;
end;
stop;
run;quit;
* _ _ _
_ __ ___ __ _| | _____ __| | __ _| |_ __ _
> '_ ` _ \ / _` | |/ / _ \ / _` |/ _` | __/ _` |
> > > > > > (_| | < __/ | (_| | (_| | || (_| |
> _| |_| |_|\__,_|_|\_\___| \__,_|\__,_|\__\__,_|
;
data have;
date = input(put(20090201, 8.), yymmdd8.);
do i = 1 to 3;
aux1 = intnx('MONTH', date, i);
aux2 = put(aux1, yymmddn8.);
list_of_ym_values = substr(aux2 , 1, 6);
output;
end;
run;
[Attachment #3 (text/html)]
<html><head></head><body><div style="color:#000; background-color:#fff; \
font-family:Courier New, courier, monaco, monospace, sans-serif;font-size:16px"><div \
id="yui_3_16_0_ym19_1_1540232491588_7026"><span>Mark,</span></div><div \
id="yui_3_16_0_ym19_1_1540232491588_7026"><span><br></span></div><div \
id="yui_3_16_0_ym19_1_1540232491588_7026" dir="ltr"><span \
id="yui_3_16_0_ym19_1_1540232491588_7445">All of these are astute observations on \
your part; and I fully agree on needing to have the APPEND method.</span></div><div \
id="yui_3_16_0_ym19_1_1540232491588_7026" dir="ltr"><span><br></span></div><div \
id="yui_3_16_0_ym19_1_1540232491588_7026" dir="ltr"><span \
id="yui_3_16_0_ym19_1_1540232491588_7370">Another way to reduce the memory footprint \
is to index the original file and use BY+hash - provided, of course, that no BY group \
is too large. However, it essentially requires an extra pass through the input file \
(albeit reading the keys only). </span></div><div \
id="yui_3_16_0_ym19_1_1540232491588_7026" dir="ltr"><span><br></span></div><div \
id="yui_3_16_0_ym19_1_1540232491588_7026" dir="ltr"><span>Best \
regards</span></div><div id="yui_3_16_0_ym19_1_1540232491588_7026" \
dir="ltr"><span>Paul Dorfman</span></div><div class="qtdSeparateBR" \
id="yui_3_16_0_ym19_1_1540232491588_7371"><br></div><div class="yahoo_quoted" \
id="yui_3_16_0_ym19_1_1540232491588_7077" style="display: block;"> <div \
style="font-family: Courier New, courier, monaco, monospace, sans-serif; font-size: \
16px;" id="yui_3_16_0_ym19_1_1540232491588_7076"> <div style="font-family: \
HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; \
font-size: 16px;" id="yui_3_16_0_ym19_1_1540232491588_7075"> <div dir="ltr" \
id="yui_3_16_0_ym19_1_1540232491588_7366"> <font size="2" face="Arial" \
id="yui_3_16_0_ym19_1_1540232491588_7367"> <hr size="1" \
id="yui_3_16_0_ym19_1_1540232491588_7372"> <b \
id="yui_3_16_0_ym19_1_1540232491588_7369"><span style="font-weight:bold;" \
id="yui_3_16_0_ym19_1_1540232491588_7368">From:</span></b> "Keintz, Mark" \
<mkeintz@WHARTON.UPENN.EDU><br> <b \
id="yui_3_16_0_ym19_1_1540232491588_7574"><span style="font-weight: bold;" \
id="yui_3_16_0_ym19_1_1540232491588_7573">To:</span></b> SAS-L@LISTSERV.UGA.EDU <br> \
<b id="yui_3_16_0_ym19_1_1540232491588_7576"><span style="font-weight: bold;" \
id="yui_3_16_0_ym19_1_1540232491588_7575">Sent:</span></b> Monday, October 22, 2018 \
11:01 AM<br> <b><span style="font-weight: bold;">Subject:</span></b> Re: \
StackOverflow: Split a tablle into three tables<br> </font> </div> <div \
class="y_msg_container" id="yui_3_16_0_ym19_1_1540232491588_7074"><br><div dir="ltr" \
id="yui_3_16_0_ym19_1_1540232491588_7073">Yes, I practice the hash-of-hashes approach \
all the time for dynamic output naming, but, in a single data pass regime, it is a \
memory hog.<br></div><div dir="ltr" \
id="yui_3_16_0_ym19_1_1540232491588_7316"><br></div><div dir="ltr" \
id="yui_3_16_0_ym19_1_1540232491588_7078">As a single pass solution, this takes a lot \
less resources than hash (consider a dataset with hundreds of variables), so (1) it \
has to be considerably faser, and (2) has a near-zero probability of failing due to \
memory constraints. <br></div><div dir="ltr" \
id="yui_3_16_0_ym19_1_1540232491588_7086"><br></div><div dir="ltr" \
id="yui_3_16_0_ym19_1_1540232491588_7087">And you'll notice in the 2nd half of my \
email directly addresses the possibility of unexpectedly high classifier \
cardinality. It uses hash-of-hashes for the presumably less frequent (therefore \
less memory consuming) categories of the classifier. <br></div><div dir="ltr" \
id="yui_3_16_0_ym19_1_1540232491588_7088"><br></div><div dir="ltr">If only the hash \
object had an append method to complement the output method.<br></div><div \
dir="ltr"><br></div><div dir="ltr">Regards,<br></div><div \
dir="ltr">Mark<br></div><div dir="ltr"><br></div><div dir="ltr"><br></div><div \
dir="ltr">From: Roger DeAngelis [mailto:<a ymailto="mailto:rogerjdeangelis@gmail.com" \
href="mailto:rogerjdeangelis@gmail.com">rogerjdeangelis@gmail.com</a>] <br></div><div \
dir="ltr">Sent: Monday, October 22, 2018 9:06 AM<br></div><div dir="ltr">To: Keintz, \
Mark <<a ymailto="mailto:mkeintz@wharton.upenn.edu" \
href="mailto:mkeintz@wharton.upenn.edu">mkeintz@wharton.upenn.edu</a>><br></div><div \
dir="ltr">Cc: <a ymailto="mailto:SAS-L@LISTSERV.UGA.EDU" \
href="mailto:SAS-L@LISTSERV.UGA.EDU">SAS-L@LISTSERV.UGA.EDU</a><br></div><div \
dir="ltr">Subject: Re: StackOverflow: Split a tablle into three tables<br></div><div \
dir="ltr"><br></div><div dir="ltr">Hi Mark<br></div><div dir="ltr"><br></div><div \
dir="ltr"> Interesting, but is'nt it possible to 'output' dynamic datasets with \
a hash, without the the want datasets.<br></div><div dir="ltr"><br></div><div \
dir="ltr"> ie <br></div><div dir="ltr"><br></div><div dir="ltr">data \
_null_; <br></div><div dir="ltr"> <br></div><div \
dir="ltr"> h.output(dataset:cats('team_',team));<br></div><div \
dir="ltr"><br></div><div dir="ltr"> <br></div><div \
dir="ltr"><br></div><div dir="ltr">On Mon, Oct 22, 2018 at 6:02 AM Keintz, Mark \
<<a ymailto="mailto:mkeintz@wharton.upenn.edu" \
href="mailto:mkeintz@wharton.upenn.edu">mkeintz@wharton.upenn.edu</a>> \
wrote:<br></div><div dir="ltr">If you want dynamically named subsets, but only know \
there will be no more than, say 8, how about outputting WANT1 through WANT8, followed \
by a proc datasets for rename purposes? It's one pass through the data and has \
low memory requirements. Say you want one file for each team in dataset \
have:<br></div><div dir="ltr"><br></div><div dir="ltr">data have;<br></div><div \
dir="ltr"> set sashelp.baseball;<br></div><div dir="ltr"> where \
team<'Detroit'; /*8 or fewer teams*/<br></div><div dir="ltr"> \
team=compress(team);<br></div><div dir="ltr">run;<br></div><div \
dir="ltr"><br></div><div dir="ltr">data want1 want2 want3 want4 want5 want6 want7 \
want8 unexpected;<br></div><div dir="ltr"> set have \
end=end_of_have;<br></div><div dir="ltr"> array tm {8} $14 _temporary_ \
;<br></div><div dir="ltr"><br></div><div dir="ltr"> i=whichc(team,of \
tm{*});<br></div><div dir="ltr"> if i=0 and nout<=8 then do;<br></div><div \
dir="ltr"> nout+1; <br></div><div dir="ltr"> \
i=nout; <br></div><div dir="ltr"> if i<=8 then \
tm{i}=team;<br></div><div dir="ltr"> end;<br></div><div dir="ltr"> \
select(i);<br></div><div dir="ltr"> when (1) output want1;<br></div><div \
dir="ltr"> when (2) output want2;<br></div><div dir="ltr"> \
when (3) output want3;<br></div><div dir="ltr"> when (4) output \
want4;<br></div><div dir="ltr"> when (5) output want5;<br></div><div \
dir="ltr"> when (6) output want6;<br></div><div dir="ltr"> \
when (7) output want7;<br></div><div dir="ltr"> when (8) output \
want8;<br></div><div dir="ltr"> otherwise output \
unexpected;<br></div><div dir="ltr"> end;<br></div><div dir="ltr"> if \
end_of_have then do;<br></div><div dir="ltr"> call execute('proc \
datasets nolist library=work;');<br></div><div dir="ltr"> do i=1 to \
min(8,nout);<br></div><div dir="ltr"> call execute(cats('change \
want',i,'=team_',tm{i},';'));<br></div><div dir="ltr"> \
end;<br></div><div dir="ltr"> if nout<8 then do i=nout+1 to \
8;<br></div><div dir="ltr"> call execute(cats('delete \
want',i,';'));<br></div><div dir="ltr"> end;<br></div><div \
dir="ltr"> if nout<9 then call execute('delete \
unexpected;');<br></div><div dir="ltr"> call \
execute('quit;');<br></div><div dir="ltr"> end;<br></div><div dir="ltr"> \
drop i nout;<br></div><div dir="ltr">run;<br></div><div dir="ltr"><br></div><div \
dir="ltr">The UNEXPECTED dataset is for obvious purposes, just in case you \
underestimate the cardinality of teams. It could then be submitted to the same \
program, essentially requiring a re-read of a presumably small subset of the original \
data.<br></div><div dir="ltr"><br></div><div dir="ltr">Of course you also could \
combine the above with the hash-of-hashes approach for any teams that otherwise would \
go to dataset unexpected. It would reduce the likelihood of excess memory \
requirements for hash objects, without extra reading of any data. \
<br></div><div dir="ltr"><br></div><div dir="ltr"><br></div><div dir="ltr">data \
have;<br></div><div dir="ltr"> set sashelp.baseball;<br></div><div \
dir="ltr"> team=compress(team);<br></div><div dir="ltr">run;<br></div><div \
dir="ltr"><br></div><div dir="ltr">data want1 want2 want3 want4 want5 want6 want7 \
want8;<br></div><div dir="ltr"> set have end=end_of_have;<br></div><div \
dir="ltr"> array tm {8} $14 _temporary_ ;<br></div><div \
dir="ltr"><br></div><div dir="ltr"> if _n_=1 then do;<br></div><div \
dir="ltr"> declare hash h;<br></div><div dir="ltr"> declare \
hash hoh();<br></div><div dir="ltr"> \
hoh.definekey('team');<br></div><div dir="ltr"> \
hoh.definedata('team','h');<br></div><div dir="ltr"> \
hoh.definedone();<br></div><div dir="ltr"> declare hiter ihoh \
('hoh');<br></div><div dir="ltr"> end;<br></div><div dir="ltr"><br></div><div \
dir="ltr"> i=whichc(team,of tm{*});<br></div><div dir="ltr"> if i=0 and \
nout<8 then do;<br></div><div dir="ltr"> nout+1; i=nout; \
tm{i}=team;<br></div><div dir="ltr"> end;<br></div><div dir="ltr"> if \
i^=0 then select(i);<br></div><div dir="ltr"> when (1) output \
want1;<br></div><div dir="ltr"> when (2) output want2;<br></div><div \
dir="ltr"> when (3) output want3;<br></div><div dir="ltr"> \
when (4) output want4;<br></div><div dir="ltr"> when (5) output \
want5;<br></div><div dir="ltr"> when (6) output want6;<br></div><div \
dir="ltr"> when (7) output want7;<br></div><div dir="ltr"> \
when (8) output want8;<br></div><div dir="ltr"> end;<br></div><div \
dir="ltr"> else do;<br></div><div dir="ltr"> if hoh.find()^=0 then \
do;<br></div><div dir="ltr"> h=_new_ \
hash(dataset:'sashelp.baseball(obs=0)');<br></div><div dir="ltr"> \
h.definekey('name');<br></div><div dir="ltr"> \
h.definedata(all:'Y');<br></div><div dir="ltr"> \
h.definedone();<br></div><div dir="ltr"> \
hoh.add();<br></div><div dir="ltr"> end;<br></div><div \
dir="ltr"> h.add();<br></div><div dir="ltr"> \
end;<br></div><div dir="ltr"> if end_of_have then do;<br></div><div \
dir="ltr"> call execute('proc datasets nolist \
library=work;');<br></div><div dir="ltr"> do i=1 to nout;<br></div><div \
dir="ltr"> call execute(cats('change \
want',i,'=team_',tm{i},';'));<br></div><div dir="ltr"> \
end;<br></div><div dir="ltr"> if nout<8 then do \
i=nout+1 to 8;<br></div><div dir="ltr"> call \
execute(cats('delete want',i,';'));<br></div><div dir="ltr"> \
end;<br></div><div dir="ltr"> call \
execute('quit;');<br></div><div dir="ltr"> if \
hoh.num_items>0 then do while (ihoh.next()=0);<br></div><div dir="ltr"> \
h.output(dataset:cats('team_',team));<br></div><div \
dir="ltr"> end;<br></div><div dir="ltr"> \
end;<br></div><div dir="ltr"> drop i nout;<br></div><div \
dir="ltr">run;<br></div><div dir="ltr"><br></div><div \
dir="ltr">Regards,<br></div><div dir="ltr">Mark<br></div><div dir="ltr">-----Original \
Message-----<br></div><div dir="ltr">From: SAS(r) Discussion [mailto:<a \
ymailto="mailto:SAS-L@LISTSERV.UGA.EDU" \
href="mailto:SAS-L@LISTSERV.UGA.EDU">SAS-L@LISTSERV.UGA.EDU</a>] On Behalf Of Roger \
DeAngelis<br></div><div dir="ltr">Sent: Sunday, October 21, 2018 12:51 \
PM<br></div><div dir="ltr">To: <a ymailto="mailto:SAS-L@LISTSERV.UGA.EDU" \
href="mailto:SAS-L@LISTSERV.UGA.EDU">SAS-L@LISTSERV.UGA.EDU</a><br></div><div \
dir="ltr">Subject: StackOverflow: Split a tablle into three tables<br></div><div \
dir="ltr"><br></div><div dir="ltr">StackOverflow: Split a tablle into three \
tables<br></div><div dir="ltr"><br></div><div dir="ltr">Stack Overflow<br></div><div \
dir="ltr"><a href="https://stackoverflow.com/questions/52890433/iterate-over-a-custom-set-in-sas" \
target="_blank">https://stackoverflow.com/questions/52890433/iterate-over-a-custom-set-in-sas</a><br></div><div \
dir="ltr"><br></div><div dir="ltr">see (14 techniques to split a table)<br></div><div \
dir="ltr"><a href="https://github.com/rogerjdeangelis/utl_thirteen_algorithms_to_split_a_table_based_on_groups_of_data" \
target="_blank">https://github.com/rogerjdeangelis/utl_thirteen_algorithms_to_split_a_table_based_on_groups_of_data</a><br></div><div \
dir="ltr"><br></div><div dir="ltr">Below is a solution closest to what you were \
doing?<br></div><div dir="ltr"><br></div><div dir="ltr"><br></div><div \
dir="ltr">INPUT (split into three based on LIST_OF_YM_VALUES \
==================================================<br></div><div \
dir="ltr"><br></div><div dir="ltr"><br></div><div dir="ltr">WORK.HAVE total \
obs=3<br></div><div dir="ltr"><br></div><div dir="ltr"> \
\
LIST_OF_<br></div><div dir="ltr"> \
\
YM_<br></div><div dir="ltr"> DATE \
I AUX1 AUX2 \
VALUES<br></div><div dir="ltr"><br></div><div dir="ltr"> 17929 \
1 17957 20090301 \
200903<br></div><div dir="ltr"> 17929 2 \
17988 20090401 200904<br></div><div dir="ltr"> \
17929 3 18018 20090501 \
200905<br></div><div dir="ltr"><br></div><div dir="ltr">EXAMPLE \
OUTPUT<br></div><div dir="ltr">--------------<br></div><div dir="ltr"><br></div><div \
dir="ltr"> WORK.LOG total obs=3<br></div><div dir="ltr"><br></div><div \
dir="ltr"> TABLE RC \
STATUS<br></div><div dir="ltr"><br></div><div dir="ltr"> 200903 \
0 Table Created<br></div><div dir="ltr"> 200904 \
0 Table Created<br></div><div dir="ltr"> 200905 \
0 Table Created<br></div><div dir="ltr"><br></div><div \
dir="ltr"><br></div><div dir="ltr">THREE OUTPUT TABLES<br></div><div \
dir="ltr"><br></div><div dir="ltr"> WANT_200903 total obs=1 \
LIST_OF_<br></div><div dir="ltr"> \
\
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic