cyqian

cyqian 查看完整档案

苏州编辑  |  填写毕业院校格网科技  |  VP 编辑 qiancy.com 编辑
编辑

用地理学思维和现代信息技术解决城市问题。

个人动态

cyqian 赞了文章 · 2016-08-06

D3 源代码解构

D3是一个数据可视化的javascript库,相对于highchart和echarts专注图表可视化的库,D3更适合做大数据处理的可视化,它只提供基础的可视化功能,灵活而丰富的接口让我们能开发出各式各样的图表。

D3代码版本:“3.5.17”

D3的代码骨架比较简洁,相比jquery来说更适合阅读,你可以很舒服地自上而下的看下去而不用看到一个新的函数发现声明在千里之外,然后在代码中跳来跳去。

内部代码流水线

  • 基本的数学计算:最小最大、均值中值方差、偏分值……

  • 各种集合类型: map、set、nest……

  • 集合的操作、方法: text、html、append、insert、remove

  • d3的dragging

  • 图形操作

  • ……

自执行匿名函数

首先是典型的自执行匿名函数,对外提供接口,隐藏实现方式,实现私有变量等等功能。

!function() {
   // code here
}()

这里用到的是感叹号,其实和使用括号是一样的作用,就是将函数声明变成函数表达式,以便于函数的自执行调用,你可以试试

function() {
  console.log('no console')
}()

这是因为JS禁止函数声明和函数调用混用,而括号、逻辑运算符(+、-、&&、||)、逗号、new等都可以将函数声明变成函数表达式,然后便可以自执行。有人做过调查关于这些转化的方法哪个更快,可以查看这篇博客,大概new是最慢的,相比使用括号是基本最快,感叹号反而性能一般,所以其实用哪个都没什么区别,当然如果你想省敲一个符号也是可以用感叹号的。

对外暴露私有变量d3

对于d3,采用的是创建私有变量对象,然后对它进行扩展,最后对外暴露

var d3 = {
  version: '3.5.17'
};

// code here
//...

if (typeof define === 'function' && defind.amd) 
  this.d3 = d3, define(d3);
else if (typeof module == 'object' && module.exports)
  module.exports = d3;
else
  this.d3 = d3;

第一种为异步模块加载模式,第二种为同步模块加载或者是ecma6的import机制,第三种则是将d3设置为全局变量,因为匿名自执行函数中,函数的环境就是全局的,所以this == window。

创建公用方法

d3的方法是属于d3对象的属性:

d3_xhr( url, mimeType, response, callback) {
  // code 
}
d3.json = function(url, callback) {
  return d3_xhr(url, 'application/json', d3_json, callback);
};
function d3_json(request) {
  return JSON.parse(request.responseText);
}

不太好的是d3没有在命名上区分哪些是私有函数,哪些是公用函数,不过对于通过创建对象来对外暴露接口的对象来说,应该也不用去区分吧。

提取一些常用的原生函数

var d3_arraySlice = [].slice, d3_array = function(list) {
  return d3_arraySlice.call(list);
};
var d3_document = this.document;

提取slice方法,使用它来生成数组的副本,slice不会对原生数组做切割,而是会返回数组的复制品,但是要注意是浅复制,对于数组中的对象、数组,是单纯的引用,所以对原数组中的对象或数组的更改还是会影响到复制品。

部分代码实现阅读

一段用来测试d3_array的函数,但什么情况下会重写d3_array函数呢?

【line15】

if (d3_document) {
  var test = d3_array(d3_document.documentElement.childNodes);
  console.log(test);
  try {
    d3_array(d3_document.documentElement.childNodes)[0].nodeType;
  } catch (e) {
    console.log('catch error:', e);
    d3_array = function(list) {
      var i = list.length, array = new Array(i);
      while (i--) array[i] = list[i];
      return array;
    };
  }
}

由前面我们可以知道d3_array可以用来获取传入数组的副本,通过try来测试document的子节点的第一个子元素,一般就是header这个元素,我们通过查询w3c可以知道nodeType为1,表示html element,感觉应该是测试是否是浏览器环境,如果不是的话,就换成自己写的函数的意思吗?还是为了兼容一些少数的浏览器呢?

设置对象属性的兼容?

【line 30】

if (d3_document) {
  try {
    d3_document.createElement("DIV").style.setProperty("opacity", 0, "");
  } catch (error) {
    var d3_element_prototype = this.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = this.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty;
    d3_element_prototype.setAttribute = function(name, value) {
      d3_element_setAttribute.call(this, name, value + "");
    };
    d3_element_prototype.setAttributeNS = function(space, local, value) {
      d3_element_setAttributeNS.call(this, space, local, value + "");
    };
    d3_style_prototype.setProperty = function(name, value, priority) {
      d3_style_setProperty.call(this, name, value + "", priority);
    };
  }
}

暂时不知道是为了跨浏览器还是跨文档而做的检测,待研究。

数组最小值函数

【line 53】

  d3.min = function(array, f) {
    var i = -1, n = array.length, a, b;
    if (arguments.length === 1) {
      while (++i < n) if ((b = array[i]) != null && b >= b) {
        a = b;
        break;
      }
      while (++i < n) if ((b = array[i]) != null && a > b) a = b;
    } else {
      while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) {
        a = b;
        break;
      }
      while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b;
    }
    return a;
  };

首先获取第一个可比较的元素,测试了下,发现对于b >= b,无论b是数字、字符串、数组甚至是对象都是可以比较的,那么什么情况下 b>=b == false呢,对于NaN来说,无论和哪个数字比较,都是false的,但是对于Infinity却返回真,是个点。所以应该是为了排除NaN这种有问题的数字。

d3的洗牌方法

  d3.shuffle = function(array, i0, i1) {
    if ((m = arguments.length) < 3) {
      i1 = array.length;
      if (m < 2) i0 = 0;
    }
    var m = i1 - i0, t, i;
    while (m) {
      i = Math.random() * m-- | 0;
      t = array[m + i0], array[m + i0] = array[i + i0], array[i + i0] = t;
      console.log(i, m);
    }
    return array;
  };

d3使用的洗牌算法,关于Fisher-Yates shuffle的文章可以参考一下,它的演变思路简单而优雅:

正常的思路是

  • 每次从原数组中随机选择一个元素,判断是否已经被选取,是的话删除并放入新的数组中,不是的话重新选择。

  • 缺点:越到后面重复选择的概率越大,放入新数组的时间越长。

优化

  • 为了防止重复,每次随机选择第m张卡牌,m为待洗牌组从原始长度n逐步递减的值

  • 缺点:每次都要重新获取剩余数组中的卡牌的紧凑数组,实际的效率为n2

再次优化

  • 就地随机洗牌,使用数组的后一部分作为存储新的洗牌后的地方,前一部分为洗牌前的地方,从而将效率提升为n。

d3.map 关于内置对象

【line 291】

  function d3_class(ctor, properties) {
    for (var key in properties) {
      Object.defineProperty(ctor.prototype, key, {
        value: properties[key],
        enumerable: false
      });
    }
  }
  d3.map = function(object, f) {
    var map = new d3_Map();
    if (object instanceof d3_Map) {
      object.forEach(function(key, value) {
        map.set(key, value);
      });
    } else if (Array.isArray(object)) {
      var i = -1, n = object.length, o;
      if (arguments.length === 1) while (++i < n) map.set(i, object[i]); else while (++i < n) map.set(f.call(object, o = object[i], i), o);
    } else {
      for (var key in object) map.set(key, object[key]);
    }
    return map;
  };
  function d3_Map() {
    this._ = Object.create(null);
  }
  var d3_map_proto = "__proto__", d3_map_zero = "\x00";
  d3_class(d3_Map, {
    has: d3_map_has,
    get: function(key) {
      return this._[d3_map_escape(key)];
    },
    set: function(key, value) {
      return this._[d3_map_escape(key)] = value;
    },
    remove: d3_map_remove,
    keys: d3_map_keys,
    values: function() {
      var values = [];
      for (var key in this._) values.push(this._[key]);
      return values;
    },
    entries: function() {
      var entries = [];
      for (var key in this._) entries.push({
        key: d3_map_unescape(key),
        value: this._[key]
      });
      return entries;
    },
    size: d3_map_size,
    empty: d3_map_empty,
    forEach: function(f) {
      for (var key in this._) f.call(this, d3_map_unescape(key), this._[key]);
    }
  });

关于enumerable

在这里,使用d3_Map来作为对象的构造函数,d3_class来封装类,这里调用了Object.defineProperty来设置属性和值,这里有一个enumerable: false的属性,它将该属性的可枚举性设置为false,使得该属性在一般的遍历中(for...in...)等中无法被获取,但是还是可以通过obj.key直接获取到,如果需要获取对象自身的所有属性,不管enumerable的值,可以使用 Object.getOwnPropertyNames 方法。

为什么要设置这个属性呢?我们可以看到对d3_Map构造对象时,引入了一些原生内置的方法,其中有一个叫做empty的方法用来判断后来设置的属性是否为空,我们来看看这个函数的实现:

  function d3_map_empty() {
    for (var key in this._) return false;
    return true;
  }

看完之后再结合上面提到的enumerable设置为false的属性在for循环中会被忽略,这样的话就不用再写额外地条件去判断是否为内置属性,很棒的实现方式。

数据绑定函数data

还记得D3独特的将数据和图形领域联系起来的方式吗?进入(enter)--更新(update)--退出(exit) 模式。

【line 832】

d3.selectAll('div')
  .data(dataSet)
  .enter()
  .append('div')
  ;
d3.selectAll('div')
  .data(data)
  .style('width', function(d) {
     return d + 'px';
  })
 ;
d3.selectAll('div')
  .data(newDataSet)
  .exit()
  .remove()
  ;

这里涉及到了三个函数,data、enter、exit,每次进行操作前我们需要先调用data对数据进行绑定,然后再调用enter或者exit对图形领域进行操作,那么内部实现原理是怎么样的呢,看完下面这段代码就恍然大悟了:

  d3_selectionPrototype.data = function(value, key) {
    var i = -1, n = this.length, group, node;
    if (!arguments.length) {
      value = new Array(n = (group = this[0]).length);
      while (++i < n) {
        if (node = group[i]) {
          value[i] = node.__data__;
        }
      }
      return value;
    }
    function bind(group, groupData) {
      var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData;
      if (key) {
        var nodeByKeyValue = new d3_Map(), keyValues = new Array(n), keyValue;
        for (i = -1; ++i < n; ) {
          if (node = group[i]) {
            if (nodeByKeyValue.has(keyValue = key.call(node, node.__data__, i))) {
              exitNodes[i] = node;
            } else {
              nodeByKeyValue.set(keyValue, node);
            }
            keyValues[i] = keyValue;
          }
        }
        for (i = -1; ++i < m; ) {
          if (!(node = nodeByKeyValue.get(keyValue = key.call(groupData, nodeData = groupData[i], i)))) {
            enterNodes[i] = d3_selection_dataNode(nodeData);
          } else if (node !== true) {
            updateNodes[i] = node;
            node.__data__ = nodeData;
          }
          nodeByKeyValue.set(keyValue, true);
        }
        for (i = -1; ++i < n; ) {
          if (i in keyValues && nodeByKeyValue.get(keyValues[i]) !== true) {
            exitNodes[i] = group[i];
          }
        }
      } else {
        for (i = -1; ++i < n0; ) {
          node = group[i];
          nodeData = groupData[i];
          if (node) {
            node.__data__ = nodeData;
            updateNodes[i] = node;
          } else {
            enterNodes[i] = d3_selection_dataNode(nodeData);
          }
        }
        for (;i < m; ++i) {
          enterNodes[i] = d3_selection_dataNode(groupData[i]);
        }
        for (;i < n; ++i) {
          exitNodes[i] = group[i];
        }
      }
      enterNodes.update = updateNodes;
      enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode;
      enter.push(enterNodes);
      update.push(updateNodes);
      exit.push(exitNodes);
    }
    var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]);
    if (typeof value === "function") {
      while (++i < n) {
        bind(group = this[i], value.call(group, group.parentNode.__data__, i));
      }
    } else {
      while (++i < n) {
        bind(group = this[i], value);
      }
    }
    update.enter = function() {
      return enter;
    };
    update.exit = function() {
      return exit;
    };
    return update;
  };

数据绑定函数data最终返回了变量update,这个变量update一开始为一个空集合,它拥有d3的集合操作方法,然后data函数通过调用bind函数对传入的参数进行逐项绑定,获得update集合作为本身,以及enter集合和exit集合,最后在update上绑定了函数enter和exit,使得用户在调用data后,可以再次调用enter和exit去获取另外两个集合。

关于后期debug的足迹

d3也会有bug的时候,这个时候需要对bug进行修复,然后再更新,为了方便下次找到修改的bug,在代码里面对其进行命名,是很好的做法:

【1167】

var d3_mouse_bug44083 = this.navigator && /WebKit/.test(this.navigator.userAgent) ? -1 : 0;

D3的颜色空间

D3支持五种颜色表示方式,除了我们常常接触了rgb、hsl外,还有lab、hcl、cubehelix,它们之间都可以转化为rgb,内部的实现方式值得参考:

【line 1582】

  function d3_hsl_rgb(h, s, l) {
    var m1, m2;
    h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h;
    s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s;
    l = l < 0 ? 0 : l > 1 ? 1 : l;
    m2 = l <= .5 ? l * (1 + s) : l + s - l * s;
    m1 = 2 * l - m2;
    function v(h) {
      if (h > 360) h -= 360; else if (h < 0) h += 360;
      if (h < 60) return m1 + (m2 - m1) * h / 60;
      if (h < 180) return m2;
      if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60;
      return m1;
    }
    function vv(h) {
      return Math.round(v(h) * 255);
    }
    return new d3_rgb(vv(h + 120), vv(h), vv(h - 120));
  }

关于csv、dsv、tsv存储方式

看代码的好处之一是能看到很多平时不会用到的接口,然后会主动去了解是干什么的。

csv格式

在文本数据处理和传输过程中,我们常常遇到把多个字段通过分隔符连接在一起的需求,如采用著名的CSV格式(comma-separated values)。CSV文件的每一行是一条记录(record),每一行的各个字段通过逗号','分隔。

dsv格式

由于逗号和双引号这两个特殊字符的存在,我们不能简单地通过字符串的split操作对CSV文件进行解析,而必须进行CSV语法分析。虽然我们可以通过库的形式进行封装,或者直接采用现成的库,但毕竟各种平台下库的丰富程度差异很大,这些库和split、join这样的简单字符串操作相比也更加复杂。为此,我们在CSV格式的基础上设计了一种DSV (double separated values)格式。DSV格式的主要设计目的就是为了简化CSV语法,生成和解析只需要replace, join, split这3个基本的字符串操作,而不需要进行语法分析。

DSV的语法非常简单,只包括以下两点:

  • 通过双竖线'||'作为字段分隔符

  • 把字段值中的'|'替换为'_|'进行转义

tsv格式

TSV 是Tab-separated values的缩写,即制表符分隔值。

查询网上关于这三种格式的定义是如上所示,不过d3的实现不太一样,dsv是可以定义为任何一种分隔符,但是分隔符只能为长度为1的字符,csv是以半角符逗号作为分割符,tsv则是以斜杠作为分隔符。

d3.geo

【line 2854】

geo是d3的图形处理实现,应该算是核心代码了,不过到了4.0版本被分割成依赖,并且不再有d3.geo.path了,而是改用d3.geoPath的方式去引用。

总结

版本3的d3九千多行代码,版本4的d4则进行了依赖分割,如果全部依赖引入的话不压缩就要过16000行了,如果想整体去看骨架的话,版本3是比较清晰的,版本4则适合深入研究每一部分的实现,因为依赖都分割得很清晰了,并且相互独立开。

初步了解整个d3的骨架后,接下来可以深入到代码函数实现中去研究其中奥妙。

查看原文

赞 4 收藏 27 评论 0

cyqian 发布了文章 · 2016-05-09

中文维基百科文本数据获取与预处理

照例,先讲下环境,Mac OSX 10.11.2 ,Python 3.4.3。

下载数据

方法1:使用官方dump的xml数据

最新打包的中文文档下载地址是:https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2

方法2:也是官方,结构化数据(json)

下载地址是:https://dumps.wikimedia.org/wikidatawiki/entities/ 。目前尚未测试使用此数据,不多作介绍。但数据模型和已有的工具都可以在wikidata的站点上找到。

解压与转存

我使用方法1,下载后需要对该xml文件的压缩包作处理,所幸gensim的WikiCorpus已经预置了部分处理。几行关键的python代码如下:

input_file = "zhwiki-latest-pages-articles.xml.bz2"
wiki = WikiCorpus(input_file, lemmatize=False, dictionary={})
    for text in wiki.get_texts():
        str_line = bytes.join(b' ', text).decode()
        #以下可以存入文件或数据库

更详细的关于WikiCorpus的介绍可以看这里。

在上面的代码中,补下自己的漏,python3里,str和bytes是两个不同的东西,有点类似python2中的str和unicode。下面是str和bytes的相互转换方法:

# str转bytes
data = ""  #string
data = "".encode()  #bytes
data = b""  #bytes

# bytes转str
data = b""  #bytes
data = b"".decode()  #string
data = str(b"")  #string

除了用gensim,还有个哥们写了一个wikiextractor工具来处理wiki的dump数据,若感兴趣可拿来参考,详见github地址。

根据我的数据,1.17G的原始数据处理所得的文本文件845M,246497篇文章(这个数字随时间往后是越来越大)。

繁简转换

这是个糟糕的话题,占这么大篇幅真得感叹中华崛起之重要。中文维基数据繁简混杂——大家都说存在这个问题,但wikipedia的网站是将繁体中文和简体中文分开处理的,所以一直觉得从数据库到dump结构都应有方法将两者区分开,暂罢,待有空研究其数据时再议。关于繁简转换,来斯惟和52nlp的博文都用到了一个繁简转换工具——OpenCC,关于此,引官方介绍如下:

Open Chinese Convert(OpenCC)是一個中文簡繁轉換開源項目,提供高質量的簡繁轉換詞庫和可供調用的函數庫(libopencc)。還提供命令行簡繁轉換工具,人工校對工具,詞典生成程序,以及圖形用戶界面。

这里使用的是命令行工具。至于安装方法,可以看Google Code上的项目页面。如在Mac下,直接:

brew install opencc

将繁体转为简体的命令如下:

opencc -i wiki_zh.text -o wiki_zhs.text -c zht2zhs_config.json
那个json是什么鬼?OpenCC的配置文件,现在已支持json写法,如下:

{
  "name": "Traditional Chinese to Simplified Chinese",
  "segmentation": {
    "type": "mmseg",
    "dict": {
      "type": "ocd",
      "file": "TSPhrases.ocd"
    }
  },
  "conversion_chain": [{
    "dict": {
      "type": "group",
      "dicts": [{
        "type": "ocd",
        "file": "TSPhrases.ocd"
      }, {
        "type": "ocd",
        "file": "TSCharacters.ocd"
      }]
    }
  }]
}

中文分词

OK,这是个大话题,有很多选择,网上有不少推荐结巴分词,其实是挺不错的。但这里使用哈工大的LTP,github地址,篇幅原因暂时不详细介绍这个了,只讲我认为的三点:

  1. 计算语言学的基本任务,解释地比较透彻,无论是代码还是文档。

  2. 配套论文产量和质量都不错。

  3. 良心的python封装。

尽管,国内大学中不乏类似工作,清华、复旦等也做了不少。

LTP目前我使用release里的3.3.2,模型文件使用3.3.1,python封装使用0.1.9。由于是Mac下使用源文件编译,所以由于OSX编译器类型变迁,pyltp 0.1.9的setup.py中一定要记得修改这行:

extra_compile_args += ['-std=c++11', '-Wno-c++11-narrowing',"-mmacosx-version-min=10.8",'-stdlib=libc++']

其重点是-mmacosx-version-min这个参数。当然,根据issue记录,25天前这个问题已修复且合并进mater分支。所以据拍脑袋估计,git源码安装也是可行:

$ git clone https://github.com/HIT-SCIR/pyltp
$ git submodule init
$ git submodule update
$ python setup.py install

具体分词的写法就比较简单了,例子如下:

# -*- coding: utf-8 -*-
from pyltp import Segmentor
segmentor = Segmentor()
segmentor.load("/path/to/your/cws/model")
words = segmentor.segment("这句句子要分词")
print "|".join(words)
segmentor.release()

找一篇语料对比下分词前后,分词前:

巨蟹座 是一颗环绕巨蟹座 a运转的系外行星 轨道周期为 地球日 它是距离其中央恒星第三近的行星 其质量接近于土星 该行星于 日被发现 发现
和大多数系外行星一样 而之前 巨蟹座 该恒星仍然会出现视向速度位移 进一步的探测发现在距中央恒星 但是即使摒除了这两颗行星的影响
中央恒星仍然存在周期为 地球日的扰动现象 由于该周期接近于巨蟹座 a的自转周期 尽管如此 在同一份报告中 科学家宣布发现了巨蟹座 d和巨蟹座
对其中央恒星进行的长达 而且视向速度位移的幅度较大 无法为巨蟹座 a不大活跃的光球层活动所解释 轨道和质量 在巨蟹座 行星系统中
迄今为止已经发现了 颗行星 颗行星中 巨蟹座 c的轨道属于轻度偏心轨道 其远拱点较之近拱点远了 该行星的轨道周期要长于热木星
但是其轨道与巨蟹座 模拟表明该行星与巨蟹座 的比值 由于视向速度法的局限性 如果此预测无误 那么该行星的真实质量就为 倍木星质量 物理特性
由于科学家只能间接地探测该行星 所以至今还不知道其半径 物质构成和表面温度 该行星质量接近土星 所以它可能属于类木行星 从而并不拥有固体表面
参考文献 外部链接 extrasolar visions cancri

分词后:

巨蟹座|是|一|颗|环绕|巨蟹座|a|运转|的|系|外行星|轨道|周期|为|地球日|它|是|距离|其|中央|恒星|第三|近|的|行星|其|质量|接近|于|土星|该|行星|于|日|被|发现|发现|和|大多数|系外|行星|一样|而|之前|巨蟹座|该|恒星|仍然|会|出现|视|向|速度|位移|进一步|的|探测|发现|在|距|中央|恒星|但是|即使|摒除|了|这|两|颗|行星|的|影响|中央|恒星|仍然|存在|周期|为|地球日|的|扰动|现象|由于|该|周期|接近|于|巨蟹座|a|的|自转|周期|尽管|如此|在|同一|份|报告|中|科学家|宣布|发现|了|巨蟹座|d|和|巨蟹座|对|其|中央|恒星|进行|的|长|达|而且|视|向|速度|位移|的|幅度|较|大|无法|为|巨蟹座|a|不|大|活跃|的|光球层|活动|所|解释|轨道|和|质量|在|巨蟹座|行星|系统|中|迄今为止|已经|发现|了|颗|行星|颗|行星|中|巨蟹座|c|的|轨道|属于|轻度|偏心|轨道|其|远|拱点|较之|近|拱点|远|了|该|行星|的|轨道|周期|要|长|于|热|木星|但是|其|轨道|与|巨蟹座|模拟|表明|该行星|与|巨蟹座|的|比值|由于|视|向|速度|法|的|局限性|如果|此|预测|无误|那么|该行星|的|真实|质量|就|为|倍|木星|质量|物理|特性|由于|科学家|只能|间接|地|探测|该行|星所|以至今|还|不|知道|其|半径|物质|构成|和|表面|温度|该行|星|质量|接近|土星|所以|它|可能|属于|类|木行星|从而|并|不|拥有|固体|表面|参考|文献|外部|链接|extrasolar|visions|cancri

小结

这篇主要基于网络上的资料,重走了一遍数据下载到中文分词的技术点,并汇总了已知的工具链。上述结果对比维基的原网页,明显还存在不少问题,例如语料中的数字均丢失了,对于其中数量、年份等信息对于文本理解其实很重要。尚不确定是否是WikiCorpus造成的问题。

下一篇计划尝试用此语料做词嵌入相关的部分实验。

To be continued.

原文链接:http://qiancy.com/2016/05/08/wiki-text-analysis-prepare/

查看原文

赞 3 收藏 9 评论 0

cyqian 发布了文章 · 2016-04-07

PostgreSQL两三事

不要用Graphic Installer

至少在Ubuntu下,觉得原生的apt-get管理方式更合适,PG的文件资源会被分配到应该的地方,Linux的系统文件结构也是种非常稳健的架构。例如在/etc/postgresql下可以找到conf文件是一件清晰到爽的事情。正常通过apt安装的方式参见这篇博文

之前有一台VM就是没有忍住一时之快,用了Graphic Installer,所有的东西被塞到/opt/PostgreSQL/x.x中,当然,conf、bin等文件都在其中。在没有手动配置的情况下,start/stop/restart/reload之类的事情,不得不交给pg_ctl做,而与postgres账户之间的来回切换,也会浪费宝贵的时间。例如重启PG需要

./pg_ctl restart -D ../data

而出现冲突的时候还会需要忍不住 -m fast 一下。而如果使用系统服务,至少可以这样:

sudo /etc/init.d/postgresql restart

修改postgresql.conf

数据库系统参数是很重要的功能,根据应用特点进行性能调优时,往往需要用到。目前用到的几个参数里,shared_buffers一般会尽量设置大一些,有人建议设为RAM的10%,其实我觉得更大一些也没什么问题。tcp_keepalives_idle作为秒数表示空闲时间间隔,当一个tcp连接持续该时间闲置,db会发送tcp_keeplive包给客户端,若连续tcp_keepalives_count个包都在tcp_keepalives_interval秒内没有回应,则会认为这个tcp已死。

修改postgresql.conf后可以通过 select pg_reload_conf();重新加载配置。但是配置里有些是支持动态的,而有些必须要重启db,例如shared_buffers就如此,悲催。重加载或重启后,可通过show <配置项>命令查看当前已生效的配置项值,例如:

show tcp_keepalives_idle;

查看当前的服务器状态

可调用PG自带的一个视图:

select * from pg_stat_activity

这个视图可以查出目前的连接,以及各自连接的状态、时间点、SQL内容等,视图内容:

CREATE OR REPLACE VIEW pg_stat_activity AS 
SELECT s.datid, d.datname, s.procpid, s.usesysid, 
u.rolname AS usename, s.application_name, s.client_addr, 
s.client_hostname, s.client_port, s.backend_start, 
s.xact_start, s.query_start, s.waiting, s.current_query 
FROM pg_database d, pg_stat_get_activity(NULL::integer) 
s(datid, procpid, usesysid, application_name, current_query, 
waiting, xact_start, query_start, backend_start, client_addr, 
client_hostname, client_port), pg_authid u 
WHERE s.datid = d.oid AND s.usesysid = u.oid;

当然,只要不是很老的PG版本,可以用pg_terminate_backend来终止相应的会话,这篇文章例举了更多的使用场景。例如:

SELECT pg_terminate_backend(procpid)
FROM pg_stat_activity
WHERE datname = 'databasename'

终止某个用户的会话:

SELECT pg_terminate_backend(procpid)
FROM pg_stat_activity
WHERE usename = 'username'

如果觉得terminate太暴力,还可以使用pg_cancel_backend。

查看原文

赞 2 收藏 2 评论 0

cyqian 发布了文章 · 2016-03-01

项目迁移-从Eclipse到Android Studio

最近做的比较多的事情就是把公司的eclipse项目转入Android Studio中。很多问题的解决其实凭直觉瞎捣鼓即可,但是总还是能形成一些操作习惯和步骤,记录下来。

1、在AS中新建空项目。具体做法看个人喜好,但我比较偷懒,喜欢一开始就有完整的结构,所以在新建选项中选择自带Empty Activity,因此AndroidManifest.xml也会被一并创建。

2、文件拷贝。尽管Android Studio中已有工具号称协助从Eclipse中Import工程并尝试自动转换为AS项目,但实践证明它并没有足够智能地处理冲突,并且挺容易把原本清晰的文件结构和配置弄的混乱。因此我的做法是,在Android Studio中新建项目B,并从Eclipse项目A文件夹中将文件逐步拷贝过来。具体的,src、res、assets等目录可以手动逐个拷贝,其中res和assets两个目录在AS中应位于src/main中,这点应注意。

3、项目设置。Eclipse项目中的AndroidManifest.xml文件内容可以先全部拷贝,然后将部分内容挪至AS项目的build.gradle文件,主要是compileSdkVersion,buildToolsVersion,minSdkVersion,targetSdkVersion,versionCode,versionName这几个。

4、依赖处理。通常有jar包、aar包、library依赖等。当然,首先应先检查依赖包是否可从线上各种maven库中取得,这种幸福的方式不应错过,直接以compile语句形式添加到app的build.gradle中即可。本地文件例如jar或aar,可以直接放入本地文件夹,例如libs。

jar包的引入方式:

 compile fileTree(include: ['*.jar'], dir: 'libs')

如果是本地aar文件,则需在module层的build.gradle中加入:

repositories {
    ...
    flatDir {
        dirs 'libs'
    }
}

然后类似:

compile(name: 'arcgis-android-v10.2.7', ext: 'aar')
如果是library形式,则在module的settings里边新建模块,导入相应工程,并在自己的app的dependengcies里通过添加Library denpendency的方式将其加入。

5、处理文件重复。例如:Duplicate files copied in APK META-INF/LICENSE,在Module:app的build.grade文件中的android节点中增加packagingOptions选项,并用exclude关键字添加相应文件的路径,例如:

packagingOptions {
    exclude 'META-INF/LGPL2.1'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE.txt'
    exclude 'META-INF/NOTICE.txt'
}

6、不要忘了删除此前新建AS项目时的java文件,例如EmptyActivity.java之类,以及其资源和Manifest配置。

查看原文

赞 0 收藏 7 评论 0

cyqian 赞了文章 · 2016-02-14

一份代码构建移动、桌面、Web全平台应用

Web本身就是跨平台的,这意味着这中间存在着无限的可能性。

我是一名Web Developer,对于我来能用Web开发的事情就用Web来完成就好了——不需要编译,不需要等它编译完。我想到哪我就可以写到哪,我改到哪我就可以发生哪发生了变化。

最近我在写Growth——一个帮助开发人员成长的应用,在近一个月的业余时间里,完成了这个应用的:

  • 移动应用版:Android、Windows Phone、iOS(等账号和上线)

  • Web版

  • 桌面版:Mac OS、Windows、GNU/Linux

截图合并如下:

growth-full-platforms.png

而更重要的是它们使用了同一份代码——除了对特定设备进行一些处理就没有其他修改。相信全栈的你已经看出来了:

Web = Chrome + Angular.js + Ionic

Desktop = Electron + Angular.js + Ionic

Mobile = Cordova + Angular.js + Ionic

除了前面的WebView不一样,后面都是Angular.js + Ionic。

从Web到混合应用,再到桌面应用

在最打开的时候它只是一个单纯的混合应用,我想总结一下我的学习经验,分享一下学习的心得,如:

  • 完整的Web开发,运维,部署,维护介绍

  • 如何写好代码——重构、测试、模式

  • 遗留代码、遗留系统的形成

  • 不同阶段所需的技能

  • 书籍推荐

  • 技术栈推荐

  • Web应用解决方案

接着我用Ionic创建了这个应用,这是一个再普通不过的过程。在这个过程里,我一直使用Chrome在调度我的代码。因为我是Android用户,我有Google Play的账号,便发布了Android版本。这时候遇到了一个问题,我并没有Apple Developer账号(现在在申请ing。。),而主要的用户对象程序员,这是一群不土的土豪。

iPHONE

偶然间我才想到,我只要上传Web版本的代码就可以暂时性实现这个需求了。接着找了个AWS S3的插件,直接上传到了AWS S3上托管成静态文件服务。

几天前在Github上收到一个issue——关于创造桌面版, 我便想着这也是可能的,我只需要写一个启动脚本和编译脚本即可。

所以,最后我们的流程图就如下所示:

Growth Arch

除了显示到VR设备上,好像什么也不缺了。并且在我之前的文章《Oculus + Node.js + Three.js 打造VR世界》,也展示了Web在VR世界的可能性。

在这实现期间有几个点可以分享一下:

  1. 响应式设计

  2. 平台/设备特定代码

响应式设计

响应式设计可以主要依赖于Media Query,而响应式设计主要要追随的一点是不同的设备不同的显示,如:

full-platforms.jpg

这也意味着,我们需要对不同的设备进行一些处理,如在大的屏幕下,我们需要展示菜单:

gnu-linux.png

而这可以依赖于Ionic的expose-aside-when="large",而并非所有的情形都是这么简单的。如我最近遇到的问题就是图片缩放的问题,之前的图片针对的都是手机版——经过了一定的缩放。

这时在桌面应用上就会出现问题,就需要限定大小等等。

而这个问题相比于平台特定问题则更容易解决。

平台特定代码

对于特定平台才有的问题就不是一件容易解决的事,分享一下:

存储

我遇到的第一个问题是数据存储的问题。最开始的时候,我只需要开始混合应用。因此我可以用Preferences、或者SQLite来存储数据。

后来,我扩展到了Web版,我只好用LocalStoarge。于是,我就开始抽象出一个$storageServices来做相应的事。接着遇到一系列的问题,我舍弃了原有的方案,直接使用LocalStoarge。

数据分析

为了开发方便,我使用Google Analytics来分析用户的行为——毕竟数据对我来说也不是特别重要,只要可以看到有人使用就可以了。

这时候遇到的一个问题是,我不需要记录Web用户的行为,但是我希望可以看到有这样的请求发出。于是对于Web用户来说,只需要:

        trackView: function (view) {
          console.log(view);
        }

而对于手机用户则是:

      trackView: function (view) {
        $window.analytics.startTrackerWithId('UA-71907748-1');
        $window.analytics.trackView(view)
      }

这样在我调试的时候我只需要打个Log,在产品环境时就会Track。

更新

同样的,对于Android用户来说,他们可以选择自行下载更新,所以我需要针对Android用户有一个自动更新:

var isAndroid = ionic.Platform.isAndroid();
if(isAndroid) {
  $updateServices.check('main');
}

桌面应用

对于桌面应用来说也会有类似的问题,我遇到的第一个问题是Electron默认开启了AMD。于是,直接删之:

<script>
//remove module for electron
if(typeof module !== 'undefined' && module && module.exports){
  delete module;
}
</script>

类似的问题还有许多,不过由于应用内容的限制,这些问题就没有那么严重了。

如果有一天,我有钱开放这个应用的应用号,那么我就会再次献上这个图:

六边形架构

未来

我就开始思索这个问题,未来的趋势是合并到一起,而这一个趋势在现在就已经是完成时了。

那么未来呢?你觉得会是怎样的?

项目源码: https://github.com/phodal/growth

更多内容请关注我的微信公众号:phodal

图片描述

查看原文

赞 6 收藏 50 评论 5

cyqian 赞了文章 · 2016-01-26

事件总线源码分析

基本概念

在安卓中处理不同组件之间的事件传递依靠广播机制,即Intent/BroadcastReceiver机制,其原理类似于传感网中的Ad hoc网络模式,所有组件处在一种无序状态;

事件总线机制则引入中心控制节点来集中管理事件,类似于移动通信网络中的基站功能。

总线这个概念来自于计算机,计算机中各种功能部件如CPU,显卡之类不会采用两两互联的方式,那样布线会非常复杂,实际是使用总线作为公共通信干线,挂载上所有的功能部件。

事件总线框架采用订阅/发布模型
事件总线的模型

在这个模型中包含以下元素

1.事件类,这是要传递的事件对象。
2.事件总线,是中心控制节点,负责管理事件的注册,接收发布的事件,并完成事件匹配以进行事件处理。
3.发布者,负责分发事件。
4.订阅者,负责事件处理并注册到事件总线。

事件总线模型比广播机制的优势在于

1.简化了组件之间的通信
2.实现了事件分发和事件处理的解耦,因此二者可以在不同线程中实现。
3.扩展了事件类,实际上事件类就是根类Object,而不必藏身在Intent中了。

事件总线模型不能完全替代广播机制,因为广播机制可以完成跨App间组件调用。

EventBus

EventBus采用发布/订阅模式,想要接收事件必须先订阅总线,这与手机要注册基站类似。EventBus的作用在于优化Activities, Fragments等之间的通信,并处理能够线程问题。

EventBus的使用非常简单,包括以下步骤

1.总线的建立。

EventBus eventBus = EventBus.getDefault();

毫无疑问,事件总线对象应该是单例实现,如果要对其进行初始化配置最好放在Applicaiton类中进行。
2.创建事件,事件就是普通的Java类,如

public class MessageEvent {
    public final String message;

    public MessageEvent(String message) {
        this.message = message;
    }
}

3.创建订阅方法。订阅者实际上是处理事件的方法,以onEvent来命名。

public void onEvent(MessageEvent event){
    Toast.makeText(this, event.message, Toast.LENGTH_SHORT).show();
}

4.订阅者注册和取消。将包含订阅方法的类注册到总线。

protected void onCreate(Bundle savedInstanceState) {
    //。。。
    EventBus.getDefault().register(this,1);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().register(this);
}

5.事件发布。

EventBus.getDefault().post(new MessageEvent("MessageEvent"));

线程切换

关于线程切换要注意在事件分发是处在当前线程中的,是比较容易控制的。EventBus主要简化的是事件处理所处的线程。共有四种线程切换方法,其区别在于事件处理方法的命名。

  • onEvent方法,为默认PostThread模式。处理线程就是分发线程。

  • onEventMainThread方法,主线程模式。处理线程最终为UI线程,与分发线程无关。

  • onEventBackgroundThread方法,背景线程模式。处理线程最终为背景线程。这意味着如果分发线程是UI线程,则将新建一背景线程作处理线程;如果分发线程不是UI线程,则分发线程就用作处理线程。

  • onEventAsync方法,异步模式。不管分发线程如何,处理线程都将新建一线程,底层采用线程池技术实现。

Otto

OttoSquare推出的事件总线框架,基于Guava框架。Guava框架查找订阅方法采用遍历类方法,Otto则是使用注解来查找,虽然EventBus在初始版本中也采用注解订阅方法,但因为性能问题改为按照方法名查找。

Otto的使用与EventBus类似,最大区别在于是否对订阅方法使用注解。

Bus bus = new Bus(ThreadEnforcer.MAIN);

@Subscribe 
public void answerAvailable(AnswerAvailableEvent event){
    // TODO: React to the event somehow!
}

bus.register(this);

bus.post(new AnswerAvailableEvent(42));

两种框架的对比

可以看到在功能和性能上EventBus都完胜Otto。

EventBus源码解析

首先要理解几个概念,先看看订阅方法

void onEvent(MessageEvent event)

订阅方法中包含一个具体事件类作参数,并通过重载实现不同的订阅方法。

  • SubscriberMethod类表示单个订阅方法,主要域包括

Method method;//订阅方法的反射表示,实现ding'y方法的
ThreadMode threadMode;//处理事件所用线程模型
Class<?> eventType;//具体事件类类型
  • Subscription类也表示订阅方法,是SubscriberMethod类的进一步封装,主要域包括

Object subscriber; //具体事件类对象
SubscriberMethod subscriberMethod;
int priority; //订阅方法优先级

总的说来,事件总线的原理是在总线中维持两个集合:一个表示订阅方法集合,一个表示事件类型集合。
注册订阅方法分为两步:首先查找所有出所有订阅方法;其次将订阅方法及其事件类型分别加入总线的两个集合中。
事件分发时,需要根据事件对象提取出事件类型,而后构建订阅方法,在总线两个集合中分别查找是否存在该事件;如果存在就按照线程模型分别执行。

  • PostingThreadState

List<Object> eventQueue = new ArrayList<Object>();
boolean isPosting;
boolean isMainThread;
Subscription subscription;//订阅类
Object event;              //事件类型
boolean canceled;

1.EventBus创建使用getDefault()方法采用单例模式,也可以通过buidler指定,需要同步创建。

2.订阅方法注册

EventBus.getDefault().register(this);

总的说来方法订阅包括两步:

这里注意形式上注册到总线的是MainActivity对象,并不是具体订阅方法,所以存在一个查找出活动对象中所有订阅方法的过程。

同时总线中要维持两个集合:订阅类集合事件类型集合。新的订阅方法要添加到这两个集合中。

private synchronized void register(Object subscriber, boolean sticky, int priority) {
    //1.首先对活动类查找出其包含的订阅方法(SubscriberMethod)集合
    List<SubscriberMethod> subscriberMethods = findSubscriberMethods(subscriber.getClass());
    //2.而后将对每一个订阅方法(SubscriberMethod)注册
    for (SubscriberMethod subscriberMethod : subscriberMethods) {
        subscribe(subscriber, subscriberMethod, sticky, priority);
    }
}

注册订阅方法(SubscriberMethod)简化版如下,不考虑striky情况,完成将一个订阅方法A插入总线集合中:

//Object subscriber 如 MainActivity
//SubscriberMethod  订阅方法,如onEvent
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod, boolean sticky, int priority) {
    //获取订阅方法A中的事件类型A.eventType,如MessageEvent
    Class<?> eventType = subscriberMethod.eventType;
    //根据事件类型A.eventType获取总线中已经存在的订阅方法(Subscription)集合S
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    //根据订阅方法A创建其对应的封装订阅方法B
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod, priority);
    //在订阅方法集合S中查找封装订阅方法B
    //首先要处理S=NULL的情况,此时要创建S,并将B插入S。
    //如果S已经包含B,抛出异常,即不能重复注册同一个订阅方法。
    if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<Subscription>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }
    //如果S不为空且B不在S中,要将B按照优先级顺序插入到S
    int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            if (i == size || newSubscription.priority > subscriptions.get(i).priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }
    //总线中维护一个事件类型集合,还需要将新事件类型A.eventType加入该集合
    List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
    if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<Class<?>>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);
    }

3.事件分发方法简化版为

public void post(Object event) {
    //分发事件时将具体事件入队
    PostingThreadState postingState = currentPostingThreadState.get();
    List<Object> eventQueue = postingState.eventQueue;
    eventQueue.add(event);
    //处理队列中优先级最高的事件
    while (!eventQueue.isEmpty()) {
        postSingleEvent(eventQueue.remove(0), postingState);
    }
}
//Object event 事件类对象
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    //获取具体事件类对象event对应的事件类型E,如MessageEvent
    Class<?> eventClass = event.getClass();
    //在事件总线集合中查找是否存在具体事件类型E,如果不存在,则分发NoSubscriberEvent事件;如果存在,继续分发。
    boolean subscriptionFound = false;
    subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
}
private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
    //根据事件类型同步获取总线中的封装订阅方法集合S,这里要注意某个具体事件类型可能有多个线程版本
    CopyOnWriteArrayList<Subscription> subscriptions;
    //遍历S中订阅方法,进行事件处理
    if (subscriptions != null && !subscriptions.isEmpty()) {
        for (Subscription subscription : subscriptions) {
            postToSubscription(subscription, event, postingState.isMainThread);
        }
        return true;
    }
    return false;
}

根据ThreadMode类型处理事件

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case PostThread:
            invokeSubscriber(subscription, event);
            break;
        case MainThread:
            if (isMainThread) {
                invokeSubscriber(subscription, event);
            } else {
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case BackgroundThread:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case Async:
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}

4.几种线程有关的事件处理方法
PostThread方式采用反射完成事件处理。

void invokeSubscriber(Subscription subscription, Object event) {
    subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
}

MainThread模式在异步情况下采用Handler完成事件处理,具体类为HandlerPoster类,这个类采用Looper.getMainLooper()构造以保证事件处理执行在主线程中。

mainThreadPoster.enqueue(subscription, event);
void enqueue(Subscription subscription, Object event) {
    //构造一个PendingPost类并将其入队
    PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
    queue.enqueue(pendingPost);
}

BackgroundThreadAsync模式采用线程池来执行,

eventBus.getExecutorService() = Executors.newCachedThreadPool();
public void enqueue(Subscription subscription, Object event) {
    PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
    queue.enqueue(pendingPost);
    eventBus.getExecutorService().execute(this);
}

参考文献

greenrobot-EventBus-HOWTO.md
EventBus for Android™
Otto
EventBus Comparison with Square's Otto
eventbus-for-android

查看原文

赞 3 收藏 7 评论 0

cyqian 发布了文章 · 2015-07-11

如何给nexus 5/6 刷上Android M preview 2

官方时间表

尽管不久的七月底应该就出第三个preview了,还是忍不住当了回小白,把nexus6刷成android M了,没有尝试nexus5,但就网上资料看,情况大致一样。首先看下google官方的timeline吧,传说中的M第三季度即将正式到来。
clipboard.png

下载image文件

目前为止preview2的下载位置是这里。选择合适的版本,比如nexus6是shamu。
clipboard.png

解压下载包后,里边的东西是这样的:

clipboard.png

bootloader和radio的文件名跟设备和版本有关。image-shamu-**.zip压缩包里是这些东西:

clipboard.png

刷入步骤

android developer网站上有详尽的指南,原本按照这些步骤,应该就衣食无忧了。但实际上fastboot工具没法直接找到image-shamu-MPZ79M下的几个img文件,很明显flash-all脚本写的不够人性化。网上有修改该脚本的讨论,没有仔细去研究,因为手动操作几下总体上比较省时间。参考网上的这篇文章后,大致的操作步骤变更为如下:

  1. 将android-sdk下的platform-tools文件夹添加到操作系统的PATH变量里(例如osx下可写入~/.bash_profile文件),主要是要让flash-all脚本能用到。
  2. 进入开发者模式,确保usb调试状态打开;确保OEM可解锁。
  3. 进入fastboot模式,命令行:adb reboot bootloader。此时小机器人画面会出现。
  4. OEM解锁(以后可以在lock回去):fastboot oem unlock。此处手机上需要确认YES or NO,按提示操作。
  5. 命令行执行flash-all.sh(osx下./flash-all.sh),按照官方指南,此处应已万事大吉。可实际情况的确会出现一堆ok,直到出现某个image缺失,那就是没有找到image-shamu-MPZ79M中的内容了,于是逐个加载,注意是有顺序的。所有都执行完后,fastboot reboot一下。逐个加载过程如下:
fastboot flash bootloader bootloader-***.img
fastboot flash radio radio-***.img
fastboot reboot-bootloader
fastboot flash recovery recovery.img
fastboot flash boot boot.img
fastboot flash system system.img

其间,只有system.img的导入会稍微耗时一些:

$ fastboot flash system system.img
target reported max download size of 536870912 bytes
sending sparse 'system' (517175 KB)...
OKAY [ 19.143s]
writing 'system'...
OKAY [  6.862s]
sending sparse 'system' (523942 KB)...
OKAY [ 20.071s]
writing 'system'...
OKAY [  7.175s]
sending sparse 'system' (506839 KB)...
OKAY [ 21.062s]
writing 'system'...
OKAY [  6.758s]
sending sparse 'system' (434117 KB)...
OKAY [ 17.244s]
writing 'system'...
OKAY [  5.779s]
finished. total time: 104.093s
$ fastboot reboot
rebooting...

ok,就是这样了。

查看原文

赞 0 收藏 1 评论 0

cyqian 发布了文章 · 2015-06-07

空间权重矩阵(SWM)

基本原理

如何利用数学(如用面积、距离等)建立空间数据和非空间数据之间的关系?对于空间统计而言,空间权重矩阵是一种有效的表达空间关系的方式。因此,它是用量化的方法表示了数据之间的“空间结构”。

关于如何进行要素间实际交互方式的概念化,ESRI举出的例子:

如果要测量森林中某种特定种类的种子繁殖树种的聚类,使用某种形式的反距离可能最适合。但是,如果要评估某一地区通勤者的地理分布,行程时间和行程成本可能是更好的选择。

空间权重矩阵是N*N的表,N为数据集中的要素数量。因此给定行列组合后,其对应的值即为权重。这种空间关系的权重矩阵在许多空间统计工具里都有用,比如空间自相关、热点分析、聚类和异常值分析等。

创建策略

  1. 二进制策略:某个要素要么是邻域-1,要么不是-0。例如:固定距离、K 最近邻域、Delaunay 三角测量、邻接或空间 - 时间窗口。
  2. 权重策略:邻近要素有不同量级的影响,并通过计算权重来反映该变化。例如:反距离或无差别的区域。

工具使用

arcgis中的SWM使用稀疏矩阵存储,因此只存非零部分。实际情况,每个要素一般只跟其他某几个要素发生关系。

工具名字:Generate Spatial Weights Matrix (Spatial Statistics),工具界面如图:

clipboard.png

SWM的使用:以热点分析为例

具体权重的使用,以热点分析为例,几个主要的步骤如下:

integrate与collect event

integrate = arcpy.Integrate_management("911Copied.shp #", "500 Feet")
ce = arcpy.CollectEvents_stats("911Copied.shp", "911Count.shp", "Count", "#")

collect event的结果数据

重点是需要生成feature class,并且确保这个feature class有一个唯一id字段。

af = arcpy.AddField_management("911Count.shp", "MyID", "LONG", "#", "#", "#", "#","NON_NULLABLE", "NON_REQUIRED", "#", "911Count.shp")
cf = arcpy.CalculateField_management("911Count.shp", "MyID", "[FID]", "VB")

Generate Spatial Weights Matrix

swm = arcpy.GenerateSpatialWeightsMatrix_stats("911Count.shp", "MYID","euclidean6Neighs.swm","K_NEAREST_NEIGHBORS","#", "#", "#", 6,"NO_STANDARDIZATION") 

Hot Spot Analysis (Getis-Ord Gi*)

hs = arcpy.HotSpots_stats("911Count.shp", "ICOUNT", "911HotSpots.shp","GET_SPATIAL_WEIGHTS_FROM_FILE","EUCLIDEAN_DISTANCE", "NONE","#", "#", "euclidean6Neighs.swm","NO_FDR")
查看原文

赞 1 收藏 4 评论 0

cyqian 发布了文章 · 2015-06-01

热点分析的原理

重读了下esri的文档,觉得其实讲的挺清晰的。下面是参考官方文档后的理解。

z得分和p值

热点分析可以得到比较重要的两个东西,z得分和p值。这两个值用来帮助判断是否可以拒绝零假设。对于热点分析而言,零假设是要素的完全空间随机性。简单来说,我们希望要素能够得出的结论是具有显著的聚集或离散模式,而不是随机模式。

p值表示概率,表示所观测到的空间模式是由某种随机过程创建而成的概率。如果p很小,则说明观测到的空间模式不太可能产生随机过程(小概率事件),因此可以拒绝零假设。

z得分是标准差的倍数。如果z得分是+2.5,可以说z得分是标准差的2.5倍。z得分的绝对值越高,高值(热点)的聚类就越紧密。所不同的是,正的高值表示热点,负得高值表示冷点。

GUID-CBF63B74-D1B2-44FC-A316-7AC2B1C1D464-web.gif

上图真tm清晰。如果要作出判断,则必须选择置信度,不同置信度和临界p值、临界z得分之间的关系。

clipboard.png

z得分的绝对值很大,同时p值很小的时候(即出现在正态分布的两端),往往意味着数据里蕴含着有趣的东西。例如“热点分析”中的显著的热点与冷点。

热点分析的意义

所以,回到热点分析,如果需要有显著意义,第一是要素自己是高值,第二是要被其他同样具有高值的要素包围。如果把某个要素及相邻要素的局部总和与所有要素的总和进行比较,如果与预期相比差异很大,则说明不随机,会产生一个有显著的统计学意义的z得分。

clipboard.png

查看原文

赞 0 收藏 0 评论 0

cyqian 发布了文章 · 2015-05-31

热点与聚集分析

将具有地理位置的数据定位到地图上进行可视化,是常用的方法,但是大量点数据的可视化往往具有迷惑性。例如某些数据集中会很多数据聚集在同一地理坐标上。

造成这一结果的原因有很多可能,比如:

  1. 坐标取整到某一个精度,导致实质数据网格化到同一位置
  2. 观察尺度问题,即在小尺度下观察大比例尺的数据
  3. 确实存在数据在同一点,例如在某个公共场所
  4. 数据登记位置非发生位置,例如劳务工人登记的地址大多是劳务公司而非实际服务的单位
  5. 测量误差
  6. ……

所以热点或热图的表达就有了重要意义。

下图引自链接描述

2011121111421317.png

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 7 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-01-01
个人主页被 367 人浏览