제가 이 블로그를 통해서 날짜 정보를 다루는 방법, 날짜 기반의 데이터를 표출하는 방법 등에 관하여 종종 소개를 드린 바 있습니다. 사실 그래서 이번에 올리는 게시물의 제목을 어떻게 해야 지난 게시물들과 차별화된 느낌을 줄 수 있을 것인지 약간 고민스러웠습니다. 하지만 고민의 결과 치고는 너무 평범한 제목을 사용하게 되었는데요. 그래도 내용상으로는 예전에 올렸던 게시물들과는 비슷한 부분도 다소 있을 수 있지만 그래도 여러모로 차별화된 부분들도 있을 것이라는 점을 미리 말씀드립니다.
이번에 다루고자 하는 내용은 날짜 기반의 데이터가 수록된 파일을 읽고 데이터를 그래픽으로 표출하는 일련의 과정입니다. 즉 날짜/시각별데이터가 수록된 파일이 나에게 주어졌을 때, 이 파일의 내용을 읽고 그 데이터를 사용하여 표출하는 과정까지를 한꺼번에 다뤄보고자 합니다. 예제 데이터도 가상의 것이 아닌 실제로 존재하는 데이터 파일을 사용하게 됩니다. 이 파일은 ACE라고 하는 태양풍 관측 위성의 관측 데이터들 중 태양풍 속도에 대한 데이터가 수록된 파일로서, 2015년 8월 14일 하루 동안의 데이터가 1분 간격으로 수록되어 있습니다. 원래 이 데이터는 SWPC(Space Weather Prediction Center)의 웹이나 FTP 서비스를 통하여 받을 수 있는데, 이번에 예제로 사용할 데이터 파일은 제가 따로 걸어놓은 링크를 통하여 받으시면 됩니다.
이 파일을 열어서 내용을 보면 앞부분의 모습은 다음과 같습니다.
이 파일에서 전반부 18줄은 헤더(Header)에 해당됩니다. 실제 데이터 값이 아니라 데이터에 관한 설명에 해당되는 부분입니다. 그리고 실제 데이터가 수록된 부분을 보면 총 10개의 컬럼들로 구성되어 있습니다. 이 중 우리에게 필요한 부분은 1, 2, 3, 4, 9 총 5개의 컬럼들입니다. 나머지 컬럼들은 필요가 없습니다. 이 중 컬럼 1~4는 데이터가 관측된 날짜/시각 정보에 해당되며 컬럼 9는 태양풍 속도 값에 해당됩니다. 그래서 우리의 궁극적인 목적은 이 데이터 파일을 읽어서 X축이 날짜/시각에 해당되고 Y축이 태양풍 속도 값에 해당되는 플롯의 형태로 데이터를 표출하는 것입니다. 그래픽 표출은 IDL의 NG 체계의 그래픽 기법을 사용하기로 합니다.
가장 먼저 해야 할 일은 데이터 파일을 읽는 것입니다. 이와 같이 컬럼 기반으로 데이터 값들이 수록된 텍스트 파일을 IDL에서 읽는 방법에 관해서는 제가 전에도 관련 게시물들을 올린 바 있습니다. 이 블로그에서 "텍스트"라는 검색어로 찾아보시면 "텍스트 파일을 효과적으로 읽는 방법에 관하여"라는 제목으로 4회에 걸쳐서 올렸던 게시물들입니다. 이 내용을 보면 크게 두가지 정도의 방법으로 나눠볼 수 있습니다. 하나는 READCOL 명령을 사용하는 비교적 '간단한 방법'이고, 또 하나는 텍스트 파일을 읽고 문자값을 처리하는데 사용 가능한 다양한 명령들(OPENR, READF, STRSPLIT 등)을 사용하는 '복잡한 방법'이었습니다. 오늘 이 게시물에서는 READCOL 명령을 사용하는 간단한 방법을 사용하겠습니다. 다음과 같이 READCOL 명령을 사용하여 데이터 파일의 내용을 읽어봅시다.
file = '20150814_ace_swepam_1m.txt'
READCOL, file, years, months, days, hhmm, speeds, FORMAT='I,I,I,A,X,X,X,X,F'
데이터 파일의 내용을 보면 컬럼 1~3은 각각 년, 월, 일에 해당되는 값들입니다. 그래서 각 컬럼별 데이터를 years, months, days라는 정수형 배열로 읽도록 하였습니다. 그리고 컬럼 4는 시간(hour)과 분(minute) 값이 서로 붙어있는 형태의 데이터입니다. 따라서 이 컬럼의 경우는 hhmm이라는 문자형 배열로 읽도록 하였습니다. 굳이 문자형으로 처리한 것은 나름의 이유가 있는데 처리과정에서 다시 언급하겠습니다. 그리고 컬럼 9는 태양풍 속도값들이므로 실수형 배열로 읽도록 하였습니다. 이와 같이 컬럼별로 배열에 수록될 값들의 자료형을 규정하기 위하여 FORMAT 키워드에 명시된 포맷 코드상에서 I, A, F와 같은 문자가 사용되었습니다. 또한 필요없는 컬럼들에 대해서는 X 문자를 사용하였습니다. 이 READCOL 명령이 성공적으로 수행된 후에는 파일의 내용을 제대로 읽었는지 확인하기 위하여 다음과 같이 HELP 명령을 사용해봅시다.
HELP, years, months, days, hhmm, speeds
HELP 명령에 의하여 출력된 결과는 다음과 같습니다.
YEARS INT = Array[1440]
MONTHS INT = Array[1440]
DAYS INT = Array[1440]
HHMM STRING = Array[1440]
SPEEDS FLOAT = Array[1440]
각 배열의 자료형은 우리가 의도했던 대로입니다. 그리고 각 배열마다 1440개의 값들을 포함하고 있음을 확인할 수 있습니다. 1440개인 이유는 하루 24시간치의 데이터가 1분 간격으로 수록되어 있기 때문입니다. 그리고 데이터 파일의 전반부에서 18줄의 헤더 부분이 있지만, 우리는 이 사실을 굳이 모르더라도 READCOL이 알아서 헤더 부분을 제외하고 실제 데이터 부분을 잘 읽어옵니다. 그러면 파일로부터 데이터를 읽어서 배열로 생성하는 작업은 일단 완료입니다.
* 파일을 읽는데 있어서 READCOL을 사용하실 분들은 바로 아래의 초록색 라인으로 둘러싼 섹션은 그냥 넘어가시면 됩니다.
--------------------------------------------------------------------------------
만약 READCOL을 사용하는 방법이 아닌 다른 방법으로 파일을 읽어야 할 경우에는 다음과 같이 '복잡한 방법'을 사용할 수도 있습니다. 이 경우에 대한 코드의 내용은 아래에 수록합니다. 다만 자세한 설명은 생략하겠습니다. 이 내용에 관하여 이해가 필요하시다면, 앞서 언급한 "텍스트 파일을 효과적으로 읽는 방법에 관하여"라는 제목으로 4회에 걸쳐서 올렸던 게시물들의 내용을 살펴보시기 바랍니다.
nl = FILE_LINES(file)
nh = 18
OPENR, lun, file, /GET_LUN
ss = ''
FOR j = 0, nh-1 DO READF, lun, ss
years = !null
months = !null
days = !null
hhmm = !null
speeds = !null
FOR j = 0, nl-nh-1 DO BEGIN
READF, lun, ss
spl = STRSPLIT(ss, ' ', /EXTRACT)
years = [years, FIX(spl[0])]
months = [months, FIX(spl[1])]
days = [days, FIX(spl[2])]
hhmm = [hhmm, spl[3]]
speeds = [speeds, FLOAT(spl[8])]
ENDFOR
FREE_LUN, lun
--------------------------------------------------------------------------------
다음으로 해야 할 일은 hhmm이라는 문자값 배열을 처리하여 시, 분 배열로 분할하는 것입니다. 이 작업은 문자 처리 함수인 STRMID를 사용하여 다음과 같이 처리하면 됩니다.
hours = FIX(STRMID(hhmm, 0, 2))
minutes = FIX(STRMID(hhmm, 2, 2))
실제로 hhmm 배열 내의 각각의 값은 4개의 문자들로 구성되어 있습니다. 예를 들어 '0632'라는 값도 그 중 하나입니다. 여기서 '06'은 시(hour)에 해당되고 '32'는 분(minute)에 해당됩니다. 그래서 '0632'에 대하여 STRMID 함수를 사용하여 문자값의 0번 위치부터 2개의 문자들을 추출하면 시(hour)에 해당되고, 2번 위치부터 2개의 문자들을 추출하면 분(minute)에 해당됩니다. 이러한 작업을 hhmm 배열을 대상으로 처리한 다음, 각 추출 데이터를 hours 및 minutes 배열로 생성하는 작업입니다. 이렇게 하면 날짜/시각에 해당되는 정보를 담은 정수형 배열들(years, months, days, hours, minutes)을 모두 얻을 수 있습니다. HELP를 사용하여 이 배열들에 관한 정보를 확인해보면 결과는 다음과 같습니다.
HELP, years, months, days, hours, minutes
YEARS INT = Array[1440]
MONTHS INT = Array[1440]
DAYS INT = Array[1440]
HOURS INT = Array[1440]
MINUTES INT = Array[1440]
그러면 이제 날짜/시각에 관한 정보를 담은 배열들 및 관측값을 담은 배열까지 모두 얻은 셈입니다. 그런데 이 데이터를 사용하여 플롯을 표출하는 것이 우리의 목적이기 때문에 PLOT 함수에 투입할 X, Y 데이터가 필요합니다. 일단 Y 데이터는 관측값 배열인 speeds를 사용하면 됩니다. 그런데 문제는 X 데이터입니다. 사실 우리가 얻은 years, months, days, hours, minutes 등의 배열은 X 데이터로 바로 사용하기에는 적절하지 않습니다. 플롯의 X축에 날짜 정보가 제대로 반영되기 위해서는, Julian Date 값들로 구성된 배열이 플롯의 X 데이터가 되어야하기 때문입니다. 즉 Julian Date 값들로 구성된 배열을 우리가 만들어줘야 합니다. 이 작업은 다음과 같이 해주면 됩니다.
tjs = JULDAY(months, days, years, hours, minutes)
이와 같이 JULDAY 함수를 사용하여 Julian Date 값들로 구성된 배열을 생성하는 방법에 관하여 궁금하시다면 관련 게시물의 내용을 참조하시기 바랍니다. 플롯에서 날짜/시각 기반의 값들이 축에 반영되도록 하려면 그 값들이 Julian Date의 형태로 환산된 값들로 구성된 배열이 필요합니다. 이제는 플롯의 X, Y 데이터에 해당되는 배열을 모두 확보한 것이므로, 그림을 표출하는 단계로 들어갈 수 있습니다. 일단 다음과 같이 NG 체계 기반으로 플롯을 표출해 봅시다.
win = WINDOW(DIMENSIONS=[600, 500], /NO_TOOLBAR)
pl = PLOT(tjs, speeds, COLOR='crimson', $
XTICKUNITS='hour', XTICKINTERVAL=6, $
XTICKFORMAT='(C(CMoA, " ", CDI, " ", CHI2.2, ":", CMI2.2))', $
TITLE='Solar Wind Speed', $
MARGIN=0.1, FONT_SIZE=10, /CURRENT)
여기서는 날짜 기반의 표출을 위하여 사용된 특수한 항목들이 있습니다. 먼저 XTICKUNITS 속성은 날짜/시각에 해당되는 축에서 단위를 설정하는 역할을 합니다. 바로 이어서 사용된 XTICKINTERVAL 속성은 축 눈금의 간격에 해당됩니다. 따라서 위와 같이 하면 X축은 6시간 간격의 눈금을 갖게 됩니다. 그리고 XTICKFORMAT 속성을 보면 포맷 코드가 사용되고 있는데, X축의 각 눈금마다 값을 표시하는 서식을 설정하는 역할을 합니다. 여기서는 날짜/시각 값을 우리가 알아보기 쉬운 형태로 표시하기 위하여 적절한 C() 포맷코드를 사용하였습니다. 여기서 사용된 C() 포맷코드에 관하여 궁금하시다면 관련 게시물의 내용을 참조하시기 바랍니다. 이 과정에 의하여 표출된 그림은 다음과 같습니다.
이 그림에서 먼저 X축을 주목해 봅시다. 각 눈금마다 날짜/시각이 표시되어 있음을 알 수 있습니다. 눈금별로 표시된 눈금값은 앞서 PLOT 함수의 XTICKFORMAT 속성에 부여된 C() 포맷코드를 따른 것입니다. 그리고 눈금 간격이 6시간이라는 것도 그림상의 X축에서 바로 알 수 있습니다. 그래서 X축에 날짜/시각 데이터가 반영되도록 하는 작업은 어느 정도는 성공적으로 진행된 것으로 보입니다.
그런데 관측값인 태양풍 속도값을 나타내는 붉은색 선을 보면 표출된 것은 맞는 것 같으나 뭔가 좀 이상합니다. 사실 원래의 데이터 파일에서 태양풍 속도값의 단위는 km/s이며 값은 평상시에는 200~500 정도의 범위를 갖습니다. 물론 태양활동의 강도에 따라서는 더 높은 값이 나올 경우도 있는데, 그 경우에는 700~800까지 올라가기도 합니다. 그렇지만 speed라는 값의 특성상 (-)값은 나오지 않는 것이 정상입니다. 그럼에도 불구하고 그림과 같이 -10000에 육박하는 값이 나타나는 이유는, 관측값이 없는 시점에 대해서는 -9999라는 값이 들어가기 때문입니다. 즉 -9999는 결측값에 해당됩니다. 원본 데이터가 수록된 텍스트 파일의 내용을 조금만 살펴보면 이러한 값들이 군데군데 존재하는 것을 확인할 수 있습니다. 따라서 -9999는 결측에 해당되는 의미없는 값이기 때문에 표출에 있어서는 제외시킬 필요가 있습니다. 그러기 위해서 먼저 다음과 같이 WHERE 함수를 사용하여 결측값이 얼마나 발생하였는지 확인해 봅시다.
ww = WHERE(speeds LT -9000, count)
PRINT, count
이렇게 하여 출력된 count의 값은 실제로 39로 나옵니다. 39회의 결측이 발생했다는 얘기입니다. 그러면 39회의 결측치에 대한 처리를 어떻게든 할 필요가 있습니다. 어떻게 해야 할까요? 한가지 방법은 결측이 아닌 정상적인 값들만을 추출하여 플롯에 반영하는 것입니다. 만약 이렇게 한다면 1440-39=1401개의 값들만 남게 됩니다. 그래서 tjs, speeds 배열 모두 원래 1440개 짜리 배열에서 1401개 짜리 배열로 변환하여 플롯에 활용하는 것입니다. 그런데 저는 이 방법보다는 다른 방법을 권해드리고 싶습니다. 원래 데이터가 1440개라는 것을 그대로 살리면서도 결측 데이터를 제외시키는 방법입니다. 바로 결측값을 NaN으로 대체하는 방법입니다. 위에서 얻은 ww를 바로 이용하여 다음과 같이 처리하면 됩니다.
IF count NE 0 THEN speeds[ww] = !values.f_nan
이렇게 하면 tjs, speeds 배열 모두 원래의 크기인 1440을 그대로 유지하게 됩니다. 다만 speeds 배열 내에서 결측값들을 NaN으로 대체한 것입니다. 이러한 처리를 거친 후 플롯이 표출되도록 하면 다음과 같은 그림을 얻게 됩니다.
앞서 얻었던 그림과 비교해 본다면 인위적인 결측치인 -9999 값들이 모두 제거되고 의미있는 값들로만 구성된 플롯이 그려진 것을 볼 수 있습니다. 특히 이 그림에서 중간 부분(8월 14일 12시 부근)을 보면 플롯의 선이 끊어져 있는 것이 보입니다. 원래 PLOT 함수로 플롯을 표출하면 데이터 포인트들을 모두 선으로 이어주는 것이 디폴트이며, 지금 이 그림의 경우도 그러한 방식으로 표출된 상태입니다. 그런데 이와 같이 중간에 끊어진 부분이 보이는 것은, 그 부분이 바로 NaN 값이 존재하는 위치이기 때문입니다. 이와 같이 표출되면 결측이 발생한 부분을 알아보기가 더 쉽습니다. 만약에 1401개의 정상값들을 추출하여 그 값들로만 배열을 새로 구성하고 표출에 반영하게 되면, 결측 부분도 그냥 선으로 이어버리게 됩니다. 물론 이렇게 표출한다고 해서 크게 문제가 되지 않을 수도 있겠지만, 결측 지점의 명확한 파악을 위해서는 제가 소개해드린 방법이 더 효율적이지 않을까 합니다.
오늘은 일단 여기까지 진행하겠습니다. 사실 우리가 오늘 의미있는 표출 결과를 얻은 것은 분명합니다. 하지만 표출 과정을 개선하여 좀 더 괜찮은 표출 결과를 얻을 수 있는 여지가 아직 더 남아 있습니다. 다음 시간에는 이러한 과정을 소개하도록 하겠습니다.
'IDL > New Graphics' 카테고리의 다른 글
날짜 기반의 데이터를 읽고 표출하기 [3] (0) | 2019.09.17 |
---|---|
날짜 기반의 데이터를 읽고 표출하기 [2] (0) | 2019.09.09 |
NG 체계에서 Order 메서드의 활용 (0) | 2019.09.02 |
NG 체계에서 CURSOR를? (0) | 2019.03.25 |
NG 체계에서 서피스(Surface)와 타 그래픽 요소의 중첩 표출 [2] (2) | 2018.12.21 |