I want to uniformly sample $(0,1)$-matrices with certain constraints. In particular, I want sum of each row to be rowsum
and the sums of columns to equal columnsum
. If uniform sampling wouldn't be a requirement this could be solved using SatisfiabilityInstances
, but to my understanding there's no way to guarantee sampling properties with it.
I have implemented the following, both ugly and slow version of a function to solve this problem:
ClearAll@matrixSample;
matrixSample[rows_, columns_, rowsum_, columnsum_] :=
Enclose[
(* Check consistency of arguments *)
ConfirmAssert[rows rowsum == Total@columnsum,
"Conflicting rows, rowsum and columnsum"];
With[
{bitpositions =
(* List of individual bit positions sets to perform over rows *)
Flatten@MapIndexed[Table[First@#2 - 1, #1] &, columnsum]},
(* Retry until successful *)
NestWhile[
Enclose[
(* Add a bit at a time *)
Fold[#1 +
Outer[Times,
(* Add to a random row which has not reached rowsum bit limit
and doesn't have this bit set *)
UnitVector[rows,
(* Confirm that such a row is found,
otherwise start from scratch *)
First@RandomChoice@ConfirmBy[
Position[#1, {l_, v_} /;
l < rowsum && BitGet[v, #2] == 0], # != {} &]],
{1, 2^#2}] &,
(* Initial {bitcount, bitvector} values *)
ConstantArray[{0, 0}, rows],
bitpositions]] &,
Failure[x, x], FailureQ]]
(* Decode integers to rows *)
// Map[Reverse@IntegerDigits[Last@#, 2, columns] &]]
It does work:
(m = matrixSample[8, 10, 7, {7, 6, 6, 6, 6, 6, 5, 5, 5, 4}]) // MatrixForm
$\left( \begin{array}{cccccccccc} 1 & 1 & 1 & 0 & 1 & 1 & 1 & 1 & 0 & 0 \\ 1 & 1 & 1 & 0 & 1 & 1 & 0 & 1 & 1 & 0 \\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 0 & 0 & 0 \\ 1 & 1 & 1 & 1 & 0 & 0 & 1 & 0 & 1 & 1 \\ 1 & 0 & 1 & 1 & 0 & 1 & 0 & 1 & 1 & 1 \\ 1 & 1 & 0 & 1 & 1 & 1 & 1 & 1 & 0 & 0 \\ 1 & 1 & 0 & 1 & 1 & 0 & 0 & 1 & 1 & 1 \\ 0 & 0 & 1 & 1 & 1 & 1 & 1 & 0 & 1 & 1 \\ \end{array} \right)$
Results fulfill specified constraints:
{Plus @@ Transpose@m, Plus @@ m}
{{7, 7, 7, 7, 7, 7, 7, 7}, {7, 6, 6, 6, 6, 6, 5, 5, 5, 4}}
But performance is abysmal (I'd want to generate tens of millions of samples of larger matrices):
RepeatedTiming[
matrixSample[8, 10, 7, {7, 6, 6, 6, 6, 6, 5, 5, 5, 4}];, 10]
{0.0141238, Null}
How to improve or rewrite this function?
EDIT: Some performance improvement can be acquired by penalising sampling of rows with have lots of bits set when choosing a row to add a new bit. This lowers chances of failure:
ClearAll@matrixSample;
matrixSample[rows_, columns_, rowsum_, columnsum_] :=
Enclose[
(* Check consistency of arguments *)
ConfirmAssert[rows rowsum == Total@columnsum,
"Conflicting rows, rowsum and columnsum"];
With[
{bitpositions =
(* List of individual bit positions sets to perform over rows *)
Flatten@MapIndexed[Table[First@#2 - 1, #1] &, columnsum]},
(* Retry until successful *)
NestWhile[
Enclose[
(* Add a bit at a time *)
Fold[#1 +
Outer[Times,
(* Add to a random row which has not reached rowsum bit limit
and doesn't have this bit set *)
UnitVector[rows,
(* Confirm that such a row is found,
otherwise start from scratch *)
RandomChoice[
(* Penalise rows with lots of entries
on candidate sampling *)
Rule @@ Transpose@With[{bps = #1},
{(rowsum - bps[[First@#, 1]])/rowsum, First@#} & /@
ConfirmBy[
Position[#1, {l_, v_} /;
l < rowsum && BitGet[v, #2] == 0], # != {} &]]]],
{1, 2^#2}] &,
(* Initial {bitcount, bitvector} values *)
ConstantArray[{0, 0}, rows],
bitpositions]] &,
Failure[x, x], FailureQ]]
(* Decode integers to rows *)
// Map[Reverse@IntegerDigits[Last@#, 2, columns] &]]
But does it skew sampling? At least it speeds it up:
RepeatedTiming[
matrixSample[8, 10, 7, {7, 6, 6, 6, 6, 6, 5, 5, 5, 4}];, 10]
{0.00621864, Null}
EDIT 2: Further performance improvement can be achieved by adding bits to each column in a single operation:
ClearAll@matrixSample;
matrixSample[rows_, columns_, rowsum_, columnsum_] :=
Enclose[
(* Check consistency of arguments *)
ConfirmAssert[rows rowsum == Total@columnsum,
"Conflicting rows, rowsum and columnsum"];
With[
{bitcounts =
(* {bitposition, bitcount} list *)
Transpose@{Range@columns - 1, columnsum}},
(* Retry until successful *)
NestWhile[
Enclose[
(* Add a column of bits at a time *)
Fold[#1 +
Outer[Times,
(* Add to random rows which have not reached
rowsum bit limit *)
ReplacePart[ConstantArray[0, rows],
(* Confirm that enough such rows are found,
otherwise start from scratch *)
RandomSample[
(* Penalise rows with lots of entries on
candidate sampling *)
Rule @@ Transpose@With[{bps = #1},
{(rowsum - bps[[First@#, 1]])/rowsum, #} & /@
With[{count = Last@#2},
ConfirmBy[
Position[#1, {l_, _} /; l < rowsum],
Length@# >= count &]]], Last@#2] -> 1],
{1, 2^First@#2}] &,
(* Initial {bitcount, bitvector} values *)
ConstantArray[{0, 0}, rows],
bitcounts]] &,
Failure[x, x], FailureQ]]
(* Decode integers to rows *)
// Map[Reverse@IntegerDigits[Last@#, 2, columns] &]]
This is roughly 8 times faster than the original:
RepeatedTiming[
matrixSample[8, 10, 7, {7, 6, 6, 6, 6, 6, 5, 5, 5, 4}];, 10]
{0.00177051, Null}
Still, it's godawful code. Would there be any better approach?
IGBigraphicalQ
from my IGraph/M package tests if it is possible to build such a matrix. $\endgroup$