R Markdown Nedir? Analizleri Raporlama ve Otomatikleştirme
Kodu, metni ve çıktıyı tek kaynakta birleştiren R Markdown ile tekrarlanabilir rapor üretmeyi; chunk seçenekleri, parametreler ve otomasyonla birlikte ele alıyorum.
- Araçlar

Kod bir dosyada, grafikler bir klasörde, bulgular ise sonradan elle doldurulan bir Word belgesinde. Periyodik raporlarla çalışan herkesin tanıdığı bir dağınıklık bu ve asıl maliyeti analizi ilk kez yapmak değil, aynı süreci her ay yeniden yürütmek.
R Markdown, analiz kodunu, açıklayıcı metni ve üretilen çıktıları tek bir rapor kaynağında birleştiren bir raporlama yaklaşımı. Bir .Rmd dosyası; Markdown ile yazılmış metin, R kod blokları ve belgenin nasıl üretileceğini belirleyen YAML üst bilgisinden oluşuyor. Dosya işlendiğinde kod çalışıyor, tablolar ve grafikler oluşuyor, sonuçlar açıklamalarla birlikte seçilen formata yerleşiyor.
“R Markdown yalnızca grafikleri bir belgeye koymaz; o grafiklerin nasıl üretildiğini de raporun kaynağında tutar.”
1. Raporu Otomatikleştirmenin Gerçek Faydası
Veri yenilendiğinde tabloyu yeniden kopyalamak, grafikleri dışa aktarmak ve açıklamaları elle güncellemek hem zaman kaybettirir hem de eski bir değerin raporda kalması gibi sessiz hatalara kapı açar. Tek kaynaklı bir rapor bu adımları ortadan kaldırır.
Rapor üretimi
Aynı şablon ve analiz adımları, dönemler arası karşılaştırmayı tutarlı kılar. Başlık, metrik tanımı ve grafik biçimi her raporda korunur.
Analiz ile yorum
Bir sonuç değiştiğinde onu üreten kod aynı kaynakta görünür; bulgunun hangi veri ve işlemden geldiği izlenebilir.
Paydaşa uygun
Teknik kod ile yönetici özeti aynı belgede birleşebilir; gerekirse kod çıktıda gizlenir.
Düşük maliyet
Yeni dönem için dosya kopyalamak yerine veri ve parametre güncellenip aynı şablon yeniden çalıştırılır.
2. Bir .Rmd Dosyasının Anatomisi
Bir R Markdown dosyası üç katmandan oluşur: YAML üst bilgisi belge ayarlarını, Markdown metni açıklamaları, kod blokları ise hesaplamaları tanımlar.
---
title: "Aylık Satış Performansı"
author: "Veri Analizi Ekibi"
date: "`r Sys.Date()`"
output:
html_document:
toc: true
toc_float: true
theme: readable
---
# Yönetici Özeti
Bu rapor, seçilen dönemdeki satış performansını inceler.
```{r setup, include=FALSE}
knitr::opts_chunk$set(
echo = FALSE,
warning = FALSE,
message = FALSE
)
```
## Veri ve Yöntem
Aşağıdaki analiz, kategori bazında geliri özetler.| Parça | Rolü | Pratik not |
|---|---|---|
| YAML | Başlık, yazar, tarih ve çıktı biçimi | Dosyanın en başına, iki adet üç-tire satırı arasına yazılır. |
| Markdown | Başlıklar, listeler, bağlantılar ve anlatı | Yöntemin ve bağlamın yazıldığı yer burasıdır. |
| R chunk | Kodu çalıştırıp sonucu belgeye ekler | Chunk'lara anlamlı isim vermek bakımı kolaylaştırır. |
İş bölümü şöyle: knitr kod bloklarını işler, rmarkdown ise belgeyi HTML, PDF veya Word'e dönüştüren render sürecini yönetir.
3. Chunk Seçenekleri: Kod Ne Zaman Çalışsın, Ne Görünsün?
Bir raporda her kod satırının görünmesi gerekmez. Chunk seçenekleri sunum biçimini değiştirir; kodun analiz mantığını değil.
```{r category-summary, echo=FALSE, message=FALSE, warning=FALSE}
summary_table <- sales |>
dplyr::group_by(category) |>
dplyr::summarise(revenue = sum(revenue), .groups = "drop")
knitr::kable(summary_table)
```| Seçenek | Ne yapar? | Dikkat |
|---|---|---|
echo=FALSE | Kodu gizler, çıktıyı gösterir. | Teknik olmayan okuyucuya yönelik raporlarda yararlıdır. |
include=FALSE | Kodu çalıştırır, ne kodu ne çıktıyı rapora ekler. | Veri hazırlama ve kurulum ayarları için uygundur. |
eval=FALSE | Kodu gösterir ama çalıştırmaz. | Çalıştırılması istenmeyen eğitim örnekleri için. |
message=FALSE | Paket ve işlem mesajlarını gizler. | Gerçek hataları gizlemek için kullanılmamalı. |
warning=FALSE | Uyarıları çıktıda göstermez. | Uyarıyı anlamadan kapatmak sorunları görünmez kılar. |
cache=TRUE | Chunk çıktısını önbelleğe alır. | Veri veya bağımlılık değiştiğinde önbelleğin geçerliliği kontrol edilmeli. |
warning=FALSE ve message=FALSE kodun sorunsuz çalıştığını kanıtlamaz. Geliştirme sırasında uyarıları incelemek, son rapordan önce ise temiz bir oturumda render testi yapmak daha güvenli.Her chunk'a tek tek yazmamak: global varsayılanlar
Aynı seçeneği otuz chunk'a tek tek yazmak hem yorucu hem de hataya açık. knitr::opts_chunk$set() belgenin tamamı için varsayılanları belirler; tek bir chunk farklı davranmalıysa orada ezersin.
```{r kurulum, include=FALSE}
knitr::opts_chunk$set(
echo = FALSE, # varsayilan: kod gizli
message = FALSE,
warning = FALSE,
fig.width = 8,
fig.height = 4.5,
dpi = 150,
fig.align = "center"
)
options(scipen = 999, digits = 3) # bilimsel gosterimi kapat
```Grafik boyutlarını burada tanımlamak ayrıca bir tutarlılık kazancı: raporun bütün grafikleri aynı en-boy oranında ve aynı çözünürlükte çıkıyor.
cache=TRUE'nun sessiz tuzağı
Önbellek, uzun süren bir hesaplamayı her render'da tekrar çalıştırmaktan kurtarır. Tuzak şu: knitr varsayılan olarak yalnızca chunk'ın kodu değiştiğinde önbelleği geçersiz kılar. Kod aynı kalıp veri dosyası değiştiyse, rapor eski sonucu göstermeye devam eder — üstelik hiçbir uyarı vermeden.
```{r veri-oku, cache=TRUE, cache.extra=file.mtime("data/satis.csv")}
satis <- readr::read_csv("data/satis.csv")
```
```{r ozet, cache=TRUE, dependson="veri-oku"}
ozet <- satis |> dplyr::summarise(gelir = sum(gelir), .by = kategori)
```| Seçenek | Ne işe yarar? |
|---|---|
cache.extra | Verilen ifade değiştiğinde önbelleği geçersiz kılar. Dosya tarihi (file.mtime) en yaygın kullanımı. |
dependson | Başka bir chunk yeniden çalıştığında bunu da yeniden çalıştırır. Zincirleme bağımlılıklar için şart. |
cache.path | Önbellek klasörünü belirler; sürüm kontrolünde yoksayılmalı. |
dependson zincirini elle kur. Şüphe duyduğunda önbellek klasörünü silip tam bir render almak, bir saatlik hata ayıklamadan hızlıdır. Nihai raporu paylaşmadan önce en az bir kez temiz önbellekle üretmek iyi bir alışkanlık.4. Uçtan Uca Örnek: Satış Verisinden Rapora
Aşağıdaki örnek küçük bir satış veri seti hazırlıyor, kategori bazında geliri hesaplıyor ve sonucu tabloyla grafiğe dönüştürüyor. Gerçek bir projede aynı yapıya CSV, Excel veya veri tabanı kaynağı bağlanabilir.
```{r sales-summary, message=FALSE}
library(dplyr)
library(ggplot2)
library(knitr)
sales <- data.frame(
date = as.Date(c(
"2026-01-05", "2026-01-12", "2026-02-03",
"2026-02-17", "2026-03-01", "2026-03-11"
)),
category = c(
"Elektronik", "Ev", "Elektronik",
"Giyim", "Ev", "Giyim"
),
revenue = c(4200, 1800, 2600, 950, 2200, 1500)
)
category_summary <- sales |>
group_by(category) |>
summarise(
total_revenue = sum(revenue),
transaction_count = n(),
.groups = "drop"
) |>
arrange(desc(total_revenue))
knitr::kable(
category_summary,
caption = "Kategori Bazında Gelir Özeti",
col.names = c("Kategori", "Toplam gelir", "İşlem sayısı")
)
```Özet tablosu hesaplandıktan sonra aynı nesne kullanılarak grafik hazırlanabilir:
```{r category-chart, fig.width=8, fig.height=4}
ggplot(
category_summary,
aes(x = reorder(category, total_revenue), y = total_revenue)
) +
geom_col(fill = "#8B7CFF", width = 0.68) +
coord_flip() +
scale_y_continuous(
labels = scales::label_number(big.mark = ".", decimal.mark = ",")
) +
labs(title = "Kategori Bazında Toplam Gelir", x = NULL, y = "Gelir") +
theme_minimal(base_size = 12)
```category_summary nesnesinden üretiliyor. Hesaplama mantığı iki ayrı yere kopyalanmadığı için sonuçlar arasında tutarsızlık oluşma ihtimali azalıyor.Üretim ortamında bir adım daha gerekir: veri doğrulama. Beklenen kolonlar var mı, tarih aralığı doğru mu, gelir alanında eksik veya negatif değer bulunuyor mu? Rapor yalnızca sayı üretmemeli, verinin beklenen yapıda olduğunu da doğrulamalı.
Tabloyu okunabilir hâle getirmek
Yukarıdaki örnek tabloyu ham R çıktısı olarak basıyor. Bir rapora giden tablonun biçimlendirilmesi gerekir: sayı biçimi, sütun başlıkları, hizalama, binlik ayracı. R'da bunun için birkaç paket var ve seçim büyük ölçüde çıktı formatına bağlı.
knitr::kable(
category_summary,
col.names = c("Kategori", "Gelir (TL)", "İşlem", "Ortalama sepet"),
digits = c(0, 0, 0, 2),
format.args = list(big.mark = ".", decimal.mark = ","),
align = "lrrr",
caption = "Kategori bazinda gelir ozeti"
)| Paket | En iyi olduğu yer | Dikkat |
|---|---|---|
knitr::kable | Her formatta çalışan, bağımlılıksız temel çözüm. | Biçimlendirme imkânları sınırlı. |
kableExtra | kable'ı gruplanmış başlık, koşullu renk ve sabit başlıkla genişletir. | HTML ve PDF'te farklı davranabilir; ikisini de test et. |
gt | HTML çıktıda en zengin görünüm: alt başlık, dipnot, özet satırı. | Word çıktısında desteği sınırlı. |
flextable | Word ve PowerPoint çıktıları için en güvenilir seçenek. | Sözdizimi diğerlerinden farklı. |
DT | Okuyucunun sıralayıp arayabildiği etkileşimli tablolar. | Yalnızca HTML; PDF'te boş çıkar. |
gt ile uğraşmanın anlamı yok; HTML'de kalacaksa flextable gereksiz bir kısıt. En sık yapılan hata, tabloyu HTML'de mükemmelleştirip son anda PDF istendiğinde her şeyi yeniden yazmak.Veri doğrulamayı rapora gömmek
Bir önceki paragrafta geçen veri doğrulama, pratikte birkaç satırlık bir chunk'tan ibaret olabilir. Önemli olan, kontrol başarısız olduğunda raporun sessizce devam etmemesi.
```{r dogrulama, include=FALSE}
beklenen <- c("tarih", "kategori", "gelir", "adet")
stopifnot(
"Beklenen kolonlar eksik" = all(beklenen %in% names(satis)),
"Veri bos geldi" = nrow(satis) > 0,
"Negatif gelir var" = all(satis$gelir >= 0, na.rm = TRUE),
"Tarih araligi disinda" = max(satis$tarih) <= Sys.Date()
)
eksik_oran <- mean(is.na(satis$gelir))
if (eksik_oran > 0.05)
warning("Gelir alaninda %", round(eksik_oran * 100), " eksik")
```stopifnot() koşul sağlanmazsa render'ı hata vererek durdurur — yani bozuk veriyle üretilmiş bir rapor hiç ortaya çıkmaz. Adlandırılmış koşullar kullanmak, hata mesajını "all(...) is not TRUE" olmaktan çıkarıp "Beklenen kolonlar eksik" gibi okunabilir bir şeye çevirir.
include=FALSE ile gizlemek yerine, raporun sonuna küçük bir "veri kalitesi" bölümü koymak çoğu zaman daha iyi: kaç satır okundu, hangi tarih aralığı, eksik oranı ne. Okuyucu sayılara güvenip güvenmeyeceğine kendisi karar verebilsin.5. Parametreli Raporlama
Parametreler, aynı şablonun farklı dönemler, bölgeler veya segmentler için kullanılmasını sağlar. Her bölge için ayrı dosya kopyalamak yerine raporun parametresi değişir ve aynı mantıkla yeni çıktı alınır.
---
title: "Bölgesel Satış Raporu"
output: html_document
params:
selected_region: "Marmara"
report_year: 2026
---filtered_sales <- sales |>
dplyr::filter(
region == params$selected_region,
format(date, "%Y") == as.character(params$report_year)
)
summary(filtered_sales)Parametre kullanırken değerin veri tipi önemli: yıl parametresi sayısal mı, karakter mi? Yukarıdaki örnekte as.character() tam olarak bu belirsizliği kapatmak için var. Filtre sonrası hiç kayıt kalmaması durumu da ele alınmalı.
6. Raporları Otomatik Üretmek
Rapor üretimini otomatikleştirmek için rmarkdown::render() kullanılır. Bu fonksiyon bir R betiğinden, zamanlanmış görevden veya başka bir iş akışından çağrılabilir.
- Veri alCSV / Excel / DB
- Kontrol etŞema ve kalite
- Analiz etHesaplar ve grafikler
- Render etHTML / PDF / Word
- PaylaşKontrollü dağıtım
rmarkdown::render(
input = "monthly-report.Rmd",
output_format = "html_document",
output_file = "monthly-report.html",
params = list(
selected_region = "Marmara",
report_year = 2026
),
envir = new.env(parent = globalenv())
)Buradaki envir = new.env(...) küçük ama kritik: raporu temiz bir ortamda çalıştırarak, o anda bellekte duran nesnelere farkında olmadan bağımlı kalmayı önler.
Raporu zamanında ve kendiliğinden çalıştırmak
render() çağrısını bir betiğe koymak işin yarısı. Diğer yarısı, o betiğin kimsenin hatırlamasına gerek kalmadan çalışması. Üç yaygın yol var ve seçim büyük ölçüde raporun nerede durduğuna bağlı.
| Yöntem | Nerede uygun? | Dikkat edilecek |
|---|---|---|
| Windows Görev Zamanlayıcı | Rapor şirket içi bir makinede üretiliyorsa. | Makine kapalıysa çalışmaz; çalışma dizini mutlaka belirtilmeli. |
| cron (Linux/macOS) | Sunucuda duran raporlama hattı. | cron'un PATH'i kısıtlıdır; Rscript'in tam yolu yazılmalı. |
| GitHub Actions | Kod zaten depodaysa ve veri uzaktan çekilebiliyorsa. | Paket kurulumu her çalıştırmada zaman alır; önbellek kullan. |
"C:\Program Files\R\R-4.4.1\bin\Rscript.exe" ^
"D:\raporlar\render.R" ^
>> "D:\raporlar\log\render.log" 2>&1name: aylik-rapor
on:
schedule:
- cron: "0 6 1 * *" # her ayin 1'i, 06:00 UTC
workflow_dispatch: # elle de tetiklenebilsin
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-r@v2
- uses: r-lib/actions/setup-renv@v2
- run: Rscript render.R
- uses: actions/upload-artifact@v4
with:
name: rapor
path: output/*.html7. Raporun Yeniden Üretilebilir Olması
Veriyi tanımla
Dosya yolu, dönem ve veri kaynağı raporda izlenebilir olsun.
Sabitle
Simülasyon veya örnekleme varsa uygun yerde set.seed() kullan.
Bağımlılıkları izle
Paketlerin başka bir bilgisayarda da kurulabilmesini düşün.
Temiz render
Bellekte duran nesnelere farkında olmadan bağımlı kalma.
Doğrulama ekle
Beklenen satır sayısı, kolon isimleri ve eksik değerler kontrol edilsin.
Çıktıyı incele
Tarih, sayılar, grafikler ve hassas bilgiler paylaşımdan önce gözden geçirilsin.
Tekrar üretilebilirlik yalnızca aynı kodu saklamak değil. Verinin hangi sürümünün kullanıldığı, kodun hangi koşullarda çalıştığı ve sonucu yeniden oluşturmak için hangi bağımlılıkların gerektiği de bu tanımın parçası.
Bağımlılıkları kilitlemek: renv
"Paketlerin başka bir bilgisayarda da kurulabilmesi" iyi bir niyet beyanı, ama tek başına yeterli değil. Altı ay sonra bir paketin yeni sürümü fonksiyonun varsayılan davranışını değiştirdiğinde rapor sessizce farklı sayılar üretebilir. renv paketi projeye özgü bir kütüphane kurar ve kullanılan sürümleri bir kilit dosyasına yazar.
renv::init() # projeye ozel kutuphane olustur
renv::snapshot() # kullanilan paket surumlerini renv.lock'a yaz
# Baska bir makinede, depoyu klonladiktan sonra:
renv::restore() # tam olarak ayni surumleri kurrenv.lock dosyası sürüm kontrolüne eklenir; renv/library/ klasörü eklenmez. Böylece depoyu klonlayan herkes aynı paket sürümleriyle çalışır.
| Katman | Ne sabitlenir? | Araç |
|---|---|---|
| Paket sürümleri | Hangi paketin hangi sürümü kullanıldı. | renv.lock |
| R sürümü | Hangi R yorumlayıcısı çalıştı. | renv.lock içinde kayıtlı; kurulumu ayrı. |
| Rastgelelik | Örnekleme ve simülasyon sonuçları. | set.seed() |
| Veri sürümü | Hangi tarihli veri okundu. | Dosya adında tarih veya raporda file.mtime() yazdırmak. |
| Çalışma ortamı | İşletim sistemi ve sistem kütüphaneleri. | Docker veya sessionInfo() çıktısını rapora eklemek. |
```{r oturum, echo=FALSE}
cat("Rapor uretim zamani:", format(Sys.time(), "%d.%m.%Y %H:%M"), "\n")
cat("Veri dosyasi tarihi:", format(file.mtime("data/satis.csv")), "\n")
sessionInfo()
```8. R Markdown mı, Quarto mu?
R Markdown uzun süredir kullanılan ve mevcut projelerde sıkça karşılaşılan bir yaklaşım. Quarto ise çok dilli doküman, rapor, sunum ve web yayınları için daha yeni bir yayınlama sistemi. İkisi de tekrarlanabilir raporlama akışı kurar; seçim mevcut proje yapısına göre yapılmalı.
| Kriter | R Markdown | Quarto |
|---|---|---|
| Mevcut R projeleri | Yerleşik bir akış varsa devam etmek pratiktir. | Geçiş yapılacaksa doküman ve çıktı uyumluluğu test edilmeli. |
| Yeni projeler | Özel gereksinimlerde uygun bir seçenek olabilir. | Çok formatlı ve çok dilli yayın akışları için değerlendirmeye değer. |
| Temel yaklaşım | Markdown + kod chunk'ları + render. | Markdown temelli yayın sistemi; birden fazla dil ve çıktı türü. |
9. Kaliteli Bir Rapor İçin Kontrol Listesi
İş sorusu belli
Rapor okuyucunun hangi kararını destekliyor?
Yöntem anlatılmış
Metrik ve filtre tanımları açık mı?
Kod düzenli
Chunk isimleri anlaşılır ve tekrar az mı?
Veri kontrolü var
Eksik veya olağan dışı veriler tespit ediliyor mu?
Çıktı okunabilir
Başlıklar, tablolar, renkler ve sayı biçimleri tutarlı mı?
Otomasyon güvenli
Başarısız üretimde dağıtım engelleniyor mu?
10. Sık Sorulan Sorular
| Soru | Yanıt |
|---|---|
| İleri düzey R bilgisi gerekir mi? | Belge oluşturmak ve basit chunk'lar çalıştırmak için temel R bilgisi yeterli olabilir. Analiz karmaşıklaştıkça veri işleme, hata yönetimi ve kod organizasyonu önem kazanır. |
| PDF üretimi neden hata verir? | PDF çıktısı çoğu akışta LaTeX tabanlı bir araç zinciri gerektirir. HTML çalışırken PDF'in hata vermesi bu yüzden olağandır; ortam ve bağımlılıklar kontrol edilmeli. |
| İnteraktif dashboard oluşturur mu? | R Markdown ağırlıklı olarak rapor üretir. Kullanıcının filtreleri değiştirip sonuçları anlık keşfettiği uygulamalar için R Shiny daha uygun; bazı HTML raporlara ise etkileşimli grafikler eklenebilir. |
Sonuç
R Markdown; kodu, metni ve çıktıyı aynı yerde tutarak veri analizini daha izlenebilir ve sürdürülebilir hale getiriyor. En büyük faydası rapor hazırlamayı hızlandırması değil, rapordaki sonucun hangi veri ve yöntemle üretildiğini görünür kılması.
İyi kurulmuş bir akış; veri doğrulama, analiz, görselleştirme, yorumlama ve render adımlarını birleştirir. Böylece rapor, dönem sonunda hazırlanan bir belge olmaktan çıkıp karar alma sürecinin tekrar üretilebilir bir parçasına dönüşür.
“Analizi bir kez tasarla, raporu güvenilir biçimde yeniden üret.”
The code lives in one file, the charts in a folder, and the findings in a Word document filled in by hand afterwards. Anyone who works with periodic reports knows this mess, and its real cost is not doing the analysis once — it is running the same process again every month.
R Markdown is a reporting approach that brings analysis code, explanatory text and generated output together in a single report source. An .Rmd file consists of text written in Markdown, R code chunks, and a YAML header that defines how the document is produced. When the file is processed, the code runs, tables and charts are generated, and the results land in the chosen output format alongside the narrative.
“R Markdown does not just put charts into a document; it keeps how those charts were produced in the report's source.”
1. The Real Benefit of Automating a Report
When the data refreshes, copying the table again, exporting the charts and updating the commentary by hand costs time and opens the door to quiet errors — such as an old figure left in the report. A single-source report removes those steps.
Report production
The same template and analysis steps make period-to-period comparison consistent. Titles, metric definitions and chart style stay the same.
Analysis and commentary
When a result changes, the code that produced it is visible in the same source; you can trace which data and which operation it came from.
Fit for stakeholders
Technical code and an executive summary can live in one document; the code can be hidden in the output when needed.
Lower cost
Instead of copying a file for the new period, update the data and parameters and re-run the same template.
2. The Anatomy of an .Rmd File
An R Markdown file has three layers: the YAML header defines document settings, the Markdown text carries the narrative, and the code chunks hold the calculations.
---
title: "Monthly Sales Performance"
author: "Data Analytics Team"
date: "`r Sys.Date()`"
output:
html_document:
toc: true
toc_float: true
theme: readable
---
# Executive Summary
This report reviews sales performance for the selected period.
```{r setup, include=FALSE}
knitr::opts_chunk$set(
echo = FALSE,
warning = FALSE,
message = FALSE
)
```
## Data and Method
The analysis below summarizes revenue by category.| Part | Role | Practical note |
|---|---|---|
| YAML | Title, author, date and output format | Written at the very top, between two lines of three dashes. |
| Markdown | Headings, lists, links and narrative | This is where the method and the context are written. |
| R chunk | Runs code and inserts the result | Giving chunks meaningful names makes maintenance easier. |
The division of labour: knitr processes the code chunks, while rmarkdown manages the render process that converts the document into HTML, PDF or Word.
3. Chunk Options: When Should Code Run, and What Should Show?
Not every line of code needs to appear in a report. Chunk options change how things are presented — not the analysis logic of the code.
```{r category-summary, echo=FALSE, message=FALSE, warning=FALSE}
summary_table <- sales |>
dplyr::group_by(category) |>
dplyr::summarise(revenue = sum(revenue), .groups = "drop")
knitr::kable(summary_table)
```| Option | What it does | Watch out for |
|---|---|---|
echo=FALSE | Hides the code, shows the output. | Useful for reports aimed at non-technical readers. |
include=FALSE | Runs the code but adds neither code nor output. | Good for data preparation and setup. |
eval=FALSE | Shows the code but does not run it. | Suitable for teaching examples that should not execute. |
message=FALSE | Hides package and process messages. | Must not be used to hide real errors. |
warning=FALSE | Keeps warnings out of the output. | Silencing a warning you do not understand hides problems. |
cache=TRUE | Caches the chunk output. | Check cache validity when data or dependencies change. |
warning=FALSE and message=FALSE do not prove the code ran cleanly. Review warnings during development, and run a render test in a clean session before the final report.Not writing it in every chunk: global defaults
Repeating the same option across thirty chunks is tedious and error-prone. knitr::opts_chunk$set() sets defaults for the whole document; a chunk that needs to behave differently overrides them locally.
```{r setup, include=FALSE}
knitr::opts_chunk$set(
echo = FALSE, # default: hide the code
message = FALSE,
warning = FALSE,
fig.width = 8,
fig.height = 4.5,
dpi = 150,
fig.align = "center"
)
options(scipen = 999, digits = 3) # turn off scientific notation
```Declaring figure dimensions here buys consistency as well: every chart in the report comes out at the same aspect ratio and the same resolution.
The silent trap in cache=TRUE
Caching saves you from rerunning an expensive computation on every render. The trap: by default knitr invalidates the cache only when the chunk's code changes. If the code stays the same but the data file changes, the report keeps showing the old result — with no warning at all.
```{r read-data, cache=TRUE, cache.extra=file.mtime("data/sales.csv")}
sales <- readr::read_csv("data/sales.csv")
```
```{r summary, cache=TRUE, dependson="read-data"}
summary_tbl <- sales |> dplyr::summarise(revenue = sum(revenue), .by = category)
```| Option | What it does |
|---|---|
cache.extra | Invalidates the cache when the given expression changes. A file timestamp (file.mtime) is the most common use. |
dependson | Reruns this chunk whenever another one reruns. Essential for chained dependencies. |
cache.path | Sets the cache folder; it should be ignored by version control. |
dependson chain by hand. When in doubt, deleting the cache folder and taking a full render is faster than an hour of debugging. Producing the final report at least once with a clean cache is a good habit.4. End to End: From Sales Data to a Report
The example below prepares a small sales data set, computes revenue by category, and turns the result into a table and a chart. In a real project the same structure can be connected to a CSV, Excel or database source.
```{r sales-summary, message=FALSE}
library(dplyr)
library(ggplot2)
library(knitr)
sales <- data.frame(
date = as.Date(c(
"2026-01-05", "2026-01-12", "2026-02-03",
"2026-02-17", "2026-03-01", "2026-03-11"
)),
category = c(
"Electronics", "Home", "Electronics",
"Apparel", "Home", "Apparel"
),
revenue = c(4200, 1800, 2600, 950, 2200, 1500)
)
category_summary <- sales |>
group_by(category) |>
summarise(
total_revenue = sum(revenue),
transaction_count = n(),
.groups = "drop"
) |>
arrange(desc(total_revenue))
knitr::kable(
category_summary,
caption = "Revenue Summary by Category",
col.names = c("Category", "Total revenue", "Transactions")
)
```Once the summary table exists, a chart can be built from the same object:
```{r category-chart, fig.width=8, fig.height=4}
ggplot(
category_summary,
aes(x = reorder(category, total_revenue), y = total_revenue)
) +
geom_col(fill = "#8B7CFF", width = 0.68) +
coord_flip() +
scale_y_continuous(labels = scales::label_number(big.mark = ",")) +
labs(title = "Total Revenue by Category", x = NULL, y = "Revenue") +
theme_minimal(base_size = 12)
```category_summary object. Because the calculation is not copied into two places, the chance of inconsistency between the two drops.In production one more step is needed: data validation. Are the expected columns present, is the date range right, are there missing or negative values in the revenue field? A report should not only produce numbers — it should confirm the data has the expected shape.
Making the table readable
The example above prints the table as raw R output. A table headed for a report needs formatting: number format, column headers, alignment, thousands separators. R offers several packages for this, and the choice depends largely on the output format.
knitr::kable(
category_summary,
col.names = c("Category", "Revenue", "Transactions", "Avg. basket"),
digits = c(0, 0, 0, 2),
format.args = list(big.mark = ","),
align = "lrrr",
caption = "Revenue summary by category"
)| Package | Where it is strongest | Watch out for |
|---|---|---|
knitr::kable | The dependency-free baseline that works in every format. | Limited formatting options. |
kableExtra | Extends kable with grouped headers, conditional colour and sticky headers. | Can behave differently in HTML and PDF; test both. |
gt | The richest look in HTML: subtitles, footnotes, summary rows. | Limited support in Word output. |
flextable | The most reliable choice for Word and PowerPoint output. | Its syntax differs from the others. |
DT | Interactive tables readers can sort and search. | HTML only; it renders empty in PDF. |
gt; if it stays in HTML, flextable is a needless constraint. The most common mistake is perfecting a table in HTML and rewriting everything when PDF is requested at the last minute.Building data validation into the report
The data validation mentioned above can in practice be a chunk of a few lines. What matters is that when a check fails, the report does not quietly carry on.
```{r validation, include=FALSE}
expected <- c("date", "category", "revenue", "units")
stopifnot(
"Expected columns are missing" = all(expected %in% names(sales)),
"The data arrived empty" = nrow(sales) > 0,
"Negative revenue present" = all(sales$revenue >= 0, na.rm = TRUE),
"Date outside valid range" = max(sales$date) <= Sys.Date()
)
missing_rate <- mean(is.na(sales$revenue))
if (missing_rate > 0.05)
warning(round(missing_rate * 100), "% of revenue values are missing")
```stopifnot() aborts the render with an error when a condition fails, so a report built on broken data never appears at all. Using named conditions turns the error message from "all(...) is not TRUE" into something readable like "Expected columns are missing".
include=FALSE, a short "data quality" section at the end of the report is often better: how many rows were read, which date range, what share was missing. Let the reader decide how much to trust the numbers.5. Parameterized Reporting
Parameters let the same template serve different periods, regions or segments. Instead of copying a file per region, you change the report's parameter and get new output from the same logic.
---
title: "Regional Sales Report"
output: html_document
params:
selected_region: "Marmara"
report_year: 2026
---filtered_sales <- sales |>
dplyr::filter(
region == params$selected_region,
format(date, "%Y") == as.character(params$report_year)
)
summary(filtered_sales)The data type of a parameter matters: is the year numeric or character? In the example above, as.character() exists precisely to close that ambiguity. The case where no records remain after filtering must be handled too.
6. Producing Reports Automatically
To automate report production, use rmarkdown::render(). The function can be called from an R script, a scheduled job or another workflow.
- Fetch dataCSV / Excel / DB
- Check itSchema and quality
- AnalyzeCalculations and charts
- RenderHTML / PDF / Word
- ShareControlled distribution
rmarkdown::render(
input = "monthly-report.Rmd",
output_format = "html_document",
output_file = "monthly-report.html",
params = list(
selected_region = "Marmara",
report_year = 2026
),
envir = new.env(parent = globalenv())
)That envir = new.env(...) is small but critical: it runs the report in a clean environment, preventing an accidental dependency on objects that happen to be in memory.
Running the report on schedule, without anyone remembering
Putting the render() call in a script is half the job. The other half is making that script run without anyone having to remember it. There are three common routes, and the choice depends largely on where the report lives.
| Method | Where it fits | Watch out for |
|---|---|---|
| Windows Task Scheduler | When the report is produced on an in-house machine. | It will not run if the machine is off; always set the working directory. |
| cron (Linux/macOS) | A reporting pipeline living on a server. | cron has a minimal PATH; write the full path to Rscript. |
| GitHub Actions | When the code is already in a repository and the data can be fetched remotely. | Installing packages costs time on every run; use caching. |
"C:\Program Files\R\R-4.4.1\bin\Rscript.exe" ^
"D:\reports\render.R" ^
>> "D:\reports\log\render.log" 2>&1name: monthly-report
on:
schedule:
- cron: "0 6 1 * *" # the 1st of each month, 06:00 UTC
workflow_dispatch: # allow manual runs too
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-r@v2
- uses: r-lib/actions/setup-renv@v2
- run: Rscript render.R
- uses: actions/upload-artifact@v4
with:
name: report
path: output/*.html7. Making the Report Reproducible
Identify the data
File path, period and data source should be traceable in the report.
Fix it
If there is simulation or sampling, use set.seed() in the right place.
Track dependencies
Think about whether the packages can be installed on another machine.
Render clean
Do not depend unknowingly on objects left in memory.
Add validation
Check expected row counts, column names and missing values.
Inspect the output
Dates, numbers, charts and sensitive information should be reviewed before sharing.
Reproducibility is not only about keeping the same code. Which version of the data was used, under what conditions the code ran, and which dependencies are needed to rebuild the result are all part of the definition.
Locking dependencies: renv
"Thinking about whether the packages can be installed elsewhere" is a good intention, but not sufficient on its own. Six months later, when a new release of a package changes a function's default behaviour, the report can silently produce different numbers. The renv package creates a project-specific library and records the versions in use in a lockfile.
renv::init() # create a project-specific library
renv::snapshot() # write the package versions in use to renv.lock
# On another machine, after cloning the repository:
renv::restore() # install exactly the same versionsThe renv.lock file goes into version control; the renv/library/ folder does not. Anyone who clones the repository then works with the same package versions.
| Layer | What gets pinned | Tool |
|---|---|---|
| Package versions | Which version of which package was used. | renv.lock |
| R version | Which R interpreter ran. | Recorded in renv.lock; installing it is separate. |
| Randomness | Sampling and simulation results. | set.seed() |
| Data version | Which vintage of the data was read. | A date in the filename, or printing file.mtime() in the report. |
| Environment | Operating system and system libraries. | Docker, or adding sessionInfo() output to the report. |
```{r session, echo=FALSE}
cat("Report generated:", format(Sys.time(), "%Y-%m-%d %H:%M"), "\n")
cat("Data file timestamp:", format(file.mtime("data/sales.csv")), "\n")
sessionInfo()
```8. R Markdown or Quarto?
R Markdown is a long-established approach you will often meet in existing projects. Quarto is a newer publishing system for multi-language documents, reports, presentations and websites. Both can support a reproducible reporting workflow; the choice depends on the project you already have.
| Criterion | R Markdown | Quarto |
|---|---|---|
| Existing R projects | If a working flow is in place, staying with it is practical. | If you migrate, test document and output compatibility. |
| New projects | Can be the right option for specific requirements. | Worth evaluating for multi-format, multi-language publishing. |
| Core approach | Markdown + code chunks + render. | A Markdown-based publishing system across several languages and output types. |
9. A Checklist for a Quality Report
Clear business question
Which decision does the report support?
Method explained
Are metric and filter definitions stated?
Tidy code
Are chunk names clear and repetition low?
Data checks exist
Are missing or unusual values detected?
Readable output
Are headings, tables, colors and number formats consistent?
Safe automation
Is distribution blocked when a run fails?
10. Frequently Asked Questions
| Question | Answer |
|---|---|
| Do I need advanced R? | Basic R can be enough to create a document and run simple chunks. As the analysis grows, data wrangling, error handling and code organization matter more. |
| Why does PDF output fail? | PDF output usually requires a LaTeX-based toolchain. HTML working while PDF fails is therefore common; check the environment and its dependencies. |
| Can it build an interactive dashboard? | R Markdown mainly produces reports. For applications where users change filters and explore results live, R Shiny is the better tool; some HTML reports can still embed interactive charts. |
Conclusion
By keeping code, text and output in one place, R Markdown makes data analysis more traceable and more maintainable. Its biggest benefit is not that it speeds up report writing, but that it makes visible which data and which method produced the result.
A well-built flow joins validation, analysis, visualization, interpretation and rendering. The report stops being a document assembled at the end of the period and becomes a reproducible part of how decisions get made.
“Design the analysis once; reproduce the report reliably.”