This package contains Python and C code for fitting the basis-function
mixture model of Newman (2025).

Files:
  emlpa.c: C code for the EM algorithm
  blpa.c: C code for the MCMC algorithm
  Makefile: Makefile for compiling the code on Unix-style systems such as Linux
  pylpa.py: Python module for using the code in Python
  em_wine.py: Example code reproducing the wine analysis from the paper
  em_pancreatic.py: Example code for EM algorithm on biomarkers data set
  mc_pancreatic.py: Example code for MCMC algorithm on biomarkers data set
  mc_big5.py: Example code for MCMC algorithm on personality test data set
  wine.txt: The wine data set, processed for use with this code
  pancreatic:txt: The pancreatic biomarkers data set, processed suitably
  big5.txt: The Big 5 personality test data, processed suitably
  license.txt: Licensing
  readme.txt: This readme file

Installation:

The code that actually performs the calculations is written in the C
programming language for speed.  (C code is typically about 100 times
faster than equivalent code in Python.)  To install this package you must
compile the code, which requires that you have a C compiler, such as gcc on
Linux or Visual C on Windows.  On Linux, compilation can be done with the
following commands, executed in the directory containing the code:

gcc -O2 -shared -fPIC -o libemlpa.so emlpa.c -lm -lgsl -lgslcblas
gcc -O2 -shared -fPIC -o libblpa.so blpa.c -lm -lgsl -lgslcblas

This will produce files libemlpa.so and libblpa.so, which should be placed
in the directory where you will be running the code.  A Linux make file is
also included for automated compilation.  You will also need to have the GNU Scientific Library (GSL) installed on your system; it can be freely downloaded from the web if you don't already have it.

On Windows or Mac there are equivalent compilation commands, but the author
does not currently have access to these systems, so the code has not been
tested there.


Use:

The software takes a data set in the form of an array of N rows and M
columns.  In the nomenclature of the paper, each row corresponds to an
"observation" and each column corresponds to an "item".  For instance, for
the personality test data in "big5.txt" each row is a respondent and each
column is one of the fifty questions on the test.

Alternatively, the software can take processed data in the form of the
quantities phi[i,j,t] described in the paper.  This allows advanced users
to employ their own basis function sets, without needing to modify the code
in any way.

The package provides a number of Python classes in the pylpa package as
described below.  Most users will probably want to use the LPA_Bernstein()
and LPA_Gamma() classes, which are described toward the end.

class State(k=None, g=None, h=None):

    Objects in this class represent assignments of components and "slots".
    The object has the following variables, whose values are initialized
    from the arguments of the same name:

    k: integer, number of components

    g: array of N integers with values 0...k-1, representing the
    component to which each observation belongs

    h: N-by-M array of integers with values 0...T-1, representing the
    slot assignment for each observation and each item

class LPA(phi, nt):

    This class provides methods for fitting the model and analyzing the
    results.

    phi: A 3d array of floats with dimension [N,M,tdim], where tdim is at
    least as great as the largest value in nt[].  This array contains the
    values of the quantities phi[i,j,t] defined in the paper.

    nt: An array of M integer elements giving the number T[j] of basis
    functions for each item j.


    The LPA object provides the following methods:

    LPA.em(k,maxiter=10000,tolerance=1e-10):

	Run the EM algorithm with k components.  Iteration of the algorithm
	ends when either the parameter values change by less than tolerance
	on each iteration or maxiter iterations have been completed,
	whichever comes sooner.

	return values:
	    q: array of estimated probabilities q[i,r] that observation i is
               a member of component r
            theta: array of estimated parameter values theta[r,j,t]

    LPA.burnin(nsweeps,initial=None):

	Run the Monte Carlo algorithm, performing the given number of Monte
	Carlo sweeps to "burn in" the calculation.  Markov chain Monte
	Carlo methods, such as the one used here, need to equilibrate,
	meaning the distribution of samples needs to converge from its
	initial state (which is usually a low-probability state) to the
	posterior distribution of the model.  This function does this.

	nsweeps: integer, the number of sweeps to perform to burn in the
	Monte Carlo.  A "sweep" is N Monte Carlo steps, where N is the
	number of observations.  A typical value for thorough burn-in would
        be nsweeps=2500.

	initial: a State() object containing the initial state for the
	Monte Carlo.  If this parameter is given, the function will use it
	as the initial state.  If it is not given, the Monte Carlo will
	start from a state with all observations in a single component and
	randomly assigned slots.  This is usually a perfectly good starting
	point, so it's safe to omit this parameter.

    LPA.sample(nsweeps,tsample,initial=None,slots=False,permute=True):

	Perform the given number of sweeps of the Monte Carlo, starting
	either from the given initial state or the prevailing current
	state, and record the component assignments and (optionally) the
	slot assignments for all observations at specified intervals.  You
	can do repeated calls to this function and each run will
	automatically continue from wherever the previous one left off, so
	you can easily pause in the middle of a calculation, or go back and
	take more samples if you don't have enough.

	The "current state" would normally be created by first running the
	function LPA.burnin() above: normal use is to call LPA.burnin()
	first, then LPA.sample().  However, an initial state can also be
	specified explicitly if preferred.

	nsweeps: integer, total number of sweeps to perform

	tsample: integer, sampling interval.  The program records the
	assignments every tsample sweeps.

	initial: a State() object containing the initial state for the
	Monte Carlo.  If this parameter is given, the function will use it
	as the initial state.  If it is not given, the Monte Carlo will
	start from the current state, which is usually the state from a
	previous call to LPA.burnin() or LPA.sample().

	slots: record the slot assignments as well as the component
	assignments.  Usually the slot assignments are not of interest, and
	they can take up a lot of space, so there's no point in recording
	them unless you need them.  The most obvious use case where you
	would need them is if you were going to calculate the mutual
	information using the LPA.mi() function below, which requires slot
	assignments.

	permute: bool, whether to permute component labels.  If True (the
        default) the component labels are randomly permuted before each
        sample is drawn.  This has no effect on the results, but eliminates
        the possibility of any apparent significance to specific component
        labels, which is formally impossible because of symmetry with
        respect to permutations (so-called label switching).  The
        permutation action is performed efficiently and incurs no
        significant performance penalty.  Omit this parameter unless you
        know what you are doing.

	Return value: numpy array of State() objects, one for each sample,
	each containing a number of components k, a component assignment
	array g, and (optionally) a slot assignment array h.  The length of
	the array of State() objects is, by definition, equal to
	nsweeps//tsamples.  For instance, if you ask for 100,000 sweeps and
	a sampling interval of 10, you will get an array of 10,000 samples.

    cmatrix(samples):

	Calculate the consensus matrix for a set of samples.  The consensus
	matrix is the matrix with elements C[i,j] equal to the fraction of
	times observations i and j were assigned to the same component.
	Return value is an N-by-N array of floats containing the matrix.

    consensus(samples,kvalue=0,cmatrix=None):

        Calculate a consensus clustering for a set of samples, using the L2
        method described in the paper.

	samples: a numpy array of State() objects containing the samples

	kvalue: If non-zero, the function will return a consensus
	clustering with exactly kvalue components.  If zero (the default),
	then the function will return the best consensus clustering it can
	find, regardless of the number of components.

	cmatrix: An N-by-N consensus matrix of floats.  If provided, the
	calculation will use this consensus matrix to compute the
	clustering.  If not provided, the code will calculate its own
	consensus matrix from the samples provided.

	return value: A State() object containing the sample state that
        best represents the entire set.

    mi(samples):

        Calculate the mutual information between component and slot
        assignments for a set of samples.  Returns a float array of mutual
        information values, one for each item in the data set.

	samples: a numpy array of State() objects containing the samples

    esttheta(sample,tolerance=1e-10):

        Estimate the theta parameters from a set of component assignments
        using the iterative method described in the paper.

	sample: a State() object containing the component assignments.  Any
	included slot assignments are ignored.

	tolerance: iteration stops once the parameter values change by less
	than this amount on each iteration.


class LPA_Bernstein(x,d):

    A child class of LPA() that implements the model using the Bernstein
    polynomial basis.

    x: Two-dimensional float array of the actual observational data, with
    values mapped to the interval [0,1].  If values fall outside this
    interval, behavior is undefined.

    d: Degree of polynomials to use for the basis functions.

    This class has the same methods as the parent LPA() class (see above).


class LPA_Gamma(x,d):

    A child class of LPA() that implements the model with the gamma basis.

    x: Two dimensional float array of the actual observational data,
    suitably scaled.  We find that scaling values for each item to have
    mean 0.5 works well.

    d: Maximum degree of polynomials to use for the basis functions.

    This class has the same methods as the parent LPA() class.


Seeding the random number generator:

The C code uses the GSL Mersenne twister random number generator, but this
generator is seeded from the standard Python random number generator.  So
if you want to seed the random numbers explicitly you should just call
random.seed() in your Python code as normal before the start of any
calculations.  Repeated runs seeded with the same seed will then produce
identical results, which can be useful for reproducibility, debugging, and
running on parallel architectures.  If no seed is specified, you will get
the default Python behavior, which on most systems means the program will
choose a random seed, for instance from the system clock or from some
operating-system-level source of randomness.


Example applications:

The files em_wine.py and em_pancreatic.py contain example code to reproduce
the EM analysis of the wide and pancreatic biomarker data sets from the
paper.  The files mc_pancreatic.py and mc_big5.py contain example code to
reproduce the MCMC analysis of biomarker and personality test data sets.
