Spatial Analysis in Ecology

生态统计学中的空间数据

Qin Li

SEES, ECNU

Oct 19, 2026

生态学中的空间分析

  • 重要性
    • 揭示空间格局与生态过程的关系(尺度效应)
    • 支持环境保护与管理决策(保护生物学)
    • 量化生态系统服务的空间分布(如水资源、碳储量)
    • 评估生态响应与气候变化(全球变化)

生态学中的空间分析

  • 应用场景
    • 多尺度生物多样性分析
    • 生态网络与景观生态学
    • 物种分布模型
    • 空间自相关与生态推断

生态学中的空间尺度

  • 微观尺度:单个栖息地中的个体分布(点格局);
  • 中观尺度:区域内的生态过程(如种群分布变迁);
  • 宏观尺度:全球生物多样性格局或生态系统服务;

生态学中的空间尺度

  • 微观尺度 / 中观尺度/ 宏观尺度

scale difference

空间数据

  • 基于物种出现与不出现的数据
  • 植物区系的空间格局
center

空间数据的可视化

  • 矢量数据 vector
    • 点 point
    • 线 line
    • 面 polygon
  • 栅格数据 raster
    • 栅格 grid/pixel/cell

矢量数据

矢量数据

栅格数据

  • 海拔 DEM (digital elevation model)

栅格数据

  • DEM的分辨率:10km, 1km, 90m, 30m

栅格数据

  • 气候数据:10, 5, 2.5 minutes, 30 seconds

Bioclimate @ WorldClim

Spatial Data in R

  • 点数据

Spatial Data in R

  • 线数据

Spatial Data in R

  • 面数据

Spatial Data in R

  • R code
library(sf)

# point data
plot(st_point(c(0,0)))
plot(st_point(c(0.5,0.5)), add = T)

# line data
plot(st_linestring(matrix(c(0,0,1,1,2,0), ncol=2)) )

# polygon data
plot(st_polygon(list(rbind(c(0,0), c(1,0), c(1,1), c(0,1), c(0,0)))))

Spatial Data in R

  • 怎么画出一个五角星?

Spatial Data in R

  • 五角星: R code
library(sf)  # For handling spatial data
library(ggplot2)  # For plotting

# 五角星的polygon的顶点的数据
star_coords <- rbind(
  c(0, 1), c(0.2245, 0.309), c(0.9511, 0.309), c(0.3633, -0.118),
  c(0.5878, -0.809), c(0, -0.382), c(-0.5878, -0.809), c(-0.3633, -0.118),
  c(-0.9511, 0.309), c(-0.2245, 0.309), c(0, 1))

star_polygon <- st_polygon(list(star_coords))
star_sf <- st_sf(geometry = st_sfc(star_polygon, crs = 4326))

# 画图
plot(st_geometry(star_sf), col = "lightblue", border = "black", main = "Star Polygon")

Spatial Data in R

  • 查看矢量文件
> sh_polygon # 输入数据文件在R中的名字

Simple feature collection with 2 features and 0 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 0 ymin: 0 xmax: 4 ymax: 4
CRS:           EPSG:4326
                        geometry
1 POLYGON ((0 0, 0 2, 2 2, 2 ...
2 POLYGON ((2 2, 2 4, 4 4, 4 ...

Spatial Data in R

  • 栅格文件

Spatial Data in R

  • 栅格文件
library(sf)  # For handling spatial data
library(ggplot2)  # For plotting
library(raster)  # For raster data manipulation

# Create a simple raster object
nrows <- 10  # Number of rows
ncols <- 10  # Number of columns
r <- raster(nrows = nrows, ncols = ncols, xmn = 0, xmx = 10, ymn = 0, ymx = 10)

# Assign random values to raster cells
set.seed(42)  # For reproducibility
values(r) <- runif(ncell(r), min = 0, max = 1)
raster_data <- as.data.frame(r, xy = TRUE)  # Convert raster to a data frame
raster_data[1:2,]  # Display the first two rows
    x   y     layer
1 0.5 9.5 0.9148060
2 1.5 9.5 0.9370754

Spatial Data in R

  • 绘制栅格文件

Spatial Data in R

  • 查看栅格文件
> r # 栅格文件的名字

class      : RasterLayer 
dimensions : 10, 10, 100  (nrow, ncol, ncell)
resolution : 1, 1  (x, y)
extent     : 0, 10, 0, 10  (xmin, xmax, ymin, ymax)
crs        : +proj=longlat +datum=WGS84 +ellps=WGS84 +towgs84=0,0,0 
source     : memory
names      : layer 
values     : 0.0002388966, 0.9888917  (min, max)

数据采集

  • 生物数据:物种出现点(或栅格化)
    • 标本数据: GBIF(汇总)
    • 野外采集或观察数据
    • 物种名录(分辨率相对较低)
  • 环境数据

分析处理工具

  • QGIS:https://www.qgis.org
    • 免费GIS软件,用于地图制作与分析;
  • R:开源语言,提供多种空间分析工具;

数据文件格式

  • 矢量数据
    • 文件格式:
      • Shapefile .shp(点/线/面均可)
      • 坐标点表格 .csv(点)
    • R中读取
      • read_sf(), st_read()(兼容多种格式)
      • read.csv()

数据文件格式

  • R中生成并存储(写入函数)
    • st_write() (适用于矢量数据,例如 .shp, .geojson, .gpkg)
    • writeRaster() (适用于栅格数据,例如 .tif, .asc)
st_write(sf_object, "output.shp")
writeRaster(raster_object, "output.tif", format = "GTiff")

数据文件格式

  • 栅格数据
    • 文件格式:
      • GeoTIFF .tif, .tiff
      • ASCII 格式 .asc
    • R中读取
      • raster::raster()
      • terra::rast()

格式转换

library(sf)
species_data <- data.frame(
  species = rep("Example_species", 10),
  # Random latitudes between -90 and 90
  latitude = runif(10, min = -90, max = 90),
  # Random longitudes between -180 and 180
  longitude = runif(10, min = -180, max = 180))

#write.csv(species_data, file = "species_points.csv",row.names=F) # save data
#species_data <- read.csv("species_points.csv") # read data

sf_data <- st_as_sf(species_data, coords = c("lon", "lat"), crs = 4326)
plot(sf_data)

坐标系统

  • 经纬度坐标 (Geographic Coordinate System)
    • 常见坐标系:WGS84 (EPSG:4326),为全球通用标准;
      • 不适合计算距离或面积(非等距、非等积);
  • 大地坐标 (Projected Coordinate System)
    • 通过投影将地球表面(3D)映射到平面(2D),单位为米或英尺;
    • 常见投影:
      • UTM (Universal Transverse Mercator): 按区域分割的投影,适合局部分析;
      • Albers Equal Area: 用于面积分析(如分布区大小);

为什么需要投影转换

  • 不同数据可能采用不同的坐标系;
    • 直接叠加可能导致错误;
  • 距离、面积等计算依赖于正确的投影;
library(sf)

# point shapefile (WGS84)
point <- st_as_sf(data.frame(lon = 120, lat = 30), 
                  coords = c("lon", "lat"), crs = 4326)
print(point)  # 查看经纬度坐标

# UTM projection (适合中国地区:带号50N)
point_utm <- st_transform(point, crs = 32650)
print(point_utm)  # 查看投影后的坐标

# crs = 32650 -- >
# crs = "+proj=utm +zone=50 +datum=WGS84 +units=m +no_defs"

投影系统参数

  • Coordinate Reference Systems (CRS)
    • crs = 32650
PROJCS["WGS_1984_UTM_Zone_50N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",117.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]

投影系统参数

  • crs = 32650

投影系统参数

  • Spatial Reference List

投影对比

  • 中国的大陆行政区划

投影对比

library(sf)  # For handling shapefiles and spatial data
library(ggplot2)  # For visualization
library(gridExtra)

# 1. 读取中国国家的 shapefile 数据
china <- st_read("spatial_data/CHN_adm0-3/CHN_adm0.shp", quiet = T)
st_crs(china) # 查看原始投影信息

# 2. 投影转换: 适合中国区域的投影 (Albers Equal Area)
albers_crs <- "+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 
              +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
china_albers <- st_transform(china, crs = albers_crs)
st_crs(china_albers) # 查看转换后的投影信息

# 3. 可视化比较投影前后的数据
map_before <- ggplot() +
  geom_sf(data = china, fill = "lightblue", color = "black", alpha = 0.6) +
  ggtitle("原始投影") +
  theme_minimal()

map_after <- ggplot() +
  geom_sf(data = china_albers, fill = "lightgreen", color = "black", alpha = 0.6) +
  ggtitle("Albers Equal Area 投影") +
  theme_minimal()

grid.arrange(map_before, map_after)

投影对比

library(sf)  # For handling shapefiles and spatial data
library(ggplot2)  # For visualization

# 3. 可视化比较投影前后的数据
map_before <- ggplot() +
  geom_sf(data = china, fill = "lightblue", color = "black", alpha = 0.6) +
  ggtitle("原始投影") +
  theme_minimal()

map_after <- ggplot() +
  geom_sf(data = china_albers, fill = "lightgreen", color = "black", alpha = 0.6) +
  ggtitle("Albers Equal Area 投影") +
  theme_minimal()

grid.arrange(map_before, map_after)

栅格文件的投影转换

  • 空间文件均可做投影转换
library(sf)
library(raster)  # For handling raster data

global_raster <- raster("bio01.tif") # 读取全球气候栅格文件
st_crs(global_raster) # 投影信息(通常为 WGS84)

# 定义目标投影 (Robinson)
robinson_crs <- "+proj=robin +lon_0=0 +datum=WGS84 +units=m +no_defs"

# 投影转换
global_raster_robinson <- projectRaster(global_raster, 
                      crs = robinson_crs, method = "bilinear")
crs(global_raster_robinson) # 查看转换后的投影信息

par(mfrow = c(1, 2))  # 将绘图窗口分为两部分
plot(global_raster, main = "原始投影 (WGS84)")
plot(global_raster_robinson, main = "Robinson 投影")

# 5. 保存转换后的栅格文件
writeRaster(global_raster_robinson, "global_climate_robinson.tif", format = "GTiff", overwrite = TRUE)

栅格文件的投影转换

  • 定义目标投影
albers_crs <- "+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 
              +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"

robinson_crs <- "+proj=robin +lon_0=0 +datum=WGS84 +units=m +no_defs"

空间数据的基本处理

  • 矢量文件的子集选择
shp_subset <- shp_polygon %>% filter(field1 > 60)
shp_subset

st_write(shp_subset, "shp_subset", driver = "ESRI Shapefile", delete_layer = TRUE)
# 'delete_layer = TRUE' overwrite a shapefile with the same name

空间数据的基本处理

  • 栅格文件
bio01 <- rast("spatial_data/bio01.tif") 
bio01
class(bio01)
names(bio01)
summary(bio01)
ext(bio01)
bio01_update <- bio01
bio01_update[ bio01_update < 0 ] = NA
plot(bio01_update)

bio01_update2 <- bio01 / 10
plot(bio01_update2)

空间数据的基本处理

  • 根据矢量数据从栅格文件中提取数据
library(terra)  # For raster and vector data handling
library(sf)     # For shapefile handling
bio01 <- rast("spatial_data/bio01.tif") 

# 根据随机点提取数据
ranSamps <- st_sample(st_as_sfc(st_bbox(bio01)), size=30, type = "random")
ranSamps <- st_as_sf(ranSamps) #Convert to simple feature collection
ranSamps
plot(bio01)
plot(ranSamps, add = T)

空间数据处理

  • 矢量数据叠加

空间数据处理

  • 矢量数据叠加
library(sf)
china_shp <- st_read("spatial_data/CHN_adm0-3/CHN_adm1.shp", quiet = TRUE)
head(as.data.frame(china_shp))
prov4_shp <- china_shp[china_shp$NAME_1 %in% c("Anhui","Jiangsu","Shanghai","Zhejiang"), ]
prov4_shp <- as(prov4_shp, "Spatial")
prov4_shp2 <- vect(prov4_shp)
prov4_shp2$value <- c("AH", "JS", "SH", "ZJ")
xy <- centroids(prov4_shp2)
plot(prov4_shp2, col=2:5)
points(xy, cex=4, pch=20, col='white')
text(prov4_shp2, 'value', cex=0.8)

空间数据处理

  • 矢量数据裁剪

空间数据处理

  • 矢量数据裁剪
library(sf)
china_shp <- st_read("spatial_data/CHN_adm0-3/CHN_adm1.shp", quiet = TRUE)
ginkgo_point <- read.csv("spatial_data/ginkgo_xy.csv")
ginkgo_point <- ginkgo_point %>% filter(complete.cases(.))
ginkgo_sf <- st_as_sf(ginkgo_point, coords = c("decimalLongitude", "decimalLatitude"), crs = 4326)

# 裁剪
ginkgo_china <- st_crop(ginkgo_sf, china_shp)
plot(ginkgo_china)

空间数据处理

  • 矢量数据缓冲区(buffer)

空间数据处理

  • 矢量数据缓冲区(buffer)
library(sf)
china_shp <- st_read("spatial_data/CHN_adm0-3/CHN_adm1.shp", quiet = TRUE)
ginkgo_point <- read.csv("spatial_data/ginkgo_xy.csv")
ginkgo_point <- ginkgo_point %>% filter(complete.cases(.))
ginkgo_sf <- st_as_sf(ginkgo_point, coords = c("decimalLongitude", "decimalLatitude"), crs = 4326)

#ginkgo_china <- st_crop(ginkgo_sf, china_shp)
#plot(ginkgo_china)
ginkgo_china <- st_intersection(ginkgo_sf, china_shp)

# need to make projection, convert to unit - m
albers_crs <- "+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 
              +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
ginkgo_albers <- st_transform(ginkgo_china, crs = albers_crs)
# plot(st_geometry(ginkgo_albers))
china_albers <- st_transform(china_shp, crs = albers_crs)

# generate buffer around each point
buff_point <- st_buffer(ginkgo_albers, dist = 100000) # 10km-radius

ggplot() +
  geom_sf(data = china_albers, fill = "lightblue", color = "black") + 
  geom_sf(data = buff_point, fill = "darkgray", color = "black") + 
  geom_sf(data = ginkgo_albers, aes(color = "Points"), size = 1) + 
  theme_minimal() +
  theme(legend.position = "bottom")

空间数据处理

  • 根据点数据生成面(物种出现点 –> 分布区)

空间数据处理

  • 根据点数据生成面(物种出现点 –> 分布区)
# join the POINT geometries into a single MULTIPOINT geometry
ginkgo_union <- st_union(ginkgo_albers)
ginkgo_range <- st_convex_hull(ginkgo_union)
ginkgo_range_buff <- st_buffer(ginkgo_range, dist = 100000) # 10km-radius

ggplot() +
    geom_sf(data = ginkgo_range_buff, fill = NA, color = "black") + 
    geom_sf(data = ginkgo_range, fill = "lightblue", color = "lightblue") + 
    geom_sf(data = ginkgo_albers, aes(color = "Points"), size = 1) + 
    theme_minimal()

空间数据处理

  • 栅格化矢量数据

空间数据处理

  • 栅格化矢量数据

空间数据处理

  • 栅格化矢量数据
ginkgo_vect <- vect(ginkgo_albers)
# Define the extent and resolution of the raster
raster_extent <- ext(ginkgo_vect)  # Automatically match extent of points
resolution <- 50000  # Cell size (in map units, e.g., meters)

# Create an empty raster template
raster_template <- rast(ext = raster_extent, resolution = resolution, crs = crs(ginkgo_vect))

# Rasterize points: Count the number of points per cell
ginkgo_raster <- rasterize(ginkgo_vect, raster_template, fun = "length")

# Plot the raster
plot(ginkgo_raster, main = "Ginkgo Rasterized: Point Density")
plot(st_geometry(ginkgo_albers), add = TRUE, pch = 1, cex = 0.5)

空间数据处理

  • 栅格化矢量数据

空间数据处理

  • 栅格化矢量数据
r_df <- as.data.frame(ginkgo_raster, xy = TRUE)  # Extract cell values and coordinates

# Plot using ggplot2
ggplot() +
    # Add raster layer
    geom_tile(data = r_df, aes(x = x, y = y, fill = V1_length)) +
    scale_fill_viridis_c(name = "Value") +  # Color scale for raster values
    coord_equal() +  # Equal aspect ratio
    # Add spatial points
    geom_sf(data = ginkgo_albers, aes(geometry = geometry), 
            color = "red", size = 1, pch = 1) +
    # Add titles and themes
    labs(title = "Raster with Points Overlay",
         x = "Lon", y = "Lat") +
    theme_minimal()

空间数据的基本处理

  • 根据矢量数据裁剪栅格文件
library(terra)  # For raster and vector data handling
library(sf)     # For shapefile handling

bio01 <- rast("spatial_data/bio01.tif") 
china_shp <- st_read("spatial_data/CHN_adm0-3/CHN_adm0.shp", quiet = T)

# 需检查两类型的文件是否有相同投影系统
crs(bio01)
st_crs(china_shp)

# 裁剪/切割
cropped_raster <- crop(bio01, china_shp)  # 按范围裁剪
bio01_china <- mask(cropped_raster, vect(china_shp))  # 按多边形边界裁剪

par(mfrow = c(1, 2))  # 分为两个绘图区域
plot(bio01, main = "Original Raster")  # 原始栅格
plot(bio01_china, main = "Cropped Raster")  # 裁剪后的栅格

# 保存新的栅格文件
writeRaster(bio01_china, "bio01_cropped.tif", format = "GTiff", overwrite = TRUE)

物种出现点及其环境因子

  • 物种出现点
# ref: https://rpubs.com/jonesor/gettinggbifdata
library(rgbif)
x <- name_backbone("species example")
gbif_download <- occ_download(
  pred("taxonKey", x$usageKey),
  pred("country", "DE"), 
  pred("hasGeospatialIssue", FALSE), 
  pred("hasCoordinate", TRUE), 
  pred("occurrenceStatus", "PRESENT"), 
  pred_gte("year", 1900), 
  user = user, pwd = pwd, email = email, # your account info
  format = "SIMPLE_CSV")

物种出现点及其环境因子

  • 物种出现点:银杏 (Ginkgo biloba)
library(sf)
library(tidyverse)
library(rgbif)
#occ_data <- occ_search(taxonKey = name_backbone("Ginkgo biloba")$usageKey)
#head(occ_data$data)
#write.csv(occ_data$data, "spatial_data/ginkgo.csv", row.names = F, quote = F)
#ginkgo_point <- occ_data$data %>%
#    select(scientificName,decimalLatitude,decimalLongitude)
#write.csv(ginkgo_point, "spatial_data/ginkgo_xy.csv", row.names = F, quote = F)
ginkgo_point <- read.csv("spatial_data/ginkgo_xy.csv")
ginkgo_point <- ginkgo_point %>% filter(complete.cases(.))
ginkgo_sf <- st_as_sf(ginkgo_point, coords = c("decimalLongitude", "decimalLatitude"), crs = 4326)


library(ggplot2)
library(rnaturalearth)
library(rnaturalearthdata)

# 加载世界地图数据(中等分辨率)
world <- ne_countries(scale = "medium", returnclass = "sf")

ggplot() +
  geom_sf(data = world, fill = "lightblue", color = "black") +  # 世界地图
  geom_sf(data = ginkgo_sf, aes(color = "Points"), size = 1) +  # 点数据
  scale_color_manual(values = "red", labels = "Occurrence") +  # 自定义颜色
  theme_minimal() +
  ggtitle("World Map with Localities of Gingko") +
  theme(legend.position = "bottom")  # 调整图例位置

物种出现点及其环境因子

  • 物种出现点:银杏 (Ginkgo biloba)
library(sf)
library(tidyverse)
library(rgbif)

# download data
occ_data <- occ_search(taxonKey = name_backbone("Ginkgo biloba")$usageKey)
head(occ_data$data)
#write.csv(occ_data$data, "spatial_data/ginkgo.csv", row.names = F, quote = F)
ginkgo_point <- occ_data$data %>%
    select(scientificName,decimalLatitude,decimalLongitude)
#write.csv(ginkgo_point, "spatial_data/ginkgo_xy.csv", row.names = F, quote = F)
ginkgo_point <- read.csv("spatial_data/ginkgo_xy.csv")
ginkgo_point <- ginkgo_point %>% filter(complete.cases(.))
ginkgo_sf <- st_as_sf(ginkgo_point, coords = c("decimalLongitude", "decimalLatitude"), crs = 4326)

物种出现点及其环境因子

  • 物种出现点:银杏 (Ginkgo biloba)
library(sf)
library(tidyverse)
library(rgbif)

# download data
# ...

library(ggplot2)
library(rnaturalearth)
library(rnaturalearthdata)

# 加载世界地图数据(中等分辨率)
world <- ne_countries(scale = "medium", returnclass = "sf")

ggplot() +
  geom_sf(data = world, fill = "lightblue", color = "black") +  # 世界地图
  geom_sf(data = ginkgo_sf, aes(color = "Points"), size = 1) +  # 点数据
  scale_color_manual(values = "red", labels = "Occurrence") +  # 自定义颜色
  theme_minimal() +
  ggtitle("World Map with Localities of Gingko") +
  theme(legend.position = "bottom")  # 调整图例位置

物种出现点及其环境因子

  • 根据点数据从环境栅格文件中提取数据
    • terra::extract(r, sf_point)

物种出现点及其环境因子

  • 根据点数据从环境栅格文件中提取数据
# extract bio01 values for points
bio01_values <- terra::extract(bio01_albers_cropped, ginkgo_albers)

ginkgo_sf_bio01 <- ginkgo_albers %>% mutate(values = bio01_values)
ginkgo_bio01_df <- st_coordinates(ginkgo_sf_bio01) %>%  # Extract x and y coordinates
    as.data.frame() %>%  # Convert to a data frame
    cbind(st_drop_geometry(ginkgo_sf_bio01))  # Bind other attributes (drop geometry column)

ginkgo_bio01_df2 <- st_coordinates(ginkgo_sf_bio01) %>%  # Extract x and y coordinates
    as.data.frame() %>%  # Convert to a data frame
    mutate(bio01 = bio01_values$bio01)

ginkgo_bio01_df2[1:3,]
        X       Y    bio01
1 1110452 3538280 15.59324
2 1142990 3451406 16.18330
3 1262705 3438210 15.93047

空间自相关

  • 越相邻的单元越相似:Moran’s I

    Moran I test under randomisation

data:  ginkgo_bio01  
weights: weights    

Moran I statistic standard deviate = 7.6875, p-value = 7.5e-15
alternative hypothesis: greater
sample estimates:
Moran I statistic       Expectation          Variance 
     0.1479903515     -0.0204081633      0.0004798459 

空间自相关

  • 越相邻的单元越相似:Moran’s I
library(spdep)  # For spatial autocorrelation
ginkgo_xy <- st_coordinates(ginkgo_albers)
ginkgo_bio01 <- bio01_values$bio01

# Create a neighbors list based on the coordinates
neighbors <- dnearneigh(ginkgo_xy, 0, 1000000)  # Specify a distance threshold (1000 km in projected CRS)
weights <- nb2listw(neighbors, style = "W")  # Create spatial weights

# Calculate Moran's I for bio01
moran_test <- moran.test(ginkgo_bio01, weights)

# Print Moran's I test result
print(moran_test)

空间自相关

  • 越相邻的单元越相似:Moran’s I
# Visualization: Plot spatial autocorrelation
moran_plot <- moran.plot(ginkgo_bio01, listw = weights, main = "Moran's I Plot")

作业

  • 提取中国范围内的温度和降水的栅格文件
    • 先分别做投影转换,再裁减
    • 保存为tif文件
  • 提取物种出现点对应的气候数据
    • 选择一种在中国境内分布的物种
    • 在GBIF上下载经纬度坐标
    • 先投影转换,再提取,保存为csv文件