Il presente articolo è stato tradotto automaticamente.

Esecuzione di test

Clustering di dati mediante un'utilità di gestione categorie

James McCaffrey

Scarica il codice di esempio

Dr. James McCaffreyClustering di dati sono il processo di inserimento di elementi di dati in diversi gruppi — cluster, in modo tale che gli elementi in un determinato gruppo sono simili tra loro e diversi da quelli di altri gruppi. Il clustering è utilizza una macchina tecnica che ha molte importanti pratiche di apprendimento. Ad esempio, analisi dei cluster possono essere utilizzato per determinare quali tipi di elementi sono spesso acquistati insieme affinché la pubblicità mirata può essere indirizzata a consumatori, o per determinare quali aziende sono simili, così che i prezzi del mercato azionario possono essere previsto.

La forma più semplice di clustering utilizza quello che ha chiamato l'algoritmo k-means. (Vedi il mio articolo, "Rilevamento dati anomali usando k-Means Clustering," a msdn.microsoft.com/magazine/jj891054.) Purtroppo, il k-means clustering può essere utilizzato solo in situazioni in cui gli elementi di dati sono completamente numerici. Clustering di dati categoriali (come il colore, che può assumere valori come "rosso" e "blu") è un problema difficile. In questo articolo vi presento un inedito (per quanto è possibile determinare) clustering di algoritmo che ho progettato che può gestire gli elementi di dati numerici o categoriali o una miscela di entrambi. Io chiamo questo algoritmo Greedy agglomerato categoria Utility Clustering (GACUC) per distinguerlo da molti altri algoritmi clustering. In questo articolo vi darà tutte le informazioni si bisogno di sperimentare con clustering di dati, aggiungere funzionalità di clustering di un sistema o applicazione .NET, o creare uno strumento potente standalone clustering di dati.

Il modo migliore per capire ciò che il clustering è e vedere dove sono diretto in questo articolo è quello di dare un'occhiata a Figura 1. Il programma demo mostrato in figura è clustering un piccolo set di dati fittizi cinque elementi. Elementi di dati sono a volte chiamati tuple in clustering di terminologia. Ogni tupla ha tre attributi categorici: colore, lunghezza e rigidità. Colore può assumere uno dei quattro possibili valori: rosso, blu, verde o giallo. La lunghezza può essere breve, medio o lungo. Rigidità può essere true o false.

Clustering Categorical Data in Action
Figura 1 Clustering di dati categoriali in azione

Il programma demo converte i dati raw stringa in forma di numero intero per l'elaborazione più efficiente. Per il colore, rosso, blu, verde e giallo sono codificati come 0, 1, 2 e 3, rispettivamente. Lunghezza, corto, medio e lungo sono codificate come 0, 1, 2, rispettivamente. Per la rigidità, false è codificata come 0 e vero è codificato come 1. Così, il primo elemento di dati grezzi, "Red Short vero," è codificato come "0 0 1.

Molti algoritmi di clustering, tra cui GACUC, richiedono il numero di cluster da precisare. In questo caso, il numero di cluster è impostato su 2. Il programma demo utilizza quindi GACUC per trovare il miglior raggruppamento dei dati. Dietro le quinte, l'algoritmo inizia inserendo le tuple di seme 0 e 4 in cluster di 0 e 1, rispettivamente. L'algoritmo di clustering quindi scorre le tuple, assegnando a ciascuno al cluster che genera il miglior risultato complessivo. Perché non usa alcun tipo di dati di training, clustering è chiamato apprendimento non supervisionato. Dopo un passaggio preliminare di clustering, l'algoritmo GACUC esegue un passaggio di raffinazione per cercare di migliorare il clustering. In questo esempio non viene trovato nessun miglioramento.

Internamente, il programma demo definisce un clustering come un array di int. L'indice della matrice indica un indice tupla, e il valore della cella nella matrice indica un cluster. In Figura 1, il clustering migliore ottenuto dall'algoritmo è [0,0,1,1,1], che significa tuple 0 ("rosso breve vero") è in cluster 0; Tuple 1 ("rosso lungo False") è in cluster 0; Tuple 2 ("Blue Medium vero") è in cluster 1; e così via. Il programma demo Visualizza il raggruppamento finale, migliore in formato di stringa per la leggibilità e visualizza anche una metrica chiave chiamata l'utilità di categoria (CU) dei migliori clustering, 0.3733 in questo esempio. Come si vedrà, categoria utilità è la chiave per l'algoritmo di clustering GACUC.

Questo articolo presuppone che si sono avanzate competenze di programmazione con un linguaggio C-famiglia ma assumere non che si sa nulla di clustering di dati. Ho codificato il programma demo utilizzando un approccio non-OOP con il linguaggio c#, ma non dovreste avere troppi problemi refactoring del codice demo di OOP o un'altra lingua. Ho rimosso tutti i errore normale controllo per maggiore chiarezza. Il codice del programma demo è troppo lungo per presentare nella sua interezza in questo articolo, così mi sono concentrato sull'algoritmo GACUC, così sarete in grado di modificare il codice demo per soddisfare il proprio bisogno. Il codice sorgente completo per il programma demo è disponibile presso archive.msdn.microsoft.com/mag201305TestRun.

Categoria utilità

Clustering di dati coinvolge risolvere i due problemi principali. Il primo problema è definire esattamente che cosa rende un buon clustering di dati. Il secondo problema consiste nel determinare una tecnica efficace per la ricerca attraverso tutte le possibili combinazioni per trovare quello migliore di clustering. Utilità di categoria risolve il primo problema. CU è una metrica intelligente che definisce un clustering di bontà. Piccoli valori di CU indicano clustering di poveri mentre i valori più alti indicano meglio clustering. Per quanto ho potuto determinare, CU in primo luogo è stato definito da M. Gluck e J. Corter in un documento di ricerca del 1985 intitolato "Informazioni, incertezza e l'utilità delle categorie."

L'equazione matematica per CU è un po ' intimidatorio a prima vista:

Ma l'equazione è in realtà più semplice di quanto sembra. Nell'equazione, C maiuscola è un clustering complessiva; m minuscola è il numero di cluster; P maiuscola significa "probabilità"; Maiuscola significa attributo (ad esempio colore); V maiuscolo significa valore attributo (ad esempio rosso). A meno che non sei un matematico, computing utilità categoria è meglio compreso da esempio. Supponiamo che il set di dati per essere cluster è quella mostrata Figura 1, e si desidera calcolare il CU del clustering:

k = 0
Red      Short    True
Red      Long     False
k = 1
Blue     Medium   True
Green    Medium   True
Green    Medium   False

Il primo passo è calcolare P(Ck), quali sono le probabilità di ogni cluster. Per k = 0, perché ci sono cinque le tuple del set di dati e due di loro sono in cluster 0, P(C0) = 2/5 = 0,40. Analogamente, P(C1) = 3/5 = 0.60.

Il secondo passo è quello di calcolare l'ultima doppia sommatoria nell'equazione, chiamato il termine probabilità incondizionata. Il calcolo è la somma dei termini N dove N è il numero totale dei diversi valori di attributo nel set di dati e va come questa:

Red: (2/5)^2 = 0.1600
Blue: (1/5)^2 = 0.0400
Green: (2/5)^2 = 0.1600
Yellow: (0/5)^2 = 0.0000
Short: (1/5)^2 = 0.0400
Medium: (3/5)^2 = 0.3600
Long: (1/5)^2 = 0.0400
False: (2/5)^2 = 0.1600
True: (3/5)^2 = 0.3600
Unconditional sum = 1.3200

Il terzo passo è calcolare la sommatoria doppio termine medio, chiamata i termini di probabilità condizionata. Ci sono somme m (dove m è il numero di cluster), ognuno dei quali ha N termini.

Per k = 0 il calcolo va:

Red: (2/2)^2 = 1.0000
Blue: (0/2)^2 = 0.0000
Green: (0/2)^2 = 0.0000
Yellow: (0/2)^2 = 0.0000
Short: (1/2)^2 = 0.2500
Medium: (0/2)^2 = 0.0000
Long: (1/2)^2 = 0.2500
False: (1/2)^2 = 0.2500
True: (1/2)^2 = 0.2500
Conditional k = 0 sum = 2.0000

Per k = 1 il calcolo è:

Red: (0/3)^2 = 0.0000
Blue: (1/3)^2 = 0.1111
Green: (2/3)^2 = 0.4444
Yellow: (0/3)^2 = 0.0000
Short: (0/3)^2 = 0.0000
Medium: (3/3)^2 = 1.0000
Long: (0/3)^2 = 0.0000
False: (1/3)^2 = 0.1111
True: (2/3)^2 = 0.4444
Conditional k = 1 sum = 2.1111

L'ultimo passo è quello di combinare le somme calcolate secondo l'equazione di CU:

CU = 1/2 * [ 0.40 * (2.0000 - 1.3200) + 0.60 * (2.1111 - 1.3200) ]
   = 0.3733

Una spiegazione dettagliata di esattamente perché CU misura la bontà di un clustering è affascina, ma purtroppo di fuori della portata di questo articolo. Il punto chiave è che per ogni cluster di un set di dati contenenti dati categorici, è possibile calcolare un valore che descrive come il clustering è buona.

Ricerca per il Clustering di migliori

Dopo aver definito un modo per misurare la bontà di clustering, il secondo problema da risolvere in qualsiasi algoritmo di clustering è venuta su con una tecnica per la ricerca attraverso tutti i possibili clusterings per il clustering di migliori. In generale non è fattibile per esaminare ogni possibile clustering di un set di dati. Ad esempio, per un set di dati con solo 100 tuple e m = 2 gruppi, ci sono 2 ^ 100 / 2! = 2 ^ 99 clusterings possibile. Anche se in qualche modo è possibile esaminare 1 trilione clusterings al secondo, ci vorrebbero circa 19 miliardi anni a controllare ogni possibile clustering. (Per confronto, l'età stimata dell'universo è circa 14 miliardi di anni).

Diversi algoritmi di clustering utilizzano diverse tecniche per la ricerca attraverso tutti i possibili clusterings. L'algoritmo GACUC utilizza quello che viene chiamato un approccio agglomerativo avido. L'idea è di iniziare da semina ogni cluster con una singola tupla e quindi, per ogni tupla rimanente, determinare quale k cluster', se la tupla corrente sono state aggiunte ad esso, si produrrebbe il clustering complessiva migliore. Quindi, la tupla corrente è effettivamente assegnata a cluster k'. Questa tecnica non garantisce che il clustering ottimale sarà trovato, ma gli esperimenti hanno dimostrato che la tecnica produce generalmente un ragionevolmente buona clustering. Il raggruppamento finale prodotto dall'algoritmo di GACUC dipende quale m tuple sono selezionate come seme tuple e l'ordine in cui le tuple rimanenti vengono assegnate al cluster.

Si scopre che selezionando una tupla di seme per ogni cluster non è banale. Un approccio naïve sarebbe semplicemente selezionare le tuple casuale come i semi. Tuttavia, se le tuple di seme sono simili a vicenda, il clustering risultante sarà scarsa. Un approccio migliore per la selezione di sementi Tuple è scegliere m tuple che sono diverse gli uni dagli altri. Qui è dove utilità categoria viene pratico ancora — il CU di qualsiasi potenziale insieme di tuple di seme del candidato può essere calcolato, e il set di tuple con il CU migliore (più grande valore, significato più dissimili) può essere usato come le tuple di seme. Come prima, non è generalmente fattibile per esaminare ogni possibile insieme di tuple di seme, quindi l'approccio è quello di selezionare casuale Tuple m ripetutamente, calcolare il CU di quelle Tuple casuale e utilizzare il set che ha il miglior CU come semi.

Struttura generale del programma

Il metodo principale del programma demo mostrato in esecuzione in Figura1, con alcune istruzioni WriteLine e commenti rimossi, è elencato Figura 2. Ho usato Visual Studio 2010 con Microsoft .NET Framework 4, ma il codice demo ha significativi non dipendano­dencies e qualsiasi versione di Visual Studio che supporta il .NET Framework 2.0 o superiore dovrebbe funzionare bene. Per semplicità, ho creato una singola applicazione console c# denominata Clustering­categorico; è possibile implementare il clustering come una libreria di classi.

Figura 2 struttura generale del programma

using System;
using System.Collections.Generic;
namespace ClusteringCategorical
{
  class ClusteringCategoricalProgram
  {
    static Random random = null;
    static void Main(string[] args)
    {
      try
      {
        random = new Random(2);
        Console.WriteLine("\nBegin category utility demo\n");
        string[] attNames = new string[] { "Color", "Length", "Rigid" };
        string[][] attValues = new string[attNames.Length][];
        attValues[0] = new string[] { "Red", "Blue", "Green", "Yellow" };
        attValues[1] = new string[] { "Short", "Medium", "Long" };
        attValues[2] = new string[] { "False", "True" };
        string[][] tuples = new string[5][];
        tuples[0] = new string[] { "Red", "Short", "True" };
        tuples[1] = new string[] { "Red", "Long", "False" };
        tuples[2] = new string[] { "Blue", "Medium", "True" };
        tuples[3] = new string[] { "Green", "Medium", "True" };
        tuples[4] = new string[] { "Green", "Medium", "False" };
        Console.WriteLine("Tuples in raw (string) form:\n");
        DisplayMatrix(tuples);
        int[][] tuplesAsInt = TuplesToInts(tuples, attValues);
        Console.WriteLine("\nTuples in integer form:\n");
        DisplayMatrix(tuplesAsInt);
        int numClusters = 2;
        int numSeedTrials = 10;
        Console.WriteLine("Initializing clustering result array");
        int[] clustering = InitClustering(tuplesAsInt);
        int[][][] valueCounts = InitValueCounts(tuplesAsInt, attValues,
          numClusters);
        int[][] valueSums = InitValueSums(tuplesAsInt, attValues);
        int[] clusterCounts = new int[numClusters];
        Console.WriteLine("\nBeginning clustering routine");
        Cluster(tuplesAsInt, attValues, clustering, valueCounts,
          valueSums, clusterCounts, numSeedTrials);
        double cu = CategoryUtility(valueCounts, valueSums,
          clusterCounts);
        Console.WriteLine("\nCategory Utility of clustering = " +
          cu.ToString("F4"));
        DisplayVector(clustering);
        Refine(20, tuplesAsInt, clustering, valueCounts, valueSums,
          clusterCounts);
        Console.WriteLine("Refining complete");
        DisplayVector(clustering);
        Console.WriteLine("\nFinal clustering in string form:\n");
        DisplayClustering(numClusters, clustering, tuples);
        cu = CategoryUtility(valueCounts, valueSums, clusterCounts);
        Console.WriteLine("\nFinal clustering CU = " + cu.ToString("F4"));
        Console.WriteLine("\nEnd demo\n");
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
      }
    } // Main
    // All other methods here
  } // class Program
} // ns

In Figura 2, metodo Cluster esegue un'iterazione dell'algoritmo di clustering base di GACUC. NumSeedTrials variabile è impostata su 10 ed è passato alla routine che determina il seme iniziale tuple che sono assegnate a ciascun cluster. Metodo affinare esegue post-clustering passa attraverso i dati nel tentativo di trovare un clustering che produce una migliore utilità di categoria.

Le strutture di dati chiave

Anche se è possibile calcolare l'utilità di categoria di un set di dati al volo scorrendo ogni tupla del set di dati, perché il metodo di clustering deve calcolare utilità categoria molte volte, un approccio migliore è quello di memorizzare i conteggi dei valori di attributo di tuple che sono assegnate a cluster in un dato punto nel tempo. Figura 3 Mostra la maggior parte delle strutture di dati chiave utilizzate dall'algoritmo GACUC.

Key Data Structures
Figura 3 strutture dati chiave

Matrice valueCounts contiene il numero di valori di attributi dal cluster. Ad esempio, valueCounts [0] [2] [1] contiene il numero di tuple con attributo 0 (colore) e il valore 2 (verde) che sono attualmente assegnati al cluster 1. Matrice valueSums contiene la somma dei conti, attraverso tutti i cluster, per ogni valore di attributo. Ad esempio, valueSums [0] [2] contiene il numero totale di tuple con attributo 0 (colore) e il valore 2 (verde) che sono assegnate a tutti i cluster. Matrice clusterCounts contiene il numero di tuple che sono attualmente assegnati a ciascun cluster. Ad esempio, se numClusters = 2 e clusterCounts = [2,2], poi ci sono due tuple assegnate a 0 del cluster e due tuple assegnate al cluster 1. Array clustering di codifica per l'assegnazione di tuple di cluster. L'indice di clustering rappresenta una tupla con un valore di cella rappresenta un cluster e un valore di -1 indica che la tupla associata non ancora assegnata a ogni cluster. Per esempio, clustering [2] = 1, tupla 2 viene assegnato al cluster 1.

Codifica del metodo CategoryUtility

Il codice per il metodo che calcola le utilità di categoria non è concettualmente difficile, ma è un po ' complicato. Inizio della definizione del metodo:

static double CategoryUtility(int[][][] valueCounts, 
  int[][] valueSums,
  int[] clusterCounts)
{
  int numTuplesAssigned = 0;
  for (int k = 0; k < clusterCounts.Length; ++k)
    numTuplesAssigned += clusterCounts[k];
  int numClusters = clusterCounts.Length;
  double[] clusterProbs = new double[numClusters];   // P(Ck)
  for (int k = 0; k < numClusters; ++k)
    clusterProbs[k] = (clusterCounts[k] * 1.0) / numTuplesAssigned;

I tre input matrici, valueCounts, valueSums e clusterCounts sono presupposti per contenere valori validi che riflettono il clustering di corrente, come descritto nella sezione precedente e in Figura 3. Il metodo inizia con la scansione attraverso la matrice di clusterCounts per calcolare il numero totale di tuple che sono attualmente assegnati al cluster. Il numero di cluster viene dedotto dalla proprietà della matrice clusterCounts di lunghezza, e la probabilità per i cluster sono quindi calcolato e memorizzato nella matrice locale clusterProbs.

Il passo successivo è quello di calcolare la singola probabilità incondizionata per il clustering di corrente:

double unconditional = 0.0;
for (int i = 0; i < valueSums.Length; ++i)
{
  for (int j = 0; j < valueSums[i].Length; ++j)
  {
    double p = (valueSums[i][j] * 1.0) / numTuplesAssigned;
    unconditional += (p * p);
  }
}

Dopo che è stata calcolata la probabilità incondizionata, il passo successivo è di calcolare una probabilità condizionale per ogni cluster:

double[] conditionals = new double[numClusters];
for (int k = 0; k < numClusters; ++k)
{
  for (int i = 0; i < valueCounts.Length; ++i)
  {
    for (int j = 0; j < valueCounts[i].Length; ++j)
    {
      double p = (valueCounts[i][j][k] * 1.0) / clusterCounts[k];
      conditionals[k] += (p * p);
    }
  }
}

Ora, con la probabilità di ogni cluster nella matrice clusterProbs, il termine probabilità incondizionata nella variabile incondizionato e i termini di probabilità condizionata in istruzioni condizionali di matrice, l'utilità di categoria per il clustering può essere determinato:

double summation = 0.0;
for (int k = 0; k < numClusters; ++k)
  summation += clusterProbs[k] * 
    (conditionals[k] - unconditional);
return summation / numClusters;
}

Un buon modo per capire il comportamento della funzione CU è a sperimentare con il codice demo fornendo clusterings hardcoded. Ad esempio, è possibile modificare il codice in Figura 2 lungo le linee di:

string[] attNames = new string[] { "Color", "Length", "Rigid" };
// And so on, as in Figure 1
int[][] tuplesAsInt = TuplesToInts(tuples, attValues);
int[] clustering[] = new int[] { 0, 1, 0, 1, 0 };
// Hardcoded clustering
double cu = CategoryUtility(valueCounts, valueSums, clusterCounts);
Console.WriteLine("CU of clustering = " + cu.ToString("F4"));

Implementazione del metodo Cluster

Con un metodo di utilità di categoria in mano, implementazione di un metodo di dati cluster è relativamente semplice. In pseudo-codice ad alto livello, metodo Cluster è:

define an empty clustering with all tuples unassigned
determine m good (dissimilar) seed tuples
assign one good seed tuple to each cluster
loop each unassigned tuple t
  compute the CU for each cluster if t were actually assigned
  determine the cluster that gives the best CU
  assign tuple t to best cluster
end loop (each tuple)
return clustering

Per semplicità, metodo Cluster scorre ogni tupla non assegnato in ordine, a partire da tuple [0]. Questo dà efficacemente più influenza di tuple con basso numero di indici. Un approccio alternativo che uso spesso è quello di scorrere le tuple non assegnate in ordine casuale o nell'ordine criptato utilizzando un generatore lineare, congruenziale, ciclo completo.

Una parte sorprendentemente difficile dell'algoritmo è trovare m buon seme Tuple. In pseudo-codice ad alto livello, il metodo GetGoodIndexes è:

loop numSeedTrials times
  create a mini-tuples array with length numClusters
  pick numClusters random tuple indexes
  populate the mini-tuples with values from data set
  compute CU of mini data set
  if CU > bestCU
    bestCU = CU
    indexes of best mini data set = indexes of data set
  end if
end loop
return indexes of best mini data set

Questo approccio genera casuali tuple indici utilizzando un approccio brute-force generare-controllo-se-non-usato, che ha funzionato bene per me in pratica.

Perché il metodo Cluster sarà, in generale, restituire un clustering buono ma non ottimale, l'algoritmo GACUC facoltativamente chiama un metodo di affinare che tenta di riorganizzare le assegnazioni di cluster tupla nel tentativo di trovare un cluster con un valore di utilità categoria migliore. In pseudo-codice, metodo Raffini è:

loop numRefineTrials times
  select a random tuple that is in a cluster with at least two tuples
  determine the random tuple's cluster
  select a second cluster that is different from curr cluster
  unassign tuple from curr cluster
  assign tuple to different cluster
  compute new CU
  if new CU > old CU
    leave the cluster switch alone
  else
    unassign tuple from different cluster
    assign tuple back to original cluster
  end if
end loop

Ci sono molti altri perfezionamenti post-elaborazione con cui si può sperimentare. La raffinatezza precedente mantiene un numero fisso di cluster. Una possibilità consiste nel definire un metodo di perfezionamento che permette il numero di cluster per aumentare o diminuire.

Un metodo di supporto chiave dell'algoritmo di clustering GACUC è quella che assegna una tupla di un cluster:

static void Assign(int tupleIndex, int[][] tuplesAsInt, 
  int cluster,  int[] clustering, 
  int[][][] valueCounts, int[][] valueSums,
  int[] clusterCounts)
{
  clustering[tupleIndex] = cluster;  // Assign
  for (int i = 0; i < valueCounts.Length; ++i)  // Update
  {
    int v = tuplesAsInt[tupleIndex][i];
    ++valueCounts[i][v][cluster];
    ++valueSums[i][v];
  }
  ++clusterCounts[cluster];  // Update
}

Si noti che il metodo assegna accetta un sacco di parametri; Questa è una debolezza generale dell'utilizzo di metodi statici piuttosto che un approccio OOP. Metodo assegnare modifica la matrice di clustering e quindi aggiorna le matrici che tengono i conteggi per ogni valore dell'attributo di cluster (valueCounts), i conteggi per ogni valore dell'attributo attraverso tutti i cluster (valueSums) e il numero di tuple assegnate a ogni cluster (clusterCounts). Metodo l'assegnazione è essenzialmente l'opposto di assegnazione. Le tre istruzioni chiave nel metodo di assegnazione sono:

clustering[tupleIndex] = -1;  // Unassign
--valueCounts[i][v][cluster]; // Update
--valueSums[i][v];            // Update
--clusterCounts[cluster];     // Update

Conclusioni

Download del codice che accompagna questo articolo, insieme con la spiegazione di GACUC clustering algoritmo presentato qui, dovrebbe arrivare e in esecuzione se volete sperimentare con clustering di dati, aggiungere funzionalità di clustering per un'applicazione, o creare uno strumento di utilità clustering. A differenza di molti algoritmi di clustering (tra cui k-means), l'algoritmo GACUC può essere facilmente modificato per gestire dati numerici o misti dati numerici e categorici. L'idea è di pre-elaborazione dei dati numerici da esso binning in categorie. Ad esempio, se i dati raw contenevano altezze del popolo misurate in pollici, si potrebbe bin altezza così che i valori meno di 60,0 sono "breve," valori tra 60,0 74,0 "medio", e sono valori maggiori di 74,0 sono "alti".

Esistono molti algoritmi per il clustering di dati categoriali, inclusi gli algoritmi che si basano sull'utilità di categoria per definire la bontà di clustering. Tuttavia, l'algoritmo presentato qui è relativamente semplice, ha lavorato bene nella pratica, può essere applicato ai dati numerici sia categorici e scala bene per grandi insiemi di dati. GACUC clustering può essere una preziosa aggiunta alla vostra strumento per sviluppatori insieme dati.

Dr.James McCaffrey funziona per Volt Information Sciences Inc., dove gestisce la formazione tecnica per gli ingegneri software Microsoft Redmond, Wash., campus. Si è occupato di diversi prodotti Microsoft, inclusi Internet Explorer e MSN Search. Egli è l'autore di ".NET Test Automation Recipes" (Apress, 2006) e può essere raggiunto a jammc@microsoft.com.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Dan Liebling (Microsoft Research)