분류 수준이 열인 7 백만 생물 다양성 레코드의 CSV가 있습니다. 예를 들어 :
RecordID,kingdom,phylum,class,order,family,genus,species
1,Animalia,Chordata,Mammalia,Primates,Hominidae,Homo,Homo sapiens
2,Animalia,Chordata,Mammalia,Carnivora,Canidae,Canis,Canis
3,Plantae,nan,Magnoliopsida,Brassicales,Brassicaceae,Arabidopsis,Arabidopsis thaliana
4,Plantae,nan,Magnoliopsida,Fabales,Fabaceae,Phaseoulus,Phaseolus vulgaris
D3에서 시각화를 만들고 싶지만 데이터 형식은 네트워크 여야합니다. 여기서 각기 다른 열 값은 특정 값에 대한 이전 열의 자식입니다. CSV에서 다음과 같이 갈 필요가 있습니다.
{
name: 'Animalia',
children: [{
name: 'Chordata',
children: [{
name: 'Mammalia',
children: [{
name: 'Primates',
children: 'Hominidae'
}, {
name: 'Carnivora',
children: 'Canidae'
}]
}]
}]
}
수천 개의 for 루프를 사용하지 않고이를 수행하는 방법에 대한 아이디어를 얻지 못했습니다. 파이썬이나 자바 스크립트 에서이 네트워크를 만드는 방법에 대한 제안이 있습니까?
답변
정확히 중첩 된 객체를 만들려면 순수한 JavaScript와 D3 메소드를 혼합하여 사용합니다 d3.stratify
. 그러나 7 백만 개의 행이 있다는 것을 명심하십시오 ( post scriptum 참조). 아래 참조)은 계산해야 할 것이 많다는 점을 .
이 제안 된 솔루션에 대해 왕국 을 다른 데이터 배열 (예 :)을 사용하여 분리 해야한다는 점을 언급하는 것이 매우 중요합니다 Array.prototype.filter
. 이 제한은 루트 노드가 필요하기 때문에 발생하며 Linnaean 분류 체계에서 왕국 사이에는 아무런 관계가 없습니다 ( “도메인” 을 최상위 순위로 만들지 않는 한 모든 진핵 생물의 루트가되지만 Archaea와 Bacteria에 대한 문제).
따라서 하나의 왕국 으로이 CSV (행을 추가했습니다)가 있다고 가정하십시오.
RecordID,kingdom,phylum,class,order,family,genus,species
1,Animalia,Chordata,Mammalia,Primates,Hominidae,Homo,Homo sapiens
2,Animalia,Chordata,Mammalia,Carnivora,Canidae,Canis,Canis latrans
3,Animalia,Chordata,Mammalia,Cetacea,Delphinidae,Tursiops,Tursiops truncatus
1,Animalia,Chordata,Mammalia,Primates,Hominidae,Pan,Pan paniscus
해당 CSV를 기반으로 tableOfRelationships
이름에서 알 수 있듯이 순위 사이에 관계가있는 여기에 명명 된 배열을 만듭니다 .
const data = d3.csvParse(csv);
const taxonomicRanks = data.columns.filter(d => d !== "RecordID");
const tableOfRelationships = [];
data.forEach(row => {
taxonomicRanks.forEach((d, i) => {
if (!tableOfRelationships.find(e => e.name === row[d])) tableOfRelationships.push({
name: row[d],
parent: row[taxonomicRanks[i - 1]] || null
})
})
});
위 데이터의 경우 다음과 tableOfRelationships
같습니다.
+---------+----------------------+---------------+
| (Index) | name | parent |
+---------+----------------------+---------------+
| 0 | "Animalia" | null |
| 1 | "Chordata" | "Animalia" |
| 2 | "Mammalia" | "Chordata" |
| 3 | "Primates" | "Mammalia" |
| 4 | "Hominidae" | "Primates" |
| 5 | "Homo" | "Hominidae" |
| 6 | "Homo sapiens" | "Homo" |
| 7 | "Carnivora" | "Mammalia" |
| 8 | "Canidae" | "Carnivora" |
| 9 | "Canis" | "Canidae" |
| 10 | "Canis latrans" | "Canis" |
| 11 | "Cetacea" | "Mammalia" |
| 12 | "Delphinidae" | "Cetacea" |
| 13 | "Tursiops" | "Delphinidae" |
| 14 | "Tursiops truncatus" | "Tursiops" |
| 15 | "Pan" | "Hominidae" |
| 16 | "Pan paniscus" | "Pan" |
+---------+----------------------+---------------+
null
부모님을 살펴보십시오 Animalia
. 그래서 왕국별로 데이터 세트를 분리해야한다고 말한 이유 null
는 전체 테이블에 하나의 값만 있을 수 있습니다 .
마지막으로 해당 테이블을 기반으로 다음을 사용하여 계층을 만듭니다 d3.stratify()
.
const stratify = d3.stratify()
.id(function(d) { return d.name; })
.parentId(function(d) { return d.parent; });
const hierarchicalData = stratify(tableOfRelationships);
그리고 여기 데모가 있습니다. 브라우저 콘솔을 열고 (스 니펫 콘솔은이 작업에 적합하지 않음 children
) 개체 의 여러 수준 ( )을 검사하십시오 .
추신 : 나는 어떤 종류의 dataviz를 만들지 모르겠지만 분류학 순위를 피해야합니다. Linnaean 분류 체계 전체가 구식이므로 더 이상 순위를 사용하지 않습니다. 60 년대 중반에 계통 발생 체계가 개발 되었기 때문에 분류 학적 순위가없는 분류 체계 만 사용합니다 (진화 생물학 교사). 또한, 우리는 백만 종이 넘는 종을 묘사했기 때문에이 7 백만 줄에 대해 매우 궁금합니다.
답변
파이썬과 python-benedict
라이브러리를 사용하여 필요한 것을 정확하게 수행하는 것은 쉽습니다 ( Github의 오픈 소스입니다) :
설치 pip install python-benedict
from benedict import benedict as bdict
# data source can be a filepath or an url
data_source = """
RecordID,kingdom,phylum,class,order,family,genus,species
1,Animalia,Chordata,Mammalia,Primates,Hominidae,Homo,Homo sapiens
2,Animalia,Chordata,Mammalia,Carnivora,Canidae,Canis,Canis
3,Plantae,nan,Magnoliopsida,Brassicales,Brassicaceae,Arabidopsis,Arabidopsis thaliana
4,Plantae,nan,Magnoliopsida,Fabales,Fabaceae,Phaseoulus,Phaseolus vulgaris
"""
data_input = bdict.from_csv(data_source)
data_output = bdict()
ancestors_hierarchy = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species']
for value in data_input['values']:
data_output['.'.join([value[ancestor] for ancestor in ancestors_hierarchy])] = bdict()
print(data_output.dump())
# if this output is ok for your needs, you don't need the following code
keypaths = sorted(data_output.keypaths(), key=lambda item: len(item.split('.')), reverse=True)
data_output['children'] = []
def transform_data(d, key, value):
if isinstance(value, dict):
value.update({ 'name':key, 'children':[] })
data_output.traverse(transform_data)
for keypath in keypaths:
target_keypath = '.'.join(keypath.split('.')[:-1] + ['children'])
data_output[target_keypath].append(data_output.pop(keypath))
print(data_output.dump())
첫 번째 인쇄 출력은 다음과 같습니다.
{
"Animalia": {
"Chordata": {
"Mammalia": {
"Carnivora": {
"Canidae": {
"Canis": {
"Canis": {}
}
}
},
"Primates": {
"Hominidae": {
"Homo": {
"Homo sapiens": {}
}
}
}
}
}
},
"Plantae": {
"nan": {
"Magnoliopsida": {
"Brassicales": {
"Brassicaceae": {
"Arabidopsis": {
"Arabidopsis thaliana": {}
}
}
},
"Fabales": {
"Fabaceae": {
"Phaseoulus": {
"Phaseolus vulgaris": {}
}
}
}
}
}
}
}
두 번째 인쇄 출력은 다음과 같습니다.
{
"children": [
{
"name": "Animalia",
"children": [
{
"name": "Chordata",
"children": [
{
"name": "Mammalia",
"children": [
{
"name": "Carnivora",
"children": [
{
"name": "Canidae",
"children": [
{
"name": "Canis",
"children": [
{
"name": "Canis",
"children": []
}
]
}
]
}
]
},
{
"name": "Primates",
"children": [
{
"name": "Hominidae",
"children": [
{
"name": "Homo",
"children": [
{
"name": "Homo sapiens",
"children": []
}
]
}
]
}
]
}
]
}
]
}
]
},
{
"name": "Plantae",
"children": [
{
"name": "nan",
"children": [
{
"name": "Magnoliopsida",
"children": [
{
"name": "Brassicales",
"children": [
{
"name": "Brassicaceae",
"children": [
{
"name": "Arabidopsis",
"children": [
{
"name": "Arabidopsis thaliana",
"children": []
}
]
}
]
}
]
},
{
"name": "Fabales",
"children": [
{
"name": "Fabaceae",
"children": [
{
"name": "Phaseoulus",
"children": [
{
"name": "Phaseolus vulgaris",
"children": []
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
답변
var log = console.log;
var data = `
1,Animalia,Chordata,Mammalia,Primates,Hominidae,Homo,Homo sapiens
2,Animalia,Chordata,Mammalia,Carnivora,Canidae,Canis,Canis
3,Plantae,nan,Magnoliopsida,Brassicales,Brassicaceae,Arabidopsis,Arabidopsis thaliana
4,Plantae,nan,Magnoliopsida,Fabales,Fabaceae,Phaseoulus,Phaseolus vulgaris`;
//make array of rows with array of values
data = data.split("\n").map(v=>v.split(","));
//init tree
var tree = {};
data.forEach(row=>{
//set current = root of tree for every row
var cur = tree;
var id = false;
row.forEach((value,i)=>{
if (i == 0) {
//set id and skip value
id = value;
return;
}
//If branch not exists create.
//If last value - write id
if (!cur[value]) cur[value] = (i == row.length - 1) ? id : {};
//Move link down on hierarhy
cur = cur[value];
});
});
log("Tree:");
log(JSON.stringify(tree, null, " "));
//Now you have hierarhy in tree and can do anything with it.
var toStruct = function(obj) {
let ret = [];
for (let key in obj) {
let child = obj[key];
let rec = {};
rec.name = key;
if (typeof child == "object") rec.children = toStruct(child);
ret.push(rec);
}
return ret;
}
var struct = toStruct(tree);
console.log("Struct:");
console.log(struct);
답변
이것은 간단 해 보이므로 귀하의 문제를 이해하지 못할 수도 있습니다.
원하는 데이터 구조는 중첩 된 사전 세트, 키 / 값 쌍입니다. 최상위 왕국 사전에는 각 왕국에 대한 열쇠가 있으며 그 가치는 phylum 사전입니다. phylum 사전 (한 왕국에 대한)에는 각 phylum 이름에 대한 키가 있으며 각 키에는 클래스 사전 등의 값이 있습니다.
코드 작성을 간단하게하기 위해 속 사전에는 각 종의 키가 있지만 종 값은 빈 사전이됩니다.
이것은 당신이 원하는 것이어야합니다. 이상한 라이브러리가 필요하지 않습니다.
import csv
def read_data(filename):
tree = {}
with open(filename) as f:
f.readline() # skip the column headers line of the file
for animal_cols in csv.reader(f):
spot = tree
for name in animal_cols[1:]: # each name, skipping the record number
if name in spot: # The parent is already in the tree
spot = spot[name]
else:
spot[name] = {} # creates a new entry in the tree
spot = spot[name]
return tree
그것을 테스트하기 위해 나는 당신의 데이터를 사용했고 pprint
표준 라이브러리를 사용했습니다.
from pprint import pprint
pprint(read_data('data.txt'))
점점
{'Animalia': {'Chordata': {'Mammalia': {'Carnivora': {'Canidae': {'Canis': {'Canis': {}}}},
'Primates': {'Hominidae': {'Homo': {'Homo sapiens': {}}}}}}},
'Plantae': {'nan': {'Magnoliopsida': {'Brassicales': {'Brassicaceae': {'Arabidopsis': {'Arabidopsis thaliana': {}}}},
'Fabales': {'Fabaceae': {'Phaseoulus': {'Phaseolus vulgaris': {}}}}}}}}
질문을 다시 읽으면 큰 쌍의 테이블 ( ‘일반 그룹의 링크’, ‘보다 구체적인 그룹에 대한 링크’)이 필요할 수 있습니다. 즉, ‘Animalia’는 ‘Animalia : Chordata’에 연결되고 ‘Animalia : Chordata’는 ‘Animalia : Chordata : Mammalia “에 연결됩니다. 불행히도, 데이터의’nan ‘은 각 링크에서 전체 이름이 필요하다는 의미입니다. 부모, 자식) 쌍은 당신이 원하는 것입니다, 나무를 이런 식으로 걷습니다.
def walk_children(tree, parent=''):
for child in tree.keys():
full_name = parent + ':' + child
yield (parent, full_name)
yield from walk_children(tree[child], full_name)
tree = read_data('data.txt')
for (parent, child) in walk_children(tree):
print(f'parent="{parent}" child="{child}"')
기부:
parent="" child=":Animalia"
parent=":Animalia" child=":Animalia:Chordata"
parent=":Animalia:Chordata" child=":Animalia:Chordata:Mammalia"
parent=":Animalia:Chordata:Mammalia" child=":Animalia:Chordata:Mammalia:Primates"
parent=":Animalia:Chordata:Mammalia:Primates" child=":Animalia:Chordata:Mammalia:Primates:Hominidae"
parent=":Animalia:Chordata:Mammalia:Primates:Hominidae" child=":Animalia:Chordata:Mammalia:Primates:Hominidae:Homo"
parent=":Animalia:Chordata:Mammalia:Primates:Hominidae:Homo" child=":Animalia:Chordata:Mammalia:Primates:Hominidae:Homo:Homo sapiens"
parent=":Animalia:Chordata:Mammalia" child=":Animalia:Chordata:Mammalia:Carnivora"
parent=":Animalia:Chordata:Mammalia:Carnivora" child=":Animalia:Chordata:Mammalia:Carnivora:Canidae"
parent=":Animalia:Chordata:Mammalia:Carnivora:Canidae" child=":Animalia:Chordata:Mammalia:Carnivora:Canidae:Canis"
parent=":Animalia:Chordata:Mammalia:Carnivora:Canidae:Canis" child=":Animalia:Chordata:Mammalia:Carnivora:Canidae:Canis:Canis"
parent="" child=":Plantae"
parent=":Plantae" child=":Plantae:nan"
parent=":Plantae:nan" child=":Plantae:nan:Magnoliopsida"
parent=":Plantae:nan:Magnoliopsida" child=":Plantae:nan:Magnoliopsida:Brassicales"
parent=":Plantae:nan:Magnoliopsida:Brassicales" child=":Plantae:nan:Magnoliopsida:Brassicales:Brassicaceae"
parent=":Plantae:nan:Magnoliopsida:Brassicales:Brassicaceae" child=":Plantae:nan:Magnoliopsida:Brassicales:Brassicaceae:Arabidopsis"
parent=":Plantae:nan:Magnoliopsida:Brassicales:Brassicaceae:Arabidopsis" child=":Plantae:nan:Magnoliopsida:Brassicales:Brassicaceae:Arabidopsis:Arabidopsis thaliana"
parent=":Plantae:nan:Magnoliopsida" child=":Plantae:nan:Magnoliopsida:Fabales"
parent=":Plantae:nan:Magnoliopsida:Fabales" child=":Plantae:nan:Magnoliopsida:Fabales:Fabaceae"
parent=":Plantae:nan:Magnoliopsida:Fabales:Fabaceae" child=":Plantae:nan:Magnoliopsida:Fabales:Fabaceae:Phaseoulus"
parent=":Plantae:nan:Magnoliopsida:Fabales:Fabaceae:Phaseoulus" child=":Plantae:nan:Magnoliopsida:Fabales:Fabaceae:Phaseoulus:Phaseolus vulgaris"
답변
파이썬에서 트리를 인코딩하는 한 가지 방법 dict
은 키 를 사용하여 노드를 나타내고 관련 값이 노드의 부모 인를 사용하는 것입니다.
{'Homo sapiens': 'Homo',
'Canis': 'Canidae',
'Arabidopsis thaliana': 'Arabidopsis',
'Phaseolus vulgaris': 'Phaseoulus',
'Homo': 'Hominidae',
'Arabidopsis': 'Brassicaceae',
'Phaseoulus': 'Fabaceae',
'Hominidae': 'Primates',
'Canidae': 'Carnivora',
'Brassicaceae': 'Brassicales',
'Fabaceae': 'Fabales',
'Primates': 'Mammalia',
'Carnivora': 'Mammalia',
'Brassicales': 'Magnoliopsida',
'Fabales': 'Magnoliopsida',
'Mammalia': 'Chordata',
'Magnoliopsida': 'nan',
'Chordata': 'Animalia',
'nan': 'Plantae',
'Animalia': None,
'Plantae': None}
이것의 장점은 노드가 고유하다는 것입니다. dicts
은 중복 키를 가질 수 없으므로 입니다.
보다 일반적인 지향 그래프를 대신 인코딩하려면 (즉, 노드에 둘 이상의 부모가있을 수 있음) 값 목록을 사용하고 자식 (또는 부모)을 나타내는 것으로 가정 할 수 있습니다.
{'Homo': ['Homo sapiens', 'ManBearPig'],
'Ursus': ['Ursus arctos', 'ManBearPig'],
'Sus': ['ManBearPig']}
필요한 경우 JS의 객체와 비슷한 것을 수행하여 목록을 배열로 대체 할 수 있습니다.
위의 첫 번째 dict를 작성하는 데 사용한 Python 코드는 다음과 같습니다.
import csv
ROWS = []
# Load file: tbl.csv
with open('tbl.csv', 'r') as in_file:
csvreader = csv.reader(in_file)
# Ignore leading row numbers
ROWS = [row[1:] for row in csvreader]
# Drop header row
del ROWS[0]
# Build dict
mytree = {row[i]: row[i-1] for row in ROWS for i in range(len(row)-1, 0, -1)}
# Add top-level nodes
mytree = {**mytree, **{row[0]: None for row in ROWS}}
답변
아마도 데이터를 계층 구조로 바꾸는 가장 간단한 방법은 D3의 내장 중첩 연산자를 사용하는 것입니다 d3.nest()
.
중첩을 사용하면 배열의 요소를 계층 트리 구조로 그룹화 할 수 있습니다.
주요 기능을 통해 등록 nest.key()
하면 계층 구조를 쉽게 지정할 수 있습니다. Gerardo가 자신의 답변에 제시 한 것처럼 .columns
CSV를 구문 분석 한 후 데이터 배열에 노출 된 속성을 사용하여 이러한 주요 기능 생성을 자동화 할 수 있습니다 . 전체 코드는 다음 줄로 요약됩니다.
const nester = d3.nest(); // Create a nest operator
const [, ...taxonomicRanks] = data.columns; // Get rid of the RecordID property
taxonomicRanks.forEach(r => nester.key(d => d[r])); // Register key functions
const nest = nester.entries(data); // Calculate hierarchy
그러나 결과 계층 구조는 객체가 { key, values }
아니라 질문에 요청 된 구조와 정확하게 일치하지 않습니다 { name, children }
. 그건 그렇고, 이것은 Gerardo의 대답에도 마찬가지입니다. 그러나 자식 접근 자 함수를 d3.hierarchy()
지정 하여 결과가 혼잡해질 수 있으므로 두 가지 대답 모두에 해를 끼치 지 않습니다 .
d3.hierarchy(nest, d => d.values) // Second argument is the children accessor
다음 데모는 모든 부분을 정리합니다.
게시 된 구조가 정확히 필요하다고 생각되면 d3.nest () 키와 값을 이름과 자식 으로 변환 하는 것을 살펴볼 수도 있습니다 .
답변
재미있는 도전. 이 자바 스크립트 코드를 사용해보십시오. 나는 Lodash의 설정을 간단하게 사용합니다.
import { set } from 'lodash'
const csvString = `RecordID,kingdom,phylum,class,order,family,genus,species
1,Animalia,Chordata,Mammalia,Primates,Hominidae,Homo,Homo sapiens
2,Animalia,Chordata,Mammalia,Carnivora,Canidae,Canis,Canis
3,Plantae,nan,Magnoliopsida,Brassicales,Brassicaceae,Arabidopsis,Arabidopsis thaliana
4,Plantae,nan,Magnoliopsida,Fabales,Fabaceae,Phaseoulus,Phaseolus vulgaris`
// First create a quick lookup map
const result = csvString
.split('\n') // Split for Rows
.slice(1) // Remove headers
.reduce((acc, row) => {
const path = row
.split(',') // Split for columns
.filter(item => item !== 'nan') // OPTIONAL: Filter 'nan'
.slice(1) // Remove record id
const species = path.pop() // Pull out species (last entry)
set(acc, path, species)
return acc
}, {})
console.log(JSON.stringify(result, null, 2))
// Then convert to the name-children structure by recursively calling this function
const convert = (obj) => {
// If we're at the end of our chain, end the chain (children is empty)
if (typeof obj === 'string') {
return [{
name: obj,
children: [],
}]
}
// Else loop through each entry and add them as children
return Object.entries(obj)
.reduce((acc, [key, value]) => acc.concat({
name: key,
children: convert(value), // Recursive call
}), [])
}
const result2 = convert(result)
console.log(JSON.stringify(result2, null, 2))
이렇게하면 원하는 결과와 비슷한 최종 결과가 생성됩니다.
[
{
"name": "Animalia",
"children": [
{
"name": "Chordata",
"children": [
{
"name": "Mammalia",
"children": [
{
"name": "Primates",
"children": [
{
"name": "Hominidae",
"children": [
{
"name": "Homo",
"children": [
{
"name": "Homo sapiens",
"children": []
}
]
}
]
}
]
},
{
"name": "Carnivora",
"children": [
{
"name": "Canidae",
"children": [
{
"name": "Canis",
"children": [
{
"name": "Canis",
"children": []
}
]
}
]
}
]
}
]
}
]
}
]
},
{
"name": "Plantae",
"children": [
{
"name": "Magnoliopsida",
"children": [
{
"name": "Brassicales",
"children": [
{
"name": "Brassicaceae",
"children": [
{
"name": "Arabidopsis",
"children": [
{
"name": "Arabidopsis thaliana",
"children": []
}
]
}
]
}
]
},
{
"name": "Fabales",
"children": [
{
"name": "Fabaceae",
"children": [
{
"name": "Phaseoulus",
"children": [
{
"name": "Phaseolus vulgaris",
"children": []
}
]
}
]
}
]
}
]
}
]
}
]
