Подгонка многолетних данных NDVI методом наименьших квадратов

Описание процедуры и вспомогательные функции

Есть временные ряды NDVI, собранные за несколько лет. В данных есть шумы, обусловленные состоянием атмосферы, условиями съемки и т.д. Цель -- найти "модельную" кривую NDVI. Решение: строится зависимость NDVI от номера дня и методом наименьших квадратов подгоняется полином степени $d$.

Реализация

Способ первый "решение в лоб".


In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

from tools import get_ndvi_frame  # Функция импорта данных ndvi

In [2]:
frame = get_ndvi_frame('data/Landsat/point_1_band3.csv', 'data/Landsat/point_1_band4.csv')
print frame.head()

plot(frame.Day, frame.NDVI, '.')


                            Year  Day        B3        B4      NDVI  DayNum
toar_LT51610161984127KIS00  1984  127  0.653173  0.618082 -0.027603  724402
toar_LT51590161984129KIS00  1984  129  0.577255  0.540409 -0.032967  724404
toar_LT51600161984184XXX05  1984  184  0.075905  0.285286  0.579696  724459
toar_LT51610161985065KIS00  1985   65  0.474825  0.523001  0.048281  724706
toar_LT51590161985067KIS00  1985   67  0.588754  0.646601  0.046826  724708
Out[2]:
[<matplotlib.lines.Line2D at 0x7fb70b2a50d0>]

Откровенно говоря, результат не впечатляет. Но все равно попробуем подогнать кривую. Для удобства расчетов центрируем наши данные перед подгонкой.


In [3]:
x = frame.Day - frame.Day.mean()
y = frame.NDVI

Начнем с параболы:


In [4]:
coef = np.polyfit(x, y, 2)
print coef

new_x = np.linspace(np.min(x), np.max(x), 100)
predict = np.poly1d(coef)

plot(new_x, predict(new_x), '-', x, y, '.')


[ -1.65873274e-05   9.29678898e-04   3.53835765e-01]
Out[4]:
[<matplotlib.lines.Line2D at 0x7fb70b1b3310>,
 <matplotlib.lines.Line2D at 0x7fb70b1b3590>]

Куб:


In [5]:
coef = np.polyfit(x, y, 3)

new_x = np.linspace(np.min(x), np.max(x), 100)
predict = np.poly1d(coef)

plot(new_x, predict(new_x), '-', x, y, '.')


Out[5]:
[<matplotlib.lines.Line2D at 0x7fb70b0f5ad0>,
 <matplotlib.lines.Line2D at 0x7fb70b0f5d50>]

Пятая степень


In [6]:
coef = np.polyfit(x, y, 5)
print coef

new_x = np.linspace(np.min(x), np.max(x), 100)
predict = np.poly1d(coef)

plot(new_x, predict(new_x), '-', x, y, '.')


[  4.99338163e-12   1.28807868e-09  -2.12166585e-07  -4.58778846e-05
   2.20178694e-03   4.07093016e-01]
Out[6]:
[<matplotlib.lines.Line2D at 0x7fb70b045810>,
 <matplotlib.lines.Line2D at 0x7fb70b045a90>]

Ничего хорошего, что не удивительно при таких входных данных. Простой способ не прошел, будем действовать иначе.

Способ второй. Поиск кривой, проходящей по верхней границе.

Если посмотреть на график, видно, что кривая должна иметь колоколообразную форму. Те точки, которые попали во внутреннюю часть колокола -- скорее всего шумовые и на них не нужно обращать внимания. Поэтому первое, что приходит в голову --- следует построить кривую, проходящую по верхней границе облака точек. Один из способов это сделать --- последовательно пробежать по каждому дню года и определить наибольшее значение NDVI, приходящееся на этот день. Оставив в данных только максимальное значение, мы значительно сократим влияние шумовых точек, и у нас появится возможность построить аппроксимацию верхней границы. Правда, такой подход, по всей видимости, будет несколько завышать значения NDVI в начале и конце вегетационного периода (т.е. удлинять вегетационный период) за счет выбора максимального, а не среднего отклика.


In [7]:
grouped = frame.groupby('Day').max()
print grouped.head()
plot(grouped.index, grouped.NDVI, '.')


     Year        B3        B4      NDVI  DayNum
Day                                            
9    1987  0.290771  0.384429  0.138712  725380
23   1987  0.418864  0.458184  0.044832  725394
32   1987  0.452343  0.538812  0.087240  725403
41   1987  0.361494  0.417854  0.072318  725412
64   1987  0.607564  0.662068  0.042929  725435
Out[7]:
[<matplotlib.lines.Line2D at 0x7fb70af9d6d0>]

Видно, что группировка и поиск максимума за один день не дает удовлетворительного результата. Придется поступать стандартным способом (например, как строятся ряды NDVI у MODIS): группировать данные за N дней и только потом находить максимум в каждой группе. Для удобства группировки добавим еще одну колонку -- номер группы:


In [8]:
size = 5   # Будем группировать по size дней
frame['GroupNum'] = (frame.Day / size).apply(np.int)
print frame.head()


                            Year  Day        B3        B4      NDVI  DayNum  \
toar_LT51610161984127KIS00  1984  127  0.653173  0.618082 -0.027603  724402   
toar_LT51590161984129KIS00  1984  129  0.577255  0.540409 -0.032967  724404   
toar_LT51600161984184XXX05  1984  184  0.075905  0.285286  0.579696  724459   
toar_LT51610161985065KIS00  1985   65  0.474825  0.523001  0.048281  724706   
toar_LT51590161985067KIS00  1985   67  0.588754  0.646601  0.046826  724708   

                            GroupNum  
toar_LT51610161984127KIS00        25  
toar_LT51590161984129KIS00        25  
toar_LT51600161984184XXX05        36  
toar_LT51610161985065KIS00        13  
toar_LT51590161985067KIS00        13  

In [9]:
grouped = frame.groupby('GroupNum').max()
print grouped.head()
plot(grouped.index, grouped.NDVI, '.')


          Year  Day        B3        B4      NDVI  DayNum
GroupNum                                                 
1         1987    9  0.290771  0.384429  0.138712  725380
4         1987   23  0.418864  0.458184  0.044832  725394
6         1987   32  0.452343  0.538812  0.087240  725403
8         1987   41  0.361494  0.417854  0.072318  725412
12        1987   64  0.607564  0.662068  0.042929  725435
Out[9]:
[<matplotlib.lines.Line2D at 0x7fb70ae64bd0>]

Это выглядит гораздо лучше, если не считать нехорошие выбросы в начале и конце периода. Попробуем повторить процедуру с подгонкой кривых.


In [10]:
x = grouped.Day - grouped.Day.mean()
y = grouped.NDVI

Как и в прошлый раз, начнем с параболы.


In [11]:
coef = np.polyfit(x, y, 2)

new_x = np.linspace(np.min(x), np.max(x), 100)
predict = np.poly1d(coef)

plot(new_x, predict(new_x), '-', x, y, '.')


Out[11]:
[<matplotlib.lines.Line2D at 0x7fb70ada7ad0>,
 <matplotlib.lines.Line2D at 0x7fb70ada7d50>]

Куб:


In [12]:
coef = np.polyfit(x, y, 3)

new_x = np.linspace(np.min(x), np.max(x), 100)
predict = np.poly1d(coef)

plot(new_x, predict(new_x), '-', x, y, '.')


Out[12]:
[<matplotlib.lines.Line2D at 0x7fb70acee510>,
 <matplotlib.lines.Line2D at 0x7fb70acee790>]

Пятая степень:


In [13]:
coef = np.polyfit(x, y, 5)

new_x = np.linspace(np.min(x), np.max(x), 100)
predict = np.poly1d(coef)

plot(new_x, predict(new_x), '-', x, y, '.')


Out[13]:
[<matplotlib.lines.Line2D at 0x7fb70b37db90>,
 <matplotlib.lines.Line2D at 0x7fb70b37d790>]