您现在的位置是:网站首页 > 数据转换与预处理文章详情

数据转换与预处理

数据转换与预处理是数据可视化中不可或缺的环节,尤其在ECharts这类工具中,原始数据往往需要经过清洗、格式化或聚合才能适配图表需求。从简单的数据类型转换到复杂的聚合计算,每一步都可能直接影响最终呈现效果。

数据格式标准化

ECharts大部分图表要求数据为特定格式。例如折线图需要xAxisyAxis的数值数组,而饼图需要{value, name}的对象数组。原始数据可能是CSV字符串或嵌套JSON,需要进行扁平化处理:

// 原始CSV数据
const csvData = `日期,销售额
2023-01,1500
2023-02,3200`;

// 转换为ECharts需要的格式
function parseCSV(csv) {
  const lines = csv.split('\n');
  const headers = lines[0].split(',');
  return lines.slice(1).map(line => {
    const values = line.split(',');
    return {
      [headers[0]]: values[0],
      [headers[1]]: parseFloat(values[1])
    };
  });
}

const seriesData = parseCSV(csvData).map(item => item['销售额']);
const xAxisData = parseCSV(csvData).map(item => item['日期']);

时间序列处理

处理时间数据时经常需要转换时间格式。ECharts的时间轴(xAxis.type='time')要求时间戳或ISO格式字符串:

const rawData = [
  { date: '01/15/2023', value: 42 },
  { date: '02/20/2023', value: 78 }
];

// 使用Day.js进行日期格式化
import dayjs from 'dayjs';

const processedData = rawData.map(item => ({
  date: dayjs(item.date, 'MM/DD/YYYY').format('YYYY-MM-DD'),
  value: item.value
}));

option = {
  xAxis: {
    type: 'time',
    data: processedData.map(d => d.date)
  },
  series: [{
    data: processedData.map(d => d.value)
  }]
};

数据聚合与分组

当原始数据过于详细时,需要按维度聚合。比如将每日数据聚合成月平均值:

const dailyData = [
  { date: '2023-01-01', temperature: 12 },
  { date: '2023-01-02', temperature: 14 },
  // ...更多数据
  { date: '2023-02-01', temperature: 18 }
];

const monthlyAvg = {};
dailyData.forEach(item => {
  const month = item.date.substring(0, 7); // 提取年月
  if (!monthlyAvg[month]) {
    monthlyAvg[month] = { sum: 0, count: 0 };
  }
  monthlyAvg[month].sum += item.temperature;
  monthlyAvg[month].count += 1;
});

const result = Object.keys(monthlyAvg).map(month => ({
  month,
  temperature: monthlyAvg[month].sum / monthlyAvg[month].count
}));

缺失值处理

实际数据常存在缺失值,ECharts中可用以下策略处理:

  1. 线性插值法填补缺失数据点:
function interpolateMissing(data, key) {
  return data.map((current, i) => {
    if (current[key] !== null) return current;
    
    // 找到前一个有效值
    let prev = i - 1;
    while (prev >= 0 && data[prev][key] === null) prev--;
    
    // 找到后一个有效值
    let next = i + 1;
    while (next < data.length && data[next][key] === null) next++;
    
    // 两端插值
    if (prev < 0) return { ...current, [key]: data[next][key] };
    if (next >= data.length) return { ...current, [key]: data[prev][key] };
    
    // 线性插值
    const ratio = (i - prev) / (next - prev);
    const val = data[prev][key] + (data[next][key] - data[prev][key]) * ratio;
    return { ...current, [key]: val };
  });
}
  1. 使用ECharts的视觉映射突出显示缺失点:
series: [{
  data: [
    { value: 10 },
    { value: null, itemStyle: { color: '#ff0000' } },
    { value: 20 }
  ]
}]

数据归一化

当不同系列的数据量级差异较大时,需要进行归一化处理:

const dataA = [120, 132, 101, 134, 90];
const dataB = [0.12, 0.19, 0.13, 0.14, 0.17];

function minMaxNormalize(arr) {
  const min = Math.min(...arr);
  const max = Math.max(...arr);
  return arr.map(v => (v - min) / (max - min));
}

option = {
  yAxis: [{ type: 'value' }, { type: 'value' }],
  series: [
    { data: dataA, yAxisIndex: 0 },
    { data: minMaxNormalize(dataB), yAxisIndex: 1 }
  ]
};

地理数据编码

处理地图数据时,常需要将地理名称转换为坐标:

// 使用阿里云地理编码API示例
async function geoEncode(address) {
  const response = await fetch(
    `https://geocode-api.aliyun.com/geocode?address=${encodeURIComponent(address)}`
  );
  const { lon, lat } = await response.json();
  return [parseFloat(lon), parseFloat(lat)];
}

const cities = ['北京', '上海', '广州'];
const coords = await Promise.all(cities.map(geoEncode));

option = {
  series: [{
    type: 'scatter',
    coordinateSystem: 'geo',
    data: coords.map((coord, i) => ({
      name: cities[i],
      value: [...coord, Math.random() * 100] // [经度, 纬度, 数值]
    }))
  }]
};

树形数据转换

ECharts的树图需要特定层级结构,需将扁平数据转换为嵌套结构:

const flatData = [
  { id: 1, name: '总部', parentId: null },
  { id: 2, name: '华东分部', parentId: 1 },
  { id: 3, name: '杭州办事处', parentId: 2 }
];

function buildTree(data, rootId) {
  const nodeMap = {};
  data.forEach(item => nodeMap[item.id] = { ...item, children: [] });
  
  const tree = [];
  data.forEach(item => {
    if (item.parentId === rootId) {
      tree.push(nodeMap[item.id]);
    } else if (nodeMap[item.parentId]) {
      nodeMap[item.parentId].children.push(nodeMap[item.id]);
    }
  });
  return tree;
}

option = {
  series: [{
    type: 'tree',
    data: buildTree(flatData, null)
  }]
};

动态数据更新

对于实时数据流,需要进行滑动窗口处理:

class DataWindow {
  constructor(size) {
    this.size = size;
    this.buffer = [];
  }
  
  push(data) {
    this.buffer.push(data);
    if (this.buffer.length > this.size) {
      this.buffer.shift();
    }
    return this.buffer;
  }
}

const tempWindow = new DataWindow(60); // 保留最近60个数据点

// 模拟实时数据推送
setInterval(() => {
  const newTemp = Math.random() * 30 + 10;
  const seriesData = tempWindow.push(newTemp);
  
  myChart.setOption({
    series: [{
      data: seriesData
    }]
  });
}, 1000);

多维数据透视

处理多维数据时,可能需要转换为ECharts支持的平行坐标系格式:

const rawData = [
  { cpu: 0.12, memory: 0.45, network: 1.2, disk: 0.8 },
  { cpu: 0.23, memory: 0.67, network: 0.9, disk: 1.1 }
];

const dimensions = ['cpu', 'memory', 'network', 'disk'];
const parallelData = rawData.map(item => 
  dimensions.map(dim => item[dim])
);

option = {
  parallelAxis: dimensions.map(dim => ({ dim: dimensions.indexOf(dim), name: dim })),
  series: {
    type: 'parallel',
    data: parallelData
  }
};

数据采样优化

当数据量过大时,需要进行降采样以提高渲染性能:

function downsample(data, factor, accessor) {
  const sampled = [];
  for (let i = 0; i < data.length; i += factor) {
    const segment = data.slice(i, i + factor);
    const avgValue = segment.reduce((sum, d) => sum + accessor(d), 0) / segment.length;
    sampled.push({
      ...data[i],
      _value: avgValue  // 保留原始数据引用同时存储采样值
    });
  }
  return sampled;
}

const highFreqData = Array.from({ length: 10000 }, (_, i) => ({
  timestamp: Date.now() + i * 1000,
  value: Math.sin(i / 100) * 50 + 100
}));

const sampledData = downsample(highFreqData, 10, d => d.value);

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步