// File for testing: cansas.laz, all variables // In the help file example No of Desired Clusters is 4 unit KMeansUnit; {$mode objfpc}{$H+} interface uses Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons, ExtCtrls, MainUnit, Globals, BasicStatsReportFormUnit; type { TKMeansForm } TKMeansForm = class(TBasicStatsReportForm) DescriptiveChk: TCheckBox; Panel1: TPanel; VarInBtn: TBitBtn; VarOutBtn: TBitBtn; AllBtn: TBitBtn; StandardizeChk: TCheckBox; ReplaceChk: TCheckBox; GroupBox1: TGroupBox; ItersEdit: TEdit; Label2: TLabel; Label3: TLabel; Label4: TLabel; SelList: TListBox; VarList: TListBox; NoClustersEdit: TEdit; Label1: TLabel; procedure AllBtnClick(Sender: TObject); procedure SelListDblClick(Sender: TObject); procedure StandardizeChkChange(Sender: TObject); procedure VarInBtnClick(Sender: TObject); procedure VarListDblClick(Sender: TObject); procedure VarListSelectionChange(Sender: TObject; {%H-}User: boolean); procedure VarOutBtnClick(Sender: TObject); private procedure KMNS(VAR A : DblDyneMat; M, N : integer; VAR C : DblDyneMat; K : integer; VAR IC1 : IntDyneVec; VAR IC2 : IntDyneVec; VAR NC : IntDyneVec; VAR AN1 : DblDyneVec; VAR AN2 : DblDyneVec; VAR NCP : IntDyneVec; VAR D : DblDyneVec; VAR ITRAN : IntDyneVec; VAR LIVE : IntDyneVec; ITER : integer; VAR WSS : DblDyneVec; out IFAULT : integer); procedure OPTRA(VAR A : DblDyneMat; M, N : integer; VAR C : DblDyneMat; K : integer; VAR IC1 : IntDyneVec; VAR IC2 : IntDyneVec; VAR NC : IntDyneVec; VAR AN1 : DblDyneVec; VAR AN2 : DblDyneVec; VAR NCP : IntDyneVec; VAR D : DblDyneVec; VAR ITRAN : IntDyneVec; VAR LIVE : IntDyneVec; INDX : integer); procedure QTRAN(VAR A : DblDyneMat; M, N : integer; VAR C : DblDyneMat; K : integer; VAR IC1 : IntDyneVec; VAR IC2 : IntDyneVec; VAR NC : IntDyneVec; VAR AN1 : DblDyneVec; VAR AN2 : DblDyneVec; VAR NCP : IntDyneVec; VAR D : DblDyneVec; VAR ITRAN : IntDyneVec; INDX : integer); protected procedure AdjustConstraints; override; procedure Compute; override; procedure UpdateBtnStates; override; function Validate(out AMsg: String; out AControl: TWinControl): Boolean; override; public constructor Create(AOwner: TComponent); override; procedure Reset; override; end; var KMeansForm: TKMeansForm; implementation {$R *.lfm} uses Math, Utils, GridProcs, MatrixUnit; { TKMeansForm } constructor TKMeansForm.Create(AOwner: TComponent); begin inherited; end; procedure TKMeansForm.AdjustConstraints; begin inherited; ParamsPanel.Constraints.MinHeight := AllBtn.Top + AllBtn.Height + VarList.BorderSpacing.Bottom + GroupBox1.Height + ButtonBevel.Height + CloseBtn.BorderSpacing.Top + CloseBtn.Height; ParamsPanel.Constraints.MinWidth := Max( 4*CloseBtn.Width + 3*CloseBtn.BorderSpacing.Left, GroupBox1.Width + GroupBox1.BorderSpacing.Right + Max(Label1.Width, Label2.Width) + Label1.BorderSpacing.Right + NoClustersEdit.Width ); end; procedure TKMeansForm.AllBtnClick(Sender: TObject); var index: integer; cellstring: string; begin for index := 0 to VarList.Items.Count - 1 do begin cellstring := VarList.Items[index]; SelList.Items.Add(cellstring); end; VarList.Clear; UpdateBtnStates; end; procedure TKMeansForm.Compute; var i, j, L, Ncols, N, M, K,IFAULT, ITER, col : integer; center: integer; IC1: IntDyneVec = nil; IC2: IntDyneVec = nil; NC: IntDyneVec = nil; NCP: IntDyneVec = nil; ITRAN: IntDyneVec = nil; LIVE: IntDyneVec = nil; ColSelected: IntDyneVec = nil; A: DblDyneMat = nil; C: DblDyneMat = nil; D: DblDyneVec = nil; AN1: DblDyneVec = nil; AN2: DblDyneVec = nil; WSS: DblDyneVec = nil; varlabels: StrDyneVec = nil; rowlabels: StrDyneVec = nil; Mean, stddev : double; outline: string; lReport: TStrings; begin Ncols := SelList.Items.Count; K := StrToInt(NoClustersEdit.Text); ITER := StrToInt(ItersEdit.Text); IFAULT := 0; N := NCols; M := NoCases; SetLength(A,M+1,N+1); SetLength(C,K+1,N+1); SetLength(D,M+1); SetLength(AN1,K+1); SetLength(AN2,K+1); SetLength(WSS,K+1); SetLength(IC1,M+1); SetLength(IC2,M+1); SetLength(NC,K+1); SetLength(NCP,K+1); SetLength(ITRAN,K+1); SetLength(LIVE,K+1); // Get labels and columns of selected variables SetLength(ColSelected, nCols); SetLength(varlabels, nCols); for i := 0 to nCols - 1 do begin varLabels[i] := SelList.Items[i]; ColSelected[i] := GetVariableIndex(OS3MainFrm.DataGrid, varLabels[i]); end; // Get labels of rows SetLength(rowlabels, noCases); for i := 0 to noCases - 1 do rowlabels[i] := OS3MainFrm.DataGrid.Cells[0, i+1]; // Read the data for i := 1 to M do begin if not GoodRecord(OS3MainFrm.DataGrid, i, ColSelected) then continue; for j := 1 to N do A[i,j] := StrToFloat(OS3MainFrm.DataGrid.Cells[ColSelected[j-1],i]); end; lReport := TStringList.Create; try lReport.Add('K-MEANS CLUSTERING'); lReport.Add('Adapted from AS 136 APPL. STATIST. (1979) VOL.28, NO.1'); lReport.Add(''); lReport.Add('File: %s', [OS3MainFrm.FileNameEdit.Text]); lReport.Add('No. Cases: %8d', [M]); lReport.Add('No. Variables: %8d', [N]); lReport.Add('No. Clusters: %8d', [K]); lReport.Add(''); // transform to z scores if needed if StandardizeChk.Checked then begin if DescriptiveChk.Checked then begin lReport.Add('DESCRIPTIVE STATISTICS'); lReport.Add(' Variable Mean StdDev '); lReport.Add('------------ ------------ ------------'); end; for j := 1 to N do begin Mean := 0.0; stddev := 0.0; for i := 1 to M do begin Mean := Mean + A[i,j]; stddev := stddev + sqr(A[i,j]); end; stddev := (stddev - sqr(Mean) / M) / (M - 1); Mean := Mean / M; if DescriptiveChk.Checked then lReport.Add('%12s %12.3f %12.3f', [varLabels[j-1], mean, stdDev]); for i := 1 to M do begin A[i,j] := (A[i,j] - Mean) / stddev; if ReplaceChk.Checked then begin col := ColSelected[j-1]; OS3MainFrm.DataGrid.Cells[col,i] := Format('%.5f', [A[i,j]]); end; end; end; end; // Now enter initial points for L := 1 to K do begin center := 1 + (L-1) * (M div K); // initial cluster center // wp: why not ((L-1)*M) div K for j := 1 to N do C[L, j] := A[center, j]; end; // do analysis KMNS(A,M,N,C,K,IC1,IC2,NC,AN1,AN2,NCP,D,ITRAN,LIVE,ITER,WSS,IFAULT); // show results // sort subjects by cluster for i := 1 to M do IC2[i] := i; // store ids in here for i := 1 to M - 1 do begin for j := i+1 to M do begin if (IC1[i] > IC1[j]) then // swap these clusters and ids begin Exchange(IC1[i], IC1[j]); Exchange(IC2[i], IC2[j]); end; end; end; lReport.Add(''); lReport.Add('NUMBER OF SUBJECTS IN EACH CLUSTER'); for i := 1 to K do lReport.Add('Cluster %d with %d cases.', [i, NC[i]]); lReport.Add(''); lReport.Add('PLACEMENT OF SUBJECTS IN CLUSTERS'); lReport.Add('CLUSTER SUBJECT'); for i := 1 to M do lReport.Add(' %3d %3d', [IC1[i], IC2[i]]); lReport.Add(''); lReport.Add('AVERAGE VARIABLE VALUES BY CLUSTER'); lReport.Add(' Variables'); outline := 'Cluster'; for j := 1 to N do outline := outline + Format(' %3d ',[j]); lReport.Add(outline); lReport.Add(' '); for i := 1 to K do begin outline := format(' %3d ',[i]); for j := 1 to N do outline := outline + Format('%5.2f ', [C[i,j]]); lReport.Add(outline); end; lReport.Add(''); lReport.Add('WITHIN CLUSTER SUMS OF SQUARES'); for i := 1 to K do lReport.Add('Cluster %d: %6.3f', [i, WSS[i]]); FReportFrame.DisplayReport(lReport); finally lReport.Free; LIVE := nil; ITRAN := nil; NCP := nil; NC := nil; IC2 := nil; IC1 := nil; WSS := nil; AN2 := nil; AN1 := nil; D := nil; C := nil; A := nil; ColSelected := nil; rowlabels := nil; varlabels := nil; end; end; procedure TKMeansForm.KMNS(VAR A : DblDyneMat; M, N : integer; VAR C : DblDyneMat; K : integer; VAR IC1 : IntDyneVec; VAR IC2 : IntDyneVec; VAR NC : IntDyneVec; VAR AN1 : DblDyneVec; VAR AN2 : DblDyneVec; VAR NCP : IntDyneVec; VAR D : DblDyneVec; VAR ITRAN : IntDyneVec; VAR LIVE : IntDyneVec; ITER : integer; VAR WSS : DblDyneVec; out IFAULT : integer); const BIG = 1.0E30; ZERO = 0.0; ONE = 1.0; VAR DT: array[0..2] of double; DA, DB, DC, TEMP, AA: double; L, II, INDX, I, J, IL, IJ: integer; label cont50, cont40, cont150; begin // SUBROUTINE KMNS(A, M, N, C, K, IC1, IC2, NC, AN1, AN2, NCP, D, // * ITRAN, LIVE, ITER, WSS, IFAULT) // // ALGORITHM AS 136 APPL. STATIST. (1979) VOL.28, NO.1 // Divide M points in N-dimensional space into K clusters so that // the within cluster sum of squares is minimized. // // INTEGER IC1(M), IC2(M), NC(K), NCP(K), ITRAN(K), LIVE(K) // REAL A(M,N), D(M), C(K,N), AN1(K), AN2(K), WSS(K), DT(2) // REAL ZERO, ONE // // Define BIG to be a very large positive number // // DATA BIG /1.E30/, ZERO /0.0/, ONE /1.0/ // IFAULT := 3; if (K <= 1) or (K >= M) then begin MessageDlg('The no. of clusters must be less than the no. of variables.', mtError, [mbOK], 0); exit; end; // For each point I, find its two closest centres, IC1(I) and // IC2(I). Assign it to IC1(I). // for I := 1 to M do begin IC1[I] := 1; IC2[I] := 2; for IL := 1 to 2 do begin DT[IL] := ZERO; for J := 1 to N do begin DA := A[I,J] - C[IL,J]; DT[IL] := DT[IL] + (DA * DA); //(squared difference for this comparison) end; // 10 CONTINUE end; // 10 CONTINUE if (DT[1] > DT[2]) then // THEN swap begin IC1[I] := 2; IC2[I] := 1; TEMP := DT[1]; DT[1] := DT[2]; DT[2] := TEMP; end; // END IF for L := 3 to K do // (remaining clusters) begin DB := ZERO; for J := 1 to N do // (variables) begin DC := A[I,J] - C[L,J]; DB := DB + DC * DC; if (DB >= DT[2]) then goto cont50; end; if (DB < DT[1]) then goto cont40; DT[2] := DB; IC2[I] := L; goto cont50; cont40: DT[2] := DT[1]; IC2[I] := IC1[I]; DT[1] := DB; IC1[I] := L; cont50: end; end; // 50 CONTINUE (next case) // Update cluster centres to be the average of points contained // within them. // for L := 1 to K do // (clusters) begin NC[L] := 0; for J := 1 to N do C[L,J] := ZERO; //(initialize clusters) end; for I := 1 to M do // (subjects) begin L := IC1[I]; // which cluster the Ith case is in NC[L] := NC[L] + 1; // no. in the cluster L for J := 1 to N do C[L,J] := C[L,J] + A[I,J]; // sum of var. values in the cluster L end; // Check to see if there is any empty cluster at this stage // for L := 1 to K do begin if (NC[L] = 0) then begin IFAULT := 1; exit; end; AA := NC[L]; for J := 1 to N do C[L,J] := C[L,J] / AA; // average the values in the cluster // Initialize AN1, AN2, ITRAN & NCP // AN1(L) := NC(L) / (NC(L) - 1) // AN2(L) := NC(L) / (NC(L) + 1) // ITRAN(L) := 1 if cluster L is updated in the quick-transfer stage, // := 0 otherwise // In the optimal-transfer stage, NCP(L) stores the step at which // cluster L is last updated. // In the quick-transfer stage, NCP(L) stores the step at which // cluster L is last updated plus M. // AN2[L] := AA / (AA + ONE); AN1[L] := BIG; if (AA > ONE) then AN1[L] := AA / (AA - ONE); ITRAN[L] := 1; NCP[L] := -1; end; INDX := 0; for IJ := 1 to ITER do begin // // In this stage, there is only one pass through the data. Each // point is re-allocated, if necessary, to the cluster that will // induce the maximum reduction in within-cluster sum of squares. // OPTRA(A, M, N, C, K, IC1, IC2, NC, AN1, AN2, NCP, D, ITRAN, LIVE, INDX); // // Stop if no transfer took place in the last M optimal transfer steps. // if (INDX = M) then goto cont150; // // Each point is tested in turn to see if it should be re-allocated // to the cluster to which it is most likely to be transferred, // IC2(I), from its present cluster, IC1(I). Loop through the // data until no further change is to take place. // QTRAN(A, M, N, C, K, IC1, IC2, NC, AN1, AN2, NCP, D, ITRAN, INDX); // // If there are only two clusters, there is no need to re-enter the // optimal transfer stage. // if (K = 2) then goto cont150; // // NCP has to be set to 0 before entering OPTRA. // for L := 1 to K do NCP[L] := 0; end; // // Since the specified number of iterations has been exceeded, set // IFAULT := 2. This may indicate unforeseen looping. // IFAULT := 2; // // Compute within-cluster sum of squares for each cluster. // cont150: for L := 1 to K do begin WSS[L] := ZERO; for J := 1 to N do C[L,J] := ZERO; end; for I := 1 to M do begin II := IC1[I]; for J := 1 to N do C[II,J] := C[II,J] + A[I,J]; end; for J := 1 to N do begin for L := 1 to K do C[L,J] := C[L,J] / (NC[L]); for I := 1 to M do begin II := IC1[I]; DA := A[I,J] - C[II,J]; WSS[II] := WSS[II] + DA * DA; end; end; // 190 CONTINUE end; procedure TKMeansForm.OPTRA(VAR A : DblDyneMat; M, N : integer; VAR C : DblDyneMat; K : integer; VAR IC1 : IntDyneVec; VAR IC2 : IntDyneVec; VAR NC : IntDyneVec; VAR AN1 : DblDyneVec; VAR AN2 : DblDyneVec; VAR NCP : IntDyneVec; VAR D : DblDyneVec; VAR ITRAN : IntDyneVec; VAR LIVE : IntDyneVec; INDX : integer); VAR ZERO, ONE, BIG,DE, DF, DD, DC, DB, DA, R2, RR, AL1, AL2, ALT, ALW : double; I, J, L, L1, L2, LL : integer; label cont30, cont60, cont70, cont90; begin // SUBROUTINE OPTRA(A, M, N, C, K, IC1, IC2, NC, AN1, AN2, NCP, D, // * ITRAN, LIVE, INDX) // // ALGORITHM AS 136.1 APPL. STATIST. (1979) VOL.28, NO.1 // // This is the optimal transfer stage. // // Each point is re-allocated, if necessary, to the cluster that // will induce a maximum reduction in the within-cluster sum of // squares. // // INTEGER IC1(M), IC2(M), NC(K), NCP(K), ITRAN(K), LIVE(K) // REAL A(M,N), D(M), C(K,N), AN1(K), AN2(K), ZERO, ONE // // Define BIG to be a very large positive number. // // DATA BIG /1.0E30/, ZERO /0.0/, ONE/1.0/ // // If cluster L is updated in the last quick-transfer stage, it // belongs to the live set throughout this stage. Otherwise, at // each step, it is not in the live set if it has not been updated // in the last M optimal transfer steps. // ZERO := 0.0; ONE := 1.0; BIG := 1.0e30; for L := 1 to K do begin if (ITRAN[L] = 1) then LIVE[L] := M + 1; end; // 10 CONTINUE for I := 1 to M do begin INDX := INDX + 1; L1 := IC1[I]; L2 := IC2[I]; LL := L2; // // If point I is the only member of cluster L1, no transfer. // if (NC[L1] = 1) then goto cont90; // GO TO 90 // // If L1 has not yet been updated in this stage, no need to // re-compute D(I). // if (NCP[L1] = 0) then goto cont30; // GO TO 30 DE := ZERO; for J := 1 to N do begin DF := A[I,J] - C[L1,J]; DE := DE + DF * DF; end; D[I] := DE * AN1[L1]; // // Find the cluster with minimum R2. // cont30: DA := ZERO; for J := 1 to N do begin DB := A[I,J] - C[L2,J]; DA := DA + DB * DB; end; R2 := DA * AN2[L2]; for L := 1 to K do begin // // If I >:= LIVE(L1), then L1 is not in the live set. If this is // true, we only need to consider clusters that are in the live set // for possible transfer of point I. Otherwise, we need to consider // all possible clusters. // if ((I >= LIVE[L1]) and (I >= LIVE[L]) or (L = L1) or (L = LL)) then goto cont60; RR := R2 / AN2[L]; DC := ZERO; for J := 1 to N do begin DD := A[I,J] - C[L,J]; DC := DC + DD * DD; if (DC >= RR) then goto cont60; end; R2 := DC * AN2[L]; L2 := L; cont60: end; // 60 CONTINUE if (R2 < D[I]) then goto cont70; // // If no transfer is necessary, L2 is the new IC2(I). // IC2[I] := L2; goto cont90; // GO TO 90 // // Update cluster centres, LIVE, NCP, AN1 & AN2 for clusters L1 and // L2, and update IC1(I) & IC2(I). // cont70: INDX := 0; LIVE[L1] := M + I; LIVE[L2] := M + I; NCP[L1] := I; NCP[L2] := I; AL1 := NC[L1]; ALW := AL1 - ONE; AL2 := NC[L2]; ALT := AL2 + ONE; for J := 1 to N do begin C[L1,J] := (C[L1,J] * AL1 - A[I,J]) / ALW; C[L2,J] := (C[L2,J] * AL2 + A[I,J]) / ALT; end; NC[L1] := NC[L1] - 1; NC[L2] := NC[L2] + 1; AN2[L1] := ALW / AL1; AN1[L1] := BIG; if (ALW > ONE) then AN1[L1] := ALW / (ALW - ONE); AN1[L2] := ALT / AL2; AN2[L2] := ALT / (ALT + ONE); IC1[I] := L2; IC2[I] := L1; cont90: // 90 CONTINUE if (INDX = M) then exit; end; // 100 CONTINUE for L := 1 to K do begin // // ITRAN(L) := 0 before entering QTRAN. Also, LIVE(L) has to be // decreased by M before re-entering OPTRA. // ITRAN[L] := 0; LIVE[L] := LIVE[L] - M; end; // 110 CONTINUE end; { SUBROUTINE QTRAN(A, M, N, C, K, IC1, IC2, NC, AN1, AN2, NCP, D, ITRAN, INDX) ALGORITHM AS 136.2 APPL. STATIST. (1979) VOL.28, NO.1 This is the quick transfer stage. IC1(I) is the cluster which point I belongs to. IC2(I) is the cluster which point I is most likely to be transferred to. For each point I, IC1(I) & IC2(I) are switched, if necessary, to reduce within-cluster sum of squares. The cluster centres are updated after each step. INTEGER IC1(M), IC2(M), NC(K), NCP(K), ITRAN(K) REAL A(M,N), D(M), C(K,N), AN1(K), AN2(K), ZERO, ONE Define BIG to be a very large positive number DATA BIG /1.0E30/, ZERO /0.0/, ONE /1.0/ In the optimal transfer stage, NCP(L) indicates the step at which cluster L is last updated. In the quick transfer stage, NCP(L) is equal to the step at which cluster L is last updated plus M. } procedure TKMeansForm.QTRAN(VAR A : DblDyneMat; M, N : integer; VAR C : DblDyneMat; K : integer; VAR IC1 : IntDyneVec; VAR IC2 : IntDyneVec; VAR NC : IntDyneVec; VAR AN1 : DblDyneVec; VAR AN2 : DblDyneVec; VAR NCP : IntDyneVec; VAR D : DblDyneVec; VAR ITRAN : IntDyneVec; INDX : integer); const BIG = 1E304; ZERO = 0.0; ONE = 1.0; var DA, DB, DE, DD, R2, AL1, ALW, AL2, ALT: double; I, J, ICOUN, ISTEP, L1, L2: integer; label cont10, cont30, cont60; begin ICOUN := 0; ISTEP := 0; cont10: for I := 1 to M do begin ICOUN := ICOUN + 1; ISTEP := ISTEP + 1; L1 := IC1[I]; L2 := IC2[I]; // // If point I is the only member of cluster L1, no transfer. // if (NC[L1] = 1) then goto cont60; // // If ISTEP > NCP(L1), no need to re-compute distance from point I to // cluster L1. Note that if cluster L1 is last updated exactly M // steps ago, we still need to compute the distance from point I to // cluster L1. // if (ISTEP > NCP[L1]) then goto cont30; DA := ZERO; for J := 1 to N do begin DB := A[I,J] - C[L1,J]; DA := DA + DB * DB; end; D[I] := DA * AN1[L1]; // // If ISTEP >:= both NCP(L1) & NCP(L2) there will be no transfer of // point I at this step. // cont30: if ((ISTEP >= NCP[L1]) and (ISTEP >= NCP[L2])) then goto cont60; R2 := D[I] / AN2[L2]; DD := ZERO; for J := 1 to N do begin DE := A[I,J] - C[L2,J]; DD := DD + DE * DE; if (DD >= R2) then goto cont60; end; // 40 CONTINUE // // Update cluster centres, NCP, NC, ITRAN, AN1 & AN2 for clusters // L1 & L2. Also update IC1(I) & IC2(I). Note that if any // updating occurs in this stage, INDX is set back to 0. // ICOUN := 0; INDX := 0; ITRAN[L1] := 1; ITRAN[L2] := 1; NCP[L1] := ISTEP + M; NCP[L2] := ISTEP + M; AL1 := NC[L1]; ALW := AL1 - ONE; AL2 := NC[L2]; ALT := AL2 + ONE; for J := 1 to N do begin C[L1,J] := (C[L1,J] * AL1 - A[I,J]) / ALW; C[L2,J] := (C[L2,J] * AL2 + A[I,J]) / ALT; end; // 50 CONTINUE NC[L1] := NC[L1] - 1; NC[L2] := NC[L2] + 1; AN2[L1] := ALW / AL1; AN1[L1] := BIG; if (ALW > ONE) then AN1[L1] := ALW / (ALW - ONE); AN1[L2] := ALT / AL2; AN2[L2] := ALT / (ALT + ONE); IC1[I] := L2; IC2[I] := L1; // // If no re-allocation took place in the last M steps, return. // cont60: if (ICOUN = M) then exit; end; // 70 CONTINUE goto cont10; end; procedure TKMeansForm.Reset; begin inherited; CollectVariableNames(OS3MainFrm.DataGrid, VarList.Items); SelList.Clear; ReplaceChk.Checked := false; StandardizeChk.Checked := true; DescriptiveChk.Checked := false; NoClustersEdit.Clear; ItersEdit.Text := '100'; UpdateBtnStates; end; procedure TKMeansForm.SelListDblClick(Sender: TObject); var index: Integer; begin index := SelList.ItemIndex; if index > -1 then begin VarList.Items.Add(SelList.Items[index]); SelList.Items.Delete(index); UpdateBtnStates; end; end; procedure TKMeansForm.StandardizeChkChange(Sender: TObject); begin ReplaceChk.Enabled := StandardizeChk.Checked; DescriptiveChk.Enabled := StandardizeChk.Checked; end; procedure TKMeansForm.UpdateBtnStates; begin inherited; VarInBtn.Enabled := AnySelected(VarList); VarOutBtn.Enabled := AnySelected(SelList); AllBtn.Enabled := VarList.Items.Count > 0; end; function TKMeansForm.Validate(out AMsg: String; out AControl: TWinControl): Boolean; var n: Integer; begin Result := false; if SelList.Items.Count <= 0 then begin AMsg := 'No variables selected to cluster.'; AControl := VarList; exit; end; if NoClustersEdit.Text = '' then begin AControl := NoClustersEdit; AMsg := 'You must enter the desired number of clusters.'; exit; end; if not TryStrToInt(NoClustersEdit.Text, n) or (n <= 0) then begin AControl := NoClustersEdit; AMsg := 'You must enter the desired number of clusters as a positive value.'; exit; end; if ItersEdit.Text = '' then begin AControl := ItersEdit; AMsg := 'This field cannot be empty.'; exit; end; if not TryStrToInt(ItersEdit.Text, n) or (n <= 0) then begin AControl := ItersEdit; AMsg := 'Positive number required.'; exit; end; Result := true; end; procedure TKMeansForm.VarInBtnClick(Sender: TObject); var i: integer; begin i := 0; while i < VarList.Items.Count do begin if VarList.Selected[i] then begin SelList.Items.Add(VarList.Items[i]); VarList.Items.Delete(i); i := 0; end else i := i + 1; end; UpdateBtnStates; end; procedure TKMeansForm.VarListDblClick(Sender: TObject); var index: Integer; begin index := VarList.ItemIndex; if index > -1 then begin SelList.Items.Add(VarList.Items[index]); VarList.Items.Delete(index); UpdateBtnStates; end; end; procedure TKMeansForm.VarListSelectionChange(Sender: TObject; User: boolean); begin UpdateBtnStates; end; procedure TKMeansForm.VarOutBtnClick(Sender: TObject); var i: integer; begin i := 0; while i < SelList.Items.Count do begin if SelList.Selected[i] then begin VarList.Items.Add(SelList.Items[i]); SelList.Items.Delete(i); i := 0; end else i := i + 1; end; UpdateBtnStates; end; end.