65.9K
CodeProject 正在变化。 阅读更多。
Home

WordCloud - 词频的方形树图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (12投票s)

2007年8月10日

4分钟阅读

viewsIcon

85343

downloadIcon

2281

一个词频的方形树图。

Screenshot - wordcloud1.gif
Screenshot - wordcloud2.gif

引言

WordCloud 是一个可视化表示,展示给定词集中一个词被使用的次数,或者说它的频率。它通过以下步骤实现:读取纯文本、过滤“停用词”、计算词语使用的次数,并将结果显示在一个方形树图中。(在上面的图片中,节点越大、颜色越饱和,表示使用频率越高。)

背景

我非常欣赏并受到 Chirag Mehta 的 酷炫的基于 Web 的标签云生成器(统计美国总统演讲) 的启发。于是我尝试用 .NET 实现一个简化版本。

我最多算是一个业余爱好者,对这个示例中使用的技术也只是略知一二,所以我参考了许多我读过的文章来创建 WordCloud。

方形树图

  • 显示由 Microsoft 的 TreemapGenerator 处理,它是数据可视化组件套件的一部分。虽然真正的树图同时利用了层级和比例属性,但 WordCloud 只使用比例属性来显示词频。
  • 维基百科上的 树图概述 是理解其起源的好地方。
  • Jonathan Hodgson 在 CodeProject 上的 方形树图文章 对这个主题进行了深入的探讨。
  • WordCloud 执行的功能与 标签云 基本相同。
  • Newsmap - 一个令人印象深刻的基于 Flash 的 Google 新闻方形树图。
  • 互联网标签网站 del.icio.us 最受欢迎的树图

词干提取

停用词

  • 停用词 用于在处理前过滤掉常用词。

代码

要构建 WordCloud,你需要获取最新版本的 Microsoft 数据可视化组件,并更新 WordCloud 的项目引用以包含 `TreemapGenerator`。你可以在 \VisualizationComponents\Treemap\Latest\Components\TreemapGenerator.dll 中找到此引用。注意:WordCloud 需要 .NET Framework 2.0 或更高版本才能构建和运行。

TreemapPanel.cs

TreemapPanel 负责节点渲染。节点被预处理成一个 `ArrayList` 集合,然后添加到 `TreemapGenerator` 中。对象数据以 `NodeInfo` 的形式存储在每个节点中。

// Treemap drawing engine in TreemapPanel.cs
protected TreemapGenerator m_oTreemapGenerator;

...

public void PopulateTreeMap(Hashtable wordsHash, Hashtable stemmedWordsHash)
{
    AssertValid();

    ArrayList nodes = new ArrayList();
    ArrayList aKeys = new ArrayList(stemmedWordsHash.Keys);
    aKeys.Sort();

    foreach (string key in aKeys)
    {
        //build each node element
        int count = (int)stemmedWordsHash[key];
        string name = (string)wordsHash[key];

        //show count in node?
        if(m_bShowWordCount)
            name += String.Format(" ({0})", count);
        NodeInfo nodeinfo = new NodeInfo(name, count);
        nodes.Add(nodeinfo);
    }
    m_nodes = nodes;
    RepopulateTreeMap();
}

...

private void RepopulateTreeMap()
{
    if(m_nodes.Count == 0)
        return;

    Nodes TreemapGeneratorNodes;

    //reset treemap
    m_TreemapGenerator.Clear();

    TreemapGeneratorNodes = m_TreemapGenerator.Nodes;

    foreach(NodeInfo n in m_nodes)
    {
        //does this node have enough to display?
        if(n.Count >= m_nDisplayCount)
        {
            //Create node with basic default size and color
            Node oWordNode = new Node(n.Name, n.Count * 50.0f, 0F);

            //set object data
            oWordNode.Tag = n;

            //add category to tree
            TreemapGeneratorNodes.Add(oWordNode);

            //used later for determining node color
            if (n.Count > m_nLargestCount)
                m_nLargestCount = n.Count;
            else if (n.Count < m_nSmallestCount)
                m_nSmallestCount = n.Count;
        }
    }
}

绘制节点

树图使用自定义的节点绘制方法,该方法在 OnPaint 中调用。

// We want to do owner drawing, so handle the DrawItem event.
m_TreemapGenerator.DrawItem += 
    new TreemapGenerator.TreemapDrawItemEventHandler(DrawItem);

...

protected override void OnPaint(PaintEventArgs e)
{
    AssertValid();

    // Save the Graphics object so it can be accessed by OnDrawItem().
    m_Graphics = e.Graphics;

    // Tell the TreemapGenerator to draw the treemap using owner-
    // implemented code.  This causes the DrawItem event to get fired for
    // each node in the treemap.
    m_TreemapGenerator.Draw(this.ClientRectangle);

    // All DrawItem events have been fired.  Make sure the Graphics object
    // doesn't get used again.
    m_Graphics = null;
}

节点渲染在 `DrawItem()` 方法中处理。在该方法中,我们提取 `NodeInfo` 对象,获取名称和计数,根据计数设置颜色和文本大小,然后绘制节点。最终的节点结果是:计数越大,文本越大,颜色越饱和。

private void DrawItem(Object sender, TreemapDrawItemEventArgs e)
{
    AssertValid();

    Node oNode = e.Node;
    float fontSize = m_FontSize;
    int count = 0;

    // Retrieve the NodeInfo object from the node's tag.
    if (oNode.Tag is NodeInfo)
    {
        //get word count
        NodeInfo oInfo = (NodeInfo)oNode.Tag;
        count = oInfo.Count;

        //if we're using text scaling, increment font size
        if(m_bUseTextScaling)
            fontSize += oInfo.Count;
    }
    else
    {
        //should never get here
        Debug.WriteLine("DrawItem: Skipping node - bad");
        return;
    }
    //set color alpha based on frequency
    Color newStartColor = GetColor(count, m_startColor);
    Color newEndColor = GetColor(count, m_endColor);

    //set gradient colors and gamma
    LinearGradientBrush nodeBrush = new LinearGradientBrush(e.Bounds,
        newStartColor, newEndColor, LinearGradientMode.Vertical);

    nodeBrush.GammaCorrection = true;

    m_Graphics.FillRectangle(nodeBrush, e.Bounds);

    // Create font and align in the center
    Font newfont = new Font(m_FontName, fontSize, m_FontStyle);
    StringFormat sf = new StringFormat();
    sf.Alignment = StringAlignment.Center;
    sf.LineAlignment = StringAlignment.Center;

    //draw the text
    m_Graphics.DrawString(e.Node.Text, newfont, new SolidBrush(m_FontColor), 
        e.Bounds, sf);

    // Draw a black border around each node
    Pen blackPen = new Pen(Color.Black, 2);
    m_Graphics.DrawRectangle(blackPen, e.Bounds);

    //clean up
    nodeBrush.Dispose();
    newfont.Dispose();
    blackPen.Dispose();
}

文本“整理”

主窗体中的一个工作线程方法 `DoWordProcessing()` 处理词集合文档。词干提取也在该方法中进行,用于剥离词的后缀。

private void DoWordProcessing(object obj)
{
    //unpack array
    object[] objArray = (object[])obj;
    IProgressCallback callback = (IProgressCallback)objArray[0];
    StringBuilder sbRawText = (StringBuilder)objArray[1];
    ArrayList arrStopWords = (ArrayList)objArray[2];

    try
    {
        //Build a hash of words and thier frequency
        Hashtable wordsHash = new Hashtable();
        Hashtable stemmedWordsHash = new Hashtable();
        PorterStemmer ps = new PorterStemmer();

        //construct our document from the input text
        Document doc = new Document(sbRawText.ToString());

        callback.Begin(0, doc.Words.Count);

        for (int i = 0; i < doc.Words.Count; ++i)
        {
            //cancel button clicked?
            if (callback.IsAborting)
            {
                callback.End();
                return;
            }
            //update progress dialog
            callback.SetText(String.Format("Reading word: {0}", i));
            callback.StepTo(i);

            //Don't do numbers
            if (!IsNumeric(doc.Words[i]))
            {
                // normalize each word to lowercase
                string key = doc.Words[i].ToLower();

                //check stop words list
                if (!arrStopWords.Contains(key))
                {
                    //set our stemming term
                    ps.stemTerm(key);

                    //get the stem word
                    string stemmedKey = ps.getTerm();

                    //either add to hash or increment frequency
                    if (!stemmedWordsHash.Contains(stemmedKey))
                    {
                        //add new word
                        stemmedWordsHash.Add(stemmedKey, 1);
                        wordsHash.Add(stemmedKey, key);
                    }
                    else
                    {
                        //increment word count
                        stemmedWordsHash[stemmedKey] = 
                            (int)stemmedWordsHash[stemmedKey] + 1;
                    }
                }
            }
        }
        //now let the treemap load the information
        this.TreePanel.PopulateTreeMap(wordsHash, stemmedWordsHash);
    }
    catch (System.Threading.ThreadAbortException)
    {
        // noop
    }
    catch (System.Threading.ThreadInterruptedException)
    {
        // noop
    }
    finally
    {
        if (callback != null)
        {
            callback.End();
        }
    }
}

演示应用程序

控件

Screenshot - wordcloud3.gif

工具栏按钮说明(从左到右依次)

  • 打开文本文件:打开一个文本文件文档进行可视化
  • 输入文本:在此对话框中粘贴其他文档的文本进行可视化(最大 128k,但可根据需要更改)
  • 停用词:一个对话框,允许你修改停用词集**
    Screenshot - wordcloud4.gif
  • 字体:一个对话框,允许你设置显示字体
  • 节点颜色:一个对话框,允许你设置节点显示的渐变颜色
    Screenshot - wordcloud5.gif
  • 缩放文本:切换以根据计数缩放文本
  • 显示计数:切换以显示/隐藏节点中的词频**
  • 最小词频滑块:根据词频动态控制显示多少个节点
  • 另存为图像:将树图保存为 gif 图像

**注意:** 文档文本不会保留在内存中;它只会被解析,作为节点添加到树图中,然后丢弃。因此,“显示计数”和“停用词”功能仅在打开/输入文本之前有用;它不会动态显示/隐藏节点计数或应用停用词。

输入数据

我尝试了各种文档大小,从 400 到 6000 字不等——主要是总统演讲等。在项目中,我包含了两个文本文件:*mlk.txt* 和 *kennedy.txt*。它们分别是马丁·路德·金于 1963 年 8 月 28 日在华盛顿游行上的“我有一个梦想”演讲,以及前美国总统约翰·F·肯尼迪 1961 年的国情咨文——分别为 1,588 和 5,184 字。

另一个需要注意的问题是停用词。我添加了一组默认的停用词,这些停用词是用户可配置的,并且极大地影响了词语解析。提供的 430 个停用词相当标准,涵盖了大量停用词,但又不至于过于激进。

结论

虽然这个示例非常基础,未经优化,非基于 Web,充其量只能算入门级(与其他标签/词云生成器相比),但它或许可以为对这个想法感兴趣的人提供一个起点。它也可以作为使用 Microsoft 数据可视化组件套件中 TreemapGenerator 的一个基本示例。

鸣谢

Tony Capone 在 Google Groups 帖子 中提供的 TreemapGenerator 代码

Matthew Adams 的 进度对话框

Leif Azzopardi 对 Porter 的 Porter 词干提取算法的移植

© . All rights reserved.