コーディングのベスト プラクティス

このドキュメントでは、複雑な計算や費用のかかる Earth Engine 計算の成功率を最大化することを目的としたコーディング手法について説明します。ここで説明するメソッドは、インタラクティブ(Code Editor など)とバッチ(Export)の両方の計算に適用されますが、通常、長時間実行される計算はバッチシステムで実行する必要があります。

クライアントの関数とオブジェクトをサーバーの関数とオブジェクトと混同しない

Earth Engine サーバー オブジェクトは、コンストラクタが ee で始まるオブジェクトです(例: ee.Imageee.Reducer)。このようなオブジェクト上の関数はサーバー関数です。この方法で作成されていないオブジェクトはクライアント オブジェクトです。クライアント オブジェクトは、コードエディタ(MapChart など)または JavaScript 言語(DateMath[]{} など)から取得できます。

意図しない動作を回避するため、こちらこちらこちらで説明されているように、スクリプトでクライアント関数とサーバー関数を混在させないでください。Earth Engine のクライアントとサーバーの詳細については、こちらのページまたはこちらのチュートリアルをご覧ください。次の例は、クライアント機能とサーバー機能を混在させる危険性を示しています。

エラー - このコードはご利用いただけません。

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Won't work.
for(var i=0; i<table.size(); i++) {
  print('No!');
}

エラーがわかりますか?table.size() はサーバー オブジェクトのサーバー メソッドであり、< 条件などのクライアントサイド機能では使用できません。

Code Editor の ui オブジェクトとメソッドはクライアントサイドであるため、UI の設定で for ループを使用する場合があります。(Earth Engine でユーザー インターフェースを作成する方法の詳細)。次に例を示します。

UI の設定にクライアント関数を使用する。

var panel = ui.Panel();
for(var i=1; i<8; i++) {
  panel.widgets().set(i, ui.Button('button ' + i))
}
print(panel);

逆に、map() はサーバー関数であり、map() に渡された関数内ではクライアント機能は機能しません。次に例を示します。

エラー - このコードはご利用いただけません。

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Error:
var foobar = table.map(function(f) {
  print(f); // Can't use a client function here.
  // Can't Export, either.
});

コレクション内のすべての要素に対して何かを行うには、コレクションの関数を map() し、プロパティを set() します。

map()set() を使用してください。

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
print(table.first());

// Do something to every element of a collection.
var withMoreProperties = table.map(function(f) {
  // Set a property.
  return f.set('area_sq_meters', f.area())
});
print(withMoreProperties.first());

計算されたプロパティまたは既存のプロパティに基づいてコレクションを filter() し、結果を print() することもできます。5,000 個を超える要素を含むコレクションは出力できません。「5, 000 個を超える要素の蓄積後にコレクション クエリが中止されました」というエラーが発生した場合は、出力前にコレクションを filter() または limit() します。

リストへの変換を不必要に回避する

Earth Engine のコレクションは、コレクションを List 型または Array 型に変換することで破棄される最適化を使用して処理されます。コレクション要素へのランダム アクセスが必要な場合(コレクションの i 番目の要素を取得する必要がある場合)を除き、コレクションにフィルタを使用して個々のコレクション要素にアクセスします。次の例は、コレクション内の要素にアクセスするための型変換(推奨されない)とフィルタリング(推奨)の違いを示しています。

リストに変換しすぎないでください。

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!!
var list = table.toList(table.size());
print(list.get(13)); // User memory limit exceeded.

コレクションをリストに変換すると、不要なエラーが簡単に発生する可能性があることに注意してください。より安全な方法は、filter() を使用することです。

filter() を使用してください。

print(table.filter(ee.Filter.eq('country_na', 'Niger')).first());

分析のできるだけ早い段階でフィルタを使用する必要があります。

ee.Algorithms.If() を避ける

特にマッピングされた関数で、ee.Algorithms.If() を使用して分岐ロジックを実装しないでください。次の例に示すように、ee.Algorithms.If() はメモリを大量に消費する可能性があるため、推奨されません。

If() は使用しないでください。

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!
var veryBad = table.map(function(f) {
  return ee.Algorithms.If({
    condition: ee.String(f.get('country_na')).compareTo('Chad').gt(0),
    trueCase: f,      // Do something.
    falseCase: null   // Do something else.
  });
}, true);
print(veryBad); // User memory limit exceeded.

// If() may evaluate both the true and false cases.

map() の 2 番目の引数は true です。つまり、マッピングされた関数が null を返す可能性があり、結果のコレクションで null が破棄されます。これは(If() なしで)便利な場合もありますが、ここではフィルタを使用するのが最も簡単なソリューションです。

filter() を使用してください。

print(table.filter(ee.Filter.eq('country_na', 'Chad')));

このチュートリアルで説明したように、フィルタを使用する関数型プログラミング アプローチは、コレクションの一部の要素に 1 つのロジックを適用し、コレクションの他の要素に別のロジックを適用する正しい方法です。

reproject() を避ける

絶対に必要な場合を除き、再投影は使用しないでください。reproject() を使用する理由の 1 つは、Code Editor の計算を特定のスケールで強制的に実行し、目的のスケールで分析結果を調べることです。次の例では、ホットピクセルのパッチが計算され、各パッチ内のピクセル数が計算されます。サンプルを実行し、いずれかのパッチをクリックします。ピクセル数は、再投影されたデータと再投影されていないデータで異なります。

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.405, 37.786]);
Map.centerObject(sf, 13);

// A reason to reproject - counting pixels and exploring interactively.
var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.00341802).add(149);  // Apply scale factors.
Map.addLayer(image, {bands: ['ST_B10'], min: 280, max: 317}, 'image');

var hotspots = image.select('ST_B10').gt(317)
  .selfMask()
  .rename('hotspots');
var objectSize = hotspots.connectedPixelCount(256);

Map.addLayer(objectSize, {min: 1, max: 256}, 'Size No Reproject', false);

// Beware of reproject!  Don't zoom out on reprojected data.
var reprojected = objectSize.reproject(hotspots.projection());
Map.addLayer(reprojected, {min: 1, max: 256}, 'Size Reproject', false);

差異が生じる理由は、分析のスケールが Code Editor のズームレベルによって設定されるためです。reproject() を呼び出すと、Code Editor ではなく計算のスケールを設定できます。reproject() は、こちらのドキュメントに記載されている理由から、慎重に使用してください。

フィルタと select() を先に

通常、コレクションで他の処理を行う前に、時間、場所、メタデータなどで入力コレクションをフィルタします。より選択的なフィルタを、より選択性の低いフィルタの前に適用します。空間フィルタや時間フィルタは、より選択的であることが多いです。たとえば、select()filter()map()に適用されます。

var images = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED');
var sf = ee.Geometry.Point([-122.463, 37.768]);

// Expensive function to reduce the neighborhood of an image.
var reduceFunction = function(image) {
  return image.reduceNeighborhood({
    reducer: ee.Reducer.mean(),
    kernel: ee.Kernel.square(4)
  });
};

var bands = ['B4', 'B3', 'B2'];
// Select and filter first!
var reasonableComputation = images
    .select(bands)
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-02-01')
    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 1))
    .aside(print) // Useful for debugging.
    .map(reduceFunction)
    .reduce('mean')
    .rename(bands);
var viz = {bands: bands, min: 0, max: 10000};
Map.addLayer(reasonableComputation, viz, 'reasonableComputation');

mask() ではなく updateMask() を使用する

updateMask()mask() の違いは、前者は引数(新しいマスク)と既存のイメージマスクの論理 and() を実行するのに対し、後者はイメージマスクを引数に置き換えるだけです。mask()後者の危険性は、意図せずピクセルのマスクを解除する可能性があることです。この例では、標高が 300 メートル以下のピクセルをマスクすることを目的としています。ご覧のとおり(ズームアウト)、mask() を使用すると、対象の画像に属していない多くのピクセルがマスク解除されます。

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.40554461769182, 37.786807309873716]);
var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

Map.centerObject(sf, 7);

var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.0000275).subtract(0.2);  // Apply scale factors.
var vis = {bands: ['SR_B4', 'SR_B3', 'SR_B2'], min: 0, max: 0.3};
Map.addLayer(image, vis, 'image', false);

var mask = aw3d30.select('AVE').gt(300);
Map.addLayer(mask, {}, 'mask', false);

// NO!  Don't do this!
var badMask = image.mask(mask);
Map.addLayer(badMask, vis, 'badMask');

var goodMask = image.updateMask(mask);
Map.addLayer(goodMask, vis, 'goodMask', false);

リデューサーを組み合わせる

単一の入力(画像領域など)から複数の統計情報(平均や標準偏差など)が必要な場合は、レジューサーを組み合わせた方が効率的です。たとえば、画像統計情報を取得するには、次のようにレジューサーを結合します。

var image = ee.Image(
  'COPERNICUS/S2_HARMONIZED/20150821T111616_20160314T094808_T30UWU');

// Get mean and SD in every band by combining reducers.
var stats = image.reduceRegion({
  reducer: ee.Reducer.mean().combine({
    reducer2: ee.Reducer.stdDev(),
    sharedInputs: true
  }),
  geometry: ee.Geometry.Rectangle([-2.15, 48.55, -1.83, 48.72]),
  scale: 10,
  bestEffort: true // Use maxPixels if you care about scale.
});

print(stats);

// Extract means and SDs to images.
var meansImage = stats.toImage().select('.*_mean');
var sdsImage = stats.toImage().select('.*_stdDev');

この例では、平均レジューサーが標準偏差レジューサーと結合され、sharedInputs が true になっているため、入力ピクセルを 1 回だけ処理します。出力ディクショナリでは、バンド名にレジューサーの名前が追加されます。平均と標準偏差の画像を取得するには(入力画像を正規化するなど)、値を画像に変換し、正規表現を使用して平均と標準偏差を個別に抽出します(例を参照)。

Export

Code Editor で「ユーザーのメモリ上限を超えました」または「計算がタイムアウトしました」というエラーが発生する計算でも、Export を使用すると同じ計算が成功することがあります。これは、バッチシステム(エクスポートが実行される場所)で実行する場合、タイムアウトが長く、許容されるメモリのフットプリントが大きいためです。(デバッグ ドキュメントに記載されているように、最初に試す方法が他にもあります)。前の例を続け、辞書がエラーを返したとします。次のようにすると結果を取得できます。

var link = '86836482971a35a5e735a17e93c23272';
Export.table.toDrive({
  collection: ee.FeatureCollection([ee.Feature(null, stats)]),
  description: 'exported_stats_demo_' + link,
  fileFormat: 'CSV'
});

再現性を高めるために、リンクはアセット名に埋め込まれています。また、toAsset をエクスポートする場合は、ジオメトリを指定する必要があります。ジオメトリは、計算が簡単で小さい画像の重心など、任意の値にできます。(必要でない場合は複雑なジオメトリを使用しないでください)。

Export を使用して「計算がタイムアウトしました」と「同時実行される集計が多すぎます」を解決する例については、デバッグ ページをご覧ください。エクスポート全般の詳細については、こちらのドキュメントをご覧ください。

クリップする必要がない場合は、clip() を使用しないでください。

clip() を不必要に使用すると、計算時間が増加します。分析に必要な場合を除き、clip() は使用しないでください。不明な場合は、クリップしないでください。クリップの不適切な使用例:

入力を不必要にクリップしないでください。

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var belgium = table.filter(ee.Filter.eq('country_na', 'Belgium')).first();

// Do NOT clip unless you need to.
var unnecessaryClip = l8sr
    .select('SR_B4')                          // Good.
    .filterBounds(belgium.geometry())         // Good.
    .filterDate('2019-01-01', '2019-04-01')   // Good.
    .map(function(image) {
      return image.clip(belgium.geometry());  // NO! Bad! Not necessary.
    })
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(),
      scale: 30,
      maxPixels: 1e10,
    });
print(unnecessaryClip);

入力画像のクリッピングは、reduceRegion() 呼び出しで領域が指定されているため、完全にスキップできます。

出力のリージョンを指定してください。

var noClipNeeded = l8sr
    .select('SR_B4')                           // Good.
    .filterBounds(belgium.geometry())          // Good.
    .filterDate('2019-01-01', '2019-12-31') // Good.
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(), // Geometry is specified here.
      scale: 30,
      maxPixels: 1e10,
    });
print(noClipNeeded);

この計算がタイムアウトした場合は、この例のように Export します。

複雑なコレクションでクリップする必要がある場合は、clipToCollection() を使用します。

クリップが本当に必要な場合、クリップに使用するジオメトリがコレクションにある場合は、clipToCollection() を使用します。

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME',
                         'Tropical & Subtropical Moist Broadleaf Forests'));
Map.addLayer(complexCollection, {}, 'complexCollection');

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay', false);

大規模なコレクションや複雑なコレクションでは、featureCollection.geometry()featureCollection.union() を使用しないでください。メモリ使用量が増加する可能性があります。

複雑なコレクションをレジューサーのリージョンとして使用しない

複数のリージョンからの入力を FeatureCollection にプールするように、空間的な集約を行う必要がある場合は、featureCollection.geometry()geometry 入力としてレジューサーに指定しないでください。代わりに、clipToCollection() と、コレクションの境界を包含するのに十分な大きさのリージョンを使用します。次に例を示します。

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME', 'Tropical & Subtropical Moist Broadleaf Forests'));

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay');

var reduction = clippedTheRightWay.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: ee.Geometry.Rectangle({
    coords: [-179.9, -50, 179.9, 50],  // Almost global.
    geodesic: false
  }),
  scale: 30,
  maxPixels: 1e12
});
print(reduction); // If this times out, export it.

ゼロ以外の errorMargin を使用する

計算に時間がかかりそうなジオメトリ演算の場合は、計算に必要な精度を考慮して、可能な限り大きな誤差マージンを使用します。誤差マージンは、ジオメトリのオペレーション(再投影など)中に許容される最大許容誤差(メートル単位)を指定します。エラーマージンを小さく指定すると、座標を使用してジオメトリを高密度化する必要が生じることがあります。これはメモリを大量に消費する可能性があります。計算には、できるだけ大きな誤差マージンを指定することをおすすめします。

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');

var complexCollection = ecoregions.limit(10);
Map.centerObject(complexCollection);
Map.addLayer(complexCollection);

var expensiveOps = complexCollection.map(function(f) {
  return f.buffer(10000, 200).bounds(200);
});
Map.addLayer(expensiveOps, {}, 'expensiveOps');

reduceToVectors() で非常に小さいスケールは使用しない

ラスターからベクターに変換する場合は、適切なスケールを使用します。非常に小さいスケールを指定すると、計算コストが大幅に増加する可能性があります。必要な精度に合わせて、スケールをできるだけ高く設定します。たとえば、世界中の陸地を表すポリゴンを取得するには:

var etopo = ee.Image('NOAA/NGDC/ETOPO1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});
Map.addLayer(almostGlobal, {}, 'almostGlobal');

var vectors = bounds.selfMask().reduceToVectors({
  reducer: ee.Reducer.countEvery(),
  geometry: almostGlobal,
  // Set the scale to the maximum possible given
  // the required precision of the computation.
  scale: 50000,
});
Map.addLayer(vectors, {}, 'vectors');

上の例では、グローバルな削減に非測地線ポリゴンが使用されています。

reduceRegions()reduceToVectors() を使用しない

reduceToVectors() から返された FeatureCollectionreduceRegions() の入力として使用しないでください。代わりに、reduceToVectors() を呼び出す前に、減らすバンドを追加します。

var etopo = ee.Image('NOAA/NGDC/ETOPO1');
var mod11a1 = ee.ImageCollection('MODIS/006/MOD11A1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});

var lst = mod11a1.first().select(0);

var means = bounds.selfMask().addBands(lst).reduceToVectors({
  reducer: ee.Reducer.mean(),
  geometry: almostGlobal,
  scale: 1000,
  maxPixels: 1e10
});
print(means.limit(10));

別の画像のゾーン内で 1 つの画像のピクセルを減らす方法としては、reduceConnectedCommponents()グループ化レデューサなどもあります。

近傍オペレーションに fastDistanceTransform() を使用する

畳み込み演算によっては、fastDistanceTransform()reduceNeighborhood()convolve() よりも効率的である場合があります。たとえば、バイナリ入力の侵食や拡大を行うには、次のようにします。

var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

// Make a simple binary layer from a threshold on elevation.
var mask = aw3d30.select('AVE').gt(300);
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(mask, {}, 'mask');

// Distance in pixel units.
var distance = mask.fastDistanceTransform().sqrt();
// Threshold on distance (three pixels) for a dilation.
var dilation = distance.lt(3);
Map.addLayer(dilation, {}, 'dilation');

// Do the reverse for an erosion.
var notDistance = mask.not().fastDistanceTransform().sqrt();
var erosion = notDistance.gt(3);
Map.addLayer(erosion, {}, 'erosion');

reduceNeighborhood() の最適化を使用する

畳み込みを実行する必要があるが fastDistanceTransform() を使用できない場合は、reduceNeighborhood() の最適化を使用します。

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);

var bands = ['B4', 'B3', 'B2'];

var optimizedConvolution = composite.select(bands).reduceNeighborhood({
  reducer: ee.Reducer.mean(),
  kernel: ee.Kernel.square(3),
  optimization: 'boxcar' // Suitable optimization for mean.
}).rename(bands);

var viz = {bands: bands, min: 0, max: 72};
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(composite, viz, 'composite');
Map.addLayer(optimizedConvolution, viz, 'optimizedConvolution');

必要以上のデータをサンプリングしない

トレーニング データセットのサイズを不必要に増やす必要はありません。トレーニング データの量を増やすことは、状況によっては効果的な ML 戦略ですが、精度が向上しないまま計算コストが増加することもあります。(トレーニング データセットのサイズを増やすタイミングについては、こちらのリファレンスをご覧ください)。次の例は、トレーニング データを過度にリクエストすると、「計算された値が大きすぎる」という恐ろしいエラーが発生する仕組みを示しています。

サンプルデータが多すぎないようにしてください。

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// No!  Not necessary.  Don't do this:
labels = labels.map(function(f) { return f.buffer(100000, 1000); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var training = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

var classifier = ee.Classifier.smileCart().train({
  features: training,
  classProperty: 'landcover',
  inputProperties: bands
});
print(classifier.explain()); // Computed value is too large

適切なアプローチは、まず適度な量のデータから始めて、分類器のハイパーパラメータを調整し、目的の精度を達成できるかどうかを判断することです。

ハイパーパラメータを調整する

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// Increase the data a little bit, possibly introducing noise.
labels = labels.map(function(f) { return f.buffer(100, 10); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var data = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

// Add a column of uniform random numbers called 'random'.
data = data.randomColumn();

// Partition into training and testing.
var training = data.filter(ee.Filter.lt('random', 0.5));
var testing = data.filter(ee.Filter.gte('random', 0.5));

// Tune the minLeafPopulation parameter.
var minLeafPops = ee.List.sequence(1, 10);

var accuracies = minLeafPops.map(function(p) {
  var classifier = ee.Classifier.smileCart({minLeafPopulation: p})
      .train({
        features: training,
        classProperty: 'landcover',
        inputProperties: bands
      });

  return testing
    .classify(classifier)
    .errorMatrix('landcover', 'classification')
    .accuracy();
});

print(ui.Chart.array.values({
  array: ee.Array(accuracies),
  axis: 0,
  xLabels: minLeafPops
}));

この例では、分類器はすでに非常に正確であるため、チューニングを行う必要はありません。必要な精度を維持しながら、可能な限り小さいツリー(最大の minLeafPopulation)を選択することをおすすめします。

Export 中間結果

比較的複雑な計算画像からサンプルを取得することを目的としているとします。多くの場合、画像を Export toAsset() して、エクスポートされた画像を読み込み、サンプリングする方が効率的です。次に例を示します。

var image = ee.Image('UMD/hansen/global_forest_change_2018_v1_6');
var geometry = ee.Geometry.Polygon(
    [[[-76.64069800085349, 5.511777325802095],
      [-76.64069800085349, -20.483938229362376],
      [-35.15632300085349, -20.483938229362376],
      [-35.15632300085349, 5.511777325802095]]], null, false);
var testRegion = ee.Geometry.Polygon(
    [[[-48.86726050085349, -3.0475996402515717],
      [-48.86726050085349, -3.9248707849303295],
      [-47.46101050085349, -3.9248707849303295],
      [-47.46101050085349, -3.0475996402515717]]], null, false);

// Forest loss in 2016, to stratify a sample.
var loss = image.select('lossyear');
var loss16 = loss.eq(16).rename('loss16');

// Scales and masks Landsat 8 surface reflectance images.
function prepSrL8(image) {
  var qaMask = image.select('QA_PIXEL').bitwiseAnd(parseInt('11111', 2)).eq(0);
  var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2);
  var thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0);
  return image.addBands(opticalBands, null, true)
      .addBands(thermalBands, null, true)
      .updateMask(qaMask);
}

var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
    .map(prepSrL8);

// Create two annual cloud-free composites.
var composite1 = collection.filterDate('2015-01-01', '2015-12-31').median();
var composite2 = collection.filterDate('2017-01-01', '2017-12-31').median();

// We want a strtatified sample of this stack.
var stack = composite1.addBands(composite2)
    .float(); // Export the smallest size possible.

// Export the image.  This block is commented because the export is complete.
/*
var link = '0b8023b0af6c1b0ac7b5be649b54db06'
var desc = 'Logistic_regression_stack_' + link;
Export.image.toAsset({
  image: stack,
  description: desc,
  assetId: desc,
  region: geometry,
  scale: 30,
  maxPixels: 1e10
})
*/

// Load the exported image.
var exportedStack = ee.Image(
  'projects/google/Logistic_regression_stack_0b8023b0af6c1b0ac7b5be649b54db06');

// Take a very small sample first, to debug.
var testSample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 1,
  classBand: 'loss16',
  region: testRegion,
  scale: 30,
  geometries: true
});
print(testSample); // Check this in the console.

// Take a large sample.
var sample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 10000,
  classBand: 'loss16',
  region: geometry,
  scale: 30,
});

// Export the large sample...

この例では、画像は浮動小数点数としてエクスポートされます。どうしても必要な場合を除き、倍精度でエクスポートしないでください。このエクスポートを行う際は、再現性を高めるために、Code Editor のリンク(エクスポートの直前に取得)がファイル名に埋め込まれることに注意してください。

エクスポートが完了したら、アセットを再読み込みして、アセットからのサンプリングに進みます。デバッグのために、非常に小さなテスト領域で非常に小さなサンプルが最初に実行されます。成功したことを確認したら、より大きなサンプルを取得してエクスポートします。このような大規模なサンプルは通常、エクスポートする必要があります。このようなサンプルは、最初にエクスポートせずに、インタラクティブに(print() などを使用して)使用したり、使用したり(分類システムへの入力など)しないでください。

結合とマップフィルタ

時間、場所、またはメタデータ プロパティに基づいてコレクションを結合するとします。一般的に、結合を使用すると最も効率的に処理できます。次の例では、Landsat 8 コレクションと Sentinel-2 コレクションの空間時間結合を行います。

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var joined = ee.Join.saveAll('landsat').apply({
  primary: s2,
  secondary: l8,
  condition: ee.Filter.and(
    ee.Filter.maxDifference({
      difference: 1000 * 60 * 60 * 24, // One day in milliseconds
      leftField: 'system:time_start',
      rightField: 'system:time_start',
    }),
    ee.Filter.intersects({
      leftField: '.geo',
      rightField: '.geo',
    })
  )
});
print(joined);

最初に結合(必要に応じて Export)を試す必要がありますが、特に非常に大きなコレクションの場合は、map() 内の filter() も効果的です。

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var mappedFilter = s2.map(function(image) {
  var date = image.date();
  var landsat = l8
      .filterBounds(image.geometry())
      .filterDate(date.advance(-1, 'day'), date.advance(1, 'day'));
  // Return the input image with matching scenes in a property.
  return image.set({
    landsat: landsat,
    size: landsat.size()
  });
}).filter(ee.Filter.gt('size', 0));
print(mappedFilter);

reduceRegion()reduceRegions() と for ループ

非常に大きいまたは複雑な FeatureCollection を入力として reduceRegions() を呼び出すと、「計算値が大きすぎる」という恐ろしいエラーが発生する可能性があります。考えられる解決策の 1 つは、代わりに FeatureCollectionreduceRegion() をマッピングすることです。別の解決策として、(驚くべきことに)for ループを使用する方法もあります。こちらこちらこちらで説明されているように、Earth Engine ではこれを強くおすすめしませんが、reduceRegion() を for ループに実装して大規模な減算を実行できます。

ImageCollection 内の各画像の FeatureCollection 内の各特徴のピクセルの平均(または任意の統計情報)を取得することを目的としています。次の例では、前述の 3 つのアプローチを比較します。

// Table of countries.
var countriesTable = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017");
// Time series of images.
var mod13a1 = ee.ImageCollection("MODIS/006/MOD13A1");

// MODIS vegetation indices (always use the most recent version).
var band = 'NDVI';
var imagery = mod13a1.select(band);

// Option 1: reduceRegions()
var testTable = countriesTable.limit(1); // Do this outside map()s and loops.
var data = imagery.map(function(image) {
  return image.reduceRegions({
    collection: testTable,
    reducer: ee.Reducer.mean(),
    scale: 500
  }).map(function(f) {
    return f.set({
      time: image.date().millis(),
      date: image.date().format()
    });
  });
}).flatten();
print(data.first());

// Option 2: mapped reduceRegion()
var data = countriesTable.map(function(feature) {
  return imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  });
}).flatten();
print(data.first());

// Option 3: for-loop (WATCH OUT!)
var size = countriesTable.size();
// print(size); // 312
var countriesList = countriesTable.toList(1); // Adjust size.
var data = ee.FeatureCollection([]); // Empty table.
for (var j=0; j<1; j++) { // Adjust size.
  var feature = ee.Feature(countriesList.get(j));
  // Convert ImageCollection > FeatureCollection
  var fc = ee.FeatureCollection(imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  }));
  data = data.merge(fc);
}
print(data.first());

デバッグ目的で、各コレクションの first() が印刷されます。完全な結果をインタラクティブに利用できるとは思わないでください。Export する必要があります。また、for ループは最後の手段としてのみ、慎重に使用してください。最後に、for ループでは、入力コレクションのサイズを手動で取得し、適切な場所にハードコードする必要があります。これらの内容が不明な場合は、for ループを使用しないでください。

時間的に近い隣接するデータに対して前方差分を使用する

時系列で並べられた ImageCollection(時系列)があり、各画像を前の(または次の)画像と比較するとします。この目的に iterate() を使用するのではなく、配列ベースの順方向差分を使用する方が効率的です。次の例では、この方法を使用して Sentinel-2 コレクションの重複を除去します。ここで、重複は同じ日付の画像として定義されます。

var sentinel2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED');
var sf = ee.Geometry.Point([-122.47555371521855, 37.76884708376152]);
var s2 = sentinel2
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-12-31');

var withDoys = s2.map(function(image) {
  var ndvi = image.normalizedDifference(['B4', 'B8']).rename('ndvi');
  var date = image.date();
  var doy = date.getRelative('day', 'year');
  var time = image.metadata('system:time_start');
  var doyImage = ee.Image(doy)
      .rename('doy')
      .int();
  return ndvi.addBands(doyImage).addBands(time)
      .clip(image.geometry()); // Appropriate use of clip.
});

var array = withDoys.toArray();
var timeAxis = 0;
var bandAxis = 1;

var dedupe = function(array) {
  var time = array.arraySlice(bandAxis, -1);
  var sorted = array.arraySort(time);
  var doy = sorted.arraySlice(bandAxis, -2, -1);
  var left = doy.arraySlice(timeAxis, 1);
  var right = doy.arraySlice(timeAxis, 0, -1);
  var mask = ee.Image(ee.Array([[1]]))
      .arrayCat(left.neq(right), timeAxis);
  return array.arrayMask(mask);
};

var deduped = dedupe(array);

// Inspect these outputs to confirm that duplicates have been removed.
print(array.reduceRegion('first', sf, 10));
print(deduped.reduceRegion('first', sf, 10));

印刷されたコレクションを調べて、重複が削除されていることを確認します。