数据新闻学Skill data-journalism

数据新闻学是一种利用数据分析、统计方法和可视化技术来发现、分析和呈现新闻故事的技能。它涉及数据采集、清洗、分析、可视化以及基于数据的叙事构建,适用于记者、新闻编辑室和研究人员处理定量信息。关键词:数据新闻、数据分析、数据可视化、新闻报道、数据故事、统计方法、数据清洗、数据驱动叙事。

数据分析 0 次安装 0 次浏览 更新于 3/15/2026

名称: 数据新闻学 描述: 数据新闻学工作流程,用于分析、可视化和故事讲述。适用于分析数据集、创建图表和地图、清理混乱数据、计算统计数据或构建数据驱动故事。对于处理定量信息的记者、新闻编辑室和研究人员至关重要。

数据新闻学方法论

系统化方法,用于新闻学中数据的发现、分析和呈现。

数据新闻学故事结构

数据新闻学框架


数据新闻学框架由Philip Meyer建立,他是Knight-Ridder的记者、哈佛Nieman研究员和UNC-Chapel Hill的教授。在他的书《新精确新闻学》中,概述了他的理念,Meyer鼓励记者将新闻学“视为一门科学”,通过采用科学方法:
- 进行观察/提出问题
- 研究问题/收集、存储和检索数据
- 形成假设
- 测试假设,使用定性(采访、文档等)和定量(数据分析等)方法
- 分析结果,将其减少到最重要的发现
- 呈现给受众

这个过程应被视为迭代的,而不是顺序的。

## 数据故事弧

### 1. 钩子(核心段落)
- 关键发现是什么?
- 为什么读者应该关心?
- 人类影响是什么?

### 2. 证据
- 展示数据
- 解释方法论
- 承认局限性

### 3. 上下文
- 这与过去相比如何?
- 这与其他地方相比如何?
- 趋势是什么?

### 4. 人类元素
- 个体示例,说明数据
- 专家解释
- 受影响的声音

### 5. 意义
- 这对未来意味着什么?
- 哪些问题仍然存在?
- 可能产生什么行动?

### 6. 方法论框
- 数据来自哪里?
- 如何分析的?
- 局限性是什么?
- 读者如何进一步探索?

方法论文档模板

## 我们如何进行分析

### 数据来源
[列出所有数据来源,包含链接和访问日期]

### 时间周期
[指定确切覆盖的时间周期]

### 定义
[定义关键术语以及如何操作化它们]

### 分析步骤
1. [第一步分析]
2. [第二步]
3. [继续...]

### 局限性
- [局限性1]
- [局限性2]

### 我们排除了什么及原因
- [排除类别]: [原因]

### 验证
[如何验证/检查发现]

### 代码和数据可用性
[如果共享代码/数据,链接到GitHub仓库]

### 联系
[读者如何联系您提出问题]

数据获取

公共数据来源

## 联邦数据来源

### 一般
- Data.gov - 联邦开放数据门户
- 人口普查局 (census.gov) - 人口统计、经济数据
- BLS (bls.gov) - 就业、通货膨胀、工资
- BEA (bea.gov) - GDP、经济账户
- 联邦储备 (federalreserve.gov) - 金融数据
- SEC EDGAR - 公司文件

### 特定领域
- EPA (epa.gov/data) - 环境数据
- FDA (fda.gov/data) - 药物批准、召回、不良事件
- CDC WONDER - 健康统计
- NHTSA - 车辆安全数据
- DOT - 交通统计
- FEC - 竞选资金
- USASpending.gov - 联邦合同和赠款

### 州和地方
- 州开放数据门户(搜索:“[州] open data”)
- Socrata-powered网站(许多城市/州)
- OpenStreets,市政GIS门户
- 州审计长/审计员报告

数据请求策略

## 获取非公开数据

### 公共记录请求(如FOIA)用于数据集
- 请求数据库,而不仅仅是文档
- 请求数据字典/模式
- 请求原生格式(CSV、SQL转储)
- 指定字段级需求

### 构建自己的数据集
- 抓取公共信息
- 从读者众包
- 系统化文档审查
- 调查(使用正确方法论)

### 商业数据来源(针对新闻编辑室)
- LexisNexis
- Refinitiv
- Bloomberg
- 行业特定数据库

数据清洗和准备

常见数据问题

from typing import Any

import pandas as pd
import numpy as np
from rapidfuzz import fuzz
from itertools import combinations

# 通胀调整
import cpi
import wbdata

def standardize_name(name: Any) -> str | None:
    """标准化名称格式为'名 姓'。"""
    if pd.isna(name):
        return None
    name = str(name).strip().upper()
    # 处理“姓, 名”格式
    if ',' in name:
        parts = name.split(',')
        name = f"{parts[1].strip()} {parts[0].strip()}"
    return name

def parse_date(date_str: Any) -> pd.Timestamp | None:
    """解析各种格式的日期。"""
    if pd.isna(date_str):
        return None

    formats = [
        '%m/%d/%Y', '%Y-%m-%d', '%B %d, %Y',
        '%d-%b-%y', '%m-%d-%Y', '%Y/%m/%d'
    ]

    for fmt in formats:
        try:
            return pd.to_datetime(date_str, format=fmt)
        except:
            continue

    # 回退到pandas解析器
    try:
        return pd.to_datetime(date_str)
    except:
        return None


def handle_missing(df:pd.DataFrame, thresh:int | None, per_thresh:float | None, required_col:str | None) -> pd.DataFrame:
    '''处理具有太多缺失值的DataFrame,由用户定义。''' 
    if thresh and data_clean.isna().sum() >= thresh:
        return df.dropna(subset=[required_col]).reset_index(drop=True).copy()
    
    elif per_thresh and (data_clean.isna().sum() / len(data_clean) * 100) >= per_thresh:
        return df.dropna(subset=[required_col]).reset_index(drop=True).copy()
    
    else:
        return df


def handle_duplicates(df:pd.DataFrame, thresh=int | None)
    '''处理数据的重复行。'''
    if thresh and df.duplicated().sum() >= thresh:
        return df.drop_duplicates().reset_index(drop=True).copy()
    else:
        return df


def flag_similar_names(df: pd.DataFrame, name_col: str, threshold: int = 85) -> pd.DataFrame:
    """标记具有潜在重复名称的行,使用向量化比较。"""
    
    names = df[name_col].dropna().unique()
    
    # 使用combinations()避免嵌套循环和重复比较
    dup_names: set[Any] = {
        name
        for name1, name2 in combinations(names, 2)
        if fuzz.ratio(str(name1).lower(), str(name2).lower()) >= threshold
        for name in (name1, name2)
    }
    
    df['has_similar_name'] = df[name_col].isin(dup_names)
    return df


def flag_outliers(series: pd.Series, method: str = 'iqr', threshold: float = 1.5) -> pd.Series:
    """标记统计异常值。"""
    if method == 'iqr':
        Q1 = series.quantile(0.25)
        Q3 = series.quantile(0.75)
        IQR = Q3 - Q1
        lower = Q1 - threshold * IQR
        upper = Q3 + threshold * IQR
        return (series < lower) | (series > upper)
    elif method == 'zscore':
        z_scores = np.abs((series - series.mean()) / series.std())
        return z_scores > threshold



# 使用描述性变量名并链式方法
data_clean = (pd

            # 加载混乱数据 — raw_data是占位符
            # 确保对文件类型使用正确的读取器
            .read_csv('..data/raw/raw_data.csv')

            # 数据类型校正
            # 确保分析的正确类型
            .assign(# 转换为数值(处理错误)
                    amount = lambda x: pd.to_numeric(x['amount'], errors='coerce'),
                    
                    # 转换为分类(节省内存,启用排序)
                    status = lambda x: pd.Categorical(x['status'])) 
            
            .assign(
                    # 不一致格式
                    # 问题:名称在不同格式中
                    # 如“SMITH, JOHN” vs “John Smith” vs “smith john”
                    name_clean = lambda x: standardize_name(x['name']),
                    
                    # 日期不一致
                    # 问题:日期在多种格式中
                    # 如“01/15/2024”、“2024-01-15”、“January 15, 2024”、“15-Jan-24”
                    parse_date = lambda x: parse_date(x['date']),
                    
                    # 异常值
                    # 识别潜在数据输入错误
                    amount_outlier = lambda x: flag_outliers(x['amount']),
                    
                    )
            
            # 模糊重复(相似但不相同)
            # 使用记录链接或手动审查
            .pipe(find_similar_names, name_col='name_clean', threshold=85)

            # 缺失值
            # 策略取决于上下文
            # 首先检查缺失值模式
            .pipe(handle_missing, thresh=None, per_thresh=None)

            # 重复 — 查找和处理重复
            .pipe(handle_duplicates, thresh=None)
            
            .reset_index(drop=True)
            .copy())


数据验证清单

## 预分析数据验证

### 结构检查
- [ ] 行计数匹配预期
- [ ] 列计数和名称正确
- [ ] 数据类型适当
- [ ] 没有意外的空列

### 内容检查
- [ ] 日期范围合理
- [ ] 数值在预期范围内
- [ ] 分类值匹配预期选项
- [ ] 地理数据正确解析
- [ ] ID在预期处唯一

### 一致性检查
- [ ] 总计加到预期值
- [ ] 交叉表平衡
- [ ] 相关字段一致
- [ ] 时间序列连续

### 来源验证
- [ ] 可追溯到原始来源
- [ ] 方法论已记录
- [ ] 已知局限性已注明
- [ ] 更新频率已理解

新闻学统计分析

带上下文的基本统计

# 任何数据集的基本统计
def describe_for_journalism(df: pd.DataFrame, col: str) -> pd.DataFrame:
    """生成记者友好的统计。"""
    stats = df[col].describe(percentiles=[0.25, 0.5, 0.75, 0.9, 0.99])
    
    # 将偏度添加到describe()输出
    stats['skewness'] = df[col].skew()
    
    return stats.to_frame(name=col)

# 示例解释
stats = describe_for_journalism(salaries, 'salary')

print(f"""
分析
---------------
我们分析了{stats['count']:,}个工资记录。

中位数工资为${stats['median']:,.0f},意味着一半工人
赚得更多,一半赚得更少。

平均工资为${stats['mean']:,.0f},这
比中位数{'高' if stats['mean'] > stats['median'] else '低'},
表明分布是{'右偏(被高收入者拉高)'
if stats['skewness'] > 0 else '左偏'}。

收入最高的10%至少赚${stats['90th_percentile']:,.0f}。
收入最高的1%至少赚${stats['99th_percentile']:,.0f}。
""")

比较和上下文

# 计算列的变更指标
def calculate_change(df: pd.DataFrame, col: str, periods: int = 1) -> pd.DataFrame:
    """使用内置pandas方法向DataFrame添加变更指标。
    
    参数:
        df: 输入DataFrame
        col: 计算变更的列
        periods: 回顾的行数(1=前一行,12=月度数据的逐年对比)
    """
    return df.assign(
        absolute_change=df[col].diff(periods),
        percent_change=df[col].pct_change(periods) * 100,
        direction=np.sign(df[col].diff(periods)).map({1: '增加', -1: '减少', 0: '不变'})
    )

# 用法:
# changes = data_clean.pipe(calculate_change, 'revenue', periods=12) # 月度数据的逐年对比

# 人均计算(公平比较的关键)
def per_capita(value: float, population: float, multiplier: int = 100000) -> float:
    """计算人均率。"""
    return (value / population) * multiplier  # 每100,000是标准

# 示例:犯罪率
city_a = {'crimes': 5000, 'population': 100000}
city_b = {'crimes': 8000, 'population': 500000}

rate_a = per_capita(city_a['crimes'], city_a['population'])
rate_b = per_capita(city_b['crimes'], city_b['population'])

print(f"城市A: {rate_a:.1f} 每100,000居民的犯罪数")
print(f"城市B: {rate_b:.1f} 每100,000居民的犯罪数")
# 尽管总犯罪数较少,城市A实际上犯罪率更高!


def adjust_for_inflation(
    amount: float | pd.Series, 
    from_year: int | pd.Series, 
    to_year: int,
    country: str = 'US'
) -> float | pd.Series:
    """调整美元金额以考虑通胀。适用于标量或Series的.assign()。
    
    参数:
        amount: 要调整的值
        from_year: 金额的原始年份
        to_year: 调整的目标年份
        country: ISO 2字母国家代码(默认'US')。美国使用BLS数据通过cpi包,
                其他国家使用世界银行CPI数据(FP.CPI.TOTL指标)
    """
    if country == 'US':
        # 使用cpi包(更准确,来自BLS)
        if isinstance(from_year, pd.Series):
            return pd.Series([cpi.inflate(amt, yr, to=to_year) 
                            for amt, yr in zip(amount, from_year)], index=amount.index)
        return cpi.inflate(amount, from_year, to=to_year)
    else:
        # 使用世界银行数据于其他国家
        cpi_data = wbdata.get_dataframe(
            {'FP.CPI.TOTL': 'cpi'}, 
            country=country
        )['cpi'].to_dict()
        
        from_cpi = pd.Series(from_year).map(cpi_data) if isinstance(from_year, pd.Series) else cpi_data[from_year]
        to_cpi = cpi_data[to_year]
        return amount * (to_cpi / from_cpi)

# 用法:
# adjust_for_inflation(100, 2020, 2024)  # 默认美国
# adjust_for_inflation(100, 2020, 2024, country='GB')  # 英国
# df.assign(inf_adjust24=lambda x: adjust_for_inflation(x['amount'], x['year'], 2024, country='DE'))

# 比较跨年美元时,始终调整!

相关性 vs 因果性

## 负责任地报告相关性

### 你可以说什么
- “X和Y相关”
- “当X增加时,Y倾向于增加”
- “X较高的区域也倾向于有较高的Y”
- “X与Y关联”

### 你不能说什么(没有更多证据)
- “X导致Y”
- “X导致Y”
- “Y发生是因为X”

### 暗示因果性前要问的问题
1. 是否有合理的机制?
2. 时间线是否合理(原因在效果之前)?
3. 是否有剂量-反应关系?
4. 发现是否已被复制?
5. 是否控制了混杂变量?
6. 是否有替代解释?

### 虚假相关的红旗
- 与无关事物的极高相关性(r > 0.95)
- 变量之间没有逻辑连接
- 第三变量可能解释两者
- 小样本大小和高方差

数据可视化

图表选择指南

## 选择正确的图表

### 比较
- **条形图**: 比较类别
- **分组条形图**: 跨组比较类别
- **子弹图**: 实际 vs 目标

### 随时间变化
- **折线图**: 时间趋势
- **面积图**: 随时间累积总计
- **斜率图**: 两点间变化

### 分布
- **直方图**: 单变量分布
- **箱线图**: 跨组比较分布
- **小提琴图**: 详细的分布形状

### 关系
- **散点图**: 两变量关系
- **气泡图**: 三变量(x, y, 大小)
- **连接散点图**: 随时间的关系变化

### 组成
- **饼图**: 整体部分(几乎从不使用,最多5片,优先选择环图)
- **环图**: 整体部分
- **堆叠条形图**: 跨类别的整体部分
- **树状图**: 层次组成

### 地理
- **等值线图**: 按区域的值(使用标准化数据!)
- **点图**: 个体位置
- **比例符号图**: 位置的量级

使用Plotly Express进行探索性交互可视化

import plotly.express as px

# 为所有图表设置默认模板
px.defaults.template = 'simple_white'

def create_bar_chart(
    data: pd.DataFrame, 
    title: str, 
    source: str,
    desc: str  = '', 
    x_val: str, 
    y_val: str,
    x_lab: str | None,
    y_lab: str | None
) -> px.bar:
    """创建条形图。"""
    
    fig = px.bar(
        data, 
        x=x_val, 
        y=y_val,
        text=desc,
        title=title,
        labels={'category': (x_lab if x_lab else x_val), 'value': (y_lab if y_lab else y_val)}
    )
    
    return fig

# 示例
fig = create_bar_chart(
    data,
    title='年度小部件生产',
    source='小部件部门, 2024',
    desc='小部件部门从2014年开始大幅增加生产。',
    x_val='year',
    y_val='widgets_prod',
    x_lab='年份',
    y_label='生产单位'
)

fig.show()  # 交互显示

使用Datawrapper进行出版就绪的自动化数据可视化

import pandas as pd
import datawrapper as dw

# 认证:设置DATAWRAPPER_ACCESS_TOKEN环境变量,
# 或从文件读取并传递给create()
with open('datawrapper_api_key.txt', 'r') as f:
    api_key = f.read().strip()

# 读入您的数据
data = pd.read_csv('../data/raw/data.csv')

# 使用新OOP API创建条形图
chart = dw.BarChart(
    title='我的条形图标题',
    intro='副标题或描述文本',
    data=data,

    # 格式化选项
    value_label_format=dw.NumberFormat.ONE_DECIMAL,
    show_value_labels=True,
    value_label_alignment='left',
    sort_bars=True,  # 按值排序
    reverse_order=False,

    # 来源归属
    source_name='您的数据来源',
    source_url='https://example.com',
    byline='您的姓名',

    # 可选:自定义基本颜色
    base_color='#1d81a2'
)

# 创建和发布(使用DATAWRAPPER_ACCESS_TOKEN环境变量,或传递token)
chart.create(access_token=api_key)
chart.publish()

# 获取图表URL和嵌入代码
print(f"图表ID: {chart.chart_id}")
print(f"图表URL: https://datawrapper.dwcdn.net/{chart.chart_id}")
iframe_code = chart.get_iframe_code(responsive=True)

# 用新数据更新现有图表(用于实时更新图表)
existing_chart = dw.get_chart('YOUR_CHART_ID')  # 按ID检索
existing_chart.data = new_df  # 分配新DataFrame
existing_chart.title = '更新标题'  # 修改属性
existing_chart.update()  # 推送更改到Datawrapper
existing_chart.publish()  # 重新发布以使其生效

# 可选 — 导出图表为图像
chart.export(filepath='chart.png', width=800, height=600)

#查看图表
chart

避免误导性可视化

## 图表完整性清单

### 轴
- [ ] Y轴从零开始(对于条形图)
- [ ] 轴标签清晰
- [ ] 比例适当(未截断以夸大)
- [ ] 两个轴都标有单位

### 数据表示
- [ ] 所有数据点可见
- [ ] 颜色可区分(包括色盲)
- [ ] 比例准确
- [ ] 3D效果不扭曲感知

### 上下文
- [ ] 标题描述显示内容,而非结论
- [ ] 时间周期明确说明
- [ ] 来源引用
- [ ] 样本大小/方法论如有相关则注明
- [ ] 不确定性在适当处显示

### 诚实
- [ ] 避免挑选日期
- [ ] 异常值被解释,而非隐藏
- [ ] 双轴被证明合理(通常避免)
- [ ] 注释不误导

处理地理空间数据

地理编码数据

美国人口普查地理编码器

最适合: 仅美国地址。返回人口普查地理(区、块、FIPS代码)以及坐标—对于与人口普查人口统计数据的连接至关重要。

优点: 完全免费,无需API密钥。返回人口普查地理(州/县FIPS、区、块),让您与ACS/十年人口普查数据连接。对于标准美国地址匹配率好。

缺点: 每批限制10,000个地址。仅美国地址。比商业替代品慢。对于非标准地址(邮政信箱、农村路线、新建建筑)匹配率较低。

使用时: 您需要地理编码格式良好的美国地址,或者没有预算使用付费服务。

# pip install censusbatchgeocoder
import censusbatchgeocoder
import pandas as pd

# DataFrame必须有列: id, address, city, state, zipcode
# (state和zipcode可选但提高匹配率)

def census_geocode(
    df: pd.DataFrame,
    id_col: str = 'id',
    address_col: str = 'address',
    city_col: str = 'city',
    state_col: str = 'state',
    zipcode_col: str = 'zipcode',
    chunk_size: int = 9999
) -> pd.DataFrame:
    """
    使用美国人口普查批处理地理编码器地理编码DataFrame。
    通过分块自动处理大于10,000行的数据集。
    
    返回带有经纬度、州FIPS、县FIPS、区、块、是否匹配、是否精确、返回地址、地理编码地址的DataFrame。
    """
    # 重命名列到预期格式
    col_map = {id_col: 'id', address_col: 'address', city_col: 'city'}
    if state_col and state_col in df.columns:
        col_map[state_col] = 'state'
    if zipcode_col and zipcode_col in df.columns:
        col_map[zipcode_col] = 'zipcode'
    
    renamed_df = df.rename(columns=col_map)
    records = renamed_df.to_dict('records')
    
    # 小数据集:直接地理编码
    if len(records) <= chunk_size:
        results = censusbatchgeocoder.geocode(records)
        return pd.DataFrame(results)
    
    # 大数据集:分块处理以保持在10,000限制下
    all_results = []
    for i in range(0, len(records), chunk_size):
        chunk = records[i:i + chunk_size]
        print(f"地理编码行 {i:,} 到 {i + len(chunk):,} of {len(records):,}...")
        
        try:
            results = censusbatchgeocoder.geocode(chunk)
            all_results.extend(results)
        except Exception as e:
            print(f"从{i}开始的块错误: {e}")
            for record in chunk:
                all_results.append({**record, 'is_match': 'No_Match', 'latitude': None, 'longitude': None})
    
    return pd.DataFrame(all_results)

# 用法:
geocoded = (pd
              .read_csv('../data/raw/addresses.csv')
              .assign(id=lambda x: x.index)
              .pipe(census_geocode, 
                    id_col='id', 
                    address_col='street', 
                    city_col='city',
                    state_col='state',
                    zipcode_col='zip'))

Google Maps地理编码器

最适合: 国际地址、高匹配率,以及混乱/非标准地址格式。

优点: 优秀匹配率,即使对于格式化不良的地址。全球适用。快速可靠。返回丰富元数据(地点类型、地址组件、地点ID)。

缺点: 需要付费(免费层后每1,000请求$5)。需要API密钥和计费账户。不返回人口普查地理—您需要进行单独的空间连接。

使用时: 您需要地理编码国际地址、有混乱地址数据无法通过人口普查地理编码器匹配,或者需要最高可能匹配率并有预算。

import googlemaps
from typing import Optional

def geocode_address_google(address: str, api_key: str) -> Optional[dict]:
    """
    使用Google Maps API地理编码地址。
    需要启用Geocoding API的API密钥。
    """
    gmaps = googlemaps.Client(key=api_key)
    result = gmaps.geocode(address)
    
    if result:
        location = result[0]['geometry']['location']
        return {
            'formatted_address': result[0]['formatted_address'],
            'lat': location['lat'],
            'lon': location['lng'],
            'place_id': result[0]['place_id']
        }
    return None

# 批处理地理编码DataFrame
def batch_geocode(df: pd.DataFrame, address_col: str, api_key: str) -> pd.DataFrame:
    gmaps = googlemaps.Client(key=api_key)
    
    results = []
    for address in df[address_col]:
        try:
            result = gmaps.geocode(address)
            if result:
                loc = result[0]['geometry']['location']
                results.append({'lat': loc['lat'], 'lon': loc['lng']})
            else:
                results.append({'lat': None, 'lon': None})
        except Exception:
            results.append({'lat': None, 'lon': None})
    
    return pd.concat([df, pd.DataFrame(results)], axis=1)

Geopandas

import geopandas as gpd
import pandas as pd
from shapely.geometry import Point

# 从各种格式读取数据
gdf = gpd.read_file('data.geojson')                    # GeoJSON
gdf = gpd.read_file('data.shp')                         # Shapefile
gdf = gpd.read_file('https://example.com/data.geojson') # 从URL
gdf = gpd.read_parquet('data.parquet')                  # GeoParquet(快速!)

# 将带有经纬度的DataFrame转换为GeoDataFrame
df = pd.read_csv('locations.csv')
geometry = [Point(xy) for xy in zip(df['longitude'], df['latitude'])]
gdf = gpd.GeoDataFrame(df, geometry=geometry)

# 设置CRS(坐标系)
# EPSG:4326 = WGS84(标准纬度、经度)
gdf = gdf.set_crs('EPSG:4326')

# 转换到不同CRS(用于面积/距离计算,使用投影CRS)
gdf_projected = gdf.to_crs('EPSG:3857')  # Web Mercator,用于米制距离

# 基本空间操作

#查找形状面积
gdf['area'] = gdf_projected.geometry.area 

#查找形状中心
gdf['centroid'] = gdf.geometry.centroid

#绘制点周围1km边界
gdf['buffer_1km'] = gdf_projected.geometry.buffer(1000) #当设置为CRS 3857

# 空间连接:查找多边形内的点
points = gpd.read_file('points.geojson')
polygons = gpd.read_file('boundaries.geojson')
joined = gpd.sjoin(points, polygons, predicate='within')

# 溶解:按属性合并几何
dissolved = gdf.dissolve(by='state', aggfunc='sum')

# 导出到各种格式
gdf.to_parquet('output.parquet')          # GeoParquet(推荐)
gdf.to_file('output.geojson', driver='GeoJSON') #用于不支持GeoParquet的工具

使用.explore()lonboard和Datawrapper进行地理可视化

.explore()

最适合: 数据分析和原型设计期间的快速探索。

优点: 内置在GeoPandas中—方法可用于任何GeoDataFrame。非常适合探索性数据分析—检查数据是否正确,探索空间模式,快速迭代地图设计。

缺点: 大数据集(>100k特征)变慢。与专用映射库相比自定义有限。需要额外依赖安装。

使用时: 您在分析过程中,想快速可视化GeoDataFrame而无需切换工具。

所需依赖:

pip install folium mapclassify matplotlib
  • folium - 对于.explore()工作必需(渲染交互地图)
  • mapclassify - 使用scheme=参数进行分类时必需(例如’naturalbreaks’、‘quantiles’、‘equalinterval’)
  • matplotlib - 对于颜色映射(cmap=)支持必需
import geopandas as gpd
# folium、mapclassify和matplotlib必须安装但无需导入
# geopandas在调用.explore()时自动导入它们

# 基本交互地图(使用folium引擎)
gdf.explore()

# 自定义等值线图
# (需要mapclassify用于scheme参数)
gdf.explore(
    column='population',           # 颜色刻度列
    cmap='YlOrRd',                 # Matplotlib颜色映射
    scheme='naturalbreaks',        # 分类方案(需要mapclassify)
    k=5,                           # 箱数
    legend=True,
    tooltip=['name', 'population'],  # 悬停显示的列
    popup=True,                    # 点击显示所有列
    tiles='CartoDB positron',      # 背景瓦片
    style_kwds={'color': 'black', 'weight': 0.5}  # 边框样式
)

lonboard

最适合: 大数据集和Jupyter笔记本中的高性能可视化。

优点: 通过deck.gl的GPU加速渲染,可平滑处理数百万点。优秀交互性—平移、缩放和悬停即使大数据集也流畅。原生支持GeoArrow格式以高效数据传输。

缺点: 需要单独安装(pip install lonboard)。样式选项更技术性(RGBA数组、deck.gl约定)。

使用时: 您有大点数据集(犯罪事件、传感器读数、业务位置)或需要100k+特征的流畅交互性。

import geopandas as gpd
from lonboard import viz, Map, ScatterplotLayer, PolygonLayer

# 快速可视化(自动检测几何类型)
viz(gdf)

# 点的自定义ScatterplotLayer
layer = ScatterplotLayer.from_geopandas(
    gdf,
    get_radius=100,
    get_fill_color=[255, 0, 0, 200],  # RGBA
    pickable=True
)
m = Map(layer)
m

# 基于列的PolygonLayer
from lonboard.colormap import apply_continuous_cmap
import matplotlib.pyplot as plt

colors = apply_continuous_cmap(gdf['value'], plt.cm.viridis)
layer = PolygonLayer.from_geopandas(
    gdf,
    get_fill_color=colors,
    get_line_color=[0, 0, 0, 100],
    pickable=True
)
Map(layer)

Datawrapper

最适合: 文章和报告的出版就绪的等值线图和比例符号地图。

优点: 开箱即用的精美、专业默认。生成可嵌入、响应的iframe,适用于任何CMS。读者可交互(悬停、点击)而无需运行代码。可访问且移动友好。易于程序化更新数据以更新数据。

缺点: 需要Datawrapper账户(有免费层)。限于Datawrapper支持的分界文件—您无法引入任意几何。自定义可视化灵活性较低。

使用时: 您需要抛光的地图以用于出版。理想用于显示区域统计的等值线图(按州的失业率、按县的COVID病例、按选区的选举结果)。您的受众将在浏览器中查看地图,而非笔记本。

.explore()lonboard不同,您不传递原始几何—而是使用标准代码(FIPS、ISO等)将数据与Datawrapper的内置分界文件匹配。

import datawrapper as dw
import pandas as pd

# 读取API密钥
with open('datawrapper_api_key.txt', 'r') as f:
    api_key = f.read().strip()

# 准备数据,带有匹配Datawrapper分界文件的位置代码
# 对于美国州:使用2字母缩写或FIPS代码
# 对于国家:使用ISO 3166-1 alpha-2代码
df = pd.DataFrame({
    'state': ['AL', 'AK', 'AZ', 'AR', 'CA'],  # 州缩写
    'unemployment_rate': [4.9, 3.2, 7.1, 4.2, 5.8]
})

# 创建等值线图
chart = dw.ChoroplethMap(
    title='按州失业率',
    intro='2024年失业劳动力百分比',
    data=df,

    # 地图配置
    basemap='us-states',           # 内置美国州分界
    basemap_key='state',           # 数据中带有位置代码的列
    value_column='unemployment_rate',

    # 样式
    color_palette='YlOrRd',        # 颜色方案
    legend_title='失业率 %',

    # 归属
    source_name='劳工统计局',
    source_url='https://www.bls.gov/',
    byline='您的姓名'
)

# 创建和发布
chart.create(access_token=api_key)
chart.publish()

# 获取文章嵌入代码
iframe = chart.get_iframe_code(responsive=True)
print(f"图表URL: https://datawrapper.dwcdn.net/{chart.chart_id}")

# 用新数据更新(用于实时更新地图)
new_df = pd.DataFrame({...})  # 更新数据
existing_chart = dw.get_chart('YOUR_CHART_ID')
existing_chart.data = new_df
existing_chart.update()
existing_chart.publish()

可用Datawrapper基图包括:

  • us-statesus-countiesus-congressional-districts
  • worldeuropeafricaasia
  • 国家特定地图(例如germany-statesuk-constituencies

学习资源