In [1]:
import pandas as pd
import geopandas as gpd
import numpy as np
import os
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = 'Microsoft YaHei'
import rasterio
import json
from shapely.geometry import Point
import contextily as ctx
In [ ]:
youbike_sta = pd.read_csv('./dataset/youbike_station.csv')
youbike_sta = gpd.GeoDataFrame(youbike_sta,
    geometry=gpd.points_from_xy(youbike_sta['lon'], youbike_sta['lat']),crs='EPSG:4326').to_crs('EPSG:3826')

spot = gpd.read_file('./dataset/Scenic_spot_C_f.shp').to_crs('EPSG:3826')
spot = spot[spot['Region'] == '臺北市']

school = pd.read_csv('./dataset/112年_各級學校名錄_240828.csv')
school = school[school['縣市別'] == '臺北市']
school = gpd.GeoDataFrame(school, geometry=gpd.points_from_xy(school['經度'], school['緯度']), crs='EPSG:4326').to_crs('EPSG:3826')

FF = gpd.read_file('./dataset/TPE_FastFood.shp', encoding='big5', crs='EPSG:3826')
TPE = gpd.read_file('./dataset/Taipei_Vill.shp', encoding='big5', crs='EPSG:3826')


tif = rasterio.open('./dataset/taipei_s2.tif')

r = tif.read(3).astype(float)
nir = tif.read(7).astype(float)
NDVI = (nir - r) / (nir + r + 1e-8)
In [10]:
def classify_schools(school_gdf):
    """分類學校層級"""
    # 使用學校級別欄位進行分類
    school_gdf['school_type'] = 'others'
    
    # 國小相關
    elementary_mask = school_gdf['學校級別'].isin(['國民小學', '附設國民小學'])
    school_gdf.loc[elementary_mask, 'school_type'] = 'elementary'
    
    # 國中高中相關  
    middle_high_mask = school_gdf['學校級別'].isin(['國民中學', '附設國民中學', '高級中等學校'])
    school_gdf.loc[middle_high_mask, 'school_type'] = 'middle_high'
    
    # 大學及專科相關
    university_mask = school_gdf['學校級別'].isin(['大學', '專科學校', '學院'])
    school_gdf.loc[university_mask, 'school_type'] = 'university'
    
    return school_gdf

def create_fastfood_restriction_zones(school_gdf, ff_gdf):
    """建立速食店管制區域"""
    # 轉換至投影坐標系進行緩衝區計算
    school_proj = school_gdf.to_crs('EPSG:3826')
    ff_proj = ff_gdf.to_crs('EPSG:3826')
    
    # 建立不同學校類型的緩衝區
    elementary = school_proj[school_proj['school_type'] == 'elementary']
    middle_high = school_proj[school_proj['school_type'] == 'middle_high']
    
    all_buffers = []
    
    # 國小500公尺緩衝區
    if len(elementary) > 0:
        elementary_buffer = elementary.geometry.buffer(500)
        elem_gdf = gpd.GeoDataFrame({
            'school_type': ['elementary'] * len(elementary_buffer),
            'buffer_distance': [500] * len(elementary_buffer)
        }, geometry=elementary_buffer, crs='EPSG:3826')
        all_buffers.append(elem_gdf)
    
    # 國高中300公尺緩衝區
    if len(middle_high) > 0:
        middle_high_buffer = middle_high.geometry.buffer(300)
        mh_gdf = gpd.GeoDataFrame({
            'school_type': ['middle_high'] * len(middle_high_buffer),
            'buffer_distance': [300] * len(middle_high_buffer)
        }, geometry=middle_high_buffer, crs='EPSG:3826')
        all_buffers.append(mh_gdf)
    
    # 合併所有管制區域
    if all_buffers:
        all_buffers = pd.concat(all_buffers, ignore_index=True)
    else:
        # 如果沒有緩衝區,建立空的 GeoDataFrame
        all_buffers = gpd.GeoDataFrame({
            'school_type': [],
            'buffer_distance': []
        }, geometry=[], crs='EPSG:3826')
    
    # 分析違規速食店
    violation_analysis = {}
    for school_type in ['elementary', 'middle_high']:
        buffers = all_buffers[all_buffers['school_type'] == school_type]
        if len(buffers) > 0:
            buffer_union = buffers.geometry.unary_union
            # 確保 buffer_union 不是 None
            if buffer_union is not None:
                # 找出緩衝區內的速食店
                violations = ff_proj[ff_proj.geometry.within(buffer_union)]
            else:
                violations = gpd.GeoDataFrame(columns=ff_proj.columns, crs=ff_proj.crs)
        else:
            violations = gpd.GeoDataFrame(columns=ff_proj.columns, crs=ff_proj.crs)
            
        violation_analysis[school_type] = {
            'count': len(violations),
            'locations': violations
        }
    
    return all_buffers.to_crs('EPSG:4326'), violation_analysis

def analyze_green_accessibility(school_gdf, spot_gdf, youbike_gdf):
    """分析綠地可及性"""
    school_proj = school_gdf.to_crs('EPSG:3826')
    spot_proj = spot_gdf.to_crs('EPSG:3826')
    youbike_proj = youbike_gdf.to_crs('EPSG:3826')
    
    accessibility_results = []
    
    for idx, school in school_proj.iterrows():
        school_point = school.geometry
        
        # 步行可及性分析 (100, 300, 500公尺)
        walking_distances = [100, 300, 500]
        walking_access = {}
        
        for dist in walking_distances:
            buffer = school_point.buffer(dist)
            accessible_spots = spot_proj[spot_proj.geometry.within(buffer)]
            walking_access[f'walk_{dist}m'] = len(accessible_spots)
        
        # 騎行可及性分析 (1000, 2000公尺至YouBike + 300公尺步行至綠地)
        cycling_access = {}
        for bike_dist in [1000, 2000]:
            bike_buffer = school_point.buffer(bike_dist)
            accessible_youbikes = youbike_proj[youbike_proj.geometry.within(bike_buffer)]
            
            # 從YouBike站點300公尺內的綠地
            green_from_bike = 0
            for _, bike in accessible_youbikes.iterrows():
                green_buffer = bike.geometry.buffer(300)
                nearby_greens = spot_proj[spot_proj.geometry.within(green_buffer)]
                green_from_bike += len(nearby_greens)
            
            cycling_access[f'bike_{bike_dist}m'] = green_from_bike
        
        # 整合結果
        result = {
            'school_id': idx,
            'school_name': school['學校名稱'],
            'school_type': school['school_type'],
            **walking_access,
            **cycling_access
        }
        accessibility_results.append(result)
    
    return pd.DataFrame(accessibility_results)

def extract_ndvi_for_schools(school_gdf, ndvi_array, transform):
    """提取學校周邊NDVI值"""
    # 確保學校資料與NDVI使用相同的坐標系
    school_proj = school_gdf.to_crs(tif.crs)  # 使用tif檔案的坐標系
    
    ndvi_results = []
    for idx, school in school_proj.iterrows():
        # 提取學校位置的NDVI值
        try:
            # 轉換座標至raster索引
            row, col = rasterio.transform.rowcol(transform, school.geometry.x, school.geometry.y)
            
            if 0 <= row < ndvi_array.shape[0] and 0 <= col < ndvi_array.shape[1]:
                ndvi_value = ndvi_array[row, col]
            else:
                ndvi_value = np.nan
                
            ndvi_results.append({
                'school_id': idx,
                'ndvi_value': ndvi_value
            })
        except Exception as e:
            print(f"Error processing school {idx}: {e}")
            ndvi_results.append({
                'school_id': idx, 
                'ndvi_value': np.nan
            })
    
    return pd.DataFrame(ndvi_results)

def create_health_zones(accessibility_df, violation_analysis, ndvi_df):
    """建立健康區域評分系統 - 分析所有學校"""
    
    # 合併所有分析結果
    health_df = accessibility_df.copy()
    health_df = health_df.merge(ndvi_df, on='school_id', how='left')
    
    # 計算健康指標分數 (0-100)
    health_df['green_access_score'] = (
        health_df['walk_100m'] * 3 + 
        health_df['walk_300m'] * 2 + 
        health_df['walk_500m'] * 1 +
        health_df['bike_1000m'] * 0.5 +
        health_df['bike_2000m'] * 0.3
    ) / 10 * 100
    
    # NDVI分數標準化 (處理NaN值)
    ndvi_min = health_df['ndvi_value'].min()
    ndvi_max = health_df['ndvi_value'].max()
    
    if pd.notna(ndvi_min) and pd.notna(ndvi_max) and ndvi_max != ndvi_min:
        health_df['ndvi_score'] = ((health_df['ndvi_value'] - ndvi_min) / 
                                  (ndvi_max - ndvi_min) * 100)
    else:
        health_df['ndvi_score'] = 50  # 預設分數
    
    # 速食店風險分數計算 - 針對所有學校
    health_df['fastfood_risk_score'] = 100  # 預設滿分
    
    # 對有速食店管制的學校類型進行扣分
    for school_type in ['elementary', 'middle_high']:
        if school_type in violation_analysis:
            violation_count = violation_analysis[school_type]['count']
            # 對該類型學校進行風險評估
            school_mask = health_df['school_type'] == school_type
            if violation_count > 0:
                # 根據違規速食店數量調整風險分數
                risk_penalty = min(violation_count * 10, 50)  # 每家違規店扣10分,最多扣50分
                health_df.loc[school_mask, 'fastfood_risk_score'] -= risk_penalty
    
    # 綜合健康指標 - 對所有學校適用
    health_df['overall_health_score'] = (
        health_df['green_access_score'] * 0.4 +
        health_df['ndvi_score'] * 0.3 +
        health_df['fastfood_risk_score'] * 0.3
    )
    
    # 分級 - 適用於所有學校
    health_df['health_zone'] = pd.cut(health_df['overall_health_score'], 
                                     bins=[0, 30, 60, 100], 
                                     labels=['需改善', '一般', '優良'])
    
    return health_df

# === 3. 執行分析 ===

# 檢查學校分類結果
school = classify_schools(school)
print("學校分類結果:")
print(school['school_type'].value_counts())
print("\n各級學校數量:")
print(school['學校級別'].value_counts())

# 分類學校
school = classify_schools(school)

# 速食店管制分析
restriction_zones, violations = create_fastfood_restriction_zones(school, FF)

# 綠地可及性分析
accessibility_results = analyze_green_accessibility(school, spot, youbike_sta)

# NDVI分析
ndvi_results = extract_ndvi_for_schools(school, NDVI, tif.transform)

# 健康區域劃分
health_zones = create_health_zones(accessibility_results, violations, ndvi_results)

print("分析執行完成!")
print(f"分析學校數量: {len(school)}")
print(f"國小數量: {len(school[school['school_type'] == 'elementary'])}")
print(f"國高中數量: {len(school[school['school_type'] == 'middle_high'])}")



# === 4. 視覺化分析結果 ===

fig, axes = plt.subplots(2, 2, figsize=(20, 16))

# 轉換所有資料到相同的坐標系統進行視覺化
TPE_viz = TPE.to_crs('EPSG:4326')
FF_viz = FF.to_crs('EPSG:4326')
school_viz = school.to_crs('EPSG:4326')
spot_viz = spot.to_crs('EPSG:4326')

# 安全的轉換 restriction_zones
if len(restriction_zones) > 0 and not restriction_zones.is_empty.all():
    restriction_zones_viz = restriction_zones.copy()
    # 確保幾何有效性
    restriction_zones_viz = restriction_zones_viz[~restriction_zones_viz.is_empty]
    restriction_zones_viz = restriction_zones_viz[restriction_zones_viz.geometry.is_valid]
    
    if len(restriction_zones_viz) > 0:
        # 檢查邊界值是否有效
        bounds = restriction_zones_viz.total_bounds
        if np.isfinite(bounds).all():
            pass  # 邊界值正常
        else:
            restriction_zones_viz = gpd.GeoDataFrame()  # 清空無效資料
    else:
        restriction_zones_viz = gpd.GeoDataFrame()
else:
    restriction_zones_viz = gpd.GeoDataFrame()

# 4.1 速食店管制區域圖
ax1 = axes[0, 0]
TPE_viz.plot(ax=ax1, color='lightgray', alpha=0.5)

# 安全地繪製管制區域
if len(restriction_zones_viz) > 0:
    try:
        restriction_zones_viz.plot(ax=ax1, color='red', alpha=0.3)
    except (ValueError, AttributeError) as e:
        print(f"警告:無法繪製管制區域 - {e}")

FF_viz.plot(ax=ax1, color='orange', markersize=5, label='速食店')
school_viz.plot(ax=ax1, color='blue', markersize=3, label='學校')
ax1.set_title('學校周邊速食店管制區域')
ax1.legend()
ax1.set_aspect('equal')  # 手動設定比例
ax1.axis('off')

# 4.2 綠地可及性熱圖
ax2 = axes[0, 1]
try:
    school_with_health = school_viz.merge(health_zones, left_index=True, right_on='school_id')
    school_with_health = gpd.GeoDataFrame(school_with_health, geometry=school_with_health.geometry, crs='EPSG:4326')
    
    TPE_viz.plot(ax=ax2, color='lightgray', alpha=0.3)
    if 'green_access_score' in school_with_health.columns:
        # 過濾掉無效值
        valid_data = school_with_health.dropna(subset=['green_access_score'])
        if len(valid_data) > 0:
            valid_data.plot(ax=ax2, column='green_access_score', 
                           cmap='Greens', markersize=20, legend=True, 
                           legend_kwds={'shrink': 0.5})
    spot_viz.plot(ax=ax2, color='green', markersize=1, alpha=0.7)
except Exception as e:
    print(f"警告:繪製綠地可及性圖時發生錯誤 - {e}")
    TPE_viz.plot(ax=ax2, color='lightgray', alpha=0.3)

ax2.set_title('學校綠地可及性分析')
ax2.set_aspect('equal')
ax2.axis('off')

# 4.3 NDVI分布圖
ax3 = axes[1, 0]
try:
    TPE_viz.plot(ax=ax3, color='lightgray', alpha=0.3)
    if 'school_with_health' in locals() and 'ndvi_score' in school_with_health.columns:
        # 過濾掉無效值
        valid_ndvi = school_with_health.dropna(subset=['ndvi_score'])
        if len(valid_ndvi) > 0:
            valid_ndvi.plot(ax=ax3, column='ndvi_score', 
                           cmap='RdYlGn', markersize=20, legend=True,
                           legend_kwds={'shrink': 0.5})
except Exception as e:
    print(f"警告:繪製NDVI圖時發生錯誤 - {e}")
    TPE_viz.plot(ax=ax3, color='lightgray', alpha=0.3)

ax3.set_title('學校周邊綠度分析(NDVI)')
ax3.set_aspect('equal')
ax3.axis('off')

# 4.4 綜合健康區域圖
ax4 = axes[1, 1]
try:
    TPE_viz.plot(ax=ax4, color='lightgray', alpha=0.3)
    colors = {'需改善': 'red', '一般': 'yellow', '優良': 'green'}
    
    if 'school_with_health' in locals() and 'health_zone' in health_zones.columns:
        for zone in health_zones['health_zone'].unique():
            if pd.notna(zone):
                subset = school_with_health[school_with_health['health_zone'] == zone]
                if len(subset) > 0:
                    subset.plot(ax=ax4, color=colors.get(zone, 'gray'), markersize=20, 
                               label=f'{zone}區域', alpha=0.7)
except Exception as e:
    print(f"警告:繪製健康區域圖時發生錯誤 - {e}")
    TPE_viz.plot(ax=ax4, color='lightgray', alpha=0.3)

ax4.set_title('校園周邊綜合健康區域分級')
ax4.legend()
ax4.set_aspect('equal')
ax4.axis('off')

plt.tight_layout()
plt.show()

# ...existing code...

# === 5. 統計分析報告 ===

print("\n=== 台北市校園周邊健康生活分析報告 ===\n")

print("1. 速食店管制區域分析:")
print(f"   - 國小500m緩衝區內違規速食店: {violations['elementary']['count']}家")
print(f"   - 國高中300m緩衝區內違規速食店: {violations['middle_high']['count']}家")
print("   - 大學院校:無速食店管制限制")

print("\n2. 綠地可及性統計:")
accessibility_stats = accessibility_results.describe()
print(f"   - 平均步行500m內綠地數量: {accessibility_stats.loc['mean', 'walk_500m']:.2f}")
print(f"   - 平均騎行1000m+步行300m可達綠地數量: {accessibility_stats.loc['mean', 'bike_1000m']:.2f}")

print("\n3. 健康區域分布(涵蓋所有學校類型):")
if 'health_zone' in health_zones.columns:
    zone_counts = health_zones['health_zone'].value_counts()
    for zone, count in zone_counts.items():
        percentage = count / len(health_zones) * 100
        print(f"   - {zone}區域: {count}所學校 ({percentage:.1f}%)")
    
    # 各學校類型的健康區域分布
    print("\n各學校類型健康區域分布:")
    school_type_mapping = {
        'elementary': '國小',
        'middle_high': '國高中',
        'university': '大學院校',
        'others': '其他'
    }
    
    for school_type in health_zones['school_type'].unique():
        if pd.notna(school_type):
            type_name = school_type_mapping.get(school_type, school_type)
            type_data = health_zones[health_zones['school_type'] == school_type]
            type_zones = type_data['health_zone'].value_counts()
            print(f"   {type_name}:")
            for zone, count in type_zones.items():
                percentage = count / len(type_data) * 100
                print(f"     - {zone}: {count}所 ({percentage:.1f}%)")

print("\n4. 政策建議:")
print("   - 國小及國高中:優先改善'需改善'區域的綠地建設和YouBike站點配置")
print("   - 國小及國高中:加強管制違規速食店的設立")
print("   - 大學院校:著重綠地可及性和環境品質提升")
print("   - 在綠地不足區域增設小型公園或綠化空間")
print("   - 建立校園健康環境監測機制")

# 在進階分析中增加各學校類型的詳細統計
print("\n=== 進階分析結果 ===")

# 7.1 學校類型與健康區域交叉分析(包含所有類型)
if 'health_zone' in health_zones.columns:
    print("\n7.1 學校類型與健康區域交叉分析:")
    cross_analysis = pd.crosstab(health_zones['school_type'], health_zones['health_zone'])
    # 重命名索引以便閱讀
    cross_analysis.index = cross_analysis.index.map(school_type_mapping)
    print(cross_analysis)

# 7.2 綠地可及性詳細統計(所有學校類型)
print("\n7.2 綠地可及性詳細統計:")
green_stats = accessibility_results.groupby('school_type').agg({
    'walk_100m': ['mean', 'std'],
    'walk_300m': ['mean', 'std'], 
    'walk_500m': ['mean', 'std'],
    'bike_1000m': ['mean', 'std'],
    'bike_2000m': ['mean', 'std']
}).round(2)

# 重命名索引
green_stats.index = green_stats.index.map(school_type_mapping)
print(green_stats)

# 7.4 各學校類型健康指標平均值比較
print("\n7.4 各學校類型健康指標平均值:")
health_by_type = health_zones.groupby('school_type').agg({
    'green_access_score': 'mean',
    'ndvi_score': 'mean',
    'fastfood_risk_score': 'mean',
    'overall_health_score': 'mean'
}).round(2)

# 重命名索引
health_by_type.index = health_by_type.index.map(school_type_mapping)
print(health_by_type)
學校分類結果:
school_type
middle_high    160
elementary     154
others          62
Name: count, dtype: int64

各級學校數量:
學校級別
國民小學             147
高級中等學校            70
國民中學              59
附設國民中學            31
國中小補校             28
大專校院              24
附設國民小學             7
特殊教育學校             4
宗教研修學院             2
空大及大專校院附設進修學校      2
軍警大專校院             2
Name: count, dtype: int64
C:\Users\Chang Tzu Cheng\AppData\Local\Temp\ipykernel_38544\1768818812.py:65: FutureWarning: `unary_union` returned None due to all-None GeoSeries. In future, `unary_union` will return 'GEOMETRYCOLLECTION EMPTY' instead.
  buffer_union = buffers.geometry.unary_union
C:\Users\Chang Tzu Cheng\AppData\Local\Temp\ipykernel_38544\1768818812.py:65: FutureWarning: `unary_union` returned None due to all-None GeoSeries. In future, `unary_union` will return 'GEOMETRYCOLLECTION EMPTY' instead.
  buffer_union = buffers.geometry.unary_union
分析執行完成!
分析學校數量: 376
國小數量: 154
國高中數量: 160
No description has been provided for this image
=== 台北市校園周邊健康生活分析報告 ===

1. 速食店管制區域分析:
   - 國小500m緩衝區內違規速食店: 0家
   - 國高中300m緩衝區內違規速食店: 0家
   - 大學院校:無速食店管制限制

2. 綠地可及性統計:
   - 平均步行500m內綠地數量: 2.59
   - 平均騎行1000m+步行300m可達綠地數量: 40.53

3. 健康區域分布(涵蓋所有學校類型):
   - 優良區域: 83所學校 (22.1%)
   - 一般區域: 21所學校 (5.6%)
   - 需改善區域: 0所學校 (0.0%)

各學校類型健康區域分布:
   其他:
     - 優良: 18所 (29.0%)
     - 一般: 1所 (1.6%)
     - 需改善: 0所 (0.0%)
   國高中:
     - 優良: 35所 (21.9%)
     - 一般: 8所 (5.0%)
     - 需改善: 0所 (0.0%)
   國小:
     - 優良: 30所 (19.5%)
     - 一般: 12所 (7.8%)
     - 需改善: 0所 (0.0%)

4. 政策建議:
   - 國小及國高中:優先改善'需改善'區域的綠地建設和YouBike站點配置
   - 國小及國高中:加強管制違規速食店的設立
   - 大學院校:著重綠地可及性和環境品質提升
   - 在綠地不足區域增設小型公園或綠化空間
   - 建立校園健康環境監測機制

=== 進階分析結果 ===

7.1 學校類型與健康區域交叉分析:
health_zone  一般  優良
school_type        
國小           12  30
國高中           8  35
其他            1  18

7.2 綠地可及性詳細統計:
            walk_100m       walk_300m       walk_500m       bike_1000m         \
                 mean   std      mean   std      mean   std       mean    std   
school_type                                                                     
國小               0.06  0.25      0.95  1.61      2.49  3.61      41.16  48.75   
國高中              0.04  0.21      0.78  1.25      2.52  2.99      39.24  46.39   
其他               0.13  0.38      1.02  1.58      3.02  3.71      42.31  47.31   

            bike_2000m          
                  mean     std  
school_type                     
國小              131.44  128.71  
國高中             142.72  134.75  
其他              138.85  135.15  

7.4 各學校類型健康指標平均值:
             green_access_score  ndvi_score  fastfood_risk_score  \
school_type                                                        
國小                       645.90       53.77                100.0   
國高中                      666.38       48.04                100.0   
其他                       682.45       51.81                100.0   

             overall_health_score  
school_type                        
國小                         304.49  
國高中                        310.96  
其他                         318.52  
In [ ]:
 
No description has been provided for this image